Просмотр исходного кода

按钮级权限码接入与员工档案列结构重排:各页操作按钮统一加 hasPermission 判断(码形如 hr:xxx:yyy),动作码按场景分流(合同草稿保存/提交/作废、offer 通知发送与线下确认、薪资模板 create/edit/change、薪资项目启停);员工档案列表按另一套系统重排列(姓名/所属机构/部门/工号/手机号/岗位/所属工厂/用户账号/性别/状态/是否启用/创建时间/操作),新增是否启用与创建时间列、工厂与部门名称按字典翻译不编造,列显隐缓存 bump 到 v3,左树改 tree-scroll 跟随右侧表格同高;员工档案详情状态文案改读 hr_user_status 字典(改取 statusCode,不再硬编码 active/probation 文案);岗位管理与技能矩阵的部门筛选由扁平下拉改树形级联并剔除法人机构节点,技能分类侧栏暂隐;员工档案编辑态锁定机构/工厂/部门/岗位序列/岗位/性质/层级/直属上级/状态,员工状态补必填星号;人员结构看板取消锁一屏改自然滚动、图表区 4 列改 3 列并调整模块顺序;员工导入改走 /main/excelUpload/newImportUser;组织列表 createTime 统一格式化为 yyyy-MM-dd HH:mm:ss;薪资版本状态映射补 ENABLED/DISABLED

lihaiting 10 часов назад
Родитель
Сommit
5588563b23
36 измененных файлов с 733 добавлено и 480 удалено
  1. 24 2
      src/api/organization/index.js
  2. 2 1
      src/api/salary/employeeSalary.js
  3. 12 7
      src/styles/views/employeeArchive/index.scss
  4. 16 17
      src/styles/views/personnelStructureAnalysisBoard/index.css
  5. 8 0
      src/styles/views/positionManagement/index.scss
  6. 2 2
      src/utils/employee-status-dict.js
  7. 13 4
      src/views/attendance/report/index.vue
  8. 16 11
      src/views/attendance/rule/index.vue
  9. 72 30
      src/views/contractManagement/index.vue
  10. 4 2
      src/views/hiring/hiringApproval/components/ApprovalFormDrawer.vue
  11. 10 8
      src/views/hiring/hiringApproval/index.vue
  12. 18 3
      src/views/hiring/offerNotify/index.vue
  13. 44 57
      src/views/hrAnalysis/personnelStructureAnalysisBoard/index.vue
  14. 24 3
      src/views/hrManagement/employeeArchive/components/EmployeeArchiveDetail.vue
  15. 13 11
      src/views/hrManagement/employeeArchive/components/EmployeeArchiveForm.vue
  16. 111 60
      src/views/hrManagement/employeeArchive/index.vue
  17. 10 8
      src/views/hrManagement/itemReceiptManagement/index.vue
  18. 10 9
      src/views/hrManagement/onboardingHandling/index.vue
  19. 12 10
      src/views/hrManagement/positionTransfer/index.vue
  20. 12 10
      src/views/hrManagement/regularizationHandling/index.vue
  21. 21 15
      src/views/hrManagement/resignationApplication/index.vue
  22. 16 13
      src/views/hrManagement/resignationHandoverOrder/index.vue
  23. 8 7
      src/views/position/positionApplication/index.vue
  24. 11 2
      src/views/position/positionManagement/components/PositionEditDialog.vue
  25. 53 98
      src/views/position/positionManagement/components/SkillMatrixPanel.vue
  26. 104 37
      src/views/position/positionManagement/index.vue
  27. 6 3
      src/views/position/positionSequence/index.vue
  28. 1 1
      src/views/salary/appeal/index.vue
  29. 10 1
      src/views/salary/components/user-edit.vue
  30. 4 2
      src/views/salary/employeeArchive/index.vue
  31. 12 5
      src/views/salary/fieldSetting/index.vue
  32. 20 17
      src/views/salary/salarySlip/index.vue
  33. 16 11
      src/views/salary/salaryTemplate/index.vue
  34. 12 9
      src/views/salary/socialScheme/index.vue
  35. 4 3
      src/views/salary/statistics/index.vue
  36. 2 1
      src/views/salary/taxSetting/index.vue

+ 24 - 2
src/api/organization/index.js

@@ -73,6 +73,16 @@ function formatDate(value) {
   return `${y}-${m}-${d}`;
 }
 
+/* 带时分的日期时间(列表「创建时间」列):已是 yyyy-MM-dd HH:mm:ss 原样展示,其余复用 formatDate */
+function formatDateTime(value) {
+  if (!value) return "";
+  const text = String(value);
+  if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text)) {
+    return text.replace("T", " ").slice(0, 19);
+  }
+  return formatDate(value);
+}
+
 /* 文件流下载统一走 utils/download 的 downloadBlobResponse:
    文件名解码(filename= 里的 percent-encoding 不 decode 会乱码)+ JSON 业务错误拦截 */
 
