This commit is contained in:
guochunsi
2026-01-06 16:39:57 +08:00
parent 7f91263205
commit 9e583c3c30
32 changed files with 2830 additions and 481 deletions

View File

@@ -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',

View File

@@ -0,0 +1,114 @@
<template>
<el-tag
v-if="showTag"
:type="type"
:effect="effect"
>
<span class="gender-tag">
<el-icon>
<Male v-if="isMale" />
<Female v-else-if="isFemale" />
</el-icon>
<span class="gender-label">{{ label }}</span>
</span>
</el-tag>
<span v-else class="gender-tag" :class="genderClass">
<el-icon>
<Male v-if="isMale" />
<Female v-else-if="isFemale" />
</el-icon>
<span class="gender-label">{{ label }}</span>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Male, Female } from '@element-plus/icons-vue'
interface Props {
sex?: string | number; // 性别值1=男0=女
showTag?: boolean; // 是否显示标签样式(有边框和背景),默认为 false
}
const props = withDefaults(defineProps<Props>(), {
sex: '',
showTag: false
})
// 判断是否为男性1=男)
const isMale = computed(() => {
const sex = String(props.sex)
const sexNum = Number(props.sex)
return sexNum === 1 || sex === '1'
})
// 判断是否为女性0=女)
const isFemale = computed(() => {
const sex = String(props.sex)
const sexNum = Number(props.sex)
return sexNum === 0 || sex === '0'
})
// 根据性别计算显示内容
const label = computed(() => {
if (isMale.value) {
return '男'
} else if (isFemale.value) {
return '女'
}
return '-'
})
const type = computed(() => {
if (isMale.value) {
return 'primary' // 蓝色
} else if (isFemale.value) {
return 'danger' // 红色/粉色
}
return 'info' // 灰色
})
const effect = computed(() => {
if (isMale.value || isFemale.value) {
return 'light'
}
return 'plain'
})
const genderClass = computed(() => {
if (isMale.value) {
return 'gender-male'
} else if (isFemale.value) {
return 'gender-female'
}
return 'gender-unknown'
})
</script>
<style scoped>
.gender-tag {
display: inline-flex;
align-items: center;
gap: 2px;
border: none;
background: transparent;
padding: 0;
}
.gender-tag .el-icon {
font-size: 12px;
}
.gender-male {
color: var(--el-color-primary);
}
.gender-female {
color: var(--el-color-danger);
}
.gender-unknown {
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -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<string \| number, { type: string; effect?: string }>` | `{}` | 否 |
| colorMap | 自定义颜色映射,用于纯文本模式,如:`{'1': '#E6A23C'}` | `Record<string \| number, string>` | `{}` | 否 |
### 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
<template>
<StatusTag
:value="scope.row.tied"
:options="YES_OR_NO"
/>
</template>
<script setup>
import StatusTag from '/@/components/StatusTag/index.vue'
const YES_OR_NO = [
{ label: '是', value: '1' },
{ label: '否', value: '0' }
]
</script>
```
### 纯文本模式(无边框和背景)
```vue
<StatusTag
:value="scope.row.tied"
:options="YES_OR_NO"
:show-tag="false"
/>
```
### 自定义类型映射
```vue
<StatusTag
:value="scope.row.status"
:options="statusOptions"
:type-map="{
'1': { type: 'success', effect: 'dark' },
'0': { type: 'danger', effect: 'light' }
}"
/>
```
### 自定义颜色映射(纯文本模式)
```vue
<StatusTag
:value="scope.row.status"
:options="statusOptions"
:show-tag="false"
:color-map="{
'1': '#67C23A',
'0': '#F56C6C'
}"
/>
```
### 完整示例
```vue
<template>
<el-table :data="tableData">
<el-table-column label="是否退休" width="100" align="center">
<template #default="scope">
<StatusTag
:value="scope.row.tied"
:options="YES_OR_NO"
/>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import global from '/@/components/tools/commondict.vue'
const StatusTag = defineAsyncComponent(() => import('/@/components/StatusTag/index.vue'))
const YES_OR_NO = global.YES_OR_NO
</script>
```
## 注意事项
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 等)。

View File

