InventoryManage.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <template>
  2. <div class="inventory-manage admin-page">
  3. <el-card>
  4. <PageHeader slot="header" title="库存管理">
  5. <el-button type="success" size="mini" icon="el-icon-plus" @click="$router.push('/inventory/inbound-create')">创建入库单</el-button>
  6. </PageHeader>
  7. <el-tabs v-model="activeTab">
  8. <!-- 库存流水 -->
  9. <el-tab-pane label="库存流水" name="transactions">
  10. <div class="filter-bar" style="margin-bottom:12px;">
  11. <el-input v-model="txFilters.productName" placeholder="商品名称" clearable style="width:160px;margin-right:8px;" />
  12. <el-select v-model="txFilters.type" placeholder="类型" clearable style="width:120px;margin-right:8px;">
  13. <el-option label="全部" value="" />
  14. <el-option label="入库" value="inbound" />
  15. <el-option label="出库" value="outbound" />
  16. <el-option label="调账" value="adjustment" />
  17. <el-option label="盘点" value="counting" />
  18. <el-option label="退款入库" value="refund" />
  19. </el-select>
  20. <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;" />
  21. <el-button type="primary" size="mini" @click="loadTransactions">查询</el-button>
  22. </div>
  23. <div class="adm-table-wrap">
  24. <el-table :data="txList" v-loading="txLoading" border stripe height="100%">
  25. <el-table-column prop="id" label="ID" width="60" />
  26. <el-table-column label="商品" min-width="150">
  27. <template slot-scope="{ row }">{{ row.productName || 'ID:' + row.productId }}</template>
  28. </el-table-column>
  29. <el-table-column label="类型" width="80">
  30. <template slot-scope="{ row }">
  31. <el-tag :type="row.type === 'inbound' || row.type === 'refund' ? 'success' : row.type === 'outbound' ? 'danger' : 'warning'" size="mini">
  32. {{ {'inbound':'入库','outbound':'出库','adjustment':'调账','counting':'盘点','refund':'退款入库'}[row.type] || row.type }}
  33. </el-tag>
  34. </template>
  35. </el-table-column>
  36. <el-table-column label="数量" width="80">
  37. <template slot-scope="{ row }">
  38. <span :style="{ color: row.direction === 1 ? '#67c23a' : '#f56c6c' }">{{ row.direction === 1 ? '+' : '-' }}{{ row.quantity }}</span>
  39. </template>
  40. </el-table-column>
  41. <el-table-column prop="beforeStock" label="变动前" width="70">
  42. <template slot="header">
  43. <span class="sort-header" @click.stop="onSortClick('beforeStock', $event)">变动前<span v-if="sortIndex('beforeStock') !== null" class="sort-index" :class="sortClass('beforeStock') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('beforeStock') }}</span>{{ sortIcon('beforeStock') }}</span>
  44. </template>
  45. </el-table-column>
  46. <el-table-column prop="afterStock" label="变动后" width="70">
  47. <template slot="header">
  48. <span class="sort-header" @click.stop="onSortClick('afterStock', $event)">变动后<span v-if="sortIndex('afterStock') !== null" class="sort-index" :class="sortClass('afterStock') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('afterStock') }}</span>{{ sortIcon('afterStock') }}</span>
  49. </template>
  50. </el-table-column>
  51. <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
  52. <el-table-column prop="createdAt" label="时间" width="160">
  53. <template slot-scope="{ row }">{{ formatTime ? formatTime(row.createdAt) : row.createdAt }}</template>
  54. <template slot="header">
  55. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
  56. </template>
  57. </el-table-column>
  58. </el-table>
  59. </div>
  60. <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" />
  61. </el-tab-pane>
  62. <!-- 入库单 -->
  63. <el-tab-pane label="入库单" name="inbound">
  64. <div class="adm-table-wrap">
  65. <el-table :data="inboundList" v-loading="inboundLoading" border stripe height="100%">
  66. <el-table-column prop="orderNo" label="入库单号" width="180">
  67. <template slot="header">
  68. <span class="sort-header" @click.stop="onSortClick('orderNo', $event)">入库单号<span v-if="sortIndex('orderNo') !== null" class="sort-index" :class="sortClass('orderNo') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('orderNo') }}</span>{{ sortIcon('orderNo') }}</span>
  69. </template>
  70. </el-table-column>
  71. <el-table-column prop="supplierName" label="供应商" width="120">
  72. <template slot="header">
  73. <span class="sort-header" @click.stop="onSortClick('supplierName', $event)">供应商<span v-if="sortIndex('supplierName') !== null" class="sort-index" :class="sortClass('supplierName') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('supplierName') }}</span>{{ sortIcon('supplierName') }}</span>
  74. </template>
  75. </el-table-column>
  76. <el-table-column label="入库日期" width="100">
  77. <template slot-scope="{ row }">{{ row.inboundDate || '-' }}</template>
  78. </el-table-column>
  79. <el-table-column label="总金额" width="100">
  80. <template slot-scope="{ row }">{{ row.totalAmount ? '¥' + (row.totalAmount / 100).toFixed(2) : '-' }}</template>
  81. </el-table-column>
  82. <el-table-column prop="totalItems" label="商品项数" width="80">
  83. <template slot="header">
  84. <span class="sort-header" @click.stop="onSortClick('totalItems', $event)">商品项数<span v-if="sortIndex('totalItems') !== null" class="sort-index" :class="sortClass('totalItems') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('totalItems') }}</span>{{ sortIcon('totalItems') }}</span>
  85. </template>
  86. </el-table-column>
  87. <el-table-column label="状态" width="90">
  88. <template slot-scope="{ row }">
  89. <el-tag :type="row.status === 'completed' ? 'success' : row.status === 'cancelled' ? 'info' : 'warning'" size="mini">
  90. {{ {'pending':'待确认','completed':'已完成','cancelled':'已取消'}[row.status] || row.status }}
  91. </el-tag>
  92. </template>
  93. </el-table-column>
  94. <el-table-column prop="createdAt" label="创建时间" width="160">
  95. <template slot-scope="{ row }">{{ formatTime ? formatTime(row.createdAt) : row.createdAt }}</template>
  96. <template slot="header">
  97. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
  98. </template>
  99. </el-table-column>
  100. <el-table-column label="操作" width="200" fixed="right">
  101. <template slot-scope="{ row }">
  102. <el-button size="mini" @click="showInboundDetail(row)">详情</el-button>
  103. <el-button v-if="row.status === 'pending'" size="mini" type="success" @click="handleConfirmInbound(row)">确认入库</el-button>
  104. <el-button v-if="row.status === 'pending'" size="mini" type="danger" @click="handleCancelInbound(row)">取消</el-button>
  105. </template>
  106. </el-table-column>
  107. </el-table>
  108. </div>
  109. <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" />
  110. </el-tab-pane>
  111. <!-- 库存预警 -->
  112. <el-tab-pane label="库存预警" name="alerts">
  113. <el-button type="primary" size="mini" @click="loadAlerts" style="margin-bottom:12px;">刷新</el-button>
  114. <div class="adm-table-wrap">
  115. <el-table :data="alertList" v-loading="alertLoading" border stripe height="100%">
  116. <el-table-column prop="productName" label="商品名称" min-width="200" />
  117. <el-table-column prop="specs" label="SKU规格" width="150">
  118. <template slot-scope="{ row }">{{ row.specs || '-' }}</template>
  119. </el-table-column>
  120. <el-table-column prop="currentStock" label="当前库存" width="80">
  121. <template slot-scope="{ row }"><span style="color:#f56c6c;font-weight:bold;">{{ row.currentStock }}</span></template>
  122. </el-table-column>
  123. <el-table-column prop="minStockAlert" label="预警下限" width="80" />
  124. </el-table>
  125. </div>
  126. </el-tab-pane>
  127. <!-- 出库单 -->
  128. <el-tab-pane label="出库单" name="outbound">
  129. <el-button type="primary" size="mini" @click="showCreateOutbound = true" style="margin-bottom:12px;">新建出库单</el-button>
  130. <div class="adm-table-wrap">
  131. <el-table :data="outboundList" v-loading="outboundLoading" border stripe height="100%">
  132. <el-table-column prop="id" label="ID" width="60">
  133. <template slot="header">
  134. <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
  135. </template>
  136. </el-table-column>
  137. <el-table-column label="商品" min-width="150">
  138. <template slot-scope="{ row }">{{ row.productName || 'ID:' + row.productId }}</template>
  139. </el-table-column>
  140. <el-table-column label="数量" width="70">
  141. <template slot-scope="{ row }"><span style="color:#f56c6c;">-{{ row.quantity }}</span></template>
  142. </el-table-column>
  143. <el-table-column prop="remark" label="出库原因" min-width="200" show-overflow-tooltip>
  144. <template slot="header">
  145. <span class="sort-header" @click.stop="onSortClick('remark', $event)">出库原因<span v-if="sortIndex('remark') !== null" class="sort-index" :class="sortClass('remark') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('remark') }}</span>{{ sortIcon('remark') }}</span>
  146. </template>
  147. </el-table-column>
  148. <el-table-column prop="createdAt" label="时间" width="160">
  149. <template slot="header">
  150. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
  151. </template>
  152. </el-table-column>
  153. </el-table>
  154. </div>
  155. <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" />
  156. </el-tab-pane>
  157. <!-- 盘点记录 -->
  158. <el-tab-pane label="盘点记录" name="counting">
  159. <el-button type="primary" size="mini" @click="showCreateCounting = true" style="margin-bottom:12px;">新建盘点</el-button>
  160. <div class="adm-table-wrap">
  161. <el-table :data="countingList" v-loading="countingLoading" border stripe height="100%">
  162. <el-table-column prop="id" label="ID" width="60">
  163. <template slot="header">
  164. <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
  165. </template>
  166. </el-table-column>
  167. <el-table-column label="系统库存" width="80">
  168. <template slot-scope="{ row }">{{ row.expectedStock }}</template>
  169. </el-table-column>
  170. <el-table-column label="实际库存" width="80">
  171. <template slot-scope="{ row }">{{ row.actualStock }}</template>
  172. </el-table-column>
  173. <el-table-column label="差异" width="80">
  174. <template slot-scope="{ row }">
  175. <span :style="{ color: row.difference > 0 ? '#67c23a' : row.difference < 0 ? '#f56c6c' : '#999' }">{{ row.difference > 0 ? '+' : '' }}{{ row.difference }}</span>
  176. </template>
  177. </el-table-column>
  178. <el-table-column label="状态" width="80">
  179. <template slot-scope="{ row }">
  180. <el-tag :type="row.status === 'confirmed' ? 'success' : 'warning'" size="mini">{{ row.status === 'confirmed' ? '已确认' : '待确认' }}</el-tag>
  181. </template>
  182. </el-table-column>
  183. <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip>
  184. <template slot="header">
  185. <span class="sort-header" @click.stop="onSortClick('remark', $event)">备注<span v-if="sortIndex('remark') !== null" class="sort-index" :class="sortClass('remark') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('remark') }}</span>{{ sortIcon('remark') }}</span>
  186. </template>
  187. </el-table-column>
  188. <el-table-column prop="createdAt" label="时间" width="160">
  189. <template slot="header">
  190. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
  191. </template>
  192. </el-table-column>
  193. <el-table-column label="操作" width="100" fixed="right">
  194. <template slot-scope="{ row }">
  195. <el-button v-if="row.status === 'pending'" size="mini" type="success" @click="handleConfirmCounting(row)">确认</el-button>
  196. </template>
  197. </el-table-column>
  198. </el-table>
  199. </div>
  200. <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" />
  201. </el-tab-pane>
  202. </el-tabs>
  203. </el-card>
  204. <!-- 入库单详情弹窗 -->
  205. <el-dialog title="入库单详情" :visible.sync="detailDialogVisible" width="700px">
  206. <el-descriptions :column="2" border v-if="inboundDetail">
  207. <el-descriptions-item label="入库单号">{{ inboundDetail.order.orderNo }}</el-descriptions-item>
  208. <el-descriptions-item label="供应商">{{ inboundDetail.order.supplierName || '-' }}</el-descriptions-item>
  209. <el-descriptions-item label="状态">{{ inboundDetail.order.status }}</el-descriptions-item>
  210. <el-descriptions-item label="备注">{{ inboundDetail.order.remark || '-' }}</el-descriptions-item>
  211. </el-descriptions>
  212. <el-table :data="inboundDetail.items" border stripe style="margin-top:12px;" v-if="inboundDetail">
  213. <el-table-column prop="productId" label="商品ID" width="70" />
  214. <el-table-column prop="skuId" label="SKU ID" width="70" />
  215. <el-table-column prop="quantity" label="数量" width="70" />
  216. </el-dialog>
  217. <!-- 新建出库弹窗 -->
  218. <el-dialog title="新建出库单" :visible.sync="showCreateOutbound" width="500px">
  219. <el-form label-width="100px">
  220. <el-form-item label="商品">
  221. <el-select v-model="outboundForm.productId" filterable remote placeholder="搜索商品" :remote-method="searchOutboundProduct" style="width:300px;">
  222. <el-option v-for="p in outboundProductOptions" :key="p.id" :label="p.name + ' (库存:' + (p.stock || 0) + ')'" :value="p.id" />
  223. </el-select>
  224. </el-form-item>
  225. <el-form-item label="SKU" v-if="outboundSkuOptions.length">
  226. <el-select v-model="outboundForm.skuId" placeholder="选择规格" clearable style="width:200px;">
  227. <el-option v-for="s in outboundSkuOptions" :key="s.id" :label="s.specs || '默认' + ' (库存:' + (s.stock || 0) + ')'" :value="s.id" />
  228. </el-select>
  229. </el-form-item>
  230. <el-form-item label="数量">
  231. <el-input v-model.number="outboundForm.quantity" type="number" min="1" />
  232. </el-form-item>
  233. <el-form-item label="出库原因">
  234. <el-select v-model="outboundForm.reason" placeholder="选择原因" style="width:200px;">
  235. <el-option label="赠品" value="赠品" />
  236. <el-option label="损耗" value="损耗" />
  237. <el-option label="样品" value="样品" />
  238. <el-option label="其他" value="其他" />
  239. </el-select>
  240. </el-form-item>
  241. <el-form-item label="备注">
  242. <el-input v-model="outboundForm.remark" type="textarea" :rows="2" />
  243. </el-form-item>
  244. </el-form>
  245. <span slot="footer">
  246. <el-button @click="showCreateOutbound = false">取消</el-button>
  247. <el-button type="danger" @click="handleCreateOutbound">确认出库</el-button>
  248. </span>
  249. </el-dialog>
  250. <!-- 新建盘点弹窗 -->
  251. <el-dialog title="新建盘点" :visible.sync="showCreateCounting" width="450px">
  252. <el-form label-width="100px">
  253. <el-form-item label="商品ID">
  254. <el-input v-model="countingForm.productId" type="number" />
  255. </el-form-item>
  256. <el-form-item label="SKU ID">
  257. <el-input v-model="countingForm.skuId" type="number" placeholder="选填" />
  258. </el-form-item>
  259. <el-form-item label="实际库存">
  260. <el-input v-model="countingForm.actualStock" type="number" />
  261. </el-form-item>
  262. <el-form-item label="备注">
  263. <el-input v-model="countingForm.remark" type="textarea" :rows="2" />
  264. </el-form-item>
  265. </el-form>
  266. <span slot="footer">
  267. <el-button @click="showCreateCounting = false">取消</el-button>
  268. <el-button type="primary" @click="handleCreateCounting">提交</el-button>
  269. </span>
  270. </el-dialog>
  271. </el-table>
  272. </div>
  273. </template>
  274. <script>
  275. import SortMixin from '@/mixins/SortMixin'
  276. import ListStateMixin from '@/mixins/ListStateMixin'
  277. import { getInventoryTransactions, getInboundOrderList, getInboundOrderDetail, confirmInbound, cancelInbound, getStockAlerts, getCountingList, createCountingRecord, confirmCounting, createManualOutbound, getManualOutboundList, getProductList, getProductSkuList } from '@/api/admin'
  278. export default {
  279. mixins: [SortMixin, ListStateMixin],
  280. name: 'InventoryManage',
  281. data() {
  282. return {
  283. activeTab: 'transactions',
  284. txList: [], txLoading: false, txPage: 1, txPageSize: 20, txTotal: 0,
  285. txFilters: { productName: '', type: '', dateRange: null },
  286. inboundList: [], inboundLoading: false, inboundPage: 1, inboundPageSize: 20, inboundTotal: 0,
  287. inboundDetail: null, detailDialogVisible: false,
  288. alertList: [], alertLoading: false,
  289. countingList: [], countingLoading: false, countingPage: 1, countingPageSize: 20, countingTotal: 0,
  290. showCreateCounting: false, countingForm: { productId: '', skuId: '', actualStock: '', remark: '' },
  291. outboundList: [], outboundLoading: false, outboundPage: 1, outboundPageSize: 20, outboundTotal: 0,
  292. showCreateOutbound: false, outboundProductOptions: [], outboundSkuOptions: [],
  293. outboundForm: { productId: '', skuId: '', quantity: 1, reason: '', remark: '' },
  294. sortableColumns: { id: 'ID', createdAt: '时间', beforeStock: '变动前', afterStock: '变动后', orderNo: '入库单号', supplierName: '供应商', totalItems: '商品项数', remark: '备注' }
  295. }
  296. },
  297. mounted() {
  298. this.loadTransactions()
  299. this.loadInboundOrders()
  300. this.loadAlerts()
  301. this.loadCountingRecords()
  302. this.loadOutboundList()
  303. },
  304. methods: {
  305. loadList() { this.loadTransactions() },
  306. async loadTransactions() {
  307. this.txLoading = true
  308. try {
  309. const params = {
  310. page: this.txPage, size: this.txPageSize,
  311. type: this.txFilters.type || null,
  312. startDate: this.txFilters.dateRange ? this.txFilters.dateRange[0] : null,
  313. endDate: this.txFilters.dateRange ? this.txFilters.dateRange[1] : null
  314. }
  315. if (this.sortState.length > 0) params.sort = this.sortState
  316. const res = await getInventoryTransactions(params)
  317. this.txList = res.data.records || []
  318. this.txTotal = res.data.total || 0
  319. } catch (e) { this.$message.error('加载失败') }
  320. finally { this.txLoading = false }
  321. },
  322. async loadInboundOrders() {
  323. this.inboundLoading = true
  324. try {
  325. const res = await getInboundOrderList({ page: this.inboundPage, size: this.inboundPageSize })
  326. this.inboundList = res.data.records || []
  327. this.inboundTotal = res.data.total || 0
  328. } catch (e) { this.$message.error('加载失败') }
  329. finally { this.inboundLoading = false }
  330. },
  331. async showInboundDetail(row) {
  332. try {
  333. const res = await getInboundOrderDetail(row.id)
  334. this.inboundDetail = res.data
  335. this.detailDialogVisible = true
  336. } catch (e) { this.$message.error('加载失败') }
  337. },
  338. async handleConfirmInbound(row) {
  339. try {
  340. await this.$confirm('确认入库?库存将被增加,不可撤销。', '提示')
  341. await confirmInbound(row.id)
  342. this.$message.success('入库成功')
  343. this.loadInboundOrders()
  344. } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
  345. },
  346. async handleCancelInbound(row) {
  347. try {
  348. await this.$confirm('确认取消该入库单?', '提示')
  349. await cancelInbound(row.id)
  350. this.$message.success('已取消')
  351. this.loadInboundOrders()
  352. } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
  353. },
  354. async loadAlerts() {
  355. this.alertLoading = true
  356. try {
  357. const res = await getStockAlerts()
  358. this.alertList = res.data || []
  359. } catch (e) { this.$message.error('加载失败') }
  360. finally { this.alertLoading = false }
  361. },
  362. async loadCountingRecords() {
  363. this.countingLoading = true
  364. try {
  365. const res = await getCountingList({ page: this.countingPage, size: this.countingPageSize })
  366. this.countingList = res.data.records || []
  367. this.countingTotal = res.data.total || 0
  368. } catch (e) { this.$message.error('加载失败') }
  369. finally { this.countingLoading = false }
  370. },
  371. async handleCreateCounting() {
  372. if (!this.countingForm.productId || !this.countingForm.actualStock) {
  373. return this.$message.warning('请填写必填项')
  374. }
  375. try {
  376. await createCountingRecord({
  377. productId: parseInt(this.countingForm.productId),
  378. skuId: this.countingForm.skuId ? parseInt(this.countingForm.skuId) : null,
  379. actualStock: parseInt(this.countingForm.actualStock),
  380. remark: this.countingForm.remark
  381. })
  382. this.$message.success('盘点记录已创建')
  383. this.showCreateCounting = false
  384. this.countingForm = { productId: '', skuId: '', actualStock: '', remark: '' }
  385. this.loadCountingRecords()
  386. } catch (e) { this.$message.error('操作失败') }
  387. },
  388. async handleConfirmCounting(row) {
  389. try {
  390. await this.$confirm('确认盘点结果?差异将自动调账。', '提示')
  391. await confirmCounting(row.id)
  392. this.$message.success('盘点已确认')
  393. this.loadCountingRecords()
  394. } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
  395. },
  396. async loadOutboundList() {
  397. this.outboundLoading = true
  398. try {
  399. const res = await getManualOutboundList({ page: this.outboundPage, size: this.outboundPageSize })
  400. this.outboundList = res.data.records || []
  401. this.outboundTotal = res.data.total || 0
  402. } catch (e) { this.$message.error('加载失败') }
  403. finally { this.outboundLoading = false }
  404. },
  405. async searchOutboundProduct(query) {
  406. if (!query || query.length < 1) return
  407. try {
  408. const res = await getProductList({ keyword: query, page: 1, size: 20 })
  409. this.outboundProductOptions = res.data.records || []
  410. } catch (e) { /* ignore */ }
  411. },
  412. async handleCreateOutbound() {
  413. if (!this.outboundForm.productId || !this.outboundForm.quantity) {
  414. return this.$message.warning('请填写必填项')
  415. }
  416. try {
  417. await this.$confirm('确认出库?库存将被扣减。', '提示')
  418. await createManualOutbound({
  419. productId: parseInt(this.outboundForm.productId),
  420. skuId: this.outboundForm.skuId ? parseInt(this.outboundForm.skuId) : null,
  421. quantity: parseInt(this.outboundForm.quantity),
  422. reason: this.outboundForm.reason || null,
  423. remark: this.outboundForm.remark || null
  424. })
  425. this.$message.success('出库成功')
  426. this.showCreateOutbound = false
  427. this.outboundForm = { productId: '', skuId: '', quantity: 1, reason: '', remark: '' }
  428. this.loadOutboundList()
  429. } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
  430. }
  431. }
  432. }
  433. </script>
  434. <style scoped>
  435. .inventory-manage { padding: 20px; }
  436. .filter-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
  437. .pagination-wrap { margin-top: 12px; text-align: right; }
  438. .sort-header {
  439. cursor: pointer;
  440. user-select: none;
  441. font-weight: 500;
  442. font-size: 12px;
  443. }
  444. .sort-header:hover {
  445. color: #409eff;
  446. }
  447. .sort-tag {
  448. margin-left: 8px;
  449. cursor: pointer;
  450. white-space: nowrap;
  451. }
  452. </style>