已修改12个文件
981 ■■■■■ 文件已修改
src/api/collaborativeApproval/sealManagement.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalManagement/index.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 208 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/components/approvalDia.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/index.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/sealManagement.js
@@ -38,6 +38,15 @@
  });
}
// 用印申请详情(按审批流程id)
export function getSealDetail(approveProcessId) {
  return request({
    url: "/sealApplicationManagement/detail",
    method: "get",
    params: { approveProcessId },
  });
}
// 新增印章申请
export function addSealApplication(data) {
  return request({
src/api/productionManagement/workOrder.js
@@ -24,6 +24,15 @@
  });
}
// 报工弹窗数据查询(返回默认班组成员 reportPersons)
export function getProductWorkOrderById(id) {
  return request({
    url: "/productWorkOrder/getProductWorkOrderById",
    method: "get",
    params: { id },
  });
}
// 下载工单流转卡(返回文件流)
export function downProductWorkOrder(id) {
  return request({
src/views/collaborativeApproval/approvalManagement/index.vue
@@ -36,7 +36,7 @@
            <div class="header-info">
              <span class="type-name">{{ currentApproveTypeName }}</span>
              <el-tag :type="approverList.length > 0 ? 'success' : 'warning'" size="small" effect="light">
                {{ approverList.length > 0 ? `已配置 ${approverList.length} 个审批人` : '未配置审批人' }}
                {{ approverCountText }}
              </el-tag>
            </div>
          </div>
@@ -62,18 +62,18 @@
            class="flow-item"
          >
            <!-- 审批节点卡片 -->
            <div class="node-card" :class="{ 'empty': !item.approverId }">
            <div class="node-card" :class="{ 'empty': !(item.approverIds && item.approverIds.length) }">
              <!-- 顶部序号和级别 -->
              <div class="node-badge">{{ index + 1 }}</div>
              <!-- 头像区域 -->
              <div class="node-avatar-section">
                <div
                  class="node-avatar"
                  :class="{ 'has-user': item.approverId }"
                  :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}"
                <div
                  class="node-avatar"
                  :class="{ 'has-user': item.approverIds && item.approverIds.length }"
                  :style="item.approverIds && item.approverIds.length ? { backgroundColor: getAvatarColor(item.approverNames?.[0] || '') } : {}"
                >
                  <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span>
                  <span v-if="item.approverIds && item.approverIds.length">{{ (item.approverNames?.[0] || '?').charAt(0) }}</span>
                  <el-icon v-else :size="24"><User /></el-icon>
                </div>
                <div class="node-level">{{ getLevelText(index) }}</div>
@@ -82,8 +82,11 @@
              <!-- 选择区域 -->
              <div class="node-select-section">
                <el-select
                  v-model="item.approverId"
                  placeholder="选择审批人"
                  v-model="item.approverIds"
                  multiple
                  collapse-tags
                  collapse-tags-tooltip
                  placeholder="选择审批人(可多选)"
                  filterable
                  size="default"
                  @change="(val) => handleApproverChange(val, item)"
@@ -95,6 +98,13 @@
                    :value="user.userId"
                  />
                </el-select>
                <div class="node-mode">
                  <span class="mode-label">审批方式</span>
                  <el-radio-group v-model="item.approveMode" size="small">
                    <el-radio-button value="or">或签</el-radio-button>
                    <el-radio-button value="and">会签</el-radio-button>
                  </el-radio-group>
                </div>
              </div>
              <!-- 操作按钮 -->
@@ -150,7 +160,7 @@
              <div class="add-icon-wrapper">
                <el-icon :size="28"><Plus /></el-icon>
              </div>
              <span class="add-text">新增审批人</span>
              <span class="add-text">新增审批节点</span>
            </div>
          </div>
        </div>
@@ -166,7 +176,7 @@
          <div class="empty-subtext">点击下方按钮添加第一个审批人</div>
          <el-button type="primary" size="large" @click="handleAdd" class="empty-add-btn">
            <el-icon><Plus /></el-icon>
            新增审批人
            新增审批节点
          </el-button>
        </div>
      </div>
@@ -175,7 +185,7 @@
    <!-- 底部提示 -->
    <div class="bottom-tips">
      <el-icon><InfoFilled /></el-icon>
      <span>提示:每个流程至少配置一个审批人,审批按顺序流转,可通过箭头调整顺序</span>
      <span>提示:每个节点可配置多名审批人并选择或签/会签;或签=任意一人通过即流转,会签=需全部人通过;审批按节点顺序流转</span>
    </div>
  </div>
</template>
@@ -186,7 +196,7 @@
import {
  Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting,
  Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked,
  Van, ArrowRight, User, InfoFilled
  Van, ArrowRight, User, InfoFilled, Stamp, WarningFilled
} from '@element-plus/icons-vue';
import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement';
import { userListNoPage } from '@/api/system/user';
@@ -203,6 +213,8 @@
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  { value: '8', label: '危险作业审批', icon: 'WarningFilled', color: '#E74C3C' },
  { value: '9', label: '用印审批', icon: 'Stamp', color: '#7D3C98' },
];
// 审批类型名称映射
@@ -214,6 +226,8 @@
  5: '采购审批',
  6: '报价审批',
  7: '发货审批',
  8: '危险作业审批',
  9: '用印审批',
};
// 审批类型图标映射
@@ -225,6 +239,8 @@
  5: 'ShoppingCart',
  6: 'DocumentChecked',
  7: 'Van',
  8: 'WarningFilled',
  9: 'Stamp',
};
// 审批类型颜色映射
@@ -236,6 +252,8 @@
  5: '#909399',
  6: '#9B59B6',
  7: '#1ABC9C',
  8: '#E74C3C',
  9: '#7D3C98',
};
// 头像颜色池
@@ -249,6 +267,14 @@
// 当前审批类型
const currentApproveType = computed(() => {
  return Number(activeTab.value);
});
// 头部统计文案:节点数 / 审批人数
const approverCountText = computed(() => {
  const nodes = approverList.value.length;
  const users = approverList.value.reduce((sum, item) => sum + (item.approverIds ? item.approverIds.length : 0), 0);
  if (nodes === 0) return '未配置审批人';
  return `已配置 ${nodes} 节点 / ${users} 审批人`;
});
// 获取类型图标
@@ -291,7 +317,7 @@
  loadData();
};
// 加载审批配置数据(模拟)
// 加载审批配置数据:接口扁平行 → 按 nodeOrder 分组合并为节点卡片(同节点多审批人)
const loadData = async () => {
  loading.value = true;
  try {
@@ -303,12 +329,19 @@
        : Array.isArray(res?.data?.records)
          ? res.data.records
          : [];
    const data = source.map((item, index) => ({
      ...item,
      sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1,
    }));
    approverList.value = data.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
    originalList.value = JSON.parse(JSON.stringify(approverList.value));
    const map = new Map();
    source.forEach(item => {
      const order = item.nodeOrder ?? 1;
      if (!map.has(order)) {
        map.set(order, { nodeOrder: order, approverIds: [], approverNames: [], approveMode: item.approveMode || 'or' });
      }
      const card = map.get(order);
      card.approverIds.push(item.approverId);
      if (item.approverName) card.approverNames.push(item.approverName);
    });
    const cards = [...map.values()].sort((a, b) => a.nodeOrder - b.nodeOrder);
    approverList.value = cards;
    originalList.value = JSON.parse(JSON.stringify(cards));
  } catch (error) {
    approverList.value = [];
    originalList.value = [];
@@ -328,29 +361,28 @@
  }
};
// 审批人选择变化
const handleApproverChange = (userId, row) => {
  const user = userList.value.find((u) => u.userId === userId);
  if (user) {
    row.approverName = user.nickName;
  }
// 审批人选择变化(多选)
const handleApproverChange = (ids, row) => {
  row.approverNames = (ids || []).map(id => {
    const user = userList.value.find((u) => u.userId === id);
    return user ? user.nickName : '';
  }).filter(Boolean);
};
// 新增审批人
// 新增审批节点
const handleAdd = () => {
  const newOrder = approverList.value.length + 1;
  approverList.value.push({
    id: null,
    approveType: currentApproveType.value,
    approverId: null,
    approverName: '',
    sortOrder: newOrder,
    nodeOrder: newOrder,
    approverIds: [],
    approverNames: [],
    approveMode: 'or',
  });
};
// 删除审批人
// 删除审批节点
const handleDelete = (index) => {
  ElMessageBox.confirm('确定删除该审批人吗?', '提示', {
  ElMessageBox.confirm('确定删除该审批节点吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
@@ -358,7 +390,7 @@
    .then(() => {
      approverList.value.splice(index, 1);
      approverList.value.forEach((item, idx) => {
        item.sortOrder = idx + 1;
        item.nodeOrder = idx + 1;
      });
      ElMessage.success('删除成功');
    })
@@ -371,8 +403,7 @@
  const temp = approverList.value[index];
  approverList.value[index] = approverList.value[index - 1];
  approverList.value[index - 1] = temp;
  approverList.value[index].sortOrder = index + 1;
  approverList.value[index - 1].sortOrder = index;
  approverList.value.forEach((item, idx) => { item.nodeOrder = idx + 1; });
};
// 后移
@@ -381,38 +412,38 @@
  const temp = approverList.value[index];
  approverList.value[index] = approverList.value[index + 1];
  approverList.value[index + 1] = temp;
  approverList.value[index].sortOrder = index + 1;
  approverList.value[index + 1].sortOrder = index + 2;
  approverList.value.forEach((item, idx) => { item.nodeOrder = idx + 1; });
};
// 保存配置
// 保存配置:节点卡片按审批人展开为多行 payload
const handleSave = async () => {
  if (approverList.value.length === 0) {
    ElMessage.warning('请至少配置一个审批人');
    ElMessage.warning('请至少配置一个审批节点');
    return;
  }
  const hasEmptyApprover = approverList.value.some((item) => !item.approverId);
  const hasEmptyApprover = approverList.value.some((item) => !item.approverIds || item.approverIds.length === 0);
  if (hasEmptyApprover) {
    ElMessage.warning('请选择所有审批人');
    return;
  }
  const approverIds = approverList.value.map((item) => item.approverId);
  const uniqueIds = [...new Set(approverIds)];
  if (uniqueIds.length !== approverIds.length) {
    ElMessage.warning('审批人不能重复');
    ElMessage.warning('请为每个节点选择审批人');
    return;
  }
  saveLoading.value = true;
  try {
    const payload = approverList.value.map((item, index) => ({
      approveType: currentApproveType.value,
      nodeOrder: index + 1,
      approverId: item.approverId,
      approverName: item.approverName,
    }));
    const payload = [];
    approverList.value.forEach((card) => {
      const uniqueIds = [...new Set(card.approverIds || [])];
      uniqueIds.forEach(id => {
        const user = userList.value.find(u => u.userId === id);
        payload.push({
          approveType: currentApproveType.value,
          nodeOrder: card.nodeOrder,
          approverId: id,
          approverName: user ? user.nickName : '',
          approveMode: card.approveMode || 'or',
        });
      });
    });
    await addApproveProcessConfigNode(payload);
    ElMessage.success('保存成功');
    await loadData();
@@ -680,6 +711,20 @@
  width: 100%;
}
.node-mode {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed var(--el-border-color-light, #ebeef5);
}
.mode-label {
  font-size: 12px;
  color: var(--el-text-color-secondary, #909399);
}
.node-actions {
  display: flex;
  justify-content: center;
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -17,18 +17,7 @@
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="申请部门:">
                            <el-select
                                disabled
                                v-model="form.approveDeptId"
                                placeholder="选择部门"
                            >
                                <el-option
                                    v-for="user in productOptions"
                                    :key="user.deptId"
                                    :label="user.deptName"
                                    :value="user.deptId"
                                />
                            </el-select>
                            <el-input v-model="form.approveDeptName" placeholder="申请部门" disabled />
                        </el-form-item>
                    </el-col>
                </el-row>
@@ -135,42 +124,81 @@
        </el-skeleton>
      </div>
      <!-- 用印审批:展示用印申请详情 -->
      <div v-if="isSealApproval" style="margin: 10px 0 18px;">
        <el-divider content-position="left">用印详情</el-divider>
        <el-skeleton :loading="sealLoading" animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentSeal || !currentSeal.applicationNum" description="未查询到对应用印申请详情" />
            <template v-else>
              <el-descriptions :column="2" border>
                <el-descriptions-item label="申请编号">{{ currentSeal.applicationNum }}</el-descriptions-item>
                <el-descriptions-item label="申请标题">{{ currentSeal.title }}</el-descriptions-item>
                <el-descriptions-item label="用印类型">{{ getSealTypeText(currentSeal.sealType) }}</el-descriptions-item>
                <el-descriptions-item label="紧急程度">{{ getUrgencyText(currentSeal.urgency) }}</el-descriptions-item>
                <el-descriptions-item label="申请原因" :span="2">{{ currentSeal.reason }}</el-descriptions-item>
              </el-descriptions>
              <div v-if="currentSeal.commonFileList?.length" style="margin-top: 20px;">
                <h4>附件</h4>
                <el-table :data="currentSeal.commonFileList" border style="width: 100%">
                  <el-table-column label="附件名称" show-overflow-tooltip>
                    <template #default="scope">{{ scope.row.name }}</template>
                  </el-table-column>
                  <el-table-column label="操作" width="150" align="center">
                    <template #default="scope">
                      <el-button link type="primary" size="small" @click="previewSealFile(scope.row)">预览</el-button>
                      <el-button link type="primary" size="small" @click="downloadSealFile(scope.row)">下载</el-button>
                    </template>
                  </el-table-column>
                </el-table>
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }" ref="formRef" label-position="top">
        <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
                <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
          <el-step
            v-for="(activity, index) in activities"
            :key="index"
                        finish-status="success"
            :title="getNodeTitle(index, activities.length)"
            :description="activity.approveNodeUser"
            :icon="getNodeIcon(activity, index)"
            v-for="(group, gIndex) in stepGroups"
            :key="gIndex"
            finish-status="success"
            :title="getNodeTitle(gIndex, stepGroups.length)"
            :icon="getNodeIcon(group)"
          >
                        <template #icon>
                            <el-icon v-if="activity.approveNodeStatus === 2" color="red" :size="22"><WarningFilled /></el-icon>
                            <el-icon v-else-if="activity.isShen" color="#1890ff" :size="22"><Edit /></el-icon>
                            <el-icon v-else-if="activity.approveNodeStatus === 1" color="#67C23A" :size="26"><Check /></el-icon>
                            <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon>
                        </template>
            <template #icon>
              <el-icon v-if="group.some(a => a.approveNodeStatus === 2)" color="red" :size="22"><WarningFilled /></el-icon>
              <el-icon v-else-if="group.some(a => a.isShen)" color="#1890ff" :size="22"><Edit /></el-icon>
              <el-icon v-else-if="group.every(a => a.approveNodeStatus === 1)" color="#67C23A" :size="26"><Check /></el-icon>
              <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon>
            </template>
            <template #title>
              <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
              <span style="color: #000000">{{ getNodeTitle(gIndex, stepGroups.length) }}</span>
              <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'" size="small" style="margin-left: 6px">{{ group[0]?.approveMode === 'and' ? '会签' : '或签' }}</el-tag>
            </template>
            <template #description>
              <div class="node-user">
                <div class="avatar-wrapper">
                  <img :src="userStore.avatar" class="user-avatar" alt=""/>
              <div v-for="(activity, aIndex) in group" :key="activity.id || aIndex" class="node-user-item">
                <div class="node-user">
                  <div class="avatar-wrapper">
                    <img :src="userStore.avatar" class="user-avatar" alt=""/>
                  </div>
                  <span style="color: #000000">{{ activity.approveNodeUser }}({{ activity.isApproval }})</span>
                </div>
                <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
              </div>
              <div v-if="!activity.isShen" class="node-reason">
                <span>审批意见:</span>{{ activity.approveNodeReason }}
              </div>
              <div v-else-if="activity.isShen">
                <el-form-item
                  :prop="'activities.' + index + '.approveNodeReason'"
                  :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]"
                >
                  <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input>
                </el-form-item>
                <div v-if="!activity.isShen" class="node-reason">
                  <span>审批意见:</span>{{ activity.approveNodeReason }}
                </div>
                <div v-else-if="activity.isShen">
                  <el-form-item
                    :prop="'activities.' + getActivityIndex(activity) + '.approveNodeReason'"
                    :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]"
                  >
                    <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input>
                  </el-form-item>
                </div>
              </div>
            </template>
          </el-step>
@@ -184,6 +212,7 @@
        </div>
      </template>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
@@ -198,6 +227,8 @@
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
import { getSealDetail } from "@/api/collaborativeApproval/sealManagement.js";
import FilePreview from '@/components/filePreview/index.vue'
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
@@ -218,8 +249,12 @@
const currentQuotation = ref({})
const purchaseLoading = ref(false)
const currentPurchase = ref({})
const sealLoading = ref(false)
const currentSeal = ref({})
const filePreviewRef = ref()
const isQuotationApproval = computed(() => Number(props.approveType) === 6)
const isPurchaseApproval = computed(() => Number(props.approveType) === 5)
const isSealApproval = computed(() => Number(props.approveType) === 9)
const data = reactive({
    form: {
@@ -237,19 +272,31 @@
  return '审批';
};
// 获取当前激活步骤
// 获取当前激活步骤(分组后:含待审人的组下标,无则返回组总数=全部完成)
const getActiveStep = () => {
  // 如果所有 isShen 都为 false,返回最后一个步骤(全部完成)
  const hasActive = activities.value.some(a => a.isShen === true);
  if (!hasActive) return activities.value.length;
  // 当前节点索引
  return activities.value.findIndex(a => a.isShen  == true);
  const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true));
  if (idx === -1) return stepGroups.value.length;
  return idx;
};
// 步骤icon
const getNodeIcon = (activity, index) => {
  if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // 不通过
  if (activity.isShen) return 'Edit';
// 步骤icon(组级:任一人驳回/任一人待审)
const getNodeIcon = (group) => {
  if (group.some(a => a.approveNodeStatus === 2)) return 'el-icon-warning'; // 不通过
  if (group.some(a => a.isShen)) return 'Edit';
  return '';
};
// 审批节点分组(同一 approveNodeOrder = 同一节点的多名审批人,或签/会签)
const stepGroups = computed(() => {
  const map = new Map();
  (activities.value || []).forEach(a => {
    const order = a.approveNodeOrder;
    if (!map.has(order)) map.set(order, []);
    map.get(order).push(a);
  });
  return Array.from(map.values());
});
// 获取节点成员在 activities 中的索引(用于表单校验 prop)
const getActivityIndex = (activity) => {
  return activities.value.findIndex(a => a.id === activity.id);
};
// 打开弹框
@@ -315,6 +362,19 @@
    }
  }
  // 用印审批:按审批流程 DB 主键(row.id)查用印申请详情
  if (isSealApproval.value) {
    sealLoading.value = true
    getSealDetail(row.id).then((res) => {
      currentSeal.value = res.data || {}
    }).catch((err) => {
      console.error('查询用印详情失败:', err)
      proxy.$modal.msgError('查询用印详情失败')
    }).finally(() => {
      sealLoading.value = false
    })
  }
  approveProcessDetails(row.approveId).then((res) => {
    activities.value = res.data
    // 增加isApproval字段
@@ -339,6 +399,42 @@
        productOptions.value = res.data;
    });
};
// 用印类型文本
const getSealTypeText = (sealType) => {
  const map = { official: '公章', contract: '合同专用章', finance: '财务专用章', legal: '法人章' }
  return map[sealType] || sealType || '未知'
}
// 紧急程度文本
const getUrgencyText = (urgency) => {
  const map = { normal: '普通', urgent: '紧急', 'very-urgent': '特急' }
  return map[urgency] || urgency || '未知'
}
// 预览附件(URL 为完整地址,原样打开,不拼 host)
const previewSealFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url
  if (url && filePreviewRef.value) {
    filePreviewRef.value.openUrl(url)
  } else {
    proxy.$modal.msgWarning('文件地址无效,无法预览')
  }
}
// 下载附件(锚点方式,URL 为完整地址)
const downloadSealFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download'
    const a = document.createElement('a')
    a.href = url
    a.download = filename
    a.target = '_blank'
    a.rel = 'noopener noreferrer'
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
  } else {
    proxy.$modal.msgWarning('文件地址无效,无法下载')
  }
}
// 提交审批
const submitForm = (status) => {
  const filteredActivities = activities.value.filter(activity => activity.isShen);
@@ -352,9 +448,8 @@
    return;
  }
  currentActivity.approveNodeStatus = status;
  // 判断是否为最后一步
  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1;
  updateApproveNode({ ...currentActivity, isLast }).then(() => {
  // 流转判断由后端完成,前端不再传 isLast
  updateApproveNode({ ...currentActivity }).then(() => {
    proxy.$modal.msgSuccess("提交成功");
    closeDia();
  });
@@ -367,6 +462,8 @@
  currentQuotation.value = {}
  purchaseLoading.value = false
  currentPurchase.value = {}
  sealLoading.value = false
  currentSeal.value = {}
  emit('close')
};
defineExpose({
@@ -384,6 +481,9 @@
  align-items: center;
  gap: 8px;
}
.node-user-item {
  margin: 8px 0;
}
.node-status {
  color: #1890ff;
  margin-left: 8px;
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -70,7 +70,7 @@
          <el-button
            type="primary"
            @click="openForm('add')"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 9"
            class="action-btn primary"
          >
            <el-icon><Plus /></el-icon>
@@ -84,7 +84,7 @@
            type="danger"
            plain
            @click="handleDelete"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 9"
            class="action-btn danger"
          >
            <el-icon><Delete /></el-icon>
@@ -142,7 +142,16 @@
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
// 当前用户是否为管理员(角色 普通2/admin/最高1),可不受审批人限制审批
const isAdminUser = computed(() =>
  ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r))
);
const route = useRoute();
// 管理员账号(角色为 普通2 / admin / 最高1)可审批任意流程,不受审批人限制
const isAdminUser = computed(() =>
  ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r))
);
// 当前选中的标签页,默认为公出管理
const activeTab = ref('1');
@@ -159,6 +168,7 @@
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  { value: '9', label: '用印审批', icon: 'Stamp', color: '#7D3C98' },
];
// 当前审批类型信息
@@ -286,11 +296,11 @@
    }
  );
  
  // 当前审批人列
  // 当前审批人列(支持同节点多审批人)
  baseColumns.push({
    label: "当前审批人",
    prop: "approveUserCurrentName",
    width: 120
    prop: "currentApproverNames",
    width: 180
  });
  
  // 操作列