@@ -0,0 +1,140 @@
<template>
<el-tag
v-if="showTag"
:type="tagType"
:effect="tagEffect"
>
{{ label }}
</el-tag>
<span v-else class="status-tag" :class="statusClass" :style="statusStyle">
{{ label }}
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Option {
label: string
value: string | number
}
interface Props {
value?: string | number // 当前值
options?: Option[] // 选项列表,格式:[{label: '是', value: '1'}, {label: '否', value: '0'}]
showTag?: boolean // 是否显示标签样式(有边框和背景),默认为 true
typeMap?: Record<string | number, { type: string; effect?: string }> // 自定义类型映射,如 {'1': {type: 'warning', effect: 'dark'}}
colorMap?: Record<string | number, string> // 纯文本模式下的颜色映射,如 {'1': '#E6A23C'}
}
const props = withDefaults(defineProps<Props>(), {
value: '',
options: () => [],
showTag: true,
typeMap: () => ({}),
colorMap: () => ({})
})
// 默认的类型映射(只使用字符串键)
const defaultTypeMap: Record<string, { type: string; effect: string }> = {
'1': { type: 'warning', effect: 'dark' },
'0': { type: 'primary', effect: 'light' }
}
// 默认的颜色映射(只使用字符串键)
const defaultColorMap: Record<string, string> = {
'1': 'var(--el-color-warning)',
'0': 'var(--el-color-primary)'
}
// 获取值的字符串形式(用于查找映射)
const getValueKey = (value: string | number): string => {
return String(value)
}
// 合并后的类型映射(外部传入优先,否则使用默认)
const mergedTypeMap = computed(() => {
// 将外部传入的 typeMap 也转换为字符串键
const externalTypeMap: Record<string, { type: string; effect?: string }> = {}
Object.keys(props.typeMap).forEach(key => {
externalTypeMap[String(key)] = props.typeMap[key]
})
return { ...defaultTypeMap, ...externalTypeMap }
})
// 合并后的颜色映射(外部传入优先,否则使用默认)
const mergedColorMap = computed(() => {
// 将外部传入的 colorMap 也转换为字符串键
const externalColorMap: Record<string, string> = {}
Object.keys(props.colorMap).forEach(key => {
externalColorMap[String(key)] = props.colorMap[key]
})
return { ...defaultColorMap, ...externalColorMap }
})
// 合并后的选项列表(必须通过外部传入 options
const mergedOptions = computed(() => {
// 必须传入 options否则返回空数组
return props.options && props.options.length > 0 ? props.options : []
})
// 根据值找到对应的选项
const currentOption = computed(() => {
return mergedOptions.value.find((opt: Option) => {
const optValue = String(opt.value)
const propValue = String(props.value)
return optValue === propValue || Number(opt.value) === Number(props.value)
})
})
// 显示标签
const label = computed(() => {
return currentOption.value?.label || '-'
})
// 标签类型showTag 为 true 时使用)
const tagType = computed(() => {
const valueKey = getValueKey(props.value)
if (mergedTypeMap.value[valueKey]) {
return mergedTypeMap.value[valueKey].type
}
return 'info'
})
// 标签效果showTag 为 true 时使用)
const tagEffect = computed(() => {
const valueKey = getValueKey(props.value)
if (mergedTypeMap.value[valueKey]?.effect) {
return mergedTypeMap.value[valueKey].effect
}
return 'light'
})
// 纯文本模式下的样式类
const statusClass = computed(() => {
if (props.colorMap[props.value]) {
return ''
}
return 'status-default'
})
// 纯文本模式下的内联样式
const statusStyle = computed(() => {
const valueKey = getValueKey(props.value)
if (mergedColorMap.value[valueKey]) {
return { color: mergedColorMap.value[valueKey] }
}
return {}
})
</script>
<style scoped>
.status-tag {
display: inline-block;
}
.status-default {
color: var(--el-text-color-regular);
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<slot />
</template>
<script setup lang="ts">
import { provide } from 'vue'
interface Props {
isColumnVisible: (propOrLabel: string) => boolean
}
const props = defineProps<Props>()
// 提供 isColumnVisible 函数给子组件
provide('isColumnVisible', props.isColumnVisible)
</script>

View File

@@ -0,0 +1,76 @@
# TableColumn 组件
一个自动处理列显示/隐藏的 `el-table-column` 包装组件。
## 功能
- 自动根据 `isColumnVisible` 函数控制列的显示/隐藏
- 完全兼容 `el-table-column` 的所有属性和插槽
- 无需在每个列上手动添加 `v-if="isColumnVisible('xxx')"`
## 使用方法
### 1. 在父组件中提供 `isColumnVisible` 函数
```vue
<template>
<el-table ref="tableRef">
<TableColumnProvider :is-column-visible="isColumnVisible">
<TableColumn prop="name" label="姓名" width="120" />
<TableColumn prop="age" label="年龄" width="80" />
</TableColumnProvider>
</el-table>
</template>
<script setup>
import { provide } from 'vue'
import TableColumnProvider from '/@/components/TableColumn/Provider.vue'
import TableColumn from '/@/components/TableColumn/index.vue'
const isColumnVisible = (propOrLabel: string) => {
// 你的列显示逻辑
return true
}
provide('isColumnVisible', isColumnVisible)
</script>
```
### 2. 直接使用(如果已经在父组件中 provide
```vue
<template>
<el-table>
<TableColumn prop="name" label="姓名" width="120" />
<TableColumn prop="age" label="年龄" width="80" />
</el-table>
</template>
<script setup>
import TableColumn from '/@/components/TableColumn/index.vue'
</script>
```
## 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`,列将始终显示

View File

@@ -0,0 +1,64 @@
<template>
<el-table-column
v-if="shouldShow"
v-bind="$attrs"
>
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
<slot :name="name" v-bind="slotProps" />
</template>
</el-table-column>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
interface Props {
prop?: string
label?: string
// 其他 el-table-column 的所有属性都通过 $attrs 传递
}
const props = withDefaults(defineProps<Props>(), {
prop: '',
label: ''
})
// 从父组件注入 isColumnVisible 函数
const isColumnVisible = inject<(propOrLabel: string) => boolean>('isColumnVisible', () => true)
// 计算是否应该显示该列
const shouldShow = computed(() => {
// 优先使用 prop如果没有 prop 则使用 label
let key = props.prop || props.label || ''
if (!key) {
// 如果没有 prop 和 label默认显示可能是序号列等特殊列
return true
}
// 如果 key 是 label尝试通过 labelToPropMap 映射到 prop
// 这样可以确保与 useTableColumns 的提取逻辑一致
if (!props.prop && props.label) {
const labelToPropMap: Record<string, string> = {
'是否退休': 'tied',
'姓名/工号': 'nameNo',
'性别': 'sex',
'部门': 'deptName',
'学历学位': 'dgreeName',
'职称等级': 'professionalTitle',
'岗位级别': 'stationLevelName',
'职业资格等级': 'levelName',
'职业资格工种': 'workName',
'用工性质': 'employmentNatureName',
'手机': 'telPhone',
'家庭住址': 'homeAddress',
'授课类型': 'teacherCate',
'操作': 'action',
}
// 如果 label 在映射表中,使用映射后的 prop否则使用 label
key = labelToPropMap[props.label] || props.label
}
return isColumnVisible(key)
})
</script>

View File

@@ -0,0 +1,162 @@
# TableColumnControl 表格列显隐控制组件
一个通用的表格列显示/隐藏控制组件,支持动态控制表格列的显示状态。
## 功能特性
- ✅ 动态控制表格列的显示/隐藏
- ✅ 支持全选/全不选
- ✅ 支持重置为默认值
- ✅ 支持 localStorage 持久化
- ✅ 支持固定列(不可隐藏)
- ✅ 支持始终显示的列
- ✅ 可自定义触发按钮样式
## 使用方法
### 基础用法
```vue
<template>
<div>
<!-- 表格列控制按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
@change="handleColumnChange"
/>
<!-- 表格 -->
<el-table :data="tableData">
<el-table-column
v-for="col in visibleTableColumns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
/>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
const tableColumns = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'age', label: '年龄', width: 80 },
{ prop: 'email', label: '邮箱', width: 200 },
{ prop: 'address', label: '地址', width: 300 }
]
const visibleColumns = ref<string[]>(['name', 'age', 'email', 'address'])
// 根据 visibleColumns 过滤出需要显示的列
const visibleTableColumns = computed(() => {
return tableColumns.filter(col =>
visibleColumns.value.includes(col.prop || col.label)
)
})
const handleColumnChange = (columns: string[]) => {
console.log('显示的列:', columns)
}
</script>
```
### 使用 localStorage 持久化
```vue
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
storage-key="my-table-columns"
/>
```
### 固定列(不可隐藏)
```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
<!-- 使用图标按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
/>
<!-- 使用文字按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-text="列设置"
trigger-type="primary"
/>
<!-- 使用链接按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-link
trigger-text="自定义列"
/>
```
## 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 | 自定义触发按钮内容 |

View File

@@ -0,0 +1,196 @@
# TableColumnControl 使用指南
## 两种使用方式
### 方式一:自动提取(推荐)✨
自动从 `el-table` 中提取列配置,无需手动配置。
```vue
<template>
<div>
<!-- 列设置按钮 -->
<TableColumnControl
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
trigger-text="列设置"
:auto-extract-options="{
alwaysShow: ['name', 'action'], // 始终显示的列prop label
defaultHidden: ['remark'], // 默认隐藏的列
}"
/>
<!-- 表格 -->
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="age" label="年龄" width="80" />
<el-table-column prop="email" label="邮箱" width="200" />
<el-table-column prop="remark" label="备注" width="300" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import type { TableInstance } from 'element-plus'
const tableRef = ref<TableInstance>()
const visibleTableColumns = ref<string[]>([])
const tableData = ref([...])
</script>
```
**优点:**
- ✅ 无需手动配置列信息
- ✅ 自动同步表格列的变化
- ✅ 代码更简洁
### 方式二:手动配置
手动传入列配置,适合需要自定义列信息的场景。
```vue
<template>
<div>
<!-- 列设置按钮 -->
<TableColumnControl
:columns="tableColumnConfig"
v-model="visibleTableColumns"
storage-key="my-table-columns"
trigger-text="列设置"
/>
<!-- 表格 -->
<el-table :data="tableData">
<el-table-column
v-if="isColumnVisible('name')"
prop="name"
label="姓名"
width="120"
/>
<el-table-column
v-if="isColumnVisible('age')"
prop="age"
label="年龄"
width="80"
/>
<!-- ... 其他列 ... -->
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
const tableColumnConfig = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'age', label: '年龄', width: 80 },
{ prop: 'email', label: '邮箱', width: 200 },
{ prop: 'action', label: '操作', width: 150, fixed: 'right', alwaysShow: true }
]
const visibleTableColumns = ref<string[]>([])
const tableData = ref([...])
// 判断列是否显示
const isColumnVisible = (propOrLabel: string) => {
const column = tableColumnConfig.find(col => (col.prop || col.label) === propOrLabel)
if (column && (column.fixed !== undefined || column.alwaysShow)) {
return true
}
return visibleTableColumns.value.includes(propOrLabel)
}
</script>
```
## 自动提取的配置选项
```typescript
interface AutoExtractOptions {
// 默认隐藏的列prop 或 label
defaultHidden?: string[]
// 始终显示的列prop 或 label
alwaysShow?: string[]
// 列配置映射(用于自定义列的显示名称等)
columnMap?: Record<string, Partial<ColumnConfig>>
}
```
### 示例
```vue
<TableColumnControl
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
:auto-extract-options="{
// 默认隐藏备注列
defaultHidden: ['remark', 'description'],
// 始终显示姓名和操作列
alwaysShow: ['name', 'action'],
// 自定义列的显示名称
columnMap: {
'email': { label: '电子邮箱' },
'phone': { label: '联系电话' }
}
}"
/>
```
## 注意事项
1. **自动提取的限制**
- 需要在表格渲染完成后才能提取列配置
- 如果表格列是动态生成的,可能需要调用 `refreshColumns()` 方法
2. **固定列**
- 使用 `fixed="left"``fixed="right"` 的列会自动标记为不可隐藏
-`alwaysShow` 中指定的列也会不可隐藏
3. **存储键storageKey**
- 建议为每个页面使用唯一的 `storageKey`,避免列配置冲突
- 格式建议:`页面名称-table-columns`,如 `user-list-table-columns`
4. **性能考虑**
- 自动提取会在组件挂载和表格更新时执行
- 对于大型表格,建议使用手动配置以获得更好的性能
## 迁移指南
从手动配置迁移到自动提取:
**之前(手动配置):**
```vue
<TableColumnControl
:columns="tableColumnConfig"
v-model="visibleTableColumns"
storage-key="my-table-columns"
/>
```
**之后(自动提取):**
```vue
<TableColumnControl
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
/>
```
只需要:
1.`:columns` 改为 `:table-ref="tableRef"`
2.`el-table` 上添加 `ref="tableRef"`
3. 移除 `tableColumnConfig``isColumnVisible` 函数(如果不再需要)

View File

@@ -0,0 +1,461 @@
<template>
<div class="table-column-control">
<el-button
:type="triggerType"
:icon="Setting"
:size="triggerSize"
:circle="triggerCircle"
:link="triggerLink"
@click="visible = true"
>
<slot name="trigger">
{{ triggerText || '列设置' }}
</slot>
</el-button>
<el-dialog
v-model="visible"
title="列显示设置"
:width="dialogWidth"
append-to-body
>
<div class="column-control-content">
<div class="column-control-header">
<div class="header-actions">
<el-button
type="primary"
link
@click="handleToggleSelectAll"
>
{{ isAllSelected ? '取消全选' : '全选' }}
</el-button>
</div>
</div>
<div class="column-control-body">
<el-checkbox-group v-model="checkedColumns" @change="handleColumnChange" class="column-checkbox-group">
<div
v-for="column in actualColumns"
:key="column.prop || column.label"
class="column-item"
>
<el-checkbox
:label="column.prop || column.label"
:disabled="column.fixed !== undefined || column.alwaysShow"
>
{{ column.label }}
</el-checkbox>
<el-tag v-if="column.fixed !== undefined" size="small" type="info">
{{ column.fixed === 'left' ? '固定左侧' : column.fixed === 'right' ? '固定右侧' : '固定' }}
</el-tag>
</div>
</el-checkbox-group>
</div>
</div>
<template #footer>
<div class="column-control-footer">
<el-button @click="handleReset">重置</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick, type Ref } from 'vue'
import { Setting } from '@element-plus/icons-vue'
import type { TableInstance } from 'element-plus'
import { useTableColumns, type ColumnConfig } from '/@/composables/useTableColumns'
interface Column {
prop?: string
label: string
fixed?: boolean | 'left' | 'right'
alwaysShow?: boolean // 始终显示的列,不可隐藏
[key: string]: any
}
interface Props {
columns?: Column[] // 手动配置的列(可选,如果提供了 tableRef 则自动提取)
tableRef?: Ref<TableInstance | undefined> // el-table 的 ref用于自动提取列配置
modelValue?: string[] // 当前显示的列
storageKey?: string // localStorage 存储的 key用于持久化
triggerType?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
triggerSize?: 'large' | 'default' | 'small'
triggerCircle?: boolean
triggerText?: string
triggerLink?: boolean
dialogWidth?: string // 对话框宽度
// 自动提取时的配置选项
autoExtractOptions?: {
defaultHidden?: string[]
alwaysShow?: string[]
columnMap?: Record<string, Partial<ColumnConfig>>
}
}
const props = withDefaults(defineProps<Props>(), {
triggerType: 'default',
triggerSize: 'default',
triggerCircle: false,
triggerText: '',
triggerLink: false,
dialogWidth: '600px'
})
const emit = defineEmits<{
'update:modelValue': [value: string[]]
'change': [value: string[]]
}>()
const visible = ref(false)
const checkedColumns = ref<string[]>([])
// 监听弹窗打开,触发列配置重新提取
watch(visible, (newVal) => {
console.log('[TableColumnControl] 弹窗状态变化:', newVal)
if (newVal && props.tableRef) {
console.log('[TableColumnControl] 弹窗打开tableRef 存在:', props.tableRef)
console.log('[TableColumnControl] tableRef.value:', props.tableRef.value)
console.log('[TableColumnControl] tableRef.value 类型:', typeof props.tableRef.value)
// 尝试多种方式获取表格实例
const getTableInstance = (): TableInstance | null => {
// 方法1: 直接从 props.tableRef.value 获取
if (props.tableRef?.value) {
console.log('[TableColumnControl] 从 props.tableRef.value 获取表格实例')
return props.tableRef.value
}
// 方法2: 尝试从 props.tableRef 本身获取(可能是直接的 ref 对象)
if ((props.tableRef as any).value) {
console.log('[TableColumnControl] 从 props.tableRef 的 value 属性获取表格实例')
return (props.tableRef as any).value
}
// 方法3: 如果 props.tableRef 本身就是表格实例(不应该发生,但作为备用)
if ((props.tableRef as any).$el) {
console.log('[TableColumnControl] props.tableRef 本身就是表格实例')
return props.tableRef as any
}
return null
}
// 如果 tableRef.value 已经有值,直接提取
const tableInstance = getTableInstance()
if (tableInstance) {
const tableEl = (tableInstance as any).$el
if (tableEl) {
console.log('[TableColumnControl] 表格实例已就绪,立即提取列配置')
nextTick(() => {
refreshAutoColumns()
console.log('[TableColumnControl] 刷新后列数:', actualColumns.value.length)
})
return
}
}
// 等待 tableRef.value 有值后再提取
let waitCount = 0
const maxWaitCount = 50 // 最多等待 5 秒50 * 100ms
let waitTimer: ReturnType<typeof setTimeout> | null = null
const waitForTableRef = () => {
// 尝试获取表格实例
const tableInstance = getTableInstance()
if (tableInstance) {
const tableEl = (tableInstance as any).$el
if (tableEl) {
console.log('[TableColumnControl] 表格实例已就绪,开始提取列配置')
nextTick(() => {
refreshAutoColumns()
console.log('[TableColumnControl] 刷新后列数:', actualColumns.value.length)
// 如果还是没有数据,多次重试
let retryCount = 0
const maxRetries = 10
const retryInterval = setInterval(() => {
retryCount++
console.log(`[TableColumnControl] 第 ${retryCount} 次重试刷新列配置`)
refreshAutoColumns()
console.log(`[TableColumnControl] 重试后列数:`, actualColumns.value.length)
if (actualColumns.value.length > 0 || retryCount >= maxRetries) {
console.log('[TableColumnControl] 停止重试,最终列数:', actualColumns.value.length)
clearInterval(retryInterval)
}
}, 200)
})
return // 成功获取,退出
}
}
// 继续等待
waitCount++
if (waitCount < maxWaitCount) {
console.log(`[TableColumnControl] tableRef.value 还未就绪,等待中... (${waitCount}/${maxWaitCount})`)
waitTimer = setTimeout(waitForTableRef, 100)
} else {
console.warn('[TableColumnControl] 等待超时tableRef.value 仍未就绪')
console.warn('[TableColumnControl] props.tableRef:', props.tableRef)
console.warn('[TableColumnControl] props.tableRef?.value:', props.tableRef?.value)
// 即使超时,也尝试提取一次(可能表格已经渲染了,只是 ref 没有正确绑定)
console.log('[TableColumnControl] 尝试强制提取列配置')
refreshAutoColumns()
console.log('[TableColumnControl] 强制提取后列数:', actualColumns.value.length)
}
}
// 延迟一下,确保表格已渲染
setTimeout(() => {
waitForTableRef()
}, 300)
// 清理函数:弹窗关闭时清除等待定时器
return () => {
if (waitTimer) {
clearTimeout(waitTimer)
waitTimer = null
}
}
} else {
console.warn('[TableColumnControl] 弹窗打开但 tableRef 不存在或为空')
}
})
// 如果提供了 tableRef使用自动提取否则使用手动配置的 columns
const tableColumnsResult = props.tableRef
? useTableColumns(props.tableRef, props.storageKey, props.autoExtractOptions)
: {
columns: computed(() => []),
visibleColumns: computed(() => []),
updateVisibleColumns: () => {},
refreshColumns: () => {},
isColumnVisible: () => true
}
const {
columns: autoColumns,
visibleColumns: autoVisibleColumns,
updateVisibleColumns: updateAutoVisibleColumns,
refreshColumns: refreshAutoColumns,
isColumnVisible: autoIsColumnVisible
} = tableColumnsResult
// 监听 tableRef.value 的变化,当它被赋值时触发列配置提取
if (props.tableRef) {
// 尝试多种方式监听 tableRef.value 的变化
watch(() => {
// 尝试多种方式获取 tableRef.value
if (props.tableRef?.value) {
return props.tableRef.value
}
if ((props.tableRef as any).value) {
return (props.tableRef as any).value
}
return null
}, (newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
console.log('[TableColumnControl] tableRef.value 已赋值,触发列配置提取')
// 延迟一下,确保表格完全渲染
setTimeout(() => {
nextTick(() => {
refreshAutoColumns()
console.log('[TableColumnControl] 列配置提取完成,列数:', actualColumns.value.length)
})
}, 200)
}
}, { immediate: true }) // 立即检查一次,如果 tableRef.value 已经有值,立即触发
}
// 调试:检查 tableRef 传递
console.log('[TableColumnControl] props.tableRef:', props.tableRef)
console.log('[TableColumnControl] props.tableRef?.value:', props.tableRef?.value)
// 暴露给父组件使用
defineExpose({
isColumnVisible: autoIsColumnVisible,
visibleColumns: autoVisibleColumns,
refreshColumns: refreshAutoColumns
})
// 实际使用的列配置
const actualColumns = computed(() => {
const result = props.tableRef && autoColumns.value.length > 0
? autoColumns.value
: props.columns || []
console.log('[TableColumnControl] actualColumns 计算:', {
hasTableRef: !!props.tableRef,
autoColumnsLength: autoColumns.value.length,
propsColumnsLength: props.columns?.length || 0,
resultLength: result.length
})
return result
})
// 初始化选中的列
const initCheckedColumns = () => {
if (props.modelValue && props.modelValue.length > 0) {
checkedColumns.value = [...props.modelValue]
} else if (props.tableRef && autoVisibleColumns.value.length > 0) {
// 使用自动提取的可见列
checkedColumns.value = [...autoVisibleColumns.value]
} else if (props.storageKey) {
// 从 localStorage 读取
const saved = localStorage.getItem(props.storageKey)
if (saved) {
try {
checkedColumns.value = JSON.parse(saved)
} catch (e) {
// 如果解析失败,使用默认值(所有列)
checkedColumns.value = getDefaultColumns()
}
} else {
checkedColumns.value = getDefaultColumns()
}
} else {
checkedColumns.value = getDefaultColumns()
}
}
// 获取默认显示的列(所有可隐藏的列)
const getDefaultColumns = (): string[] => {
return actualColumns.value
.filter(col => !col.alwaysShow && col.fixed === undefined)
.map(col => col.prop || col.label)
}
// 获取所有可选择的列
const selectableColumns = computed(() => {
return actualColumns.value
.filter(col => !col.alwaysShow && col.fixed === undefined)
.map(col => col.prop || col.label)
})
// 判断是否全选
const isAllSelected = computed(() => {
const selectable = selectableColumns.value
if (selectable.length === 0) return false
return selectable.every(col => checkedColumns.value.includes(col))
})
// 切换全选/全不选
const handleToggleSelectAll = () => {
if (isAllSelected.value) {
// 当前全选,执行全不选
checkedColumns.value = []
} else {
// 当前未全选,执行全选
checkedColumns.value = [...selectableColumns.value]
}
}
// 重置为默认值
const handleReset = () => {
checkedColumns.value = getDefaultColumns()
handleColumnChange(checkedColumns.value)
}
// 确认
const handleConfirm = () => {
handleColumnChange(checkedColumns.value)
visible.value = false
}
// 列变化处理
const handleColumnChange = (value: string[]) => {
emit('update:modelValue', value)
emit('change', value)
// 如果使用自动提取,同步更新
if (props.tableRef) {
updateAutoVisibleColumns(value)
} else {
// 保存到 localStorage
if (props.storageKey) {
localStorage.setItem(props.storageKey, JSON.stringify(value))
}
}
}
// 监听外部 modelValue 变化
watch(() => props.modelValue, (newVal) => {
if (newVal && newVal.length > 0) {
checkedColumns.value = [...newVal]
}
}, { immediate: true })
// 初始化
onMounted(() => {
initCheckedColumns()
})
</script>
<style lang="scss" scoped>
.table-column-control {
display: inline-block;
}
.column-control-content {
.column-control-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px solid #ebeef5;
.header-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 8px;
}
}
.column-control-body {
max-height: 400px;
overflow-y: auto;
margin-bottom: 12px;
.column-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px 16px;
}
.column-item {
display: flex;
align-items: center;
gap: 6px;
min-width: 120px;
:deep(.el-checkbox) {
margin-right: 0;
.el-checkbox__label {
font-size: 14px;
color: #606266;
padding-left: 8px;
}
}
}
}
.column-control-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@@ -0,0 +1,411 @@
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<TableInstance | undefined> | any, // 支持多种类型的 ref
storageKey?: string,
options?: {
// 默认隐藏的列prop 或 label
defaultHidden?: string[]
// 始终显示的列prop 或 label
alwaysShow?: string[]
// 列配置映射(用于自定义列的显示名称等)
columnMap?: Record<string, Partial<ColumnConfig>>
}
) {
const columns = ref<ColumnConfig[]>([])
const visibleColumns = ref<string[]>([])
// 获取表格实例的辅助函数
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 || []
} else if (store.columns) {
tableColumns = Array.isArray(store.columns) ? store.columns : (store.columns.value || [])
} else if ((table as any).columns) {
tableColumns = Array.isArray((table as any).columns) ? (table as any).columns : ((table as any).columns.value || [])
}
if (tableColumns.length > 0) {
tableColumns.forEach((col: any) => {
// 跳过序号列和没有 label 的列
if (!col.label || col.type === 'index') return
const config: ColumnConfig = {
prop: col.property || col.prop || '',
label: col.label || '',
width: col.width,
minWidth: col.minWidth,
fixed: col.fixed,
}
// 应用自定义映射
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
}
}
extracted.push(config)
})
if (extracted.length > 0) {
return extracted
}
}
}
// 方法2: 从 DOM 中提取(备用方案,更可靠)
const tableEl = (table as any).$el
console.log('[useTableColumns] tableEl:', tableEl)
if (tableEl) {
// 尝试多种选择器来查找表头
let columnHeaders: NodeListOf<HTMLElement> | 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<number, string>()
// 尝试从表格的 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) => {
const label = th.textContent?.trim() || ''
console.log(`[useTableColumns] 列 ${index}: label="${label}"`)
// 排除序号列和空列
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<string, string> = {
'是否退休': '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,
fixed,
}
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 || []
// 默认显示所有列(除了默认隐藏的列)
visibleColumns.value = columns.value
.filter(col => {
const key = col.prop || col.label
return !col.alwaysShow &&
col.fixed === undefined &&
!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 === undefined)
.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 !== undefined || 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,
}
}

