|
|
@@ -0,0 +1,284 @@
|
|
|
+<template>
|
|
|
+ <div class="inventory-manage admin-page">
|
|
|
+ <el-card>
|
|
|
+ <div slot="header" class="admin-page-header">
|
|
|
+ <span class="admin-page-title">库存管理</span>
|
|
|
+ <el-button type="success" size="mini" icon="el-icon-plus" @click="$router.push('/inventory/inbound-create')">创建入库单</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-tabs v-model="activeTab">
|
|
|
+ <!-- 库存流水 -->
|
|
|
+ <el-tab-pane label="库存流水" name="transactions">
|
|
|
+ <div class="filter-bar" style="margin-bottom:12px;">
|
|
|
+ <el-input v-model="txFilters.productName" placeholder="商品名称" clearable style="width:160px;margin-right:8px;" />
|
|
|
+ <el-select v-model="txFilters.type" placeholder="类型" clearable style="width:120px;margin-right:8px;">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
+ <el-option label="入库" value="inbound" />
|
|
|
+ <el-option label="出库" value="outbound" />
|
|
|
+ <el-option label="调账" value="adjustment" />
|
|
|
+ <el-option label="盘点" value="counting" />
|
|
|
+ <el-option label="退款入库" value="refund" />
|
|
|
+ </el-select>
|
|
|
+ <el-date-picker v-model="txFilters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" style="width:260px;margin-right:8px;" />
|
|
|
+ <el-button type="primary" size="mini" @click="loadTransactions">查询</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="txList" v-loading="txLoading" border stripe max-height="500">
|
|
|
+ <el-table-column prop="id" label="ID" width="60" />
|
|
|
+ <el-table-column label="商品" min-width="150">
|
|
|
+ <template slot-scope="{ row }">{{ row.productName || 'ID:' + row.productId }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="类型" width="80">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="row.type === 'inbound' || row.type === 'refund' ? 'success' : row.type === 'outbound' ? 'danger' : 'warning'" size="mini">
|
|
|
+ {{ {'inbound':'入库','outbound':'出库','adjustment':'调账','counting':'盘点','refund':'退款入库'}[row.type] || row.type }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="数量" width="80">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span :style="{ color: row.direction === 1 ? '#67c23a' : '#f56c6c' }">{{ row.direction === 1 ? '+' : '-' }}{{ row.quantity }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="beforeStock" label="变动前" width="70" />
|
|
|
+ <el-table-column prop="afterStock" label="变动后" width="70" />
|
|
|
+ <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="createdAt" label="时间" width="160" />
|
|
|
+ </el-table>
|
|
|
+ <el-pagination @size-change="txPageSize=$event;loadTransactions()" @current-change="txPage=$event;loadTransactions()" :current-page="txPage" :page-sizes="[10,20,50]" :page-size="txPageSize" :total="txTotal" layout="total, sizes, prev, pager, next, jumper" class="pagination-wrap" />
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 入库单 -->
|
|
|
+ <el-tab-pane label="入库单" name="inbound">
|
|
|
+ <el-table :data="inboundList" v-loading="inboundLoading" border stripe max-height="500">
|
|
|
+ <el-table-column prop="orderNo" label="入库单号" width="180" />
|
|
|
+ <el-table-column prop="supplierName" label="供应商" width="150" />
|
|
|
+ <el-table-column prop="totalItems" label="商品项数" width="80" />
|
|
|
+ <el-table-column label="状态" width="90">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="row.status === 'completed' ? 'success' : row.status === 'cancelled' ? 'info' : 'warning'" size="mini">
|
|
|
+ {{ {'pending':'待确认','completed':'已完成','cancelled':'已取消'}[row.status] || row.status }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createdAt" label="创建时间" width="160" />
|
|
|
+ <el-table-column label="操作" width="200" fixed="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button size="mini" @click="showInboundDetail(row)">详情</el-button>
|
|
|
+ <el-button v-if="row.status === 'pending'" size="mini" type="success" @click="handleConfirmInbound(row)">确认入库</el-button>
|
|
|
+ <el-button v-if="row.status === 'pending'" size="mini" type="danger" @click="handleCancelInbound(row)">取消</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination @size-change="inboundPageSize=$event;loadInboundOrders()" @current-change="inboundPage=$event;loadInboundOrders()" :current-page="inboundPage" :page-sizes="[10,20,50]" :page-size="inboundPageSize" :total="inboundTotal" layout="total, sizes, prev, pager, next, jumper" class="pagination-wrap" />
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 库存预警 -->
|
|
|
+ <el-tab-pane label="库存预警" name="alerts">
|
|
|
+ <el-button type="primary" size="mini" @click="loadAlerts" style="margin-bottom:12px;">刷新</el-button>
|
|
|
+ <el-table :data="alertList" v-loading="alertLoading" border stripe max-height="500">
|
|
|
+ <el-table-column prop="productName" label="商品名称" min-width="200" />
|
|
|
+ <el-table-column prop="specs" label="SKU规格" width="150">
|
|
|
+ <template slot-scope="{ row }">{{ row.specs || '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="currentStock" label="当前库存" width="80">
|
|
|
+ <template slot-scope="{ row }"><span style="color:#f56c6c;font-weight:bold;">{{ row.currentStock }}</span></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="minStockAlert" label="预警下限" width="80" />
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 盘点记录 -->
|
|
|
+ <el-tab-pane label="盘点记录" name="counting">
|
|
|
+ <el-button type="primary" size="mini" @click="showCreateCounting = true" style="margin-bottom:12px;">新建盘点</el-button>
|
|
|
+ <el-table :data="countingList" v-loading="countingLoading" border stripe max-height="500">
|
|
|
+ <el-table-column prop="id" label="ID" width="60" />
|
|
|
+ <el-table-column label="系统库存" width="80">
|
|
|
+ <template slot-scope="{ row }">{{ row.expectedStock }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实际库存" width="80">
|
|
|
+ <template slot-scope="{ row }">{{ row.actualStock }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="差异" width="80">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span :style="{ color: row.difference > 0 ? '#67c23a' : row.difference < 0 ? '#f56c6c' : '#999' }">{{ row.difference > 0 ? '+' : '' }}{{ row.difference }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" width="80">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="row.status === 'confirmed' ? 'success' : 'warning'" size="mini">{{ row.status === 'confirmed' ? '已确认' : '待确认' }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="remark" label="备注" min-width="150" />
|
|
|
+ <el-table-column prop="createdAt" label="时间" width="160" />
|
|
|
+ <el-table-column label="操作" width="100" fixed="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button v-if="row.status === 'pending'" size="mini" type="success" @click="handleConfirmCounting(row)">确认</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination @size-change="countingPageSize=$event;loadCountingRecords()" @current-change="countingPage=$event;loadCountingRecords()" :current-page="countingPage" :page-sizes="[10,20,50]" :page-size="countingPageSize" :total="countingTotal" layout="total, sizes, prev, pager, next, jumper" class="pagination-wrap" />
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 入库单详情弹窗 -->
|
|
|
+ <el-dialog title="入库单详情" :visible.sync="detailDialogVisible" width="700px">
|
|
|
+ <el-descriptions :column="2" border v-if="inboundDetail">
|
|
|
+ <el-descriptions-item label="入库单号">{{ inboundDetail.order.orderNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="供应商">{{ inboundDetail.order.supplierName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">{{ inboundDetail.order.status }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="备注">{{ inboundDetail.order.remark || '-' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <el-table :data="inboundDetail.items" border stripe style="margin-top:12px;" v-if="inboundDetail">
|
|
|
+ <el-table-column prop="productId" label="商品ID" width="70" />
|
|
|
+ <el-table-column prop="skuId" label="SKU ID" width="70" />
|
|
|
+ <el-table-column prop="quantity" label="数量" width="70" />
|
|
|
+ </el-table>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 新建盘点弹窗 -->
|
|
|
+ <el-dialog title="新建盘点" :visible.sync="showCreateCounting" width="450px">
|
|
|
+ <el-form label-width="100px">
|
|
|
+ <el-form-item label="商品ID">
|
|
|
+ <el-input v-model="countingForm.productId" type="number" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="SKU ID">
|
|
|
+ <el-input v-model="countingForm.skuId" type="number" placeholder="选填" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="实际库存">
|
|
|
+ <el-input v-model="countingForm.actualStock" type="number" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注">
|
|
|
+ <el-input v-model="countingForm.remark" type="textarea" :rows="2" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="showCreateCounting = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleCreateCounting">提交</el-button>
|
|
|
+ </span>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getInventoryTransactions, getInboundOrderList, getInboundOrderDetail, confirmInbound, cancelInbound, getStockAlerts, getCountingList, createCountingRecord, confirmCounting } from '@/api/admin'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'InventoryManage',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ activeTab: 'transactions',
|
|
|
+ txList: [], txLoading: false, txPage: 1, txPageSize: 20, txTotal: 0,
|
|
|
+ txFilters: { productName: '', type: '', dateRange: null },
|
|
|
+ inboundList: [], inboundLoading: false, inboundPage: 1, inboundPageSize: 20, inboundTotal: 0,
|
|
|
+ inboundDetail: null, detailDialogVisible: false,
|
|
|
+ alertList: [], alertLoading: false,
|
|
|
+ countingList: [], countingLoading: false, countingPage: 1, countingPageSize: 20, countingTotal: 0,
|
|
|
+ showCreateCounting: false, countingForm: { productId: '', skuId: '', actualStock: '', remark: '' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.loadTransactions()
|
|
|
+ this.loadInboundOrders()
|
|
|
+ this.loadAlerts()
|
|
|
+ this.loadCountingRecords()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async loadTransactions() {
|
|
|
+ this.txLoading = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ page: this.txPage, size: this.txPageSize,
|
|
|
+ type: this.txFilters.type || null,
|
|
|
+ startDate: this.txFilters.dateRange ? this.txFilters.dateRange[0] : null,
|
|
|
+ endDate: this.txFilters.dateRange ? this.txFilters.dateRange[1] : null
|
|
|
+ }
|
|
|
+ const res = await getInventoryTransactions(params)
|
|
|
+ this.txList = res.data.records || []
|
|
|
+ this.txTotal = res.data.total || 0
|
|
|
+ } catch (e) { this.$message.error('加载失败') }
|
|
|
+ finally { this.txLoading = false }
|
|
|
+ },
|
|
|
+ async loadInboundOrders() {
|
|
|
+ this.inboundLoading = true
|
|
|
+ try {
|
|
|
+ const res = await getInboundOrderList({ page: this.inboundPage, size: this.inboundPageSize })
|
|
|
+ this.inboundList = res.data.records || []
|
|
|
+ this.inboundTotal = res.data.total || 0
|
|
|
+ } catch (e) { this.$message.error('加载失败') }
|
|
|
+ finally { this.inboundLoading = false }
|
|
|
+ },
|
|
|
+ async showInboundDetail(row) {
|
|
|
+ try {
|
|
|
+ const res = await getInboundOrderDetail(row.id)
|
|
|
+ this.inboundDetail = res.data
|
|
|
+ this.detailDialogVisible = true
|
|
|
+ } catch (e) { this.$message.error('加载失败') }
|
|
|
+ },
|
|
|
+ async handleConfirmInbound(row) {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认入库?库存将被增加,不可撤销。', '提示')
|
|
|
+ await confirmInbound(row.id)
|
|
|
+ this.$message.success('入库成功')
|
|
|
+ this.loadInboundOrders()
|
|
|
+ } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
|
|
|
+ },
|
|
|
+ async handleCancelInbound(row) {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认取消该入库单?', '提示')
|
|
|
+ await cancelInbound(row.id)
|
|
|
+ this.$message.success('已取消')
|
|
|
+ this.loadInboundOrders()
|
|
|
+ } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
|
|
|
+ },
|
|
|
+ async loadAlerts() {
|
|
|
+ this.alertLoading = true
|
|
|
+ try {
|
|
|
+ const res = await getStockAlerts()
|
|
|
+ this.alertList = res.data || []
|
|
|
+ } catch (e) { this.$message.error('加载失败') }
|
|
|
+ finally { this.alertLoading = false }
|
|
|
+ },
|
|
|
+ async loadCountingRecords() {
|
|
|
+ this.countingLoading = true
|
|
|
+ try {
|
|
|
+ const res = await getCountingList({ page: this.countingPage, size: this.countingPageSize })
|
|
|
+ this.countingList = res.data.records || []
|
|
|
+ this.countingTotal = res.data.total || 0
|
|
|
+ } catch (e) { this.$message.error('加载失败') }
|
|
|
+ finally { this.countingLoading = false }
|
|
|
+ },
|
|
|
+ async handleCreateCounting() {
|
|
|
+ if (!this.countingForm.productId || !this.countingForm.actualStock) {
|
|
|
+ return this.$message.warning('请填写必填项')
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await createCountingRecord({
|
|
|
+ productId: parseInt(this.countingForm.productId),
|
|
|
+ skuId: this.countingForm.skuId ? parseInt(this.countingForm.skuId) : null,
|
|
|
+ actualStock: parseInt(this.countingForm.actualStock),
|
|
|
+ remark: this.countingForm.remark
|
|
|
+ })
|
|
|
+ this.$message.success('盘点记录已创建')
|
|
|
+ this.showCreateCounting = false
|
|
|
+ this.countingForm = { productId: '', skuId: '', actualStock: '', remark: '' }
|
|
|
+ this.loadCountingRecords()
|
|
|
+ } catch (e) { this.$message.error('操作失败') }
|
|
|
+ },
|
|
|
+ async handleConfirmCounting(row) {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认盘点结果?差异将自动调账。', '提示')
|
|
|
+ await confirmCounting(row.id)
|
|
|
+ this.$message.success('盘点已确认')
|
|
|
+ this.loadCountingRecords()
|
|
|
+ } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.inventory-manage { padding: 20px; }
|
|
|
+.filter-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
|
+.pagination-wrap { margin-top: 12px; text-align: right; }
|
|
|
+</style>
|