diff --git a/COLUMN_VISIBILITY_SOLUTIONS.md b/COLUMN_VISIBILITY_SOLUTIONS.md new file mode 100644 index 0000000..f122150 --- /dev/null +++ b/COLUMN_VISIBILITY_SOLUTIONS.md @@ -0,0 +1,163 @@ +# 列显示/隐藏方案对比 + +## 方案1:TableColumn 包装组件 ⭐推荐 + +### 优点 +- ✅ 使用简单,只需替换组件名 +- ✅ 完全兼容 `el-table-column` 的所有属性和插槽 +- ✅ 代码清晰,易于维护 +- ✅ 无需修改现有代码结构 + +### 缺点 +- ❌ 需要创建一个新组件 +- ❌ 需要在使用的地方 provide `isColumnVisible` 函数 + +### 使用示例 + +```vue + + + + + + + + + + + + + + + + + + + + +``` + +--- + +## 方案2:自定义指令 v-column-visible + +### 优点 +- ✅ 可以保留 `el-table-column` +- ✅ 使用相对简单 + +### 缺点 +- ❌ 仍然需要在每个列上添加指令 +- ❌ 实现较复杂,需要操作 DOM +- ❌ 可能不够优雅 + +### 使用示例 + +```vue + + + + + +``` + +--- + +## 方案3:使用 computed 动态生成列配置 + +### 优点 +- ✅ 最彻底,完全控制列的渲染 +- ✅ 性能最好(只渲染可见的列) + +### 缺点 +- ❌ 需要重构现有代码,改动较大 +- ❌ 需要将列配置抽离出来 +- ❌ 插槽处理较复杂 + +### 使用示例 + +```vue + + + + + + + + + + + +``` + +--- + +## 方案4:在 el-table 层面使用 provide + 自动处理 + +### 优点 +- ✅ 在表格层面统一处理 +- ✅ 子组件自动继承 + +### 缺点 +- ❌ 实现最复杂 +- ❌ 需要修改 Element Plus 的渲染逻辑 +- ❌ 可能影响性能 + +--- + +## 推荐方案 + +**推荐使用方案1(TableColumn 包装组件)**,因为: +1. 使用最简单,只需替换组件名 +2. 完全兼容现有代码 +3. 易于维护和理解 +4. 性能良好 + +## 迁移步骤(方案1) + +1. 导入组件: +```vue +import TableColumnProvider from '/@/components/TableColumn/Provider.vue' +import TableColumn from '/@/components/TableColumn/index.vue' +``` + +2. 用 `TableColumnProvider` 包裹所有列,并传入 `isColumnVisible`: +```vue + + + +``` + +3. 将所有 `el-table-column` 替换为 `TableColumn`,并移除 `v-if`: +```vue + + + + + +``` + diff --git a/docs/按钮样式规范.md b/docs/按钮样式规范.md index 73c6207..882fb0f 100644 --- a/docs/按钮样式规范.md +++ b/docs/按钮样式规范.md @@ -100,6 +100,7 @@ | 设置/配置 | `Setting` | | 锁定/解锁 | `Lock` | | 用户相关 | `User` | +| 调动/转换 | `Switch`| ### 使用示例 diff --git a/package.json b/package.json index 9b34ace..936b248 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "vue-router": "4.1.6", "vue3-tree-org": "^4.2.2", "vue3-video-play": "1.3.1-beta.6", - "vuedraggable": "^4.1.0" + "vuedraggable": "^4.1.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@swc/core": "1.6.13", diff --git a/src/api/admin/dict.ts b/src/api/admin/dict.ts index 92aca23..2f5a531 100644 --- a/src/api/admin/dict.ts +++ b/src/api/admin/dict.ts @@ -18,6 +18,15 @@ export function getTypeValue(type: string | number) { }); } +// 批量获取字典类型值 +export function getDictsByTypes(types: string[]) { + return request({ + url: '/admin/dict/item/typeList', + method: 'post', + data: types, + }); +} + export function fetchList(query: any) { return request({ url: '/admin/dict/list', diff --git a/src/api/professional/professionalyearbounds.ts b/src/api/professional/salaries/professionalyearbounds.ts similarity index 100% rename from src/api/professional/professionalyearbounds.ts rename to src/api/professional/salaries/professionalyearbounds.ts diff --git a/src/api/professional/salaryexportrecord.ts b/src/api/professional/salaries/salaryexportrecord.ts similarity index 100% rename from src/api/professional/salaryexportrecord.ts rename to src/api/professional/salaries/salaryexportrecord.ts diff --git a/src/api/professional/teacherawardtax.ts b/src/api/professional/salaries/teacherawardtax.ts similarity index 100% rename from src/api/professional/teacherawardtax.ts rename to src/api/professional/salaries/teacherawardtax.ts diff --git a/src/api/professional/teacherpayslip.ts b/src/api/professional/salaries/teacherpayslip.ts similarity index 100% rename from src/api/professional/teacherpayslip.ts rename to src/api/professional/salaries/teacherpayslip.ts diff --git a/src/api/professional/teachersalary.ts b/src/api/professional/salaries/teachersalary.ts similarity index 100% rename from src/api/professional/teachersalary.ts rename to src/api/professional/salaries/teachersalary.ts diff --git a/src/api/professional/stayschool/outercompany.ts b/src/api/professional/stayschool/outercompany.ts new file mode 100644 index 0000000..499f397 --- /dev/null +++ b/src/api/professional/stayschool/outercompany.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018-2025, cyweb All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + */ + +import request from '/@/utils/request'; + +/** + * 获取列表 + * @param query + */ +export const fetchList = (query?: any) => { + return request({ + url: '/professional/outercompany/page', + method: 'get', + params: query, + }); +}; + +/** + * 新增 + * @param obj + */ +export const addObj = (obj: any) => { + return request({ + url: '/professional/outercompany', + method: 'post', + data: obj, + }); +}; + +/** + * 获取详情 + * @param id + */ +export const getObj = (id: string | number) => { + return request({ + url: `/professional/outercompany/${id}`, + method: 'get', + }); +}; + +/** + * 删除 + * @param id + */ +export const delObj = (id: string | number) => { + return request({ + url: `/professional/outercompany/${id}`, + method: 'delete', + }); +}; + +/** + * 更新 + * @param obj + */ +export const putObj = (obj: any) => { + return request({ + url: '/professional/outercompany', + method: 'put', + data: obj, + }); +}; + +/** + * 获取列表(不分页) + * @param query + */ +export const getList = (query?: any) => { + return request({ + url: '/professional/outercompany/getList', + method: 'get', + params: query, + }); +}; + diff --git a/src/api/professional/stayschool/outercompanyemployee.ts b/src/api/professional/stayschool/outercompanyemployee.ts new file mode 100644 index 0000000..42f3c9f --- /dev/null +++ b/src/api/professional/stayschool/outercompanyemployee.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018-2025, cyweb All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + */ + +import request from '/@/utils/request'; + +/** + * 获取列表 + * @param query + */ +export const fetchList = (query?: any) => { + return request({ + url: '/professional/outercompanyemployee/page', + method: 'get', + params: query, + }); +}; + +/** + * 新增 + * @param obj + */ +export const addObj = (obj: any) => { + return request({ + url: '/professional/outercompanyemployee', + method: 'post', + data: obj, + }); +}; + +/** + * 保存第二步 + * @param obj + */ +export const saveSecond = (obj: any) => { + return request({ + url: '/professional/outercompanyemployee/saveSecond', + method: 'post', + data: obj, + }); +}; + +/** + * 获取详情 + * @param id + */ +export const getObj = (id: string | number) => { + return request({ + url: `/professional/outercompanyemployee/${id}`, + method: 'get', + }); +}; + +/** + * 删除 + * @param id + */ +export const delObj = (id: string | number) => { + return request({ + url: `/professional/outercompanyemployee/${id}`, + method: 'delete', + }); +}; + +/** + * 更新 + * @param obj + */ +export const putObj = (obj: any) => { + return request({ + url: '/professional/outercompanyemployee', + method: 'put', + data: obj, + }); +}; + +/** + * 批量删除 + * @param obj + */ +export const batchDel = (obj: any) => { + return request({ + url: '/professional/outercompanyemployee/batchDel', + method: 'post', + data: obj, + }); +}; + +/** + * 重置密码 + * @param data + */ +export const resetPassWord = (data: any) => { + return request({ + url: '/professional/outercompanyemployee/resetPassWord', + method: 'post', + data: data, + }); +}; + +/** + * 远程模糊检索 + * @param params + */ +export const remoteInfo = (params?: any) => { + return request({ + url: '/professional/outercompanyemployee/remoteInfo', + method: 'get', + params: params, + }); +}; + diff --git a/src/components/GenderTag/index.vue b/src/components/GenderTag/index.vue new file mode 100644 index 0000000..e613c73 --- /dev/null +++ b/src/components/GenderTag/index.vue @@ -0,0 +1,114 @@ + + + + + + + + {{ label }} + + + + + + + + {{ label }} + + + + + + + diff --git a/src/components/StatusTag/README.md b/src/components/StatusTag/README.md new file mode 100644 index 0000000..8c4b0ff --- /dev/null +++ b/src/components/StatusTag/README.md @@ -0,0 +1,149 @@ +# StatusTag 组件 + +状态标签组件,用于显示状态值对应的标签文本,支持自定义样式和颜色。 + +## 功能特性 + +- ✅ 支持标签样式和纯文本样式两种显示模式 +- ✅ 支持自定义类型映射(typeMap)和颜色映射(colorMap) +- ✅ 内置默认样式('1' → warning/dark,'0' → primary/light) +- ✅ 外部传入优先,支持覆盖默认样式 + +## Props + +| 参数 | 说明 | 类型 | 默认值 | 必填 | +|------|------|------|--------|------| +| value | 当前状态值 | `string \| number` | `''` | 是 | +| options | 选项列表,格式:`[{label: '是', value: '1'}, {label: '否', value: '0'}]` | `Option[]` | `[]` | 是 | +| showTag | 是否显示标签样式(有边框和背景),`false` 为纯文本样式 | `boolean` | `true` | 否 | +| typeMap | 自定义类型映射,用于标签模式,如:`{'1': {type: 'warning', effect: 'dark'}}` | `Record` | `{}` | 否 | +| colorMap | 自定义颜色映射,用于纯文本模式,如:`{'1': '#E6A23C'}` | `Record` | `{}` | 否 | + +### Option 接口 + +```typescript +interface Option { + label: string // 显示文本 + value: string | number // 选项值 +} +``` + +## 默认样式 + +组件内置了默认的样式映射,无需传入 `typeMap` 或 `colorMap` 即可使用: + +- **值 '1' 或 1**: + - 标签模式:`warning` 类型 + `dark` 效果(橙色深色) + - 纯文本模式:`var(--el-color-warning)`(橙色) + +- **值 '0' 或 0**: + - 标签模式:`primary` 类型 + `light` 效果(蓝色浅色) + - 纯文本模式:`var(--el-color-primary)`(蓝色) + +- **其他值**: + - 标签模式:`info` 类型 + `light` 效果(灰色) + - 纯文本模式:默认文本颜色 + +## 使用示例 + +### 基础用法 + +```vue + + + + + +``` + +### 纯文本模式(无边框和背景) + +```vue + +``` + +### 自定义类型映射 + +```vue + +``` + +### 自定义颜色映射(纯文本模式) + +```vue + +``` + +### 完整示例 + +```vue + + + + + + + + + + + +``` + +## 注意事项 + +1. **必须传入 `options`**:组件不提供默认选项,必须通过 `options` prop 传入选项列表 +2. **值匹配**:组件会自动匹配字符串和数字类型的值(如 `'1'` 和 `1` 会被视为相同) +3. **样式优先级**:外部传入的 `typeMap` 和 `colorMap` 会覆盖默认样式 +4. **未匹配值**:如果 `value` 在 `options` 中找不到对应项,会显示 `'-'` + +## 样式说明 + +### 标签模式(showTag: true) + +使用 Element Plus 的 `el-tag` 组件,支持所有 `el-tag` 的类型和效果: +- `type`: `success` | `info` | `warning` | `danger` | `primary` +- `effect`: `dark` | `light` | `plain` + +### 纯文本模式(showTag: false) + +使用纯文本显示,通过 CSS 颜色控制样式,支持任何颜色值(CSS 变量、十六进制、RGB 等)。 + diff --git a/src/components/StatusTag/index.vue b/src/components/StatusTag/index.vue new file mode 100644 index 0000000..5916a7b --- /dev/null +++ b/src/components/StatusTag/index.vue @@ -0,0 +1,140 @@ + + + {{ label }} + + + {{ label }} + + + + + + + diff --git a/src/components/TableColumn/Provider.vue b/src/components/TableColumn/Provider.vue new file mode 100644 index 0000000..b981fbf --- /dev/null +++ b/src/components/TableColumn/Provider.vue @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/components/TableColumn/README.md b/src/components/TableColumn/README.md new file mode 100644 index 0000000..61d7ac8 --- /dev/null +++ b/src/components/TableColumn/README.md @@ -0,0 +1,76 @@ +# TableColumn 组件 + +一个自动处理列显示/隐藏的 `el-table-column` 包装组件。 + +## 功能 + +- 自动根据 `isColumnVisible` 函数控制列的显示/隐藏 +- 完全兼容 `el-table-column` 的所有属性和插槽 +- 无需在每个列上手动添加 `v-if="isColumnVisible('xxx')"` + +## 使用方法 + +### 1. 在父组件中提供 `isColumnVisible` 函数 + +```vue + + + + + + + + + + +``` + +### 2. 直接使用(如果已经在父组件中 provide) + +```vue + + + + + + + + +``` + +## Props + +继承 `el-table-column` 的所有属性,包括: +- `prop`: 列的字段名 +- `label`: 列的标题 +- `width`: 列宽度 +- `min-width`: 最小宽度 +- `fixed`: 是否固定 +- 等等... + +## Slots + +继承 `el-table-column` 的所有插槽,包括: +- `default`: 默认插槽 +- `header`: 表头插槽 +- 等等... + +## 注意事项 + +1. 需要在父组件中使用 `provide` 提供 `isColumnVisible` 函数,或者使用 `TableColumnProvider` 组件 +2. `isColumnVisible` 函数接收 `prop` 或 `label` 作为参数 +3. 如果既没有 `prop` 也没有 `label`,列将始终显示 + diff --git a/src/components/TableColumn/index.vue b/src/components/TableColumn/index.vue new file mode 100644 index 0000000..9da0796 --- /dev/null +++ b/src/components/TableColumn/index.vue @@ -0,0 +1,66 @@ + + + + + + + + + + diff --git a/src/components/TableColumnControl/README.md b/src/components/TableColumnControl/README.md new file mode 100644 index 0000000..1528d15 --- /dev/null +++ b/src/components/TableColumnControl/README.md @@ -0,0 +1,162 @@ +# TableColumnControl 表格列显隐控制组件 + +一个通用的表格列显示/隐藏控制组件,支持动态控制表格列的显示状态。 + +## 功能特性 + +- ✅ 动态控制表格列的显示/隐藏 +- ✅ 支持全选/全不选 +- ✅ 支持重置为默认值 +- ✅ 支持 localStorage 持久化 +- ✅ 支持固定列(不可隐藏) +- ✅ 支持始终显示的列 +- ✅ 可自定义触发按钮样式 + +## 使用方法 + +### 基础用法 + +```vue + + + + + + + + + + + + + +``` + +### 使用 localStorage 持久化 + +```vue + +``` + +### 固定列(不可隐藏) + +```vue +const tableColumns = [ + { prop: 'name', label: '姓名', fixed: 'left' }, // 固定左侧,不可隐藏 + { prop: 'age', label: '年龄' }, + { prop: 'action', label: '操作', fixed: 'right' } // 固定右侧,不可隐藏 +] +``` + +### 始终显示的列 + +```vue +const tableColumns = [ + { prop: 'name', label: '姓名', alwaysShow: true }, // 始终显示,不可隐藏 + { prop: 'age', label: '年龄' } +] +``` + +### 自定义触发按钮 + +```vue + + + + + + + + +``` + +## Props + +| 参数 | 说明 | 类型 | 默认值 | +|------|------|------|--------| +| columns | 表格列配置数组 | Column[] | 必填 | +| modelValue | 当前显示的列的 prop 或 label 数组 | string[] | - | +| storageKey | localStorage 存储的 key,用于持久化 | string | - | +| triggerType | 触发按钮的类型 | 'default' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info' \| 'text' | 'default' | +| triggerSize | 触发按钮的大小 | 'large' \| 'default' \| 'small' | 'default' | +| triggerCircle | 触发按钮是否为圆形 | boolean | false | +| triggerText | 触发按钮的文字 | string | '' | +| triggerLink | 触发按钮是否为链接样式 | boolean | false | + +## Events + +| 事件名 | 说明 | 回调参数 | +|--------|------|----------| +| update:modelValue | 显示的列变化时触发 | (columns: string[]) | +| change | 显示的列变化时触发 | (columns: string[]) | + +## Column 接口 + +```typescript +interface Column { + prop?: string // 列的 prop,用于标识列 + label: string // 列的标签 + fixed?: boolean | 'left' | 'right' // 固定列,不可隐藏 + alwaysShow?: boolean // 始终显示的列,不可隐藏 + [key: string]: any // 其他属性 +} +``` + +## 插槽 + +| 插槽名 | 说明 | +|--------|------| +| trigger | 自定义触发按钮内容 | + diff --git a/src/components/TableColumnControl/USAGE.md b/src/components/TableColumnControl/USAGE.md new file mode 100644 index 0000000..9a9d4f5 --- /dev/null +++ b/src/components/TableColumnControl/USAGE.md @@ -0,0 +1,196 @@ +# TableColumnControl 使用指南 + +## 两种使用方式 + +### 方式一:自动提取(推荐)✨ + +自动从 `el-table` 中提取列配置,无需手动配置。 + +```vue + + + + + + + + + + + + + + 编辑 + + + + + + + +``` + +**优点:** +- ✅ 无需手动配置列信息 +- ✅ 自动同步表格列的变化 +- ✅ 代码更简洁 + +### 方式二:手动配置 + +手动传入列配置,适合需要自定义列信息的场景。 + +```vue + + + + + + + + + + + + + + + +``` + +## 自动提取的配置选项 + +```typescript +interface AutoExtractOptions { + // 默认隐藏的列(prop 或 label) + defaultHidden?: string[] + + // 始终显示的列(prop 或 label) + alwaysShow?: string[] + + // 列配置映射(用于自定义列的显示名称等) + columnMap?: Record> +} +``` + +### 示例 + +```vue + +``` + +## 注意事项 + +1. **自动提取的限制**: + - 需要在表格渲染完成后才能提取列配置 + - 如果表格列是动态生成的,可能需要调用 `refreshColumns()` 方法 + +2. **固定列**: + - 使用 `fixed="left"` 或 `fixed="right"` 的列会自动标记为不可隐藏 + - 在 `alwaysShow` 中指定的列也会不可隐藏 + +3. **存储键(storageKey)**: + - 建议为每个页面使用唯一的 `storageKey`,避免列配置冲突 + - 格式建议:`页面名称-table-columns`,如 `user-list-table-columns` + +4. **性能考虑**: + - 自动提取会在组件挂载和表格更新时执行 + - 对于大型表格,建议使用手动配置以获得更好的性能 + +## 迁移指南 + +从手动配置迁移到自动提取: + +**之前(手动配置):** +```vue + +``` + +**之后(自动提取):** +```vue + +``` + +只需要: +1. 将 `:columns` 改为 `:table-ref="tableRef"` +2. 在 `el-table` 上添加 `ref="tableRef"` +3. 移除 `tableColumnConfig` 和 `isColumnVisible` 函数(如果不再需要) + diff --git a/src/components/TableColumnControl/index.vue b/src/components/TableColumnControl/index.vue new file mode 100644 index 0000000..c8032ac --- /dev/null +++ b/src/components/TableColumnControl/index.vue @@ -0,0 +1,507 @@ + + + + + {{ triggerText || '列设置' }} + + + + + + + + + {{ isAllSelected ? '取消全选' : '全选' }} + + + + + + + + + {{ column.label }} + + + {{ column.fixed === 'left' ? '固定左侧' : column.fixed === 'right' ? '固定右侧' : '固定' }} + + + + + + + + + + + + + + + + + + + diff --git a/src/composables/useTableColumns.ts b/src/composables/useTableColumns.ts new file mode 100644 index 0000000..ae959a5 --- /dev/null +++ b/src/composables/useTableColumns.ts @@ -0,0 +1,481 @@ +import { ref, computed, watch, onMounted, nextTick, type Ref } from 'vue' +import type { TableInstance } from 'element-plus' + +export interface ColumnConfig { + prop?: string + label: string + width?: number | string + minWidth?: number | string + fixed?: boolean | 'left' | 'right' + alwaysShow?: boolean + [key: string]: any +} + +/** + * 自动从 el-table 提取列配置的 composable + * @param tableRef el-table 的 ref + * @param storageKey localStorage 存储的 key,用于持久化 + * @param options 额外配置选项 + */ +export function useTableColumns( + tableRef: Ref | any, // 支持多种类型的 ref + storageKey?: string, + options?: { + // 默认隐藏的列(prop 或 label) + defaultHidden?: string[] + // 始终显示的列(prop 或 label) + alwaysShow?: string[] + // 列配置映射(用于自定义列的显示名称等) + columnMap?: Record> + } +) { + const columns = ref([]) + const visibleColumns = ref([]) + + // 获取表格实例的辅助函数 + const getTableInstance = (): TableInstance | null => { + // 方法1: 直接从 tableRef.value 获取 + if (tableRef?.value) { + return tableRef.value + } + // 方法2: 如果 tableRef 本身有 value 属性(可能是直接的 ref 对象) + if ((tableRef as any).value) { + return (tableRef as any).value + } + // 方法3: 如果 tableRef 本身就是表格实例(不应该发生,但作为备用) + if ((tableRef as any).$el) { + return tableRef as any + } + return null + } + + // 从 el-table 提取列配置 + const extractColumns = (): ColumnConfig[] => { + console.log('[useTableColumns] extractColumns 开始执行') + + const tableInstance = getTableInstance() + if (!tableInstance) { + console.warn('[useTableColumns] tableRef.value 为空') + console.warn('[useTableColumns] tableRef:', tableRef) + console.warn('[useTableColumns] tableRef?.value:', tableRef?.value) + return [] + } + + const table = tableInstance + console.log('[useTableColumns] tableInstance 存在:', table) + const extracted: ColumnConfig[] = [] + + try { + // 方法1: 从 table.store 中提取(Element Plus 内部实现) + const store = (table as any).store + console.log('[useTableColumns] store:', store) + + if (store) { + // 尝试多种路径访问列数据 + let tableColumns: any[] = [] + + if (store.states && store.states.columns) { + tableColumns = store.states.columns.value || [] + console.log('[useTableColumns] 从 store.states.columns 获取到列数:', tableColumns.length) + console.log('[useTableColumns] store.states.columns 内容:', tableColumns) + } else if (store.columns) { + tableColumns = Array.isArray(store.columns) ? store.columns : (store.columns.value || []) + console.log('[useTableColumns] 从 store.columns 获取到列数:', tableColumns.length) + } else if ((table as any).columns) { + tableColumns = Array.isArray((table as any).columns) ? (table as any).columns : ((table as any).columns.value || []) + console.log('[useTableColumns] 从 table.columns 获取到列数:', tableColumns.length) + } + + if (tableColumns.length > 0) { + tableColumns.forEach((col: any, idx: number) => { + console.log(`[useTableColumns] store 列 ${idx}:`, { + type: col.type, + label: col.label, + property: col.property, + prop: col.prop, + fixed: col.fixed, + fullCol: col + }) + + // 跳过序号列 + if (col.type === 'index' || col.type === 'selection') { + console.log(`[useTableColumns] 列 ${idx} 被跳过(类型: ${col.type})`) + return + } + + // 尝试多种方式获取 label + const label = col.label || col.columnKey || col.property || col.prop || '' + + // 如果没有 label,尝试从其他属性推断 + if (!label) { + console.log(`[useTableColumns] 列 ${idx} 没有 label,尝试从其他属性推断`) + // 如果还是没有,跳过这一列 + return + } + + const config: ColumnConfig = { + prop: col.property || col.prop || '', + label: label, + width: col.width, + minWidth: col.minWidth, + // Element Plus 中非固定列的 fixed 通常是 false,这里统一将 false 归一为 undefined + fixed: col.fixed ? col.fixed : undefined, + } + + // 应用自定义映射 + if (options?.columnMap && config.prop) { + const mapped = options.columnMap[config.prop] + if (mapped) { + Object.assign(config, mapped) + } + } + + // 应用 alwaysShow 配置 + if (options?.alwaysShow) { + const key = config.prop || config.label + if (key && options.alwaysShow.includes(key)) { + config.alwaysShow = true + } + } + + console.log(`[useTableColumns] 从 store 提取到列配置:`, config) + extracted.push(config) + }) + + console.log('[useTableColumns] 从 store 总共提取到列数:', extracted.length) + if (extracted.length > 0) { + return extracted + } + } else { + console.warn('[useTableColumns] store 中没有找到列数据') + } + } + + // 方法2: 从 DOM 中提取(备用方案,更可靠) + const tableEl = (table as any).$el + console.log('[useTableColumns] tableEl:', tableEl) + + if (tableEl) { + // 尝试多种选择器来查找表头 + let columnHeaders: NodeListOf | null = null + + // 方法2.1: 从主表格头部查找 + const headerWrapper = tableEl.querySelector('.el-table__header-wrapper') + console.log('[useTableColumns] headerWrapper:', headerWrapper) + if (headerWrapper) { + columnHeaders = headerWrapper.querySelectorAll('th') + console.log('[useTableColumns] 从 headerWrapper 找到列数:', columnHeaders?.length || 0) + } + + // 方法2.2: 如果找不到,从整个表格查找 + if (!columnHeaders || columnHeaders.length === 0) { + columnHeaders = tableEl.querySelectorAll('th') + console.log('[useTableColumns] 从整个表格找到列数:', columnHeaders?.length || 0) + } + + // 方法2.3: 如果还是找不到,尝试查找固定列 + if (!columnHeaders || columnHeaders.length === 0) { + const fixedLeft = tableEl.querySelector('.el-table__fixed-left') + console.log('[useTableColumns] fixedLeft:', fixedLeft) + if (fixedLeft) { + columnHeaders = fixedLeft.querySelectorAll('th') + console.log('[useTableColumns] 从 fixedLeft 找到列数:', columnHeaders?.length || 0) + } + } + + if (!columnHeaders || columnHeaders.length === 0) { + console.warn('[useTableColumns] 未找到任何列头元素') + return [] + } + + console.log('[useTableColumns] 最终找到列数量:', columnHeaders.length) + + // 创建一个映射,通过列索引匹配 prop + const propMap = new Map() + + // 尝试从表格的 slot 或配置中获取 prop + // 通过检查表格的列定义来匹配 + if (store && store.states && store.states.columns) { + const tableColumns = store.states.columns.value || [] + tableColumns.forEach((col: any, idx: number) => { + if (col.property || col.prop) { + propMap.set(idx, col.property || col.prop) + } + }) + } + + columnHeaders.forEach((th: HTMLElement, index: number) => { + // 尝试多种方式获取 label 文本 + // 1. 从 .cell 元素 + const cell = th.querySelector('.cell') as HTMLElement | null + // 2. 从所有可能的文本节点 + let rawLabel = '' + + if (cell) { + rawLabel = cell.innerText || cell.textContent || '' + } else { + // 尝试从 th 的所有子元素中查找文本 + const textNodes: string[] = [] + const walker = document.createTreeWalker( + th, + NodeFilter.SHOW_TEXT, + null + ) + let node: Node | null + while ((node = walker.nextNode())) { + const text = node.textContent?.trim() + if (text) { + textNodes.push(text) + } + } + rawLabel = textNodes.join(' ') || th.innerText || th.textContent || '' + } + + const label = rawLabel.trim() + + // 调试:打印 th 的完整结构 + console.log(`[useTableColumns] DOM 列 ${index}:`, { + label, + rawLabel, + thHTML: th.innerHTML.substring(0, 100), + hasCell: !!cell, + cellText: cell?.innerText || cell?.textContent || '', + thInnerText: th.innerText, + thTextContent: th.textContent + }) + + // 排除序号列和空列 + if (!label || label === '序号') { + console.log(`[useTableColumns] 列 ${index} 被跳过(序号列或空列)`) + return + } + + // 检查是否是固定列 + let fixed: 'left' | 'right' | undefined + const thParent = th.closest('table') + if (thParent) { + if (thParent.classList.contains('el-table__fixed-left') || th.closest('.el-table__fixed-left')) { + fixed = 'left' + } else if (thParent.classList.contains('el-table__fixed-right') || th.closest('.el-table__fixed-right')) { + fixed = 'right' + } else if (th.classList.contains('is-left')) { + fixed = 'left' + } else if (th.classList.contains('is-right')) { + fixed = 'right' + } + } + + // 尝试从属性中获取宽度 + const widthAttr = th.getAttribute('width') || th.style.width + let width: number | string | undefined + if (widthAttr) { + const numWidth = parseInt(widthAttr) + width = isNaN(numWidth) ? widthAttr : numWidth + } + + // 尝试从对应的 body cell 中获取 data-key 或其他属性来匹配 prop + let prop = propMap.get(index) + + // 如果没有找到 prop,尝试从 label 推断(简单匹配) + if (!prop) { + // 对于常见的列,可以通过 label 推断 prop + const labelToPropMap: Record = { + '是否退休': 'tied', + '姓名/工号': 'nameNo', + '性别': 'sex', + '部门': 'deptName', + '学历学位': 'dgreeName', + '职称等级': 'professionalTitle', + '岗位级别': 'stationLevelName', + '职业资格等级': 'levelName', + '职业资格工种': 'workName', + '用工性质': 'employmentNatureName', + '手机': 'telPhone', + '家庭住址': 'homeAddress', + '授课类型': 'teacherCate', + '操作': 'action', + } + prop = labelToPropMap[label] + } + + const columnConfig = { + prop: prop || `column_${index}`, + label, + width, + // DOM 提取的 fixed 只有 left/right,这里也归一为 undefined 或 'left'/'right' + fixed: fixed ? fixed : undefined, + } + console.log(`[useTableColumns] 提取到列配置:`, columnConfig) + extracted.push(columnConfig) + }) + + console.log('[useTableColumns] 总共提取到列数:', extracted.length) + if (extracted.length > 0) { + console.log('[useTableColumns] 提取到的所有列:', extracted) + return extracted + } + } + } catch (error) { + // 提取失败,输出错误信息 + console.error('[useTableColumns] 提取列配置时出错:', error) + } + + console.warn('[useTableColumns] 提取失败,返回空数组') + return [] + } + + // 初始化列配置 + const initColumns = async () => { + // 等待多个渲染周期,确保表格完全渲染 + await nextTick() + await new Promise(resolve => setTimeout(resolve, 300)) + await nextTick() + + let extracted = extractColumns() + + // 如果第一次提取失败,多次重试 + if (extracted.length === 0) { + for (let i = 0; i < 5; i++) { + await new Promise(resolve => setTimeout(resolve, 200)) + extracted = extractColumns() + if (extracted.length > 0) break + } + } + + if (extracted.length > 0) { + columns.value = extracted + + // 初始化可见列 + if (storageKey) { + const saved = localStorage.getItem(storageKey) + if (saved) { + try { + visibleColumns.value = JSON.parse(saved) + // 验证保存的列是否仍然存在 + const validColumns = columns.value + .filter(col => !col.alwaysShow && col.fixed === undefined) + .map(col => col.prop || col.label) + visibleColumns.value = visibleColumns.value.filter(col => validColumns.includes(col)) + } catch (e) { + initDefaultVisibleColumns() + } + } else { + initDefaultVisibleColumns() + } + } else { + initDefaultVisibleColumns() + } + } else { + console.warn('[useTableColumns] initColumns: 提取失败,未设置 columns.value') + } + } + + // 初始化默认可见列 + const initDefaultVisibleColumns = () => { + const defaultHidden = options?.defaultHidden || [] + // 默认显示所有列(除了默认隐藏的列和固定列/alwaysShow列) + // 注意:固定列和 alwaysShow 列不需要在 visibleColumns 中,因为它们始终显示 + visibleColumns.value = columns.value + .filter(col => { + const key = col.prop || col.label + return !col.alwaysShow && + !col.fixed && + !defaultHidden.includes(key) + }) + .map(col => col.prop || col.label) + + // 如果所有列都被隐藏了,至少显示所有非固定列 + if (visibleColumns.value.length === 0 && columns.value.length > 0) { + visibleColumns.value = columns.value + .filter(col => !col.alwaysShow && !col.fixed) + .map(col => col.prop || col.label) + } + } + + // 判断列是否可见 + const isColumnVisible = (propOrLabel: string): boolean => { + // 如果列配置还没有提取完成,默认显示所有列(避免初始渲染时所有列被隐藏) + if (columns.value.length === 0) { + return true + } + + const column = columns.value.find(col => + (col.prop || col.label) === propOrLabel + ) + + // 如果找不到对应的列配置,默认显示(可能是新添加的列) + if (!column) { + return true + } + + // 固定列和始终显示的列始终显示 + if (column.fixed || column.alwaysShow) { + return true + } + + // 如果可见列列表为空,默认显示所有列(初始状态) + if (visibleColumns.value.length === 0) { + return true + } + + // 检查是否在可见列列表中 + return visibleColumns.value.includes(propOrLabel) + } + + // 更新可见列 + const updateVisibleColumns = (newColumns: string[]) => { + visibleColumns.value = newColumns + if (storageKey) { + localStorage.setItem(storageKey, JSON.stringify(newColumns)) + } + } + + // 监听表格变化,重新提取列配置 + watch(() => { + // 尝试多种方式获取 tableRef.value + if (tableRef?.value) { + return tableRef.value + } + if ((tableRef as any).value) { + return (tableRef as any).value + } + return null + }, (newVal, oldVal) => { + if (newVal && newVal !== oldVal) { + console.log('[useTableColumns] tableRef.value 变化,开始提取列配置') + // 延迟一下,确保表格已经渲染 + setTimeout(() => { + initColumns() + }, 200) + } else if (newVal && !oldVal) { + // 如果从 undefined 变为有值,也触发提取 + console.log('[useTableColumns] tableRef.value 从 undefined 变为有值,开始提取列配置') + setTimeout(() => { + initColumns() + }, 200) + } + }, { immediate: true }) // 改为 true,立即检查一次 + + // 组件挂载后初始化 + onMounted(() => { + // 延迟初始化,确保表格已经渲染 + // 如果 tableRef.value 已经有值,立即初始化;否则等待 watch 触发 + const tableInstance = getTableInstance() + if (tableInstance) { + setTimeout(() => { + initColumns() + }, 300) + } else { + console.log('[useTableColumns] onMounted: tableRef.value 还未就绪,等待 watch 触发') + } + }) + + return { + columns: computed(() => columns.value), + visibleColumns: computed(() => visibleColumns.value), + isColumnVisible, + updateVisibleColumns, + refreshColumns: initColumns, + } +} + diff --git a/src/directives/v-column-visible.ts b/src/directives/v-column-visible.ts new file mode 100644 index 0000000..a1e3a1e --- /dev/null +++ b/src/directives/v-column-visible.ts @@ -0,0 +1,40 @@ +/** + * 列显示/隐藏指令 + * 用法: v-column-visible="'propName'" 或 v-column-visible="propName" + */ +import type { Directive } from 'vue' + +interface ColumnVisibleBinding { + value: string // prop 或 label + arg?: string +} + +export const vColumnVisible: Directive = { + mounted(el, binding) { + // 获取 isColumnVisible 函数(需要从父组件注入或通过其他方式获取) + const isColumnVisible = (el as any).__isColumnVisible || (() => true) + const propOrLabel = binding.value + + if (!isColumnVisible(propOrLabel)) { + // 隐藏该列(通过隐藏父元素) + const columnElement = el.closest('.el-table-column') || el.parentElement + if (columnElement) { + (columnElement as HTMLElement).style.display = 'none' + } + } + }, + updated(el, binding) { + const isColumnVisible = (el as any).__isColumnVisible || (() => true) + const propOrLabel = binding.value + const columnElement = el.closest('.el-table-column') || el.parentElement + + if (columnElement) { + if (isColumnVisible(propOrLabel)) { + (columnElement as HTMLElement).style.display = '' + } else { + (columnElement as HTMLElement).style.display = 'none' + } + } + } +} + diff --git a/src/excel/Export2Excel.js b/src/excel/Export2Excel.js index 7970071..6babf2f 100644 --- a/src/excel/Export2Excel.js +++ b/src/excel/Export2Excel.js @@ -1,7 +1,9 @@ /* eslint-disable */ -require('script-loader!file-saver'); -require('script-loader!@/excel/Blob'); -require('script-loader!xlsx/dist/xlsx.core.min'); +// import { saveAs } from 'file-saver' +// import * as XLSX from 'xlsx' +// 现代浏览器已原生支持 Blob,不需要 polyfill +// 如果需要支持旧浏览器,可以取消下面的注释 +// import '/@/excel/Blob.js' function generateArray(table) { var out = []; var rows = table.querySelectorAll('tr'); @@ -60,21 +62,21 @@ function sheet_from_array_of_arrays(data, opts) { if (range.e.c < C) range.e.c = C; var cell = {v: data[R][C]}; if (cell.v == null) continue; - var cell_ref = XLSX.utils.encode_cell({c: C, r: R}); + // var cell_ref = XLSX.utils.encode_cell({c: C, r: R}); if (typeof cell.v === 'number') cell.t = 'n'; else if (typeof cell.v === 'boolean') cell.t = 'b'; else if (cell.v instanceof Date) { - cell.t = 'n'; - cell.z = XLSX.SSF._table[14]; - cell.v = datenum(cell.v); + // cell.t = 'n'; + // cell.z = XLSX.SSF._table[14]; + // cell.v = datenum(cell.v); } else cell.t = 's'; ws[cell_ref] = cell; } } - if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); + // if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); return ws; } @@ -112,9 +114,9 @@ export function export_table_to_excel(id) { wb.SheetNames.push(ws_name); wb.Sheets[ws_name] = ws; - var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'}); + // var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'}); - saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), "test.xlsx") + // saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), "test.xlsx") } function formatJson(jsonData) { @@ -135,7 +137,7 @@ export function export_json_to_excel(th, jsonData, defaultTitle) { wb.SheetNames.push(ws_name); wb.Sheets[ws_name] = ws; - var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'}); + // var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'}); var title = defaultTitle || '列表' saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), title + ".xlsx") } diff --git a/src/views/professional/common/material-show.vue b/src/views/professional/common/material-show.vue index ae7c940..f5e9cfa 100644 --- a/src/views/professional/common/material-show.vue +++ b/src/views/professional/common/material-show.vue @@ -8,8 +8,8 @@ + diff --git a/src/views/professional/teacherbase/index.vue b/src/views/professional/teacherbase/index.vue index 81463e6..217fb19 100644 --- a/src/views/professional/teacherbase/index.vue +++ b/src/views/professional/teacherbase/index.vue @@ -16,7 +16,6 @@ @@ -25,7 +24,6 @@ @@ -37,7 +35,6 @@ reserve-keyword clearable @change="chooseSecDept" - style="width: 200px" placeholder="请选择部门" > @@ -106,7 +100,6 @@ filterable reserve-keyword clearable - style="width: 200px" placeholder="请选择职称等级" > - 双师 - + --> 导入信息 + + + @@ -272,99 +276,71 @@ v-loading="state.loading" border > - + + + - - - - {{ item.label }} - - - - - - - - - - - {{ scope.row.sex === '1' ? '男' : scope.row.sex === '0' ? '女' : '' }} - - - - - - - - - - - - - - - - - - - - - - - - - - 导出 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查看 + - 查看 - - - 人员调动 - - - 党员调动 - - - 允许进出 - - - 禁止进出 - - - 重置密码 - - - + handleMoreCommand(command, scope.row, scope.$index)" + /> + + + + @@ -390,26 +366,31 @@ - - - - - - + + + + + + + + + + + + - - - - - - 基本信息 - - + + + + + 基本信息 + + - + - + - - + + - + - + - + + @@ -471,20 +455,22 @@ - + - + @@ -492,7 +478,9 @@ @@ -507,7 +495,7 @@ - + - + - + @@ -579,18 +567,23 @@ 银行信息 - - - + + + - - + + - - + + - - - + - - - - - - 政治面貌信息 - - - - 新增 - - - - - - - - {{ scope.row.joinTime ? new Date(scope.row.joinTime).toLocaleDateString() : '-' }} - - - - - {{ scope.row.correctionTime ? new Date(scope.row.correctionTime).toLocaleDateString() : '-' }} - - - - - {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} - - - - - - - 编辑 - - - - 删除 - - - - - - - - - - - - - 社会关系信息 - - - - 新增 - - - - - - - - - {{ scope.row.birthday ? new Date(scope.row.birthday).toLocaleDateString() : '-' }} - - - - - - - - - 编辑 - - - - 删除 - - - - - - - - - - - - - 学历信息 - - - - 新增 - - - - - - - {{ scope.row.graduateTime ? new Date(scope.row.graduateTime).toLocaleDateString() : '-' }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} - - - - - - - 编辑 - - - - 删除 - - - - - - - - - - - - - - 职称信息 - - - - 新增 - - - - - - - - - {{ scope.row.certificateTime ? new Date(scope.row.certificateTime).toLocaleDateString() : '-' }} - - - - - {{ scope.row.inOfficeDate ? new Date(scope.row.inOfficeDate).toLocaleDateString() : '-' }} - - - - - - - - - - - - - - - {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} - - - - - - - 编辑 - - - - 删除 - - - - - - - - - - - - - - 职业资格信息 - - - - 新增 - - - - - - - - - {{ scope.row.certificateTime ? new Date(scope.row.certificateTime).toLocaleDateString() : '-' }} - - - - - - - - - - - - - - - {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} - - - - - - - 编辑 - - - - 删除 - - - - - - - - - - - - - - 教师证信息 - - - - - - - - - - - - - - - - - - {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} - - - - - - - - - - + + @@ -973,7 +652,7 @@ - + - + - + - + - - + + - + + - + - + - + + + + + + + + + 政治面貌信息 + + + + 新增 + + + + + + + {{ getPoliticsStatusName(scope.row.politicsStatusId || scope.row.politicsStatus) }} + + + + + {{ scope.row.joinTime ? new Date(scope.row.joinTime).toLocaleDateString() : '-' }} + + + + + {{ scope.row.correctionTime ? new Date(scope.row.correctionTime).toLocaleDateString() : '-' }} + + + + + {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} + + + + + + + 编辑 + + + + 删除 + + + + + + + + + + + + + 社会关系信息 + + + + 新增 + + + + + + + + + {{ scope.row.birthday ? new Date(scope.row.birthday).toLocaleDateString() : '-' }} + + + + + {{ getPoliticsStatusName(scope.row.politicsStatusId || scope.row.politicsStatus) }} + + + + + + + + 编辑 + + + + 删除 + + + + + + + + + + + + + 学历信息 + + + + + + + {{ scope.row.graduateTime ? new Date(scope.row.graduateTime).toLocaleDateString() : '-' }} + + + + + + + + + + + + + - + + + + + + + + - + + + + + {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} + + + + + + + + + + + + + 职称信息 + + + + + + + + + {{ scope.row.certificateTime ? new Date(scope.row.certificateTime).toLocaleDateString() : '-' }} + + + + + {{ scope.row.inOfficeDate ? new Date(scope.row.inOfficeDate).toLocaleDateString() : '-' }} + + + + + + + + + - + + + + + {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} + + + + + + + + + + + + + 职业资格信息 + + + + + + + + + {{ scope.row.certificateTime ? new Date(scope.row.certificateTime).toLocaleDateString() : '-' }} + + + + + + + + + - + + + + + {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} + + + + + + + + + + + + + 教师证信息 + + + + + + + + + + + + - + + + + + {{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }} + + + + + + + + + + + + 岗位变更信息 + + @@ -1222,6 +1182,12 @@ + + + + 党员调动信息 + + @@ -1245,6 +1211,12 @@ + + + + 综合表彰信息 + + @@ -1260,6 +1232,12 @@ + + + + 教师论文信息 + + @@ -1294,6 +1272,12 @@ + + + + 教材列表信息 + + @@ -1321,6 +1305,12 @@ + + + + 课题列表信息 + + @@ -1351,6 +1341,7 @@