完善前端项目结构和依赖配置

- 更新 package.json 中的依赖版本号,包括 element-plus、axios、pinia、vue-router 等
- 删除 HelloWorld 组件,替换为 router-view 以支持路由功能
- 添加 auth、category、customer、dashboard、medicine、role、stock、stockIn、stockOut、
  supplier、user 等多个 API 接口文件
- 创建权限指令、主布局组件和全局样式
- 集成 Element Plus UI 框架并注册图标组件
- 实现应用状态管理和用户状态管理 Store
- 配置 Vue Router 路由系统,支持动态菜单路由生成
- 移除旧的 CSS 样式文件,改用 SCSS 全局样式
```
This commit is contained in:
2026-07-06 12:07:23 +08:00
parent 3fcf927aef
commit 15da4fe492
38 changed files with 2057 additions and 409 deletions

View File

@@ -0,0 +1,86 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAllCustomers } from '@/api/customer'
import { getStockList } from '@/api/stock'
import { createStockOut } from '@/api/stockOut'
import { ElMessage } from 'element-plus'
const router = useRouter()
const customerOptions = ref([])
const stockOptions = ref([])
const form = reactive({ customerId: null, warehouseId: 1, outDate: new Date().toISOString().slice(0, 10), outType: 1, remark: '' })
const details = ref([])
function addDetail() {
details.value.push({ medicineId: null, batchNo: '', quantity: 1, unitPrice: 0 })
}
function removeDetail(i) { details.value.splice(i, 1) }
function detailTotal(d) { return (d.quantity || 0) * (d.unitPrice || 0) }
const totalAmount = ref(0)
function updateTotal() { totalAmount.value = details.value.reduce((s, d) => s + detailTotal(d), 0) }
async function loadOptions() {
try {
const [cRes, sRes] = await Promise.all([getAllCustomers(), getStockList({ page: 1, size: 999 })])
customerOptions.value = cRes.data || []
stockOptions.value = sRes.data?.records || []
} catch { /* */ }
}
function onMedicineSelected(index, stockId) {
const stock = stockOptions.value.find(s => s.id === stockId)
if (stock) {
details.value[index].medicineId = stock.medicineId
details.value[index].batchNo = stock.batchNo
details.value[index].unitPrice = stock.costPrice || 0
details.value[index]._maxQty = stock.quantity
}
}
async function handleSubmit() {
if (!form.customerId) { ElMessage.warning('请选择客户'); return }
if (!details.value.length) { ElMessage.warning('请添加药品明细'); return }
updateTotal()
await createStockOut({ ...form, details: details.value, totalAmount: totalAmount.value })
ElMessage.success('出库单已创建,等待审核')
router.push('/stock-out/list')
}
onMounted(loadOptions)
</script>
<template>
<div class="page-container">
<h3 style="margin-bottom:16px">新建出库单</h3>
<el-form :model="form" label-width="90px" style="max-width:700px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="客户" required><el-select v-model="form.customerId" placeholder="选择客户" filterable style="width:100%"><el-option v-for="c in customerOptions" :key="c.id" :label="c.name" :value="c.id" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="出库类型"><el-select v-model="form.outType" style="width:100%"><el-option label="销售出库" :value="1" /><el-option label="领用出库" :value="2" /><el-option label="报损出库" :value="3" /><el-option label="退货出库" :value="4" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="出库日期" required><el-date-picker v-model="form.outDate" type="date" style="width:100%" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item></el-col>
</el-row>
</el-form>
<div style="margin:20px 0"><el-button type="primary" @click="addDetail">+ 添加药品</el-button><span style="margin-left:12px;color:#909399">总计: ¥{{ totalAmount }}</span></div>
<el-table :data="details" border>
<el-table-column label="库存批次" min-width="220">
<template #default="{row, $index}">
<el-select :model-value="row._stockId" placeholder="选择药品批次" filterable style="width:100%" @change="onMedicineSelected($index, $event)">
<el-option v-for="s in stockOptions" :key="s.id" :label="`${s.medicineName} - ${s.batchNo} (库存${s.quantity})`" :value="s.id" />
</el-select>
</template>
</el-table-column>
<el-table-column label="批号" width="120"><template #default="{row}">{{row.batchNo}}</template></el-table-column>
<el-table-column label="数量" width="100"><template #default="{row}"><el-input-number v-model="row.quantity" :min="1" :max="row._maxQty||9999" size="small" style="width:100%" @change="updateTotal" /></template></el-table-column>
<el-table-column label="单价" width="110"><template #default="{row}"><el-input-number v-model="row.unitPrice" :min="0" :precision="2" size="small" style="width:100%" @change="updateTotal" /></template></el-table-column>
<el-table-column label="小计" width="110"><template #default="{row}">¥{{detailTotal(row)}}</template></el-table-column>
<el-table-column label="操作" width="80"><template #default="{ $index }"><el-button size="small" type="danger" @click="removeDetail($index)">移除</el-button></template></el-table-column>
</el-table>
<div style="margin-top:20px">
<el-button @click="router.back()">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交出库单</el-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,55 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getStockOutList, auditStockOut } from '@/api/stockOut'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const router = useRouter()
const query = reactive({ page: 1, size: 10 })
const tableData = ref([])
const total = ref(0)
function statusTag(s) { return { 0: 'warning', 1: 'success', 2: 'info' }[s] || 'info' }
function statusText(s) { return { 0: '待审核', 1: '已出库', 2: '已取消' }[s] || '未知' }
function typeText(s) { return { 1: '销售出库', 2: '领用出库', 3: '报损出库', 4: '退货出库' }[s] || '未知' }
async function loadData() { try { const r = await getStockOutList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
function handleCreate() { router.push('/stock-out/add') }
function handleView(row) { ElMessage.info('出库单号: ' + (row.recordNo || row.record_no)) }
async function handleAudit(row) { await auditStockOut(row.id, { status: 1 }); ElMessage.success('审核通过,库存已扣减'); loadData() }
onMounted(loadData)
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="query.recordNo" placeholder="出库单号" clearable style="width:180px" />
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
<el-button :icon="Refresh" @click="Object.keys(query).forEach(k=>query[k]=k==='page'?1:k==='size'?10:'');loadData()">重置</el-button>
</div>
<div class="tool-bar">
<el-button type="primary" v-permission="'stock-out:add'" :icon="Plus" @click="handleCreate">+ 新建出库单</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="recordNo" label="出库单号" width="150" />
<el-table-column prop="customerName" label="客户" min-width="160" />
<el-table-column prop="outType" label="类型" width="100"><template #default="{row}">{{typeText(row.outType)}}</template></el-table-column>
<el-table-column prop="totalAmount" label="总金额" width="110"><template #default="{row}">¥{{row.totalAmount}}</template></el-table-column>
<el-table-column prop="outDate" label="出库日期" width="120" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{row}"><el-tag :type="statusTag(row.status)" size="small">{{statusText(row.status)}}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{row}">
<div class="table-actions">
<el-button size="small" @click="handleView(row)">查看</el-button>
<el-button v-if="row.status===0" size="small" type="success" v-permission="'stock-out:audit'" @click="handleAudit(row)">审核</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
</div>
</template>