index.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  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="search-bar">
  6. <el-input v-model="queryParams.keyword" placeholder="搜索订单号" style="width: 200px" clearable @keyup.enter="handleQuery">
  7. <template #prefix
  8. ><el-icon><Search /></el-icon
  9. ></template>
  10. </el-input>
  11. <el-date-picker
  12. v-model="queryParams.dateRange"
  13. type="daterange"
  14. range-separator="—"
  15. start-placeholder="开始日期"
  16. end-placeholder="结束日期"
  17. style="width: 240px"
  18. />
  19. <el-button type="primary" @click="handleQuery">搜索</el-button>
  20. <el-button @click="handleReset">重置</el-button>
  21. </div>
  22. <!-- 筛选栏 -->
  23. <div class="filter-bar">
  24. <span class="filter-label">下单部门</span>
  25. <el-tree-select
  26. v-model="queryParams.department"
  27. style="width: 100px"
  28. :data="deptList"
  29. :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
  30. value-key="deptId"
  31. placeholder="请选择"
  32. check-strictly
  33. :render-after-expand="false"
  34. clearable
  35. >
  36. </el-tree-select>
  37. <span class="filter-label">状态</span>
  38. <el-select v-model="queryParams.status" placeholder="请选择" style="width: 100px" clearable>
  39. <el-option v-for="dict in order_status" :key="dict.value" :label="dict.label" :value="dict.value" />
  40. </el-select>
  41. <span class="filter-label">支付方式</span>
  42. <el-select v-model="queryParams.payType" placeholder="请选择" style="width: 100px" clearable
  43. ><el-option v-for="dict in pay_method" :key="dict.value" :label="dict.label" :value="dict.value"
  44. /></el-select>
  45. <div class="filter-right">
  46. <el-dropdown
  47. ><el-button
  48. >订单导出 <el-icon><ArrowDown /></el-icon></el-button
  49. ><template #dropdown
  50. ><el-dropdown-menu><el-dropdown-item>导出Excel</el-dropdown-item><el-dropdown-item>导出PDF</el-dropdown-item></el-dropdown-menu></template
  51. ></el-dropdown
  52. >
  53. <el-dropdown
  54. ><el-button
  55. >订单打印 <el-icon><ArrowDown /></el-icon></el-button
  56. ><template #dropdown
  57. ><el-dropdown-menu
  58. ><el-dropdown-item>打印订单</el-dropdown-item><el-dropdown-item>打印发货单</el-dropdown-item></el-dropdown-menu
  59. ></template
  60. ></el-dropdown
  61. >
  62. <el-button>下载电子签单</el-button>
  63. </div>
  64. </div>
  65. <!-- Tab切换 -->
  66. <div class="tab-bar">
  67. <div class="tab-left">
  68. <div v-for="tab in statusTabs" :key="tab.key" :class="['tab-item', { active: activeTab === tab.key }]" @click="activeTab = tab.key">
  69. <el-icon><component :is="tab.icon" /></el-icon><span>{{ tab.label }}</span>
  70. </div>
  71. </div>
  72. <el-button type="danger" link
  73. ><el-icon><Delete /></el-icon>订单回收站</el-button
  74. >
  75. </div>
  76. <!-- 订单列表 -->
  77. <div class="order-list">
  78. <div v-for="order in orderList" :key="order.id" class="order-card">
  79. <div class="order-header">
  80. <el-checkbox v-model="order.checked" @change="handleOrderCheck" />
  81. <span class="order-time">{{ order.orderTime }}</span>
  82. <span class="order-info">订单号:{{ order.orderNo }}</span>
  83. <span class="order-info">下单人:{{ order.orderPerson }}</span>
  84. <span class="order-info">部门:{{ order.department }}</span>
  85. <el-button
  86. class="expand-btn"
  87. v-for="action in getOrderActions(order)"
  88. :key="action"
  89. type="primary"
  90. link
  91. @click="handleAction(action, order)"
  92. >{{ action }}</el-button
  93. >
  94. <el-button type="primary" link class="expand-btn" @click="handleExpand(order)"
  95. >{{ order.expanded ? '收起' : '展开' }} <el-icon><ArrowDown /></el-icon
  96. ></el-button>
  97. </div>
  98. <div v-if="order.countdown" class="countdown-bar">订单锁定剩余时间:{{ order.countdown }}</div>
  99. <div class="product-list">
  100. <div v-for="(item, itemIndex) in order.expanded ? order.products : order.products.slice(0, 1)" :key="itemIndex" class="product-row">
  101. <div class="product-cell product-info-cell">
  102. <div class="product-image">
  103. <el-image :src="item.image" fit="contain"
  104. ><template #error
  105. ><div class="image-placeholder">
  106. <el-icon :size="30" color="#ccc"><Picture /></el-icon></div></template
  107. ></el-image>
  108. </div>
  109. <div class="product-detail">
  110. <div class="product-name">{{ item.name }}</div>
  111. <div class="product-spec">{{ item.spec1 }} {{ item.spec2 }}</div>
  112. <div class="product-price">¥{{ item.price }}</div>
  113. </div>
  114. <div class="product-quantity">x{{ item.quantity }}</div>
  115. </div>
  116. <div class="product-cell amount-cell" v-if="itemIndex === 0">
  117. <div class="amount-info">
  118. <span class="label">支付款</span><span class="value highlight">¥{{ order.payAmount }}</span>
  119. </div>
  120. <div class="amount-info">
  121. <span class="label">含运费:</span><span class="value">¥{{ order.freight }}</span>
  122. </div>
  123. </div>
  124. <div class="product-cell status-cell" v-if="itemIndex === 0">
  125. <span class="status-text" :style="{ color: getStatusColor(order.status) }">{{ order.statusText }}</span>
  126. <!-- <el-button type="primary" link size="small" @click="handleViewDetail(order)">查看订单轨迹</el-button> -->
  127. <template v-if="order.auditStatus"
  128. ><span :class="['audit-status', getAuditStatusClass(order.auditStatus)]">{{
  129. order.auditStatus == '0' ? '待审批' : order.auditStatus == '1' ? '审批通过' : '审批驳回'
  130. }}</span>
  131. <!-- <el-button type="primary" link size="small">查看审批流</el-button> -->
  132. </template>
  133. <el-button v-if="order.fileCount" type="primary" link size="small">审核文件({{ order.fileCount }})</el-button>
  134. </div>
  135. <!-- <div class="product-cell action-cell" v-if="itemIndex === 0">
  136. <el-button
  137. v-for="action in getOrderActions(order)"
  138. :key="action"
  139. type="primary"
  140. link
  141. size="small"
  142. @click="handleAction(action, order)"
  143. >{{ action }}</el-button
  144. >
  145. </div> -->
  146. </div>
  147. </div>
  148. <!-- 显示更多商品提示 -->
  149. <div v-if="!order.expanded && order.products.length > 1" class="more-products-hint">
  150. 该订单共 {{ order.products.length }} 件商品,点击展开查看全部
  151. </div>
  152. </div>
  153. <el-empty v-if="orderList.length === 0" description="暂无订单" />
  154. </div>
  155. <!-- 底部操作栏 -->
  156. <div class="bottom-bar">
  157. <div class="bottom-left"><el-checkbox v-model="selectAll" @change="handleSelectAll">全选</el-checkbox></div>
  158. <div class="bottom-right">
  159. <span class="selected-info"
  160. >已勾选 <em>{{ selectedCount }}</em
  161. >/{{ totalOrders }}个订单 共计<em>¥{{ selectedAmount }}</em></span
  162. >
  163. <el-button @click="copyOrderNo">复制订单号</el-button>
  164. <el-dropdown
  165. ><el-button
  166. >批量订单打印 <el-icon><ArrowDown /></el-icon></el-button
  167. ><template #dropdown
  168. ><el-dropdown-menu
  169. ><el-dropdown-item>打印订单</el-dropdown-item><el-dropdown-item>打印发货单</el-dropdown-item></el-dropdown-menu
  170. ></template
  171. ></el-dropdown
  172. >
  173. <el-button type="danger" @click="batchConfirmationBtn">批量确认收货</el-button>
  174. </div>
  175. </div>
  176. <!-- 分页 -->
  177. <TablePagination v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="fetchOrderList" />
  178. <el-dialog v-model="evaluateDialogVisible" :title="evaluateDialogTitle" width="600px">
  179. <div class="evaluate-product">
  180. <div class="product-image">
  181. <el-image :src="currentProduct?.image" fit="contain">
  182. <template #error
  183. ><div class="image-placeholder">
  184. <el-icon :size="30" color="#ccc"><Picture /></el-icon></div
  185. ></template>
  186. </el-image>
  187. </div>
  188. <div class="product-info">
  189. <div class="product-name">{{ currentProduct?.name }}</div>
  190. <div class="product-spec">{{ currentProduct?.spec1 }} {{ currentProduct?.spec2 }}</div>
  191. </div>
  192. </div>
  193. <el-form ref="evaluateFormRef" :model="evaluateForm" :rules="evaluateRules" label-width="80px">
  194. <el-form-item label="商品评分" prop="deliverGoods">
  195. <el-rate v-model="evaluateForm.deliverGoods" :colors="['#e60012', '#e60012', '#e60012']" />
  196. </el-form-item>
  197. <el-form-item label="评价内容" prop="content">
  198. <el-input v-model="evaluateForm.content" type="textarea" :rows="4" placeholder="请输入评价内容" maxlength="200" show-word-limit />
  199. </el-form-item>
  200. <el-form-item label="上传图片">
  201. <el-upload action="#" list-type="picture-card" :auto-upload="false" :limit="5">
  202. <el-icon><Plus /></el-icon>
  203. </el-upload>
  204. </el-form-item>
  205. </el-form>
  206. <template #footer>
  207. <el-button @click="evaluateDialogVisible = false">取消</el-button>
  208. <el-button type="danger" @click="handleSubmitEvaluate">提交评价</el-button>
  209. </template>
  210. </el-dialog>
  211. </div>
  212. </template>
  213. <script setup lang="ts">
  214. import { ref, reactive, computed, onMounted, watch } from 'vue';
  215. import { useRouter } from 'vue-router';
  216. import { Search, ArrowDown, Delete, Document, Clock, Box, CircleCheck, CircleClose, Picture } from '@element-plus/icons-vue';
  217. import type { CheckboxValueType } from 'element-plus';
  218. import { TablePagination } from '@/components';
  219. import {
  220. getOrderList,
  221. getOrderStatusStats,
  222. getOrderProducts,
  223. cancelOrder,
  224. deleteOrder,
  225. batchConfirmation,
  226. addOrderEvaluation
  227. } from '@/api/pc/enterprise/order';
  228. import type { OrderMain, OrderStatusStats } from '@/api/pc/enterprise/orderTypes';
  229. import { ElMessage, ElMessageBox } from 'element-plus';
  230. import { getDeptTree } from '@/api/pc/organization';
  231. import { DeptInfo } from '@/api/pc/organization/types';
  232. import { addProductShoppingCart } from '@/api/goods/index';
  233. import { onPath } from '@/utils/siteConfig';
  234. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  235. const { order_status, pay_method } = toRefs<any>(proxy?.useDict('order_status', 'pay_method'));
  236. const router = useRouter();
  237. const activeTab = ref('all');
  238. const selectAll = ref(false);
  239. const loading = ref(false);
  240. const evaluateDialogVisible = ref(false);
  241. const evaluateDialogTitle = ref('商品评价');
  242. const evaluateFormRef = ref();
  243. const currentProduct = ref<any>(null);
  244. const currentOrder = ref<any>(null);
  245. const evaluateForm = reactive({ deliverGoods: 5, content: '', evaluationType: null });
  246. const evaluateRules = {
  247. deliverGoods: [{ required: true, message: '请选择评分', trigger: 'change' }],
  248. content: [{ required: true, message: '请输入评价内容', trigger: 'blur' }]
  249. };
  250. const deptList = ref([]);
  251. const statusTabs = [
  252. { key: 'all', label: '全部订单', icon: Document },
  253. { key: 'preOrder', label: '预下单', icon: Clock },
  254. { key: 'shipping', label: '待收货', icon: Box },
  255. { key: 'completed', label: '已完成', icon: CircleCheck },
  256. { key: 'cancelled', label: '已取消', icon: CircleClose }
  257. ];
  258. // 监听标签页切换,重置页码并重新获取数据
  259. watch(activeTab, (newTab) => {
  260. queryParams.pageNum = 1;
  261. // 根据标签页设置后端查询的状态参数
  262. if (newTab === 'all') {
  263. queryParams.status = '';
  264. } else {
  265. // 将前端标签页key映射回后端状态值
  266. const tabToStatusMap: Record<string, string> = {
  267. 'preOrder': '0', // 待支付
  268. 'shipping': '4', // 待发货,部分发货,发货完成
  269. 'completed': '5', // 已完成
  270. 'cancelled': '7' // 已关闭,已取消
  271. };
  272. queryParams.status = tabToStatusMap[newTab] || '';
  273. }
  274. fetchOrderList();
  275. });
  276. const queryParams = reactive({ keyword: '', dateRange: null, department: '', status: '', payType: '', pageNum: 1, pageSize: 5 });
  277. const total = ref(0);
  278. const allOrders = ref<any[]>([]);
  279. // 将后端状态映射为前端标签页key
  280. const mapBackendStatusToTabKey = (backendStatus: string) => {
  281. const statusMap: Record<string, string> = {
  282. '0': 'preOrder', // 待支付
  283. '1': 'preOrder', // 待确认
  284. '2': 'shipping', // 待发货
  285. '3': 'shipping', // 部分发货
  286. '4': 'shipping', // 发货完成
  287. '5': 'completed', // 已完成
  288. '6': 'cancelled', // 已关闭
  289. '7': 'cancelled' // 已取消
  290. };
  291. return statusMap[backendStatus] || backendStatus;
  292. };
  293. // 根据订单状态获取状态文本
  294. const getStatusText = (status: string) => {
  295. const statusMap: Record<string, string> = {
  296. '0': '待支付',
  297. '1': '待确认',
  298. '2': '待发货',
  299. '3': '部分发货',
  300. '4': '发货完成',
  301. '5': '已完成',
  302. '6': '已关闭',
  303. '7': '已取消'
  304. };
  305. return statusMap[status] || status;
  306. };
  307. // 加载部门树
  308. const loadDeptTree = async () => {
  309. try {
  310. const res = await getDeptTree();
  311. if (res.code === 200 && res.data) {
  312. deptList.value = res.data;
  313. if (Array.isArray(res.data)) {
  314. const treeData = proxy?.handleTree<DeptInfo>(res.data, 'deptId', 'parentId');
  315. deptList.value = treeData || res.data;
  316. } else {
  317. deptList.value = [];
  318. }
  319. }
  320. } catch (error) {
  321. console.error('获取部门树失败:', error);
  322. ElMessage.error('获取部门树失败');
  323. }
  324. };
  325. /** 单个加入购物车 */
  326. const handleAddCart = async (order: any) => {
  327. const res = await getOrderProducts([order.id]);
  328. if (res.code === 200 && res.rows) {
  329. const products = res.rows.map((p: any) => ({
  330. productId: p.productId,
  331. productNum: p.orderQuantity || 0
  332. }));
  333. const promises = products.map((item) => addProductShoppingCart({ productId: item.productId, productNum: item.productNum }));
  334. Promise.all(promises).then(() => {
  335. ElMessage.success(`已将${products.length}件商品加入购物车`);
  336. });
  337. }
  338. // addProductShoppingCart({ productId: item.id, productNum: 1 }).then((res: any) => {
  339. // if (res.code == 200) {
  340. // ElMessage.success('已加入购物车');
  341. // }
  342. // });
  343. };
  344. // 获取订单列表
  345. const fetchOrderList = async () => {
  346. loading.value = true;
  347. try {
  348. const params: any = {
  349. pageNum: queryParams.pageNum,
  350. pageSize: queryParams.pageSize
  351. };
  352. // 添加筛选条件
  353. if (queryParams.keyword) params.orderNo = queryParams.keyword;
  354. if (queryParams.department) params.department = queryParams.department;
  355. if (queryParams.status) params.orderStatuses = queryParams.status; // 使用orderStatuses支持多状态查询
  356. if (queryParams.payType) params.payType = queryParams.payType;
  357. if (queryParams.dateRange && queryParams.dateRange.length === 2) {
  358. params.beginTime = queryParams.dateRange[0];
  359. params.endTime = queryParams.dateRange[1];
  360. }
  361. console.log('发送到后端的参数:', params);
  362. const res = await getOrderList(params);
  363. console.log('后端返回的数据:', res);
  364. if (res.code === 200) {
  365. // 调试:打印后端返回的第一条订单数据
  366. if (res.rows && res.rows.length > 0) {
  367. console.log('后端���回的订单状态值:', res.rows[0].orderStatus);
  368. console.log('完整的订单数据:', res.rows[0]);
  369. }
  370. // 将后端数据转换为前端需要的格式
  371. allOrders.value = (res.rows || []).map((order: OrderMain) => ({
  372. id: order.id,
  373. orderTime: order.createTime,
  374. orderNo: order.orderNo,
  375. orderPerson: order.customerName, // 需要关联用户信息
  376. department: order.createDeptName, // 需要关联部门信息
  377. payAmount: order.payableAmount || 0,
  378. freight: order.shippingFee || 0,
  379. status: mapBackendStatusToTabKey(order.orderStatus || ''),
  380. statusText: getStatusText(order.orderStatus || ''),
  381. countdown: '',
  382. auditStatus: order.checkStatus,
  383. fileCount: 0,
  384. checked: false,
  385. expanded: false,
  386. products: [] // 商品信息需要单独加载
  387. }));
  388. // 调试:打印转换后的订单状态
  389. console.log(
  390. '转换后的订单状态分布:',
  391. allOrders.value.map((o) => o.status)
  392. );
  393. total.value = res.total || 0;
  394. }
  395. } catch (error) {
  396. console.error('获取订单列表失败:', error);
  397. } finally {
  398. loading.value = false;
  399. }
  400. };
  401. const orderList = computed(() => allOrders.value);
  402. const totalOrders = computed(() => total.value);
  403. const selectedCount = computed(() => orderList.value.filter((o) => o.checked).length);
  404. const selectedAmount = computed(() =>
  405. orderList.value
  406. .filter((o) => o.checked)
  407. .reduce((sum, o) => sum + parseFloat(o.payAmount), 0)
  408. .toFixed(2)
  409. );
  410. const getStatusColor = (status: string) =>
  411. ({ completed: '#67c23a', preOrder: '#e6a23c', shipping: '#409eff', cancelled: '#909399' })[status] || '#909399';
  412. const getAuditStatusClass = (auditStatus: string) => (auditStatus === '审批通过' ? 'success' : auditStatus === '审批驳回' ? 'danger' : 'warning');
  413. const getOrderActions = (order: any) => {
  414. const actions: string[] = [];
  415. switch (order.status) {
  416. case 'preOrder':
  417. // actions.push('再次购买', '加入采购单', '取消订单');
  418. actions.push('再次购买');
  419. break;
  420. case 'shipping':
  421. // actions.push('再次购买', '加入采购单', '取消订单');
  422. actions.push('再次购买');
  423. break;
  424. case 'completed':
  425. // actions.push('评价', '再次购买', '加入采购单', '申请售后', '删除订单', '查看发票');
  426. actions.push('再次购买', '申请售后');
  427. break;
  428. case 'cancelled':
  429. // actions.push('再次购买', '加入采购单', '删除订单');
  430. actions.push('再次购买');
  431. break;
  432. }
  433. return actions;
  434. };
  435. const batchConfirmationBtn = async () => {
  436. const ids = allOrders.value.filter((o) => o.checked).map((o) => o.id);
  437. try {
  438. const res = await batchConfirmation([...ids]);
  439. if (res.code === 200) {
  440. ElMessage.success('批量确认成功');
  441. fetchOrderList();
  442. } else {
  443. ElMessage.error('批量确认失败');
  444. }
  445. return;
  446. } catch (error) {
  447. console.error('批量确认失败:', error);
  448. ElMessage.error('批量确认失败');
  449. }
  450. };
  451. const handleExpand = async (order: any) => {
  452. const orderIndex = allOrders.value.findIndex((o) => o.id === order.id);
  453. if (orderIndex === -1) return;
  454. const currentOrder = allOrders.value[orderIndex];
  455. if (!currentOrder.expanded && currentOrder.products.length === 0) {
  456. try {
  457. const res = await getOrderProducts([order.id]);
  458. if (res.code === 200 && res.rows) {
  459. const products = res.rows.map((p: any) => ({
  460. image: p.productImage || '',
  461. name: p.productName || '',
  462. spec1: p.productUnit || '',
  463. spec2: p.productNo || '',
  464. price: p.orderPrice || 0,
  465. quantity: p.orderQuantity || 0
  466. }));
  467. // 替换整个数组以触发响应式更新
  468. allOrders.value = allOrders.value.map((o, i) => (i === orderIndex ? { ...o, expanded: true, products } : o));
  469. return;
  470. }
  471. } catch (error) {
  472. console.error('加载商品失败:', error);
  473. return;
  474. }
  475. }
  476. // 替换整个数组以触发响应式更新
  477. allOrders.value = allOrders.value.map((o, i) => (i === orderIndex ? { ...o, expanded: !o.expanded } : o));
  478. };
  479. const handleSelectAll = (val: CheckboxValueType) => {
  480. orderList.value.forEach((order) => {
  481. order.checked = !!val;
  482. });
  483. };
  484. async function copyOrderNo() {
  485. // 1. 先找出所有被勾选的订单
  486. const checkedOrders = orderList.value.filter((o) => o.checked);
  487. let orderNoStr = '';
  488. if (checkedOrders.length > 0) {
  489. // ✅ 情况 A:有勾选的,复制勾选的(支持多选)
  490. orderNoStr = checkedOrders.map((o) => o.orderNo).join(',');
  491. } else {
  492. // ⚠️ 情况 B:没勾选任何项
  493. // 策略 1:如果列表不为空,默认复制第一个
  494. if (orderList.value.length > 0) {
  495. orderNoStr = orderList.value[0].orderNo;
  496. ElMessage.warning('未选择订单,已默认复制第一个订单号');
  497. } else {
  498. ElMessage.error('列表为空,无法复制');
  499. return;
  500. }
  501. }
  502. // 2. 执行复制逻辑
  503. try {
  504. await navigator.clipboard.writeText(orderNoStr);
  505. ElMessage.success(`成功复制 ${checkedOrders.length || 1} 个订单号`);
  506. } catch (err) {
  507. const textarea = document.createElement('textarea');
  508. textarea.value = orderNoStr;
  509. document.body.appendChild(textarea);
  510. textarea.select();
  511. document.execCommand('copy');
  512. document.body.removeChild(textarea);
  513. ElMessage.success(`成功复制 ${checkedOrders.length || 1} 个订单号`);
  514. }
  515. }
  516. const handleOrderCheck = () => {
  517. selectAll.value = orderList.value.every((order) => order.checked);
  518. };
  519. const handleViewDetail = (order: any) => {
  520. router.push(`/order/orderManage/detail/${order.id}`);
  521. };
  522. const handleApplyAfter = (order: any) => {
  523. router.push(`/order/orderManage/applyAfter?orderId=${order.id}`);
  524. };
  525. const handleQuery = () => {
  526. queryParams.pageNum = 1;
  527. fetchOrderList();
  528. };
  529. const handleReset = () => {
  530. queryParams.keyword = '';
  531. queryParams.dateRange = null;
  532. queryParams.department = '';
  533. queryParams.payType = '';
  534. queryParams.pageNum = 1;
  535. fetchOrderList();
  536. };
  537. const handleAction = (action: string, order: any) => {
  538. switch (action) {
  539. case '取消订单':
  540. handleCancelOrder(order);
  541. break;
  542. case '删除订单':
  543. handleDeleteOrder(order);
  544. break;
  545. case '查看详情':
  546. handleViewDetail(order);
  547. break;
  548. case '评价':
  549. handleEvaluate(order);
  550. break;
  551. case '再次购买':
  552. handleAddCart(order);
  553. break;
  554. case '申请售后':
  555. handleApplyAfter(order);
  556. break;
  557. default:
  558. ElMessage.info(`${action}功能开发中`);
  559. }
  560. };
  561. const handleEvaluate = (order: any) => {
  562. currentOrder.value = order;
  563. currentProduct.value = order.products[0];
  564. evaluateDialogTitle.value = '商品评价';
  565. evaluateForm.deliverGoods = undefined;
  566. evaluateForm.content = '';
  567. evaluateForm.evaluationType = 1;
  568. evaluateDialogVisible.value = true;
  569. };
  570. const handleSubmitEvaluate = async () => {
  571. const valid = await evaluateFormRef.value?.validate();
  572. if (!valid) return;
  573. try {
  574. const submitData = {
  575. orderId: currentOrder.value?.orderId,
  576. productId: currentProduct.value?.id,
  577. evaluationType: evaluateForm.evaluationType, // 1-评价 2-追评
  578. deliverGoods: evaluateForm.deliverGoods,
  579. content: evaluateForm.content,
  580. // 图片上传暂时留空,后续可以添加
  581. images: []
  582. };
  583. const res = await addOrderEvaluation(submitData);
  584. if (res.code === 200) {
  585. ElMessage.success('评价提交成功');
  586. evaluateDialogVisible.value = false;
  587. // 重新获取订单列表
  588. await getOrderList();
  589. } else {
  590. ElMessage.error(res.msg || '评价提交失败');
  591. }
  592. } catch (error) {
  593. console.error('评价提交失败:', error);
  594. ElMessage.error('评价提交失败');
  595. }
  596. };
  597. const handleCancelOrder = async (order: any) => {
  598. try {
  599. await ElMessageBox.confirm('确定要取消该订单吗?', '提示', {
  600. confirmButtonText: '确定',
  601. cancelButtonText: '取消',
  602. type: 'warning'
  603. });
  604. const res = await cancelOrder({ id: order.id, orderStatus: '7' });
  605. if (res.code === 200) {
  606. ElMessage.success('订单已取消');
  607. fetchOrderList();
  608. } else {
  609. ElMessage.error(res.msg || '取消订单失败');
  610. }
  611. } catch (error: any) {
  612. if (error !== 'cancel') {
  613. ElMessage.error('取消订单失败');
  614. }
  615. }
  616. };
  617. const handleDeleteOrder = async (order: any) => {
  618. try {
  619. await ElMessageBox.confirm('确定要删除该订单吗?删除后无法恢复。', '提示', {
  620. confirmButtonText: '确定',
  621. cancelButtonText: '取消',
  622. type: 'warning'
  623. });
  624. const res = await deleteOrder([order.id]);
  625. if (res.code === 200) {
  626. ElMessage.success('订单已删除');
  627. fetchOrderList();
  628. } else {
  629. ElMessage.error(res.msg || '删除订单失败');
  630. }
  631. } catch (error: any) {
  632. if (error !== 'cancel') {
  633. ElMessage.error('删除订单失败');
  634. }
  635. }
  636. };
  637. // 页面加载时获取订单列表
  638. onMounted(() => {
  639. loadDeptTree();
  640. fetchOrderList();
  641. });
  642. </script>
  643. <style scoped lang="scss">
  644. .order-manage-container {
  645. padding: 20px;
  646. background: #fff;
  647. min-height: 100%;
  648. width: 100%;
  649. display: flex;
  650. flex-direction: column;
  651. max-height: calc(100vh - 120px); // 减去顶部导航栏和其他元素高度
  652. }
  653. .page-title {
  654. font-size: 16px;
  655. font-weight: bold;
  656. display: flex;
  657. align-items: center;
  658. gap: 8px;
  659. margin-bottom: 20px;
  660. }
  661. .title-bar {
  662. display: inline-block;
  663. width: 3px;
  664. height: 16px;
  665. background: #e60012;
  666. border-radius: 2px;
  667. }
  668. .search-bar {
  669. display: flex;
  670. align-items: center;
  671. gap: 15px;
  672. margin-bottom: 15px;
  673. }
  674. .filter-bar {
  675. display: flex;
  676. align-items: center;
  677. gap: 10px;
  678. margin-bottom: 15px;
  679. .filter-label {
  680. font-size: 14px;
  681. color: #666;
  682. }
  683. .filter-right {
  684. flex: 1;
  685. display: flex;
  686. justify-content: flex-end;
  687. gap: 10px;
  688. }
  689. }
  690. .tab-bar {
  691. display: flex;
  692. justify-content: space-between;
  693. align-items: center;
  694. border-bottom: 1px solid #eee;
  695. margin-bottom: 15px;
  696. .tab-left {
  697. display: flex;
  698. gap: 25px;
  699. }
  700. .tab-item {
  701. display: flex;
  702. align-items: center;
  703. gap: 5px;
  704. padding: 12px 0;
  705. cursor: pointer;
  706. color: #666;
  707. font-size: 14px;
  708. border-bottom: 2px solid transparent;
  709. margin-bottom: -1px;
  710. &:hover,
  711. &.active {
  712. color: #333;
  713. }
  714. &.active {
  715. border-bottom-color: #e60012;
  716. }
  717. }
  718. }
  719. .order-list {
  720. flex: 1;
  721. overflow-y: auto;
  722. margin-bottom: 15px;
  723. .order-card {
  724. border: 1px solid #eee;
  725. border-radius: 4px;
  726. margin-bottom: 15px;
  727. overflow: hidden;
  728. .order-header {
  729. display: flex;
  730. align-items: center;
  731. gap: 15px;
  732. padding: 12px 15px;
  733. background: #f9f9f9;
  734. border-bottom: 1px solid #eee;
  735. font-size: 13px;
  736. color: #666;
  737. .order-time {
  738. color: #333;
  739. }
  740. .expand-btn {
  741. margin-left: auto;
  742. }
  743. }
  744. .countdown-bar {
  745. background: #fff5e6;
  746. color: #e6a23c;
  747. padding: 8px 15px;
  748. font-size: 13px;
  749. border-bottom: 1px solid #eee;
  750. }
  751. .product-list {
  752. .product-row {
  753. display: flex;
  754. border-bottom: 1px solid #f5f5f5;
  755. &:last-child {
  756. border-bottom: none;
  757. }
  758. }
  759. .product-cell {
  760. padding: 15px;
  761. display: flex;
  762. flex-direction: column;
  763. justify-content: center;
  764. }
  765. .product-info-cell {
  766. flex: 1;
  767. flex-direction: row;
  768. align-items: center;
  769. gap: 15px;
  770. .product-image {
  771. width: 80px;
  772. height: 80px;
  773. background: #f5f5f5;
  774. border-radius: 4px;
  775. overflow: hidden;
  776. flex-shrink: 0;
  777. .el-image {
  778. width: 100%;
  779. height: 100%;
  780. }
  781. .image-placeholder {
  782. width: 100%;
  783. height: 100%;
  784. display: flex;
  785. align-items: center;
  786. justify-content: center;
  787. }
  788. }
  789. .product-detail {
  790. flex: 1;
  791. .product-name {
  792. font-size: 14px;
  793. color: #333;
  794. margin-bottom: 5px;
  795. line-height: 1.4;
  796. }
  797. .product-spec {
  798. font-size: 12px;
  799. color: #999;
  800. margin-bottom: 5px;
  801. }
  802. .product-price {
  803. font-size: 16px;
  804. font-weight: bold;
  805. color: #e60012;
  806. }
  807. }
  808. .product-quantity {
  809. font-size: 13px;
  810. color: #666;
  811. }
  812. }
  813. .amount-cell {
  814. width: 120px;
  815. border-left: 1px solid #f5f5f5;
  816. .amount-info {
  817. margin-bottom: 5px;
  818. .label {
  819. font-size: 12px;
  820. color: #999;
  821. }
  822. .value {
  823. font-size: 14px;
  824. color: #333;
  825. &.highlight {
  826. font-size: 16px;
  827. font-weight: bold;
  828. color: #e60012;
  829. }
  830. }
  831. }
  832. }
  833. .status-cell {
  834. width: 120px;
  835. border-left: 1px solid #f5f5f5;
  836. align-items: flex-start;
  837. gap: 5px;
  838. .status-text {
  839. font-size: 14px;
  840. font-weight: 500;
  841. }
  842. .audit-status {
  843. font-size: 12px;
  844. &.success {
  845. color: #e60012;
  846. }
  847. &.warning {
  848. color: #e6a23c;
  849. }
  850. &.danger {
  851. color: #e60012;
  852. }
  853. }
  854. }
  855. .action-cell {
  856. width: 100px;
  857. border-left: 1px solid #f5f5f5;
  858. align-items: flex-start;
  859. gap: 3px;
  860. }
  861. }
  862. .more-products-hint {
  863. padding: 10px 15px;
  864. background: #f5f5f5;
  865. font-size: 12px;
  866. color: #666;
  867. text-align: center;
  868. border-top: 1px solid #f0f0f0;
  869. }
  870. }
  871. }
  872. .bottom-bar {
  873. background: #fafafa;
  874. border: 1px solid #eee;
  875. border-radius: 4px;
  876. padding: 15px 20px;
  877. display: flex;
  878. justify-content: space-between;
  879. align-items: center;
  880. flex-shrink: 0;
  881. .bottom-right {
  882. display: flex;
  883. align-items: center;
  884. gap: 15px;
  885. .selected-info {
  886. font-size: 14px;
  887. color: #666;
  888. em {
  889. color: #e60012;
  890. font-style: normal;
  891. font-weight: bold;
  892. }
  893. }
  894. }
  895. }
  896. .evaluate-product {
  897. display: flex;
  898. align-items: center;
  899. gap: 15px;
  900. padding: 15px;
  901. background: #f9f9f9;
  902. border-radius: 4px;
  903. margin-bottom: 20px;
  904. .product-image {
  905. width: 60px;
  906. height: 60px;
  907. background: #fff;
  908. border-radius: 4px;
  909. overflow: hidden;
  910. flex-shrink: 0;
  911. .el-image {
  912. width: 100%;
  913. height: 100%;
  914. }
  915. .image-placeholder {
  916. width: 100%;
  917. height: 100%;
  918. display: flex;
  919. align-items: center;
  920. justify-content: center;
  921. }
  922. }
  923. .product-info {
  924. .product-name {
  925. font-size: 14px;
  926. color: #333;
  927. margin-bottom: 5px;
  928. }
  929. .product-spec {
  930. font-size: 12px;
  931. color: #999;
  932. }
  933. }
  934. }
  935. :deep(.table-pagination) {
  936. flex-shrink: 0;
  937. margin-top: 15px;
  938. }
  939. </style>