View File

@@ -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<HTMLElement, string> = {
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'
}
}
}
}

View File

@@ -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');

View File

@@ -8,8 +8,8 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import authImg from '@/components/tools/auth-img.vue'
import { ref, defineAsyncComponent } from 'vue'
const authImg = defineAsyncComponent(() => import('/@/components/tools/auth-img.vue'))
// Props
defineProps<{

View File

@@ -25,9 +25,11 @@
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="teacherNo" label="工号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="teacherName" label="姓名" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="姓名/工号" min-width="150" align="center">
<template #default="scope">
<TeacherNameNo :name="scope.row.teacherName" :no="scope.row.teacherNo" />
</template>
</el-table-column>
<el-table-column prop="year" label="年份" width="100" align="center" />
@@ -174,11 +176,14 @@
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { defineAsyncComponent } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserInfo } from '/@/stores/userInfo'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { fetchList, addObj, putObj, delObj } from '/@/api/professional/professionalyearbounds'
import { fetchList, addObj, putObj, delObj } from '/@/api/professional/salaries/professionalyearbounds'
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
// 使用 Pinia store
const userInfoStore = useUserInfo()

View File

@@ -92,7 +92,7 @@ import { storeToRefs } from 'pinia'
import { useUserInfo } from '/@/stores/userInfo'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { fetchList, delObj } from '/@/api/professional/salaryexportrecord'
import { fetchList, delObj } from '/@/api/professional/salaries/salaryexportrecord'
// 使用 Pinia store
const userInfoStore = useUserInfo()

