Merge branch 'developer' of ssh://code.cyweb.top:30033/scj/zhxy/v3/cloud-ui into developer
This commit is contained in:
163
COLUMN_VISIBILITY_SOLUTIONS.md
Normal file
163
COLUMN_VISIBILITY_SOLUTIONS.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 列显示/隐藏方案对比
|
||||
|
||||
## 方案1:TableColumn 包装组件 ⭐推荐
|
||||
|
||||
### 优点
|
||||
- ✅ 使用简单,只需替换组件名
|
||||
- ✅ 完全兼容 `el-table-column` 的所有属性和插槽
|
||||
- ✅ 代码清晰,易于维护
|
||||
- ✅ 无需修改现有代码结构
|
||||
|
||||
### 缺点
|
||||
- ❌ 需要创建一个新组件
|
||||
- ❌ 需要在使用的地方 provide `isColumnVisible` 函数
|
||||
|
||||
### 使用示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-table ref="tableRef">
|
||||
<TableColumnProvider :is-column-visible="isColumnVisible">
|
||||
<!-- 替换 el-table-column 为 TableColumn,移除 v-if -->
|
||||
<TableColumn prop="tied" label="是否退休" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<StatusTag :value="scope.row.tied" :options="YES_OR_NO" />
|
||||
</template>
|
||||
</TableColumn>
|
||||
|
||||
<TableColumn prop="nameNo" label="姓名/工号" min-width="150" align="center" fixed>
|
||||
<template #default="scope">
|
||||
<TeacherNameNo :name="scope.row.realName" :no="scope.row.teacherNo" />
|
||||
</template>
|
||||
</TableColumn>
|
||||
</TableColumnProvider>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TableColumnProvider from '/@/components/TableColumn/Provider.vue'
|
||||
import TableColumn from '/@/components/TableColumn/index.vue'
|
||||
|
||||
const isColumnVisible = (propOrLabel) => {
|
||||
return tableColumnControlRef.value?.isColumnVisible?.(propOrLabel) ?? true
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方案2:自定义指令 v-column-visible
|
||||
|
||||
### 优点
|
||||
- ✅ 可以保留 `el-table-column`
|
||||
- ✅ 使用相对简单
|
||||
|
||||
### 缺点
|
||||
- ❌ 仍然需要在每个列上添加指令
|
||||
- ❌ 实现较复杂,需要操作 DOM
|
||||
- ❌ 可能不够优雅
|
||||
|
||||
### 使用示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-table>
|
||||
<el-table-column
|
||||
v-column-visible="'tied'"
|
||||
prop="tied"
|
||||
label="是否退休"
|
||||
width="100"
|
||||
/>
|
||||
</el-table>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方案3:使用 computed 动态生成列配置
|
||||
|
||||
### 优点
|
||||
- ✅ 最彻底,完全控制列的渲染
|
||||
- ✅ 性能最好(只渲染可见的列)
|
||||
|
||||
### 缺点
|
||||
- ❌ 需要重构现有代码,改动较大
|
||||
- ❌ 需要将列配置抽离出来
|
||||
- ❌ 插槽处理较复杂
|
||||
|
||||
### 使用示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-table>
|
||||
<component
|
||||
v-for="col in visibleColumns"
|
||||
:is="col.component"
|
||||
v-bind="col.props"
|
||||
>
|
||||
<template v-if="col.slots" #[slotName]="slotProps" v-for="(slot, slotName) in col.slots">
|
||||
<component :is="slot" v-bind="slotProps" />
|
||||
</template>
|
||||
</component>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const columns = [
|
||||
{ prop: 'tied', label: '是否退休', component: 'el-table-column', slots: { default: StatusTag } },
|
||||
// ...
|
||||
]
|
||||
|
||||
const visibleColumns = computed(() => {
|
||||
return columns.filter(col => isColumnVisible(col.prop || col.label))
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方案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
|
||||
<TableColumnProvider :is-column-visible="isColumnVisible">
|
||||
<!-- 所有列 -->
|
||||
</TableColumnProvider>
|
||||
```
|
||||
|
||||
3. 将所有 `el-table-column` 替换为 `TableColumn`,并移除 `v-if`:
|
||||
```vue
|
||||
<!-- 之前 -->
|
||||
<el-table-column v-if="isColumnVisible('tied')" prop="tied" label="是否退休" />
|
||||
|
||||
<!-- 之后 -->
|
||||
<TableColumn prop="tied" label="是否退休" />
|
||||
```
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
| 设置/配置 | `Setting` |
|
||||
| 锁定/解锁 | `Lock` |
|
||||
| 用户相关 | `User` |
|
||||
| 调动/转换 | `Switch`|
|
||||
|
||||
### 使用示例
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
89
src/api/professional/stayschool/outercompany.ts
Normal file
89
src/api/professional/stayschool/outercompany.ts
Normal file
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
125
src/api/professional/stayschool/outercompanyemployee.ts
Normal file
125
src/api/professional/stayschool/outercompanyemployee.ts
Normal file
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
114
src/components/GenderTag/index.vue
Normal file
114
src/components/GenderTag/index.vue
Normal 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>
|
||||
|
||||
149
src/components/StatusTag/README.md
Normal file
149
src/components/StatusTag/README.md
Normal 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 等)。
|
||||
|
||||
140
src/components/StatusTag/index.vue
Normal file
140
src/components/StatusTag/index.vue
Normal 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>
|
||||
|
||||
17
src/components/TableColumn/Provider.vue
Normal file
17
src/components/TableColumn/Provider.vue
Normal 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>
|
||||
|
||||
76
src/components/TableColumn/README.md
Normal file
76
src/components/TableColumn/README.md
Normal 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`,列将始终显示
|
||||
|
||||
66
src/components/TableColumn/index.vue
Normal file
66
src/components/TableColumn/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-table-column
|
||||
v-if="shouldShow"
|
||||
:prop="prop"
|
||||
:label="label"
|
||||
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>
|
||||
|
||||
162
src/components/TableColumnControl/README.md
Normal file
162
src/components/TableColumnControl/README.md
Normal 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 | 自定义触发按钮内容 |
|
||||
|
||||
196
src/components/TableColumnControl/USAGE.md
Normal file
196
src/components/TableColumnControl/USAGE.md
Normal 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` 函数(如果不再需要)
|
||||
|
||||
507
src/components/TableColumnControl/index.vue
Normal file
507
src/components/TableColumnControl/index.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<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 || 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[]>([])
|
||||
|
||||
// 如果提供了 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
|
||||
|
||||
// 实际使用的列配置
|
||||
const actualColumns = computed(() => {
|
||||
const result = props.tableRef && autoColumns.value.length > 0
|
||||
? autoColumns.value
|
||||
: props.columns || []
|
||||
return result
|
||||
})
|
||||
|
||||
// 获取所有列(包括固定列和 alwaysShow 列)
|
||||
const getAllColumns = (): string[] => {
|
||||
return actualColumns.value.map(col => col.prop || col.label)
|
||||
}
|
||||
|
||||
// 初始化选中的列
|
||||
const initCheckedColumns = () => {
|
||||
if (props.modelValue && props.modelValue.length > 0) {
|
||||
checkedColumns.value = [...props.modelValue]
|
||||
} else if (props.tableRef && autoVisibleColumns.value.length > 0) {
|
||||
// 使用自动提取的可见列,但需要确保包含所有列(包括固定列)
|
||||
// 合并已保存的可见列和固定列/alwaysShow列
|
||||
const fixedAndAlwaysShow = actualColumns.value
|
||||
.filter(col => col.alwaysShow || !!col.fixed)
|
||||
.map(col => col.prop || col.label)
|
||||
checkedColumns.value = [...new Set([...autoVisibleColumns.value, ...fixedAndAlwaysShow])]
|
||||
} else if (props.storageKey) {
|
||||
// 从 localStorage 读取
|
||||
const saved = localStorage.getItem(props.storageKey)
|
||||
if (saved) {
|
||||
try {
|
||||
const savedColumns = JSON.parse(saved)
|
||||
// 确保固定列和 alwaysShow 列始终在选中列表中
|
||||
const fixedAndAlwaysShow = actualColumns.value
|
||||
.filter(col => col.alwaysShow || !!col.fixed)
|
||||
.map(col => col.prop || col.label)
|
||||
checkedColumns.value = [...new Set([...savedColumns, ...fixedAndAlwaysShow])]
|
||||
} catch (e) {
|
||||
// 如果解析失败,使用默认值(所有列)
|
||||
checkedColumns.value = getAllColumns()
|
||||
}
|
||||
} else {
|
||||
checkedColumns.value = getAllColumns()
|
||||
}
|
||||
} else {
|
||||
checkedColumns.value = getAllColumns()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 actualColumns 变化,更新选中状态
|
||||
watch(actualColumns, (newColumns) => {
|
||||
if (newColumns.length > 0 && checkedColumns.value.length === 0) {
|
||||
// 如果列数据已加载但选中列表为空,初始化选中所有列
|
||||
initCheckedColumns()
|
||||
} else if (newColumns.length > 0) {
|
||||
// 确保固定列和 alwaysShow 列始终在选中列表中
|
||||
const fixedAndAlwaysShow = newColumns
|
||||
.filter(col => col.alwaysShow || !!col.fixed)
|
||||
.map(col => col.prop || col.label)
|
||||
const currentChecked = checkedColumns.value
|
||||
const missingFixed = fixedAndAlwaysShow.filter(col => !currentChecked.includes(col))
|
||||
if (missingFixed.length > 0) {
|
||||
checkedColumns.value = [...currentChecked, ...missingFixed]
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 监听弹窗打开,触发列配置重新提取
|
||||
watch(visible, (newVal) => {
|
||||
console.log('[TableColumnControl] 弹窗状态变化:', newVal)
|
||||
if (newVal) {
|
||||
// 弹窗打开时,确保选中状态正确初始化
|
||||
if (actualColumns.value.length > 0) {
|
||||
initCheckedColumns()
|
||||
}
|
||||
}
|
||||
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.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 已经有值,立即触发
|
||||
}
|
||||
|
||||
// 暴露给父组件使用
|
||||
defineExpose({
|
||||
isColumnVisible: autoIsColumnVisible,
|
||||
visibleColumns: autoVisibleColumns,
|
||||
refreshColumns: refreshAutoColumns
|
||||
})
|
||||
|
||||
// 获取默认显示的列(所有可隐藏的列)- 用于重置功能
|
||||
const getDefaultColumns = (): string[] => {
|
||||
return actualColumns.value
|
||||
.filter(col => !col.alwaysShow && !col.fixed)
|
||||
.map(col => col.prop || col.label)
|
||||
}
|
||||
|
||||
// 获取所有可选择的列
|
||||
const selectableColumns = computed(() => {
|
||||
return actualColumns.value
|
||||
.filter(col => !col.alwaysShow && !col.fixed)
|
||||
.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) {
|
||||
// 当前全选,执行全不选(但保留固定列和 alwaysShow 列)
|
||||
const fixedAndAlwaysShow = actualColumns.value
|
||||
.filter(col => col.alwaysShow || !!col.fixed)
|
||||
.map(col => col.prop || col.label)
|
||||
checkedColumns.value = [...fixedAndAlwaysShow]
|
||||
} else {
|
||||
// 当前未全选,执行全选(所有列)
|
||||
checkedColumns.value = getAllColumns()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置为默认值
|
||||
const handleReset = () => {
|
||||
checkedColumns.value = getDefaultColumns()
|
||||
handleColumnChange(checkedColumns.value)
|
||||
}
|
||||
|
||||
// 确认
|
||||
const handleConfirm = () => {
|
||||
handleColumnChange(checkedColumns.value)
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 列变化处理
|
||||
const handleColumnChange = (value: string[]) => {
|
||||
// 确保固定列和 alwaysShow 列始终在选中列表中
|
||||
const fixedAndAlwaysShow = actualColumns.value
|
||||
.filter(col => col.alwaysShow || !!col.fixed)
|
||||
.map(col => col.prop || col.label)
|
||||
const finalValue = [...new Set([...value, ...fixedAndAlwaysShow])]
|
||||
|
||||
emit('update:modelValue', finalValue)
|
||||
emit('change', finalValue)
|
||||
|
||||
// 如果使用自动提取,同步更新(只更新可选择的列)
|
||||
if (props.tableRef) {
|
||||
const selectableValue = finalValue.filter(col => {
|
||||
const column = actualColumns.value.find(c => (c.prop || c.label) === col)
|
||||
return column && !column.alwaysShow && !column.fixed
|
||||
})
|
||||
updateAutoVisibleColumns(selectableValue)
|
||||
} else {
|
||||
// 保存到 localStorage(只保存可选择的列)
|
||||
if (props.storageKey) {
|
||||
const selectableValue = finalValue.filter(col => {
|
||||
const column = actualColumns.value.find(c => (c.prop || c.label) === col)
|
||||
return column && !column.alwaysShow && !column.fixed
|
||||
})
|
||||
localStorage.setItem(props.storageKey, JSON.stringify(selectableValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听外部 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>
|
||||
|
||||
|
||||
|
||||
481
src/composables/useTableColumns.ts
Normal file
481
src/composables/useTableColumns.ts
Normal file
@@ -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<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 || []
|
||||
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<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) => {
|
||||
// 尝试多种方式获取 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<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,
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
40
src/directives/v-column-visible.ts
Normal file
40
src/directives/v-column-visible.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -153,7 +153,7 @@ import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { BasicTableProps, useTable } from '/@/hooks/table'
|
||||
import { useMessage } from '/@/hooks/message'
|
||||
import { useMessageBox } from '/@/hooks/message'
|
||||
import { fetchList, addObj, putObj, delObj, getObj } from '/@/api/professional/outercompany'
|
||||
import { fetchList, addObj, putObj, delObj, getObj } from '/@/api/professional/stayschool/outercompany'
|
||||
|
||||
// 使用 Pinia store
|
||||
const userInfoStore = useUserInfo()
|
||||
@@ -153,7 +153,7 @@ import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { BasicTableProps, useTable } from '/@/hooks/table'
|
||||
import { useMessage } from '/@/hooks/message'
|
||||
import { useMessageBox } from '/@/hooks/message'
|
||||
import { fetchList, addObj, putObj, delObj, getObj } from '/@/api/professional/outercompany'
|
||||
import { fetchList, addObj, putObj, delObj, getObj } from '/@/api/professional/stayschool/outercompany'
|
||||
|
||||
// 使用 Pinia store
|
||||
const userInfoStore = useUserInfo()
|
||||
@@ -100,7 +100,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/outercompany'
|
||||
import { fetchList } from '/@/api/professional/stayschool/outercompany'
|
||||
|
||||
// 使用 Pinia store
|
||||
const userInfoStore = useUserInfo()
|
||||
@@ -405,8 +405,8 @@ import {
|
||||
delObj,
|
||||
batchDel,
|
||||
resetPassWord
|
||||
} from '/@/api/professional/outercompanyemployee'
|
||||
import { getList as getCompanyList } from '/@/api/professional/outercompany'
|
||||
} from '/@/api/professional/stayschool/outercompanyemployee'
|
||||
import { getList as getCompanyList } from '/@/api/professional/stayschool/outercompany'
|
||||
|
||||
// 使用 Pinia store
|
||||
const userInfoStore = useUserInfo()
|
||||
@@ -406,8 +406,8 @@ import {
|
||||
delObj,
|
||||
batchDel,
|
||||
resetPassWord
|
||||
} from '/@/api/professional/outercompanyemployee'
|
||||
import { getList as getCompanyList } from '/@/api/professional/outercompany'
|
||||
} from '/@/api/professional/stayschool/outercompanyemployee'
|
||||
import { getList as getCompanyList } from '/@/api/professional/stayschool/outercompany'
|
||||
|
||||
// 使用 Pinia store
|
||||
const userInfoStore = useUserInfo()
|
||||
@@ -385,8 +385,8 @@ import {
|
||||
delObj,
|
||||
batchDel,
|
||||
resetPassWord
|
||||
} from '/@/api/professional/outercompanyemployee'
|
||||
import { getList as getCompanyList } from '/@/api/professional/outercompany'
|
||||
} from '/@/api/professional/stayschool/outercompanyemployee'
|
||||
import { getList as getCompanyList } from '/@/api/professional/stayschool/outercompany'
|
||||
|
||||
// 使用 Pinia store
|
||||
const userInfoStore = useUserInfo()
|
||||
@@ -96,8 +96,8 @@
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Session } from '/@/utils/storage'
|
||||
import { useMessage } from '/@/hooks/message'
|
||||
import { getMyTeacherNo, updateOtherInfo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { putObj } from '/@/api/professional/professionaluser/professionalqualificationrelation'
|
||||
import { getMyTeacherNo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { addObj } from '/@/api/professional/professionaluser/professionalqualificationrelation'
|
||||
import { checkLocked } from '/@/api/professional/professionalstatuslock'
|
||||
import { getLevelList } from '/@/api/professional/rsbase/professionalqualificationconfig'
|
||||
import { getWorkTypeList } from '/@/api/professional/rsbase/professionalworktype'
|
||||
@@ -284,34 +284,26 @@ const dialogSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dataForm.id) {
|
||||
// 编辑:使用 putObj 接口(管理员编辑)
|
||||
dataForm.state = '0'
|
||||
await putObj(dataForm)
|
||||
message.success("修改成功")
|
||||
} else {
|
||||
// 新增:使用 updateOtherInfo 接口(与 MultiDialog 保持一致)
|
||||
const submitData: any = {
|
||||
type: 3, // 职业资格类型
|
||||
teacherNo: dataForm.teacherNo,
|
||||
worker: dataForm.worker,
|
||||
qualificationConfigId: dataForm.qualificationConfigId,
|
||||
certificateTime: dataForm.certificateTime,
|
||||
certificateNumber: dataForm.certificateNumber,
|
||||
mateA: dataForm.evidenceA || dataForm.materialA // 使用 mateA 字段(与 MultiDialog 一致)
|
||||
// 统一使用 addObj 接口(新增和编辑都使用同一个接口)
|
||||
// 确保 evidenceA 或 materialA 有值
|
||||
if (!dataForm.evidenceA && dataForm.materialA) {
|
||||
dataForm.evidenceA = dataForm.materialA
|
||||
}
|
||||
|
||||
const res = await updateOtherInfo(submitData)
|
||||
if (res.data == '-1') {
|
||||
message.warning("当前不允许提交")
|
||||
if (dataForm.id) {
|
||||
// 编辑模式
|
||||
dataForm.state = '0'
|
||||
await addObj(dataForm)
|
||||
message.success("修改成功")
|
||||
} else {
|
||||
// 新增模式
|
||||
await addObj(dataForm)
|
||||
message.success("提交成功")
|
||||
}
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('refreshData')
|
||||
} catch (error: any) {
|
||||
message.error(error?.msg || '操作失败')
|
||||
// 错误处理已在数据请求层统一处理,此处不需要提示
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -147,8 +147,8 @@
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Session } from '/@/utils/storage'
|
||||
import { useMessage } from '/@/hooks/message'
|
||||
import { getMyTeacherNo, updateOtherInfo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { putObj } from '/@/api/professional/professionaluser/professionalteacheracademicrelation'
|
||||
import { getMyTeacherNo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { addObj } from '/@/api/professional/professionaluser/professionalteacheracademicrelation'
|
||||
import { getAllTypeList } from '/@/api/professional/rsbase/professionalacademiceducationtypeconfig'
|
||||
import { getQualificationList } from '/@/api/professional/rsbase/academicqualificationsconfig'
|
||||
import { getDegreeList } from '/@/api/professional/rsbase/professionalacademicdegreeconfig'
|
||||
@@ -370,42 +370,30 @@ const dialogSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
// 统一使用 addObj 接口(新增和编辑都使用同一个接口)
|
||||
// 确保 qualificationImg 或 materialA 有值
|
||||
if (!dataForm.qualificationImg && dataForm.materialA) {
|
||||
dataForm.qualificationImg = dataForm.materialA
|
||||
}
|
||||
// 确保 degreeImg 或 materialB 有值
|
||||
if (!dataForm.degreeImg && dataForm.materialB) {
|
||||
dataForm.degreeImg = dataForm.materialB
|
||||
}
|
||||
|
||||
if (dataForm.id) {
|
||||
// 编辑:使用 putObj 接口(管理员编辑)
|
||||
// 编辑模式
|
||||
dataForm.state = '0'
|
||||
await putObj(dataForm)
|
||||
await addObj(dataForm)
|
||||
message.success("修改成功")
|
||||
} else {
|
||||
// 新增:使用 updateOtherInfo 接口(与 MultiDialog 保持一致)
|
||||
// 注意:MultiDialog 中 type 字段在提交时会被设置为 val(1),但表单中也有 type 字段用于教育类型
|
||||
// 这里直接使用 dataForm 的所有字段,后端应该能够处理
|
||||
const submitData: any = {
|
||||
type: 1, // 学历更新类型(固定值,会覆盖表单中的 type)
|
||||
teacherNo: dataForm.teacherNo,
|
||||
graduateTime: dataForm.graduateTime,
|
||||
qualificationConfigId: dataForm.qualificationConfigId,
|
||||
degreeConfigId: dataForm.degreeConfigId,
|
||||
graduateSchool: dataForm.graduateSchool,
|
||||
major: dataForm.major,
|
||||
certificateNumber: dataForm.certificateNumber,
|
||||
mateA: dataForm.qualificationImg || dataForm.materialA, // 学历证书
|
||||
mateB: dataForm.degreeImg || dataForm.materialB // 学位证书
|
||||
}
|
||||
|
||||
// 注意:MultiDialog 中教育类型字段也是 type,但在提交时会被覆盖为 val(1)
|
||||
// 如果后端需要教育类型,可能需要单独传递,这里先不传,保持与 MultiDialog 一致
|
||||
|
||||
const res = await updateOtherInfo(submitData)
|
||||
if (res.data == '-1') {
|
||||
message.warning("当前不允许提交")
|
||||
} else {
|
||||
// 新增模式
|
||||
await addObj(dataForm)
|
||||
message.success("提交成功")
|
||||
}
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('refreshData')
|
||||
} catch (error: any) {
|
||||
message.error(error?.msg || '操作失败')
|
||||
// 错误处理已在数据请求层统一处理,此处不需要提示
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Session } from '/@/utils/storage'
|
||||
import { useMessage } from '/@/hooks/message'
|
||||
import { getMyTeacherNo, updateOtherInfo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { putObj } from '/@/api/professional/professionaluser/professionalteachercertificaterelation'
|
||||
import { getMyTeacherNo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { addObj } from '/@/api/professional/professionaluser/professionalteachercertificaterelation'
|
||||
import { getTeacherCertificateList } from '/@/api/professional/rsbase/professionalteachercertificateconf'
|
||||
import { checkLocked } from '/@/api/professional/professionalstatuslock'
|
||||
|
||||
@@ -280,33 +280,30 @@ const dialogSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dataForm.id) {
|
||||
// 编辑:使用 putObj 接口(管理员编辑)
|
||||
dataForm.state = '0'
|
||||
await putObj(dataForm)
|
||||
message.success("修改成功")
|
||||
} else {
|
||||
// 新增:使用 updateOtherInfo 接口(与 MultiDialog 保持一致)
|
||||
// 注意:MultiDialog 的 type=0 表单只有 certificateConfId 和 certificateNumber,没有 certificateTime
|
||||
const submitData: any = {
|
||||
type: 0, // 教师资格证类型
|
||||
teacherNo: dataForm.teacherNo,
|
||||
certificateConfId: dataForm.certificateConfId,
|
||||
certificateNumber: dataForm.certificateNumber,
|
||||
mateA: dataForm.evidenceA || dataForm.materialA // 使用 mateA 字段(与 MultiDialog 一致)
|
||||
// 统一使用 addObj 接口(新增和编辑都使用同一个接口)
|
||||
// 确保 evidenceA 或 materialA 有值
|
||||
if (!dataForm.evidenceA && dataForm.materialA) {
|
||||
dataForm.evidenceA = dataForm.materialA
|
||||
}
|
||||
// 确保 evidenceB 或 materialB 有值
|
||||
if (!dataForm.evidenceB && dataForm.materialB) {
|
||||
dataForm.evidenceB = dataForm.materialB
|
||||
}
|
||||
|
||||
const res = await updateOtherInfo(submitData)
|
||||
if (res.data == '-1') {
|
||||
message.warning("当前不允许提交")
|
||||
if (dataForm.id) {
|
||||
// 编辑模式
|
||||
dataForm.state = '0'
|
||||
await addObj(dataForm)
|
||||
message.success("修改成功")
|
||||
} else {
|
||||
// 新增模式
|
||||
await addObj(dataForm)
|
||||
message.success("提交成功")
|
||||
}
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('refreshData')
|
||||
} catch (error: any) {
|
||||
message.error(error?.msg || '操作失败')
|
||||
// 错误处理已在数据请求层统一处理,此处不需要提示
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Session } from '/@/utils/storage'
|
||||
import { useMessage } from '/@/hooks/message'
|
||||
import { getMyTeacherNo, updateOtherInfo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { putObj } from '/@/api/professional/professionaluser/professionalteacherhonor'
|
||||
import { getMyTeacherNo } from '/@/api/professional/professionaluser/teacherbase'
|
||||
import { addObj } from '/@/api/professional/professionaluser/professionalteacherhonor'
|
||||
import { checkLocked } from '/@/api/professional/professionalstatuslock'
|
||||
|
||||
// Emits
|
||||
@@ -215,33 +215,26 @@ const dialogSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dataForm.id) {
|
||||
// 编辑:使用 putObj 接口(管理员编辑)
|
||||
dataForm.state = '0'
|
||||
await putObj(dataForm)
|
||||
message.success("修改成功")
|
||||
} else {
|
||||
// 新增:使用 updateOtherInfo 接口(与 MultiDialog 保持一致)
|
||||
const submitData: any = {
|
||||
type: 4, // 综合表彰类型
|
||||
teacherNo: dataForm.teacherNo,
|
||||
honor: dataForm.honor,
|
||||
honorCompany: dataForm.honorCompany,
|
||||
year: dataForm.year,
|
||||
mateA: dataForm.attachment || dataForm.materialA // 使用 mateA 字段(与 MultiDialog 一致)
|
||||
// 统一使用 addObj 接口(新增和编辑都使用同一个接口)
|
||||
// 确保 attachment 或 materialA 有值
|
||||
if (!dataForm.attachment && dataForm.materialA) {
|
||||
dataForm.attachment = dataForm.materialA
|
||||
}
|
||||
|
||||
const res = await updateOtherInfo(submitData)
|
||||
if (res.data == '-1') {
|
||||
message.warning("当前不允许提交")
|
||||
if (dataForm.id) {
|
||||
// 编辑模式
|
||||
dataForm.state = '0'
|
||||
await addObj(dataForm)
|
||||
message.success("修改成功")
|
||||
} else {
|
||||
// 新增模式
|
||||
await addObj(dataForm)
|
||||
message.success("提交成功")
|
||||
}
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('refreshData')
|
||||
} catch (error: any) {
|
||||
message.error(error?.msg || '操作失败')
|
||||
// 错误处理已在数据请求层统一处理,此处不需要提示
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ const openDialog = async (row?: any) => {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
// 错误处理已在数据请求层统一处理,此处不需要提示
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -306,7 +306,7 @@ const dialogSubmit = async () => {
|
||||
dialogVisible.value = false
|
||||
emit('refreshData')
|
||||
} catch (error: any) {
|
||||
message.error(error?.msg || '操作失败')
|
||||
// 错误处理已在数据请求层统一处理,此处不需要提示
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
91
src/views/professional/teacherbase/action-dropdown.vue
Normal file
91
src/views/professional/teacherbase/action-dropdown.vue
Normal 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
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
v-model="form.joinTime"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -26,8 +26,8 @@
|
||||
v-model="form.correctionTime"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="社会关系" width="600px" @close="handleClose">
|
||||
<el-form :model="form" label-width="120px" ref="formRef">
|
||||
<el-form-item label="称谓" required>
|
||||
<el-form :model="form" :rules="rules" label-width="130px" ref="formRef">
|
||||
<el-form-item label="称谓" prop="title" required>
|
||||
<el-select v-model="form.title" placeholder="请选择称谓" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in titleOptions"
|
||||
@@ -11,20 +11,20 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" required>
|
||||
<el-form-item label="姓名" prop="realName" required>
|
||||
<el-input v-model="form.realName" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="出生年月" required>
|
||||
<el-form-item label="出生年月" prop="birthday" required>
|
||||
<el-date-picker
|
||||
v-model="form.birthday"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
format="yyyy-MM-dd"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="政治面貌">
|
||||
<el-form-item label="政治面貌" prop="politicsStatusId" required>
|
||||
<el-select v-model="form.politicsStatusId" placeholder="请选择政治面貌" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in politicsStatusList"
|
||||
@@ -34,7 +34,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="工作单位及职务">
|
||||
<el-form-item label="工作单位及职务" prop="workStation" required>
|
||||
<el-input
|
||||
v-model="form.workStation"
|
||||
type="textarea"
|
||||
@@ -104,8 +104,31 @@ const handleClose = () => {
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
formRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
emit('submit', { ...form.value })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
title: [
|
||||
{ required: true, message: '请选择称谓', trigger: 'change' }
|
||||
],
|
||||
realName: [
|
||||
{ required: true, message: '请输入姓名', trigger: 'blur' }
|
||||
],
|
||||
birthday: [
|
||||
{ required: true, message: '请选择出生年月', trigger: 'change' }
|
||||
],
|
||||
politicsStatusId: [
|
||||
{ required: true, message: '请选择政治面貌', trigger: 'change' }
|
||||
],
|
||||
workStation: [
|
||||
{ required: true, message: '请输入工作单位及职务', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user