Prechádzať zdrojové kódy

feat(order): 更新订单审核流程和审批节点显示功能

- 在订单详情页添加动态审批流程节点,显示实际审批人和时间
- 实现订单审核页面的可审核订单权限控制
- 优化审批流程时间格式化显示
- 添加订单流程节点API接口和数据类型定义
- 修复审批流程状态判断逻辑和按钮显示条件
- 实现购物车商品导出功能
- 更新采购计划详情跳转逻辑
- 优化审批流状态切换的文字描述
- 修复组织架构联系人查询接口参数类型问题
- 过滤审批流联系人列表只显示启用状态的员工
- 添加必填字段星号标识样式
- 调整审批节点视觉样式颜色配置
肖路 4 týždňov pred
rodič
commit
6eb41fec40

+ 10 - 0
src/api/goods/index.ts

@@ -37,6 +37,16 @@ export const deleteProductShoppingCart = (ids: any) => {
   });
 };
 
+//导出购物车的商品
+export const exportProductShoppingCart = (data?: any) => {
+  return request({
+    url: '/product/myProduct/exportProductShoppingCart',
+    method: 'post',
+    data: data,
+    responseType: 'blob'
+  });
+};
+
 //新增商品浏览记录
 
 export const addProductBrowsingHistory = (id: any) => {

+ 22 - 2
src/api/pc/enterprise/order.ts

@@ -1,5 +1,5 @@
 import request from '@/utils/request';
-import { OrderMain, OrderProduct, OrderStatusStats, OrderCustomerFlowSaveBo, OrderCustomerFlowLinkBo, OrderCustomerFlow } from './orderTypes';
+import { OrderMain, OrderProduct, OrderStatusStats, OrderCustomerFlowSaveBo, OrderCustomerFlowLinkBo, OrderCustomerFlow, OrderCustomerFlowNodeLink } from './orderTypes';
 
 // ==================== 订单管理 ====================
 
@@ -236,7 +236,7 @@ export function startOrderFlow(id: number | string) {
   });
 }
 
-/**
+/**`
  * 关闭订单流程
  */
 export function closeOrderFlow(id: number | string) {
@@ -255,3 +255,23 @@ export function getReturnReason(params?: any) {
     params: params
   });
 }
+
+/**
+ * 查询当前用户能审核的订单 id
+ */
+export function getCheckOrderIds() {
+  return request({
+    url: '/order/pcOrder/getCheckOrderIds',
+    method: 'get'
+  });
+}
+
+/**
+ * 查询当前订单的流程节点列表
+ */
+export function getOrderFlowNodes(orderId: number | string) {
+  return request<any, OrderCustomerFlowNodeLink[]>({
+    url: `/order/pcOrder/getOrderFlowNodes/${orderId}`,
+    method: 'get'
+  });
+}

+ 21 - 0
src/api/pc/enterprise/orderTypes.ts

@@ -127,3 +127,24 @@ export interface OrderCustomerFlow {
   flowNodes?: OrderCustomerFlowNode[];
   [key: string]: any;
 }
+
+/**
+ * 订单流程节点链接
+ * 用于查询订单流程节点列表
+ */
+export interface OrderCustomerFlowNodeLink {
+  id?: number | string;
+  orderId?: number;
+  nodeId?: number | string;
+  nodeName?: string;
+  nodeType?: string;
+  sort?: number;
+  handlerId?: string;
+  handlerName?: string;
+  reviewStatus?: number;
+  auditTime?: string;
+  auditOpinion?: string;
+  createTime?: string;
+  updateTime?: string;
+  [key: string]: any;
+}

+ 1 - 1
src/api/pc/organization/index.ts

