index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <template>
  2. <div class="order-manage-container">
  3. <div class="page-title"><i class="title-bar"></i><span>历史购买</span></div>
  4. <!-- 搜索栏 -->
  5. <div class="flex-row-between">
  6. <div class="flex-row-start">
  7. <el-input v-model="queryParams.keyword" placeholder="搜索订单号" style="width: 260px" clearable @keyup.enter="handleQuery">
  8. <template #prefix
  9. ><el-icon><Search /></el-icon
  10. ></template>
  11. </el-input>
  12. <el-date-picker
  13. v-model="queryParams.dateRange"
  14. type="daterange"
  15. range-separator="—"
  16. start-placeholder="开始日期"
  17. end-placeholder="结束日期"
  18. style="width: 260px; margin-left: 10px"
  19. />
  20. </div>
  21. <div>
  22. <el-button type="primary" @click="handleQuery">搜索</el-button>
  23. <el-button @click="handleReset">重置</el-button>
  24. </div>
  25. </div>
  26. <div class="flex-row-between" style="margin-top: 10px">
  27. <div class="flex-row-start">
  28. <el-tree-select
  29. v-model="queryParams.department"
  30. style="width: 160px"
  31. :data="deptList"
  32. :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
  33. value-key="deptId"
  34. placeholder="下单部门"
  35. check-strictly
  36. :render-after-expand="false"
  37. clearable
  38. >
  39. </el-tree-select>
  40. <el-select v-model="queryParams.status" placeholder="状态" style="width: 160px; margin-left: 10px" clearable>
  41. <el-option v-for="dict in order_status" :key="dict.value" :label="dict.label" :value="dict.value" />
  42. </el-select>
  43. <el-select v-model="queryParams.payType" placeholder="支付方式" style="width: 160px; margin-left: 10px" clearable
  44. ><el-option v-for="dict in pay_method" :key="dict.value" :label="dict.label" :value="dict.value" />
  45. </el-select>
  46. </div>
  47. <div class="flex-row-start"></div>
  48. </div>
  49. <!-- Tab切换 -->
  50. <div class="tab-bar">
  51. <div class="tab-left">
  52. <div v-for="tab in statusTabs" :key="tab.key" :class="['tab-item', { active: activeTab === tab.key }]" @click="activeTab = tab.key">
  53. <el-icon><component :is="tab.icon" /></el-icon><span>{{ tab.label }}</span>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- 订单列表 -->
  58. <div class="order-list">
  59. <div v-for="order in orderList" :key="order.id" class="order-card">
  60. <div class="order-header flex-row-between">
  61. <div class="flex-row-start" style="gap: 0 15px">
  62. <el-checkbox style="margin: 2px 0px 0px 0" v-model="order.checked" @change="handleOrderCheck" />
  63. <div>{{ formatOrderTime(order.orderTime) }}</div>
  64. <div>订单号:{{ order.orderNo }}</div>
  65. <div>下单人:{{ order.orderPerson }}</div>
  66. <div>部门:{{ order.department }}</div>
  67. </div>
  68. <div class="flex-row-start">
  69. <div class="expand-btn" @click="handleAddCart(order)">批量加入购物车</div>
  70. <div class="open-btn" v-if="order.products && order.products.length > 5" @click="handleExpand(order)">
  71. <span style="margin-right: 5px"> {{ order.expanded ? '收起' : '展开' }}</span>
  72. <el-icon v-if="order.expanded"><ArrowUp /></el-icon>
  73. <el-icon v-else><ArrowDown /></el-icon>
  74. </div>
  75. </div>
  76. </div>
  77. <div v-if="order.countdown" class="countdown-bar">订单锁定剩余时间:{{ order.countdown }}</div>
  78. <div class="product-list">
  79. <div v-for="(item, itemIndex) in order.expanded ? order.products : order.products.slice(0, 5)" :key="itemIndex" class="product-row">
  80. <div class="product-info-cell">
  81. <div class="product-image">
  82. <el-image :src="item.image" fit="contain"
  83. ><template #error
  84. ><div class="image-placeholder">
  85. <el-icon :size="30" color="#ccc"><Picture /></el-icon></div></template
  86. ></el-image>
  87. </div>
  88. <div class="product-detail">
  89. <div class="product-name ellipsis">{{ item.name }}</div>
  90. <div class="product-spec">{{ item.spec1 }} | {{ item.spec2 }}</div>
  91. <div class="product-price">¥{{ item.price }}</div>
  92. </div>
  93. <div class="product-quantity">x{{ item.quantity }}</div>
  94. </div>
  95. <div class="amount-cell" v-if="itemIndex === 0">
  96. <div class="amount-info">
  97. <span class="label">支付款:</span><span class="value highlight">¥{{ order.payAmount }}</span>
  98. </div>
  99. <div class="amount-info">
  100. <span class="label">含运费:</span><span class="value">¥{{ order.freight }}</span>
  101. </div>
  102. </div>
  103. <div v-else style="width: 200px"></div>
  104. <div class="status-cell">
  105. <el-button size="small" @click="handleAddCart(order)">加入购物车</el-button>
  106. </div>
  107. </div>
  108. </div>
  109. <!-- 显示更多商品提示 -->
  110. <div
  111. v-if="!order.expanded && order.products && order.products.length > 5"
  112. class="more-products-hint"
  113. @click="handleExpand(order)"
  114. style="cursor: pointer"
  115. >
  116. 该订单共 {{ order.products.length }} 件商品,点击展开查看全部
  117. </div>
  118. </div>
  119. <el-empty v-if="orderList.length === 0" description="暂无订单" />
  120. </div>
  121. <!-- 分页 -->
  122. <TablePagination v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="fetchOrderList" />
  123. </div>
  124. </template>
  125. <script setup lang="ts">
  126. import { ref, reactive, computed, onMounted, watch } from 'vue';
  127. import { useRouter } from 'vue-router';
  128. import { Search, ArrowDown, Document, Clock, Box, CircleCheck, CircleClose, Picture } from '@element-plus/icons-vue';
  129. import { TablePagination } from '@/components';
  130. import { getOrderList, getOrderProducts } from '@/api/pc/enterprise/order';
  131. import type { OrderMain } from '@/api/pc/enterprise/orderTypes';
  132. import { ElMessage } from 'element-plus';
  133. import { getDeptTree } from '@/api/pc/organization';
  134. import { DeptInfo } from '@/api/pc/organization/types';
  135. import { addProductShoppingCart } from '@/api/goods/index';
  136. import { parseTime } from '@/utils/ruoyi';
  137. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  138. const { order_status, pay_method } = toRefs<any>(proxy?.useDict('order_status', 'pay_method'));
  139. const activeTab = ref('all');
  140. const selectAll = ref(false);
  141. const loading = ref(false);
  142. const deptList = ref([]);
  143. const statusTabs = [
  144. { key: 'all', label: '全部订单', icon: Document },
  145. { key: 'preOrder', label: '预下单', icon: Clock },
  146. { key: 'shipping', label: '待收货', icon: Box },
  147. { key: 'completed', label: '已完成', icon: CircleCheck },
  148. { key: 'cancelled', label: '已取消', icon: CircleClose }
  149. ];
  150. // 格式化时间
  151. const formatOrderTime = (timeStr: string) => {
  152. if (!timeStr) return '';
  153. try {
  154. // 处理后端返回的中文时间格式 "2026/3/13 上午3:35"
  155. const date = new Date(timeStr);
  156. if (isNaN(date.getTime())) return timeStr;
  157. return parseTime(date, '{y}-{m}-{d} {h}:{i}:{s}');
  158. } catch (e) {
  159. return timeStr;
  160. }
  161. };
  162. // 监听标签页切换,重置页码并重新获取数据
  163. watch(activeTab, (newTab) => {
  164. queryParams.pageNum = 1;
  165. // 根据标签页设置后端查询的状态参数
  166. if (newTab === 'all') {
  167. queryParams.status = '';
  168. } else {
  169. // 将前端标签页key映射回后端状态值
  170. const tabToStatusMap: Record<string, string> = {
  171. 'preOrder': '0', // 待支付
  172. 'shipping': '4', // 待发货,部分发货,发货完成
  173. 'completed': '5', // 已完成
  174. 'cancelled': '7' // 已关闭,已取消
  175. };
  176. queryParams.status = tabToStatusMap[newTab] || '';
  177. }
  178. fetchOrderList();
  179. });
  180. const queryParams = reactive({ keyword: '', dateRange: [], department: '', status: '', payType: '', pageNum: 1, pageSize: 5 });
  181. const total = ref(0);
  182. const allOrders = ref<any[]>([]);
  183. // 将后端状态映射为前端标签页key
  184. const mapBackendStatusToTabKey = (backendStatus: string) => {
  185. const statusMap: Record<string, string> = {
  186. '0': 'preOrder', // 待支付
  187. '1': 'preOrder', // 待确认
  188. '2': 'shipping', // 待发货
  189. '3': 'shipping', // 部分发货
  190. '4': 'shipping', // 发货完成
  191. '5': 'completed', // 已完成
  192. '6': 'cancelled', // 已关闭
  193. '7': 'cancelled' // 已取消
  194. };
  195. return statusMap[backendStatus] || backendStatus;
  196. };
  197. // 根据订单状态获取状态文本
  198. const getStatusText = (status: string) => {
  199. const statusMap: Record<string, string> = {
  200. '0': '待支付',
  201. '1': '待确认',
  202. '2': '待发货',
  203. '3': '部分发货',
  204. '4': '发货完成',
  205. '5': '已完成',
  206. '6': '已关闭',
  207. '7': '已取消'
  208. };
  209. return statusMap[status] || status;
  210. };
  211. // 加载部门树
  212. const loadDeptTree = async () => {
  213. try {
  214. const res = await getDeptTree();
  215. if (res.code === 200 && res.data) {
  216. deptList.value = res.data;
  217. if (Array.isArray(res.data)) {
  218. const treeData = proxy?.handleTree<DeptInfo>(res.data, 'deptId', 'parentId');
  219. deptList.value = treeData || res.data;
  220. } else {
  221. deptList.value = [];
  222. }
  223. }
  224. } catch (error) {
  225. console.error('获取部门树失败:', error);
  226. ElMessage.error('获取部门树失败');
  227. }
  228. };
  229. /** 单个加入购物车 */
  230. const handleAddCart = async (order: any) => {
  231. const res = await getOrderProducts([order.id]);
  232. if (res.code === 200 && res.rows) {
  233. const products = res.rows.map((p: any) => ({
  234. productId: p.productId,
  235. productNum: p.orderQuantity || 0
  236. }));
  237. const promises = products.map((item) => addProductShoppingCart({ productId: item.productId, productNum: item.productNum }));
  238. Promise.all(promises).then(() => {
  239. ElMessage.success(`已将${products.length}件商品加入购物车`);
  240. });
  241. }
  242. // addProductShoppingCart({ productId: item.id, productNum: 1 }).then((res: any) => {
  243. // if (res.code == 200) {
  244. // ElMessage.success('已加入购物车');
  245. // }
  246. // });
  247. };
  248. const formatDate = (date: Date | string | number): string => {
  249. if (!date) return '';
  250. const d = new Date(date);
  251. const year = d.getFullYear();
  252. const month = String(d.getMonth() + 1).padStart(2, '0');
  253. const day = String(d.getDate()).padStart(2, '0');
  254. return `${year}-${month}-${day}`;
  255. };
  256. // 获取订单列表
  257. const fetchOrderList = async () => {
  258. loading.value = true;
  259. try {
  260. const params: any = {
  261. pageNum: queryParams.pageNum,
  262. pageSize: queryParams.pageSize
  263. };
  264. // 添加筛选条件
  265. if (queryParams.keyword) params.orderNo = queryParams.keyword;
  266. if (queryParams.department) params.department = queryParams.department;
  267. if (queryParams.status) params.orderStatuses = queryParams.status; // 使用orderStatuses支持多状态查询
  268. if (queryParams.payType) params.payType = queryParams.payType;
  269. if (queryParams.dateRange && queryParams.dateRange.length == 2) {
  270. // params.beginTime = queryParams.dateRange[0];
  271. // params.endTime = queryParams.dateRange[1];
  272. params.params = {
  273. beginTime: formatDate(queryParams.dateRange[0]), // 将日期转换为字符串格式
  274. endTime: formatDate(queryParams.dateRange[1]) // 将日期转换为字符串格式
  275. };
  276. }
  277. console.log('发送到后端的参数:', params);
  278. const res = await getOrderList(params);
  279. console.log('后端返回的数据:', res);
  280. if (res.code === 200) {
  281. // 调试:打印后端返回的第一条订单数据
  282. if (res.rows && res.rows.length > 0) {
  283. console.log('后端���回的订单状态值:', res.rows[0].orderStatus);
  284. console.log('完整的订单数据:', res.rows[0]);
  285. }
  286. // 获取订单商品数据
  287. const orderProductsMap: Record<number, any[]> = {};
  288. const orderIds = (res.rows || []).map((o: any) => o.id);
  289. if (orderIds.length > 0) {
  290. try {
  291. const prodRes = await getOrderProducts(orderIds);
  292. if (prodRes.code === 200 && prodRes.rows) {
  293. prodRes.rows.forEach((p: any) => {
  294. if (!orderProductsMap[p.orderId]) {
  295. orderProductsMap[p.orderId] = [];
  296. }
  297. orderProductsMap[p.orderId].push({
  298. id: p.id,
  299. productId: p.productId,
  300. image: p.productImage || '',
  301. name: p.productName || '',
  302. spec1: p.productUnit || '',
  303. spec2: p.productNo || '',
  304. price: p.orderPrice || 0,
  305. quantity: p.orderQuantity || 0
  306. });
  307. });
  308. }
  309. } catch (e) {
  310. console.error('获取商品列表失败:', e);
  311. }
  312. }
  313. // 将后端数据转换为前端需要的格式
  314. allOrders.value = (res.rows || []).map((order: OrderMain) => ({
  315. id: order.id,
  316. orderTime: order.createTime,
  317. orderNo: order.orderNo,
  318. orderPerson: order.customerName, // 需要关联用户信息
  319. department: order.createDeptName, // 需要关联部门信息
  320. payAmount: order.payableAmount || 0,
  321. freight: order.shippingFee || 0,
  322. status: mapBackendStatusToTabKey(order.orderStatus || ''),
  323. statusText: getStatusText(order.orderStatus || ''),
  324. countdown: '',
  325. auditStatus: order.checkStatus,
  326. fileCount: 0,
  327. checked: false,
  328. expanded: false,
  329. products: orderProductsMap[order.id] || [] // 商品信息已加载
  330. }));
  331. // 调试:打印转换后的订单状态
  332. console.log(
  333. '转换后的订单状态分布:',
  334. allOrders.value.map((o) => o.status)
  335. );
  336. total.value = res.total || 0;
  337. }
  338. } catch (error) {
  339. console.error('获取订单列表失败:', error);
  340. } finally {
  341. loading.value = false;
  342. }
  343. };
  344. const orderList = computed(() => allOrders.value);
  345. const handleExpand = async (order: any) => {
  346. const orderIndex = allOrders.value.findIndex((o) => o.id === order.id);
  347. if (orderIndex === -1) return;
  348. // 替换整个数组以触发响应式更新
  349. allOrders.value = allOrders.value.map((o, i) => (i === orderIndex ? { ...o, expanded: !o.expanded } : o));
  350. };
  351. const handleOrderCheck = () => {
  352. selectAll.value = orderList.value.every((order) => order.checked);
  353. };
  354. const handleQuery = () => {
  355. queryParams.pageNum = 1;
  356. fetchOrderList();
  357. };
  358. const handleReset = () => {
  359. queryParams.keyword = '';
  360. queryParams.dateRange = null;
  361. queryParams.department = '';
  362. queryParams.payType = '';
  363. queryParams.pageNum = 1;
  364. fetchOrderList();
  365. };
  366. // 页面加载时获取订单列表
  367. onMounted(() => {
  368. loadDeptTree();
  369. fetchOrderList();
  370. });
  371. </script>
  372. <style scoped lang="scss">
  373. .order-manage-container {
  374. padding: 20px;
  375. background: #fff;
  376. min-height: 100%;
  377. width: 100%;
  378. display: flex;
  379. flex-direction: column;
  380. margin-bottom: 20px;
  381. // max-height: calc(100vh - 120px); // 减去顶部导航栏和其他元素高度
  382. }
  383. .page-title {
  384. font-size: 16px;
  385. font-weight: bold;
  386. display: flex;
  387. align-items: center;
  388. gap: 8px;
  389. margin-bottom: 20px;
  390. }
  391. .title-bar {
  392. display: inline-block;
  393. width: 3px;
  394. height: 16px;
  395. background: #e60012;
  396. border-radius: 2px;
  397. }
  398. .tab-bar {
  399. display: flex;
  400. justify-content: space-between;
  401. align-items: center;
  402. border-bottom: 1px solid #eee;
  403. margin-bottom: 15px;
  404. .tab-left {
  405. display: flex;
  406. gap: 25px;
  407. }
  408. .tab-item {
  409. display: flex;
  410. align-items: center;
  411. gap: 5px;
  412. padding: 12px 0;
  413. cursor: pointer;
  414. color: #666;
  415. font-size: 14px;
  416. border-bottom: 2px solid transparent;
  417. margin-bottom: -1px;
  418. &:hover,
  419. &.active {
  420. color: #333;
  421. }
  422. &.active {
  423. border-bottom-color: #e60012;
  424. }
  425. }
  426. }
  427. .order-list {
  428. // flex: 1;
  429. // overflow-y: auto;
  430. margin-bottom: 15px;
  431. .order-card {
  432. border: 1px solid #eee;
  433. border-radius: 4px;
  434. margin-bottom: 15px;
  435. overflow: hidden;
  436. .order-header {
  437. padding: 12px 15px;
  438. background: #f9f9f9;
  439. border-bottom: 1px solid #eee;
  440. font-size: 14px;
  441. color: #101828;
  442. .expand-btn {
  443. color: #165dff;
  444. margin-left: 10px;
  445. cursor: pointer;
  446. }
  447. .open-btn {
  448. color: #364153;
  449. margin-left: 10px;
  450. cursor: pointer;
  451. }
  452. }
  453. .countdown-bar {
  454. background: #fff5e6;
  455. color: #e6a23c;
  456. padding: 8px 15px;
  457. font-size: 13px;
  458. border-bottom: 1px solid #eee;
  459. }
  460. .product-list {
  461. .product-row {
  462. display: flex;
  463. // border-bottom: 1px solid #f5f5f5;
  464. &:last-child {
  465. border-bottom: none;
  466. }
  467. }
  468. .product-cell {
  469. padding: 15px;
  470. display: flex;
  471. flex-direction: column;
  472. justify-content: center;
  473. }
  474. .product-info-cell {
  475. display: flex;
  476. flex: 1;
  477. padding: 15px;
  478. gap: 15px;
  479. .product-image {
  480. width: 80px;
  481. height: 80px;
  482. background: #f5f5f5;
  483. border-radius: 4px;
  484. overflow: hidden;
  485. flex-shrink: 0;
  486. .el-image {
  487. width: 100%;
  488. height: 100%;
  489. }
  490. .image-placeholder {
  491. width: 100%;
  492. height: 100%;
  493. display: flex;
  494. align-items: center;
  495. justify-content: center;
  496. }
  497. }
  498. .product-detail {
  499. flex: 1;
  500. width: 0;
  501. .product-name {
  502. font-size: 14px;
  503. color: #000000;
  504. margin-bottom: 5px;
  505. }
  506. .product-spec {
  507. font-size: 12px;
  508. color: #999;
  509. margin-bottom: 10px;
  510. }
  511. .product-price {
  512. font-size: 16px;
  513. font-weight: bold;
  514. color: #e60012;
  515. margin-bottom: 5px;
  516. }
  517. }
  518. .product-quantity {
  519. font-size: 13px;
  520. color: #666;
  521. }
  522. }
  523. .amount-cell {
  524. width: 200px;
  525. padding: 15px;
  526. .amount-info {
  527. margin-bottom: 5px;
  528. .label {
  529. font-size: 12px;
  530. color: #999;
  531. margin-right: 4px;
  532. }
  533. .value {
  534. font-size: 14px;
  535. color: #333;
  536. &.highlight {
  537. font-size: 16px;
  538. font-weight: bold;
  539. color: #e60012;
  540. }
  541. }
  542. }
  543. }
  544. .status-cell {
  545. width: 140px;
  546. padding: 15px;
  547. display: flex;
  548. align-items: flex-start;
  549. justify-content: flex-end;
  550. gap: 10px;
  551. .status-text {
  552. font-size: 14px;
  553. font-weight: 500;
  554. }
  555. .audit-status {
  556. font-size: 14px;
  557. &.success {
  558. color: #e60012;
  559. }
  560. &.warning {
  561. color: #e6a23c;
  562. }
  563. &.danger {
  564. color: #e60012;
  565. }
  566. }
  567. }
  568. .action-cell {
  569. width: 100px;
  570. border-left: 1px solid #f5f5f5;
  571. align-items: flex-start;
  572. gap: 3px;
  573. }
  574. }
  575. .more-products-hint {
  576. padding: 10px 15px;
  577. background: #f5f5f5;
  578. font-size: 12px;
  579. color: #666;
  580. text-align: center;
  581. border-top: 1px solid #f0f0f0;
  582. }
  583. }
  584. }
  585. .bottom-bar {
  586. background: #fafafa;
  587. border: 1px solid #eee;
  588. border-radius: 4px;
  589. padding: 15px 20px;
  590. display: flex;
  591. justify-content: space-between;
  592. align-items: center;
  593. flex-shrink: 0;
  594. .bottom-right {
  595. display: flex;
  596. align-items: center;
  597. gap: 15px;
  598. .selected-info {
  599. font-size: 14px;
  600. color: #666;
  601. em {
  602. color: #e60012;
  603. font-style: normal;
  604. font-weight: bold;
  605. }
  606. }
  607. }
  608. }
  609. .evaluate-product {
  610. display: flex;
  611. align-items: center;
  612. gap: 15px;
  613. padding: 15px;
  614. background: #f9f9f9;
  615. border-radius: 4px;
  616. margin-bottom: 20px;
  617. .product-image {
  618. width: 60px;
  619. height: 60px;
  620. background: #fff;
  621. border-radius: 4px;
  622. overflow: hidden;
  623. flex-shrink: 0;
  624. .el-image {
  625. width: 100%;
  626. height: 100%;
  627. }
  628. .image-placeholder {
  629. width: 100%;
  630. height: 100%;
  631. display: flex;
  632. align-items: center;
  633. justify-content: center;
  634. }
  635. }
  636. .product-info {
  637. .product-name {
  638. font-size: 14px;
  639. color: #333;
  640. margin-bottom: 5px;
  641. }
  642. .product-spec {
  643. font-size: 12px;
  644. color: #999;
  645. }
  646. }
  647. }
  648. :deep(.table-pagination) {
  649. flex-shrink: 0;
  650. margin-top: 15px;
  651. }
  652. </style>