Sfoglia il codice sorgente

feat: 库存管理 - InventoryManage.vue(4标签页)

Xiaogang Liao 1 mese fa
parent
commit
5d03acdcd9
1 ha cambiato i file con 284 aggiunte e 0 eliminazioni
  1. 284 0
      cfc-web/src/views/admin/InventoryManage.vue

+ 284 - 0
cfc-web/src/views/admin/InventoryManage.vue

@@ -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>