2
0

2 Коммиты 6d34666905 ... 6c6b2eef0d

Автор SHA1 Сообщение Дата
  lihaiting 6c6b2eef0d 员工档案「隶属部门」改为非必填:去掉 deptIds 的 required 与「请选择隶属部门」校验(2026-10-29 用户要求),保留 validator 空壳只为口径统一;必填进度 x/y 的 keys 同步移除 deptIds,仍保留在 sectionProgress 完整度统计里,避免「必填数不对、一直填不满」 4 дней назад
  lihaiting aa09aaca02 新增考勤明细页 + 各审批列表组织信息拆列:attendance/daily 新增「考勤明细」页(GET /hr/attendance/daily 分页,夜班归属班次开始日;打卡方式/打卡情况文案统一在 api 层映射、补卡来源优先标「补卡」,接口没返回的经纬度直说未返回,不拿 0 坐标顶替),注册菜单与路由 attendance/daily;入职/转正/离职交接/物品领用/录用审批等列表把「组织信息」一列拆成「所属机构」「所属部门」,一律直取接口字段(groupName/organizationName/targetDepartmentName/legalEntityName/deptName),没返回就显示 -,不再拿其他模块字段拼接;调动申请「原岗位 / 部门」改回单列并裁掉历史数据里拼进去的部门串;离职列表补人员信息改走 POST /main/user/batch 批量取(原逐人 getEmployeeById 的 N+1),详情场景保留 full=true 逐人补全;岗位序列筛选去掉 draftFilters 草稿态,输入/下拉直接生效;考勤统计页删掉底部「考勤明细」表;员工档案导入入口暂时隐藏(方法保留,注释即可恢复)、主所属部门常显必填星号、所属工厂与所属机构级联且未选机构时禁用;状态 tab 边框只去左边和下边;工资条详情对已有更正草稿加提示 tag 4 дней назад

+ 109 - 0
src/api/hr/attendance.js

@@ -1868,4 +1868,113 @@ export function exceptionLabel(type) {
   return EXCEPTION_LABEL[type] || type || '其他';
 }
 
+/* ==================== 考勤明细(每日考勤 GET /hr/attendance/daily) ==================== */
+
+/** 打卡方式展示文案(punchRecords.punchMethod) */
+export const DAILY_PUNCH_METHOD_LABEL = {
+  GPS: 'GPS打卡',
+  WIFI: 'WiFi打卡',
+  BEACON: '蓝牙打卡',
+  ATTENDANCE_MACHINE: '考勤机打卡',
+  FACE: '人脸打卡',
+  PC: 'PC打卡',
+  FIELD: '外勤打卡',
+};
+
+export function dailyPunchMethodLabel(method) {
+  return DAILY_PUNCH_METHOD_LABEL[method] || method || '';
+}
+
+/** ISO 时间 → yyyy-MM-dd HH:mm:ss(无时间部分只留日期) */
+function formatDailyDateTime(value) {
+  const text = String(value || '');
+  if (!text) return '';
+  const date = text.slice(0, 10);
+  const time = text.length >= 19 ? text.slice(11, 19) : '';
+  return time ? `${date} ${time}` : date;
+}
+
+/** 打卡记录「打卡情况」:补卡来源优先标补卡,其余按 status 出文案 */
+export function dailyPunchStatusText(record = {}) {
+  if (record.sourceType === 'REPAIR') return '补卡';
+  return { VALID: '正常', INVALID: '无效', PENDING_APPROVAL: '待审批' }[record.status] || record.status || '';
+}
+
+/**
+ * 分页查询每日考勤(夜班归属班次开始日)
+ * GET /hr/attendance/daily
+ * query: dateEnd, dateStart, deptId, employeeKeyword, keyword, pageNum, periodId, punchMethod, size, status, userId
+ */
+export const getDailyAttendanceDetailPage = async (params = {}) => {
+  const query = compact({
+    dateEnd: params.dateEnd || undefined,
+    dateStart: params.dateStart || undefined,
+    deptId: keepBigIntId(params.deptId),
+    employeeKeyword: params.employeeKeyword || undefined,
+    keyword: params.keyword || undefined,
+    pageNum: params.pageNum != null ? Number(params.pageNum) : 1,
+    periodId: keepBigIntId(params.periodId),
+    punchMethod: params.punchMethod || undefined,
+    size: params.size != null ? Number(params.size) : 20,
+    status: params.status || undefined,
+    userId: keepBigIntId(params.userId),
+  });
+  return pageResult(await unwrap(await request.get('/hr/attendance/daily', { params: query })));
+};
+
+/**
+ * 每日考勤结果返回模型 → 页面行模型。
+ * ★ 接口不返回姓名/部门(只回 userId):姓名、工号、部门走 loadEmployeeBriefs 主数据批量接口补齐,
+ *   查不到就留空;打卡地址、工作地址接口完全没有对应字段,恒留空,缺口转后端改造清单。
+ */
+export function adaptDailyAttendance(raw = {}, brief = null) {
+  const records = Array.isArray(raw.punchRecords) ? raw.punchRecords : [];
+  const statusList = Array.isArray(raw.statusList) ? raw.statusList : [];
+  return {
+    id: keepBigIntId(raw.id),
+    userId: keepBigIntId(raw.userId),
+    name: brief?.name || '',
+    employeeNo: brief?.employeeNo || '',
+    department: brief?.department || '',
+    attendanceDate: String(raw.attendanceDate || '').slice(0, 10),
+    statusList,
+    statusText: statusList.map((item) => exceptionLabel(item)).join('、'),
+    punchTypes: [
+      ...new Set(records.map((item) => dailyPunchMethodLabel(item.punchMethod)).filter(Boolean)),
+    ].join('、'),
+    /* 接口未返回打卡地址(文档明确 punchRecords 不含定位),留空待后端补充 */
+    punchAddress: '',
+    punchStatuses: records.map((item) => ({
+      punchRecordId: keepBigIntId(item.punchRecordId),
+      punchTime: formatDailyDateTime(item.punchTime),
+      punchMethod: dailyPunchMethodLabel(item.punchMethod),
+      punchType: item.punchType || '',
+      sourceType: item.sourceType || '',
+      status: item.status || '',
+      statusText: dailyPunchStatusText(item),
+    })),
+    actualIn: formatDailyDateTime(raw.actualIn),
+    actualOut: formatDailyDateTime(raw.actualOut),
+    /* 接口未返回工作地址,留空待后端补充 */
+    workAddress: '',
+    scheduledStart: formatDailyDateTime(raw.scheduledStart),
+    scheduledEnd: formatDailyDateTime(raw.scheduledEnd),
+    requiredMinutes: raw.requiredMinutes ?? null,
+    actualWorkMinutes: raw.actualWorkMinutes ?? null,
+    effectiveWorkMinutes: raw.effectiveWorkMinutes ?? null,
+    lateMinutes: raw.lateMinutes ?? null,
+    earlyMinutes: raw.earlyMinutes ?? null,
+    absenceMinutes: raw.absenceMinutes ?? null,
+    leaveMinutes: raw.leaveMinutes ?? null,
+    outingMinutes: raw.outingMinutes ?? null,
+    tripMinutes: raw.tripMinutes ?? null,
+    overtimeMinutes: raw.overtimeMinutes ?? null,
+    nightMinutes: raw.nightMinutes ?? null,
+    exceptionCount: raw.exceptionCount ?? null,
+    calculationStatus: raw.calculationStatus || '',
+    lastCalcTime: formatDailyDateTime(raw.lastCalcTime),
+    raw,
+  };
+}
+
 

+ 13 - 13
src/api/hr/onboarding.js

@@ -175,23 +175,23 @@ function adaptOnboardingRecord(raw = {}, extras = {}) {
       "",
     name: raw.name || raw.employeeName || extras.candidateName || "",
     employeeName: raw.employeeName || raw.name || extras.candidateName || "",
-    department:
-      extras.departmentName ||
-      raw.targetDepartmentName ||
-      raw.departmentName ||
-      raw.deptName ||
-      "",
+    /**
+     * 组织信息字段(只认接口原始字段,没返回就留空,不做多来源拼接兜底):
+     *   groupName / deptName / deptNames = 后端组织三段快照,保留透传;
+     *   列表已拆成「所属机构 organizationName」「所属部门 targetDepartmentName」两列,
+     *   这里不再用它们拼一列展示(2026-10-29 用户要求)。
+     */
+    groupName: raw.groupName || "",
+    deptName: raw.deptName || "",
+    deptNames: raw.deptNames || "",
+    /* 以下两个仍保留给「发起入职审批」等既有逻辑用(只取单一接口字段,无兜底链) */
+    department: raw.targetDepartmentName || "",
+    company: raw.organizationName || "",
     deptId,
     targetDepartmentId: raw.targetDepartmentId ?? deptId,
-    company:
-      extras.company ||
-      raw.organizationName ||
-      raw.company ||
-      raw.legalName ||
-      "",
     legalId,
     organizationId: raw.organizationId ?? legalId,
-    organizationName: raw.organizationName || extras.company || "",
+    organizationName: raw.organizationName || "",
     position: raw.positionName || extras.positionName || "",
     positionName: raw.positionName || extras.positionName || "",
     positionId: extras.positionId || raw.positionId,

+ 51 - 29
src/api/hr/resignation.js

@@ -1,5 +1,5 @@
 import request from "@/utils/request";
-import { adaptEmployee, getEmployeeById } from "@/api/organization";
+import { adaptEmployee, getEmployeeById, loadEmployeeBriefs } from "@/api/organization";
 
 function unwrap(response) {
   const result = response?.data || {};
@@ -1002,8 +1002,11 @@ export const getResignationTemplatePage = async (params = {}) =>
     }),
   );
 
