detail.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <template>
  2. <div class="order-detail-container">
  3. <div class="page-header">
  4. <el-button link @click="handleBack"
  5. ><el-icon><ArrowLeft /></el-icon><span>返回</span></el-button
  6. >
  7. <span class="page-title">订单详情</span>
  8. </div>
  9. <div class="page-content">
  10. <!-- 订单进度 -->
  11. <div class="progress-section">
  12. <div class="progress-steps">
  13. <div
  14. v-for="(step, index) in progressSteps"
  15. :key="index"
  16. :class="['step-item', { active: index <= currentStep, current: index === currentStep }]"
  17. >
  18. <div class="step-icon">
  19. <el-icon :size="24"><component :is="step.icon" /></el-icon>
  20. </div>
  21. <div class="step-info">
  22. <div class="step-title">{{ step.title }}</div>
  23. <div class="step-desc">{{ step.desc }}</div>
  24. <div v-if="index <= currentStep" class="step-time">{{ step.time }}</div>
  25. </div>
  26. <div v-if="index < progressSteps.length - 1" class="step-line" :class="{ active: index < currentStep }"></div>
  27. </div>
  28. </div>
  29. </div>
  30. <!-- 商品信息 -->
  31. <div class="section">
  32. <div class="section-title"><i class="title-bar"></i>商品信息</div>
  33. <el-table :data="productList" border style="width: 100%">
  34. <el-table-column label="商品信息" min-width="300">
  35. <template #default="{ row }">
  36. <div class="product-cell">
  37. <div class="product-image">
  38. <el-image :src="row.image" fit="contain"
  39. ><template #error
  40. ><div class="image-placeholder">
  41. <el-icon :size="30" color="#ccc"><Picture /></el-icon></div></template
  42. ></el-image>
  43. </div>
  44. <div class="product-info">
  45. <div class="product-name">{{ row.name }}</div>
  46. <div class="product-spec">{{ row.spec1 }} {{ row.spec2 }}</div>
  47. </div>
  48. </div>
  49. </template>
  50. </el-table-column>
  51. <el-table-column prop="price" label="单价" width="100" align="center"
  52. ><template #default="{ row }">¥{{ row.price }}</template></el-table-column
  53. >
  54. <el-table-column label="数量" width="150" align="center"
  55. ><template #default="{ row }"><el-input-number v-model="row.quantity" :min="1" size="small" :controls="false" disabled /></template
  56. ></el-table-column>
  57. <el-table-column prop="subtotal" label="小计" width="100" align="center"
  58. ><template #default="{ row }">¥{{ row.subtotal }}</template></el-table-column
  59. >
  60. <!-- <el-table-column label="操作" width="80" align="center"
  61. ><template #default><el-button type="danger" link size="small">清除</el-button></template></el-table-column
  62. > -->
  63. </el-table>
  64. <div class="product-summary">
  65. 共{{ productList.length }}件商品 运费:¥{{ orderInfo.freight }} 共计<span class="total-price">¥{{ orderInfo.totalAmount }}</span>
  66. </div>
  67. </div>
  68. <!-- 收货地址 -->
  69. <div class="section">
  70. <div class="section-title"><i class="title-bar"></i>收货地址</div>
  71. <div class="address-card">
  72. <div class="address-detail">{{ orderInfo.address }}</div>
  73. <div class="address-name">{{ orderInfo.receiverName }}</div>
  74. <div class="address-phone">{{ orderInfo.receiverPhone }}</div>
  75. </div>
  76. </div>
  77. <!-- 其他信息 -->
  78. <div class="section">
  79. <div class="section-title"><i class="title-bar"></i>其他信息</div>
  80. <div class="info-table">
  81. <div class="info-row">
  82. <span class="info-label">配送时间</span><span class="info-value">{{ orderInfo.deliveryTime }}</span>
  83. </div>
  84. <div class="info-row">
  85. <span class="info-label">采购事由</span><span class="info-value">{{ orderInfo.purchaseReason || '-' }}</span>
  86. </div>
  87. <div class="info-row">
  88. <span class="info-label">费用类型</span><span class="info-value">{{ orderInfo.costType || '-' }}</span>
  89. </div>
  90. <div class="info-row">
  91. <span class="info-label">订单备注</span><span class="info-value">{{ orderInfo.remark || '-' }}</span>
  92. </div>
  93. </div>
  94. </div>
  95. <!-- 发票信息 -->
  96. <div class="section">
  97. <div class="section-title"><i class="title-bar"></i>发票信息</div>
  98. <div class="info-table">
  99. <div class="info-row">
  100. <span class="info-label">发票类型</span><span class="info-value">{{ invoiceInfo.type || '-' }}</span>
  101. </div>
  102. <div class="info-row">
  103. <span class="info-label">发票抬头</span><span class="info-value">{{ invoiceInfo.title || '-' }}</span>
  104. </div>
  105. <div class="info-row">
  106. <span class="info-label">纳税人识别号</span><span class="info-value">{{ invoiceInfo.taxNo || '-' }}</span>
  107. </div>
  108. <div class="info-row">
  109. <span class="info-label">注册地址</span><span class="info-value">{{ invoiceInfo.registerAddress || '-' }}</span>
  110. </div>
  111. <div class="info-row">
  112. <span class="info-label">注册电话</span><span class="info-value">{{ invoiceInfo.registerPhone || '-' }}</span>
  113. </div>
  114. <div class="info-row">
  115. <span class="info-label">开户银行</span><span class="info-value">{{ invoiceInfo.bankName || '-' }}</span>
  116. </div>
  117. <div class="info-row">
  118. <span class="info-label">银行账号</span><span class="info-value">{{ invoiceInfo.bankAccount || '-' }}</span>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. </div>
  124. </template>
  125. <script setup lang="ts">
  126. import { ref, reactive, onMounted } from 'vue';
  127. import { useRouter, useRoute } from 'vue-router';
  128. import { ArrowLeft, Document, User, CircleCheck, Picture } from '@element-plus/icons-vue';
  129. import { getOrderInfo, getOrderProducts, getOrderFlowNodes } from '@/api/pc/enterprise/order';
  130. import { getAddressInfo } from '@/api/pc/enterprise/address';
  131. import { getInvoiceList } from '@/api/pc/enterprise/invoice';
  132. import { getContactInfo } from '@/api/pc/organization/index';
  133. import { ElMessage } from 'element-plus';
  134. import type { OrderCustomerFlowNodeLink } from '@/api/pc/enterprise/orderTypes';
  135. const router = useRouter();
  136. const route = useRoute();
  137. // 格式化时间为 "2026/3/17 上午10:49" 格式
  138. const formatTime = (timeStr: string): string => {
  139. if (!timeStr) return '';
  140. const date = new Date(timeStr);
  141. if (isNaN(date.getTime())) return timeStr;
  142. return date.toLocaleString('zh-CN', {
  143. year: 'numeric',
  144. month: 'numeric',
  145. day: 'numeric',
  146. hour: 'numeric',
  147. minute: '2-digit',
  148. hour12: true
  149. });
  150. };
  151. const orderId = ref<any>(0);
  152. const currentStep = ref(1);
  153. const loading = ref(false);
  154. const progressSteps = ref<{ title: string; icon: any; desc: string; time: string; reviewStatus?: number }[]>([
  155. { title: '提交订单', icon: Document, desc: '订单已提交', time: '', reviewStatus: 2 },
  156. { title: '完成', icon: CircleCheck, desc: '交易完成', time: '', reviewStatus: 0 }
  157. ]);
  158. // 订单时间信息(用于流程节点时间显示)
  159. const orderTimeInfo = reactive({
  160. createTime: '', // 订单创建时间(提交订单节点)
  161. updateTime: '' // 订单更新时间(完成节点)
  162. });
  163. // 根据 handlerId(逗号分隔的多个ID)解析审批人名称
  164. // 单人:返回姓名;多人:返回"xx或xx审核"
  165. const resolveHandlerName = async (handlerId: string): Promise<string> => {
  166. if (!handlerId) return '';
  167. const ids = handlerId.split(',').map((s) => s.trim()).filter(Boolean);
  168. if (ids.length === 0) return '';
  169. try {
  170. const results = await Promise.all(
  171. ids.map((id) => getContactInfo(String(id)).catch(() => null))
  172. );
  173. const names = results
  174. .map((res: any) => res?.data?.contactName || '')
  175. .filter(Boolean);
  176. if (names.length === 0) return '';
  177. if (names.length === 1) return names[0];
  178. return names.join('或') + '审核';
  179. } catch {
  180. return '';
  181. }
  182. };
  183. // 加载审批流程节点
  184. const loadFlowNodes = async () => {
  185. try {
  186. const res = await getOrderFlowNodes(orderId.value) as any;
  187. if (res.code === 200) {
  188. const apiNodes: OrderCustomerFlowNodeLink[] = res.data || [];
  189. // 提交订单节点:显示订单的 createTime
  190. const steps: { title: string; icon: any; desc: string; time: string; reviewStatus?: number }[] = [
  191. { title: '提交订单', icon: Document, desc: '订单已提交', time: formatTime(orderTimeInfo.createTime), reviewStatus: 2 }
  192. ];
  193. // 并行解析所有节点的审批人名称
  194. const handlerNames = await Promise.all(
  195. apiNodes.map((node) => resolveHandlerName(node.handlerId || node.handlerName || ''))
  196. );
  197. apiNodes.forEach((node, idx) => {
  198. steps.push({
  199. title: node.nodeName || '审批',
  200. icon: User,
  201. desc: handlerNames[idx] || '',
  202. time: formatTime(node.updateTime || ''),
  203. reviewStatus: node.reviewStatus ?? 0
  204. });
  205. });
  206. // 完成节点:显示订单的 updateTime
  207. steps.push({ title: '完成', icon: CircleCheck, desc: '交易完成', time: formatTime(orderTimeInfo.updateTime), reviewStatus: 0 });
  208. progressSteps.value = steps;
  209. // 计算当前步骤:找最后一个已处理节点的索引(reviewStatus > 0)
  210. let lastActiveIndex = 0;
  211. steps.forEach((step, idx) => {
  212. if (step.reviewStatus && step.reviewStatus > 0) {
  213. lastActiveIndex = idx;
  214. }
  215. });
  216. // 若所有中间节点均已完成(reviewStatus === 2),则激活结束节点
  217. const middleNodes = steps.slice(1, steps.length - 1);
  218. if (middleNodes.length > 0 && middleNodes.every((s) => s.reviewStatus === 2)) {
  219. lastActiveIndex = steps.length - 1;
  220. }
  221. currentStep.value = lastActiveIndex;
  222. }
  223. } catch (error) {
  224. console.error('加载流程节点失败', error);
  225. }
  226. };
  227. const productList = ref<any[]>([]);
  228. const orderInfo = reactive({
  229. freight: '0.00',
  230. totalAmount: '0.00',
  231. address: '',
  232. receiverName: '',
  233. receiverPhone: '',
  234. deliveryTime: '',
  235. purchaseReason: '',
  236. costType: '',
  237. remark: ''
  238. });
  239. const invoiceInfo = reactive({
  240. type: '',
  241. title: '',
  242. taxNo: '',
  243. registerAddress: '',
  244. registerPhone: '',
  245. bankName: '',
  246. bankAccount: ''
  247. });
  248. // 加载订单详情
  249. const loadOrderDetail = async () => {
  250. try {
  251. loading.value = true;
  252. const res = await getOrderInfo(orderId.value);
  253. if (res.code === 200 && res.data) {
  254. const order = res.data;
  255. // 映射订单信息
  256. orderInfo.freight = order.shippingFee || '0.00';
  257. orderInfo.totalAmount = order.payableAmount || '0.00';
  258. orderInfo.deliveryTime = order.expectedDeliveryTime || '';
  259. orderInfo.purchaseReason = order.purchaseReason || '';
  260. orderInfo.remark = order.remark || '';
  261. // 保存订单时间信息(用于流程节点时间显示)
  262. orderTimeInfo.createTime = order.createTime || '';
  263. orderTimeInfo.updateTime = order.updateTime || '';
  264. // 获取商品信息
  265. const productsRes = await getOrderProducts([orderId.value]);
  266. if (productsRes.code === 200 && productsRes.rows) {
  267. productList.value = productsRes.rows.map((p: any) => ({
  268. id: p.id,
  269. name: p.productName || '',
  270. spec1: p.productUnit || '',
  271. spec2: p.productNo || '',
  272. price: p.orderPrice || '0.00',
  273. quantity: p.orderQuantity || 1,
  274. subtotal: (parseFloat(p.orderPrice || '0') * (p.orderQuantity || 1)).toFixed(2),
  275. image: p.productImage || ''
  276. }));
  277. }
  278. // 获取收货地址信息
  279. if (order.shippingAddressId) {
  280. const addressRes = await getAddressInfo(order.shippingAddressId);
  281. if (addressRes.code === 200 && addressRes.data) {
  282. orderInfo.address = addressRes.data.address || '';
  283. orderInfo.receiverName = addressRes.data.consignee || '';
  284. orderInfo.receiverPhone = addressRes.data.phone || '';
  285. }
  286. }
  287. // 获取发票信息
  288. const invoiceRes = await getInvoiceList({ pageNum: 1, pageSize: 1 });
  289. if (invoiceRes.code === 200 && invoiceRes.rows && invoiceRes.rows.length > 0) {
  290. const invoice = invoiceRes.rows[0];
  291. invoiceInfo.type = order.invoiceType || '';
  292. invoiceInfo.title = order.customerName || '';
  293. invoiceInfo.taxNo = invoice.taxId || '';
  294. invoiceInfo.registerAddress = invoice.address || '';
  295. invoiceInfo.registerPhone = invoice.phone || '';
  296. invoiceInfo.bankName = invoice.bankName || '';
  297. invoiceInfo.bankAccount = invoice.bankAccount || '';
  298. }
  299. }
  300. } catch (error) {
  301. ElMessage.error('加载订单详情失败');
  302. } finally {
  303. loading.value = false;
  304. }
  305. };
  306. onMounted(async () => {
  307. const paramId = route.query.orderId;
  308. // 直接使用字符串,不转换为数字,避免精度丢失
  309. orderId.value = paramId as string;
  310. if (orderId.value) {
  311. // 先加载订单详情(获取 createTime/updateTime),再加载流程节点
  312. await loadOrderDetail();
  313. loadFlowNodes();
  314. } else {
  315. console.error('订单ID无效,无法加载订单详情');
  316. }
  317. });
  318. const handleBack = () => {
  319. router.push('/order/orderManage');
  320. };
  321. </script>
  322. <style scoped lang="scss">
  323. .order-detail-container {
  324. flex: 1;
  325. background: #f5f5f5;
  326. min-height: 100%;
  327. }
  328. .page-header {
  329. background: #fff;
  330. padding: 15px 20px;
  331. display: flex;
  332. align-items: center;
  333. gap: 10px;
  334. border-bottom: 1px solid #eee;
  335. .page-title {
  336. font-size: 16px;
  337. font-weight: bold;
  338. color: #333;
  339. }
  340. }
  341. .page-content {
  342. padding: 20px;
  343. }
  344. .progress-section {
  345. background: #fff;
  346. border-radius: 8px;
  347. padding: 30px 50px;
  348. margin-bottom: 15px;
  349. .progress-steps {
  350. display: flex;
  351. justify-content: space-between;
  352. position: relative;
  353. .step-item {
  354. display: flex;
  355. flex-direction: column;
  356. align-items: center;
  357. position: relative;
  358. flex: 1;
  359. .step-icon {
  360. width: 50px;
  361. height: 50px;
  362. border-radius: 8px;
  363. background: #f5f5f5;
  364. display: flex;
  365. align-items: center;
  366. justify-content: center;
  367. color: #999;
  368. margin-bottom: 10px;
  369. position: relative;
  370. z-index: 1;
  371. }
  372. &.active .step-icon {
  373. background: #e60012;
  374. color: #fff;
  375. }
  376. &.current .step-icon::after {
  377. content: '';
  378. position: absolute;
  379. bottom: -5px;
  380. left: 50%;
  381. transform: translateX(-50%);
  382. width: 8px;
  383. height: 8px;
  384. border-radius: 50%;
  385. background: #e60012;
  386. }
  387. .step-info {
  388. text-align: center;
  389. .step-title {
  390. font-size: 14px;
  391. color: #999;
  392. margin-bottom: 5px;
  393. }
  394. .step-desc {
  395. font-size: 12px;
  396. color: #666;
  397. margin-bottom: 3px;
  398. }
  399. .step-time {
  400. font-size: 12px;
  401. color: #999;
  402. }
  403. }
  404. &.active .step-info .step-title {
  405. color: #333;
  406. }
  407. .step-line {
  408. position: absolute;
  409. top: 25px;
  410. left: calc(50% + 30px);
  411. width: calc(100% - 60px);
  412. height: 2px;
  413. background: #eee;
  414. &.active {
  415. background: #e60012;
  416. }
  417. }
  418. &:last-child .step-line {
  419. display: none;
  420. }
  421. }
  422. }
  423. }
  424. .section {
  425. background: #fff;
  426. border-radius: 8px;
  427. padding: 20px;
  428. margin-bottom: 15px;
  429. .section-title {
  430. font-size: 16px;
  431. font-weight: bold;
  432. display: flex;
  433. align-items: center;
  434. gap: 8px;
  435. margin-bottom: 15px;
  436. }
  437. }
  438. .title-bar {
  439. display: inline-block;
  440. width: 3px;
  441. height: 16px;
  442. background: #e60012;
  443. border-radius: 2px;
  444. }
  445. .product-cell {
  446. display: flex;
  447. align-items: center;
  448. gap: 15px;
  449. .product-image {
  450. width: 80px;
  451. height: 80px;
  452. background: #f5f5f5;
  453. border-radius: 4px;
  454. overflow: hidden;
  455. flex-shrink: 0;
  456. .el-image {
  457. width: 100%;
  458. height: 100%;
  459. }
  460. .image-placeholder {
  461. width: 100%;
  462. height: 100%;
  463. display: flex;
  464. align-items: center;
  465. justify-content: center;
  466. }
  467. }
  468. .product-info {
  469. .product-name {
  470. font-size: 14px;
  471. color: #333;
  472. margin-bottom: 5px;
  473. line-height: 1.4;
  474. }
  475. .product-spec {
  476. font-size: 12px;
  477. color: #999;
  478. }
  479. }
  480. }
  481. .product-summary {
  482. text-align: right;
  483. padding: 15px 0;
  484. font-size: 14px;
  485. color: #666;
  486. .total-price {
  487. font-size: 18px;
  488. font-weight: bold;
  489. color: #e60012;
  490. margin-left: 5px;
  491. }
  492. }
  493. .address-card {
  494. padding: 15px;
  495. border: 1px solid #eee;
  496. border-radius: 4px;
  497. .address-detail {
  498. font-size: 14px;
  499. color: #333;
  500. margin-bottom: 8px;
  501. }
  502. .address-name {
  503. font-size: 14px;
  504. color: #666;
  505. margin-bottom: 5px;
  506. }
  507. .address-phone {
  508. font-size: 14px;
  509. color: #666;
  510. }
  511. }
  512. .info-table {
  513. .info-row {
  514. display: flex;
  515. padding: 10px 0;
  516. border-bottom: 1px solid #f5f5f5;
  517. &:last-child {
  518. border-bottom: none;
  519. }
  520. .info-label {
  521. width: 100px;
  522. font-size: 14px;
  523. color: #999;
  524. flex-shrink: 0;
  525. }
  526. .info-value {
  527. flex: 1;
  528. font-size: 14px;
  529. color: #333;
  530. }
  531. }
  532. }
  533. </style>