View File

@@ -67,9 +67,11 @@
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="realName" label="姓名" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="teacherNo" label="工号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="姓名/工号" min-width="150" align="center">
<template #default="scope">
<TeacherNameNo :name="scope.row.realName" :no="scope.row.teacherNo" />
</template>
</el-table-column>
<el-table-column prop="startDate" label="开始日期" width="120" align="center" />
@@ -104,7 +106,7 @@ import { ref, reactive, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserInfo } from '/@/stores/userInfo'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList } from '/@/api/professional/teacherawardtax'
import { fetchList } from '/@/api/professional/salaries/teacherawardtax'
import ImportAwardTax from './importAwardTax.vue'
// 使用 Pinia store

View File

@@ -128,9 +128,11 @@
<el-table-column prop="numId" label="编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="teacherNo" label="工号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="realName" label="姓名" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="姓名/工号" min-width="150" align="center">
<template #default="scope">
<TeacherNameNo :name="scope.row.realName" :no="scope.row.teacherNo" />
</template>
</el-table-column>
<el-table-column prop="postSalary" label="岗位工资" min-width="120" align="center" show-overflow-tooltip />
@@ -181,17 +183,20 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { defineAsyncComponent } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserInfo } from '/@/stores/userInfo'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { fetchList, delBatch, setCanSearch } from '/@/api/professional/teacherpayslip'
import { checkAuth } from '/@/api/professional/teachersalary'
import { fetchList, delBatch, setCanSearch } from '/@/api/professional/salaries/teacherpayslip'
import { checkAuth } from '/@/api/professional/salaries/teachersalary'
import { getStationLevelList } from '/@/api/professional/professionalstationlevelconfig'
import SalaryInfo from './salaryInfo.vue'
import ImportBaseSalary from './importBaseSalary.vue'
import ExportBaseSalary from './exportBaseSalary.vue'
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
// 使用 Pinia store
const userInfoStore = useUserInfo()
const { userInfos } = storeToRefs(userInfoStore)

