detail.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <script setup lang="ts">
  2. import { ref, onMounted } from "vue";
  3. import { useRoute, useRouter } from "vue-router";
  4. import { getOrderDetail, getOrderItems, updateOrder, submitOrder, syncToErp } from "@/api/replenishmentOrder";
  5. import { message } from "@/utils/message";
  6. import { useRenderIcon } from "@/components/ReIcon/src/hooks";
  7. import DeleteIcon from "~icons/ep/delete";
  8. import dayjs from "dayjs";
  9. defineOptions({
  10. name: "ReplenishmentOrderDetail"
  11. });
  12. const route = useRoute();
  13. const router = useRouter();
  14. const loading = ref(true);
  15. const saving = ref(false);
  16. const submitting = ref(false);
  17. const order = ref<any>({});
  18. const items = ref<any[]>([]);
  19. const statusMap: Record<number, { text: string; type: string }> = {
  20. 0: { text: "草稿", type: "info" },
  21. 1: { text: "已提交", type: "warning" },
  22. 2: { text: "已同步ERP", type: "primary" },
  23. 3: { text: "已完成", type: "success" },
  24. 4: { text: "已取消", type: "danger" }
  25. };
  26. async function loadData() {
  27. loading.value = true;
  28. try {
  29. const id = route.query.id as string;
  30. if (!id) return;
  31. const [orderRes, itemsRes] = await Promise.all([
  32. getOrderDetail(id),
  33. getOrderItems(id)
  34. ]);
  35. if (orderRes.data) {
  36. order.value = orderRes.data;
  37. }
  38. if (itemsRes.data) {
  39. // 深拷贝以便编辑,plannedQuantity 绑定到 v-model
  40. items.value = (itemsRes.data || []).map((item: any) => ({ ...item }));
  41. }
  42. } finally {
  43. loading.value = false;
  44. }
  45. }
  46. function addItem() {
  47. items.value.push({
  48. orderId: order.value.id,
  49. deviceId: order.value.deviceId,
  50. productId: null,
  51. productCode: "",
  52. productName: "",
  53. plannedQuantity: 1,
  54. unitPrice: undefined,
  55. shelfNum: undefined,
  56. position: "",
  57. beforeStock: undefined,
  58. afterStock: undefined
  59. });
  60. }
  61. function removeItem(index: number) {
  62. if (items.value.length <= 1) return;
  63. items.value.splice(index, 1);
  64. }
  65. async function handleSave() {
  66. saving.value = true;
  67. try {
  68. const payload = {
  69. id: Number(order.value.id),
  70. supplierName: order.value.supplierName || undefined,
  71. warehouseName: order.value.warehouseName || undefined,
  72. expectedArrivalTime: order.value.expectedArrivalTime,
  73. remark: order.value.remark || undefined,
  74. items: items.value.map((item: any) => ({
  75. productId: item.productId ? Number(item.productId) : undefined,
  76. productCode: item.productCode || undefined,
  77. productName: item.productName || undefined,
  78. plannedQuantity: item.plannedQuantity,
  79. unitPrice: item.unitPrice,
  80. shelfNum: item.shelfNum,
  81. position: item.position || undefined
  82. }))
  83. };
  84. await updateOrder(payload);
  85. message("保存成功", { type: "success" });
  86. await loadData();
  87. } catch (e) {
  88. message("保存失败", { type: "error" });
  89. } finally {
  90. saving.value = false;
  91. }
  92. }
  93. async function handleSubmitToErp() {
  94. submitting.value = true;
  95. try {
  96. // 先提交
  97. await submitOrder(order.value.id);
  98. // 再同步 ERP
  99. try {
  100. await syncToErp(order.value.id);
  101. message("已提交并同步ERP", { type: "success" });
  102. } catch {
  103. message("已提交,但ERP同步失败", { type: "warning" });
  104. }
  105. await loadData();
  106. } catch (e) {
  107. message("提交失败", { type: "error" });
  108. } finally {
  109. submitting.value = false;
  110. }
  111. }
  112. function goBack() {
  113. router.push({ path: "/inventory/replenishment-order/list" });
  114. }
  115. const isDraft = () => order.value.status === 0;
  116. const canEdit = () => isDraft();
  117. function itemSubtotal(item: any) {
  118. return ((item.plannedQuantity || 0) * (item.unitPrice || 0)).toFixed(2);
  119. }
  120. onMounted(() => {
  121. loadData();
  122. });
  123. </script>
  124. <template>
  125. <div class="main p-6">
  126. <div class="mb-4 flex items-center justify-between">
  127. <div class="flex items-center gap-4">
  128. <el-button :icon="'ep:arrow-left'" @click="goBack">返回列表</el-button>
  129. <h2 class="text-lg font-bold">补货单详情</h2>
  130. </div>
  131. <div v-if="!loading && isDraft()" class="flex gap-2">
  132. <el-button type="primary" :loading="submitting" @click="handleSubmitToErp">
  133. 提交ERP
  134. </el-button>
  135. <el-button type="success" :loading="saving" @click="handleSave">
  136. 保存修改
  137. </el-button>
  138. </div>
  139. </div>
  140. <el-card v-loading="loading" class="mb-4">
  141. <template #header>
  142. <div class="flex items-center justify-between">
  143. <span class="font-bold">基本信息</span>
  144. <el-tag
  145. v-if="order.status != null"
  146. :type="(statusMap[order.status]?.type as any) || 'info'"
  147. >
  148. {{ (statusMap[order.status] as any)?.text || "未知" }}
  149. </el-tag>
  150. </div>
  151. </template>
  152. <el-descriptions :column="3" border>
  153. <el-descriptions-item label="补货单号">
  154. {{ order.orderNo }}
  155. </el-descriptions-item>
  156. <el-descriptions-item label="设备ID">
  157. {{ order.deviceId }}
  158. </el-descriptions-item>
  159. <el-descriptions-item label="门店">
  160. {{ order.shopName || order.shopId || "-" }}
  161. </el-descriptions-item>
  162. <el-descriptions-item v-if="canEdit()" label="供应商">
  163. <el-input v-model="order.supplierName" placeholder="供应商" size="small" style="width:160px;" />
  164. </el-descriptions-item>
  165. <el-descriptions-item v-else label="供应商">
  166. {{ order.supplierName || "-" }}
  167. </el-descriptions-item>
  168. <el-descriptions-item v-if="canEdit()" label="仓库">
  169. <el-input v-model="order.warehouseName" placeholder="仓库" size="small" style="width:160px;" />
  170. </el-descriptions-item>
  171. <el-descriptions-item v-else label="仓库">
  172. {{ order.warehouseName || "-" }}
  173. </el-descriptions-item>
  174. <el-descriptions-item label="ERP采购单号">
  175. {{ order.erpOrderNo || "-" }}
  176. </el-descriptions-item>
  177. <el-descriptions-item label="总数量">
  178. {{ items.reduce((s: number, i: any) => s + (i.plannedQuantity || 0), 0) }}
  179. </el-descriptions-item>
  180. <el-descriptions-item label="总金额">
  181. ¥{{ items.reduce((s: number, i: any) => s + (i.plannedQuantity || 0) * (i.unitPrice || 0), 0).toFixed(2) }}
  182. </el-descriptions-item>
  183. <el-descriptions-item label="预计到货时间">
  184. {{ order.expectedArrivalTime || "-" }}
  185. </el-descriptions-item>
  186. <el-descriptions-item label="创建人">
  187. {{ order.creatorName || "-" }}
  188. </el-descriptions-item>
  189. <el-descriptions-item label="创建时间">
  190. {{ order.createTime ? dayjs(order.createTime).format("YYYY-MM-DD HH:mm:ss") : "-" }}
  191. </el-descriptions-item>
  192. <el-descriptions-item v-if="canEdit()" label="备注" :span="2">
  193. <el-input v-model="order.remark" placeholder="备注" size="small" />
  194. </el-descriptions-item>
  195. <el-descriptions-item v-else label="备注" :span="2">
  196. {{ order.remark || "-" }}
  197. </el-descriptions-item>
  198. </el-descriptions>
  199. </el-card>
  200. <el-card v-loading="loading">
  201. <template #header>
  202. <div class="flex items-center justify-between">
  203. <span class="font-bold">补货明细</span>
  204. <el-button v-if="canEdit()" type="primary" size="small" :icon="'ep:plus'" @click="addItem">
  205. 添加商品
  206. </el-button>
  207. </div>
  208. </template>
  209. <el-table :data="items" border stripe>
  210. <el-table-column label="图片" width="70">
  211. <template #default="{ row }">
  212. <img
  213. v-if="row.productImage"
  214. :src="row.productImage"
  215. style="width:40px;height:40px;object-fit:cover;border-radius:4px;"
  216. @error="$event.target.style.display='none'"
  217. />
  218. <span v-else style="color:#ccc;">-</span>
  219. </template>
  220. </el-table-column>
  221. <el-table-column label="商品编码" min-width="120">
  222. <template #default="{ row }">
  223. <el-input v-if="canEdit()" v-model="row.productCode" placeholder="商品编码" size="small" />
  224. <span v-else>{{ row.productCode || "-" }}</span>
  225. </template>
  226. </el-table-column>
  227. <el-table-column label="商品名称" min-width="140">
  228. <template #default="{ row }">
  229. <el-input v-if="canEdit()" v-model="row.productName" placeholder="商品名称" size="small" />
  230. <span v-else>{{ row.productName || "-" }}</span>
  231. </template>
  232. </el-table-column>
  233. <el-table-column label="计划数量" width="110">
  234. <template #default="{ row }">
  235. <el-input-number
  236. v-if="canEdit()"
  237. v-model="row.plannedQuantity"
  238. :min="1"
  239. :max="99999"
  240. size="small"
  241. controls-position="right"
  242. />
  243. <span v-else>{{ row.plannedQuantity }}</span>
  244. </template>
  245. </el-table-column>
  246. <el-table-column label="单价" width="110">
  247. <template #default="{ row }">
  248. <el-input-number
  249. v-if="canEdit()"
  250. v-model="row.unitPrice"
  251. :min="0"
  252. :precision="2"
  253. placeholder="单价"
  254. size="small"
  255. controls-position="right"
  256. />
  257. <span v-else>{{ row.unitPrice != null ? `¥${Number(row.unitPrice).toFixed(2)}` : "-" }}</span>
  258. </template>
  259. </el-table-column>
  260. <el-table-column label="小计" width="100">
  261. <template #default="{ row }">
  262. <span class="font-medium">¥{{ itemSubtotal(row) }}</span>
  263. </template>
  264. </el-table-column>
  265. <el-table-column label="层号" width="70">
  266. <template #default="{ row }">
  267. <el-input-number
  268. v-if="canEdit()"
  269. v-model="row.shelfNum"
  270. :min="1"
  271. placeholder="层"
  272. size="small"
  273. controls-position="right"
  274. />
  275. <span v-else>{{ row.shelfNum != null ? row.shelfNum : "-" }}</span>
  276. </template>
  277. </el-table-column>
  278. <el-table-column label="货道" width="90">
  279. <template #default="{ row }">
  280. <el-input v-if="canEdit()" v-model="row.position" placeholder="位置" size="small" />
  281. <span v-else>{{ row.position || "-" }}</span>
  282. </template>
  283. </el-table-column>
  284. <el-table-column v-if="canEdit()" label="操作" width="70" fixed="right">
  285. <template #default="{ $index }">
  286. <el-button
  287. type="danger"
  288. size="small"
  289. :icon="useRenderIcon(DeleteIcon)"
  290. :disabled="items.length <= 1"
  291. @click="removeItem($index)"
  292. />
  293. </template>
  294. </el-table-column>
  295. </el-table>
  296. </el-card>
  297. </div>
  298. </template>