@@ -305,6 +315,7 @@
        currentApproveType.value === 5 ||
        currentApproveType.value === 6 ||
        currentApproveType.value === 7 ||
        currentApproveType.value === 9 ||
        row.approveStatus == 2 ||
        row.approveStatus == 1 ||
        row.approveStatus == 4
@@ -316,11 +327,10 @@
        openApprovalDia("approval", row);
      },
      disabled: (row) =>
        row.approveUserCurrentId == null ||
        row.approveStatus == 2 ||
        row.approveStatus == 3 ||
        row.approveStatus == 4 ||
        row.approveUserCurrentId !== userStore.id
        !row._canApprove
    },
    {
      name: "详情",
@@ -331,8 +341,8 @@
    },
  ];
  // 报价审批(类型 6)不展示"附件"操作
  if (!isQuotationType) {
  // 报价审批(类型 6)、用印审批(类型 9)不展示"附件"操作(附件走 common_file,不走 storage_attachment)
  if (!isQuotationType && currentApproveType.value !== 9) {
    actionOperations.push({
      name: "附件",
      type: "text",
@@ -385,7 +395,12 @@
  tableLoading.value = true;
  approveProcessListPage({...page, ...searchForm.value, approveType: currentApproveType.value}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    // 富化行数据:当前审批人显示(多值兜底旧单值)+ 当前用户是否可审核
    tableData.value = (res.data.records || []).map(r => ({
      ...r,
      currentApproverNames: r.approveUserCurrentNames || r.approveUserCurrentName || '-',
      _canApprove: isAdminUser.value || String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) || r.approveUserCurrentId === userStore.id
    }));
    page.total = res.data.total;
    // 更新当前类型数量
    typeCounts.value[activeTab.value] = res.data.total;
@@ -405,6 +420,7 @@
    5: "/approveProcess/exportFive",
    6: "/approveProcess/exportSix",
    7: "/approveProcess/exportSeven",
    9: "/approveProcess/exportNine",
  }
  const url = urlMap[type] || urlMap[0]
  const nameMap = {
@@ -416,6 +432,7 @@
    5: "采购申请审批表",
    6: "报价审批表",
    7: "发货审批表",
    9: "用印审批表",
  }
  const fileName = nameMap[type] || nameMap[0]
  proxy.download(url, {}, `${fileName}.xlsx`)
src/views/collaborativeApproval/sealManagement/index.vue
@@ -7,7 +7,6 @@
        </div>
      </template>
   <!-- 用印申请管理 -->
        <div class="tab-content">
            <el-row :gutter="20" class="mb-20 ">
@@ -75,22 +74,39 @@
        <el-form-item label="申请原因" prop="reason">
          <el-input v-model="sealForm.reason" type="textarea" :rows="4" placeholder="请详细说明用印原因" />
        </el-form-item>
        <el-form-item label="审批人" prop="approveUserId">
          <el-select v-model="sealForm.approveUserId" placeholder="请选择审批人" style="width: 100%" filterable>
            <el-option
                v-for="user in userList"
                :key="user.userId"
                :label="user.nickName"
                :value="user.userId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="紧急程度" prop="urgency">
          <el-radio-group v-model="sealForm.urgency">
            <el-radio label="normal">普通</el-radio>
            <el-radio label="urgent">紧急</el-radio>
            <el-radio label="very-urgent">特急</el-radio>
            <el-radio value="normal">普通</el-radio>
            <el-radio value="urgent">紧急</el-radio>
            <el-radio value="very-urgent">特急</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="附件上传">
          <el-upload
            multiple
            :show-file-list="false"
            :action="sealUploadUrl"
            name="files"
            :headers="uploadHeaders"
            :limit="10"
            :before-upload="handleBeforeUpload"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            :on-exceed="handleUploadExceed"
          >
            <el-button type="primary">点击上传附件</el-button>
          </el-upload>
          <el-table v-if="sealForm.attachmentList.length" :data="sealForm.attachmentList" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">{{ scope.row.name }}</template>
            </el-table-column>
            <el-table-column label="操作" width="140" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="danger" size="small" @click="removeAttachment(scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-form-item>
      </el-form>
    </FormDialog>
@@ -119,21 +135,41 @@
          </el-descriptions-item>
          <el-descriptions-item label="申请原因" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item>
        </el-descriptions>
        <!-- 附件列表 -->
        <div v-if="currentSealDetail.commonFileList?.length" class="attachment-section">
          <div class="attachment-title">附件列表:</div>
          <el-table :data="currentSealDetail.commonFileList" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">
                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }}
              </template>
            </el-table-column>
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </FormDialog>
    <!-- 文件预览组件 -->
    <FilePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue'
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listSealApplication, addSealApplication, updateSealApplication } from '@/api/collaborativeApproval/sealManagement.js'
import { userListNoPageByTenantId } from '@/api/system/user.js'
import { ElMessage } from 'element-plus'
import { listSealApplication, addSealApplication } from '@/api/collaborativeApproval/sealManagement.js'
import useUserStore from '@/store/modules/user'
import { getToken } from '@/utils/auth'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import FilePreview from '@/components/filePreview/index.vue'
// 响应式数据
// 用印申请相关
@@ -143,24 +179,27 @@
const tableLoading = ref(false)
const showSealDetailDialog = ref(false)
const currentSealDetail = ref(null)
const filePreviewRef = ref(null)
const sealFormRef = ref()
const userList = ref([])
// 附件上传(本地上传,非 minio,返回 [{ name, url }])
const sealUploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/uploadsList'
const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
const sealForm = reactive({
  id: null,
  applicationNum: '',
  title: '',
  sealType: '',
  reason: '',
  approveUserId: '',
  urgency: 'normal',
  status: 'pending'
  status: 'pending',
  attachmentList: []
})
const sealRules = {
  applicationNum: [{ required: true, message: '请输入申请编号', trigger: 'blur' }],
  title: [{ required: true, message: '请输入申请标题', trigger: 'blur' }],
  sealType: [{ required: true, message: '请选择用印类型', trigger: 'change' }],
  reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }],
  approveUserId: [{ required: true, message: '请选择审批人', trigger: 'change' }]
  reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }]
}
const sealSearchForm = reactive({
@@ -209,9 +248,9 @@
// 用印申请表格列配置(需在 getStatusText/getSealTypeText 等之后定义)
const sealTableColumn = ref([
  { label: '申请编号', prop: 'applicationNum',},
  { label: '申请编号', prop: 'applicationNum' },
  { label: '申请标题', prop: 'title', showOverflowTooltip: true },
  { label: '申请人', prop: 'createUserName', },
  { label: '申请人', prop: 'createUserName' },
  { label: '所属部门', prop: 'department', width: 150 },
  {
    label: '用印类型',
@@ -229,40 +268,27 @@
    formatData: (v) => getStatusText(v),
    formatType: (v) => getStatusType(v)
  },
  { label: '审批人', prop: 'approveUserName', width: 120 },
  {
    dataType: 'action',
    label: '操作',
    width: 250,
    width: 150,
    fixed: 'right',
    align: 'center',
    operation: [
      { name: '查看', clickFun: (row) => viewSealDetail(row) },
      {
        name: '审批',
        clickFun: (row) => approveSeal(row),
        showHide: (row) => row.status === 'pending' && row.approveUserId === userStore.id
      },
      {
        name: '拒绝',
        clickFun: (row) => rejectSeal(row),
        showHide: (row) => row.status === 'pending' && row.approveUserId === userStore.id
      },
      {
        name: '重新申请',
        clickFun: (row) => reapplySeal(row),
        showHide: (row) => row.status === 'rejected' && row.createUser === userStore.id
      }
        showHide: (row) => row.status === 'rejected' && Number(userStore.id) === row.createUser
      },
      { name: '详情', clickFun: (row) => viewSealDetail(row) }
    ]
  }
])
// 搜索印章申请
const searchSealApplications = () => {
  page.current=1
  page.current = 1
  getSealApplicationList()
  // ElMessage.success('搜索完成')
}
// 重置印章申请搜索
const resetSealSearch = () => {
@@ -271,43 +297,52 @@
  sealSearchForm.applicationNum = ''
  searchSealApplications()
}
// 提交用印申请
// 重新申请用印
const reapplySeal = (row) => {
  // 预填表单数据
  Object.assign(sealForm, {
    id: row.id,
    applicationNum: row.applicationNum,
    title: row.title,
    sealType: row.sealType,
    reason: row.reason,
    urgency: row.urgency || 'normal',
    status: 'pending',
    attachmentList: row.commonFileList || []
  })
  showSealApplyDialog.value = true
}
// 提交用印申请(新增与重新申请均走 add 接口:重新申请会重新发起协同审批流程)
const submitSealApplication = async () => {
  try {
    await sealFormRef.value.validate()
    addSealApplication(sealForm).then(res => {
      if(res.code == 200){
        ElMessage.success('申请提交成功')
      if (res.code == 200) {
        ElMessage.success(sealForm.id ? '重新申请成功' : '申请提交成功')
        closeSealApplyDialog()
        getSealApplicationList()
        Object.assign(sealForm, {
        applicationNum: '',
        title: '',
        sealType: '',
        reason: '',
        approveUserId: '',
        urgency: 'normal',
        status: 'pending'
      })
      }
    }).catch(err => {
      console.log(err.msg)
    })
  } catch (error) {
  }
}
// 关闭用印申请对话框
const closeSealApplyDialog = () => {
  // 清空表单数据
  Object.assign(sealForm, {
    id: null,
    applicationNum: '',
    title: '',
    sealType: '',
    reason: '',
    approveUserId: '',
    urgency: 'normal',
    status: 'pending'
    status: 'pending',
    attachmentList: []
  })
  // 清除表单验证状态
  if (sealFormRef.value) {
@@ -325,55 +360,64 @@
  currentSealDetail.value = row
  showSealDetailDialog.value = true
}
// 审批用印申请
const approveSeal = (row) => {
  ElMessageBox.confirm('确认通过该用印申请?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    row.status = 'approved'
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('审批通过')
        getSealApplicationList()
      }
    })
  })