View File

@@ -206,8 +206,8 @@
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { queryUserInfo, queryExtendSalaryInfo } from '/@/api/professional/teacherpayslip'
import { checkAuth } from '/@/api/professional/teachersalary'
import { queryUserInfo, queryExtendSalaryInfo } from '/@/api/professional/salaries/teacherpayslip'
import { checkAuth } from '/@/api/professional/salaries/teachersalary'
// 对话框显示状态
const visible = ref(false)

View File

@@ -140,9 +140,11 @@
<el-table-column prop="numId" label="编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="teacherNo" label="工号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="realName" label="姓名" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="姓名/工号" min-width="150" align="center">
<template #default="scope">
<TeacherNameNo :name="scope.row.realName" :no="scope.row.teacherNo" />
</template>
</el-table-column>
<el-table-column prop="postSalary" label="岗位工资" min-width="120" align="center" show-overflow-tooltip />
@@ -194,14 +196,17 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { defineAsyncComponent } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserInfo } from '/@/stores/userInfo'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { fetchList, delBatch, setCanSearch, checkAuth } from '/@/api/professional/teachersalary'
import { fetchList, delBatch, setCanSearch, checkAuth } from '/@/api/professional/salaries/teachersalary'
import { getStationLevelList } from '/@/api/professional/professionalstationlevelconfig'
import SalaryInfo from './salaryInfo.vue'
import ImportBaseSalary from './importBaseSalary.vue'
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
import ExportBaseSalary from './exportBaseSalary.vue'
import ImportTaxSalary from './importTaxSalary.vue'