-export async function withEmployeeInfo(rows = []) {
+export async function withEmployeeInfo(rows = [], options = {}) {
   const list = Array.isArray(rows) ? rows : [];
+  /* full=true:详情/发起审批等单人场景,逐人取员工档案补全岗位、入职日期等完整字段;
+     默认(列表):走 POST /main/user/batch 批量取简要信息,一次请求解决整页,不再 N+1 */
+  const full = options.full === true;
   const userIds = new Set();
   list.forEach((row) => {
     if (row.userId != null && row.userId !== "") userIds.add(row.userId);
@@ -1014,18 +1017,27 @@ export async function withEmployeeInfo(rows = []) {
     });
   });
   const employees = new Map();
-  await Promise.all(
-    [...userIds].map(async (userId) => {
-      try {
-        employees.set(
-          String(userId),
-          adaptEmployee(await getEmployeeById(userId)),
-        );
-      } catch (error) {
-        /* 员工档案失败时仍展示离职单字段 */
-      }
-    }),
-  );
+  if (full) {
+    await Promise.all(
+      [...userIds].map(async (userId) => {
+        try {
+          employees.set(
+            String(userId),
+            adaptEmployee(await getEmployeeById(userId)),
+          );
+        } catch (error) {
+          /* 员工档案失败时仍展示离职单字段 */
+        }
+      }),
+    );
+  } else {
+    try {
+      const briefs = await loadEmployeeBriefs([...userIds]);
+      briefs.forEach((brief, id) => employees.set(String(id), brief));
+    } catch (error) {
+      /* 批量接口失败时仍展示离职单字段,姓名等留空由后端补 */
+    }
+  }
   const patchItems = (items = []) =>
     (items || []).map((item) => {
       const receiver = employees.get(String(item.receiverUserId));
@@ -1042,21 +1054,31 @@ export async function withEmployeeInfo(rows = []) {
       : row.handover;
     return adaptResignation(
       { ...row, handover, items },
-      {
-        employeeName: employee?.name,
-        name: employee?.name,
-        employeeNo: employee?.employeeNo,
-        groupId: employee?.groupId,
-        company: employee?.company || employee?.legalEntity,
-        deptId: employee?.deptId,
-        department: employee?.deptName || employee?.department,
-        positionId: employee?.positionId,
-        position: employee?.position,
-        positionSequenceId: employee?.positionBindings?.[0]?.positionSequenceId,
-        positionSequence: employee?.positionBindings?.[0]?.positionSequenceName,
-        hireDate: employee?.hireDate,
-        handoverPerson: items.find((item) => item.receiverName)?.receiverName,
-      },
+      full
+        ? {
+            employeeName: employee?.name,
+            name: employee?.name,
+            employeeNo: employee?.employeeNo,
+            groupId: employee?.groupId,
+            company: employee?.company || employee?.legalEntity,
+            deptId: employee?.deptId,
+            department: employee?.deptName || employee?.department,
+            positionId: employee?.positionId,
+            position: employee?.position,
+            positionSequenceId: employee?.positionBindings?.[0]?.positionSequenceId,
+            positionSequence: employee?.positionBindings?.[0]?.positionSequenceName,
+            hireDate: employee?.hireDate,
+            handoverPerson: items.find((item) => item.receiverName)?.receiverName,
+          }
+        : {
+            /* 批量接口只补 姓名 / 工号 / 交接接收人姓名。部门、机构等展示字段只认离职单
+               分页接口自带字段(departmentName/deptName),批量接口的 deptName 数据不可靠
+               (曾出现公司名/流程状态),不拿来顶替;离职单接口没返回就显示 -,由后端补。 */
+            employeeName: employee?.name,
+            name: employee?.name,
+            employeeNo: employee?.employeeNo,
+            handoverPerson: items.find((item) => item.receiverName)?.receiverName,
+          },
     );
   });
 }

+ 5 - 0
src/config/menus.js

@@ -160,6 +160,11 @@ export const DEFAULT_MENUS = [
         component: 'attendance/report/index',
         meta: { title: '考勤统计' },
       },
+      {
+        path: '/attendance/daily',
+        component: 'attendance/daily/index',
+        meta: { title: '考勤明细' },
+      },
     ],
   },
   {

+ 2 - 0
src/router/routes.js

@@ -58,6 +58,7 @@ const VIEW_LOADERS = {
   'attendance/rule/index': () => import('@/views/attendance/rule/index.vue'),
   'attendance/report/index': () =>
     import('@/views/attendance/report/index.vue'),
+  'attendance/daily/index': () => import('@/views/attendance/daily/index.vue'),
   'hrAnalysis/personnelStructureDashboard/index': () =>
     import('@/views/hrAnalysis/personnelStructureDashboard/index.vue'),
   'hrAnalysis/personnelStructureAnalysisBoard/index': () =>
@@ -108,6 +109,7 @@ const PATH_COMPONENT_MAP = {
   '/contract-management': 'contractManagement/index',
   '/attendance/rule': 'attendance/rule/index',
   '/attendance/report': 'attendance/report/index',
+  '/attendance/daily': 'attendance/daily/index',
   '/personnel-structure-dashboard':
     'hrAnalysis/personnelStructureDashboard/index',
   '/personnel-structure-analysis-board':

+ 5 - 2
src/styles/views/employeeArchive/index.scss

@@ -53,7 +53,9 @@
   min-width: 0;
 }
 /* 员工状态 tab(2026-10-28 用户用局部截图定稿,已按像素量过):
-   分段式按钮组 —— 外层 1px 浅灰描边 + 4px 圆角 + 白底;
+   分段式按钮组 —— 4px 圆角 + 白底;
+   2026-10-29 用户要求去掉衔接处的边框,随后补充:只去左边和下边,
+   上面(与搜索区交界)和右面(全部 tab 右侧)的边框要保留;
    每段之间 1px 竖分割线;选中只把文字变蓝(不加底色 / 下划线 / 顶部条);
    实测:整体高约 40px、字号 14px、每段左右留白约 18px。
    位置:搜索区下方、表格工具栏之外(不在表头区域内) */
@@ -63,7 +65,8 @@
   max-width: 100%;
   margin: 2px 0 0;
   overflow: hidden;
-  border: 1px solid #dcdfe6;
+  border-top: 1px solid #dcdfe6;
+  border-right: 1px solid #dcdfe6;
   border-radius: 4px;
   background: #fff;
 }

+ 38 - 0
src/views/attendance/components/RuleMethodPanel.vue

@@ -56,6 +56,15 @@
         <el-table :data="form.locations || []" class="location-table" size="small" empty-text="暂未添加打卡位置">
           <el-table-column type="index" label="序号" width="60" align="center" />
           <el-table-column prop="name" label="地点名称" minWidth="220" />
+          <!-- 经纬度:数据来自规则详情 locations[].longitude / latitude(保存时同名字段回传)。
+               接口没给就直说没返回,不拿 0 或默认坐标顶替(经纬度 0,0 是几内亚湾,会被当成真实坐标)。 -->
+          <el-table-column label="经纬度" minWidth="200">
+            <template slot-scope="{ row }">
+              <span v-if="coordText(row)" class="coord-text">{{ coordText(row) }}</span>
+              <span v-else class="coord-missing"
+                title="接口未返回经纬度(longitude / latitude),请后端补充">经纬度未返回</span>
+            </template>
+          </el-table-column>
           <el-table-column prop="range" label="有效范围" width="160" />
           <el-table-column v-if="!readonly" label="操作" width="80" align="left">
             <template slot-scope="{ $index }">
@@ -174,6 +183,17 @@ function normalizeOptionId(id) {
   return String(id);
 }
 
+/**
+ * 经纬度展示格式:与「添加打卡位置」弹框的 formatCoord 口径一致(6 位小数、去掉多余的 0)。
+ * 只做格式化,不做补值——拿不到数值就返回空串,由模板显示「未返回」。
+ */
+function formatCoord(value) {
+  if (value === null || value === undefined || value === '') return '';
+  const number = Number(value);
+  if (!Number.isFinite(number)) return '';
+  return String(Number(number.toFixed(6)));
+}
+
 export default {
   name: 'RuleMethodPanel',
   props: {
@@ -227,6 +247,13 @@ export default {
     this.syncFromForm(true);
   },
   methods: {
+    /** 打卡位置行的经纬度文本:「经度, 纬度」;任一项缺失视为未返回(不拼 0.000000) */
+    coordText(row) {
+      const longitude = formatCoord(row?.longitude);
+      const latitude = formatCoord(row?.latitude);
+      if (!longitude || !latitude) return '';
+      return `${longitude}, ${latitude}`;
+    },
     onOutOfRangeChange(value) {
       (this.form.locations || []).forEach((item, index) => {
         this.$set(this.form.locations[index], 'outsidePolicy', value);
@@ -510,4 +537,15 @@ export default {
   margin-right: 2px;
   color: #f56c6c;
 }
+
+/* 经纬度列:等宽数字,读起来不跳动;缺失态用浅橙提示,弱化但不隐藏 */
+.coord-text {
+  color: #303133;
+  font-variant-numeric: tabular-nums;
+  letter-spacing: 0.2px;
+}
+.coord-missing {
+  color: #e6a23c;
+  cursor: help;
+}
 </style>

+ 315 - 0
src/views/attendance/daily/index.vue

@@ -0,0 +1,315 @@
+<template>
+  <div class="ele-body">
+    <el-card shadow="never" class="daily-card">
+      <!-- 筛选区:姓名 / 所属部门 / 日期(与设计图一致) -->
+      <el-form inline :model="filter" class="daily-filter" @submit.native.prevent>
+        <el-form-item label="姓名">
+          <el-input
+            v-model="filter.employeeKeyword"
+            placeholder="姓名模糊/工号精确"
+            clearable
+            style="width: 200px"
+            @keyup.enter.native="search"
+            @clear="search"
+          />
+        </el-form-item>
+        <el-form-item label="所属部门">
+          <el-select
+            v-model="filter.deptId"
+            placeholder="全部部门"
+            clearable
+            filterable
+            style="width: 220px"
+            @change="search"
+          >
+            <el-option
+              v-for="item in filterOrgOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="日期">
+          <el-date-picker
+            v-model="filter.dateRange"
+            type="daterange"
+            value-format="yyyy-MM-dd"
+            range-separator="-"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            style="width: 260px"
+            @change="search"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
+          <el-button icon="el-icon-refresh" @click="reset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <ele-pro-table
+        ref="table"
+        row-key="id"
+        :columns="columns"
+        :datasource="fetchPage"
+        :need-page="true"
+        :page-size="20"
+        :page-sizes="[10, 20, 50]"
+        :cache-key="$tableCacheKey('hr-attendance-daily')"
+      >
+        <template v-slot:toolbar>
+          <!-- 后端暂无导入接口:点击如实提示,缺口已转后端改造清单 -->
+          <el-button icon="el-icon-upload2" @click="onImport">导入excel</el-button>
+          <el-button type="primary" icon="el-icon-download" @click="onExport">excel导出</el-button>
+        </template>
+
+        <template slot="name" slot-scope="{ row }">
+          <span>{{ row.name }}</span>
+        </template>
+
+        <template slot="statusText" slot-scope="{ row }">
+          <span>{{ row.statusText }}</span>
+        </template>
+
+        <template slot="punchStatuses" slot-scope="{ row }">
+          <template v-if="row.punchStatuses.length">
+            <el-tag
+              v-for="item in row.punchStatuses"
+              :key="item.punchRecordId"
+              size="mini"
+              :type="punchStatusTagType(item)"
+              class="punch-tag"
+            >
+              {{ item.statusText }}
+            </el-tag>
+          </template>
+        </template>
+
+        <template slot="action" slot-scope="{ row }">
+          <el-link type="primary" :underline="false" @click="viewRow(row)">查看</el-link>
+        </template>
+
+        <template slot="empty">
+          <el-empty description="暂无数据" />
+        </template>
+      </ele-pro-table>
+    </el-card>
+
+    <!-- 查看详情弹框 -->
+    <ele-modal
+      title="考勤明细详情"
+      :visible.sync="detailVisible"
+      width="720px"
+      append-to-body
+    >
+      <el-descriptions v-if="detailRow" :column="2" border size="small">
+        <el-descriptions-item label="姓名">{{ detailRow.name }}</el-descriptions-item>
+        <el-descriptions-item label="工号">{{ detailRow.employeeNo }}</el-descriptions-item>
+        <el-descriptions-item label="所属部门">{{ detailRow.department }}</el-descriptions-item>
+        <el-descriptions-item label="日期">{{ detailRow.attendanceDate }}</el-descriptions-item>
+        <el-descriptions-item label="出勤情况" :span="2">{{ detailRow.statusText }}</el-descriptions-item>
+        <el-descriptions-item label="计划上班">{{ detailRow.scheduledStart }}</el-descriptions-item>
+        <el-descriptions-item label="计划下班">{{ detailRow.scheduledEnd }}</el-descriptions-item>
+        <el-descriptions-item label="上班打卡时间">{{ detailRow.actualIn }}</el-descriptions-item>
+        <el-descriptions-item label="下班打卡时间">{{ detailRow.actualOut }}</el-descriptions-item>
+        <el-descriptions-item label="应出勤(分钟)">{{ minuteText(detailRow.requiredMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="实际工时(分钟)">{{ minuteText(detailRow.actualWorkMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="有效工时(分钟)">{{ minuteText(detailRow.effectiveWorkMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="异常数量">{{ detailRow.exceptionCount ?? '' }}</el-descriptions-item>
+        <el-descriptions-item label="迟到(分钟)">{{ minuteText(detailRow.lateMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="早退(分钟)">{{ minuteText(detailRow.earlyMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="旷工(分钟)">{{ minuteText(detailRow.absenceMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="请假(分钟)">{{ minuteText(detailRow.leaveMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="外出(分钟)">{{ minuteText(detailRow.outingMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="出差(分钟)">{{ minuteText(detailRow.tripMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="加班(分钟)">{{ minuteText(detailRow.overtimeMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="夜班(分钟)">{{ minuteText(detailRow.nightMinutes) }}</el-descriptions-item>
+        <el-descriptions-item label="计算状态">{{ calculationStatusText(detailRow.calculationStatus) }}</el-descriptions-item>
+        <el-descriptions-item label="最近核算时间">{{ detailRow.lastCalcTime }}</el-descriptions-item>
+      </el-descriptions>
+
+      <div v-if="detailRow" class="punch-records">
+        <h4>打卡记录</h4>
+        <el-table :data="detailRow.punchStatuses" size="small" border>
+          <el-table-column prop="punchTime" label="打卡时间" min-width="150" />
+          <el-table-column prop="punchMethod" label="打卡方式" min-width="100" />
+          <el-table-column prop="punchType" label="打卡类型" min-width="120" />
+          <el-table-column prop="statusText" label="打卡情况" min-width="90" />
+          <el-table-column prop="sourceType" label="数据来源" min-width="90" />
+        </el-table>
+      </div>
+
+      <span slot="footer">
+        <el-button @click="detailVisible = false">关闭</el-button>
+      </span>
+    </ele-modal>
+  </div>
+</template>
+
+<script>
+import { loadEmployeeBriefs } from '@/api/organization';
+import { getCurrentOrgId, getOrgOptions } from '@/utils/current-org';
+import {
+  getDailyAttendanceDetailPage,
+  adaptDailyAttendance,
+} from '@/api/hr/attendance';
+
+const CALCULATION_STATUS_LABEL = {
+  PROVISIONAL: '实时暂态',
+  CALCULATED: '最终结果',
+  LOCKED: '月结锁定',
+};
+
+export default {
+  name: 'AttendanceDaily',
+  data() {
+    return {
+      filter: {
+        employeeKeyword: '',
+        deptId: getCurrentOrgId() || '',
+        dateRange: null,
+      },
+      filterOrgOptions: [],
+      columns: [
+        { type: 'index', label: '序号', width: 60, align: 'center', headerAlign: 'center' },
+        { prop: 'name', label: '姓名', minWidth: 100, slot: 'name', fixed: 'left' },
+        { prop: 'department', label: '所属部门', minWidth: 140 },
+        { prop: 'attendanceDate', label: '日期', minWidth: 110, sortable: 'custom' },
+        { prop: 'statusText', label: '出勤情况', minWidth: 120, slot: 'statusText' },
+        { prop: 'punchTypes', label: '打卡类型', minWidth: 130 },
+        /* 接口未返回打卡地址:列保留、值留空,缺口转后端改造清单 */
+        { prop: 'punchAddress', label: '打卡地址', minWidth: 140 },
+        { prop: 'punchStatuses', label: '打卡情况', minWidth: 120, slot: 'punchStatuses' },
+        { prop: 'actualIn', label: '上班打卡时间', minWidth: 160 },
+        { prop: 'actualOut', label: '下班打卡时间', minWidth: 160 },
+        /* 接口未返回工作地址:列保留、值留空,缺口转后端改造清单 */
+        { prop: 'workAddress', label: '工作地址', minWidth: 140 },
+        { prop: 'action', label: '操作', width: 80, slot: 'action', fixed: 'right' },
+      ],
+      /* 当前页数据快照:本地 excel 导出用(后端暂无导出接口) */
+      currentRows: [],
+      detailVisible: false,
+      detailRow: null,
+    };
+  },
+  created() {
+    this.loadFilterOrgOptions();
+  },
+  methods: {
+    search() {
+      this.$refs.table?.reload?.({ page: 1 });
+    },
+    reset() {
+      this.filter = {
+        employeeKeyword: '',
+        deptId: getCurrentOrgId() || '',
+        dateRange: null,
+      };
+      this.search();
+    },
+    /* 筛选「所属部门」选项:登录缓存机构列表,与其他页面口径一致 */
+    loadFilterOrgOptions() {
+      this.filterOrgOptions = getOrgOptions().map((item) => ({
+        value: item.id,
+        label: item.name || '当前机构',
+      }));
+    },
+    async fetchPage({ page, limit }) {
+      // GET /hr/attendance/daily
+      const data = await getDailyAttendanceDetailPage({
+        pageNum: page,
+        size: limit,
+        employeeKeyword: this.filter.employeeKeyword || undefined,
+        deptId: this.filter.deptId || undefined,
+        dateStart: (this.filter.dateRange || [])[0] || undefined,
+        dateEnd: (this.filter.dateRange || [])[1] || undefined,
+      });
+      /* 接口只回 userId:姓名/工号/部门走主数据批量接口补齐(≤200/次,带缓存) */
+      const briefs = await loadEmployeeBriefs((data.list || []).map((item) => item.userId));
+      this.currentRows = (data.list || []).map((raw) =>
+        adaptDailyAttendance(raw, briefs.get(String(raw.userId ?? ''))),
+      );
+      return { list: this.currentRows, count: data.count };
+    },
+    punchStatusTagType(item) {
+      if (item.statusText === '正常') return 'success';
+      if (item.statusText === '待审批') return 'warning';
+      if (item.statusText === '补卡' || item.statusText === '无效') return 'danger';
+      return 'info';
+    },
+    minuteText(value) {
+      return value === null || value === undefined ? '' : String(value);
+    },
+    calculationStatusText(status) {
+      return CALCULATION_STATUS_LABEL[status] || status || '';
+    },
+    viewRow(row) {
+      this.detailRow = row;
+      this.detailVisible = true;
+    },
+    onImport() {
+      this.$message.info('导入考勤明细的后端接口暂未提供,请后端补充导入接口');
+    },
+    /* 后端暂无导出接口:先导出当前页已加载数据(CSV),全量导出待后端补接口 */
+    onExport() {
+      if (!this.currentRows.length) {
+        this.$message.warning('暂无考勤明细可导出');
+        return;
+      }
+      const header = [
+        '序号', '姓名', '所属部门', '日期', '出勤情况', '打卡类型',
+        '打卡地址', '打卡情况', '上班打卡时间', '下班打卡时间', '工作地址',
+      ];
+      const lines = this.currentRows.map((row, index) =>
+        [
+          index + 1,
+          row.name,
+          row.department,
+          row.attendanceDate,
+          row.statusText,
+          row.punchTypes,
+          row.punchAddress,
+          row.punchStatuses.map((item) => item.statusText).join(' '),
+          row.actualIn,
+          row.actualOut,
+          row.workAddress,
+        ]
+          .map((cell) => `"${String(cell ?? '').replace(/"/g, '""')}"`)
+          .join(','),
+      );
+      const blob = new Blob(['\ufeff' + [header.join(','), ...lines].join('\n')], {
+        type: 'text/csv;charset=utf-8;',
+      });
+      const url = URL.createObjectURL(blob);
+      const link = document.createElement('a');
+      link.href = url;
+      link.download = `考勤明细-${new Date().toISOString().slice(0, 10)}.csv`;
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      URL.revokeObjectURL(url);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.daily-card {
+  .daily-filter {
+    margin-bottom: 4px;
+  }
+}
+.punch-tag {
+  margin-right: 6px;
+  margin-bottom: 2px;
+}
+.punch-records {
+  margin-top: 16px;
+  h4 {
+    margin: 0 0 8px;
+    font-size: 14px;
+    color: #24344b;
+  }
+}
+</style>

+ 1 - 249
src/views/attendance/report/index.vue

@@ -14,8 +14,6 @@
           <el-select v-model="filter.department" size="medium" placeholder="部门" @change="loadAll">
             <el-option v-for="d in departmentOptions" :key="d.value" :label="d.label" :value="d.value" />
           </el-select>
-          <el-button v-if="$hasPermission('hr:hrAttendanceReport:export')" type="primary" icon="el-icon-download"
-            size="medium" @click="exportData">导出数据</el-button>
         </div>
       </div>
     </section>
@@ -85,92 +83,17 @@
       </article>
     </section>
 
-    <!-- 明细表 -->
-    <section class="workspace-card stats-table-card">
-      <header class="card-head">
-        <div>
-          <h2>考勤明细</h2>
-        </div>
-        <el-button v-if="$hasPermission('hr:hrAttendanceReport:export')" type="primary" size="small"
-          icon="el-icon-download" @click="exportTable">导出数据</el-button>
-      </header>
-      <div class="stats-table-wrap">
-        <table class="stats-table">
-          <thead>
-            <tr>
-              <th class="col-no">序号</th>
-              <th class="col-name">员工姓名</th>
-              <th>部门</th>
-              <th>岗位</th>
-              <th>出勤天数</th>
-              <th>迟到</th>
-              <th>旷工</th>
-              <th>状态</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr v-for="(row, index) in detailRows" :key="row.id">
-              <td class="col-no">{{ index + 1 }}</td>
-              <td class="col-name">
-                <div class="employee-cell">
-                  <span>{{ (row.name || '-').charAt(0) }}</span>
-                  <!-- 点员工姓名开详情,替代原操作列「查看」入口 -->
-                  <a class="row-link" @click="viewRow(row)">{{ row.name }}</a>
-                </div>
-              </td>
-              <td>
-                <div class="dept-cell">
-                  <strong>{{ row.department }}</strong>
-                  <small v-if="row.subDept">| {{ row.subDept }}</small>
-                </div>
-              </td>
-              <td>{{ row.position }}</td>
-              <td>
-                <div class="ratio-cell">
-                  <strong>{{ row.attendance }}<i></i></strong>
-                  <span>/ {{ row.expected }}</span>
-                </div>
-              </td>
-              <td>{{ row.late }}</td>
-              <td>{{ row.absent }}</td>
-              <td>
-                <span class="status-pill" :class="statusClass(row.status)">
-                  <i></i>{{ row.statusLabel }}
-                </span>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-    </section>
-
-    <ele-modal title="个人考勤月报" :visible.sync="detailVisible" width="520px" append-to-body>
-      <el-descriptions v-if="detailRow" :column="2" border size="small">
-        <el-descriptions-item label="员工">{{ detailRow.name }}</el-descriptions-item>
-        <el-descriptions-item label="部门">{{ detailRow.department }}</el-descriptions-item>
-        <el-descriptions-item label="岗位">{{ detailRow.position }}</el-descriptions-item>
-        <el-descriptions-item label="出勤">{{ detailRow.attendance }} / {{ detailRow.expected }}</el-descriptions-item>
-        <el-descriptions-item label="迟到">{{ detailRow.late }}</el-descriptions-item>
-        <el-descriptions-item label="旷工">{{ detailRow.absent }}</el-descriptions-item>
-        <el-descriptions-item label="状态">{{ detailRow.statusLabel }}</el-descriptions-item>
-      </el-descriptions>
-      <span slot="footer">
-        <el-button @click="detailVisible = false">关闭</el-button>
-      </span>
-    </ele-modal>
   </main>
 </template>
 
 <script>
 import * as echarts from 'echarts';
-import { listOrganizations, getUserPage } from '@/api/organization';
+import { listOrganizations } from '@/api/organization';
 import { flattenOrganizationTree, buildOrganizationTree } from '@/utils/organization-tree';
 import {
   getDepartmentAttendanceReport,
-  getPersonalAttendanceReport,
   getAttendanceExceptionReport,
   getDailyAttendancePage,
-  adaptPersonalReport,
   exceptionLabel
 } from '@/api/hr/attendance';
 
@@ -210,7 +133,6 @@ export default {
       },
       monthOptions,
       departmentOptions: [{ value: '', label: '全部部门' }],
-      userMap: {},
       metrics: {
         normalUserCount: 0,
         exceptionUserCount: 0,
@@ -218,9 +140,6 @@ export default {
         lateCount: 0
       },
       otherExceptionCount: 0,
-      detailRows: [],
-      detailVisible: false,
-      detailRow: null,
       dailyTrend: { dates: [], counts: [] },
       exceptionBars: { labels: [], values: [] },
       charts: {
@@ -242,13 +161,6 @@ export default {
     Object.values(this.charts).forEach((chart) => chart && chart.dispose());
   },
   methods: {
-    statusClass(status) {
-      return ({
-        normal: 'success',
-        warning: 'pending',
-        danger: 'danger'
-      })[status] || 'muted';
-    },
     queryParams() {
       return {
         ...monthRange(this.filter.month),
@@ -268,16 +180,6 @@ export default {
         this.departmentOptions = [{ value: '', label: '全部部门' }];
       }
     },
-    async loadUsers(ids) {
-      const missing = [...new Set(ids.map(String))].filter((id) => id && !this.userMap[id]);
-      if (!missing.length) return;
-      try {
-        const data = await getUserPage({ pageNum: 1, size: 200 });
-        (data?.list || []).forEach((user) => {
-          this.userMap[String(user.id)] = user;
-        });
-      } catch (error) { /* ignore */ }
-    },
     async loadAll() {
       this.loading = true;
       try {
@@ -287,22 +189,12 @@ export default {
           getAttendanceExceptionReport(params).catch(() => ({ list: [] })),
           getDailyAttendancePage(params).catch(() => ({ list: [] }))
         ]);
-        // 明细接口失败必须如实暴露,不许静默当空数据吞掉
-        let personal = { list: [] };
-        try {
-          personal = await getPersonalAttendanceReport(params);
-        } catch (error) {
-          console.error('[考勤明细] /hr/attendance/reports/personal 加载失败:', error);
-          this.$message.error(`考勤明细加载失败:${error.message || '接口异常'}`);
-        }
         this.metrics = {
           normalUserCount: Number(dept.normalUserCount || 0),
           exceptionUserCount: Number(dept.exceptionUserCount || 0),
           attendanceRate: Number(dept.attendanceRate || 0).toFixed(1),
           lateCount: Number(dept.lateCount || 0)
         };
-        await this.loadUsers((personal.list || []).map((item) => item.userId));
-        this.detailRows = (personal.list || []).map((item) => adaptPersonalReport(item, this.userMap));
         const grouped = {};
         (exceptions.list || []).forEach((item) => {
           const key = exceptionLabel(item.exceptionType);
@@ -330,32 +222,6 @@ export default {
         this.loading = false;
       }
     },
-    viewRow(row) {
-      this.detailRow = row;
-      this.detailVisible = true;
-    },
-    exportData() {
-      this.exportTable();
-    },
-    exportTable() {
-      if (!this.detailRows.length) {
-        this.$message.warning('暂无考勤明细可导出');
-        return;
-      }
-      const header = ['序号', '员工姓名', '部门', '岗位', '出勤天数', '应出勤', '迟到', '旷工', '状态'];
-      const lines = this.detailRows.map((row, index) => [
-        index + 1, row.name, row.department, row.position, row.attendance, row.expected, row.late, row.absent, row.statusLabel
-      ].join(','));
-      const blob = new Blob(['\ufeff' + [header.join(','), ...lines].join('\n')], { type: 'text/csv;charset=utf-8;' });
-      const url = URL.createObjectURL(blob);
-      const link = document.createElement('a');
-      link.href = url;
-      link.download = `考勤明细-${this.filter.month}.csv`;
-      document.body.appendChild(link);
-      link.click();
-      document.body.removeChild(link);
-      URL.revokeObjectURL(url);
-    },
     resizeCharts() {
       Object.values(this.charts).forEach((chart) => chart && chart.resize());
     },
@@ -611,120 +477,6 @@ export default {
 .chart-panel--exception { display: flex; flex-direction: column; }
 .exception-bar { height: 280px; padding: 12px 12px 18px; }
 
-/* 明细表 */
-.stats-table-card { border-radius: 12px; }
-.stats-table-card .card-head {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 14px 18px;
-  border-bottom: 1px solid #edf1f6;
-}
-.stats-table-card .card-head h2 {
-  margin: 0;
-  font-size: 15px;
-  color: #25364e;
-  font-weight: 600;
-}
-.stats-table-wrap { padding: 0; }
-.stats-table {
-  width: 100%;
-  border-collapse: collapse;
-  font-size: 14px;
-  color: #3d4a5e;
-}
-.stats-table thead th {
-  height: 44px;
-  padding: 0 12px;
-  text-align: left;
-  color: #68778c;
-  background: #f6f8fb;
-  font-weight: 600;
-  border-bottom: 1px solid #edf0f3;
-}
-.stats-table tbody td {
-  padding: 12px;
-  border-bottom: 1px solid #edf0f3;
-  vertical-align: middle;
-}
-.stats-table tbody tr:last-child td { border-bottom: 0; }
-.stats-table .col-no {
-  width: 64px;
-  text-align: center;
-  color: #68778c;
-}
-.stats-table tbody tr:hover { background: #f2f7ff; }
-
-.stats-table .col-name { width: 18%; }
-.stats-table .row-link { color: #1890ff; cursor: pointer; }
-.stats-table .col-action { width: 100px; text-align: left; }
-.stats-table .col-action .el-button.is-link { color: #1890ff; }
-
-.stats-table .employee-cell { display: flex; align-items: center; gap: 10px; }
-.stats-table .employee-cell > span {
-  width: 30px; height: 30px;
-  display: grid; place-items: center;
-  border-radius: 50%;
-  color: #1890ff;
-  background: #eaf2ff;
-  font-size: 14px; font-weight: 600;
-  flex: none;
-}
-.stats-table .employee-cell strong {
-  color: #24344b;
-  font-size: 14px;
-  font-weight: 500;
-}
-.stats-table .dept-cell strong,
-.stats-table .dept-cell small {
-  display: inline;
-  color: #3d4a5e;
-  font-size: 14px;
-}
-.stats-table .dept-cell small {
-  color: #b0b8c2;
-  margin: 0 4px;
-}
-.stats-table .ratio-cell strong {
-  position: relative;
-  display: inline-block;
-  margin-right: 4px;
-  font-size: 14px;
-  font-weight: 600;
-  color: #24344b;
-}
-.stats-table .ratio-cell strong i {
-  display: inline-block;
-  width: 1px;
-  height: 14px;
-  background: #d6dce4;
-  vertical-align: middle;
-  margin-left: 6px;
-}
-.stats-table .ratio-cell span { color: #8d99a9; font-size: 14px; }
-
-.stats-table .status-pill {
-  display: inline-flex;
-  align-items: center;
-  gap: 6px;
-  padding: 2px 10px;
-  border-radius: 12px;
-  font-size: 14px;
-  white-space: nowrap;
-  background: #f6f8fb;
-}
-.stats-table .status-pill i {
-  width: 6px; height: 6px;
-  border-radius: 50%;
-  background: #aab4c0;
-}
-.stats-table .status-pill.success { color: #168d69; background: #e7f7f1; }
-.stats-table .status-pill.success i { background: #22a477; }
-.stats-table .status-pill.pending { color: #c67d23; background: #fff2e4; }
-.stats-table .status-pill.pending i { background: #e6a23c; }
-.stats-table .status-pill.danger { color: #d55752; background: #fff0ef; }
-.stats-table .status-pill.danger i { background: #d85e58; }
-
 @media (max-width: 1100px) {
   .stats-charts { grid-template-columns: 1fr; }
   .punch-body { grid-template-columns: 1fr; }

+ 1 - 1
src/views/hiring/hiringApproval/index.vue

@@ -177,7 +177,7 @@ export default {
         {
           prop: "departmentName",
           columnKey: "departmentName",
-          label: "部门",
+          label: "录用部门",
           minWidth: 110,
           formatter: (_r, _c, value) => value || "-",
         },

+ 94 - 80
src/views/hrManagement/employeeArchive/components/EmployeeArchiveForm.vue

@@ -96,7 +96,8 @@
                        ★ 2026-10-28 编辑态回显改成接口 deptName 文本:原来按 deptId 去部门树里找节点显示名称,
                          节点匹配不到或撞上机构根节点时就会显示成机构名字(用户实拍:主所属部门显示成机构名)。
                          名称只认详情接口 deptName,取不到就留空提示,不拿机构名 / 隶属部门拼串顶替。 -->
-                  <el-form-item label="主所属部门" :prop="isEdit ? undefined : 'primaryDeptId'">
+                  <!-- required:编辑态 prop 置空不参与校验,但星号要一直展示(2026-10-29 用户要求) -->
+                  <el-form-item label="主所属部门" required :prop="isEdit ? undefined : 'primaryDeptId'">
                     <el-input v-if="isEdit" :value="primaryDeptName" disabled
                       :placeholder="primaryDeptName ? '' : '接口未返回部门名称(deptName),请后端补充'" />
                     <el-cascader v-else :key="`primary-dept-${form.groupId || 'none'}`" v-model="form.primaryDeptId"
@@ -113,9 +114,22 @@
                       class="full-width dept-multi-cascader" :disabled="isEdit || !form.groupId"
                       :placeholder="deptCascaderPlaceholder"
                       @change="handleOrganizationChange" /></el-form-item>
+                  <!-- 所属工厂:与所属机构级联(2026-10-29 用户要求),只能选当前机构下的工厂;
+                       未选机构时禁用。编辑态仍整体只读(与其它字段口径一致)。 -->
+                  <el-form-item label="所属工厂">
+                    <el-select v-model="form.factoryId" filterable clearable class="full-width"
+                      :loading="factoryLoading" :disabled="isEdit || !form.groupId"
+                      :placeholder="form.groupId ? '请选择所属工厂' : '请先选择所属机构'" @change="handleFactoryChange">
+                      <el-option v-for="item in factoryOptions" :key="String(item.id)" :label="item.name"
+                        :value="item.id" />
+                    </el-select>
+                  </el-form-item>
+                  <!-- 岗位链路级联来源 = 主所属部门(2026-10-29 用户确认:主所属部门 → 岗位序列 → 岗位 → 性质/层级)。
+                       编辑态:已有值保持只读;空白时放开编辑,允许补选(2026-10-29 用户要求)。 -->
                   <el-form-item label="岗位序列" prop="positionSequenceId">
                     <el-select v-model="form.positionSequenceId" filterable clearable class="full-width"
-                      :loading="sequenceLoading" :disabled="isEdit || !hasDept" :placeholder="hasDept ? '请选择岗位序列' : '请先选择隶属部门'"
+                      :loading="sequenceLoading" :disabled="(isEdit && hasPositionSequence) || !hasPrimaryDept"
+                      :placeholder="hasPrimaryDept ? '请选择岗位序列' : '请先选择主所属部门'"
                       @change="handlePositionSequenceChange">
                       <el-option v-for="item in sequenceOptions" :key="`seq-${String(item.id)}`" :label="item.name"
                         :value="String(item.id)" />
@@ -123,7 +137,8 @@
                   </el-form-item>
                   <el-form-item label="岗位" prop="positionId">
                     <el-select :key="`position-select-${positionSelectKey}`" v-model="form.positionId" filterable
-                      clearable class="full-width" :loading="positionLoading" :disabled="isEdit || !hasPositionSequence"
+                      clearable class="full-width" :loading="positionLoading"
+                      :disabled="(isEdit && Boolean(form.positionId)) || !hasPositionSequence"
                       :placeholder="hasPositionSequence ? '请选择岗位' : '请先选择岗位序列'" @change="handlePositionChange">
                       <el-option v-for="item in positionOptions" :key="`pos-${String(item.id)}`"
                         :label="positionOptionLabel(item)" :value="String(item.id)" />
@@ -131,25 +146,21 @@
                   </el-form-item>
                   <el-form-item label="岗位性质" prop="employmentType"><el-select
                       v-model="form.employmentType" filterable
-                      clearable class="full-width" :disabled="isEdit" :placeholder="form.positionId ? '请选择岗位性质' : '请先选择岗位'"
+                      clearable class="full-width" :disabled="isEdit && Boolean(form.employmentType)"
+                      :placeholder="form.positionId ? '请选择岗位性质' : '请先选择岗位'"
                       @change="syncEmploymentTypeName">
                       <el-option v-for="item in employmentOptions" :key="item.key || item.value" :label="item.label"
                         :value="item.value" />
                     </el-select></el-form-item>
                   <el-form-item label="岗位层级" prop="staffingLevel"><el-select
                       v-model="form.staffingLevel"
-                      class="full-width" :disabled="isEdit || !staffingLevelOptions.length"
+                      class="full-width"
+                      :disabled="(isEdit && Boolean(form.staffingLevel)) || !staffingLevelOptions.length"
                       :placeholder="form.positionId ? '请选择岗位层级' : '请先选择岗位'" @change="handleStaffingLevelChange">
                       <el-option v-for="item in staffingLevelOptions" :key="item" :label="item"
                         :value="item" /></el-select></el-form-item>
-                  <!-- 是否启用:2026-10-28 用户要求放到左列末尾(岗位层级之下),与右列时间/状态区分开 -->
-                  <el-form-item label="是否启用">
-                    <div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{
-                      form.enabled ? '启用' :
-                        '待岗' }}</span></div>
-                  </el-form-item>
                 </el-col>
-                <!-- 右列:账号、工厂、直属上级与时间状态 -->
+                <!-- 右列:账号、直属上级与时间状态(所属工厂 2026-10-29 移到左列隶属部门之下) -->
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="用户账号"><el-input v-model.trim="form.account"
                       maxlength="30" placeholder="系统登录账号,支持自动生成、手动修改"><template
@@ -158,13 +169,6 @@
                       v-model.trim="form.employeeNo" maxlength="20" placeholder="集团全局唯一" disabled><el-button
                         v-if="!isEdit" slot="append"
                         @click="generateEmployeeNo">生成</el-button></el-input></el-form-item>
-                  <el-form-item label="所属工厂">
-                    <el-select v-model="form.factoryId" filterable clearable class="full-width"
-                      :loading="factoryLoading" :disabled="isEdit" placeholder="请选择所属工厂" @change="handleFactoryChange">
-                      <el-option v-for="item in factoryOptions" :key="String(item.id)" :label="item.name"
-                        :value="item.id" />
-                    </el-select>
-                  </el-form-item>
                   <el-form-item label="直属上级">
                     <el-select v-model="form.managerId" filterable remote clearable class="full-width"
                       :loading="managerLoading" :disabled="isEdit" :remote-method="searchManagers" placeholder="输入姓名/工号搜索全员"
@@ -182,6 +186,12 @@
                   <el-form-item label="员工状态" prop="status"><el-select v-model="form.status"
                       class="full-width" :disabled="isEdit"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label"
                         :value="item.value" /></el-select></el-form-item>
+                  <!-- 是否启用:2026-10-29 用户要求放到右列末尾(员工状态之下) -->
+                  <el-form-item label="是否启用">
+                    <div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{
+                      form.enabled ? '启用' :
+                        '待岗' }}</span></div>
+                  </el-form-item>
                 </el-col>
               </el-row>
 
@@ -809,14 +819,10 @@ export default {
           },
           trigger: 'change',
         }],
+        /* 隶属部门:非必填(2026-10-29 用户要求),保留校验器仅为占位口径统一,空值直接放行 */
         deptIds: [{
-          required: true,
           validator: (rule, value, callback) => {
             if (this._skipFormValidate) return callback();
-            if (!this.normalizeDeptIds(value).length) {
-              callback(new Error('请选择隶属部门'));
-              return;
-            }
             callback();
           },
           trigger: 'change',
@@ -1047,15 +1053,17 @@ export default {
         });
       return mark(this.deptOptions);
     },
-    hasDept() {
-      return this.normalizeDeptIds(this.form.deptIds).length > 0;
+    /* 岗位链路级联开关:只看主所属部门(2026-10-29 用户确认级联来源改为主所属部门) */
+    hasPrimaryDept() {
+      return this.form.primaryDeptId != null && this.form.primaryDeptId !== '';
     },
     hasPositionSequence() {
       return Boolean(this.form.positionSequenceId);
     },
     requiredProgress() {
-      // 与 form rules 一一对应(含主所属部门 / 身份证 / 隶属部门 / 岗位序列 / 岗位性质 / 岗位层级),保证「必填 x/y」口径准确
-      const keys = ['name', 'employeeNo', 'gender', 'idCard', 'groupId', 'primaryDeptId', 'deptIds', 'status', 'hireDate', 'positionSequenceId', 'positionId', 'employmentType', 'staffingLevel', 'phone'];
+      // 与 form rules 一一对应(含主所属部门 / 身份证 / 岗位序列 / 岗位性质 / 岗位层级),保证「必填 x/y」口径准确
+      // 隶属部门 deptIds 已改非必填(2026-10-29),不进必填计数(仍留在 sectionProgress 完整度统计里)
+      const keys = ['name', 'employeeNo', 'gender', 'idCard', 'groupId', 'primaryDeptId', 'status', 'hireDate', 'positionSequenceId', 'positionId', 'employmentType', 'staffingLevel', 'phone'];
       return { base: `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}` };
     },
     sectionProgress() {
@@ -1293,9 +1301,9 @@ export default {
         /* 新增员工默认带出所属机构(登录机构列表第一个) */
         this.applyDefaultLegalEntity();
         this.normalizeDeptAgainstOptions();
+        /* 工厂与机构级联(2026-10-29 用户要求):等机构(含新增默认带出)就绪后再按机构拉工厂 */
+        this.loadFactories().then(() => this.syncFactoryDisplay());
       });
-      /* 所属工厂与机构、部门互不关联:并行加载,不等机构 */
-      this.loadFactories().then(() => this.syncFactoryDisplay());
       this.syncAge();
       if (!this.employee) this.generateEmployeeNo();
       this.$nextTick(() => {
@@ -1841,7 +1849,9 @@ export default {
       }
       return null;
     },
-    applyDeptIds(ids, { resetDependents = false } = {}) {
+    /* 2026-10-29 岗位链路级联来源改为主所属部门,隶属部门变化不再影响岗位链路,
+       因此不再有 resetDependents 重置逻辑 */
+    applyDeptIds(ids) {
       const list = this.normalizeDeptIds(ids).filter(
         (id) => String(id) !== String(this.form.groupId || ''),
       );
@@ -1856,10 +1866,6 @@ export default {
           .filter(Boolean)
           .join('、');
       }
-      if (resetDependents) {
-        /* 直属上级不再随部门联动,这里只重置岗位 */
-        this.resetPositionsByDepartment();
-      }
     },
     /** 新增员工默认带出所属机构:登录机构列表第一个(命中法人组织选项时才填充) */
     applyDefaultLegalEntity() {
@@ -1875,7 +1881,7 @@ export default {
       this.form.groupId = String(hit.id);
     },
     handleOrganizationChange(ids) {
-      this.applyDeptIds(ids, { resetDependents: true });
+      this.applyDeptIds(ids);
       this.$nextTick(() => this.$refs.form?.validateField('deptIds'));
     },
     /**
@@ -1892,18 +1898,22 @@ export default {
         input.setAttribute('placeholder', want);
       }
     },
-    /* 所属机构切换:只重置依赖机构的部门 / 岗位级联。
-       所属工厂与机构、部门互不关联(工厂列表是独立的平铺数据),这里保持已选工厂不动;
-       直属上级已改为全员搜索,与机构/部门解耦,同样保持已选直属上级不动 */
+    /* 所属机构切换:重置依赖机构的部门 / 岗位级联,并按新机构重拉工厂(清空已选工厂);
+       直属上级已改为全员搜索,与机构/部门解耦,保持已选直属上级不动 */
     handleLegalEntityChange() {
       this.deptCascaderKey += 1;
       /* 机构变了,机构下的部门树整棵换掉 → 已选主所属部门 / 隶属部门都失效,一并清空 */
       this.form.primaryDeptId = '';
       this.applyDeptIds([]);
       this.resetPositionsByDepartment();
-    },
-    /* 主所属部门选中:把该部门从隶属部门里剔除(两者不允许重复,与参考项目 checkedKeys 一致) */
-    handlePrimaryDeptChange(id) {
+      /* 工厂与机构级联(2026-10-29 用户要求):换机构后原工厂不再属于新机构,清空并按新机构重拉 */
+      this.form.factoryId = null;
+      this.form.factory = '';
+      this.loadFactories();
+    },
+    /* 主所属部门选中:把该部门从隶属部门里剔除(两者不允许重复,与参考项目 checkedKeys 一致);
+       岗位链路(序列/岗位/性质/层级)级联来源 = 主所属部门 → 主部门变了整段重置重拉 */
+    async handlePrimaryDeptChange(id) {
       const picked = id == null || id === '' ? '' : String(id);
       if (!picked) return;
       const current = this.normalizeDeptIds(this.form.deptIds);
@@ -1911,6 +1921,7 @@ export default {
       if (rest.length !== current.length) {
         this.applyDeptIds(rest);
       }
+      await this.resetPositionsByDepartment();
     },
     /* 在组织树里按 id 找节点(主所属部门要拿机构节点做置灰根节点) */
     findOrgNodeById(id, nodes = this.organizationTree.length ? this.organizationTree : this.organizations) {
@@ -2040,14 +2051,20 @@ export default {
       }
     },
     /**
-     * 所属工厂:直接取工厂列表接口 /main/factoryarea/page(type=1 工厂)。
-     * ★ 工厂与所属机构、隶属部门不层级关联:不按机构过滤、也不从组织树里捞,
-     *   换机构不影响工厂下拉;接口失败直接报错,不做兜底。
+     * 所属工厂:取工厂列表接口 /main/factoryarea/page(type=1 工厂)。
+     * ★ 2026-10-29 用户要求:工厂与所属机构级联——只能选当前机构下的工厂。
+     *   服务端按 groupId 过滤(swagger /main/factoryarea/page 支持 groupId 参数);
+     *   未选机构时不发请求,下拉为空。
      */
     async loadFactories() {
+      const groupId = this.form.groupId;
+      if (groupId == null || groupId === '') {
+        this.factoryOptions = [];
+        return;
+      }
       this.factoryLoading = true;
       try {
-        const data = await getFactoryAreaPage();
+        const data = await getFactoryAreaPage({ groupId });
         const list = Array.isArray(data?.list) ? data.list : Array.isArray(data) ? data : [];
         this.factoryOptions = list
           .map((item) => ({
@@ -2055,8 +2072,11 @@ export default {
             name: item.name || item.factoryName || item.pathName || `工厂 ${item.id}`,
             code: item.code || '',
             enabled: item.enabled,
+            /* 行自带 groupId 时用于客户端复核(防止服务端过滤参数失效);没带就不过滤 */
+            groupId: item.groupId == null ? null : String(item.groupId),
           }))
-          .filter((item) => item.id && item.name);
+          .filter((item) => item.id && item.name)
+          .filter((item) => item.groupId == null || item.groupId === String(groupId));
       } catch (error) {
         this.factoryOptions = [];
         this.$message.error(error.message || '工厂列表加载失败');
@@ -2339,7 +2359,7 @@ export default {
       this.hrPositionIdSet = {};
       this.positionLoading = false;
     },
-    /** 隶属部门变化:清空岗位序列及以下级联,并按新的部门并集重新拉序列 */
+    /** 主所属部门(或所属机构)变化:清空岗位序列及以下级联,并按主所属部门重新拉序列 */
     async resetPositionsByDepartment() {
       this.clearPositionCascade();
       this.form.positionSequenceId = '';
@@ -2407,12 +2427,15 @@ export default {
       return this.isNumericId(raw) ? String(raw).trim() : '';
     },
     /**
-     * 隶属部门(可多选)→ 岗位序列下拉:逐部门取序列后并集去重
+     * 主所属部门 → 岗位序列下拉(2026-10-29 用户确认:级联来源从隶属部门改为主所属部门)
      * GET /fm/tmaindepartmentposition/sequence/list
      */
     async loadPositionSequences() {
-      const deptIds = this.normalizeDeptIds(this.form.deptIds);
-      if (!deptIds.length) {
+      const deptId =
+        this.form.primaryDeptId == null || this.form.primaryDeptId === ''
+          ? ''
+          : String(this.form.primaryDeptId);
+      if (!deptId) {
         this.sequenceOptions = [];
         this.sequenceLoading = false;
         return;
@@ -2420,22 +2443,16 @@ export default {
       this.sequenceLoading = true;
       let requestFailed = false;
       try {
-        const groups = await Promise.all(
-          deptIds.map((deptId) =>
-            getDepartmentPositionSequences(deptId).catch(() => {
-              requestFailed = true;
-              return [];
-            }),
-          ),
-        );
+        const data = await getDepartmentPositionSequences(deptId).catch(() => {
+          requestFailed = true;
+          return [];
+        });
         const map = new Map();
-        groups
-          .flatMap((data) => this.toOptionList(data))
-          .forEach((item) => {
-            if (!item || item.positionSequenceId == null || item.positionSequenceId === '') return;
-            const id = String(item.positionSequenceId);
-            if (!map.has(id)) map.set(id, { id, name: item.positionSequence || '' });
-          });
+        this.toOptionList(data).forEach((item) => {
+          if (!item || item.positionSequenceId == null || item.positionSequenceId === '') return;
+          const id = String(item.positionSequenceId);
+          if (!map.has(id)) map.set(id, { id, name: item.positionSequence || '' });
+        });
         const list = Array.from(map.values());
         const currentId = this.form.positionSequenceId ? String(this.form.positionSequenceId) : '';
         if (currentId && !list.some((item) => item.id === currentId)) {
@@ -2446,8 +2463,8 @@ export default {
         }
         this.sequenceOptions = list;
         if (!list.length && !requestFailed) {
-          // 所选部门下没有维护任何岗位序列(接口异常时不误报):引导去组织机构的岗位信息里维护
-          this.warnEmptySequences(deptIds);
+          // 主所属部门下没有维护任何岗位序列(接口异常时不误报):引导去组织机构的岗位信息里维护
+          this.warnEmptySequences([deptId]);
         }
       } catch (error) {
         this.sequenceOptions = [];
@@ -2475,13 +2492,16 @@ export default {
       });
     },
     /**
-     * 隶属部门 + 岗位序列 → 岗位下拉(多部门并集,逐项记录来源部门用于带出性质)
+     * 主所属部门 + 岗位序列 → 岗位下拉(2026-10-29 级联来源改为主所属部门)
      * GET /fm/tmaindepartmentposition/position/list
      */
     async loadDepartmentPositions() {
-      const deptIds = this.normalizeDeptIds(this.form.deptIds);
+      const deptId =
+        this.form.primaryDeptId == null || this.form.primaryDeptId === ''
+          ? ''
+          : String(this.form.primaryDeptId);
       const positionSequenceId = this.form.positionSequenceId;
-      if (!deptIds.length || !positionSequenceId) {
+      if (!deptId || !positionSequenceId) {
         this.positionOptions = [];
         this.hrPositionIdSet = {};
         this.positionLoading = false;
@@ -2489,15 +2509,9 @@ export default {
       }
       this.positionLoading = true;
       try {
-        const groups = await Promise.all(
-          deptIds.map(async (deptId) => {
-            const data = await getDepartmentPositions({ deptId, positionSequenceId }).catch(() => []);
-            return this.toOptionList(data).map((item) => ({ ...item, deptId }));
-          }),
-        );
+        const data = await getDepartmentPositions({ deptId, positionSequenceId }).catch(() => []);
         const sequenceName = this.form.positionSequence || this.form.positionSequenceName || '';
-        const list = groups
-          .flat()
+        const list = this.toOptionList(data)
           .filter((item) => item && item.positionId != null && String(item.positionName || '').trim())
           .map((item) => {
             const name = String(item.positionName || '').trim();
@@ -2506,7 +2520,7 @@ export default {
               positionId: String(item.positionId),
               positionName: name,
               name,
-              deptId: item.deptId,
+              deptId,
               sequenceId: String(positionSequenceId),
               positionSequenceId: String(positionSequenceId),
               positionSequence: sequenceName,

+ 5 - 2
src/views/hrManagement/employeeArchive/index.vue

@@ -97,7 +97,9 @@
                     class="ele-btn-icon" @click="openCreate">
                     新增
                   </el-button>
-                  <el-dropdown v-if="$hasPermission('main:hrEmployee:import')
+                  <!-- 导入入口暂时隐藏(2026-10-29 用户要求,只保留导出);
+                       handleImportCommand / onImportFile 方法保留,恢复时放开下面注释即可 -->
+                  <!-- <el-dropdown v-if="$hasPermission('main:hrEmployee:import')
                     || $hasPermission('main:hrEmployee:importTemplate')" trigger="click"
                     @command="handleImportCommand">
                     <el-button icon="el-icon-upload2" class="ele-btn-icon">
@@ -111,7 +113,8 @@
                     </el-dropdown-menu>
                   </el-dropdown>
                   <input ref="importInput" type="file" accept=".xlsx,.xls" style="display: none"
-                    @change="onImportFile" />
+                    @change="onImportFile" /> -->
+
                   <el-dropdown v-if="$hasPermission('main:hrEmployee:export')" trigger="click"
                     @command="handleExport">
                     <el-button icon="el-icon-download" class="ele-btn-icon">

+ 8 - 0
src/views/hrManagement/itemReceiptManagement/index.vue

@@ -256,6 +256,14 @@ export default {
           align: "left",
           formatter: (r, _c, v) => v || "-",
         },
+        {
+          prop: "legalEntityName",
+          label: "所属机构",
+          minWidth: 180,
+          align: "left",
+          showOverflowTooltip: true,
+          formatter: (r, _c, v) => v || "-",
+        },
         {
           prop: "departmentName",
           label: "所属部门",

+ 20 - 32
src/views/hrManagement/onboardingHandling/index.vue

@@ -51,9 +51,8 @@
           </el-link>
           <span v-else>-</span>
         </template>
-        <template v-slot:organization="{ row }">
-          {{ [row.company, row.department].filter(Boolean).join(" / ") || "-" }}
-        </template>
+        <!-- 组织信息拆两列(2026-10-29 用户要求):所属机构 = organizationName、
+             所属部门 = targetDepartmentName,直接取接口字段,没返回就显示 -,不做兜底 -->
         <!-- 流程进度:文案直接用 onboardingProgressValue,配色按 onboardingProgress 枚举判断 -->
         <template v-slot:flow="{ row }">
           <el-tag v-if="row.onboardingProgressValue" size="mini" effect="plain"
@@ -653,12 +652,21 @@ export default {
           slot: "name",
         },
         {
-          prop: "department",
-          label: "组织信息",
+          /* 原「组织信息」一列拆成所属机构 / 所属部门两列(2026-10-29 用户要求) */
+          prop: "organizationName",
+          label: "所属机构",
           minWidth: 200,
           align: "left",
           showOverflowTooltip: true,
-          slot: "organization",
+          formatter: (_r, _c, value) => value || "-",
+        },
+        {
+          prop: "targetDepartmentName",
+          label: "所属部门",
+          minWidth: 160,
+          align: "left",
+          showOverflowTooltip: true,
+          formatter: (_r, _c, value) => value || "-",
         },
         {
           prop: "position",
@@ -954,7 +962,9 @@ export default {
           organizationId: this.filters.organizationId,
           positionType: this.filters.positionType,
         });
-        const list = (data.list || []).map((row) => this.enrichCompany(row));
+        /* 列表所属机构 / 所属部门直接用接口原始字段 organizationName、targetDepartmentName,
+           不再按 deptId 到组织树里往上反推机构名(会把部门名/上级名当机构名显示) */
+        const list = data.list || [];
         this.rows = list;
         this.totalCount = Number(data.count || 0);
         return { list, count: this.totalCount };
@@ -1441,27 +1451,6 @@ export default {
         });
       }
     },
-    enrichCompany(row) {
-      if (row.company) return row;
-      const dept = this.orgList.find(
-        (item) => String(item.id) === String(row.deptId),
-      );
-      if (!dept) return row;
-      const parents = new Map(
-        this.orgList.map((item) => [String(item.id), item]),
-      );
-      let cursor = dept;
-      let company = dept.name;
-      while (cursor && parents.has(String(cursor.parentId || 0))) {
-        cursor = parents.get(String(cursor.parentId));
-        if (cursor) company = cursor.name;
-      }
-      return {
-        ...row,
-        department: row.department || dept.name || "",
-        company: row.company || company,
-      };
-    },
     resetAccountState() {
       this.accountChecking = false;
       this.accountChecked = false;
@@ -1811,10 +1800,9 @@ export default {
         jobNumber: saved.jobNumber || this.editing.jobNumber || "",
         name: saved.name || this.editing.name,
         company: saved.company || this.editing.company,
-        department:
-          saved.department ||
-          this.deptNameOf(saved.deptId || this.editing.deptId) ||
-          this.editing.department,
+        /* 只认接口返回的部门名,其次用刚在弹框里选的那个(同一份数据);
+           不再按 deptId 去组织树反查名字顶替 */
+        department: saved.department || this.editing.department,
         position:
           saved.position ||
           saved.positionName ||

+ 5 - 4
src/views/hrManagement/positionTransfer/index.vue

@@ -57,7 +57,7 @@
           {{ row.duties || "-" }}
         </template>
         <template v-slot:move="{ row }">
-          {{ [row.targetPosition, row.targetDept].filter(Boolean).join(" / ") || "-" }}
+          {{ row.targetPosition || "-" }}
         </template>
         <template v-slot:audit="{ row }">
           <el-tag v-if="row.auditStatus && row.auditStatus !== '-'" size="mini" :type="statusTagType(row.auditStatus)">
@@ -629,15 +629,16 @@ export default {
         // },
         {
           prop: "originPosition",
-          label: "原岗位 / 部门",
+          label: "原岗位",
           minWidth: 130,
           align: "left",
           showOverflowTooltip: true,
-          formatter: (_r, _c, value) => value || "-",
+          // 历史数据里岗位串着「 / 部门」一起存(老版本保存时拼的),展示只留岗位
+          formatter: (_r, _c, value) => String(value || "").split(" / ")[0] || "-",
         },
         {
           columnKey: "move",
-          label: "调动岗位 / 部门",
+          label: "调动岗位",
           minWidth: 170,
           align: "left",
           showOverflowTooltip: true,

+ 40 - 24
src/views/hrManagement/regularizationHandling/index.vue

@@ -80,9 +80,6 @@
           </el-link>
           <span v-else>-</span>
         </template>
-        <template v-slot:organization="{ row }">
-          {{ [row.company, row.department].filter(Boolean).join(" / ") || "-" }}
-        </template>
         <template v-slot:position="{ row }">
           {{ row.position || "-" }}
         </template>
@@ -583,12 +580,22 @@ export default {
           slot: "name",
         },
         {
-          prop: "department",
-          label: "组织信息",
-          minWidth: 280,
+          /* 所属机构直取接口 groupName(2026-10-29 用户确认字段口径),不再走 slot 拼 company */
+          prop: "groupName",
+          label: "所属机构",
+          minWidth: 240,
           align: "left",
           showOverflowTooltip: true,
-          slot: "organization",
+          formatter: (_r, _c, value) => value || "-",
+        },
+        {
+          /* 所属部门直取接口 deptName */
+          prop: "deptName",
+          label: "所属部门",
+          minWidth: 140,
+          align: "left",
+          showOverflowTooltip: true,
+          formatter: (_r, _c, value) => value || "-",
         },
         {
           prop: "position",
@@ -699,16 +706,21 @@ export default {
   },
   watch: {
     drawerVisible(val) {
-      if (val) {
-        // 弹框打开时预拉一次直属上级候选,确保自动带出的 manager 能回显
+      // 弹框打开时预拉一次直属上级候选,确保自动带出的 manager 能回显;
+      // 查看详情(readOnly)下拉是禁用的纯回显,不需要候选,省一次 getUserPage
+      if (val && !this.readOnly) {
         this.searchManagers("");
       }
     },
     "form.managerUserId"(val) {
-      // selectEmployee 异步带出 managerUserId 时,确保 options 里有对应选项
-      if (val && this.drawerVisible) {
-        this.searchManagers("");
+      if (!val || !this.drawerVisible) return;
+      // 详情态:单据里已带出直属上级名称,本地补一条选项即可回显,不再打 getUserPage
+      if (this.readOnly) {
+        this.ensureManagerOption();
+        return;
       }
+      // selectEmployee 异步带出 managerUserId 时,确保 options 里有对应选项
+      this.searchManagers("");
     },
   },
   methods: {
@@ -1053,24 +1065,28 @@ export default {
           };
         });
         // 回显兜底:如果 form.managerUserId 有值但选项里没匹配上,加一条假选项让 el-select 能显示
-        if (this.form?.managerUserId) {
-          const hasMatch = this.managerOptions.some(
-            (it) => String(it.id) === String(this.form.managerUserId),
-          );
-          if (!hasMatch) {
-            this.managerOptions.unshift({
-              id: String(this.form.managerUserId),
-              name: this.form.managerUserName || this.form.manager || "已离职员工",
-              employeeNo: "",
-            });
-          }
-        }
+        this.ensureManagerOption();
       } catch (error) {
         this.managerOptions = [];
       } finally {
         this.managerLoading = false;
       }
     },
+    /** 直属上级回显:选项里没有当前 managerUserId 时本地补一条,不发请求 */
+    ensureManagerOption() {
+      const val = this.form?.managerUserId;
+      if (!val) return;
+      const hasMatch = this.managerOptions.some(
+        (it) => String(it.id) === String(val),
+      );
+      if (!hasMatch) {
+        this.managerOptions.unshift({
+          id: String(val),
+          name: this.form.managerUserName || this.form.manager || "已离职员工",
+          employeeNo: "",
+        });
+      }
+    },
     onManagerChange(userId) {
       if (!userId) {
         this.form.managerUserId = null;

+ 3 - 2
src/views/hrManagement/resignationApplication/index.vue

@@ -1171,7 +1171,8 @@ export default {
       this.detailRow = row;
       try {
         const latest = await getResignationById(row.id);
-        this.detailRow = (await withEmployeeInfo([latest]))[0];
+        // 详情单人:取完整员工档案字段(岗位、入职日期等),只有 1 个请求
+        this.detailRow = (await withEmployeeInfo([latest], { full: true }))[0];
       } catch (error) {
         showRequestError(error, "离职单详情加载失败");
       }
@@ -1222,7 +1223,7 @@ export default {
         if (!id) return;
         let row = { ...this.editing, id };
         try {
-          row = (await withEmployeeInfo([await getResignationById(id)]))[0] || row;
+          row = (await withEmployeeInfo([await getResignationById(id)], { full: true }))[0] || row;
         } catch (error) {
           /* 保存成功后仍可带主键发起 BPM */
         }

+ 13 - 5
src/views/hrManagement/resignationHandoverOrder/index.vue

@@ -88,9 +88,6 @@
         <template v-slot:person="{ row }">
           {{ row.employee || "-" }}
         </template>
-        <template v-slot:department="{ row }">
-          {{ row.department || "-" }}
-        </template>
         <template v-slot:handoverPerson="{ row }">
           {{ row.handoverUserName || "待指定" }}
         </template>
@@ -515,11 +512,22 @@ export default {
           slot: "person",
         },
         {
-          prop: "department",
+          /* 部门 / 机构只认交接单接口原始字段 groupName、deptName(adaptHandover ...raw 透传),
+             没返回就显示 -,不取员工档案等其他来源顶替 */
+          prop: "groupName",
+          label: "所属机构",
+          minWidth: 180,
+          align: "left",
+          showOverflowTooltip: true,
+          formatter: (_r, _c, value) => value || "-",
+        },
+        {
+          prop: "deptName",
           label: "所属部门",
           minWidth: 150,
           align: "left",
-          slot: "department",
+          showOverflowTooltip: true,
+          formatter: (_r, _c, value) => value || "-",
         },
         {
           prop: "handoverUserName",

+ 1 - 1
src/views/position/positionApplication/index.vue

@@ -232,7 +232,7 @@
             <div class="form-grid">
               <el-form-item label="岗位名称" prop="positionName"><el-input v-model.trim="form.positionName" maxlength="40"
                   show-word-limit placeholder="如:数控车床操作工" /></el-form-item>
-              <el-form-item label="机构归属" prop="organizationId"><el-select v-model="form.organizationId" filterable
+              <el-form-item label="所属机构" prop="organizationId"><el-select v-model="form.organizationId" filterable
                   placeholder="请选择所属机构"><el-option v-for="item in legalEntities" :key="item.id" :label="item.name"
                     :value="item.id" /></el-select></el-form-item>
               <el-form-item label="岗位性质" prop="type"><el-select v-model="form.type" clearable

+ 1 - 1
src/views/position/positionManagement/index.vue

@@ -309,7 +309,7 @@ export default {
           slot: "name",
         },
 
-        { label: "组织归属", minWidth: 240, slot: "dept", columnKey: "org" },
+        { label: "所属机构", minWidth: 240, slot: "dept", columnKey: "org" },
         { prop: "seq", label: "序列", width: 80, align: "center" },
         {
           prop: "positionTypeName",

+ 46 - 77
src/views/position/positionSequence/index.vue

@@ -5,19 +5,20 @@
         <el-row :gutter="15">
           <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
             <el-form-item label="岗位序列">
-              <el-input v-model.trim="draftFilters.keyword" clearable placeholder="请输入" />
+              <!-- 直接绑生效条件:输入即过滤(本地模糊匹配),不必等点「查询」 -->
+              <el-input v-model.trim="filters.keyword" clearable placeholder="请输入" />
             </el-form-item>
           </el-col>
           <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
             <el-form-item label="所属机构">
-              <el-select v-model="draftFilters.groupId" clearable filterable placeholder="请选择">
+              <el-select v-model="filters.groupId" clearable filterable placeholder="请选择" @change="handleQueryChange">
                 <el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
             <el-form-item label="状态">
-              <el-select v-model="draftFilters.status" clearable placeholder="请选择">
+              <el-select v-model="filters.status" clearable placeholder="请选择" @change="handleQueryChange">
                 <el-option label="启用" :value="1" />
                 <el-option label="停用" :value="0" />
               </el-select>
@@ -318,11 +319,9 @@ export default {
       legalEntities: [],
       organizationLoading: false,
       sequences: [],
-      /* 关键词是否已由本地兜底路径过滤过(接口 sequence 查空时才会置 true) */
-      keywordMatchedLocally: false,
       /* 新增「所属机构」列,缓存键 +1 使旧的列设置缓存失效,保证新列默认可见 */
       cacheKeyUrl: "hr-pc-position-sequence-v2",
-      draftFilters: emptyFilters(),
+      /* 单一数据源:筛选条件只有这一份,输入框/下拉都直接绑它,改动立即生效 */
       filters: emptyFilters(),
       drawerVisible: false,
       mode: "view",
@@ -434,15 +433,15 @@ export default {
       ];
     },
     filteredSequences() {
-      /* 关键词已由接口按 sequence 查过时不再本地二次过滤(接口可能按编码匹配,
-         本地按名称过滤会误杀合法行);本地兜底路径返回的结果已过滤过 */
-      const keyword = this.keywordMatchedLocally
-        ? ""
-        : this.filters.keyword.toLowerCase();
+      /* 关键词一律在本地按序列名称过滤(模糊、大小写不敏感):
+         接口的 sequence 参数只在「精确命中」时才有结果,部分匹配会查空,
+         之前「接口有结果就信任接口、不再本地过滤」的写法一旦后端忽略该参数,
+         就会把全量数据原样显示出来 —— 表现就是「查询没反应」 */
+      const keyword = String(this.filters.keyword || "").trim().toLowerCase();
       const groupId = this.filters.groupId;
       return this.sequences.filter((item) => {
         const keywordMatch =
-          !keyword || item.name.toLowerCase().includes(keyword);
+          !keyword || String(item.name || "").toLowerCase().includes(keyword);
         const statusMatch =
           this.filters.status === "" ||
           this.filters.status === null ||
@@ -463,16 +462,6 @@ export default {
         return keywordMatch && statusMatch && groupMatch;
       });
     },
-    hasFilters() {
-      const groupId = this.filters.groupId;
-      const hasGroup =
-        groupId !== "" && groupId !== null && groupId !== undefined;
-      const hasStatus =
-        this.filters.status !== "" &&
-        this.filters.status !== null &&
-        this.filters.status !== undefined;
-      return Boolean(this.filters.keyword || hasStatus || hasGroup);
-    },
     readOnly() {
       return this.mode === "view";
     },
@@ -552,6 +541,11 @@ export default {
      * 拉取岗位序列:/hr/positionSequence/page 支持 deptId(机构id)查询参数,
      * 返回行带 deptId / deptName,「所属机构」列以接口字段为准。
      * 选了机构就查该机构;未选机构时逐个机构查后合并,避免后端不带 deptId 时只返回单一机构的数据。
+     *
+     * ★ 关键词(岗位序列)不下发接口,统一由 filteredSequences 在本地按名称模糊过滤:
+     *   接口的 sequence 是「精确匹配」,输入部分名称(如「行政」而序列名是「行政人事岗」)会查空;
+     *   而下发后若后端忽略该参数又会全量返回,两种表现都会让「查询」看起来失效。
+     *   本页数据量本身很小(每机构 size=1000 一次拉全),本地过滤结果稳定可控。
      */
     async fetchSequencesByOrg() {
       const groupId = this.filters.groupId;
@@ -566,32 +560,11 @@ export default {
       const status = this.filters.status;
       const hasStatus =
         status !== null && status !== undefined && status !== "";
-      const base = hasStatus ? { status: String(status) } : {};
-      const keyword = (this.filters.keyword || "").trim();
-      if (!keyword) {
-        this.keywordMatchedLocally = false;
-        return this.requestSequences(list, base);
-      }
-      // 关键词也交给接口(sequence),让「查询」真的按条件请求
-      const rows = await this.requestSequences(list, {
-        ...base,
-        sequence: keyword,
-      });
-      if (rows.length) {
-        /* 接口查到了就信任接口结果,本地不再二次按名称过滤:
-           接口可能是按序列编码匹配的,本地按名称过滤会把合法行误杀 */
-        this.keywordMatchedLocally = false;
-        return rows;
-      }
-      // 接口 sequence 若是精确匹配会查空,退回不带关键词的结果做本地模糊过滤
-      this.keywordMatchedLocally = true;
-      return this.filterByKeyword(
-        await this.requestSequences(list, base),
-        keyword,
-      );
+      return this.requestSequences(list, hasStatus ? { status: String(status) } : {});
     },
     /** 按机构分批请求并合并:多机构同一条序列只留一行 */
     async requestSequences(list, extra = {}) {
+      const failures = [];
       const batches = await Promise.all(
         list.map(async (org) => {
           const hasOrgId =
@@ -607,10 +580,23 @@ export default {
               this.adaptSequence(item, org),
             );
           } catch (error) {
+            /* ★ 不许把失败悄悄吞成「没有数据」:记录下来,请求结束后如实提示,
+               否则接口报错时页面只会显示空列表,看起来像「查询没结果」 */
+            failures.push({ org: org.name || org.id || "", error });
             return [];
           }
         }),
       );
+      if (failures.length) {
+        const detail = failures
+          .map((item) => `${item.org || "全部机构"}:${item.error?.message || "请求失败"}`)
+          .join(";");
+        if (failures.length === list.length) {
+          // 全部批次都失败 → 抛给 loadData 统一提示,列表留空,不伪造数据
+          throw new Error(detail);
+        }
+        this.$message.warning(`部分机构岗位序列加载失败 —— ${detail}`);
+      }
       /* 多机构合并不重行:同一序列主键出现在多个机构批次里(后端未按 deptId 过滤时),
          保留第一次出现的行即可。机构必须以接口返回的 deptId / deptName 为准,
          不能清空 —— 清空会把接口明明返回的 deptName 抹掉,列表只剩个别行有机构。 */
@@ -625,13 +611,6 @@ export default {
       });
       return [...byId.values()];
     },
-    /** 关键词本地模糊过滤(接口 sequence 精确匹配查空时的兜底) */
-    filterByKeyword(rows, keyword) {
-      const kw = keyword.toLowerCase();
-      return rows.filter((row) =>
-        (row.name || "").toLowerCase().includes(kw),
-      );
-    },
     /**
      * 行所属机构 id:优先接口返回的 deptId(兼容历史 groupId / legalId 字段名),
      * 只有 deptName 没有 id 时按名称反查机构选项,都没有才由调用方回退到查询机构。
@@ -681,36 +660,26 @@ export default {
     refreshData() {
       this.loadData(true);
     },
+    /** 表格回到第 1 页并按当前条件重渲染(本地过滤靠 datasource 计算属性变化后自动重载,
+        这里显式调一次,保证「查询 / 重置」后页码不会停在第 3 页看到空白) */
+    reloadFirstPage() {
+      this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
+    },
+    /** 点「查询」:重新拉一次接口(机构/状态是接口条件)+ 回到第 1 页;关键词本地生效 */
     applyFilters() {
-      const prev = { ...this.filters };
-      this.filters = { ...this.draftFilters };
-      this.refreshByQuery(prev);
+      this.loadData();
+      this.reloadFirstPage();
     },
+    /** 点「重置」:条件回到默认(清关键词/状态,机构回登录默认),再重新拉数 */
     resetFilters() {
-      const prev = { ...this.filters };
-      this.draftFilters = emptyFilters();
       this.filters = emptyFilters();
-      this.refreshByQuery(prev);
-    },
-    clearFilter(key) {
-      const prev = { ...this.filters };
-      this.draftFilters[key] = "";
-      this.filters[key] = "";
-      this.refreshByQuery(prev);
+      this.loadData();
+      this.reloadFirstPage();
     },
-    /**
-     * 机构(deptId)、状态(status)、关键词(sequence)都会进接口请求,
-     * 任一项变化就重新请求,保证「查询 / 重置」真的按条件查;
-     * 条件完全相同(例如只按回车重查)时只让表格重载,避免多余请求。
-     */
-    refreshByQuery(prev) {
-      const changed = ["groupId", "status", "keyword"].some(
-        (key) => String(prev[key] ?? "") !== String(this.filters[key] ?? ""),
-      );
-      if (changed) {
-        this.loadData();
-      }
-      this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
+    /** 机构 / 状态下拉变化:这两个是接口条件,需要重新请求 */
+    handleQueryChange() {
+      this.loadData();
+      this.reloadFirstPage();
     },
     cloneRow(row) {
       return {

+ 9 - 0
src/views/salary/salarySlip/index.vue

@@ -299,6 +299,11 @@
               · 版本 v{{ detailResult.resultVersion }}
             </template>
           </span>
+          <!-- C5 已落地:行上有 correctionId 即存在更正草稿,「发起更正」会进入草稿续编模式 -->
+          <el-tag v-if="detailRow?.correctionId != null" size="mini" type="warning" effect="plain"
+            class="draft-tag">
+            已有更正草稿 #{{ detailRow.correctionId }} · 发起更正将续编草稿
+          </el-tag>
           <el-button size="mini" icon="el-icon-refresh" :loading="detailLoading" @click="loadDetail">
             重新获取
           </el-button>
@@ -1824,6 +1829,10 @@ export default {
   margin: 12px 0;
 }
 
+.slip-detail .draft-tag {
+  flex: none;
+}
+
 .slip-detail .slip-detail-name {
   font-size: 16px;
   font-weight: 600;