// 附件上传前校验(单个文件不超过 50MB)
const handleBeforeUpload = (file) => {
  if (file.size / 1024 / 1024 > 50) {
    ElMessage.error('文件大小不能超过 50MB')
    return false
  }
  return true
}
// 拒绝用印申请
const rejectSeal = (row) => {
  ElMessageBox.prompt('请输入拒绝原因', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    inputPattern: /\S+/,
    inputErrorMessage: '拒绝原因不能为空'
  }).then(({ value }) => {
    row.status = 'rejected'
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('已拒绝申请')
        getSealApplicationList()
      }
    })
  })
// 附件上传成功回调
const handleUploadSuccess = (res) => {
  if (res.code === 200) {
    const list = res.data || []
    list.forEach(item => sealForm.attachmentList.push({ name: item.name, url: item.url }))
    ElMessage.success('上传成功')
  } else {
    ElMessage.error(res.msg || '上传失败')
  }
}
// 附件上传失败回调
const handleUploadError = () => {
  ElMessage.error('附件上传失败')
}
// 附件数量超出
const handleUploadExceed = () => {
  ElMessage.warning('最多上传 10 个附件')
}
// 删除待提交附件
const removeAttachment = (index) => {
  sealForm.attachmentList.splice(index, 1)
}
// 预览文件(完整可访问 URL,直接打开,不拼 host)
const previewFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url
  if (url && filePreviewRef.value) {
    filePreviewRef.value.openUrl(url)
  } else {
    ElMessage.warning('文件地址无效,无法预览')
  }
}
// 重新申请用印
const reapplySeal = (row) => {
  // 预填表单数据
  Object.assign(sealForm, {
    applicationNum: row.applicationNum,
    title: row.title,
    sealType: row.sealType,
    reason: row.reason,
    approveUserId: row.approveUserId,
    urgency: row.urgency || 'normal',
    status: 'pending'
  })
  showSealApplyDialog.value = true
// 下载文件
const downloadFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download'
    const a = document.createElement('a')
    a.href = url
    a.download = filename
    a.target = '_blank'
    a.rel = 'noopener noreferrer'
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
  } else {
    ElMessage.warning('文件地址无效,无法下载')
  }
}
// 导出用印申请
const { proxy } = getCurrentInstance()
const handleExport = () => {
@@ -384,13 +428,13 @@
const getSealApplicationList = async () => {
  tableLoading.value = true
  listSealApplication(page, sealSearchForm)
  .then(res => {
    sealApplications.value = res.data.records
    page.total = res.data.total
    tableLoading.value = false
  }).catch(err => {
    tableLoading.value = false
  })
    .then(res => {
      sealApplications.value = res.data.records
      page.total = res.data.total
      tableLoading.value = false
    }).catch(err => {
      tableLoading.value = false
    })
}
// 分页变化处理
const paginationChange = (obj) => {
@@ -398,15 +442,6 @@
  page.size = obj.limit;
  getSealApplicationList();
};
// 监听对话框打开,获取用户列表
watch(showSealApplyDialog, (newVal) => {
  if (newVal) {
    userListNoPageByTenantId().then((res) => {
      userList.value = res.data;
    });
  }
});
onMounted(() => {
  // 路由携带 applicationNum 时,预填并查询
@@ -442,4 +477,19 @@
.ml-10 {
  margin-left: 10px;
}
</style>
.attachment-section {
  margin-top: 20px;
}
.attachment-title {
  font-size: 14px;
  color: #606266;
  margin-bottom: 10px;
  font-weight: 500;
}
.attachment-table {
  border-radius: 4px;
}
</style>
src/views/productionManagement/productionCosting/index.vue
@@ -67,7 +67,11 @@
              :tableLoading="tableLoading1"
              style="margin-right: 20px;"
              @pagination="pagination1"
          ></PIMTable>
          >
            <template #personWorkMinutes="{ row }">
              {{ row.personWorkMinutes ?? row.workMinutes ?? '-' }}
            </template>
          </PIMTable>
                </div>
            </el-col>
        </el-row>
@@ -173,6 +177,13 @@
    prop: "workMinutes",
    width: 110,
  },
  {
    label: "人员工时(分钟)",
    prop: "personWorkMinutes",
    width: 120,
    dataType: "slot",
    slot: "personWorkMinutes",
  },
]);
// 左侧汇总台账列(生产人、产量、工资、合格率)
src/views/productionManagement/workOrder/index.vue
@@ -24,13 +24,7 @@
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination">
        <template #completionStatus="{ row }">
          <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
                       :color="progressColor(toProgressPercentage(row?.completionStatus))"
                       :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
        </template>
      </PIMTable>
                @pagination="pagination" />
    </div>
    <el-dialog v-model="editDialogVisible"
               title="编辑时间"
