index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <template>
  2. <div class="order-audit-container">
  3. <PageTitle title="审核订单" />
  4. <!-- 主Tab切换 -->
  5. <StatusTabs v-model="activeMainTab" :tabs="mainTabs" type="line" @change="handleMainTabChange" />
  6. <!-- 状态Tab切换 -->
  7. <StatusTabs v-model="activeStatusTab" :tabs="currentStatusTabs" type="pill" @change="handleStatusTabChange" />
  8. <!-- 搜索栏 -->
  9. <div class="search-bar">
  10. <el-input v-model="queryParams.keyword" placeholder="搜索" style="width: 240px" clearable>
  11. <template #prefix>
  12. <el-icon><Search /></el-icon>
  13. </template>
  14. </el-input>
  15. <div style="width: 240px">
  16. <el-date-picker
  17. v-model="queryParams.dateRange"
  18. type="daterange"
  19. range-separator="—"
  20. start-placeholder="开始日期"
  21. end-placeholder="结束日期"
  22. style="width: 240px"
  23. />
  24. </div>
  25. <el-tree-select
  26. v-model="queryParams.department"
  27. style="width: 240px"
  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. </div>
  38. <!-- 订单列表 -->
  39. <div class="order-list">
  40. <div v-for="(order, orderIndex) in orderList" :key="orderIndex" class="order-card">
  41. <div class="order-header">
  42. <el-checkbox v-model="order.checked" />
  43. <span class="order-time">{{ order.orderTime }}</span>
  44. <span class="order-info">订单号:{{ order.orderNo }}</span>
  45. <el-button type="primary" link class="detail-btn" @click="handleViewDetail(order)">
  46. 订单详情 <el-icon><ArrowRight /></el-icon>
  47. </el-button>
  48. </div>
  49. <div class="product-list">
  50. <div class="product-row">
  51. <div class="product-cell product-info-cell">
  52. <template v-if="order.products && order.products.length > 0">
  53. <div class="product-image">
  54. <el-image :src="order.products[0].image" fit="contain">
  55. <template #error>
  56. <div class="image-placeholder">
  57. <el-icon :size="30" color="#ccc"><Picture /></el-icon>
  58. </div>
  59. </template>
  60. </el-image>
  61. </div>
  62. <div class="product-detail">
  63. <div class="product-name">{{ order.products[0].name }}</div>
  64. <div class="product-spec">{{ order.products[0].spec1 }} {{ order.products[0].spec2 }}</div>
  65. <div class="product-price">¥{{ order.products[0].price }}</div>
  66. </div>
  67. <div class="product-quantity">x{{ order.products[0].quantity }}</div>
  68. </template>
  69. <template v-else>
  70. <div class="product-image">
  71. <div class="image-placeholder">
  72. <el-icon :size="30" color="#ccc"><Picture /></el-icon>
  73. </div>
  74. </div>
  75. <div class="product-detail">
  76. <div class="product-name">暂无商品信息</div>
  77. </div>
  78. </template>
  79. </div>
  80. <div class="product-cell amount-cell">
  81. <div class="amount-info">
  82. <span class="label">支付款:</span>
  83. <span class="value highlight">¥{{ order.payableAmount }}</span>
  84. </div>
  85. <div class="amount-info">
  86. <span class="label">{{ order.payMethod }}</span>
  87. </div>
  88. </div>
  89. <div class="product-cell status-cell">
  90. <span :class="['status-text', getStatusClass(order.checkStatus)]">{{ getStatusText(order.checkStatus) }}</span>
  91. <!-- <el-button type="primary" link size="small">查看订单轨迹</el-button> -->
  92. <template v-if="order.checkStatus !== '0' && activeMainTab === 'myAudit'">
  93. <!-- <el-button type="primary" link size="small">查看审批流</el-button> -->
  94. </template>
  95. <template v-if="activeMainTab === 'myApply'">
  96. <span v-if="order.checkStatus === '1'" class="result-text success">审批通过</span>
  97. <span v-else-if="order.checkStatus === '2'" class="result-text danger">审批驳回</span>
  98. <!-- <el-button type="primary" link size="small">查看审批流</el-button> -->
  99. </template>
  100. <el-button v-if="order.fileCount" type="primary" link size="small"> 审核文件({{ order.fileCount }}) </el-button>
  101. </div>
  102. <div class="product-cell action-cell">
  103. <template v-if="activeMainTab === 'myAudit' && order.checkStatus === '0'">
  104. <el-button type="success" link size="small" @click="handleApprove(order)">同意</el-button>
  105. <el-button type="danger" link size="small" @click="handleReject(order)">拒绝</el-button>
  106. </template>
  107. <template v-if="activeMainTab === 'myApply' && order.checkStatus === '0'">
  108. <el-button type="primary" link size="small" @click="handleCancelApply(order)">取消申请</el-button>
  109. </template>
  110. </div>
  111. </div>
  112. </div>
  113. </div>
  114. <el-empty v-if="orderList.length === 0" description="暂无审核订单" />
  115. </div>
  116. <!-- 分页 -->
  117. <div class="pagination-container">
  118. <el-pagination
  119. v-model:current-page="pageNum"
  120. v-model:page-size="pageSize"
  121. :total="displayTotal"
  122. :page-sizes="[10, 20, 50, 100]"
  123. layout="total, sizes, prev, pager, next, jumper"
  124. @size-change="handleSizeChange"
  125. @current-change="handlePageChange"
  126. />
  127. </div>
  128. <!-- 审批弹窗 -->
  129. <el-dialog v-model="auditDialogVisible" :title="auditDialogTitle" width="450px">
  130. <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="80px">
  131. <el-form-item label="审批意见" prop="opinion">
  132. <el-input v-model="auditForm.opinion" type="textarea" :rows="4" placeholder="请输入审批意见" />
  133. </el-form-item>
  134. </el-form>
  135. <template #footer>
  136. <el-button @click="auditDialogVisible = false">取消</el-button>
  137. <el-button type="danger" @click="handleSubmitAudit">确定</el-button>
  138. </template>
  139. </el-dialog>
  140. </div>
  141. </template>
  142. <script setup lang="ts">
  143. import { ref, reactive, computed, onMounted } from 'vue';
  144. import { useRouter } from 'vue-router';
  145. import { Search, Document, User, ArrowRight, Picture } from '@element-plus/icons-vue';
  146. import { ElMessage, ElMessageBox } from 'element-plus';
  147. import { PageTitle, StatusTabs } from '@/components';
  148. import { getDeptTree } from '@/api/pc/organization';
  149. import { DeptInfo } from '@/api/pc/organization/types';
  150. import { getOrderList, getOrderProducts, checkOrderStatus } from '@/api/pc/enterprise/order';
  151. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  152. const { complaints_suggestion_type } = toRefs<any>(proxy?.useDict('complaints_suggestion_type'));
  153. const router = useRouter();
  154. const activeMainTab = ref('myAudit');
  155. const activeStatusTab = ref('all');
  156. const auditDialogVisible = ref(false);
  157. const auditDialogTitle = ref('审批');
  158. const auditFormRef = ref();
  159. const currentAuditOrder = ref<any>(null);
  160. const currentAuditAction = ref('');
  161. const loading = ref(false);
  162. const allOrders = ref<any[]>([]);
  163. const pageNum = ref(1);
  164. const pageSize = ref(5);
  165. const total = ref(0);
  166. const deptList = ref([]);
  167. const mainTabs = [
  168. { key: 'myAudit', label: '我审批的', icon: Document }
  169. // { key: 'myApply', label: '我申请的', icon: User }
  170. // { key: 'myAudit', label: '待审批', icon: Document },
  171. // { key: 'myApply', label: '已审批', icon: User }
  172. ];
  173. const auditStatusTabs = [
  174. { key: 'all', label: '全部订单' },
  175. { key: 'pending', label: '待审批' },
  176. { key: 'approved', label: '已通过' },
  177. { key: 'rejected', label: '已驳回' }
  178. ];
  179. const applyStatusTabs = [
  180. { key: 'all', label: '全部订单' },
  181. { key: 'pending', label: '审批中' },
  182. { key: 'approved', label: '审批通过' },
  183. { key: 'rejected', label: '审批驳回' },
  184. { key: 'cancelled', label: '审批取消' }
  185. ];
  186. const currentStatusTabs = computed(() => {
  187. return activeMainTab.value === 'myAudit' ? auditStatusTabs : applyStatusTabs;
  188. });
  189. const queryParams = reactive({
  190. keyword: '',
  191. dateRange: null,
  192. department: '',
  193. filter1: ''
  194. });
  195. const checkOrderData = reactive({
  196. orderId: undefined,
  197. checkStatus: '',
  198. checkRemark: ''
  199. });
  200. const auditForm = reactive({ opinion: '' });
  201. const auditRules = { opinion: [{ required: true, message: '请输入审批意见', trigger: 'blur' }] };
  202. // 加载订单列表数据
  203. const loadOrderList = async () => {
  204. try {
  205. loading.value = true;
  206. // 根据状态tab设置checkStatus参数
  207. const params: any = {
  208. pageNum: pageNum.value,
  209. pageSize: pageSize.value
  210. };
  211. // 根据不同状态设置checkStatus参数
  212. if (activeStatusTab.value === 'pending') {
  213. params.checkStatus = '0';
  214. } else if (activeStatusTab.value === 'approved') {
  215. params.checkStatus = '1';
  216. } else if (activeStatusTab.value === 'rejected') {
  217. params.checkStatus = '2';
  218. }
  219. const res = await getOrderList(params);
  220. if (res.code === 200 && res.rows) {
  221. allOrders.value = res.rows.map((item: any) => ({
  222. id: item.id,
  223. orderTime: item.createTime,
  224. orderNo: item.orderNo,
  225. payableAmount: item.payableAmount,
  226. payMethod: item.payMethod,
  227. checkStatus: item.checkStatus,
  228. fileCount: 0,
  229. checked: false,
  230. products: []
  231. }));
  232. total.value = res.total || 0;
  233. // 获取所有订单的商品信息
  234. if (allOrders.value.length > 0) {
  235. const orderIds = allOrders.value.map((order) => order.id);
  236. const productsRes = await getOrderProducts(orderIds);
  237. if (productsRes.code === 200 && productsRes.rows) {
  238. // 将商品信息按订单ID分组
  239. const productsByOrderId = new Map();
  240. productsRes.rows.forEach((p: any) => {
  241. if (!productsByOrderId.has(p.orderId)) {
  242. productsByOrderId.set(p.orderId, []);
  243. }
  244. productsByOrderId.get(p.orderId).push({
  245. image: p.productImage || '',
  246. name: p.productName || '',
  247. spec1: p.productUnit || '',
  248. spec2: p.productNo || '',
  249. price: p.orderPrice || 0,
  250. quantity: p.orderQuantity || 0
  251. });
  252. });
  253. // 将商品信息赋值给对应的订单
  254. allOrders.value.forEach((order) => {
  255. order.products = productsByOrderId.get(order.id) || [];
  256. });
  257. }
  258. }
  259. }
  260. } catch (error) {
  261. console.error('加载订单列表失败:', error);
  262. ElMessage.error('加载订单列表失败');
  263. } finally {
  264. loading.value = false;
  265. }
  266. };
  267. // 加载部门树
  268. const loadDeptTree = async () => {
  269. try {
  270. const res = await getDeptTree();
  271. if (res.code === 200 && res.data) {
  272. deptList.value = res.data;
  273. if (Array.isArray(res.data)) {
  274. const treeData = proxy?.handleTree<DeptInfo>(res.data, 'deptId', 'parentId');
  275. deptList.value = treeData || res.data;
  276. } else {
  277. deptList.value = [];
  278. }
  279. }
  280. } catch (error) {
  281. console.error('获取部门树失败:', error);
  282. ElMessage.error('获取部门树失败');
  283. }
  284. };
  285. // 每页条数变化
  286. const handleSizeChange = () => {
  287. pageNum.value = 1;
  288. loadOrderList();
  289. };
  290. // 页码变化
  291. const handlePageChange = () => {
  292. loadOrderList();
  293. };
  294. const orderList = computed(() => {
  295. // 所有状态都由后端过滤,前端直接返回数据
  296. return allOrders.value;
  297. });
  298. // 使用后端返回的总数
  299. const displayTotal = computed(() => {
  300. return total.value;
  301. });
  302. const handleMainTabChange = () => {
  303. activeStatusTab.value = 'all';
  304. pageNum.value = 1;
  305. loadOrderList();
  306. };
  307. // 状态 tab 切换
  308. const handleStatusTabChange = () => {
  309. pageNum.value = 1;
  310. loadOrderList();
  311. };
  312. onMounted(() => {
  313. loadDeptTree();
  314. loadOrderList();
  315. });
  316. const getStatusText = (checkStatus: string) => {
  317. const map: Record<string, string> = {
  318. '0': '待审批',
  319. '1': '审批通过',
  320. '2': '审批驳回'
  321. };
  322. return map[checkStatus] || checkStatus;
  323. };
  324. const getStatusClass = (checkStatus: string) => {
  325. if (checkStatus === '1') return 'success';
  326. return 'warning';
  327. };
  328. const handleViewDetail = (order: any) => {
  329. router.push(`/order/orderManage/detail?orderId=${order.id}`);
  330. };
  331. const handleApprove = (order: any) => {
  332. currentAuditOrder.value = order;
  333. checkOrderData.orderId = order.id;
  334. currentAuditAction.value = 'approve';
  335. auditDialogTitle.value = '审批通过';
  336. auditForm.opinion = '';
  337. auditDialogVisible.value = true;
  338. };
  339. const handleReject = (order: any) => {
  340. currentAuditOrder.value = order;
  341. checkOrderData.orderId = order.id;
  342. currentAuditAction.value = 'reject';
  343. auditDialogTitle.value = '审批拒绝';
  344. auditForm.opinion = '';
  345. auditDialogVisible.value = true;
  346. };
  347. const handleCancelApply = (order: any) => {
  348. ElMessageBox.confirm('确定要取消该申请吗?', '提示', {
  349. confirmButtonText: '确定',
  350. cancelButtonText: '取消',
  351. type: 'warning'
  352. })
  353. .then(() => {
  354. order.auditStatus = 'cancelled';
  355. ElMessage.success('已取消申请');
  356. })
  357. .catch(() => {});
  358. };
  359. const handleSubmitAudit = async () => {
  360. const valid = await auditFormRef.value?.validate();
  361. if (!valid) return;
  362. try {
  363. const checkStatus = currentAuditAction.value === 'approve' ? '1' : '2';
  364. checkOrderData.checkStatus = checkStatus;
  365. checkOrderData.checkRemark = auditForm.opinion;
  366. await checkOrderStatus(checkOrderData);
  367. ElMessage.success(currentAuditAction.value === 'approve' ? '审批通过' : '已拒绝');
  368. auditDialogVisible.value = false;
  369. loadOrderList();
  370. } catch (error) {
  371. console.error('审批失败:', error);
  372. ElMessage.error('审批失败,请重试');
  373. }
  374. };
  375. </script>
  376. <style scoped lang="scss">
  377. .order-audit-container {
  378. padding: 20px;
  379. background: #fff;
  380. min-height: 100%;
  381. width: 100%;
  382. }
  383. .search-bar {
  384. display: flex;
  385. align-items: center;
  386. gap: 15px;
  387. margin-bottom: 15px;
  388. }
  389. .filter-bar {
  390. display: flex;
  391. align-items: center;
  392. gap: 10px;
  393. margin-bottom: 20px;
  394. .filter-label {
  395. font-size: 14px;
  396. color: #666;
  397. }
  398. }
  399. .order-list {
  400. .order-card {
  401. border: 1px solid #eee;
  402. border-radius: 4px;
  403. margin-bottom: 15px;
  404. overflow: hidden;
  405. .order-header {
  406. display: flex;
  407. align-items: center;
  408. gap: 15px;
  409. padding: 12px 15px;
  410. background: #f9f9f9;
  411. border-bottom: 1px solid #eee;
  412. font-size: 13px;
  413. color: #666;
  414. .order-time {
  415. color: #333;
  416. }
  417. .detail-btn {
  418. margin-left: auto;
  419. }
  420. }
  421. .product-list {
  422. .product-row {
  423. display: flex;
  424. }
  425. .product-cell {
  426. padding: 15px;
  427. display: flex;
  428. align-items: center;
  429. // flex-direction: column;
  430. justify-content: center;
  431. }
  432. .product-info-cell {
  433. flex: 1;
  434. flex-direction: row;
  435. align-items: center;
  436. gap: 15px;
  437. .product-image {
  438. width: 80px;
  439. height: 80px;
  440. background: #f5f5f5;
  441. border-radius: 4px;
  442. overflow: hidden;
  443. flex-shrink: 0;
  444. .el-image {
  445. width: 100%;
  446. height: 100%;
  447. }
  448. .image-placeholder {
  449. width: 100%;
  450. height: 100%;
  451. display: flex;
  452. align-items: center;
  453. justify-content: center;
  454. }
  455. }
  456. .product-detail {
  457. flex: 1;
  458. .product-name {
  459. font-size: 14px;
  460. color: #333;
  461. margin-bottom: 5px;
  462. line-height: 1.4;
  463. }
  464. .product-spec {
  465. font-size: 12px;
  466. color: #999;
  467. margin-bottom: 5px;
  468. }
  469. .product-price {
  470. font-size: 16px;
  471. font-weight: bold;
  472. color: #e60012;
  473. }
  474. }
  475. .product-quantity {
  476. font-size: 13px;
  477. color: #666;
  478. }
  479. }
  480. .amount-cell {
  481. width: 150px;
  482. border-left: 1px solid #f5f5f5;
  483. .amount-info {
  484. margin-bottom: 5px;
  485. .label {
  486. font-size: 12px;
  487. color: #999;
  488. margin-right: 5px;
  489. }
  490. .value {
  491. font-size: 14px;
  492. color: #333;
  493. &.highlight {
  494. font-size: 16px;
  495. font-weight: bold;
  496. color: #e60012;
  497. }
  498. }
  499. }
  500. }
  501. .status-cell {
  502. width: 140px;
  503. border-left: 1px solid #f5f5f5;
  504. // align-items: flex-start;
  505. gap: 5px;
  506. .status-text {
  507. font-size: 13px;
  508. font-weight: 500;
  509. &.success {
  510. color: #67c23a;
  511. }
  512. &.warning {
  513. color: #e6a23c;
  514. }
  515. }
  516. .result-text {
  517. font-size: 13px;
  518. font-weight: 500;
  519. &.success {
  520. color: #e60012;
  521. }
  522. &.danger {
  523. color: #e60012;
  524. }
  525. }
  526. }
  527. .action-cell {
  528. width: 100px;
  529. border-left: 1px solid #f5f5f5;
  530. // align-items: flex-start;
  531. // gap: 5px;
  532. }
  533. }
  534. }
  535. }
  536. </style>