View File

@@ -204,7 +204,7 @@
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { queryUserInfo, queryExtendSalaryInfo, checkAuth } from '/@/api/professional/teachersalary'
import { queryUserInfo, queryExtendSalaryInfo, checkAuth } from '/@/api/professional/salaries/teachersalary'
// 对话框显示状态
const visible = ref(false)

View File

@@ -0,0 +1,91 @@
<template>
<el-dropdown
v-if="hasVisibleItems"
trigger="click"
@command="handleCommand"
:style="dropdownStyle"
>
<el-button
:type="buttonType"
link
:style="buttonStyle"
>
<slot name="button">
{{ buttonText }}
<el-icon v-if="buttonIcon" class="el-icon--right" :style="iconStyle">
<component :is="buttonIcon" v-if="buttonIcon" />
</el-icon>
</slot>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in visibleItems"
:key="item.command"
:command="item.command"
>
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span :style="item.icon ? { marginLeft: '8px' } : {}">{{ item.label }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Operation } from '@element-plus/icons-vue'
interface MenuItem {
command: string
label: string
icon?: any
visible?: boolean | (() => boolean)
}
interface Props {
items: MenuItem[]
buttonText?: string
buttonIcon?: any
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
buttonStyle?: string | Record<string, any>
dropdownStyle?: string | Record<string, any>
iconStyle?: string | Record<string, any>
}
const props = withDefaults(defineProps<Props>(), {
buttonText: '更多',
buttonIcon: Operation,
buttonType: 'primary',
buttonStyle: () => ({ whiteSpace: 'nowrap' }),
dropdownStyle: () => ({ marginLeft: '12px' }),
iconStyle: () => ({ marginLeft: '4px' })
})
const emit = defineEmits<{
command: [command: string]
}>()
// 计算可见的菜单项
const visibleItems = computed(() => {
return props.items.filter(item => {
if (item.visible === undefined) return true
if (typeof item.visible === 'boolean') return item.visible
if (typeof item.visible === 'function') return item.visible()
return false
})
})
// 是否有可见的菜单项
const hasVisibleItems = computed(() => {
return visibleItems.value.length > 0
})
// 处理命令
const handleCommand = (command: string) => {
emit('command', command)
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="educationDialogFromVisible" width="80%" :title="waitShenheForm.title" append-to-body>
<el-dialog v-model="educationDialogFromVisible" width="600" :title="waitShenheForm.title" append-to-body>
<!--2.1 教师资格证-->
<el-form v-if="waitShenheForm.a" ref="teacherCertificateFormRef" :model="waitShenheForm.form" :rules="teacherCertificateRules" label-width="120px">
@@ -43,8 +43,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogSubmit(0)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
<div style="text-align: right; width: 100%;">
<el-button type="primary" @click="dialogSubmit(0)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
</div>
</el-form-item>
</el-form>
<!--2.2 学历-->
@@ -148,8 +150,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogSubmit(1)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
<div style="text-align: right; width: 100%;">
<el-button type="primary" @click="dialogSubmit(1)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
</div>
</el-form-item>
</el-form>
<!--2.3 职称-->
@@ -227,8 +231,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogSubmit(2)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
<div style="text-align: right; width: 100%;">
<el-button type="primary" @click="dialogSubmit(2)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
</div>
</el-form-item>
</el-form>
<!--2.4 职业-->
@@ -295,8 +301,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogSubmit(3)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
<div style="text-align: right; width: 100%;">
<el-button type="primary" @click="dialogSubmit(3)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
</div>
</el-form-item>
</el-form>
<!--2.5 综合表彰-->
@@ -339,8 +347,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogSubmit(4)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
<div style="text-align: right; width: 100%;">
<el-button type="primary" @click="dialogSubmit(4)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
</div>
</el-form-item>
</el-form>
<!--2.6 人员调动-->
@@ -404,8 +414,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogSubmit(5)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
<div style="text-align: right; width: 100%;">
<el-button type="primary" @click="dialogSubmit(5)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
</div>
</el-form-item>
</el-form>
<!--2.7 党员调动-->
@@ -460,8 +472,10 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogSubmit(6)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
<div style="text-align: right; width: 100%;">
<el-button type="primary" @click="dialogSubmit(6)">提交</el-button>
<el-button @click="educationDialogFromVisible = false">取消</el-button>
</div>
</el-form-item>
</el-form>
</el-dialog>