@@ -289,6 +283,19 @@
          <!--          </el-col>-->
        </el-row>
      </el-form>
      <el-table :data="personList" border style="margin-top: 12px;">
        <el-table-column label="生产人" prop="userName" min-width="100" align="center" />
        <el-table-column label="产量" min-width="120" align="center">
          <template #default="scope">
            <el-input-number v-model="scope.row.quantity" :min="0" :controls="false" placeholder="产量" style="width: 100%" />
          </template>
        </el-table-column>
        <el-table-column label="生产时长(分钟)" min-width="140" align="center">
          <template #default="scope">
            <el-input-number v-model="scope.row.workHour" :min="0" :controls="false" placeholder="时长(分钟)" style="width: 100%" />
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
@@ -490,6 +497,7 @@
    productWorkOrderPage,
    updateProductWorkOrder,
    addProductMain,
    getProductWorkOrderById,
    downProductWorkOrder,
    addProductionMachineRecord,
    productionMachineRecordListPage,
@@ -1075,25 +1083,13 @@
      width: "140",
    },
    {
      label: "加放数",
      prop: "processRouteAddNum",
      width: "100",
    },
    {
      label: "完成数量",
      prop: "completeQuantity",
      width: "140",
    },
    {
      label: "完成进度",
      prop: "completionStatus",
      dataType: "slot",
      slot: "completionStatus",
      width: "140",
    },
    {
      label: "计划开始时间",
      prop: "planStartTime",
      width: "140",
    },
    {
      label: "计划结束时间",
      prop: "planEndTime",
      width: "140",
    },
    {
@@ -1136,11 +1132,10 @@
          clickFun: row => {
            showReportDialog(row);
          },
          // 用户当前id在工单报工人或工序报工人中
          // 用户当前id在工单报工人或工序报工人中,且完成数量未达计划数量时可继续分批报工
          disabled: row =>
            row.completeQuantity >= row.planQuantity ||
            !canCurrentUserReport(row) ||
            row.hasUnreportedMachine,
            !canCurrentUserReport(row),
        },
        {
          name: "生产排产",
@@ -1218,6 +1213,8 @@
    teamList: [],
    deviceId: null,
  });
  // 班组人员产量时长明细 [{ userId, userName, quantity, workHour }]
  const personList = ref([]);
  const teamSelectOptions = computed(() => {
    const base =
      Array.isArray(reportForm.userIdsList) && reportForm.userIdsList.length > 0
@@ -1545,7 +1542,7 @@
      });
  };
  const showReportDialog = row => {
  const showReportDialog = async row => {
    currentReportRowData.value = row;
    reportForm.planQuantity = row.planQuantity - row.completeQuantity;
    reportForm.quantity = row.planQuantity - row.completeQuantity;
@@ -1558,7 +1555,7 @@
    reportForm.replenishQty = 0;
    reportForm.teamList = [];
    reportForm.scrapQty = 0;
    reportForm.addQty = 0;
    reportForm.addQty = row.processRouteAddNum ? Number(row.processRouteAddNum) : 0;
    reportForm.userIds = row.userIds || [];
    // 合并工单报工人和工序报工人,去重
@@ -1577,6 +1574,34 @@
        userId: item.userId,
        nickName: item.nickName,
      }));
    // 获取报工弹窗数据,带出默认班组成员(合并排产人员与机台操作员,去重)
    personList.value = [];
    try {
      const res = await getProductWorkOrderById(row.id);
      const reportPersons = (Array.isArray(res?.data?.reportPersons)
        ? res.data.reportPersons
        : []
      )
        .filter(p => p && typeof p === "object")
        .map(p => ({
          userId: p.userId ?? p.id ?? "",
          userName: String(p.userName || p.nickName || "").trim(),
        }))
        .filter(p => p.userName !== "");
      personList.value = reportPersons.map(p => ({
        userId: p.userId,
        userName: p.userName,
        quantity: null,
        workHour: null,
      }));
      reportForm.teamList = reportPersons.map(p => ({
        userId: p.userId,
        userName: p.userName,
      }));
    } catch (err) {
      console.error("获取报工弹窗数据失败", err);
    }
    nextTick(() => {
      reportFormRef.value?.clearValidate();
@@ -1649,10 +1674,23 @@
      //   return;
      // }
      // 校验各班组产量之和等于报工总产量
      const personQuantityTotal = personList.value.reduce(
        (sum, p) => sum + (Number(p.quantity) || 0),
        0
      );
      if (personQuantityTotal !== quantity) {
        ElMessageBox.alert("各班组产量之和必须等于报工总产量", "提示", {
          confirmButtonText: "确定",
        });
        return;
      }
      const submitData = {
        ...reportForm,
        quantity: quantity,
        scrapQty: scrapQty,
        personList: personList.value,
      };
      addProductMain(submitData).then(res => {
        if (res.code === 200) {
src/views/safeProduction/safeWorkApproval/components/approvalDia.vue
@@ -198,29 +198,28 @@
      <el-form :model="{ activities }"
               ref="formRef"
               label-position="top">
        <el-steps :active="getActiveStep()"
                <el-steps :active="getActiveStep()"
                  finish-status="success"
                  process-status="process"
                  align-center
                  direction="vertical">
          <el-step v-for="(activity, index) in activities"
                   :key="index"
          <el-step v-for="(group, gIndex) in stepGroups"
                   :key="gIndex"
                   finish-status="success"
                   :title="getNodeTitle(index, activities.length)"
                   :description="activity.approveNodeUser"
                   :icon="getNodeIcon(activity, index)">
                   :title="getNodeTitle(gIndex, stepGroups.length)"
                   :icon="getNodeIcon(group)">
            <template #icon>
              <el-icon v-if="activity.approveNodeStatus === 2"
              <el-icon v-if="group.some(a => a.approveNodeStatus === 2)"
                       color="red"
                       :size="22">
                <WarningFilled />
              </el-icon>
              <el-icon v-else-if="activity.isShen"
              <el-icon v-else-if="group.some(a => a.isShen)"
                       color="#1890ff"
                       :size="22">
                <Edit />
              </el-icon>
              <el-icon v-else-if="activity.approveNodeStatus === 1"
              <el-icon v-else-if="group.every(a => a.approveNodeStatus === 1)"
                       color="#67C23A"
                       :size="26">
                <Check />
@@ -232,37 +231,42 @@
              </el-icon>
            </template>
            <template #title>
              <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
              <span style="color: #000000">{{ getNodeTitle(gIndex, stepGroups.length) }}</span>
              <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'" size="small" style="margin-left: 6px">{{ group[0]?.approveMode === 'and' ? '会签' : '或签' }}</el-tag>
            </template>
            <template #description>
              <div class="node-user">
                <div class="avatar-wrapper">
                  <img :src="userStore.avatar"
                       class="user-avatar"
                       alt="" />
              <div v-for="(activity, aIndex) in group"
                   :key="activity.id || aIndex"
                   class="node-user-item">
                <div class="node-user">
                  <div class="avatar-wrapper">
                    <img :src="userStore.avatar"
                         class="user-avatar"
                         alt="" />
                  </div>
                  <span style="color: #000000">{{ activity.approveNodeUser }}({{ activity.isApproval }})</span>
                </div>
                <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
              </div>
              <div v-if="!activity.isShen"
                   class="node-reason">
                <span>审批意见:</span>{{ activity.approveNodeReason }}
              </div>
              <div v-if="!activity.isShen"
                   class="node-reason">
                <span>签名:</span>
                <img :src="activity.urlTem"
                     class="signImg"
                     alt=""
                     v-if="activity.urlTem" />
              </div>
              <div v-else-if="activity.isShen">
                <el-form-item :prop="'activities.' + index + '.approveNodeReason'"
                              :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]">
                  <el-input v-model="activity.approveNodeReason"
                            clearable
                            type="textarea"
                            :disabled="operationType === 'view'"></el-input>
                </el-form-item>
                <div v-if="!activity.isShen"
                     class="node-reason">
                  <span>审批意见:</span>{{ activity.approveNodeReason }}
                </div>
                <div v-if="!activity.isShen"
                     class="node-reason">
                  <span>签名:</span>
                  <img :src="activity.urlTem"
                       class="signImg"
                       alt=""
                       v-if="activity.urlTem" />
                </div>
                <div v-else-if="activity.isShen">
                  <el-form-item :prop="'activities.' + getActivityIndex(activity) + '.approveNodeReason'"
                                :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]">
                    <el-input v-model="activity.approveNodeReason"
                              clearable
                              type="textarea"
                              :disabled="operationType === 'view'"></el-input>
                  </el-form-item>
                </div>
              </div>
            </template>
          </el-step>
@@ -348,19 +352,31 @@
    return "审批";
  };
  // 获取当前激活步骤
  // 获取当前激活步骤(分组后:含待审人的组下标,无则返回组总数=全部完成)
  const getActiveStep = () => {
    // 如果所有 isShen 都为 false,返回最后一个步骤(全部完成)
    const hasActive = activities.value.some(a => a.isShen === true);
    if (!hasActive) return activities.value.length;
    // 当前节点索引
    return activities.value.findIndex(a => a.isShen == true);
    const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true));
    if (idx === -1) return stepGroups.value.length;
    return idx;
  };
  // 步骤icon
  const getNodeIcon = (activity, index) => {
    if (activity.approveNodeStatus === 2) return "el-icon-warning"; // 不通过
    if (activity.isShen) return "Edit";
  // 步骤icon(组级:任一人驳回/任一人待审)
  const getNodeIcon = group => {
    if (group.some(a => a.approveNodeStatus === 2)) return "el-icon-warning"; // 不通过
    if (group.some(a => a.isShen)) return "Edit";
    return "";
  };
  // 审批节点分组(同一 approveNodeOrder = 同一节点的多名审批人,或签/会签)
  const stepGroups = computed(() => {
    const map = new Map();
    (activities.value || []).forEach(a => {
      const order = a.approveNodeOrder;
      if (!map.has(order)) map.set(order, []);
      map.get(order).push(a);
    });
    return Array.from(map.values());
  });
  // 获取节点成员在 activities 中的索引(用于表单校验 prop)
  const getActivityIndex = activity => {
    return activities.value.findIndex(a => a.id === activity.id);
  };
  // 打开弹框
@@ -474,10 +490,8 @@
      return;
    }
    currentActivity.approveNodeStatus = status;
    // 判断是否为最后一步
    const isLast =
      activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
    updateApproveNode({ ...currentActivity, isLast }).then(() => {
    // 流转判断由后端完成,前端不再传 isLast
    updateApproveNode({ ...currentActivity }).then(() => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
    });
@@ -506,6 +520,9 @@
    align-items: center;
    gap: 8px;
  }
  .node-user-item {
    margin: 8px 0;
  }
  .node-status {
    color: #1890ff;
    margin-left: 8px;
src/views/safeProduction/safeWorkApproval/index.vue
@@ -83,7 +83,16 @@
  import useUserStore from "@/store/modules/user";
  const userStore = useUserStore();
  // 当前用户是否为管理员(角色 普通2/admin/最高1),可不受审批人限制审批
  const isAdminUser = computed(() =>
    ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r))
  );
  const route = useRoute();
  // 管理员账号(角色为 普通2 / admin / 最高1)可审批任意流程,不受审批人限制
  const isAdminUser = computed(() =>
    ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r))
  );
  // 当前审批类型,根据选中的标签页计算
  const currentApproveType = computed(() => {
@@ -179,11 +188,11 @@
      }
    );
    // 当前审批人列
    // 当前审批人列(支持同节点多审批人)
    baseColumns.push({
      label: "当前审批人",
      prop: "approveUserCurrentName",
      width: 120,
      prop: "currentApproverNames",
      width: 180,
    });
    // 操作列
