Kaynağa Gözat

feat: 库存管理 - 入库单商品搜索/日期/金额 + 手动出库 + 订单快递费

Xiaogang Liao 1 ay önce
ebeveyn
işleme
5fac0e1297

+ 26 - 2
cfc-web/src/api/admin.js

@@ -700,11 +700,11 @@ export function getOrderDetail(orderNo) {
   })
 }
 
-export function shipOrder(orderNo, trackingNumber, expressCompany) {
+export function shipOrder(orderNo, trackingNumber, expressCompany, extra) {
   return request({
     url: '/api/admin/product/order/ship',
     method: 'post',
-    data: { orderNo, trackingNumber, expressCompany }
+    data: { orderNo, trackingNumber, expressCompany, ...extra }
   })
 }
 
@@ -1773,3 +1773,27 @@ export function checkBundleStock(bundleProductId, quantity) {
     data: { bundleProductId, quantity }
   })
 }
+
+export function createManualOutbound(data) {
+  return request({
+    url: '/api/admin/inventory/outbound/create',
+    method: 'post',
+    data
+  })
+}
+
+export function getManualOutboundList(params) {
+  return request({
+    url: '/api/admin/inventory/outbound/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function updateOrderExpress(data) {
+  return request({
+    url: '/api/admin/product/order/update-express',
+    method: 'post',
+    data
+  })
+}

+ 50 - 9
cfc-web/src/views/admin/InventoryInboundCreate.vue

@@ -7,18 +7,34 @@
       </div>
 
       <el-form label-width="100px">
+        <el-form-item label="入库日期">
+          <el-date-picker v-model="form.inboundDate" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" style="width:200px;" />
+        </el-form-item>
+
+        <el-form-item label="经办人">
+          <el-input v-model="form.operatorName" placeholder="经办人姓名" style="width:200px;" />
+        </el-form-item>
+
         <el-form-item label="供应商">
           <el-input v-model="form.supplierName" placeholder="供应商名称" style="width:300px;" />
         </el-form-item>
 
+        <el-form-item label="总金额(元)">
+          <el-input v-model.number="form.totalAmount" type="number" placeholder="入库总金额" style="width:200px;" />
+        </el-form-item>
+
         <el-form-item label="入库明细">
-          <div v-for="(item, idx) in form.items" :key="idx" style="display:flex;gap:8px;margin-bottom:8px;align-items:center;">
-            <el-input v-model="item.productId" placeholder="商品ID" type="number" style="width:120px;" />
-            <el-input v-model="item.skuId" placeholder="SKU ID(选填)" type="number" style="width:120px;" />
-            <el-input v-model="item.quantity" placeholder="数量" type="number" style="width:100px;" />
+          <div v-for="(item, idx) in form.items" :key="idx" style="display:flex;gap:8px;margin-bottom:8px;align-items:center;flex-wrap:wrap;">
+            <el-select v-model="item.productId" filterable remote placeholder="搜索商品" :remote-method="(q) => searchProduct(q, idx)" style="width:220px;" @change="(v) => onProductSelect(v, idx)">
+              <el-option v-for="p in item.productOptions" :key="p.id" :label="p.name + ' (ID:' + p.id + ')'" :value="p.id" />
+            </el-select>
+            <el-select v-model="item.skuId" placeholder="选择规格" clearable style="width:150px;" v-if="item.skuOptions && item.skuOptions.length">
+              <el-option v-for="s in item.skuOptions" :key="s.id" :label="s.specs || '默认'" :value="s.id" />
+            </el-select>
+            <el-input v-model.number="item.quantity" placeholder="数量" type="number" min="1" style="width:100px;" />
             <el-button type="danger" size="mini" @click="form.items.splice(idx,1)">删除</el-button>
           </div>
-          <el-button type="primary" size="mini" @click="form.items.push({productId:'',skuId:'',quantity:1})">添加行</el-button>
+          <el-button type="primary" size="mini" @click="addItem">添加行</el-button>
         </el-form-item>
 
         <el-form-item label="备注">
@@ -34,7 +50,7 @@
 </template>
 
 <script>
-import { createInboundOrder } from '@/api/admin'
+import { createInboundOrder, getProductList, getProductSkuList } from '@/api/admin'
 
 export default {
   name: 'InventoryInboundCreate',
@@ -42,17 +58,40 @@ export default {
     return {
       submitting: false,
       form: {
+        inboundDate: '',
+        operatorName: '',
         supplierName: '',
+        totalAmount: '',
         remark: '',
-        items: [{ productId: '', skuId: '', quantity: 1 }]
+        items: [{ productId: '', skuId: '', quantity: 1, productOptions: [], skuOptions: [] }]
       }
     }
   },
   methods: {
+    addItem() {
+      this.form.items.push({ productId: '', skuId: '', quantity: 1, productOptions: [], skuOptions: [] })
+    },
+    async searchProduct(query, idx) {
+      if (!query || query.length < 1) return
+      try {
+        const res = await getProductList({ keyword: query, page: 1, size: 20 })
+        this.form.items[idx].productOptions = res.data.records || []
+      } catch (e) { /* ignore */ }
+    },
+    async onProductSelect(productId, idx) {
+      const item = this.form.items[idx]
+      item.skuId = ''
+      item.skuOptions = []
+      if (!productId) return
+      try {
+        const res = await getProductSkuList(productId)
+        item.skuOptions = res.data || []
+      } catch (e) { /* ignore */ }
+    },
     async handleSubmit() {
       const validItems = this.form.items.filter(i => i.productId && i.quantity > 0)
       if (validItems.length === 0) {
-        return this.$message.warning('请添加入库明细')
+        return this.$message.warning('请添加商品明细')
       }
       this.submitting = true
       try {
@@ -62,8 +101,10 @@ export default {
           quantity: parseInt(i.quantity)
         }))
         await createInboundOrder({
+          inboundDate: this.form.inboundDate || null,
           supplierName: this.form.supplierName || null,
-          remark: this.form.remark || null,
+          totalAmount: this.form.totalAmount ? Math.round(parseFloat(this.form.totalAmount) * 100) : null,
+          remark: (this.form.operatorName ? '经办人:' + this.form.operatorName : '') + (this.form.remark ? ' - ' + this.form.remark : ''),
           items
         })
         this.$message.success('入库单已创建')

+ 99 - 3
cfc-web/src/views/admin/InventoryManage.vue

@@ -51,7 +51,13 @@
         <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="supplierName" label="供应商" width="120" />
+            <el-table-column label="入库日期" width="100">
+              <template slot-scope="{ row }">{{ row.inboundDate || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="总金额" width="100">
+              <template slot-scope="{ row }">{{ row.totalAmount ? '¥' + (row.totalAmount / 100).toFixed(2) : '-' }}</template>
+            </el-table-column>
             <el-table-column prop="totalItems" label="商品项数" width="80" />
             <el-table-column label="状态" width="90">
               <template slot-scope="{ row }">
@@ -87,6 +93,23 @@
           </el-table>
         </el-tab-pane>
 
+        <!-- 出库单 -->
+        <el-tab-pane label="出库单" name="outbound">
+          <el-button type="primary" size="mini" @click="showCreateOutbound = true" style="margin-bottom:12px;">新建出库单</el-button>
+          <el-table :data="outboundList" v-loading="outboundLoading" 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="70">
+              <template slot-scope="{ row }"><span style="color:#f56c6c;">-{{ row.quantity }}</span></template>
+            </el-table-column>
+            <el-table-column prop="remark" label="出库原因" min-width="200" show-overflow-tooltip />
+            <el-table-column prop="createdAt" label="时间" width="160" />
+          </el-table>
+          <el-pagination @size-change="outboundPageSize=$event;loadOutboundList()" @current-change="outboundPage=$event;loadOutboundList()" :current-page="outboundPage" :page-sizes="[10,20,50]" :page-size="outboundPageSize" :total="outboundTotal" layout="total, sizes, prev, pager, next, jumper" class="pagination-wrap" />
+        </el-tab-pane>
+
         <!-- 盘点记录 -->
         <el-tab-pane label="盘点记录" name="counting">
           <el-button type="primary" size="mini" @click="showCreateCounting = true" style="margin-bottom:12px;">新建盘点</el-button>
@@ -136,6 +159,40 @@
       </el-table>
     </el-dialog>
 
+    <!-- 新建出库弹窗 -->
+    <el-dialog title="新建出库单" :visible.sync="showCreateOutbound" width="500px">
+      <el-form label-width="100px">
+        <el-form-item label="商品">
+          <el-select v-model="outboundForm.productId" filterable remote placeholder="搜索商品" :remote-method="searchOutboundProduct" style="width:300px;">
+            <el-option v-for="p in outboundProductOptions" :key="p.id" :label="p.name + ' (库存:' + (p.stock || 0) + ')'" :value="p.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="SKU" v-if="outboundSkuOptions.length">
+          <el-select v-model="outboundForm.skuId" placeholder="选择规格" clearable style="width:200px;">
+            <el-option v-for="s in outboundSkuOptions" :key="s.id" :label="s.specs || '默认' + ' (库存:' + (s.stock || 0) + ')'" :value="s.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="数量">
+          <el-input v-model.number="outboundForm.quantity" type="number" min="1" />
+        </el-form-item>
+        <el-form-item label="出库原因">
+          <el-select v-model="outboundForm.reason" placeholder="选择原因" style="width:200px;">
+            <el-option label="赠品" value="赠品" />
+            <el-option label="损耗" value="损耗" />
+            <el-option label="样品" value="样品" />
+            <el-option label="其他" value="其他" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="outboundForm.remark" type="textarea" :rows="2" />
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="showCreateOutbound = false">取消</el-button>
+        <el-button type="danger" @click="handleCreateOutbound">确认出库</el-button>
+      </span>
+    </el-dialog>
+
     <!-- 新建盘点弹窗 -->
     <el-dialog title="新建盘点" :visible.sync="showCreateCounting" width="450px">
       <el-form label-width="100px">
@@ -161,7 +218,7 @@
 </template>
 
 <script>
-import { getInventoryTransactions, getInboundOrderList, getInboundOrderDetail, confirmInbound, cancelInbound, getStockAlerts, getCountingList, createCountingRecord, confirmCounting } from '@/api/admin'
+import { getInventoryTransactions, getInboundOrderList, getInboundOrderDetail, confirmInbound, cancelInbound, getStockAlerts, getCountingList, createCountingRecord, confirmCounting, createManualOutbound, getManualOutboundList, getProductList, getProductSkuList } from '@/api/admin'
 
 export default {
   name: 'InventoryManage',
@@ -174,7 +231,10 @@ export default {
       inboundDetail: null, detailDialogVisible: false,
       alertList: [], alertLoading: false,
       countingList: [], countingLoading: false, countingPage: 1, countingPageSize: 20, countingTotal: 0,
-      showCreateCounting: false, countingForm: { productId: '', skuId: '', actualStock: '', remark: '' }
+      showCreateCounting: false, countingForm: { productId: '', skuId: '', actualStock: '', remark: '' },
+      outboundList: [], outboundLoading: false, outboundPage: 1, outboundPageSize: 20, outboundTotal: 0,
+      showCreateOutbound: false, outboundProductOptions: [], outboundSkuOptions: [],
+      outboundForm: { productId: '', skuId: '', quantity: 1, reason: '', remark: '' }
     }
   },
   mounted() {
@@ -182,6 +242,7 @@ export default {
     this.loadInboundOrders()
     this.loadAlerts()
     this.loadCountingRecords()
+    this.loadOutboundList()
   },
   methods: {
     async loadTransactions() {
@@ -272,6 +333,41 @@ export default {
         this.$message.success('盘点已确认')
         this.loadCountingRecords()
       } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
+    },
+    async loadOutboundList() {
+      this.outboundLoading = true
+      try {
+        const res = await getManualOutboundList({ page: this.outboundPage, size: this.outboundPageSize })
+        this.outboundList = res.data.records || []
+        this.outboundTotal = res.data.total || 0
+      } catch (e) { this.$message.error('加载失败') }
+      finally { this.outboundLoading = false }
+    },
+    async searchOutboundProduct(query) {
+      if (!query || query.length < 1) return
+      try {
+        const res = await getProductList({ keyword: query, page: 1, size: 20 })
+        this.outboundProductOptions = res.data.records || []
+      } catch (e) { /* ignore */ }
+    },
+    async handleCreateOutbound() {
+      if (!this.outboundForm.productId || !this.outboundForm.quantity) {
+        return this.$message.warning('请填写必填项')
+      }
+      try {
+        await this.$confirm('确认出库?库存将被扣减。', '提示')
+        await createManualOutbound({
+          productId: parseInt(this.outboundForm.productId),
+          skuId: this.outboundForm.skuId ? parseInt(this.outboundForm.skuId) : null,
+          quantity: parseInt(this.outboundForm.quantity),
+          reason: this.outboundForm.reason || null,
+          remark: this.outboundForm.remark || null
+        })
+        this.$message.success('出库成功')
+        this.showCreateOutbound = false
+        this.outboundForm = { productId: '', skuId: '', quantity: 1, reason: '', remark: '' }
+        this.loadOutboundList()
+      } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
     }
   }
 }

+ 7 - 3
cfc-web/src/views/admin/OrderManage.vue

@@ -131,6 +131,9 @@
         <el-form-item label="物流单号">
           <el-input v-model="shipForm.trackingNumber" placeholder="请输入物流单号" />
         </el-form-item>
+        <el-form-item label="快递费(元)">
+          <el-input v-model.number="shipForm.shippingCost" type="number" placeholder="0" />
+        </el-form-item>
       </el-form>
       <span slot="footer">
         <el-button @click="shipDialogVisible = false">取消</el-button>
@@ -183,7 +186,7 @@ export default {
       shipDialogVisible: false,
       shipTarget: null,
       shipLoading: false,
-      shipForm: { expressCompany: '', trackingNumber: '' },
+      shipForm: { expressCompany: '', trackingNumber: '', shippingCost: '' },
       refundDialogVisible: false,
       refundTarget: null,
       refundLoading: false,
@@ -238,7 +241,7 @@ export default {
     },
     showShipDialog(row) {
       this.shipTarget = row
-      this.shipForm = { expressCompany: '', trackingNumber: '' }
+      this.shipForm = { expressCompany: '', trackingNumber: '', shippingCost: '' }
       this.shipDialogVisible = true
     },
     async confirmShip() {
@@ -247,7 +250,8 @@ export default {
       }
       this.shipLoading = true
       try {
-        await shipOrder(this.shipTarget.orderNo, this.shipForm.trackingNumber, this.shipForm.expressCompany)
+        const shippingCost = this.shipForm.shippingCost ? Math.round(parseFloat(this.shipForm.shippingCost) * 100) : null
+        await shipOrder(this.shipTarget.orderNo, this.shipForm.trackingNumber, this.shipForm.expressCompany, { shippingCost })
         this.$message.success('发货成功')
         this.shipDialogVisible = false
         this.loadList()