@@ -91,7 +91,7 @@ export function getContactList(params?: any) {
 /**
  * 查询联系人详情
  */
-export function getContactInfo(id: number) {
+export function getContactInfo(id: number | string) {
   return request({
     url: `/customer/organization/contact/${id}`,
     method: 'get'

+ 18 - 2
src/views/cart/index.vue

@@ -6,7 +6,7 @@
           <img src="@/assets/images/cart1.png" alt="" />
           <div>我的购物车</div>
         </div>
-        <div class="head2">导出订单</div>
+        <div class="head2" @click="onExport">导出订单</div>
       </div>
       <el-table
         ref="multipleTableRef"
@@ -171,8 +171,10 @@ import {
   isProductInDefaultCollect,
   cancelProductCollect,
   addProductCollect,
-  favoritesList
+  favoritesList,
+  exportProductShoppingCart
 } from '@/api/goods/index';
+import FileSaver from 'file-saver';
 import { onPath } from '@/utils/siteConfig';
 
 onMounted(() => {
@@ -288,6 +290,16 @@ const onCancel = () => {
     }
   });
 };
+
+// 导出订单
+const onExport = () => {
+  exportProductShoppingCart().then((res: any) => {
+    const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+    FileSaver.saveAs(blob, '购物车商品.xlsx');
+  }).catch(() => {
+    ElMessage.error('导出失败,请稍后重试');
+  });
+};
 </script>
 
 <style lang="scss" scoped>
@@ -320,6 +332,10 @@ const onCancel = () => {
       color: #e7000b;
       line-height: 25px;
       text-align: center;
+      cursor: pointer;
+      &:hover {
+        background-color: #fff0f0;
+      }
     }
   }
   .cart-info {

+ 3 - 2
src/views/enterprise/purchasePlan/index.vue

@@ -6,7 +6,7 @@
     <!-- 方案列表 -->
     <div v-loading="loading" class="plan-grid">
       <div v-for="(item, index) in planList" :key="index" class="plan-card">
-        <div class="plan-image">
+        <div class="plan-image" @click="onPath(`/plan_info?id=${item.id}`)">
           <el-image :src="item.image" fit="cover">
             <template #error
               ><div class="image-placeholder">
@@ -17,7 +17,7 @@
         <div class="plan-info">
           <div class="plan-name">{{ item.name }}</div>
           <div class="plan-desc">{{ item.description }}</div>
-          <div class="plan-link" @click="handleDetail(item)">
+          <div class="plan-link" @click="onPath(`/plan_info?id=${item.id}`)">
             了解详情 <el-icon><ArrowRight /></el-icon>
           </div>
         </div>
@@ -41,6 +41,7 @@ import { Picture, ArrowRight } from '@element-plus/icons-vue';
 import { ElMessage } from 'element-plus';
 import { PageTitle, StatusTabs, TablePagination } from '@/components';
 import { getProcurementProgramProductList } from '@/api/goods/index';
+import { onPath } from '@/utils/siteConfig';
 
 // 采购方案类型映射
 const typeMap: Record<string, string> = {

+ 16 - 3
src/views/order/orderAudit/index.vue

@@ -109,7 +109,7 @@
               <el-button v-if="order.fileCount" type="primary" link size="small"> 审核文件({{ order.fileCount }}) </el-button>
             </div>
             <div class="product-cell action-cell">
-              <template v-if="activeMainTab === 'myAudit' && order.checkStatus === '0'">
+              <template v-if="activeMainTab === 'myAudit' && order.checkStatus === '0' && checkableOrderIds.includes(order.id)">
                 <el-button type="success" link size="small" @click="handleApprove(order)">同意</el-button>
                 <el-button type="danger" link size="small" @click="handleReject(order)">拒绝</el-button>
               </template>
@@ -159,7 +159,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
 import { PageTitle, StatusTabs } from '@/components';
 import { getDeptTree } from '@/api/pc/organization';
 import { DeptInfo } from '@/api/pc/organization/types';
-import { getOrderList, getOrderProducts, checkOrderStatus } from '@/api/pc/enterprise/order';
+import { getOrderList, getOrderProducts, checkOrderStatus, getCheckOrderIds } from '@/api/pc/enterprise/order';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { complaints_suggestion_type } = toRefs<any>(proxy?.useDict('complaints_suggestion_type'));
 
@@ -174,6 +174,7 @@ const currentAuditOrder = ref<any>(null);
 const currentAuditAction = ref('');
 const loading = ref(false);
 const allOrders = ref<any[]>([]);
+const checkableOrderIds = ref<string[]>([]);
 const pageNum = ref(1);
 const pageSize = ref(5);
 const total = ref(0);
@@ -375,8 +376,19 @@ const handleStatusTabChange = () => {
 onMounted(() => {
   loadDeptTree();
   loadOrderList();
+  loadCheckOrderIds();
 });
 
+// 加载当前用户可审核的订单 ID 列表
+const loadCheckOrderIds = async () => {
+  try {
+    const res = await getCheckOrderIds();
+    checkableOrderIds.value = Array.isArray(res.data) ? res.data : [];
+  } catch (error) {
+    console.error('获取可审核订单ID失败:', error);
+  }
+};
+
 const getStatusText = (checkStatus: string) => {
   const map: Record<string, string> = {
     '0': '待审批',
@@ -438,7 +450,8 @@ const handleSubmitAudit = async () => {
 
     ElMessage.success(currentAuditAction.value === 'approve' ? '审批通过' : '已拒绝');
     auditDialogVisible.value = false;
-    loadOrderList();
+    await loadOrderList();
+    await loadCheckOrderIds();
   } catch (error) {
     console.error('审批失败:', error);
     ElMessage.error('审批失败,请重试');

+ 109 - 9
src/views/order/orderManage/detail.vue

@@ -21,7 +21,7 @@
             <div class="step-info">
               <div class="step-title">{{ step.title }}</div>
               <div class="step-desc">{{ step.desc }}</div>
-              <div class="step-time">{{ step.time }}</div>
+              <div v-if="index <= currentStep" class="step-time">{{ step.time }}</div>
             </div>
             <div v-if="index < progressSteps.length - 1" class="step-line" :class="{ active: index < currentStep }"></div>
           </div>
@@ -126,24 +126,119 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
-import { ArrowLeft, Document, Search, CircleCheck, Picture } from '@element-plus/icons-vue';
-import { getOrderInfo, getOrderProducts } from '@/api/pc/enterprise/order';
+import { ArrowLeft, Document, User, CircleCheck, Picture } from '@element-plus/icons-vue';
+import { getOrderInfo, getOrderProducts, getOrderFlowNodes } from '@/api/pc/enterprise/order';
 import { getAddressInfo } from '@/api/pc/enterprise/address';
 import { getInvoiceList } from '@/api/pc/enterprise/invoice';
+import { getContactInfo } from '@/api/pc/organization/index';
 import { ElMessage } from 'element-plus';
+import type { OrderCustomerFlowNodeLink } from '@/api/pc/enterprise/orderTypes';
 
 const router = useRouter();
 const route = useRoute();
+
+// 格式化时间为 "2026/3/17 上午10:49" 格式
+const formatTime = (timeStr: string): string => {
+  if (!timeStr) return '';
+  const date = new Date(timeStr);
+  if (isNaN(date.getTime())) return timeStr;
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: 'numeric',
+    day: 'numeric',
+    hour: 'numeric',
+    minute: '2-digit',
+    hour12: true
+  });
+};
 const orderId = ref<any>(0);
 const currentStep = ref(1);
 const loading = ref(false);
 
-const progressSteps = ref([
-  { title: '提交审核', icon: Document, desc: '采购一部提交订单审核', time: '2025/02/10 15:51:21' },
-  { title: '审核中', icon: Search, desc: '采购一部提交订单审批', time: '2025/02/10 15:51:21' },
-  { title: '审核完成', icon: CircleCheck, desc: '交易完成', time: '2025/02/10 15:51:21' }
+const progressSteps = ref<{ title: string; icon: any; desc: string; time: string; reviewStatus?: number }[]>([
+  { title: '提交订单', icon: Document, desc: '订单已提交', time: '', reviewStatus: 2 },
+  { title: '完成', icon: CircleCheck, desc: '交易完成', time: '', reviewStatus: 0 }
 ]);
 
+// 订单时间信息(用于流程节点时间显示)
+const orderTimeInfo = reactive({
+  createTime: '', // 订单创建时间(提交订单节点)
+  updateTime: ''  // 订单更新时间(完成节点)
+});
+
+// 根据 handlerId(逗号分隔的多个ID)解析审批人名称
+// 单人:返回姓名;多人:返回"xx或xx审核"
+const resolveHandlerName = async (handlerId: string): Promise<string> => {
+  if (!handlerId) return '';
+  const ids = handlerId.split(',').map((s) => s.trim()).filter(Boolean);
+  if (ids.length === 0) return '';
+  try {
+    const results = await Promise.all(
+      ids.map((id) => getContactInfo(String(id)).catch(() => null))
+    );
+    const names = results
+      .map((res: any) => res?.data?.contactName || '')
+      .filter(Boolean);
+    if (names.length === 0) return '';
+    if (names.length === 1) return names[0];
+    return names.join('或') + '审核';
+  } catch {
+    return '';
+  }
+};
+
+
+// 加载审批流程节点
+const loadFlowNodes = async () => {
+  try {
+    const res = await getOrderFlowNodes(orderId.value) as any;
+    if (res.code === 200) {
+      const apiNodes: OrderCustomerFlowNodeLink[] = res.data || [];
+
+      // 提交订单节点:显示订单的 createTime
+      const steps: { title: string; icon: any; desc: string; time: string; reviewStatus?: number }[] = [
+        { title: '提交订单', icon: Document, desc: '订单已提交', time: formatTime(orderTimeInfo.createTime), reviewStatus: 2 }
+      ];
+
+      // 并行解析所有节点的审批人名称
+      const handlerNames = await Promise.all(
+        apiNodes.map((node) => resolveHandlerName(node.handlerId || node.handlerName || ''))
+      );
+
+      apiNodes.forEach((node, idx) => {
+        steps.push({
+          title: node.nodeName || '审批',
+          icon: User,
+          desc: handlerNames[idx] || '',
+          time: formatTime(node.updateTime || ''),
+          reviewStatus: node.reviewStatus ?? 0
+        });
+      });
+
+      // 完成节点:显示订单的 updateTime
+      steps.push({ title: '完成', icon: CircleCheck, desc: '交易完成', time: formatTime(orderTimeInfo.updateTime), reviewStatus: 0 });
+
+      progressSteps.value = steps;
+
+      // 计算当前步骤:找最后一个已处理节点的索引(reviewStatus > 0)
+      let lastActiveIndex = 0;
+      steps.forEach((step, idx) => {
+        if (step.reviewStatus && step.reviewStatus > 0) {
+          lastActiveIndex = idx;
+        }
+      });
+      // 若所有中间节点均已完成(reviewStatus === 2),则激活结束节点
+      const middleNodes = steps.slice(1, steps.length - 1);
+      if (middleNodes.length > 0 && middleNodes.every((s) => s.reviewStatus === 2)) {
+        lastActiveIndex = steps.length - 1;
+      }
+      currentStep.value = lastActiveIndex;
+    }
+  } catch (error) {
+    console.error('加载流程节点失败', error);
+  }
+};
+
 const productList = ref<any[]>([]);
 
 const orderInfo = reactive({
@@ -182,6 +277,9 @@ const loadOrderDetail = async () => {
       orderInfo.purchaseReason = order.purchaseReason || '';
       orderInfo.remark = order.remark || '';
 
+      // 保存订单时间信息(用于流程节点时间显示)
+      orderTimeInfo.createTime = order.createTime || '';
+      orderTimeInfo.updateTime = order.updateTime || '';
       // 获取商品信息
       const productsRes = await getOrderProducts([orderId.value]);
       if (productsRes.code === 200 && productsRes.rows) {
@@ -227,12 +325,14 @@ const loadOrderDetail = async () => {
   }
 };
 
-onMounted(() => {
+onMounted(async () => {
   const paramId = route.query.orderId;
   // 直接使用字符串,不转换为数字,避免精度丢失
   orderId.value = paramId as string;
   if (orderId.value) {
-    loadOrderDetail();
+    // 先加载订单详情(获取 createTime/updateTime),再加载流程节点
+    await loadOrderDetail();
+    loadFlowNodes();
   } else {
     console.error('订单ID无效,无法加载订单详情');
   }

+ 13 - 8
src/views/organization/approvalFlow/create.vue

@@ -19,7 +19,7 @@
 
     <div class="flow-form">
       <div class="form-item">
-        <label>流程名称</label>
+        <label><span class="required-star">*</span>流程名称</label>
         <el-input v-model="flowName" placeholder="请输入" style="width: 100%" />
       </div>
     </div>
@@ -31,7 +31,7 @@
         <div class="node-header">流程发起</div>
         <div class="node-content">
           <span>发起人:所有人</span>
-          <el-icon><ArrowRight /></el-icon>
+
         </div>
       </div>
 
@@ -47,9 +47,8 @@
       <!-- 审批人节点 -->
       <div v-for="(node, index) in approvalNodes" :key="index" class="flow-node approval-node">
         <div class="node-header">
-          <span>审批人</span>
+          <span>审批人(或签)</span>
           <div class="node-actions">
-            <el-icon @click="handleEditNode(index)"><Edit /></el-icon>
             <el-icon @click="handleDeleteNode(index)"><Delete /></el-icon>
           </div>
         </div>
@@ -80,7 +79,7 @@
     <!-- 操作按钮 -->
     <div class="footer-actions">
       <el-button @click="handleCancel">取消</el-button>
-      <el-button type="primary" :loading="submitLoading" @click="handleSave" :disabled="!flowName.trim() || approvalNodes.length === 0">
+      <el-button type="primary" :loading="submitLoading" @click="handleSave">
         {{ isEdit ? '更新流程' : '创建流程' }}
       </el-button>
     </div>
@@ -160,8 +159,10 @@ const currentEditIndex = ref<number>(-1);
 
 const filteredContactList = computed(() => {
   const key = contactSearchKey.value.trim().toLowerCase();
-  if (!key) return contactList.value;
-  return contactList.value.filter((c) => {
+  // 只展示启用状态(status === '0')的员工
+  const enabledList = contactList.value.filter((c) => c.status === '0');
+  if (!key) return enabledList;
+  return enabledList.filter((c) => {
     const name = (c.contactName || c.name || '').toLowerCase();
     const phone = (c.phone || c.mobile || '').toLowerCase();
     return name.includes(key) || phone.includes(key);
@@ -495,6 +496,10 @@ onUnmounted(() => {
       font-size: 14px;
       color: #333;
       margin-bottom: 8px;
+      .required-star {
+        color: #f56c6c;
+        margin-right: 4px;
+      }
     }
     :deep(.el-input__wrapper) {
       background: #f5f5f5;
@@ -538,7 +543,7 @@ onUnmounted(() => {
 
   &.approval-node {
     .node-header {
-      background: #409eff;
+      background: #ff6b6b;
       color: #fff;
       padding: 8px 15px;
       font-size: 13px;

+ 5 - 4
src/views/organization/approvalFlow/index.vue

@@ -19,7 +19,7 @@
         <template #default="{ row }">
           <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
           <el-button :type="row.status === '1' ? 'warning' : 'success'" link size="small" @click="handleToggleStatus(row)">{{
-            row.status === '1' ? '关闭' : '开启'
+            row.status === '1' ? '开启' : '关闭'
           }}</el-button>
           <el-button type="danger" link size="small" @click="handleDisable(row)">删除</el-button>
         </template>
@@ -67,7 +67,8 @@ const handleEdit = (row: OrderCustomerFlow) => {
 };
 
 const handleToggleStatus = (row: OrderCustomerFlow) => {
-  const isActive = row.status === '1';
+
+  const isActive = row.status === '0';
   const actionText = isActive ? '关闭' : '开启';
   ElMessageBox.confirm(`确定要${actionText}"${row.flowName}"吗?`, '提示', {
     confirmButtonText: '确定',
@@ -77,9 +78,9 @@ const handleToggleStatus = (row: OrderCustomerFlow) => {
     .then(async () => {
       try {
         if (isActive) {
-          await closeOrderFlow(row.id as number);
+          await closeOrderFlow(row.id);
         } else {
-          await startOrderFlow(row.id as number);
+          await startOrderFlow(row.id);
         }
         ElMessage.success(`${actionText}成功`);
         loadFlowList();