@@ -212,11 +221,10 @@
            openApprovalDia("approval", row);
          },
          disabled: row =>
            row.approveUserCurrentId == null ||
            row.approveStatus == 2 ||
            row.approveStatus == 3 ||
            row.approveStatus == 4 ||
            row.approveUserCurrentId !== userStore.id,
            !row._canApprove,
        },
        {
          name: "详情",
@@ -273,7 +281,12 @@
    })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        // 富化行数据:当前审批人显示(多值兜底旧单值)+ 当前用户是否可审核
        tableData.value = (res.data.records || []).map(r => ({
          ...r,
          currentApproverNames: r.approveUserCurrentNames || r.approveUserCurrentName || '-',
          _canApprove: isAdminUser.value || String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) || r.approveUserCurrentId === userStore.id
        }));
        page.total = res.data.total;
      })
      .catch(err => {
src/views/salesManagement/salesLedger/index.vue
@@ -647,7 +647,7 @@
                               :min="0"
                               v-model="productForm.taxInclusiveUnitPrice"
                               style="width: 100%"
                               :precision="4"
                               :precision="5"
                               placeholder="请输入"
                               clearable
                               @change="calculateFromQuantity" />
@@ -1179,6 +1179,12 @@
    if (Number.isNaN(num)) return "0.0000";
    return num.toFixed(4);
  };
  const formatAmount5 = value => {
    if (value === undefined || value === null || value === "") return "0.00000";
    const num = Number(value);
    if (Number.isNaN(num)) return "0.00000";
    return num.toFixed(5);
  };
  const findLedgerRecordByRow = row => {
    if (!row) return null;
    if (
@@ -1663,7 +1669,7 @@
          productForm.value.taxInclusiveUnitPrice !== null &&
          productForm.value.taxInclusiveUnitPrice !== ""
        ) {
          productForm.value.taxInclusiveUnitPrice = formatAmount4(
          productForm.value.taxInclusiveUnitPrice = formatAmount5(
            productForm.value.taxInclusiveUnitPrice
          );
        }
@@ -2298,7 +2304,7 @@
    productForm.value.taxInclusiveTotalPrice = totalPrice.toFixed(4);
    // 计算含税单价 = 含税总价 / 数量
    productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(4);
    productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(5);
    // 如果有税率,计算不含税总价
    if (productForm.value.taxRate) {
@@ -2342,7 +2348,7 @@
    // 计算含税单价 = 含税总价 / 数量
    productForm.value.taxInclusiveUnitPrice = (
      inclusiveTotalPrice / quantity
    ).toFixed(4);
    ).toFixed(5);
    isCalculating.value = false;
  };
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
      env.VITE_APP_ENV === "development"
          ? "http://1.15.17.182:9038"
          ? "http://192.168.0.10:7003"
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"