@@ -88,7 +98,10 @@ export async function listOrganizations(params, options) {
 /**
  * 机构列表(带人数):GET /main/group/getGroupListWithUserCount
  *
- * 返回体在 getGroupList 基础上多一个 userCount(后端口径的组织人数)。
+ * 文档查询参数(均可选):currentUserId 当前用户 id、deptIds 机构 id 数组、
+ * name 名称、parentId 父 id、status 状态(0 关闭 / 1 启用)。
+ *
+ * 返回体在 getGroupList 基础上多一个 userCount(当前节点及全部下级节点的在职人数)。
  * 员工档案组织树直接展示该字段,前端不再逐部门请求统计人数。
  *
  * 不走 utils/org-list-cache(那条缓存针对 getGroupList):本接口需要按需强刷人数,
@@ -442,11 +455,19 @@ export function hasJobNumber(record = {}) {
 
 
 
+/**
+ * 员工档案导入:**按 HR 任职链路导入**(POST /main/excelUpload/newImportUser)
+ *
+ * 2026-10-20 用户给定口径:导入改走 newImportUser(岗位序列 / 岗位性质 / 岗位层级 / 隶属部门编码等
+ * 字段按任职链路落库),旧的 /main/excelUpload/importUser 不再使用。
+ * swagger 快照里还没有这个接口(文档滞后),响应模型以实际返回为准:
+ * code == 0 视为成功,失败原样抛出后端 message。
+ */
 export async function importUsers(file) {
   const formData = new FormData();
   formData.append("file", file);
   // 导入是后端逐行落库,行数多时明显超过普通接口耗时,超时放宽到 2 分钟
-  const response = await request.post("/main/excelUpload/importUser", formData, {
+  const response = await request.post("/main/excelUpload/newImportUser", formData, {
     timeout: 120000,
   });
   if (response.data?.code == 0 || typeof response.data === "string") {
@@ -955,6 +976,7 @@ export function adaptEmployee(raw = {}, extras = {}) {
     statusCode: raw.status,
     // ★ 新增字段:后端直接给的状态中文名(列表「状态」列直接展示它,不再本地派生文案)
     statusName: raw.statusName || "",
+    createTime: formatDateTime(raw.createTime),
     hireDate: formatDate(raw.joinDate || raw.hireDate || raw.entryDate),
     regularDate: formatDate(raw.positiveDate),
     enabled: Number(raw.isEnabled) !== 0,

+ 2 - 1
src/api/salary/employeeSalary.js

@@ -179,7 +179,8 @@ export const CHANGE_SOURCE_OPTIONS = Object.keys(CHANGE_SOURCE_LABELS).map(
 /** 档案状态:后端未给枚举,未识别取值原样显示 */
 function profileStatusLabel(status) {
   if (!status) return '—';
-  const map = { DRAFT: '草稿', EFFECTIVE: '生效中', EXPIRED: '已失效', LOCKED: '已锁定' };
+  /* 2026-10-21 用户实测:版本状态返回 ENABLED(不是 EFFECTIVE),映射表补齐 */
+  const map = { DRAFT: '草稿', ENABLED: '启用', DISABLED: '停用', EFFECTIVE: '生效中', EXPIRED: '已失效', LOCKED: '已锁定' };
   return map[String(status).toUpperCase()] || String(status);
 }
 

+ 12 - 7
src/styles/views/employeeArchive/index.scss

@@ -31,14 +31,17 @@
   background: #fff;
   box-shadow: 0 9px 28px rgba(40, 67, 106, .06);
 }
+/* 右侧表格面板:白底卡片,与左树同为固定高度(表格本体 520px 固定 + 内部滚动),
+   不随列表数据多少伸缩 */
 .table-panel {
   overflow: hidden;
-  border: 0;
-  border-radius: 0;
-  background: transparent;
-  box-shadow: none;
+  border: 1px solid #e0e7f1;
+  border-radius: 16px;
+  background: #fff;
+  box-shadow: 0 9px 28px rgba(40, 67, 106, .06);
 }
-.organization-panel { min-width: 0; padding: 22px 18px 18px; transition: width .2s; }
+/* 面板与右侧表格同高(grid stretch),树要吃掉面板剩余高度,否则底部留一截空白 */
+.organization-panel { display: flex; flex-direction: column; min-width: 0; padding: 22px 18px 18px; transition: width .2s; }
 .organization-panel.collapsed { width: 58px; padding: 14px 8px; }
 .archive-workspace:has(.organization-panel.collapsed) { grid-template-columns: 58px minmax(0, 1fr); }
 .panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
@@ -51,7 +54,9 @@
 .all-organization.active { color: #1890ff; background: linear-gradient(90deg, #eaf3ff, #f4f9ff); }
 .all-organization i { margin-right: 7px; }
 .all-organization em, .tree-node-content em { min-width: 24px; padding: 1px 5px; border-radius: 8px; color: #8190a3; font-size: 14px; font-style: normal; text-align: center; background: #eef1f5; }
-.organization-tree { max-height: 530px; overflow-y: auto; font-size: 14px; }
+/* 树滚动区:flex 撑满面板剩余高度(面板高度由 grid 跟右侧表格对齐,树内容不参与撑高) */
+.tree-scroll { flex: 1 1 0; min-height: 0; position: relative; }
+.organization-tree { position: absolute; top: 0; right: 0; bottom: 0; left: 0; overflow-y: auto; font-size: 14px; }
 .organization-tree ::v-deep .el-tree-node__content { height: 44px; margin: 1px 0; border-radius: 9px; }
 .organization-tree ::v-deep .el-tree-node__content:hover { background: #f5f7fa; }
 .organization-tree ::v-deep .is-current > .el-tree-node__content { color: #1890ff; background: linear-gradient(90deg, #eaf3ff, #f4f9ff); }
@@ -123,7 +128,7 @@
   .organization-panel.collapsed { width: 100%; }
   .collapsed-tree-button { width: 100%; min-height: 36px; flex-direction: row; }
   .collapsed-tree-button span { writing-mode: initial; }
-  .organization-tree { max-height: 250px; }
+  .tree-scroll { flex: 0 0 auto; max-height: 250px; }
 }
 @media (max-width: 720px) {
   .employee-archive-page { padding: 12px; }

+ 16 - 17
src/styles/views/personnelStructureAnalysisBoard/index.css

@@ -18,12 +18,9 @@
   --glow-soft: 0 0 18px rgba(0, 180, 255, 0.18);
   width: 100%;
   min-width: 320px;
-  /* 与 ele-admin 固定主体 + card 页签的内容区等高(100vh - 顶栏56 - 页签40 - 间距8),
-     页面不超出内容区,外层就不会出现滚动条 */
-  height: calc(100vh - 104px);
-  max-height: 100%;
-  min-height: 0;
-  overflow: hidden;
+  /* 2026-10-21 用户要求:不再锁一屏,小模块放大、往下排,整页自然滚动 */
+  min-height: calc(100vh - 104px);
+  overflow: visible;
   color: var(--text);
   font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
   background:
@@ -42,13 +39,13 @@
 .personnel-board {
   position: relative;
   width: 100%;
-  height: 100%;
   min-height: 0;
   padding: 16px 18px 12px;
-  display: grid;
-  grid-template-rows: 54px minmax(106px, .46fr) repeat(3, minmax(0, 1fr)) 20px;
-  gap: 14px;
-  overflow: hidden;
+  /* 竖向 flex 依次往下排:header → 指标卡 → 三段图表区 → footer */
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  overflow: visible;
   background:
     linear-gradient(rgba(0, 190, 255, 0.035) 1px, transparent 1px),
     linear-gradient(90deg, rgba(0, 190, 255, 0.035) 1px, transparent 1px),
@@ -190,10 +187,11 @@
 
 /* ── Metric cards ── */
 .metric-grid {
-  min-height: 0;
+  min-height: 118px;
   display: grid;
   grid-template-columns: repeat(3, minmax(0, 1fr));
-  gap: 12px;
+  gap: 14px;
+  flex: none;
 }
 
 .metric-card {
@@ -333,17 +331,19 @@
 .dashboard-grid {
   min-height: 0;
   display: grid;
-  gap: 12px;
+  gap: 14px;
+  flex: none;
 }
 
-/* 三行均为四列:11 个维度 + 人员结构趋势 + 整体人员分布,共 12 个面板 */
+/* 2026-10-21:不再锁一屏,图表区固定 3 列,卡片给足高度往下排(12 个面板共 4 行,整页滚动) */
 .dashboard-grid--top,
 .dashboard-grid--middle,
-.dashboard-grid--bottom { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+.dashboard-grid--bottom { grid-template-columns: repeat(3, minmax(0, 1fr)); }
 
 .dashboard-card {
   position: relative;
   overflow: hidden;
+  height: 350px;
   border: 1px solid var(--border);
   border-radius: 0;
   background: var(--panel);
@@ -1148,7 +1148,6 @@
   .personnel-board {
     padding: 12px 14px 10px;
     gap: 12px;
-    grid-template-rows: 50px minmax(98px, .44fr) repeat(3, minmax(0, 1fr)) 16px;
   }
   .board-header h1 { font-size: 21px; line-height: 26px; }
   .board-heading p { margin-top: 3px; }

+ 8 - 0
src/styles/views/positionManagement/index.scss

@@ -309,9 +309,17 @@
   width: 220px;
 }
 .table-panel .position-filter-form ::v-deep .el-form-item__content .el-select,
+.table-panel .position-filter-form ::v-deep .el-form-item__content .el-cascader,
 .table-panel .position-filter-form ::v-deep .el-form-item__content .el-input {
   width: 100%;
 }
+/* 部门树形级联(popper 挂在 body 上,这里不能用 scoped) */
+.position-dept-cascader .el-cascader-panel .el-cascader-node {
+  padding-right: 24px;
+}
+.position-dept-cascader .el-cascader-menu__wrap {
+  max-height: 260px;
+}
 .table-panel .position-filter-form ::v-deep .el-input__inner {
   height: 32px;
   line-height: 32px;

+ 2 - 2
src/utils/employee-status-dict.js

@@ -1,9 +1,9 @@
 import { getDictByCode } from '@/api/sys';
 
 /**
- * 员工状态字典(hr_user_status)—— 新增/编辑员工档案的下拉、列表「状态」筛选共用同一份。
+ * 员工状态字典(hr_user_status)—— 新增/编辑员工档案的下拉、列表「状态」筛选、详情展示共用同一份。
  *
- * 约定(2026-10-14 与用户确认,字典抓包修正):
+ * 约定(2026-10-22 用户确认字典编码仍用 hr_user_status;码空间沿用原抓包结论):
  * - 选项完全由字典决定(label = 字典文案,value = 字典里的数字状态码);
  * - **下拉 value / 提交的 status / 筛选参数一律用该数字码**,前端不另维护一套码表;
  * - 实测码空间:1=正式在职、6=试用期、7=待岗、8=已离职(与 swagger 注释「1-6」不符,

+ 13 - 4
src/views/attendance/report/index.vue

@@ -14,7 +14,8 @@
           <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 type="primary" icon="el-icon-download" size="medium" @click="exportData">导出数据</el-button>
+          <el-button v-if="$hasPermission('hr:hrAttendanceReport:export')" type="primary" icon="el-icon-download"
+            size="medium" @click="exportData">导出数据</el-button>
         </div>
       </div>
     </section>
@@ -90,7 +91,8 @@
         <div>
           <h2>考勤明细</h2>
         </div>
-        <el-button type="primary" size="small" icon="el-icon-download" @click="exportTable">导出数据</el-button>
+        <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">
@@ -280,12 +282,19 @@ export default {
       this.loading = true;
       try {
         const params = this.queryParams();
-        const [dept, personal, exceptions, daily] = await Promise.all([
+        const [dept, exceptions, daily] = await Promise.all([
           getDepartmentAttendanceReport(params).catch(() => ({})),
-          getPersonalAttendanceReport(params).catch(() => ({ list: [] })),
           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),

+ 16 - 11
src/views/attendance/rule/index.vue

@@ -44,7 +44,8 @@
       <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-rule')" class="rule-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('hr:hrAttendanceRule:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新建
           </el-button>
         </template>
@@ -75,23 +76,24 @@
           </el-tag>
         </template>
         <template v-slot:action="{ row }">
-          <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
+          <el-link v-if="$hasPermission('hr:hrAttendanceRule:update')" type="primary" :underline="false"
+            icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
-          <el-link type="primary" :underline="false" icon="el-icon-document-copy" :disabled="submitting"
-            @click="handleCopy(row)">
+          <el-link v-if="$hasPermission('hr:hrAttendanceRule:copy')" type="primary" :underline="false"
+            icon="el-icon-document-copy" :disabled="submitting" @click="handleCopy(row)">
             复制
           </el-link>
-          <el-link v-if="isRuleActive(row.status)" type="warning" :underline="false" icon="el-icon-video-pause"
-            :disabled="submitting" @click="disableRule(row)">
+          <el-link v-if="isRuleActive(row.status) && $hasPermission('hr:hrAttendanceRule:disableRule')" type="warning"
+            :underline="false" icon="el-icon-video-pause" :disabled="submitting" @click="disableRule(row)">
             停用
           </el-link>
-          <el-link v-else type="success" :underline="false" icon="el-icon-video-play" :disabled="submitting"
-            @click="enableRule(row)">
+          <el-link v-else-if="!isRuleActive(row.status) && $hasPermission('hr:hrAttendanceRule:enable')"
+            type="success" :underline="false" icon="el-icon-video-play" :disabled="submitting" @click="enableRule(row)">
             启用
           </el-link>
-          <el-link type="danger" :underline="false" icon="el-icon-delete" :disabled="submitting"
-            @click="handleDelete(row)">
+          <el-link v-if="$hasPermission('hr:hrAttendanceRule:delete')" type="danger" :underline="false"
+            icon="el-icon-delete" :disabled="submitting" @click="handleDelete(row)">
             删除
           </el-link>
         </template>
@@ -124,7 +126,10 @@
       </div>
       <span slot="footer">
         <el-button @click="dialogVisible = false">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
-        <el-button v-if="!dialogReadonly" type="primary" :loading="submitting" @click="save">保存规则</el-button>
+        <el-button v-if="!dialogReadonly && (form.id
+          ? $hasPermission('hr:hrAttendanceRule:update')
+          : $hasPermission('hr:hrAttendanceRule:save'))" type="primary" :loading="submitting"
+          @click="save">保存规则</el-button>
       </span>
     </ele-modal>
 

+ 72 - 30
src/views/contractManagement/index.vue

@@ -46,11 +46,12 @@
         :datasource="fetchPage" :need-page="true" :page-size="20" :page-sizes="[10, 20, 50, 100]"
         :cache-key="$tableCacheKey('hr-contract-management')" class="cm-pro-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('hr:hrContract:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             发起合同
           </el-button>
-          <el-button type="primary" icon="el-icon-download" class="ele-btn-icon" :loading="exporting"
-            @click="exportList">
+          <el-button v-if="$hasPermission('hr:hrContract:export')" type="primary" icon="el-icon-download"
+            class="ele-btn-icon" :loading="exporting" @click="exportList">
             导出
           </el-button>
         </template>
@@ -110,36 +111,44 @@
           -->
           <template v-if="hasFlowDraft(row)">
             <template v-if="isChangeDraftStatus(row)">
-              <el-link type="primary" :underline="false" icon="el-icon-sort" @click="openChange(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:change')" type="primary" :underline="false"
+                icon="el-icon-sort" @click="openChange(row)">
                 变更
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-s-promotion" @click="launchChangeApproval(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:submitChange')" type="primary" :underline="false"
+                icon="el-icon-s-promotion" @click="launchChangeApproval(row)">
                 提交变更
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoidDraft(row, 'CHANGE')">
+              <el-link v-if="$hasPermission('hr:hrContract:cancelChange')" type="primary" :underline="false"
+                icon="el-icon-close" @click="openVoidDraft(row, 'CHANGE')">
                 作废变更
               </el-link>
             </template>
             <template v-if="isRenewDraftStatus(row)">
-              <el-link type="primary" :underline="false" icon="el-icon-refresh" @click="openRenewDraft(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:renew')" type="primary" :underline="false"
+                icon="el-icon-refresh" @click="openRenewDraft(row)">
                 续签
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-s-promotion" @click="submitRenewalApproval(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:submitRenew')" type="primary" :underline="false"
+                icon="el-icon-s-promotion" @click="submitRenewalApproval(row)">
                 提交
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoidDraft(row, 'RENEW')">
+              <el-link v-if="$hasPermission('hr:hrContract:cancelRenew')" type="primary" :underline="false"
+                icon="el-icon-close" @click="openVoidDraft(row, 'RENEW')">
                 作废续签
               </el-link>
             </template>
             <template v-if="isTerminateDraftStatus(row)">
-              <el-link type="primary" :underline="false" icon="el-icon-remove-outline" @click="openTerminate(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:terminate')" type="primary" :underline="false"
+                icon="el-icon-remove-outline" @click="openTerminate(row)">
                 解除
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-s-promotion" @click="launchTerminationApproval(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:submitTerminate')" type="primary" :underline="false"
+                icon="el-icon-s-promotion" @click="launchTerminationApproval(row)">
                 提交
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-close"
-                @click="openVoidDraft(row, 'TERMINATION')">
+              <el-link v-if="$hasPermission('hr:hrContract:cancelTerminate')" type="primary" :underline="false"
+                icon="el-icon-close" @click="openVoidDraft(row, 'TERMINATION')">
                 作废解除
               </el-link>
             </template>
@@ -164,31 +173,36 @@
           </template>
           <template v-else>
             <template v-if="isDraft(row)">
-              <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:update')" type="primary" :underline="false"
+                icon="el-icon-edit" @click="openEdit(row)">
                 编辑
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-close" @click="openVoid(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:invalid')" type="primary" :underline="false"
+                icon="el-icon-close" @click="openVoid(row)">
                 作废合同
               </el-link>
             </template>
             <template v-if="isEffective(row)">
-              <el-link type="primary" :underline="false" icon="el-icon-sort" @click="openChange(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:change')" type="primary" :underline="false"
+                icon="el-icon-sort" @click="openChange(row)">
                 变更
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-refresh" @click="openRenew(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:renew')" type="primary" :underline="false"
+                icon="el-icon-refresh" @click="openRenew(row)">
                 续签
               </el-link>
-              <el-link type="primary" :underline="false" icon="el-icon-remove-outline" @click="openTerminate(row)">
+              <el-link v-if="$hasPermission('hr:hrContract:terminate')" type="primary" :underline="false"
+                icon="el-icon-remove-outline" @click="openTerminate(row)">
                 解除
               </el-link>
             </template>
-            <el-link v-if="isDraft(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-              @click="launchApproval(row)">
+            <el-link v-if="isDraft(row) && $hasPermission('hr:hrContract:submit')" type="primary"
+              :underline="false" icon="el-icon-s-promotion" @click="launchApproval(row)">
               提交
             </el-link>
             <!-- 已解除/已终止但尚未发起解除审批:再次给出提交入口(发起的是解除审批) -->
-            <el-link v-if="canLaunchTerminationApproval(row)" type="primary" :underline="false"
-              icon="el-icon-s-promotion" @click="launchTerminationApproval(row)">
+            <el-link v-if="canLaunchTerminationApproval(row) && $hasPermission('hr:hrContract:submitTerminate')"
+              type="primary" :underline="false" icon="el-icon-s-promotion" @click="launchTerminationApproval(row)">
               提交
             </el-link>
             <!-- 续签已签署待生效:审批已走完,操作不锁死,保留刚走完的续签审批记录入口 -->
@@ -198,12 +212,12 @@
             </el-link>
             <!-- 驳回态:流程已结束,操作不锁死,但要能查看驳回原因(合同自身 / 续签 / 解除) -->
             <!-- 合同审批被驳回:可编辑后再次提交审批(与签订/续签同一个 hr_contract_approval,businessId = 合同主键) -->
-            <el-link v-if="isContractRejected(row)" type="primary" :underline="false" icon="el-icon-edit"
-              @click="openEdit(row)">
+            <el-link v-if="isContractRejected(row) && $hasPermission('hr:hrContract:update')" type="primary"
+              :underline="false" icon="el-icon-edit" @click="openEdit(row)">
               编辑
             </el-link>
-            <el-link v-if="isContractRejected(row)" type="primary" :underline="false"
-              icon="el-icon-s-promotion" @click="launchApproval(row)">
+            <el-link v-if="isContractRejected(row) && $hasPermission('hr:hrContract:submit')" type="primary"
+              :underline="false" icon="el-icon-s-promotion" @click="launchApproval(row)">
               提交
             </el-link>
             <el-link v-if="isContractRejected(row)" type="warning" :underline="false"
@@ -737,8 +751,9 @@
         <div class="cm-create-footer">
           <div class="cm-footer-actions">
             <el-button @click="closeCreate">取消</el-button>
-            <el-button :loading="saving" @click="saveCreateDraft">保存</el-button>
-            <el-button type="primary" :loading="saving" @click="submitCreate">提交</el-button>
+            <el-button v-if="canSaveContractForm" :loading="saving" @click="saveCreateDraft">保存</el-button>
+            <el-button v-if="canSubmitContractForm" type="primary" :loading="saving"
+              @click="submitCreate">提交</el-button>
           </div>
         </div>
       </div>
@@ -806,7 +821,8 @@
         <div class="cm-create-footer">
           <div class="cm-footer-actions">
             <el-button @click="terminateVisible = false">取消</el-button>
-            <el-button type="primary" :loading="terminateSubmitting" @click="submitTerminate">提交</el-button>
+            <el-button v-if="$hasPermission('hr:hrContract:submitTerminate')" type="primary"
+              :loading="terminateSubmitting" @click="submitTerminate">提交</el-button>
           </div>
         </div>
       </div>
@@ -824,7 +840,7 @@
       </el-form>
       <span slot="footer">
         <el-button @click="voidVisible = false">取消</el-button>
-        <el-button type="primary" :loading="voidSubmitting" @click="submitVoid">确定</el-button>
+        <el-button v-if="canVoidDraft" type="primary" :loading="voidSubmitting" @click="submitVoid">确定</el-button>
       </span>
     </ele-modal>
 
@@ -1444,6 +1460,32 @@ export default {
     };
   },
   computed: {
+    /** 「发起/编辑」弹框底部「保存」:按 createMode 取对应动作码 */
+    canSaveContractForm() {
+      const codeMap = {
+        create: 'hr:hrContract:save',
+        edit: 'hr:hrContract:update',
+        change: 'hr:hrContract:change',
+        renew: 'hr:hrContract:renew',
+      };
+      return this.$hasPermission(codeMap[this.createMode] || 'hr:hrContract:save');
+    },
+    /** 「发起/编辑」弹框底部「提交」:变更 / 续签走各自提交码 */
+    canSubmitContractForm() {
+      if (this.createMode === 'change') return this.$hasPermission('hr:hrContract:submitChange');
+      if (this.createMode === 'renew') return this.$hasPermission('hr:hrContract:submitRenew');
+      return this.$hasPermission('hr:hrContract:submit');
+    },
+    /** 作废弹框「确定」:按作废对象(合同 / 变更 / 续签 / 解除草稿)取对应码 */
+    canVoidDraft() {
+      const codeMap = {
+        CONTRACT: 'hr:hrContract:invalid',
+        CHANGE: 'hr:hrContract:cancelChange',
+        RENEW: 'hr:hrContract:cancelRenew',
+        TERMINATION: 'hr:hrContract:cancelTerminate',
+      };
+      return this.$hasPermission(codeMap[this.voidForm.targetType] || 'hr:hrContract:invalid');
+    },
     styleResponsive() {
       return this.$store.state.theme?.styleResponsive !== false;
     },

+ 4 - 2
src/views/hiring/hiringApproval/components/ApprovalFormDrawer.vue

@@ -174,8 +174,10 @@
     </div>
 
     <div v-if="!readOnly" slot="footer" class="dialog-footer">
-      <el-button size="small" :loading="saving" @click="saveDraft">保存</el-button>
-      <el-button size="small" type="primary" :loading="submitting" @click="submit">提交</el-button>
+      <el-button v-if="$hasPermission('hr:hrHiring:saveDraft') || $hasPermission('hr:hrHiring:saveDraft')" size="small"
+        :loading="saving" @click="saveDraft">保存</el-button>
+      <el-button v-if="$hasPermission('hr:hrHiring:submit')" size="small" type="primary" :loading="submitting"
+        @click="submit">提交</el-button>
     </div>
   </el-dialog>
 </template>

+ 10 - 8
src/views/hiring/hiringApproval/index.vue

@@ -31,7 +31,8 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey('hr-hiring-approval')" class="hiring-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('hr:hrHiring:saveDraft')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新建
           </el-button>
         </template>
@@ -56,24 +57,25 @@
           <span v-else>-</span>
         </template>
         <template v-slot:action="{ row }">
-          <el-link v-if="canEdit(row)" type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
+          <el-link v-if="canEdit(row) && $hasPermission('hr:hrHiring:saveDraft')" type="primary" :underline="false"
+            icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
           <!-- 仅草稿(尚未发起审批流程)可删除 → el-link 不支持 loading,故用 disabled 防连点 -->
-          <el-link v-if="canDelete(row)" type="danger" :underline="false" icon="el-icon-delete"
-            :disabled="submitting" @click="handleDelete(row)">
+          <el-link v-if="canDelete(row) && $hasPermission('hr:hrHiring:delete')" type="danger" :underline="false"
+            icon="el-icon-delete" :disabled="submitting" @click="handleDelete(row)">
             删除
           </el-link>
           <el-link v-if="canShowApprovalRecord(row)" type="primary" :underline="false" icon="el-icon-tickets"
             @click="openApprovalRecord(row)">
             审批记录
           </el-link>
-          <el-link v-if="canLaunch(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            @click="launchApproval(row)">
+          <el-link v-if="canLaunch(row) && $hasPermission('hr:hrHiring:submit')" type="primary" :underline="false"
+            icon="el-icon-s-promotion" @click="launchApproval(row)">
             提交
           </el-link>
-          <el-link v-if="canLaunchOffer(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            @click="openOffer(row)">
+          <el-link v-if="canLaunchOffer(row) && $hasPermission('hr:hrHiring:offer')" type="primary"
+            :underline="false" icon="el-icon-s-promotion" @click="openOffer(row)">
             发起Offer
           </el-link>
         </template>

+ 18 - 3
src/views/hiring/offerNotify/index.vue

@@ -29,7 +29,8 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey('hr-offer-notify')" class="offer-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openNotify()">
+          <el-button v-if="$hasPermission('hr:hrOfferNotify:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openNotify()">
             发起
           </el-button>
         </template>
@@ -52,8 +53,8 @@
           <span v-else>-</span>
         </template>
         <template v-slot:action="{ row }">
-          <el-link v-if="canNotify(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            :disabled="submitting" @click="handleNotifyAction(row)">
+          <el-link v-if="canNotify(row) && canNotifyByPermission(row)" type="primary" :underline="false"
+            icon="el-icon-s-promotion" :disabled="submitting" @click="handleNotifyAction(row)">
             {{ notifyActionLabel(row) }}
           </el-link>
         </template>
@@ -288,6 +289,20 @@ export default {
       }
       return "继续通知";
     },
+    /**
+     * 通知动作按权限码分流:
+     * 发送邮件/重新发送 → send;确认线下 → confirmOffline;继续通知(补录/编辑)→ save
+     */
+    canNotifyByPermission(row) {
+      const label = this.notifyActionLabel(row);
+      if (label === "确认线下") {
+        return this.$hasPermission("hr:hrOfferNotify:confirmOffline");
+      }
+      if (label === "发送邮件" || label === "重新发送") {
+        return this.$hasPermission("hr:hrOfferNotify:send");
+      }
+      return this.$hasPermission("hr:hrOfferNotify:save");
+    },
     async fetchPage({ page, limit }) {
       try {
         const data = await getOfferPage({

+ 44 - 57
src/views/hrAnalysis/personnelStructureAnalysisBoard/index.vue

@@ -6,13 +6,7 @@
           <div class="board-heading">
             <span class="board-tag">HR STRUCTURE BOARD</span>
             <h1>人员结构分析</h1>
-            <p>
-              统计日 {{ statisticsDate || EMPTY_TEXT }} · {{ monthLabel }} ·
-              {{ organizationLabel }}
-              <em v-if="reportStatusText" class="heading-warn" :title="reportMessage">
-                {{ reportStatusText }}
-              </em>
-            </p>
+            <!-- 统计日/期间/组织副标题已去掉:与右上筛选器和页脚「数据统计日」重复(2026-10-21 用户要求) -->
           </div>
           <div class="header-toolbar">
             <label class="filter-item">
@@ -69,7 +63,8 @@
             <button class="header-button header-button--plain" type="button" @click="openDetail()">
               查看明细
             </button>
-            <button class="header-button" type="button" :disabled="exporting" @click="exportBoard">
+            <button v-if="$hasPermission('hr:hrAnalysisBoard:export')" class="header-button" type="button"
+              :disabled="exporting" @click="exportBoard">
               <i class="el-icon-download"></i>{{ exporting ? "导出中" : "导出" }}
             </button>
           </div>
@@ -172,27 +167,8 @@
             <div v-if="trend.months.length" ref="trendChart" class="chart"></div>
             <div v-else class="card-empty">{{ trendMessage }}</div>
           </article>
-        </section>
-
-        <section class="dashboard-grid dashboard-grid--middle">
-          <article class="dashboard-card">
-            <panel-title title="部门人员分析" unit="单位:人" />
-            <div v-if="hasGroup(DEPARTMENT)" ref="departmentChart" class="chart"></div>
-            <div v-else class="card-empty">{{ emptyText(DEPARTMENT) }}</div>
-          </article>
-
-          <article class="dashboard-card">
-            <panel-title title="性别分析" unit="单位:人" />
-            <div v-if="hasGroup(GENDER)" ref="genderChart" class="chart"></div>
-            <div v-else class="card-empty">{{ emptyText(GENDER) }}</div>
-          </article>
-
-          <article class="dashboard-card">
-            <panel-title title="合同类型分析" unit="单位:人" />
-            <div v-if="hasGroup(CONTRACT_TYPE)" ref="contractChart" class="chart"></div>
-            <div v-else class="card-empty">{{ emptyText(CONTRACT_TYPE) }}</div>
-          </article>
 
+          <!-- 2026-10-21 用户要求:学历分析、整体人员分布移到「人员结构趋势」右侧 -->
           <article class="dashboard-card">
             <panel-title
               title="学历分析"
@@ -204,26 +180,6 @@
             <div v-if="hasGroup(educationMode)" ref="educationChart" class="chart"></div>
             <div v-else class="card-empty">{{ emptyText(educationMode) }}</div>
           </article>
-        </section>
-
-        <section class="dashboard-grid dashboard-grid--bottom">
-          <article class="dashboard-card">
-            <panel-title title="年龄分布" unit="单位:人" />
-            <div v-if="hasGroup(AGE)" ref="ageChart" class="chart"></div>
-            <div v-else class="card-empty">{{ emptyText(AGE) }}</div>
-          </article>
-
-          <article class="dashboard-card">
-            <panel-title title="司龄分析" unit="单位:人" />
-            <div v-if="hasGroup(TENURE)" ref="seniorityChart" class="chart"></div>
-            <div v-else class="card-empty">{{ emptyText(TENURE) }}</div>
-          </article>
-
-          <article class="dashboard-card">
-            <panel-title title="任职年限分析" unit="单位:人" />
-            <div v-if="hasGroup(POSITION_TENURE)" ref="tenureChart" class="chart"></div>
-            <div v-else class="card-empty">{{ emptyText(POSITION_TENURE) }}</div>
-          </article>
 
           <article class="dashboard-card">
             <panel-title title="整体人员分布" unit="单位:人" />
@@ -264,6 +220,46 @@
           </article>
         </section>
 
+        <section class="dashboard-grid dashboard-grid--middle">
+          <article class="dashboard-card">
+            <panel-title title="部门人员分析" unit="单位:人" />
+            <div v-if="hasGroup(DEPARTMENT)" ref="departmentChart" class="chart"></div>
+            <div v-else class="card-empty">{{ emptyText(DEPARTMENT) }}</div>
+          </article>
+
+          <article class="dashboard-card">
+            <panel-title title="性别分析" unit="单位:人" />
+            <div v-if="hasGroup(GENDER)" ref="genderChart" class="chart"></div>
+            <div v-else class="card-empty">{{ emptyText(GENDER) }}</div>
+          </article>
+
+          <article class="dashboard-card">
+            <panel-title title="合同类型分析" unit="单位:人" />
+            <div v-if="hasGroup(CONTRACT_TYPE)" ref="contractChart" class="chart"></div>
+            <div v-else class="card-empty">{{ emptyText(CONTRACT_TYPE) }}</div>
+          </article>
+        </section>
+
+        <section class="dashboard-grid dashboard-grid--bottom">
+          <article class="dashboard-card">
+            <panel-title title="年龄分布" unit="单位:人" />
+            <div v-if="hasGroup(AGE)" ref="ageChart" class="chart"></div>
+            <div v-else class="card-empty">{{ emptyText(AGE) }}</div>
+          </article>
+
+          <article class="dashboard-card">
+            <panel-title title="司龄分析" unit="单位:人" />
+            <div v-if="hasGroup(TENURE)" ref="seniorityChart" class="chart"></div>
+            <div v-else class="card-empty">{{ emptyText(TENURE) }}</div>
+          </article>
+
+          <article class="dashboard-card">
+            <panel-title title="任职年限分析" unit="单位:人" />
+            <div v-if="hasGroup(POSITION_TENURE)" ref="tenureChart" class="chart"></div>
+            <div v-else class="card-empty">{{ emptyText(POSITION_TENURE) }}</div>
+          </article>
+        </section>
+
         <footer class="board-footer">
           <i></i>数据统计日:{{ statisticsDate || EMPTY_TEXT }}
         </footer>
@@ -591,9 +587,6 @@ export default {
         this.filters.organizationId,
       );
     },
-    monthLabel() {
-      return `${this.filters.year}年${this.filters.month}月`;
-    },
     organizationLabel() {
       const hit = this.organizationList.find(
         (item) => String(item.value) === String(this.filters.organizationId),
@@ -607,12 +600,6 @@ export default {
     statisticsDate() {
       return this.summary.report.statisticsDate || "";
     },
-    reportStatusText() {
-      return statusText(this.summary.report.dataStatus);
-    },
-    reportMessage() {
-      return this.summary.report.message || "";
-    },
     metricCards() {
       return METRIC_CARDS.map((config) => {
         const metric = pickMetric(this.summary.metrics, config);

+ 24 - 3
src/views/hrManagement/employeeArchive/components/EmployeeArchiveDetail.vue

@@ -19,7 +19,7 @@
         <div class="profile-title">
           <div class="title-line">
             <h2>{{ employee.name }}</h2>
-            <span class="status-dot" :class="employee.status">{{ statusLabel(employee.status) }}</span>
+            <span class="status-dot" :class="employee.status">{{ statusLabel(employee.statusCode) }}</span>
           </div>
           <p>{{ employee.employeeNo }} · {{ employee.position || '未设置岗位' }} · {{ employee.level || '未定级' }}</p>
           <div class="profile-meta">
@@ -80,7 +80,7 @@
                 <div><span>入职时间</span><strong>{{ employee.hireDate || '-' }}</strong></div>
                 <div><span>转正日期</span><strong>{{ employee.regularDate || '-' }}</strong></div>
                 <div><span>岗位</span><strong>{{ employee.position || '-' }}</strong></div>
-                <div><span>员工状态</span><strong>{{ statusLabel(employee.status) }}</strong></div>
+                <div><span>员工状态</span><strong>{{ statusLabel(employee.statusCode) }}</strong></div>
                 <div><span>岗位性质</span><strong>{{ employee.employmentTypeName || employee.employmentType || '-' }}</strong></div>
                 <div><span>岗位层级</span><strong>{{ positionLevelText }}</strong></div>
                 <div><span>关键工序</span><strong>{{ employee.keyProcess || '-' }}</strong></div>
@@ -354,6 +354,8 @@ import EmployeeArchiveRecordsDetail from './EmployeeArchiveRecordsDetail.vue';
 import dictMixins from '@/mixins/dictMixins';
 /* 跨模块跳转:按后端资源树反解真实路径,禁止硬编码(未注册路由会导致整页白屏) */
 import { resolveUsableMenuPath } from '@/utils/menu-path';
+/* 员工状态文案统一取 hr_user_status 字典(与员工档案增改、列表筛选同一份,不在本组件硬编码) */
+import { loadEmployeeStatusOptions, employeeStatusLabel } from '@/utils/employee-status-dict';
 
 export default {
   name: 'EmployeeArchiveDetail',
@@ -364,6 +366,8 @@ export default {
     return {
       activeTab: 'base',
       windowWidth: window.innerWidth,
+      /* 员工状态字典就绪标记:字典是异步来的,置 true 后强制重算状态文案 */
+      statusDictLoaded: false,
       workTypeLevelOptions: [
         { label: '初级', value: '1' },
         { label: '中级', value: '2' },
@@ -451,12 +455,29 @@ export default {
   },
   created() {
     this.requestDict('工种类型');
+    this.loadStatusDict();
   },
   mounted() { window.addEventListener('resize', this.handleResize); },
   beforeDestroy() { window.removeEventListener('resize', this.handleResize); },
   methods: {
     handleResize() { this.windowWidth = window.innerWidth; },
-    statusLabel(status) { return { active: '在职', probation: '试用期', inactive: '已离职', disabled: '待岗' }[status] || '未知'; },
+    /** 员工状态字典:文案统一由 hr_user_status 翻译,拉不到明确提示(不内置兜底文案) */
+    async loadStatusDict() {
+      try {
+        await loadEmployeeStatusOptions();
+        this.statusDictLoaded = true;
+      } catch (error) {
+        console.error('[员工档案] 员工状态字典加载失败:', error);
+        this.$message.error(error.message || '员工状态字典加载失败');
+      }
+    },
+    /** 状态文案:入参是后端状态码(employee.statusCode),字典里没有就显示「-」 */
+    statusLabel(statusCode) {
+      /* 读就绪标记:字典异步到达后模板才会重新求值 */
+      if (!this.statusDictLoaded) return '-';
+      if (statusCode == null || statusCode === '') return '-';
+      return employeeStatusLabel(statusCode) || '-';
+    },
     workTypeTypeLabel(row) {
       return this.getDictValue('工种类型', row.type) || row.type || '-';
     },

+ 13 - 11
src/views/hrManagement/employeeArchive/components/EmployeeArchiveForm.vue

@@ -90,7 +90,7 @@
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="所属机构" prop="groupId">
                     <el-select v-model="form.groupId" filterable class="full-width" placeholder="请选择法人组织"
-                      @change="handleLegalEntityChange">
+                      :disabled="isEdit" @change="handleLegalEntityChange">
                       <el-option v-for="item in legalEntities" :key="String(item.id)" :label="item.name"
                         :value="String(item.id)" />
                     </el-select>
@@ -99,7 +99,7 @@
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="所属工厂">
                     <el-select v-model="form.factoryId" filterable clearable class="full-width"
-                      :loading="factoryLoading" placeholder="请选择所属工厂" @change="handleFactoryChange">
+                      :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>
@@ -109,13 +109,13 @@
                       :key="`dept-cascader-${form.groupId || 'none'}-${deptCascaderKey}`" v-model="form.deptIds"
                       :options="deptOptions" :props="deptCascaderProps" filterable clearable :show-all-levels="false"
                       :append-to-body="true" popper-class="employee-archive-dept-cascader" ref="deptCascader"
-                      class="full-width dept-multi-cascader" :disabled="!form.groupId"
+                      class="full-width dept-multi-cascader" :disabled="isEdit || !form.groupId"
                       :placeholder="deptCascaderPlaceholder"
                       @change="handleOrganizationChange" /></el-form-item></el-col>
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="岗位序列" prop="positionSequenceId">
                     <el-select v-model="form.positionSequenceId" filterable clearable class="full-width"
-                      :loading="sequenceLoading" :disabled="!hasDept" :placeholder="hasDept ? '请选择岗位序列' : '请先选择隶属部门'"
+                      :loading="sequenceLoading" :disabled="isEdit || !hasDept" :placeholder="hasDept ? '请选择岗位序列' : '请先选择隶属部门'"
                       @change="handlePositionSequenceChange">
                       <el-option v-for="item in sequenceOptions" :key="`seq-${String(item.id)}`" :label="item.name"
                         :value="String(item.id)" />
@@ -125,7 +125,7 @@
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="岗位" prop="positionId">
                     <el-select :key="`position-select-${positionSelectKey}`" v-model="form.positionId" filterable
-                      clearable class="full-width" :loading="positionLoading" :disabled="!hasPositionSequence"
+                      clearable class="full-width" :loading="positionLoading" :disabled="isEdit || !hasPositionSequence"
                       :placeholder="hasPositionSequence ? '请选择岗位' : '请先选择岗位序列'" @change="handlePositionChange">
                       <el-option v-for="item in positionOptions" :key="`pos-${String(item.id)}`"
                         :label="positionOptionLabel(item)" :value="String(item.id)" />
@@ -134,21 +134,21 @@
                 </el-col>
                 <el-col :xs="24" :sm="12"><el-form-item label="岗位性质" prop="employmentType"><el-select
                       v-model="form.employmentType" filterable
-                      clearable class="full-width" :placeholder="form.positionId ? '请选择岗位性质' : '请先选择岗位'"
+                      clearable class="full-width" :disabled="isEdit" :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-col>
                 <el-col :xs="24" :sm="12"><el-form-item label="岗位层级" prop="staffingLevel"><el-select
                       v-model="form.staffingLevel"
-                      class="full-width" :disabled="!staffingLevelOptions.length"
+                      class="full-width" :disabled="isEdit || !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></el-col>
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="直属上级">
                     <el-select v-model="form.managerId" filterable remote clearable class="full-width"
-                      :loading="managerLoading" :remote-method="searchManagers" placeholder="输入姓名/工号搜索全员"
+                      :loading="managerLoading" :disabled="isEdit" :remote-method="searchManagers" placeholder="输入姓名/工号搜索全员"
                       @change="handleManagerChange" @focus="ensureManagerOptions">
                       <el-option v-for="item in managerOptions" :key="item.id" :label="managerOptionLabel(item)"
                         :value="item.id" />
@@ -157,12 +157,12 @@
                 </el-col>
                 <el-col :xs="24" :sm="12"><el-form-item label="入职时间" prop="hireDate"><el-date-picker
                       v-model="form.hireDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择入职日期"
-                      class="full-width" /></el-form-item></el-col>
+                      class="full-width" :disabled="isEdit" /></el-form-item></el-col>
                 <el-col :xs="24" :sm="12"><el-form-item label="转正日期"><el-date-picker v-model="form.regularDate"
                       value-format="yyyy-MM-dd" type="date" placeholder="转正生效日期"
-                      class="full-width" /></el-form-item></el-col>
+                      class="full-width" :disabled="isEdit" /></el-form-item></el-col>
                 <el-col :xs="24" :sm="12"><el-form-item label="员工状态" prop="status"><el-select v-model="form.status"
-                      class="full-width"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label"
+                      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></el-col>
                 <el-col :xs="24" :sm="12"><el-form-item label="是否启用">
                     <div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{
@@ -801,7 +801,9 @@ export default {
           },
           trigger: 'change',
         }],
+        /* 员工状态:必填。required 只负责显示必填星号,实际校验走 validator(_skipFormValidate 时跳过) */
         status: [{
+          required: true,
           validator: (rule, value, callback) => {
             if (this._skipFormValidate) return callback();
             if (!value) callback(new Error('请选择员工状态'));

+ 111 - 60
src/views/hrManagement/employeeArchive/index.vue

@@ -19,17 +19,20 @@
             <div class="all-organization" :class="{ active: !selectedOrganizationId }" @click="selectAllOrganizations">
               <span><i class="el-icon-menu"></i>全部员工</span><em>{{ metricsTotal }}</em>
             </div>
-            <el-tree ref="organizationTree" :data="organizationTree" node-key="id" :props="treeProps"
-              :filter-node-method="filterTreeNode" :expand-on-click-node="false" :default-expand-all="true"
-              highlight-current class="organization-tree" @node-click="selectOrganization">
-              <span slot-scope="{ data }" class="tree-node-content">
-                <span class="tree-node-label">
-                  <i :class="isCompanyNode(data) ? 'el-icon-office-building' : 'el-icon-folder'" />
-                  <span>{{ data.name }}</span>
+            <!-- 树滚动区:绝对定位撑满剩余空间,树内容再长也不会把面板撑高(面板高度跟右侧表格走) -->
+            <div class="tree-scroll">
+              <el-tree ref="organizationTree" :data="organizationTree" node-key="id" :props="treeProps"
+                :filter-node-method="filterTreeNode" :expand-on-click-node="false" :default-expand-all="true"
+                highlight-current class="organization-tree" @node-click="selectOrganization">
+                <span slot-scope="{ data }" class="tree-node-content">
+                  <span class="tree-node-label">
+                    <i :class="isCompanyNode(data) ? 'el-icon-office-building' : 'el-icon-folder'" />
+                    <span>{{ data.name }}</span>
+                  </span>
+                  <em>{{ data.userCount == null ? '—' : data.userCount }}</em>
                 </span>
-                <em>{{ data.userCount == null ? '—' : data.userCount }}</em>
-              </span>
-            </el-tree>
+              </el-tree>
+            </div>
           </template>
           <button v-else type="button" class="collapsed-tree-button" @click="treeCollapsed = false">
             <i class="el-icon-office-building"></i><span>组织</span>
@@ -81,23 +84,27 @@
             </el-row>
           </el-form>
           <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
-            :init-load="false" :page-size="20" :cache-key="$tableCacheKey(cacheKeyUrl)" stripe class="employee-table"
-            @selection-change="selectedRows = $event">
+            :init-load="false" :page-size="20" :height="520" :cache-key="$tableCacheKey(cacheKeyUrl)" stripe
+            class="employee-table" @selection-change="selectedRows = $event">
             <template v-slot:toolbar>
-              <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+              <el-button v-if="$hasPermission('main:hrEmployee:save')" type="primary" icon="el-icon-plus"
+                class="ele-btn-icon" @click="openCreate">
                 新增
               </el-button>
-              <el-dropdown trigger="click" @command="handleImportCommand">
+              <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">
                   导入<i class="el-icon-arrow-down el-icon--right"></i>
                 </el-button>
                 <el-dropdown-menu slot="dropdown">
-                  <el-dropdown-item command="template" icon="el-icon-document">下载导入模板</el-dropdown-item>
-                  <el-dropdown-item command="upload" icon="el-icon-upload2">上传 Excel 导入</el-dropdown-item>
+                  <el-dropdown-item v-if="$hasPermission('main:hrEmployee:importTemplate')" command="template"
+                    icon="el-icon-document">下载导入模板</el-dropdown-item>
+                  <el-dropdown-item v-if="$hasPermission('main:hrEmployee:import')" command="upload"
+                    icon="el-icon-upload2">上传 Excel 导入</el-dropdown-item>
                 </el-dropdown-menu>
               </el-dropdown>
               <input ref="importInput" type="file" accept=".xlsx,.xls" style="display: none" @change="onImportFile" />
-              <el-dropdown trigger="click" @command="handleExport">
+              <el-dropdown v-if="$hasPermission('main:hrEmployee:export')" trigger="click" @command="handleExport">
                 <el-button icon="el-icon-download" class="ele-btn-icon">
                   导出<i class="el-icon-arrow-down el-icon--right"></i>
                 </el-button>
@@ -117,25 +124,10 @@
                   <img v-if="row.avatar" :src="row.avatar" :alt="row.name" />
                   <span v-else>{{ (row.name || '-').slice(-1) }}</span>
                 </span>
-                <span class="employee-cell-main">
-                  <!-- 员工姓名:与其它列表页一致的蓝色可点击样式 -->
-                  <el-link type="primary" :underline="false" @click.stop="openDetail(row)">
-                    {{ row.name || '-' }}
-                  </el-link>
-                  <small>{{ row.employeeNo }}</small>
-                </span>
-              </div>
-            </template>
-            <template v-slot:dept="{ row }">
-              <div class="department-cell">
-                <span>{{ row.deptName }}</span>
-                <!-- <small>{{ row.company }}</small> -->
-              </div>
-            </template>
-            <template v-slot:position="{ row }">
-              <div class="position-cell">
-                <span>{{ row.position }}</span>
-                <el-tag v-if="row.level" size="mini" type="info">{{ row.level }}</el-tag>
+                <!-- 姓名:可点击进详情;工号已独立成列,这里不再重复展示 -->
+                <el-link type="primary" :underline="false" @click.stop="openDetail(row)">
+                  {{ row.name || '-' }}
+                </el-link>
               </div>
             </template>
             <template v-slot:status="{ row }">
@@ -145,9 +137,14 @@
               </el-tag>
               <span v-else>-</span>
             </template>
+            <template v-slot:enabled="{ row }">
+              <!-- 是否启用:接口只给 isEnabled(0/1),文案由前端渲染成 启用/停用(暂不加后端文案字段) -->
+              <span>{{ row.enabled ? '启用' : '停用' }}</span>
+            </template>
             <template v-slot:action="{ row }">
               <!-- 员工列已可点开详情,此处移除原「查看」入口 -->
-              <el-link type="primary" :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
+              <el-link v-if="$hasPermission('main:hrEmployee:update')" type="primary" :underline="false"
+                icon="el-icon-edit" @click.stop="openEdit(row)">
                 编辑
               </el-link>
             </template>
@@ -191,6 +188,7 @@ import {
 import { writeEmployeeTemplateXlsx, downloadEmployeeTemplate } from '@/utils/employee-template';
 /* 员工档案可见机构范围:只取登录缓存所属机构(sessionStorage.loginChangeGroupVOList) */
 import { getOrgOptions, getCurrentOrgId } from '@/utils/current-org';
+import { getCurrentUser } from '@/utils/token-util';
 /* 「状态」筛选项:与新增/编辑员工档案共用 hr_user_status 字典(value = 字典值 = 后端 status) */
 import { loadEmployeeStatusOptions } from '@/utils/employee-status-dict';
 /* 性别:接口口径 1 男 / 2 女(筛选项与新增/编辑同一套码,前端按码过滤) */
@@ -226,7 +224,7 @@ export default {
       defaultOrganizationId: '',
       filters: { keyword: '', status: '', gender: '', hireRange: [] },
       appliedFilters: { keyword: '', status: '', gender: '', hireRange: [] },
-      cacheKeyUrl: 'hr-pc-employee-archive-v2',
+      cacheKeyUrl: 'hr-pc-employee-archive-v3',
       selectedRows: [],
       formVisible: false,
       editingEmployee: null,
@@ -238,6 +236,7 @@ export default {
       employeeNumbersLoaded: false,
       /* 导出用字典(工厂 / 班组):人员行只有 factoryId / teamId,名称要查字典;导出时按需加载一次并缓存 */
       factoryById: {},
+      factoryDictLoaded: false,
       teamById: {},
       exportDictLoaded: false
     };
@@ -263,18 +262,28 @@ export default {
       const count = node?.userCount;
       return count == null || count === '' ? null : Number(count);
     },
+    /**
+     * 列结构对齐另一套系统的员工列表(2026-10-21 用户拍板):
+     * 姓名 / 所属机构 / 部门 / 工号 / 手机号 / 岗位 / 所属工厂 / 用户账号 /
+     * 性别 / 状态 / 是否启用 / 创建时间 / 操作。
+     * 列结构变了,cacheKeyUrl 要跟着 bump(v3),否则旧的列显隐缓存会串。
+     */
     columns() {
       return [
         { type: 'selection', width: 56, fixed: 'left', align: 'center', headerAlign: 'center' },
         { type: 'index', label: '序号', width: 60, fixed: 'left', align: 'center', headerAlign: 'center' },
-        { prop: 'name', label: '员工', minWidth: 170, fixed: 'left', slot: 'name' },
-        { prop: 'deptName', label: '所属部门', minWidth: 150, slot: 'dept' },
-        { prop: 'position', label: '岗位 / 职级', minWidth: 230, slot: 'position' },
-        { prop: 'hireDate', label: '入职日期', width: 118, align: 'center', sortable: 'custom' },
+        { prop: 'name', label: '姓名', minWidth: 120, fixed: 'left', slot: 'name' },
+        { prop: 'company', label: '所属机构', minWidth: 160, showOverflowTooltip: true },
+        { prop: 'deptNames', label: '部门', minWidth: 150, showOverflowTooltip: true },
+        { prop: 'employeeNo', label: '工号', width: 110, align: 'center' },
         { prop: 'phone', label: '手机号', width: 122, align: 'center' },
+        { prop: 'position', label: '岗位', minWidth: 140, showOverflowTooltip: true },
+        { prop: 'factoryName', label: '所属工厂', minWidth: 120, showOverflowTooltip: true },
+        { prop: 'account', label: '用户账号', width: 120, showOverflowTooltip: true },
         { prop: 'gender', label: '性别', width: 74, align: 'center' },
-        { prop: 'age', label: '年龄', width: 74, align: 'center', sortable: 'custom' },
-        { prop: 'status', label: '状态', width: 100, align: 'center', slot: 'status' },
+        { prop: 'statusName', label: '状态', width: 100, align: 'center', slot: 'status' },
+        { prop: 'enabled', label: '是否启用', width: 90, align: 'center', slot: 'enabled' },
+        { prop: 'createTime', label: '创建时间', width: 100, align: 'center' },
         { columnKey: 'action', label: '操作', width: 140, fixed: 'right', align: 'left', resizable: false, slot: 'action' }
       ];
     },
@@ -324,6 +333,8 @@ export default {
       this.loading = true;
       try {
         await this.loadOrganizations();
+        // 工厂字典:列表「所属工厂」列翻译 factoryId 用,就绪后再发列表请求(否则首屏该列全空)
+        await this.ensureFactoryDictionary();
         // 页面加载只发两个请求:组织列表(含 userCount)+ 当前这一页员工列表。
         // 「全部员工」人数、左树人数都取组织列表下发的 userCount,前端不再额外统计。
         // 表格 :init-load="false":只在这里发唯一一次列表请求(等 orgById 就绪,部门/公司名映射完整)
@@ -332,6 +343,26 @@ export default {
         this.loading = false;
       }
     },
+    /**
+     * 工厂字典(列表「所属工厂」列翻译 factoryId → 名称)。
+     * 加载失败明确报错,字典缺失时该列留空,不编造名称;下次刷新重试。
+     */
+    async ensureFactoryDictionary() {
+      if (this.factoryDictLoaded || this.exportDictLoaded) return;
+      try {
+        const data = await getFactoryAreaPage();
+        const list = Array.isArray(data) ? data : data?.list || [];
+        const map = {};
+        list.forEach((item) => {
+          if (item?.id == null) return;
+          map[String(item.id)] = item.name || '';
+        });
+        this.factoryById = map;
+        this.factoryDictLoaded = true;
+      } catch (error) {
+        this.$message.error(error?.message || '工厂字典加载失败,所属工厂列暂无数据');
+      }
+    },
     attachOrgMeta(employee) {
       const deptId = employee.deptId || '';
       const groupId = employee.groupId || employee.legalEntityId || '';
@@ -362,6 +393,15 @@ export default {
         groupId: groupId !== '' && groupId != null ? String(groupId) : null,
         deptId: deptId !== '' && deptId != null ? String(deptId) : '',
         deptName: employee.deptName || deptOrg?.name || '',
+        // 隶属部门(可多选):deptIds 逐个查组织字典翻译名称;接口没给 id 就留空,不编造
+        deptNames: (employee.deptIds || [])
+          .map((id) => this.orgById[String(id)]?.name)
+          .filter(Boolean)
+          .join('、'),
+        // 所属工厂:人员行只带 factoryId,名称按工厂字典翻译(与导出同源口径),查不到留空
+        factoryName: employee.factoryId != null
+          ? (this.factoryById[String(employee.factoryId)] ?? '')
+          : '',
         company: company || employee.company || '',
         legalEntity: company || employee.legalEntity || ''
       };
@@ -373,7 +413,11 @@ export default {
      */
     async loadOrganizations() {
       try {
-        const list = await listOrganizationsWithUserCount();
+        /* 按文档传 currentUserId(当前登录用户 id),让后端按当前用户口径返回组织列表 */
+        const currentUserId = getCurrentUser()?.id;
+        const list = await listOrganizationsWithUserCount(
+          currentUserId != null ? { currentUserId } : {},
+        );
         const records = Array.isArray(list) ? list : [];
         const map = {};
         records.forEach((item) => { map[String(item.id)] = item; });
@@ -758,8 +802,11 @@ export default {
       if (!file) return;
       try {
         this.loading = true;
-        await importUsers(file);
-        this.$message.success('导入成功');
+        // 导入走后端「按 HR 任职链路导入」接口,返回 message 时原样展示(如「成功 10 条,失败 2 条」),
+        // 不吞后端的统计/失败原因;没有 message 才用通用成功文案
+        const result = await importUsers(file);
+        const tip = typeof result === 'string' && result.trim() ? result.trim() : '导入成功';
+        this.$message.success(tip);
         await this.refreshOrganizations();
         this.$refs.table?.reload({ page: 1 });
       } catch (error) {
@@ -775,27 +822,31 @@ export default {
      */
     async ensureExportDictionaries() {
       if (this.exportDictLoaded) return;
-      const [factoryData, teamData] = await Promise.all([
-        getFactoryAreaPage().catch((error) => {
-          throw new Error(`工厂字典加载失败:${error?.message || '未知错误'}`);
-        }),
+      // 工厂字典可能已被列表页拉过(factoryDictLoaded),避免重复请求
+      const [factoryList, teamList] = await Promise.all([
+        this.factoryDictLoaded
+          ? Promise.resolve([])
+          : getFactoryAreaPage().catch((error) => {
+            throw new Error(`工厂字典加载失败:${error?.message || '未知错误'}`);
+          }),
         listTeams().catch((error) => {
           throw new Error(`班组字典加载失败:${error?.message || '未知错误'}`);
         })
       ]);
-      const factoryList = Array.isArray(factoryData) ? factoryData : factoryData?.list || [];
-      const teamList = Array.isArray(teamData) ? teamData : teamData?.list || [];
-      const factoryById = {};
-      factoryList.forEach((item) => {
-        if (item?.id == null) return;
-        factoryById[String(item.id)] = item.name || '';
-      });
+      if (!this.factoryDictLoaded) {
+        const factoryById = {};
+        (Array.isArray(factoryList) ? factoryList : factoryList?.list || []).forEach((item) => {
+          if (item?.id == null) return;
+          factoryById[String(item.id)] = item.name || '';
+        });
+        this.factoryById = factoryById;
+        this.factoryDictLoaded = true;
+      }
       const teamById = {};
-      teamList.forEach((item) => {
+      (Array.isArray(teamList) ? teamList : teamList?.list || []).forEach((item) => {
         if (item?.id == null) return;
         teamById[String(item.id)] = item.name || '';
       });
-      this.factoryById = factoryById;
       this.teamById = teamById;
       this.exportDictLoaded = true;
     },

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

@@ -38,7 +38,8 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey('hr-item-receipt')">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" :loading="creating" @click="openCreate">
+          <el-button v-if="$hasPermission('main:hrItemReceipt:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" :loading="creating" @click="openCreate">
             新建
           </el-button>
         </template>
@@ -69,19 +70,20 @@
         </template>
 
         <template v-slot:action="{ row }">
-          <el-link v-if="canEdit(row)" type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
+          <el-link v-if="canEdit(row) && $hasPermission('main:hrItemReceipt:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
-          <el-link v-if="canSubmit(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            @click="submitApproval(row)">
+          <el-link v-if="canSubmit(row) && $hasPermission('main:hrItemReceipt:submit')" type="primary"
+            :underline="false" icon="el-icon-s-promotion" @click="submitApproval(row)">
             提交
           </el-link>
           <el-link v-if="canViewApproval(row)" type="primary" :underline="false" icon="el-icon-share"
             @click="openApprovalRecord(row)">
             审批记录
           </el-link>
-          <el-link v-if="canDelete(row)" type="danger" :underline="false" icon="el-icon-delete"
-            :disabled="submitting" @click="confirmDelete(row)">
+          <el-link v-if="canDelete(row) && $hasPermission('main:hrItemReceipt:delete')" type="danger"
+            :underline="false" icon="el-icon-delete" :disabled="submitting" @click="confirmDelete(row)">
             删除
           </el-link>
         </template>
@@ -169,8 +171,8 @@
       </div>
       <span slot="footer">
         <el-button @click="detailVisible = false">关闭</el-button>
-        <el-button v-if="detailRow && canEdit(detailRow)" type="primary" icon="el-icon-edit-outline"
-          @click="editFromDetail">
+        <el-button v-if="detailRow && canEdit(detailRow) && $hasPermission('main:hrItemReceipt:update')"
+          type="primary" icon="el-icon-edit-outline" @click="editFromDetail">
           编辑
         </el-button>
       </span>

+ 10 - 9
src/views/hrManagement/onboardingHandling/index.vue

@@ -39,7 +39,8 @@
       <ele-pro-table ref="table" row-key="userId" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey('hr-onboarding-handling')" class="onboarding-table" @selection-change="selectedRows = $event">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('main:hrOnboarding:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新建
           </el-button>
         </template>
@@ -68,12 +69,12 @@
         </template>
         <!-- 操作栏:能否办理/删除一律由 onboardingProgress 枚举判断 -->
         <template v-slot:action="{ row }">
-          <el-link v-if="!isProgressCompleted(row)" type="primary" :underline="false"
-            icon="el-icon-edit" @click.stop="openEdit(row)">
+          <el-link v-if="!isProgressCompleted(row) && $hasPermission('main:hrOnboarding:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
             办理
           </el-link>
-          <el-link v-if="canDelete(row)" type="danger" :underline="false" icon="el-icon-delete"
-            @click.stop="deleteRecord(row)">
+          <el-link v-if="canDelete(row) && $hasPermission('main:hrOnboarding:delete')" type="danger"
+            :underline="false" icon="el-icon-delete" @click.stop="deleteRecord(row)">
             删除
           </el-link>
         </template>
@@ -364,16 +365,16 @@
           <template v-else-if="currentStep === 'contract'">
             <el-button v-if="contractAlreadySaved" type="primary" icon="el-icon-document"
               @click="goViewContract">去查看合同</el-button>
-            <el-button v-if="platformAccount && !contractAlreadySaved" type="primary"
-              @click="goSignContract">去签署合同</el-button>
+            <el-button v-if="platformAccount && !contractAlreadySaved && $hasPermission('main:hrOnboarding:sign')"
+              type="primary" @click="goSignContract">去签署合同</el-button>
           </template>
 
           <!-- assets 节点 -->
           <template v-else-if="currentStep === 'assets'">
             <el-button v-if="!receiptApproving && editing.assets !== '已领用'" :loading="receiptSaving"
               @click="saveReceipt">保存</el-button>
-            <el-button v-if="!receiptApproving && editing.assets !== '已领用'" type="primary" :loading="receiptSubmitting"
-              @click="submitReceipt">提交</el-button>
+            <el-button v-if="!receiptApproving && editing.assets !== '已领用' && $hasPermission('main:hrOnboarding:submit')"
+              type="primary" :loading="receiptSubmitting" @click="submitReceipt">提交</el-button>
             <el-button v-if="receiptApproving || editing.assets === '已领用'" type="primary" plain icon="el-icon-tickets"
               @click="openReceiptApprovalRecord">审批记录</el-button>
           </template>

+ 12 - 10
src/views/hrManagement/positionTransfer/index.vue

@@ -37,7 +37,8 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey(cacheKeyUrl)" class="transfer-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('main:hrPositionTransfer:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新建
           </el-button>
         </template>
@@ -85,16 +86,16 @@
           <span v-else>-</span>
         </template>
         <template v-slot:action="{ row }">
-          <el-link v-if="canEdit(row)" type="primary" :underline="false" icon="el-icon-edit"
-            @click.stop="openEdit(row)">
+          <el-link v-if="canEdit(row) && $hasPermission('main:hrPositionTransfer:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
             编辑
           </el-link>
-          <el-link v-if="canCancel(row)" type="warning" :underline="false" icon="el-icon-close"
-            :disabled="submitting" @click.stop="cancelRecord(row)">
+          <el-link v-if="canCancel(row) && $hasPermission('main:hrPositionTransfer:cancel')" type="warning"
+            :underline="false" icon="el-icon-close" :disabled="submitting" @click.stop="cancelRecord(row)">
             取消
           </el-link>
-          <el-link v-if="canLaunch(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            @click.stop="launchApproval(row)">
+          <el-link v-if="canLaunch(row) && $hasPermission('main:hrPositionTransfer:submit')" type="primary"
+            :underline="false" icon="el-icon-s-promotion" @click.stop="launchApproval(row)">
             提交
           </el-link>
           <el-link v-if="canShowApprovalRecord(row)" type="primary" :underline="false" icon="el-icon-tickets"
@@ -335,15 +336,16 @@
         <span><i class="el-icon-lock"></i> 异动单号由系统自动生成</span>
         <div v-if="formReadOnly">
           <el-button @click="formVisible = false">关闭</el-button>
-          <el-button v-if="canEdit(form)" type="primary" icon="el-icon-edit" @click="formMode = 'edit'">编辑</el-button>
+          <el-button v-if="canEdit(form) && $hasPermission('main:hrPositionTransfer:update')" type="primary"
+            icon="el-icon-edit" @click="formMode = 'edit'">编辑</el-button>
         </div>
         <div v-else>
           <el-button @click="closeForm()">取消</el-button>
           <el-button :loading="submitting" @click="saveForm(false)">保存</el-button>
           <!-- 提交口径与列表「提交」一致:已取消 / 已撤回等不可提交的单据不展示提交入口,
                避免「先保存成功、再被拦下不能提交」的双提示 -->
-          <el-button v-if="canSubmitForm" type="primary" icon="el-icon-position" :loading="submitting"
-            @click="saveForm(true)">提交</el-button>
+          <el-button v-if="canSubmitForm && $hasPermission('main:hrPositionTransfer:submit')" type="primary"
+            icon="el-icon-position" :loading="submitting" @click="saveForm(true)">提交</el-button>
         </div>
       </div>
     </el-drawer>

+ 12 - 10
src/views/hrManagement/regularizationHandling/index.vue

@@ -64,10 +64,12 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey(cacheKey)" class="regularization-table" @selection-change="selectedRows = $event">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate()">
+          <el-button v-if="$hasPermission('main:hrRegularization:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate()">
             新建
           </el-button>
-          <el-button icon="el-icon-download" class="ele-btn-icon" @click="exportRows">
+          <el-button v-if="$hasPermission('main:hrRegularization:export')" icon="el-icon-download"
+            class="ele-btn-icon" @click="exportRows">
             导出
           </el-button>
         </template>
@@ -99,20 +101,20 @@
           </el-tag>
         </template>
         <template v-slot:action="{ row }">
-          <el-link v-if="canEdit(row)" type="primary" :underline="false" icon="el-icon-edit"
-            @click.stop="openCreate(row)">
+          <el-link v-if="canEdit(row) && $hasPermission('main:hrRegularization:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click.stop="openCreate(row)">
             编辑
           </el-link>
-          <el-link v-if="canSubmit(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            @click.stop="launchApproval(row)">
+          <el-link v-if="canSubmit(row) && $hasPermission('main:hrRegularization:submit')" type="primary"
+            :underline="false" icon="el-icon-s-promotion" @click.stop="launchApproval(row)">
             提交
           </el-link>
           <el-link v-if="canViewApproval(row)" type="primary" :underline="false" icon="el-icon-connection"
             @click.stop="openApprovalRecord(row)">
             审批记录
           </el-link>
-          <el-link v-if="canCancel(row)" type="danger" :underline="false" icon="el-icon-close"
-            @click.stop="cancelRow(row)">
+          <el-link v-if="canCancel(row) && $hasPermission('main:hrRegularization:cancel')" type="danger"
+            :underline="false" icon="el-icon-close" @click.stop="cancelRow(row)">
             取消
           </el-link>
         </template>
@@ -292,8 +294,8 @@
         <div>
           <el-button :disabled="submitting" @click="drawerVisible = false">关闭</el-button>
           <el-button v-if="!readOnly" :loading="submitting" @click="saveDraft">保存</el-button>
-          <el-button v-if="!readOnly && !isLockedForSubmit" type="primary" :loading="submitting"
-            @click="submitApplication">提交审核</el-button>
+          <el-button v-if="!readOnly && !isLockedForSubmit && $hasPermission('main:hrRegularization:submit')"
+            type="primary" :loading="submitting" @click="submitApplication">提交审核</el-button>
         </div>
       </div>
     </el-drawer>

+ 21 - 15
src/views/hrManagement/resignationApplication/index.vue

@@ -38,10 +38,12 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey('hr-resignation-application')" class="resignation-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('main:hrResignation:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新建
           </el-button>
-          <el-button icon="el-icon-download" class="ele-btn-icon" @click="exportData">
+          <el-button v-if="$hasPermission('main:hrResignation:export')" icon="el-icon-download"
+            class="ele-btn-icon" @click="exportData">
             导出
           </el-button>
         </template>
@@ -86,27 +88,28 @@
           <span v-else>-</span>
         </template>
         <template v-slot:action="{ row }">
-          <el-link v-if="canEdit(row)" type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
+          <el-link v-if="canEdit(row) && $hasPermission('main:hrResignation:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
-          <el-link v-if="canLaunch(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            @click="launchApproval(row)">
+          <el-link v-if="canLaunch(row) && $hasPermission('main:hrResignation:submit')" type="primary"
+            :underline="false" icon="el-icon-s-promotion" @click="launchApproval(row)">
             提交
           </el-link>
           <el-link v-if="canShowApprovalRecord(row)" type="primary" :underline="false" icon="el-icon-tickets"
             @click="openApprovalRecord(row)">
             审批记录
           </el-link>
-          <el-link v-if="canGenerate(row)" type="primary" :underline="false" icon="el-icon-document-add"
-            @click="openGenerateHandover(row)">
+          <el-link v-if="canGenerate(row) && $hasPermission('main:hrResignation:handover')" type="primary"
+            :underline="false" icon="el-icon-document-add" @click="openGenerateHandover(row)">
             发起交接单
           </el-link>
-          <el-link v-if="canArchive(row)" type="primary" :underline="false" icon="el-icon-archive"
-            :disabled="submitting" @click="handleCommand('archive', row)">
+          <el-link v-if="canArchive(row) && $hasPermission('main:hrResignation:archive')" type="primary"
+            :underline="false" icon="el-icon-archive" :disabled="submitting" @click="handleCommand('archive', row)">
             归档
           </el-link>
-          <el-link v-if="canCancel(row)" type="danger" :underline="false" icon="el-icon-close"
-            :disabled="submitting" @click="handleCommand('cancel', row)">
+          <el-link v-if="canCancel(row) && $hasPermission('main:hrResignation:cancel')" type="danger"
+            :underline="false" icon="el-icon-close" :disabled="submitting" @click="handleCommand('cancel', row)">
             取消
           </el-link>
           <!-- <el-link v-if="canForce(row)" type="danger" :underline="false" icon="el-icon-warning-outline"
@@ -289,8 +292,11 @@
       <div class="drawer-footer">
         <div>
           <el-button :disabled="submitting" @click="dialogVisible = false">关闭</el-button>
-          <el-button v-if="!formReadOnly" :loading="submitting" @click="saveDraft">保存</el-button>
-          <el-button v-if="!formReadOnly" type="primary" :loading="submitting" @click="saveAndSubmit">提交</el-button>
+          <el-button v-if="!formReadOnly && (editing.id
+            ? $hasPermission('main:hrResignation:update')
+            : $hasPermission('main:hrResignation:save'))" :loading="submitting" @click="saveDraft">保存</el-button>
+          <el-button v-if="!formReadOnly && $hasPermission('main:hrResignation:submit')" type="primary"
+            :loading="submitting" @click="saveAndSubmit">提交</el-button>
         </div>
       </div>
     </el-drawer>
@@ -377,8 +383,8 @@
               <i :class="item.done ? 'el-icon-circle-check' : 'el-icon-time'"></i>
               <span>{{ item.categoryLabel || item.itemName }}</span>
               <small>{{ item.name || item.itemName }}{{ item.detail ? ` · ${item.detail}` : "" }}</small>
-              <el-button v-if="!item.done && item.id" type="text" size="mini" :loading="acting"
-                @click="completeItem(item)">完成</el-button>
+              <el-button v-if="!item.done && item.id && $hasPermission('main:hrResignation:completeItem')" type="text"
+                size="mini" :loading="acting" @click="completeItem(item)">完成</el-button>
             </div>
           </div>
         </div>

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

@@ -69,10 +69,12 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey('hr-resignation-handover')" class="resignation-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openGenerate">
+          <el-button v-if="$hasPermission('main:hrHandover:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openGenerate">
             新建
           </el-button>
-          <el-button icon="el-icon-download" class="ele-btn-icon" @click="exportData">
+          <el-button v-if="$hasPermission('main:hrHandover:export')" icon="el-icon-download"
+            class="ele-btn-icon" @click="exportData">
             导出
           </el-button>
         </template>
@@ -131,23 +133,24 @@
           <span v-else>-</span>
         </template>
         <template v-slot:action="{ row }">
-          <el-link v-if="canEdit(row)" type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
+          <el-link v-if="canEdit(row) && $hasPermission('main:hrHandover:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
-          <el-link v-if="canLaunch(row)" type="primary" :underline="false" icon="el-icon-s-promotion"
-            @click="launchApproval(row)">
+          <el-link v-if="canLaunch(row) && $hasPermission('main:hrHandover:submit')" type="primary"
+            :underline="false" icon="el-icon-s-promotion" @click="launchApproval(row)">
             提交
           </el-link>
           <el-link v-if="canShowApprovalRecord(row)" type="primary" :underline="false" icon="el-icon-tickets"
             @click="openApprovalRecord(row)">
             审批记录
           </el-link>
-          <el-link v-if="canDelete(row)" type="primary" :underline="false" icon="el-icon-delete"
-            :disabled="submitting" @click="removeRow(row)">
+          <el-link v-if="canDelete(row) && $hasPermission('main:hrHandover:delete')" type="primary"
+            :underline="false" icon="el-icon-delete" :disabled="submitting" @click="removeRow(row)">
             删除
           </el-link>
-          <el-link v-if="canArchive(row)" type="primary" :underline="false" icon="el-icon-archive"
-            :disabled="submitting" @click="handleCommand('archive', row)">
+          <el-link v-if="canArchive(row) && $hasPermission('main:hrHandover:archive')" type="primary"
+            :underline="false" icon="el-icon-archive" :disabled="submitting" @click="handleCommand('archive', row)">
             归档
           </el-link>
           <!-- <el-link v-if="canForce(row)" type="danger" :underline="false" icon="el-icon-warning-outline"
@@ -332,12 +335,12 @@
             </el-table-column>
             <el-table-column label="操作" width="118" align="center">
               <template slot-scope="scope">
-                <el-button v-if="canCompleteItem(scope.row)" type="text" size="mini" :loading="acting"
-                  @click="completeItem(scope.row)">
+                <el-button v-if="canCompleteItem(scope.row) && $hasPermission('main:hrHandover:complete')"
+                  type="text" size="mini" :loading="acting" @click="completeItem(scope.row)">
                   完成
                 </el-button>
-                <el-button v-if="canRollbackItem(scope.row)" type="text" size="mini" :loading="acting"
-                  @click="rollbackItem(scope.row)">
+                <el-button v-if="canRollbackItem(scope.row) && $hasPermission('main:hrHandover:rollback')"
+                  type="text" size="mini" :loading="acting" @click="rollbackItem(scope.row)">
                   回退
                 </el-button>
                 <span v-if="

+ 8 - 7
src/views/position/positionApplication/index.vue

@@ -38,7 +38,8 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey(cacheKeyUrl)" class="application-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('hr:hrPositionApply:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新增
           </el-button>
         </template>
@@ -70,16 +71,16 @@
             @click.stop="openApprovalRecord(row)">
             审批记录
           </el-link>
-          <el-link v-if="canLaunch(row)" type="primary" :underline="false" icon="el-icon-position"
-            @click.stop="launchApplication(row)">
+          <el-link v-if="canLaunch(row) && $hasPermission('hr:hrPositionApply:submit')" type="primary"
+            :underline="false" icon="el-icon-position" @click.stop="launchApplication(row)">
             提交
           </el-link>
-          <el-link v-if="canEdit(row)" type="primary" :underline="false" icon="el-icon-edit"
-            @click.stop="openEdit(row)">
+          <el-link v-if="canEdit(row) && $hasPermission('hr:hrPositionApply:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
             编辑
           </el-link>
-          <el-link v-if="canDelete(row)" type="danger" :underline="false" icon="el-icon-delete"
-            @click.stop="removeApplication(row)">
+          <el-link v-if="canDelete(row) && $hasPermission('hr:hrPositionApply:delete')" type="danger"
+            :underline="false" icon="el-icon-delete" @click.stop="removeApplication(row)">
             删除
           </el-link>
         </template>

+ 11 - 2
src/views/position/positionManagement/components/PositionEditDialog.vue

@@ -388,11 +388,20 @@
     <div class="drawer-footer">
       <div v-if="readonly">
         <el-button @click="closeDrawer()">关闭</el-button>
-        <el-button type="primary" @click="$emit('edit')">编辑</el-button>
+        <el-button
+          v-if="$hasPermission(isUpdate ? 'hr:hrPosition:update' : 'hr:hrPosition:save')"
+          type="primary"
+          @click="$emit('edit')"
+        >编辑</el-button>
       </div>
       <div v-else>
         <el-button @click="closeDrawer()">取消</el-button>
-        <el-button type="primary" :loading="saving" @click="save">
+        <el-button
+          v-if="$hasPermission(isUpdate ? 'hr:hrPosition:update' : 'hr:hrPosition:save')"
+          type="primary"
+          :loading="saving"
+          @click="save"
+        >
           {{ saving ? "正在保存" : "保存岗位" }}
         </el-button>
       </div>

+ 53 - 98
src/views/position/positionManagement/components/SkillMatrixPanel.vue

@@ -13,10 +13,10 @@
           </label>
           <label class="filter-item">
             <span>部门</span>
-            <el-select v-model="filters.deptId" clearable filterable :popper-append-to-body="false" placeholder="全部"
-              class="small-filter local-filter-select" @change="reloadMatrix">
-              <el-option v-for="item in matrixDepartments" :key="item.id" :label="item.name" :value="item.id" />
-            </el-select>
+            <!-- 部门有二级三级:树形级联(可选父级),不再用扁平下拉 -->
+            <el-cascader v-model="filters.deptId" :options="matrixDepartmentTree" :props="departmentCascaderProps"
+              clearable filterable popper-class="position-dept-cascader" placeholder="全部"
+              class="small-filter local-filter-select" @change="reloadMatrix" />
           </label>
           <label class="filter-item">
             <span>序列</span>
@@ -47,10 +47,12 @@
               <el-option v-for="item in skillLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
           </label>
-          <el-button icon="el-icon-data-line" :loading="analysisLoading" @click="runGapAnalysis">
+          <el-button v-if="$hasPermission('hr:hrPosition:analysis')" icon="el-icon-data-line"
+            :loading="analysisLoading" @click="runGapAnalysis">
             差距分析
           </el-button>
-          <el-button type="primary" icon="el-icon-download" :loading="exporting" @click="exportMatrix">
+          <el-button v-if="$hasPermission('hr:hrPosition:exportMatrix')" type="primary" icon="el-icon-download"
+            :loading="exporting" @click="exportMatrix">
             导出矩阵
           </el-button>
         </div>
@@ -100,24 +102,8 @@
       </p>
     </div>
 
-    <aside class="side-column">
-      <div class="side-card skill-library">
-        <div class="side-heading">
-          <h2>技能分类</h2>
-        </div>
-        <div v-if="skillTree.length" class="skill-tree-wrap">
-          <el-tree :key="'skill-tree-' + skillTree.length" :data="skillTree"
-            :props="{ label: 'name', children: 'children' }" node-key="key" default-expand-all
-            :expand-on-click-node="false" @node-click="onSkillTreeClick">
-            <span class="tree-node" slot-scope="{ data }" :class="'type-' + (data.nodeType || 'SKILL')">
-              <i :class="data.nodeType === 'CATEGORY' ? 'el-icon-folder-opened' : 'el-icon-document'"></i>
-              {{ data.name }}
-            </span>
-          </el-tree>
-        </div>
-        <div v-else class="side-empty">暂无数据</div>
-      </div>
-    </aside>
+    <!-- 技能分类树(右侧 side-column)2026-10-20 用户要求先隐藏,对应 skill-tree 接口也不再调用;
+         恢复时连同 loadSkillTree()/adaptSkillTree()/onSkillTreeClick() 一起还原 -->
 
     <el-dialog title="技能差距分析" :visible.sync="analysisVisible" width="760px" append-to-body>
       <div v-loading="analysisLoading" class="analysis-body">
@@ -211,7 +197,6 @@ import {
   exportSkillMatrix,
   getSkillCategoryTree,
   getSkillMatrixCellDetail,
-  getSkillMatrixSkillTree,
   matrixCellStatusLabel,
   querySkillMatrix,
   skillLevelClass,
@@ -233,6 +218,8 @@ export default {
   props: {
     legalEntities: { type: Array, default: () => [] },
     departments: { type: Array, default: () => [] },
+    /* 部门树(一级 → 二级 → 三级…,根为登录机构),由父组件按组织列表构建 */
+    departmentTree: { type: Array, default: () => [] },
     sequenceTree: { type: Array, default: () => [] },
   },
   data() {
@@ -252,10 +239,18 @@ export default {
         skillHeaders: [],
         positions: [],
       },
+      /* 部门级联:按 id 取值,允许只选父级部门 */
+      departmentCascaderProps: {
+        value: 'id',
+        label: 'name',
+        children: 'children',
+        emitPath: false,
+        checkStrictly: true,
+        expandTrigger: 'hover',
+      },
       displaySkills: [],
       displayPositions: [],
       displayRows: [],
-      skillTree: [],
       skillCategoryOptions: [],
       analysisVisible: false,
       analysisLoading: false,
@@ -279,6 +274,23 @@ export default {
           String(item.legalId) === String(this.filters.organizationId),
       );
     },
+    /**
+     * 部门树(按当前法人收窄):法人根节点命中就整棵保留,否则只留命中的子树。
+     * 二级三级部门都挂在父级下,供 el-cascader 展开。
+     */
+    matrixDepartmentTree() {
+      if (!this.filters.organizationId) return this.departmentTree;
+      const legalId = String(this.filters.organizationId);
+      const prune = (nodes) =>
+        nodes.reduce((acc, node) => {
+          const children = prune(node.children || []);
+          if (String(node.id) === legalId || children.length) {
+            acc.push({ ...node, ...(children.length ? { children } : {}) });
+          }
+          return acc;
+        }, []);
+      return prune(this.departmentTree || []);
+    },
     positionLevelOptions() {
       const seq = this.sequenceTree.find(
         (item) =>
@@ -317,7 +329,6 @@ export default {
       try {
         await Promise.all([
           this.loadMatrix(),
-          this.loadSkillTree(),
           this.loadCategories(),
         ]);
       } finally {
@@ -327,7 +338,7 @@ export default {
     async reloadMatrix() {
       this.loading = true;
       try {
-        await Promise.all([this.loadMatrix(), this.loadSkillTree()]);
+        await this.loadMatrix();
       } finally {
         this.loading = false;
       }
@@ -521,69 +532,6 @@ export default {
       this.filters.positionLevel = '';
       this.reloadMatrix();
     },
-    async loadSkillTree() {
-      try {
-        const tree = await getSkillMatrixSkillTree({
-          organizationId: this.filters.organizationId || undefined,
-          deptId: this.filters.deptId || undefined,
-        });
-        this.skillTree = this.adaptSkillTree(tree || []);
-        const categories = [];
-        this.skillTree.forEach((node) => {
-          if (node.nodeType === 'CATEGORY' && node.name) categories.push(node.name);
-        });
-        if (categories.length) {
-          this.skillCategoryOptions = [...new Set([
-            ...this.skillCategoryOptions,
-            ...categories,
-          ])].sort();
-        }
-      } catch (error) {
-        this.skillTree = [];
-      }
-    },
-    adaptSkillTree(list = [], prefix = 'n') {
-      return (Array.isArray(list) ? list : []).map((item, index) => {
-        const key = `${prefix}-${item.nodeType || 'N'}-${item.skillId || item.name || index}`;
-        return {
-          ...item,
-          key,
-          name: item.name,
-          children: this.adaptSkillTree(item.children || [], key),
-        };
-      });
-    },
-    onSkillTreeClick(node) {
-      if (node.nodeType === 'CATEGORY') {
-        this.filters.skillCategory = node.name;
-        this.reloadMatrix();
-      } else if (node.nodeType === 'SKILL' && node.skillId) {
-        this.filters.skillCategory = '';
-        this.loading = true;
-        querySkillMatrix({
-          ...this.queryPayload(),
-          skillId: node.skillId,
-        })
-          .then((data) => {
-            const positions = data?.positions || [];
-            this.matrix = {
-              skillHeaders: this.deriveSkillHeaders(positions, data?.skillHeaders || []),
-              positions,
-            };
-            this.positionHasNext = Boolean(data?.positionHasNext);
-            this.buildCellIndex();
-            this.applyClientFilters();
-          })
-          .catch((error) => {
-            if (!error.__toastShown) {
-              this.$message.error(error.message || '筛选失败');
-            }
-          })
-          .finally(() => {
-            this.loading = false;
-          });
-      }
-    },
     async runGapAnalysis() {
       this.analysisVisible = true;
       this.analysisLoading = true;
@@ -678,7 +626,8 @@ export default {
 <style scoped lang="scss">
 .skill-workbench {
   display: grid;
-  grid-template-columns: minmax(0, 1fr) 300px;
+  /* 右侧技能分类树已隐藏,去掉 300px 右栏占位,看板铺满整行 */
+  grid-template-columns: minmax(0, 1fr);
   gap: 16px;
   align-items: start;
 }
@@ -696,12 +645,7 @@ export default {
   min-width: 0;
 }
 
-.side-column {
-  display: flex;
-  flex-direction: column;
-  gap: 14px;
-  min-width: 0;
-}
+/* side-column / side-card 样式保留:右侧技能分类树恢复时直接可用(见模板 105 行注释) */
 
 .side-card {
   padding: 16px 14px 14px;
@@ -751,6 +695,17 @@ export default {
   width: 108px;
 }
 
+/* 部门树形级联:比普通下拉宽一些,保证三级部门名不被截断 */
+.matrix-filters ::v-deep .small-filter.el-cascader {
+  width: 132px;
+}
+
+.matrix-filters ::v-deep .el-cascader .el-input__inner {
+  padding-right: 22px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
 .local-filter-select {
   position: relative;
 }

+ 104 - 37
src/views/position/positionManagement/index.vue

@@ -72,9 +72,9 @@
               </el-select>
             </el-form-item>
             <el-form-item label="部门">
-              <el-select v-model="deptFilter" clearable filterable placeholder="请选择">
-                <el-option v-for="item in filterDepartments" :key="item.id" :label="item.name" :value="item.id" />
-              </el-select>
+              <!-- 树形:部门有二级三级,扁平下拉只剩一级 → 用级联树 + 可搜索 + 允许选父级 -->
+              <el-cascader v-model="deptFilter" :options="filterDepartmentTree" :props="departmentCascaderProps"
+                clearable filterable popper-class="position-dept-cascader" placeholder="请选择" style="width: 100%" />
             </el-form-item>
             <el-form-item label="状态" class="status-item">
               <el-select v-model="statusFilter" clearable placeholder="请选择">
@@ -95,7 +95,8 @@
             :page-size="20" :cache-key="$tableCacheKey(cacheKeyUrl)" class="position-table" @columns-change="handleColumnChange"
             @fullscreen-change="handleTableFullscreen" @row-click="selectPosition">
             <template v-slot:toolbar>
-              <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+              <el-button v-if="$hasPermission('hr:hrPosition:save')" type="primary" icon="el-icon-plus"
+                class="ele-btn-icon" @click="openCreate">
                 新增
               </el-button>
             </template>
@@ -129,13 +130,17 @@
             </template>
             <template v-slot:action="{ row }">
               <div class="table-row-actions" @click.stop>
-                <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">编辑</el-link>
+                <el-link v-if="$hasPermission('hr:hrPosition:update')" type="primary" :underline="false"
+                  icon="el-icon-edit" @click="openEdit(row)">编辑</el-link>
                 <el-link type="primary" :underline="false" icon="el-icon-time"
                   @click="openPositionHistory(row)">变更历史</el-link>
-                <el-link type="primary" :underline="false" :disabled="submitting"
+                <el-link v-if="Number(row.status) === 1
+                  ? $hasPermission('hr:hrPosition:updateStatus')
+                  : $hasPermission('hr:hrPosition:updateStatus')" type="primary" :underline="false" :disabled="submitting"
                   :icon="Number(row.status) === 1 ? 'el-icon-video-pause' : 'el-icon-video-play'"
                   @click="togglePositionStatus(row)">{{ Number(row.status) === 1 ? "停用" : "启用" }}</el-link>
-                <el-link type="danger" :underline="false" :disabled="submitting" icon="el-icon-delete"
+                <el-link v-if="$hasPermission('hr:hrPosition:delete')" type="danger" :underline="false"
+                  :disabled="submitting" icon="el-icon-delete"
                   @click="removePosition(row.id || row.positionId, row)">删除</el-link>
               </div>
             </template>
@@ -155,7 +160,8 @@
               <span class="panel-hint">左侧为岗位序列,右侧为该序列下的启用层级,自左向右按层级级别逐级晋升</span>
             </div>
             <div class="canvas-tools">
-              <el-button icon="el-icon-download" @click="exportChannels">导出通道</el-button>
+              <el-button v-if="$hasPermission('hr:hrPosition:exportChannel')" icon="el-icon-download"
+                @click="exportChannels">导出通道</el-button>
             </div>
           </div>
           <div class="ladder-board">
@@ -179,7 +185,7 @@
       </section>
 
       <skill-matrix-panel v-else-if="activeView === 'skill'" :legal-entities="legalEntities" :departments="departments"
-        :sequence-tree="sequenceTree" />
+        :department-tree="departmentTree" :sequence-tree="sequenceTree" />
     </el-card>
 
     <position-edit-dialog :visible.sync="editVisible" :data="editData" :readonly="detailReadonly"
@@ -256,6 +262,15 @@ export default {
       /* 所属机构筛选:默认带出登录机构列表第一个(左侧序列与岗位列表共用) */
       legalFilter: getCurrentOrgId() || "",
       deptFilter: "",
+      /* 部门级联:按 id 取值(emitPath: false 直接拿部门 id);checkStrictly 允许只选父级部门 */
+      departmentCascaderProps: {
+        value: "id",
+        label: "name",
+        children: "children",
+        emitPath: false,
+        checkStrictly: true,
+        expandTrigger: "hover",
+      },
       statusFilter: "",
       /* 更换 cacheKey 使旧的本体列顺序缓存(含拖拽/显隐记录)失效,让代码列顺序生效 */
       cacheKeyUrl: "fc7bccc0-hr-pc-position-management-v3",
@@ -266,8 +281,10 @@ export default {
       lastSyncTime: "--:--",
       sequenceTree: [],
       legalEntities: [],
-      /* 机构 id → 组织机构(列表/详情展示机构名与编码) */
+      /* 部门 id → 组织机构(列表/详情展示机构名与编码) */
       orgMap: {},
+      /* 部门树(一级 → 二级 → 三级…,根为登录机构),部门筛选下拉用 */
+      departmentTree: [],
       departments: [],
       historyVisible: false,
       historyPositions: [],
@@ -492,6 +509,23 @@ export default {
           String(item.legalId) === String(this.legalFilter),
       );
     },
+    /**
+     * 部门树(按当前机构收窄):机构根节点命中就整棵保留,否则只留命中的子树。
+     * 一级部门下挂二级、三级部门,供 el-cascader 用。
+     */
+    filterDepartmentTree() {
+      if (!this.legalFilter) return this.departmentTree;
+      const legalId = String(this.legalFilter);
+      const prune = (nodes) =>
+        nodes.reduce((acc, node) => {
+          const children = prune(node.children || []);
+          /* 机构根节点本身按 id 命中;部门节点只要子树保留下来就保留 */
+          const hit = String(node.id) === legalId || children.length > 0;
+          if (hit) acc.push({ ...node, ...(children.length ? { children } : {}) });
+          return acc;
+        }, []);
+      return prune(this.departmentTree || []);
+    },
   },
   created() {
     this.loadData();
@@ -664,33 +698,61 @@ export default {
       this.orgMap = Object.fromEntries(
         records.map((item) => [String(item.id), item]),
       );
-      this.departments = records.map((item) => {
-        let current = item;
-        while (parents.has(String(current.parentId)))
-          current = parents.get(String(current.parentId));
-        return {
-          id: item.id,
-          name: item.name,
-          code: item.groupCode || item.code || "",
-          legalId: current.id,
-        };
+      /* 机构 id 集合:以登录缓存 loginChangeGroupVOList(legalEntities)为准;
+         缓存缺失时退化为组织列表的顶层节点(无父节点的就是登录机构根)。
+         ★ 组织列表是「机构 + 部门」混在一起的扁平全量,部门下拉必须把机构节点剔掉,
+         否则技能矩阵 / 岗位列表的「部门」筛选里会混入法人机构名(2026-10-20 用户反馈)。 */
+      const legalIdSet = new Set(
+        (this.legalEntities.length
+          ? this.legalEntities
+          : records.filter((item) => !parents.has(String(item.parentId)))
+        ).map((item) => String(item.id)),
+      );
+      this.departments = records
+        .filter((item) => !legalIdSet.has(String(item.id)))
+        .map((item) => {
+          let current = item;
+          while (parents.has(String(current.parentId)))
+            current = parents.get(String(current.parentId));
+          return {
+            id: item.id,
+            name: item.name,
+            code: item.groupCode || item.code || "",
+            legalId: current.id,
+          };
+        });
+      /* 部门树(保留 一级 → 二级 → 三级…):机构节点当根,直接展开它的下级部门;
+         部门下拉一律用树,扁平的 departments 只用于按 id 反查名称。 */
+      const legalById = new Map(this.legalEntities.map((item) => [String(item.id), item]));
+      const childrenOf = new Map();
+      records.forEach((item) => {
+        const key = String(item.parentId);
+        if (!childrenOf.has(key)) childrenOf.set(key, []);
+        childrenOf.get(key).push(item);
+      });
+      const buildDeptTree = (parentKey) =>
+        (childrenOf.get(parentKey) || [])
+          .filter((item) => !legalIdSet.has(String(item.id)))
+          .map((item) => {
+            const children = buildDeptTree(String(item.id));
+            const node = { id: item.id, name: item.name || "" };
+            if (children.length) node.children = children;
+            return node;
+          });
+      const roots = this.legalEntities.length
+        ? this.legalEntities.map((item) => String(item.id))
+        : records
+            .filter((item) => !parents.has(String(item.parentId)))
+            .map((item) => String(item.id));
+      this.departmentTree = roots.flatMap((rootId) => {
+        const children = buildDeptTree(rootId);
+        if (!children.length) return [];
+        /* 登录机构当一级根节点,下面挂一级部门 → 二级部门 → 三级部门… */
+        const rootName = legalById.get(rootId)?.name || parents.get(rootId)?.name || "";
+        return legalById.has(rootId) && rootName
+          ? [{ id: rootId, name: rootName, children }]
+          : children;
       });
-      if (!this.legalEntities.length) {
-        this.legalEntities = [
-          ...new Map(
-            this.localPositions
-              .filter((item) => item.legalId)
-              .map((item) => [
-                String(item.legalId),
-                {
-                  id: item.legalId,
-                  name: item.legalName || `组织${item.legalId}`,
-                  code: "ZY",
-                },
-              ]),
-          ).values(),
-        ];
-      }
       if (!this.departments.length) {
         this.departments = [
           ...new Map(
@@ -704,8 +766,13 @@ export default {
                   legalId: item.legalId,
                 },
               ]),
-          ).values(),
+            ).values(),
         ];
+        /* 兜底分支没有层级信息,树 = 一层平铺,保证下拉仍可用 */
+        this.departmentTree = this.departments.map((item) => ({
+          id: item.id,
+          name: item.name,
+        }));
       }
     },
     buildSequenceTree(configuredSequences) {

+ 6 - 3
src/views/position/positionSequence/index.vue

@@ -39,7 +39,8 @@
       <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="filteredSequences" :need-page="true"
         :page-size="20" :cache-key="$tableCacheKey(cacheKeyUrl)" class="sequence-table">
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('hr:hrPositionSequence:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新增
           </el-button>
         </template>
@@ -75,10 +76,12 @@
           </el-tag>
         </template>
         <template v-slot:action="{ row }">
-          <el-link type="primary" :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
+          <el-link v-if="$hasPermission('hr:hrPositionSequence:update')" type="primary" :underline="false"
+            icon="el-icon-edit" @click.stop="openEdit(row)">
             编辑
           </el-link>
-          <el-link type="primary" :underline="false" icon="el-icon-s-grid" @click.stop="openLevelManage(row)">
+          <el-link v-if="$hasPermission('hr:hrPositionSequence:update')" type="primary" :underline="false"
+            icon="el-icon-s-grid" @click.stop="openLevelManage(row)">
             维护层级
           </el-link>
         </template>

+ 1 - 1
src/views/salary/appeal/index.vue

@@ -86,7 +86,7 @@
 
         <template v-slot:action="{ row }">
           <el-link
-            v-if="canHandle(row)"
+            v-if="canHandle(row) && $hasPermission('hr:payrollPayslips:correct')"
             type="primary"
             :underline="false"
             icon="el-icon-s-check"

+ 10 - 1
src/views/salary/components/user-edit.vue

@@ -408,7 +408,7 @@
 
     <template v-slot:footer>
       <el-button @click="updateVisible(false)">{{ isView ? '关闭' : '取消' }}</el-button>
-      <el-button v-if="!isView" type="primary" :loading="saving"
+      <el-button v-if="!isView && canSaveTemplate" type="primary" :loading="saving"
         :disabled="templateSaveMode === 'blocked' || detailFailed || (detailConfigMissing && !emptyConfigConfirmed)"
         @click="handleSave">{{ templateSaveMode === 'change' ? '变更并保存' : '保存' }}</el-button>
     </template>
@@ -720,6 +720,15 @@ export default {
       if (key === 'DRAFT') return 'update';
       return 'blocked';
     },
+    /** 底部「保存 / 变更并保存」按保存模式取权限码 */
+    canSaveTemplate() {
+      const codeMap = {
+        create: 'hr:payrollTemplate:create',
+        update: 'hr:payrollTemplate:edit',
+        change: 'hr:hrSalaryTemplate:change',
+      };
+      return this.$hasPermission(codeMap[this.templateSaveMode] || 'hr:payrollTemplate:edit');
+    },
     dialogTitle() {
       if (this.isView) return '薪资模板详情';
       if (this.templateSaveMode === 'create') return '新增薪资模板';

+ 4 - 2
src/views/salary/employeeArchive/index.vue

@@ -137,7 +137,8 @@
 
         <template v-slot:action="{ row }">
           <el-link type="primary" :underline="false" icon="el-icon-time" @click="openProfile(row)">档案版本</el-link>
-          <el-link class="ele-action" type="primary" :underline="false" icon="el-icon-plus" @click="openCreate(row)">
+          <el-link v-if="$hasPermission('hr:hrSalaryProfile:adjust')" class="ele-action" type="primary"
+            :underline="false" icon="el-icon-plus" @click="openCreate(row)">
             调薪
           </el-link>
         </template>
@@ -159,7 +160,8 @@
         </div>
 
         <div class="drawer-actions">
-          <el-button type="primary" size="mini" icon="el-icon-plus" @click="openCreate(drawerEmployee)">
+          <el-button v-if="$hasPermission('hr:payrollEmployeeSalary:save')" type="primary" size="mini"
+            icon="el-icon-plus" @click="openCreate(drawerEmployee)">
             新增调薪版本
           </el-button>
         </div>

+ 12 - 5
src/views/salary/fieldSetting/index.vue

@@ -28,12 +28,14 @@
         :page-size="pageSize" @columns-change="handleColumnChange" :cache-key="$tableCacheKey(cacheKeyUrl)">
         <!-- 表头工具栏 -->
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openEdit()">
+          <el-button v-if="$hasPermission('hr:payrollItem:create')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openEdit()">
             新增
           </el-button>
           <!-- C17:批量删除接口已就绪,勾选后出现批量删除入口 -->
-          <el-button v-if="SALARY_ITEM_BATCH_DELETE_READY" type="danger" icon="el-icon-delete"
-            class="ele-btn-icon" :disabled="!selection.length" @click="removeSelected">
+          <el-button v-if="SALARY_ITEM_BATCH_DELETE_READY && $hasPermission('hr:payrollItem:delete')"
+            type="danger" icon="el-icon-delete" class="ele-btn-icon" :disabled="!selection.length"
+            @click="removeSelected">
             批量删除{{ selection.length ? `(${selection.length})` : '' }}
           </el-button>
         </template>
@@ -41,6 +43,9 @@
         <!-- 是否启用列 -->
         <template v-slot:enabled="{ row }">
           <el-switch v-model="row.enabled" :active-value="true" :inactive-value="false"
+            :disabled="!$hasPermission(row.enabled
+              ? 'hr:hrSalaryItem:disable'
+              : 'hr:hrSalaryItem:enable')"
             @change="(val) => handleToggleStatus(row, val)" />
         </template>
 
@@ -59,10 +64,12 @@
 
         <!-- 操作列 -->
         <template v-slot:action="{ row }">
-          <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
+          <el-link v-if="$hasPermission('hr:payrollItem:edit')" type="primary" :underline="false"
+            icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
-          <el-popconfirm class="ele-action" title="确定要删除当前薪资项目吗?删除后不可恢复" @confirm="remove(row)">
+          <el-popconfirm v-if="$hasPermission('hr:payrollItem:delete')" class="ele-action"
+            title="确定要删除当前薪资项目吗?删除后不可恢复" @confirm="remove(row)">
             <template v-slot:reference>
               <el-link type="danger" :underline="false" icon="el-icon-delete">
                 删除

+ 20 - 17
src/views/salary/salarySlip/index.vue

@@ -23,7 +23,8 @@
           </el-form>
         </div>
         <div class="calc-header-action">
-          <el-button type="primary" icon="el-icon-video-play" @click="openStartCalc">
+          <el-button v-if="$hasPermission('hr:payrollBatches:start')" type="primary" icon="el-icon-video-play"
+            @click="openStartCalc">
             启动薪资核算
           </el-button>
         </div>
@@ -42,17 +43,19 @@
           </span>
         </div>
         <div class="steps-head-right">
-          <el-button type="primary" icon="el-icon-video-play" size="small" :loading="calcActionRunning === 'calculate'"
+          <el-button v-if="$hasPermission('hr:payrollBatches:calculate')" type="primary" icon="el-icon-video-play"
+            size="small" :loading="calcActionRunning === 'calculate'"
             :disabled="!canCalculate || !!calcActionRunning" @click="runCalc('calculate')">
             执行核算
           </el-button>
-          <el-button type="warning" icon="el-icon-refresh-right" size="small" plain
-            :loading="calcActionRunning === 'recalculate'" :disabled="!canRecalculate || !!calcActionRunning"
-            @click="runCalc('recalculate')">
+          <el-button v-if="$hasPermission('hr:payrollBatches:recalculate')" type="warning"
+            icon="el-icon-refresh-right" size="small" plain :loading="calcActionRunning === 'recalculate'"
+            :disabled="!canRecalculate || !!calcActionRunning" @click="runCalc('recalculate')">
             重算
           </el-button>
-          <el-button v-if="canCancel" type="danger" icon="el-icon-circle-close" size="small" plain
-            :loading="cancelLoading" :disabled="!!calcActionRunning" @click="openCancel">
+          <el-button v-if="canCancel && $hasPermission('hr:hrPayroll:cancel')" type="danger"
+            icon="el-icon-circle-close" size="small" plain :loading="cancelLoading"
+            :disabled="!!calcActionRunning" @click="openCancel">
             取消批次
           </el-button>
         </div>
@@ -203,12 +206,12 @@
           </span>
         </div>
         <div class="table-header-right">
-          <el-button type="primary" icon="el-icon-s-promotion" plain :disabled="!canPublish"
-            :title="actionHint || '仅审批锁定批次可发布'" @click="handleBatchPublish">
+          <el-button v-if="$hasPermission('hr:payrollBatches:publishPayslips')" type="primary" icon="el-icon-s-promotion"
+            plain :disabled="!canPublish" :title="actionHint || '仅审批锁定批次可发布'" @click="handleBatchPublish">
             批量发布工资条
           </el-button>
-          <el-button type="primary" icon="el-icon-download" plain :disabled="!canExport"
-            :title="actionHint || '仅审批锁定批次可导出'" @click="openExport">
+          <el-button v-if="$hasPermission('hr:payrollBatches:export')" type="primary" icon="el-icon-download" plain
+            :disabled="!canExport" :title="actionHint || '仅审批锁定批次可导出'" @click="openExport">
             导出薪资表
           </el-button>
         </div>
@@ -262,12 +265,12 @@
         </template>
 
         <template v-slot:action="{ row }">
-          <el-link type="primary" :underline="false" icon="el-icon-view" :disabled="!row.resultId"
-            @click="openDetail(row)">
+          <el-link v-if="$hasPermission('hr:hrPayroll:viewAmount')" type="primary" :underline="false"
+            icon="el-icon-view" :disabled="!row.resultId" @click="openDetail(row)">
             查看金额
           </el-link>
-          <el-link class="ele-action" type="warning" :underline="false" icon="el-icon-edit-outline"
-            :disabled="!row.resultId" @click="openAdjust(row)">
+          <el-link v-if="$hasPermission('hr:payrollBatches:adjust')" class="ele-action" type="warning"
+            :underline="false" icon="el-icon-edit-outline" :disabled="!row.resultId" @click="openAdjust(row)">
             人工调整
           </el-link>
         </template>
@@ -359,8 +362,8 @@
       </div>
       <div slot="footer">
         <el-button @click="detailVisible = false">关闭</el-button>
-        <el-button type="warning" :disabled="!detailRow?.resultId || !(detailResult?.itemDetails || []).length"
-          @click="openCorrection">
+        <el-button v-if="$hasPermission('hr:payrollPayslips:correct')" type="warning"
+          :disabled="!detailRow?.resultId || !(detailResult?.itemDetails || []).length" @click="openCorrection">
           发起更正
         </el-button>
       </div>

+ 16 - 11
src/views/salary/salaryTemplate/index.vue

@@ -22,7 +22,8 @@
         :page-size="pageSize" @columns-change="handleColumnChange" :cache-key="$tableCacheKey(cacheKeyUrl)">
         <!-- 表头工具栏 -->
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
+          <el-button v-if="$hasPermission('hr:payrollTemplate:create')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openCreate">
             新增
           </el-button>
         </template>
@@ -73,28 +74,32 @@
           按钮随即显示为「编辑」。已启用不出现该入口。
         -->
         <template v-slot:action="{ row }">
-          <el-link v-if="canEdit(row) || canChange(row)" type="primary" :underline="false"
-            icon="el-icon-edit" @click="openEdit(row)" :title="editActionHint(row)">
+          <el-link
+            v-if="(canEdit(row) && $hasPermission('hr:payrollTemplate:edit'))
+              || (canChange(row) && $hasPermission('hr:hrSalaryTemplate:change'))"
+            type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)" :title="editActionHint(row)">
             {{ canChange(row) ? '变更' : '编辑' }}
           </el-link>
-          <el-link class="ele-action" type="primary" :underline="false" icon="el-icon-copy-document"
-            @click="openCopy(row)">
+          <el-link v-if="$hasPermission('hr:payrollTemplate:copy')" class="ele-action" type="primary"
+            :underline="false" icon="el-icon-copy-document" @click="openCopy(row)">
             复制
           </el-link>
           <el-link v-if="canViewVersion(row)" class="ele-action" type="info" :underline="false"
             icon="el-icon-time" @click="openVersionHistory(row)">
             历史版本
           </el-link>
-          <el-link v-if="canDisable(row)" class="ele-action" type="warning" :underline="false"
-            :disabled="submitting" icon="el-icon-remove-outline" @click="openDisable(row)">
+          <el-link v-if="canDisable(row) && $hasPermission('hr:payrollTemplate:disable')" class="ele-action"
+            type="warning" :underline="false" :disabled="submitting" icon="el-icon-remove-outline"
+            @click="openDisable(row)">
             停用
           </el-link>
-          <el-link v-if="canEnable(row)" class="ele-action" type="success" :underline="false"
-            :disabled="submitting" icon="el-icon-refresh-left" @click="openEnable(row)">
+          <el-link v-if="canEnable(row) && $hasPermission('hr:payrollTemplate:enable')" class="ele-action"
+            type="success" :underline="false" :disabled="submitting" icon="el-icon-refresh-left"
+            @click="openEnable(row)">
             启用
           </el-link>
-          <el-link v-if="canDelete(row)" class="ele-action" type="danger" :underline="false"
-            :disabled="submitting" icon="el-icon-delete" @click="openDelete(row)">
+          <el-link v-if="canDelete(row) && $hasPermission('hr:payrollTemplate:delete')" class="ele-action"
+            type="danger" :underline="false" :disabled="submitting" icon="el-icon-delete" @click="openDelete(row)">
             删除
           </el-link>
         </template>

+ 12 - 9
src/views/salary/socialScheme/index.vue

@@ -54,7 +54,8 @@
         class="scheme-table" @columns-change="handleColumnChange" :cache-key="$tableCacheKey(cacheKeyUrl)">
         <!-- 表头工具栏:新增按钮放表格左上角(与其他列表页一致) -->
         <template v-slot:toolbar>
-          <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openEdit()">
+          <el-button v-if="$hasPermission('hr:hrSocialScheme:save')" type="primary" icon="el-icon-plus"
+            class="ele-btn-icon" @click="openEdit()">
             新增
           </el-button>
         </template>
@@ -102,20 +103,22 @@
         <!-- 操作 -->
         <template v-slot:action="{ row }">
           <!-- 仅未启用(需要编辑)时显示「编辑」;已启用的查看改为点「方案编号」 -->
-          <el-link v-if="!isEnabledRow(row)" type="primary" :underline="false" icon="el-icon-edit"
-            @click="openEdit(row)">
+          <el-link v-if="!isEnabledRow(row) && $hasPermission('hr:hrSocialScheme:update')" type="primary"
+            :underline="false" icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
-          <el-link class="ele-action" type="primary" :underline="false" icon="el-icon-document-copy"
-            @click="openCopy(row)">
+          <el-link v-if="$hasPermission('hr:hrSocialScheme:copy')" class="ele-action" type="primary"
+            :underline="false" icon="el-icon-document-copy" @click="openCopy(row)">
             以此方案新建
           </el-link>
-          <el-link v-if="canEnable(row)" class="ele-action" type="success" :underline="false"
-            :disabled="submitting" icon="el-icon-circle-check" @click="handleToggle(row, true)">
+          <el-link v-if="canEnable(row) && $hasPermission('hr:hrSocialScheme:enable')" class="ele-action"
+            type="success" :underline="false" :disabled="submitting" icon="el-icon-circle-check"
+            @click="handleToggle(row, true)">
             启用
           </el-link>
-          <el-link v-if="canDisable(row)" class="ele-action" type="warning" :underline="false"
-            :disabled="submitting" icon="el-icon-remove-outline" @click="handleToggle(row, false)">
+          <el-link v-if="canDisable(row) && $hasPermission('hr:hrSocialScheme:disable')" class="ele-action"
+            type="warning" :underline="false" :disabled="submitting" icon="el-icon-remove-outline"
+            @click="handleToggle(row, false)">
             停用
           </el-link>
         </template>

+ 4 - 3
src/views/salary/statistics/index.vue

@@ -179,7 +179,8 @@
       <span class="panel__scan"></span>
       <div class="panel-header table-panel-header">
         <h2><em></em>工资条明细</h2>
-        <el-button type="primary" icon="el-icon-download" size="small" plain @click="openExport">受控导出</el-button>
+        <el-button v-if="$hasPermission('hr:hrPayrollStat:export')" type="primary" icon="el-icon-download"
+          size="small" plain @click="openExport">受控导出</el-button>
       </div>
       <div class="table-wrap">
         <ele-pro-table ref="table" :columns="columns" :datasource="datasource" row-key="id" :page-size="pageSize"
@@ -192,8 +193,8 @@
             }}</el-tag>
           </template>
           <template v-slot:action="{ row }">
-            <el-link type="primary" :underline="false" icon="el-icon-view" :disabled="!row.resultId"
-              @click="openDetail(row)">查看金额</el-link>
+            <el-link v-if="$hasPermission('hr:hrPayrollStat:viewAmount')" type="primary" :underline="false"
+              icon="el-icon-view" :disabled="!row.resultId" @click="openDetail(row)">查看金额</el-link>
           </template>
           <template slot="empty"><el-empty description="暂无工资条数据" /></template>
         </ele-pro-table>

+ 2 - 1
src/views/salary/taxSetting/index.vue

@@ -74,7 +74,8 @@
           </el-form-item>
 
           <el-form-item>
-            <el-button type="primary" size="small" :loading="policySaving" @click="savePolicy">
+            <el-button v-if="$hasPermission('hr:hrTaxSetting:save')" type="primary" size="small"
+              :loading="policySaving" @click="savePolicy">
               保存
             </el-button>
             <el-button size="small" @click="loadPolicy">重置</el-button>