vue2server7 7.0.108 → 7.0.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/superpowers/plans/2026-05-13-table-column-config-demo-plan.md +275 -0
- package/docs/superpowers/plans/2026-05-13-table-column-settings-plan.md +443 -0
- package/docs/superpowers/specs/2026-05-13-table-column-config-demo-design.md +74 -0
- package/docs/superpowers/specs/2026-05-13-table-column-settings-component.md +69 -0
- package/frontEnd/src/components/TableColumnSettings.vue +296 -0
- package/frontEnd/src/pages/ColumnConfigDemoPage.vue +166 -0
- package/frontEnd/src/router/routes.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# 表格列配置演示页面 Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** 在前端项目中新增一个"表格列配置演示"路由页面,提供基础的表格框架和预留列配置扩展点
|
|
6
|
+
|
|
7
|
+
**Architecture:** 遵循现有项目结构,创建新的 Vue 页面组件并添加路由配置,保持与现有页面一致的代码风格
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Vue 3 + Composition API + TypeScript + Element Plus
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 文件结构
|
|
14
|
+
|
|
15
|
+
| 文件 | 操作 | 说明 |
|
|
16
|
+
|------|------|------|
|
|
17
|
+
| `frontEnd/src/pages/ColumnConfigDemoPage.vue` | 创建 | 表格列配置演示页面 |
|
|
18
|
+
| `frontEnd/src/router/routes.js` | 修改 | 添加新路由配置 |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: 创建列配置演示页面组件
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Create: `frontEnd/src/pages/ColumnConfigDemoPage.vue`
|
|
26
|
+
|
|
27
|
+
- [ ] **Step 1: 创建基础页面模板和脚本**
|
|
28
|
+
|
|
29
|
+
```vue
|
|
30
|
+
<template>
|
|
31
|
+
<section class="page column-config-demo-page">
|
|
32
|
+
<h1 class="title">表格列配置演示</h1>
|
|
33
|
+
|
|
34
|
+
<div class="toolbar">
|
|
35
|
+
<el-button type="primary">查询</el-button>
|
|
36
|
+
<el-button>重置</el-button>
|
|
37
|
+
|
|
38
|
+
<!-- 列设置按钮 - 预留位置,用户自行实现弹窗逻辑 -->
|
|
39
|
+
<el-popover placement="bottom" trigger="click" width="300">
|
|
40
|
+
<template #reference>
|
|
41
|
+
<el-button>列设置</el-button>
|
|
42
|
+
</template>
|
|
43
|
+
<!-- 列配置内容区域 - 用户自行开发 -->
|
|
44
|
+
<div class="column-settings-panel">
|
|
45
|
+
<p>列配置区域</p>
|
|
46
|
+
<p>请在此处实现列配置功能</p>
|
|
47
|
+
</div>
|
|
48
|
+
</el-popover>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<el-table
|
|
52
|
+
:key="tableKey"
|
|
53
|
+
:data="tableData"
|
|
54
|
+
border
|
|
55
|
+
stripe
|
|
56
|
+
size="small"
|
|
57
|
+
style="width: 100%"
|
|
58
|
+
>
|
|
59
|
+
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
60
|
+
<el-table-column
|
|
61
|
+
v-for="col in visibleColumns"
|
|
62
|
+
:key="col.key"
|
|
63
|
+
:prop="col.prop"
|
|
64
|
+
:label="col.label"
|
|
65
|
+
:width="col.width"
|
|
66
|
+
:min-width="col.minWidth"
|
|
67
|
+
:align="col.align || 'left'"
|
|
68
|
+
:fixed="col.fixed || false"
|
|
69
|
+
>
|
|
70
|
+
</el-table-column>
|
|
71
|
+
</el-table>
|
|
72
|
+
|
|
73
|
+
<div class="pagination">
|
|
74
|
+
<el-pagination
|
|
75
|
+
layout="total, prev, pager, next"
|
|
76
|
+
:current-page="page"
|
|
77
|
+
:page-size="pageSize"
|
|
78
|
+
:total="total"
|
|
79
|
+
@current-change="onPageChange"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<script setup lang="ts">
|
|
86
|
+
import { ref, reactive, computed, onMounted } from 'vue'
|
|
87
|
+
|
|
88
|
+
// 列配置接口
|
|
89
|
+
interface ColumnConfig {
|
|
90
|
+
key: string
|
|
91
|
+
prop: string
|
|
92
|
+
label: string
|
|
93
|
+
width?: number
|
|
94
|
+
minWidth?: number
|
|
95
|
+
align?: 'left' | 'center' | 'right'
|
|
96
|
+
fixed?: boolean | 'left' | 'right'
|
|
97
|
+
visible?: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 表格数据接口
|
|
101
|
+
interface TableRow {
|
|
102
|
+
id: number
|
|
103
|
+
name: string
|
|
104
|
+
department: string
|
|
105
|
+
position: string
|
|
106
|
+
phone: string
|
|
107
|
+
email: string
|
|
108
|
+
joinDate: string
|
|
109
|
+
status: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 响应式数据
|
|
113
|
+
const tableKey = ref(0)
|
|
114
|
+
const page = ref(1)
|
|
115
|
+
const pageSize = ref(10)
|
|
116
|
+
const total = ref(50)
|
|
117
|
+
const tableData = ref<TableRow[]>([])
|
|
118
|
+
|
|
119
|
+
// 列配置 - 可在此基础上扩展更多属性
|
|
120
|
+
const columns = reactive<ColumnConfig[]>([
|
|
121
|
+
{ key: 'name', prop: 'name', label: '姓名', width: 100 },
|
|
122
|
+
{ key: 'department', prop: 'department', label: '部门', width: 120 },
|
|
123
|
+
{ key: 'position', prop: 'position', label: '职位', width: 120 },
|
|
124
|
+
{ key: 'phone', prop: 'phone', label: '电话', width: 140 },
|
|
125
|
+
{ key: 'email', prop: 'email', label: '邮箱', minWidth: 180 },
|
|
126
|
+
{ key: 'joinDate', prop: 'joinDate', label: '入职日期', width: 120, align: 'center' },
|
|
127
|
+
{ key: 'status', prop: 'status', label: '状态', width: 100, align: 'center' }
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
// 可见列计算 - 预留,用户可自行控制显示/隐藏
|
|
131
|
+
const visibleColumns = computed(() => {
|
|
132
|
+
return columns.filter(col => col.visible !== false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// 分页切换
|
|
136
|
+
const onPageChange = (p: number) => {
|
|
137
|
+
page.value = p
|
|
138
|
+
loadData()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 加载模拟数据
|
|
142
|
+
const loadData = () => {
|
|
143
|
+
const data: TableRow[] = []
|
|
144
|
+
for (let i = 0; i < pageSize.value; i++) {
|
|
145
|
+
const index = (page.value - 1) * pageSize.value + i + 1
|
|
146
|
+
data.push({
|
|
147
|
+
id: index,
|
|
148
|
+
name: `员工${index}`,
|
|
149
|
+
department: ['技术部', '产品部', '运营部', '市场部'][index % 4],
|
|
150
|
+
position: ['开发工程师', '产品经理', '运营专员', '市场专员'][index % 4],
|
|
151
|
+
phone: `138${String(index).padStart(8, '0')}`,
|
|
152
|
+
email: `employee${index}@example.com`,
|
|
153
|
+
joinDate: `2024-${String((index % 12) + 1).padStart(2, '0')}-${String((index % 28) + 1).padStart(2, '0')}`,
|
|
154
|
+
status: ['在职', '离职', '休假'][index % 3]
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
tableData.value = data
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
loadData()
|
|
162
|
+
})
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<style scoped>
|
|
166
|
+
.page.column-config-demo-page {
|
|
167
|
+
padding: 16px;
|
|
168
|
+
}
|
|
169
|
+
.title {
|
|
170
|
+
font-size: 18px;
|
|
171
|
+
margin-bottom: 12px;
|
|
172
|
+
}
|
|
173
|
+
.toolbar {
|
|
174
|
+
margin-bottom: 12px;
|
|
175
|
+
}
|
|
176
|
+
.pagination {
|
|
177
|
+
margin-top: 12px;
|
|
178
|
+
text-align: right;
|
|
179
|
+
}
|
|
180
|
+
.column-settings-panel {
|
|
181
|
+
padding: 8px 0;
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- [ ] **Step 2: 验证文件创建成功**
|
|
187
|
+
|
|
188
|
+
检查文件是否存在:
|
|
189
|
+
```bash
|
|
190
|
+
ls -la frontEnd/src/pages/ColumnConfigDemoPage.vue
|
|
191
|
+
```
|
|
192
|
+
Expected: 文件存在,大小约 3-4KB
|
|
193
|
+
|
|
194
|
+
- [ ] **Step 3: 提交**
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git add frontEnd/src/pages/ColumnConfigDemoPage.vue
|
|
198
|
+
git commit -m "feat: add column config demo page skeleton"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Task 2: 添加路由配置
|
|
204
|
+
|
|
205
|
+
**Files:**
|
|
206
|
+
- Modify: `frontEnd/src/router/routes.js:8-87`
|
|
207
|
+
|
|
208
|
+
- [ ] **Step 1: 修改 routes.js 添加导入和路由配置**
|
|
209
|
+
|
|
210
|
+
在文件顶部导入语句区域添加(第8行后):
|
|
211
|
+
```javascript
|
|
212
|
+
import ColumnConfigDemoPage from '../pages/ColumnConfigDemoPage.vue'
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
在 routes 数组末尾添加新路由(第86行前):
|
|
216
|
+
```javascript
|
|
217
|
+
{
|
|
218
|
+
path: '/column-config-demo',
|
|
219
|
+
name: 'ColumnConfigDemo',
|
|
220
|
+
component: ColumnConfigDemoPage,
|
|
221
|
+
meta: {
|
|
222
|
+
title: '表格列配置演示',
|
|
223
|
+
showInMenu: true
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- [ ] **Step 2: 验证路由配置**
|
|
229
|
+
|
|
230
|
+
检查文件内容:
|
|
231
|
+
```bash
|
|
232
|
+
grep -n "ColumnConfigDemo" frontEnd/src/router/routes.js
|
|
233
|
+
```
|
|
234
|
+
Expected: 至少两行输出(导入语句和路由配置)
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 3: 提交**
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
git add frontEnd/src/router/routes.js
|
|
240
|
+
git commit -m "feat: add column config demo route"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### Task 3: 验证页面功能
|
|
246
|
+
|
|
247
|
+
**Files:**
|
|
248
|
+
- Test: 手动运行前端项目验证
|
|
249
|
+
|
|
250
|
+
- [ ] **Step 1: 启动前端开发服务器**
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm run front-dev
|
|
254
|
+
```
|
|
255
|
+
Expected: Vite 服务器启动成功,无编译错误
|
|
256
|
+
|
|
257
|
+
- [ ] **Step 2: 手动验证**
|
|
258
|
+
1. 浏览器访问页面
|
|
259
|
+
2. 检查侧边栏菜单是否显示"表格列配置演示"
|
|
260
|
+
3. 点击菜单项,页面是否正常加载
|
|
261
|
+
4. 检查表格数据是否显示
|
|
262
|
+
5. 检查分页是否正常工作
|
|
263
|
+
6. 检查"列设置"按钮是否可点击
|
|
264
|
+
|
|
265
|
+
- [ ] **Step 3: 提交验证完成(无代码变更)**
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 计划完成检查清单
|
|
270
|
+
|
|
271
|
+
- [x] 所有文件路径正确
|
|
272
|
+
- [x] 代码示例完整无占位符
|
|
273
|
+
- [x] 命令和预期输出明确
|
|
274
|
+
- [x] 与现有项目风格一致
|
|
275
|
+
- [x] 预留了用户自定义扩展点
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# 表格列配置组件 Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** 集成 TableColumnSettings 列配置组件到项目中,并在演示页面使用
|
|
6
|
+
|
|
7
|
+
**Architecture:** 创建独立组件文件,在演示页面中引入,实现动态列显示/隐藏功能
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Vue 3 + Composition API + Element Plus
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 文件结构
|
|
14
|
+
|
|
15
|
+
| 文件 | 操作 | 说明 |
|
|
16
|
+
|------|------|------|
|
|
17
|
+
| `frontEnd/src/components/TableColumnSettings.vue` | 创建 | 表格列配置组件 |
|
|
18
|
+
| `frontEnd/src/pages/ColumnConfigDemoPage.vue` | 修改/创建 | 演示页面集成组件 |
|
|
19
|
+
| `frontEnd/src/router/routes.js` | 修改 | 添加路由(如未添加) |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### Task 1: 创建 TableColumnSettings 组件
|
|
24
|
+
|
|
25
|
+
**Files:**
|
|
26
|
+
- Create: `frontEnd/src/components/TableColumnSettings.vue`
|
|
27
|
+
|
|
28
|
+
- [ ] **Step 1: 创建组件文件**
|
|
29
|
+
|
|
30
|
+
```vue
|
|
31
|
+
<template>
|
|
32
|
+
<el-button @click="visible = true">
|
|
33
|
+
<el-icon><Setting /></el-icon>
|
|
34
|
+
列配置
|
|
35
|
+
</el-button>
|
|
36
|
+
|
|
37
|
+
<el-dialog
|
|
38
|
+
v-model="visible"
|
|
39
|
+
title="表格列配置"
|
|
40
|
+
width="760px"
|
|
41
|
+
append-to-body
|
|
42
|
+
>
|
|
43
|
+
<div class="desc">请选择需要在表格中显示的数据列</div>
|
|
44
|
+
|
|
45
|
+
<div class="column-box">
|
|
46
|
+
<div class="check-all-row">
|
|
47
|
+
<el-checkbox
|
|
48
|
+
v-model="checkAll"
|
|
49
|
+
:indeterminate="isIndeterminate"
|
|
50
|
+
@change="handleCheckAllChange"
|
|
51
|
+
>
|
|
52
|
+
全选
|
|
53
|
+
</el-checkbox>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<el-checkbox-group
|
|
57
|
+
v-model="checkedKeys"
|
|
58
|
+
class="column-list"
|
|
59
|
+
@change="updateCheckAllStatus"
|
|
60
|
+
>
|
|
61
|
+
<el-checkbox
|
|
62
|
+
v-for="item in columns"
|
|
63
|
+
:key="item.prop"
|
|
64
|
+
:label="item.prop"
|
|
65
|
+
:disabled="item.disabled"
|
|
66
|
+
>
|
|
67
|
+
{{ item.label }}
|
|
68
|
+
</el-checkbox>
|
|
69
|
+
</el-checkbox-group>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<template #footer>
|
|
73
|
+
<el-button @click="visible = false">取消</el-button>
|
|
74
|
+
<el-button type="primary" @click="handleConfirm">
|
|
75
|
+
确认
|
|
76
|
+
</el-button>
|
|
77
|
+
</template>
|
|
78
|
+
</el-dialog>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup>
|
|
82
|
+
import { ref, computed, watch } from 'vue'
|
|
83
|
+
import { Setting } from '@element-plus/icons-vue'
|
|
84
|
+
|
|
85
|
+
const props = defineProps({
|
|
86
|
+
columns: {
|
|
87
|
+
type: Array,
|
|
88
|
+
default: () => []
|
|
89
|
+
},
|
|
90
|
+
selectedKeys: {
|
|
91
|
+
type: Array,
|
|
92
|
+
default: () => []
|
|
93
|
+
},
|
|
94
|
+
buttonText: {
|
|
95
|
+
type: String,
|
|
96
|
+
default: '列配置'
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const emit = defineEmits(['confirm'])
|
|
101
|
+
|
|
102
|
+
const visible = ref(false)
|
|
103
|
+
const checkedKeys = ref([])
|
|
104
|
+
const checkAll = ref(false)
|
|
105
|
+
const isIndeterminate = ref(false)
|
|
106
|
+
|
|
107
|
+
const enabledKeys = computed(() => {
|
|
108
|
+
return props.columns
|
|
109
|
+
.filter(item => !item.disabled)
|
|
110
|
+
.map(item => item.prop)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
watch(visible, val => {
|
|
114
|
+
if (val) {
|
|
115
|
+
checkedKeys.value = [...props.selectedKeys]
|
|
116
|
+
updateCheckAllStatus()
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
function handleCheckAllChange(val) {
|
|
121
|
+
const disabledCheckedKeys = props.columns
|
|
122
|
+
.filter(item => item.disabled && checkedKeys.value.includes(item.prop))
|
|
123
|
+
.map(item => item.prop)
|
|
124
|
+
|
|
125
|
+
checkedKeys.value = val
|
|
126
|
+
? [...new Set([...disabledCheckedKeys, ...enabledKeys.value])]
|
|
127
|
+
: disabledCheckedKeys
|
|
128
|
+
|
|
129
|
+
updateCheckAllStatus()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function updateCheckAllStatus() {
|
|
133
|
+
const count = checkedKeys.value.filter(key =>
|
|
134
|
+
enabledKeys.value.includes(key)
|
|
135
|
+
).length
|
|
136
|
+
|
|
137
|
+
checkAll.value = count === enabledKeys.value.length
|
|
138
|
+
isIndeterminate.value = count > 0 && count < enabledKeys.value.length
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleConfirm() {
|
|
142
|
+
emit('confirm', checkedKeys.value)
|
|
143
|
+
visible.value = false
|
|
144
|
+
}
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<style scoped>
|
|
148
|
+
.desc {
|
|
149
|
+
margin-bottom: 22px;
|
|
150
|
+
color: #666;
|
|
151
|
+
font-size: 16px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.column-box {
|
|
155
|
+
border: 1px solid #dcdfe6;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.check-all-row {
|
|
159
|
+
padding: 18px 24px;
|
|
160
|
+
border-bottom: 1px solid #dcdfe6;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.column-list {
|
|
164
|
+
padding: 22px 24px;
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-wrap: wrap;
|
|
167
|
+
gap: 22px 28px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
:deep(.el-checkbox) {
|
|
171
|
+
margin-right: 0;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- [ ] **Step 2: 验证文件创建成功**
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
ls -la frontEnd/src/components/TableColumnSettings.vue
|
|
180
|
+
```
|
|
181
|
+
Expected: 文件存在
|
|
182
|
+
|
|
183
|
+
- [ ] **Step 3: 提交**
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
git add frontEnd/src/components/TableColumnSettings.vue
|
|
187
|
+
git commit -m "feat: add TableColumnSettings component"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Task 2: 创建/更新 ColumnConfigDemoPage 页面
|
|
193
|
+
|
|
194
|
+
**Files:**
|
|
195
|
+
- Create/Modify: `frontEnd/src/pages/ColumnConfigDemoPage.vue`
|
|
196
|
+
|
|
197
|
+
- [ ] **Step 1: 创建完整的演示页面(如果文件不存在则创建)**
|
|
198
|
+
|
|
199
|
+
```vue
|
|
200
|
+
<template>
|
|
201
|
+
<section class="page column-config-demo-page">
|
|
202
|
+
<h1 class="title">表格列配置演示</h1>
|
|
203
|
+
|
|
204
|
+
<div class="toolbar">
|
|
205
|
+
<el-button type="primary">查询</el-button>
|
|
206
|
+
<el-button>重置</el-button>
|
|
207
|
+
|
|
208
|
+
<!-- 列配置组件 -->
|
|
209
|
+
<TableColumnSettings
|
|
210
|
+
:columns="columnSettings"
|
|
211
|
+
:selected-keys="selectedKeys"
|
|
212
|
+
@confirm="onColumnConfirm"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<el-table
|
|
217
|
+
:key="tableKey"
|
|
218
|
+
:data="tableData"
|
|
219
|
+
border
|
|
220
|
+
stripe
|
|
221
|
+
size="small"
|
|
222
|
+
style="width: 100%"
|
|
223
|
+
>
|
|
224
|
+
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
225
|
+
<el-table-column
|
|
226
|
+
v-for="col in visibleColumns"
|
|
227
|
+
:key="col.prop"
|
|
228
|
+
:prop="col.prop"
|
|
229
|
+
:label="col.label"
|
|
230
|
+
:width="col.width"
|
|
231
|
+
:min-width="col.minWidth"
|
|
232
|
+
:align="col.align || 'left'"
|
|
233
|
+
:fixed="col.fixed || false"
|
|
234
|
+
>
|
|
235
|
+
</el-table-column>
|
|
236
|
+
</el-table>
|
|
237
|
+
|
|
238
|
+
<div class="pagination">
|
|
239
|
+
<el-pagination
|
|
240
|
+
layout="total, prev, pager, next"
|
|
241
|
+
:current-page="page"
|
|
242
|
+
:page-size="pageSize"
|
|
243
|
+
:total="total"
|
|
244
|
+
@current-change="onPageChange"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
</section>
|
|
248
|
+
</template>
|
|
249
|
+
|
|
250
|
+
<script setup lang="ts">
|
|
251
|
+
import { ref, reactive, computed, onMounted } from 'vue'
|
|
252
|
+
import TableColumnSettings from '../components/TableColumnSettings.vue'
|
|
253
|
+
|
|
254
|
+
// 表格数据接口
|
|
255
|
+
interface TableRow {
|
|
256
|
+
id: number
|
|
257
|
+
name: string
|
|
258
|
+
department: string
|
|
259
|
+
position: string
|
|
260
|
+
phone: string
|
|
261
|
+
email: string
|
|
262
|
+
joinDate: string
|
|
263
|
+
status: string
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 响应式数据
|
|
267
|
+
const tableKey = ref(0)
|
|
268
|
+
const page = ref(1)
|
|
269
|
+
const pageSize = ref(10)
|
|
270
|
+
const total = ref(50)
|
|
271
|
+
const tableData = ref<TableRow[]>([])
|
|
272
|
+
|
|
273
|
+
// 列配置 - 用于表格渲染
|
|
274
|
+
const columns = reactive([
|
|
275
|
+
{ key: 'name', prop: 'name', label: '姓名', width: 100 },
|
|
276
|
+
{ key: 'department', prop: 'department', label: '部门', width: 120 },
|
|
277
|
+
{ key: 'position', prop: 'position', label: '职位', width: 120 },
|
|
278
|
+
{ key: 'phone', prop: 'phone', label: '电话', width: 140 },
|
|
279
|
+
{ key: 'email', prop: 'email', label: '邮箱', minWidth: 180 },
|
|
280
|
+
{ key: 'joinDate', prop: 'joinDate', label: '入职日期', width: 120, align: 'center' },
|
|
281
|
+
{ key: 'status', prop: 'status', label: '状态', width: 100, align: 'center' }
|
|
282
|
+
])
|
|
283
|
+
|
|
284
|
+
// 列配置 - 用于列配置组件
|
|
285
|
+
const columnSettings = computed(() => {
|
|
286
|
+
return columns.map(col => ({
|
|
287
|
+
prop: col.prop,
|
|
288
|
+
label: col.label,
|
|
289
|
+
disabled: col.prop === 'name' // 姓名列强制显示,不可取消
|
|
290
|
+
}))
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// 当前选中的列
|
|
294
|
+
const selectedKeys = ref<string[]>([])
|
|
295
|
+
|
|
296
|
+
// 可见列
|
|
297
|
+
const visibleColumns = computed(() => {
|
|
298
|
+
return columns.filter(col => selectedKeys.value.includes(col.prop))
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// 列配置确认
|
|
302
|
+
const onColumnConfirm = (keys: string[]) => {
|
|
303
|
+
selectedKeys.value = keys
|
|
304
|
+
tableKey.value += 1 // 强制刷新表格
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 分页切换
|
|
308
|
+
const onPageChange = (p: number) => {
|
|
309
|
+
page.value = p
|
|
310
|
+
loadData()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 加载模拟数据
|
|
314
|
+
const loadData = () => {
|
|
315
|
+
const data: TableRow[] = []
|
|
316
|
+
for (let i = 0; i < pageSize.value; i++) {
|
|
317
|
+
const index = (page.value - 1) * pageSize.value + i + 1
|
|
318
|
+
data.push({
|
|
319
|
+
id: index,
|
|
320
|
+
name: `员工${index}`,
|
|
321
|
+
department: ['技术部', '产品部', '运营部', '市场部'][index % 4],
|
|
322
|
+
position: ['开发工程师', '产品经理', '运营专员', '市场专员'][index % 4],
|
|
323
|
+
phone: `138${String(index).padStart(8, '0')}`,
|
|
324
|
+
email: `employee${index}@example.com`,
|
|
325
|
+
joinDate: `2024-${String((index % 12) + 1).padStart(2, '0')}-${String((index % 28) + 1).padStart(2, '0')}`,
|
|
326
|
+
status: ['在职', '离职', '休假'][index % 3]
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
tableData.value = data
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
onMounted(() => {
|
|
333
|
+
// 默认选中所有列
|
|
334
|
+
selectedKeys.value = columns.map(col => col.prop)
|
|
335
|
+
loadData()
|
|
336
|
+
})
|
|
337
|
+
</script>
|
|
338
|
+
|
|
339
|
+
<style scoped>
|
|
340
|
+
.page.column-config-demo-page {
|
|
341
|
+
padding: 16px;
|
|
342
|
+
}
|
|
343
|
+
.title {
|
|
344
|
+
font-size: 18px;
|
|
345
|
+
margin-bottom: 12px;
|
|
346
|
+
}
|
|
347
|
+
.toolbar {
|
|
348
|
+
margin-bottom: 12px;
|
|
349
|
+
}
|
|
350
|
+
.pagination {
|
|
351
|
+
margin-top: 12px;
|
|
352
|
+
text-align: right;
|
|
353
|
+
}
|
|
354
|
+
</style>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
- [ ] **Step 2: 验证文件**
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
ls -la frontEnd/src/pages/ColumnConfigDemoPage.vue
|
|
361
|
+
```
|
|
362
|
+
Expected: 文件存在
|
|
363
|
+
|
|
364
|
+
- [ ] **Step 3: 提交**
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
git add frontEnd/src/pages/ColumnConfigDemoPage.vue
|
|
368
|
+
git commit -m "feat: update ColumnConfigDemoPage with TableColumnSettings"
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
### Task 3: 添加路由配置(如未添加)
|
|
374
|
+
|
|
375
|
+
**Files:**
|
|
376
|
+
- Modify: `frontEnd/src/router/routes.js`
|
|
377
|
+
|
|
378
|
+
- [ ] **Step 1: 检查并添加路由**
|
|
379
|
+
|
|
380
|
+
如果 routes.js 中还没有 ColumnConfigDemo 路由,添加:
|
|
381
|
+
|
|
382
|
+
在导入部分添加:
|
|
383
|
+
```javascript
|
|
384
|
+
import ColumnConfigDemoPage from '../pages/ColumnConfigDemoPage.vue'
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
在 routes 数组中添加:
|
|
388
|
+
```javascript
|
|
389
|
+
{
|
|
390
|
+
path: '/column-config-demo',
|
|
391
|
+
name: 'ColumnConfigDemo',
|
|
392
|
+
component: ColumnConfigDemoPage,
|
|
393
|
+
meta: {
|
|
394
|
+
title: '表格列配置演示',
|
|
395
|
+
showInMenu: true
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- [ ] **Step 2: 验证路由配置**
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
grep -n "ColumnConfigDemo" frontEnd/src/router/routes.js
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
- [ ] **Step 3: 提交**
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
git add frontEnd/src/router/routes.js
|
|
410
|
+
git commit -m "feat: add column config demo route"
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### Task 4: 验证功能
|
|
416
|
+
|
|
417
|
+
**Files:**
|
|
418
|
+
- Test: 手动运行测试
|
|
419
|
+
|
|
420
|
+
- [ ] **Step 1: 启动前端开发服务器**
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
npm run front-dev
|
|
424
|
+
```
|
|
425
|
+
Expected: 无编译错误,服务器正常启动
|
|
426
|
+
|
|
427
|
+
- [ ] **Step 2: 手动验证功能**
|
|
428
|
+
1. 访问页面,点击"列配置"按钮
|
|
429
|
+
2. 确认弹窗显示所有列选项
|
|
430
|
+
3. 确认"姓名"列是禁用状态(不可取消)
|
|
431
|
+
4. 测试全选/取消全选
|
|
432
|
+
5. 测试部分选中,确认半选状态显示
|
|
433
|
+
6. 点击确认,表格列动态更新
|
|
434
|
+
7. 再次打开弹窗,确认状态保持
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## 计划完成检查清单
|
|
439
|
+
|
|
440
|
+
- [x] 组件代码完整
|
|
441
|
+
- [x] 演示页面集成完整
|
|
442
|
+
- [x] 文件路径正确
|
|
443
|
+
- [x] 功能描述清晰
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 表格列配置演示页面设计
|
|
3
|
+
description: 新增表格列配置演示页面的设计文档
|
|
4
|
+
type: project
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 表格列配置演示页面设计
|
|
8
|
+
|
|
9
|
+
## 概述
|
|
10
|
+
|
|
11
|
+
为前端项目新增一个"表格列配置演示"路由页面,提供基础的表格页面框架,预留列配置功能的扩展点。
|
|
12
|
+
|
|
13
|
+
## 功能需求
|
|
14
|
+
|
|
15
|
+
1. **基础表格页面**:包含标题、工具栏、表格组件、分页
|
|
16
|
+
2. **模拟数据**:提供基础的表格数据和列配置数据结构
|
|
17
|
+
3. **预留扩展点**:列设置按钮、预留列配置功能位置
|
|
18
|
+
4. **路由配置**:在侧边栏菜单中显示"表格列配置演示"
|
|
19
|
+
|
|
20
|
+
## 架构设计
|
|
21
|
+
|
|
22
|
+
### 文件结构
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
frontEnd/src/
|
|
26
|
+
├── pages/
|
|
27
|
+
│ └── ColumnConfigDemoPage.vue # 新增:列配置演示页面
|
|
28
|
+
└── router/
|
|
29
|
+
└── routes.js # 修改:添加新路由
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 技术栈
|
|
33
|
+
|
|
34
|
+
- Vue 3 + Composition API + TypeScript
|
|
35
|
+
- Element Plus UI 组件库
|
|
36
|
+
- vue-router 4.x
|
|
37
|
+
|
|
38
|
+
## 实现步骤
|
|
39
|
+
|
|
40
|
+
1. 创建 `ColumnConfigDemoPage.vue` 页面文件
|
|
41
|
+
- 基础页面结构(标题、工具栏、el-table、分页)
|
|
42
|
+
- 模拟列配置数据(含 key、label、width、align 等属性)
|
|
43
|
+
- 模拟表格数据
|
|
44
|
+
- 预留"列设置"按钮及相关 UI 位置
|
|
45
|
+
|
|
46
|
+
2. 修改 `routes.js` 添加新路由
|
|
47
|
+
- 路径:/column-config-demo
|
|
48
|
+
- 名称:ColumnConfigDemo
|
|
49
|
+
- 菜单标题:表格列配置演示
|
|
50
|
+
- showInMenu: true
|
|
51
|
+
|
|
52
|
+
## 数据结构
|
|
53
|
+
|
|
54
|
+
### 列配置接口
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
interface ColumnConfig {
|
|
58
|
+
key: string // 列唯一标识
|
|
59
|
+
prop: string // 数据字段名
|
|
60
|
+
label: string // 列标题
|
|
61
|
+
width?: number // 列宽
|
|
62
|
+
minWidth?: number // 最小列宽
|
|
63
|
+
align?: 'left' | 'center' | 'right'
|
|
64
|
+
fixed?: boolean | 'left' | 'right'
|
|
65
|
+
sortable?: boolean | 'custom'
|
|
66
|
+
visible?: boolean // 是否显示
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 注意事项
|
|
71
|
+
|
|
72
|
+
- 遵循现有页面的代码风格和目录结构
|
|
73
|
+
- 列配置的具体实现逻辑由用户自行开发
|
|
74
|
+
- 页面结构保持简洁,便于后续扩展
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 表格列配置组件设计
|
|
3
|
+
description: 集成 TableColumnSettings 列配置组件到项目
|
|
4
|
+
type: project
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 表格列配置组件设计
|
|
8
|
+
|
|
9
|
+
## 概述
|
|
10
|
+
|
|
11
|
+
将用户提供的 TableColumnSettings 列配置组件集成到项目中,并更新 ColumnConfigDemoPage 演示页面使用该组件。
|
|
12
|
+
|
|
13
|
+
## 功能需求
|
|
14
|
+
|
|
15
|
+
1. **TableColumnSettings 组件**
|
|
16
|
+
- 弹窗形式的列配置面板
|
|
17
|
+
- 支持全选/反选、多选框组
|
|
18
|
+
- 支持禁用某些列(不可取消选中)
|
|
19
|
+
- 确认后触发事件返回选中的列 keys
|
|
20
|
+
- 自定义按钮文本
|
|
21
|
+
|
|
22
|
+
2. **演示页面集成**
|
|
23
|
+
- 在 ColumnConfigDemoPage 中使用列配置组件
|
|
24
|
+
- 实现列显示/隐藏的动态更新
|
|
25
|
+
- 支持强制显示的列(disabled)
|
|
26
|
+
|
|
27
|
+
## 架构设计
|
|
28
|
+
|
|
29
|
+
### 文件结构
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
frontEnd/src/
|
|
33
|
+
├── components/
|
|
34
|
+
│ └── TableColumnSettings.vue # 新增:列配置组件
|
|
35
|
+
└── pages/
|
|
36
|
+
└── ColumnConfigDemoPage.vue # 修改:集成组件
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 组件接口
|
|
40
|
+
|
|
41
|
+
### Props
|
|
42
|
+
|
|
43
|
+
| 名称 | 类型 | 说明 | 默认值 |
|
|
44
|
+
|------|------|------|--------|
|
|
45
|
+
| columns | Array | 列配置数组,每项需包含 prop、label、disabled | [] |
|
|
46
|
+
| selectedKeys | Array | 当前选中的列 key 数组 | [] |
|
|
47
|
+
| buttonText | String | 按钮显示文本 | '列配置' |
|
|
48
|
+
|
|
49
|
+
### Events
|
|
50
|
+
|
|
51
|
+
| 名称 | 参数 | 说明 |
|
|
52
|
+
|------|------|------|
|
|
53
|
+
| confirm | selectedKeys | 用户点击确认时触发,返回选中的列 keys 数组 |
|
|
54
|
+
|
|
55
|
+
### Column 数据结构
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
{
|
|
59
|
+
prop: 'name', // 列唯一标识
|
|
60
|
+
label: '姓名', // 列显示名称
|
|
61
|
+
disabled: false // 是否禁用(禁用的列始终显示,不可取消选中)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 实现步骤
|
|
66
|
+
|
|
67
|
+
1. 创建 `TableColumnSettings.vue` 组件
|
|
68
|
+
2. 更新 `ColumnConfigDemoPage.vue` 集成组件
|
|
69
|
+
3. 验证功能正常工作
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- 列配置触发按钮 -->
|
|
3
|
+
<el-button @click="visible = true">
|
|
4
|
+
<el-icon><Setting /></el-icon>
|
|
5
|
+
列配置
|
|
6
|
+
</el-button>
|
|
7
|
+
|
|
8
|
+
<!-- 列配置弹窗 -->
|
|
9
|
+
<el-dialog
|
|
10
|
+
v-model="visible"
|
|
11
|
+
title="表格列配置"
|
|
12
|
+
width="760px"
|
|
13
|
+
append-to-body
|
|
14
|
+
>
|
|
15
|
+
<!-- 提示文字 -->
|
|
16
|
+
<div class="desc">请选择需要在表格中显示的数据列</div>
|
|
17
|
+
|
|
18
|
+
<!-- 列选择容器 -->
|
|
19
|
+
<div class="column-box">
|
|
20
|
+
<!-- 全选行 -->
|
|
21
|
+
<div class="check-all-row">
|
|
22
|
+
<el-checkbox
|
|
23
|
+
v-model="checkAll"
|
|
24
|
+
:indeterminate="isIndeterminate"
|
|
25
|
+
@change="handleCheckAllChange"
|
|
26
|
+
>
|
|
27
|
+
全选
|
|
28
|
+
</el-checkbox>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- 列多选组 -->
|
|
32
|
+
<el-checkbox-group
|
|
33
|
+
v-model="checkedKeys"
|
|
34
|
+
class="column-list"
|
|
35
|
+
@change="updateCheckAllStatus"
|
|
36
|
+
>
|
|
37
|
+
<el-checkbox
|
|
38
|
+
v-for="item in columns"
|
|
39
|
+
:key="item.prop"
|
|
40
|
+
:label="item.prop"
|
|
41
|
+
:disabled="item.disabled"
|
|
42
|
+
>
|
|
43
|
+
{{ item.label }}
|
|
44
|
+
</el-checkbox>
|
|
45
|
+
</el-checkbox-group>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- 弹窗底部按钮 -->
|
|
49
|
+
<template #footer>
|
|
50
|
+
<el-button @click="visible = false">取消</el-button>
|
|
51
|
+
<el-button type="primary" @click="handleConfirm">
|
|
52
|
+
确认
|
|
53
|
+
</el-button>
|
|
54
|
+
</template>
|
|
55
|
+
</el-dialog>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup>
|
|
59
|
+
// 导入 Vue 组合式 API
|
|
60
|
+
import { ref, computed, watch } from 'vue'
|
|
61
|
+
// 导入 Element Plus 图标
|
|
62
|
+
import { Setting } from '@element-plus/icons-vue'
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 组件 Props 定义
|
|
66
|
+
*/
|
|
67
|
+
const props = defineProps({
|
|
68
|
+
/**
|
|
69
|
+
* 列配置数组
|
|
70
|
+
* 每个对象包含:
|
|
71
|
+
* - prop: 列唯一标识(对应表格列的 prop)
|
|
72
|
+
* - label: 列显示名称
|
|
73
|
+
* - disabled: 是否禁用(禁用的列始终显示,不可取消选中)
|
|
74
|
+
*/
|
|
75
|
+
columns: {
|
|
76
|
+
type: Array,
|
|
77
|
+
default: () => []
|
|
78
|
+
},
|
|
79
|
+
/**
|
|
80
|
+
* 当前选中的列 key 数组
|
|
81
|
+
* 用于外部控制初始选中状态
|
|
82
|
+
*/
|
|
83
|
+
selectedKeys: {
|
|
84
|
+
type: Array,
|
|
85
|
+
default: () => []
|
|
86
|
+
},
|
|
87
|
+
/**
|
|
88
|
+
* 按钮显示文本
|
|
89
|
+
* 可自定义按钮文字,默认"列配置"
|
|
90
|
+
*/
|
|
91
|
+
buttonText: {
|
|
92
|
+
type: String,
|
|
93
|
+
default: '列配置'
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* localStorage 存储键名
|
|
97
|
+
* 设置后会自动将列配置持久化到浏览器本地存储
|
|
98
|
+
* 刷新页面后配置自动恢复
|
|
99
|
+
*/
|
|
100
|
+
storageKey: {
|
|
101
|
+
type: String,
|
|
102
|
+
default: ''
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 组件 Events 定义
|
|
108
|
+
* confirm: 用户点击确认按钮时触发,参数为选中的列 key 数组
|
|
109
|
+
*/
|
|
110
|
+
const emit = defineEmits(['confirm'])
|
|
111
|
+
|
|
112
|
+
// ==================== 响应式状态 ====================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 弹窗显示/隐藏状态
|
|
116
|
+
* true: 弹窗打开
|
|
117
|
+
* false: 弹窗关闭
|
|
118
|
+
*/
|
|
119
|
+
const visible = ref(false)
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 当前选中的列 key 数组
|
|
123
|
+
* 存储用户在弹窗中勾选的列 prop
|
|
124
|
+
*/
|
|
125
|
+
const checkedKeys = ref([])
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 全选复选框状态
|
|
129
|
+
* true: 全部选中
|
|
130
|
+
* false: 全部未选中
|
|
131
|
+
*/
|
|
132
|
+
const checkAll = ref(false)
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 半选状态标识
|
|
136
|
+
* true: 部分选中(显示减号)
|
|
137
|
+
* false: 全选或全不选
|
|
138
|
+
*/
|
|
139
|
+
const isIndeterminate = ref(false)
|
|
140
|
+
|
|
141
|
+
// ==================== 计算属性 ====================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 可操作的列 key 数组
|
|
145
|
+
* 过滤掉 disabled = true 的列
|
|
146
|
+
* 这些列可以被用户自由勾选/取消
|
|
147
|
+
*/
|
|
148
|
+
const enabledKeys = computed(() => {
|
|
149
|
+
return props.columns
|
|
150
|
+
.filter(item => !item.disabled)
|
|
151
|
+
.map(item => item.prop)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// ==================== 监听器 ====================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 监听弹窗打开状态
|
|
158
|
+
* 弹窗打开时:
|
|
159
|
+
* 1. 优先从 localStorage 读取保存的配置
|
|
160
|
+
* 2. 没有存储则使用外部传入的 selectedKeys
|
|
161
|
+
* 3. 更新全选/半选状态
|
|
162
|
+
*/
|
|
163
|
+
watch(visible, val => {
|
|
164
|
+
if (val) {
|
|
165
|
+
// 优先从 localStorage 读取,其次使用 props
|
|
166
|
+
if (props.storageKey) {
|
|
167
|
+
const stored = localStorage.getItem(props.storageKey)
|
|
168
|
+
if (stored) {
|
|
169
|
+
try {
|
|
170
|
+
checkedKeys.value = JSON.parse(stored)
|
|
171
|
+
} catch {
|
|
172
|
+
// JSON 解析失败时回退到默认值
|
|
173
|
+
checkedKeys.value = [...props.selectedKeys]
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
checkedKeys.value = [...props.selectedKeys]
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
checkedKeys.value = [...props.selectedKeys]
|
|
180
|
+
}
|
|
181
|
+
// 更新全选复选框状态
|
|
182
|
+
updateCheckAllStatus()
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ==================== 方法 ====================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 全选/取消全选处理函数
|
|
190
|
+
* @param {boolean} val - 全选框当前值
|
|
191
|
+
*
|
|
192
|
+
* 逻辑说明:
|
|
193
|
+
* - 全选时:所有可操作列 + 已选中的禁用列 都被选中
|
|
194
|
+
* - 取消全选时:只保留禁用列中已选中的项
|
|
195
|
+
*/
|
|
196
|
+
function handleCheckAllChange(val) {
|
|
197
|
+
// 找出禁用且已选中的列(这些列必须始终保持选中)
|
|
198
|
+
const disabledCheckedKeys = props.columns
|
|
199
|
+
.filter(item => item.disabled && checkedKeys.value.includes(item.prop))
|
|
200
|
+
.map(item => item.prop)
|
|
201
|
+
|
|
202
|
+
checkedKeys.value = val
|
|
203
|
+
// 全选:禁用的已选中项 + 所有可操作项
|
|
204
|
+
? [...new Set([...disabledCheckedKeys, ...enabledKeys.value])]
|
|
205
|
+
// 取消全选:只保留禁用的已选中项
|
|
206
|
+
: disabledCheckedKeys
|
|
207
|
+
|
|
208
|
+
// 更新全选和半选状态
|
|
209
|
+
updateCheckAllStatus()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 更新全选和半选状态
|
|
214
|
+
*
|
|
215
|
+
* 计算逻辑:
|
|
216
|
+
* 1. 统计可操作列中已选中的数量
|
|
217
|
+
* 2. 数量 = 总数 → 全选
|
|
218
|
+
* 3. 0 < 数量 < 总数 → 半选
|
|
219
|
+
* 4. 数量 = 0 → 全不选
|
|
220
|
+
*/
|
|
221
|
+
function updateCheckAllStatus() {
|
|
222
|
+
// 统计可操作列中已选中的数量
|
|
223
|
+
const count = checkedKeys.value.filter(key =>
|
|
224
|
+
enabledKeys.value.includes(key)
|
|
225
|
+
).length
|
|
226
|
+
|
|
227
|
+
// 设置全选状态
|
|
228
|
+
checkAll.value = count === enabledKeys.value.length
|
|
229
|
+
// 设置半选状态
|
|
230
|
+
isIndeterminate.value = count > 0 && count < enabledKeys.value.length
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 确认按钮处理函数
|
|
235
|
+
*
|
|
236
|
+
* 逻辑:
|
|
237
|
+
* 1. 如果设置了 storageKey,将配置保存到 localStorage
|
|
238
|
+
* 2. 触发 confirm 事件,通知父组件
|
|
239
|
+
* 3. 关闭弹窗
|
|
240
|
+
*/
|
|
241
|
+
function handleConfirm() {
|
|
242
|
+
// 保存到 localStorage 持久化
|
|
243
|
+
if (props.storageKey) {
|
|
244
|
+
localStorage.setItem(props.storageKey, JSON.stringify(checkedKeys.value))
|
|
245
|
+
}
|
|
246
|
+
// 通知父组件选中结果
|
|
247
|
+
emit('confirm', checkedKeys.value)
|
|
248
|
+
// 关闭弹窗
|
|
249
|
+
visible.value = false
|
|
250
|
+
}
|
|
251
|
+
</script>
|
|
252
|
+
|
|
253
|
+
<style scoped>
|
|
254
|
+
/**
|
|
255
|
+
* 提示文字样式
|
|
256
|
+
*/
|
|
257
|
+
.desc {
|
|
258
|
+
margin-bottom: 22px;
|
|
259
|
+
color: #666;
|
|
260
|
+
font-size: 16px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 列选择容器边框
|
|
265
|
+
*/
|
|
266
|
+
.column-box {
|
|
267
|
+
border: 1px solid #dcdfe6;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 全选行样式
|
|
272
|
+
*/
|
|
273
|
+
.check-all-row {
|
|
274
|
+
padding: 18px 24px;
|
|
275
|
+
border-bottom: 1px solid #dcdfe6;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 列列表样式
|
|
280
|
+
* flex 布局自动换行
|
|
281
|
+
*/
|
|
282
|
+
.column-list {
|
|
283
|
+
padding: 22px 24px;
|
|
284
|
+
display: flex;
|
|
285
|
+
flex-wrap: wrap;
|
|
286
|
+
gap: 22px 28px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 覆盖 Element Plus 默认复选框右边距
|
|
291
|
+
* 因为使用 gap 控制间距,不需要默认的 margin-right
|
|
292
|
+
*/
|
|
293
|
+
:deep(.el-checkbox) {
|
|
294
|
+
margin-right: 0;
|
|
295
|
+
}
|
|
296
|
+
</style>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="page column-config-demo-page">
|
|
3
|
+
<h1 class="title">表格列配置演示</h1>
|
|
4
|
+
|
|
5
|
+
<div class="toolbar">
|
|
6
|
+
<el-button type="primary">查询</el-button>
|
|
7
|
+
<el-button>重置</el-button>
|
|
8
|
+
|
|
9
|
+
<!-- 列配置组件 -->
|
|
10
|
+
<TableColumnSettings
|
|
11
|
+
:columns="columnSettings"
|
|
12
|
+
:selected-keys="selectedKeys"
|
|
13
|
+
storage-key="column-config-demo"
|
|
14
|
+
@confirm="onColumnConfirm"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<el-table
|
|
19
|
+
:key="tableKey"
|
|
20
|
+
:data="tableData"
|
|
21
|
+
border
|
|
22
|
+
stripe
|
|
23
|
+
size="small"
|
|
24
|
+
style="width: 100%"
|
|
25
|
+
>
|
|
26
|
+
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
27
|
+
<el-table-column
|
|
28
|
+
v-for="col in visibleColumns"
|
|
29
|
+
:key="col.prop"
|
|
30
|
+
:prop="col.prop"
|
|
31
|
+
:label="col.label"
|
|
32
|
+
:width="col.width"
|
|
33
|
+
:min-width="col.minWidth"
|
|
34
|
+
:align="col.align || 'left'"
|
|
35
|
+
:fixed="col.fixed || false"
|
|
36
|
+
>
|
|
37
|
+
</el-table-column>
|
|
38
|
+
</el-table>
|
|
39
|
+
|
|
40
|
+
<div class="pagination">
|
|
41
|
+
<el-pagination
|
|
42
|
+
layout="total, prev, pager, next"
|
|
43
|
+
:current-page="page"
|
|
44
|
+
:page-size="pageSize"
|
|
45
|
+
:total="total"
|
|
46
|
+
@current-change="onPageChange"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { ref, reactive, computed, onMounted } from 'vue'
|
|
54
|
+
import TableColumnSettings from '../components/TableColumnSettings.vue'
|
|
55
|
+
|
|
56
|
+
// 表格数据接口
|
|
57
|
+
interface TableRow {
|
|
58
|
+
id: number
|
|
59
|
+
name: string
|
|
60
|
+
department: string
|
|
61
|
+
position: string
|
|
62
|
+
phone: string
|
|
63
|
+
email: string
|
|
64
|
+
joinDate: string
|
|
65
|
+
status: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 响应式数据
|
|
69
|
+
const tableKey = ref(0)
|
|
70
|
+
const page = ref(1)
|
|
71
|
+
const pageSize = ref(10)
|
|
72
|
+
const total = ref(50)
|
|
73
|
+
const tableData = ref<TableRow[]>([])
|
|
74
|
+
|
|
75
|
+
// 列配置 - 用于表格渲染
|
|
76
|
+
const columns = reactive([
|
|
77
|
+
{ key: 'name', prop: 'name', label: '姓名', width: 100 },
|
|
78
|
+
{ key: 'department', prop: 'department', label: '部门', width: 120 },
|
|
79
|
+
{ key: 'position', prop: 'position', label: '职位', width: 120 },
|
|
80
|
+
{ key: 'phone', prop: 'phone', label: '电话', width: 140 },
|
|
81
|
+
{ key: 'email', prop: 'email', label: '邮箱', minWidth: 180 },
|
|
82
|
+
{ key: 'joinDate', prop: 'joinDate', label: '入职日期', width: 120, align: 'center' },
|
|
83
|
+
{ key: 'status', prop: 'status', label: '状态', width: 100, align: 'center' }
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
// 列配置 - 用于列配置组件
|
|
87
|
+
const columnSettings = computed(() => {
|
|
88
|
+
return columns.map(col => ({
|
|
89
|
+
prop: col.prop,
|
|
90
|
+
label: col.label,
|
|
91
|
+
disabled: col.prop === 'name' // 姓名列强制显示,不可取消
|
|
92
|
+
}))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// 当前选中的列
|
|
96
|
+
const selectedKeys = ref<string[]>([])
|
|
97
|
+
|
|
98
|
+
// 可见列
|
|
99
|
+
const visibleColumns = computed(() => {
|
|
100
|
+
return columns.filter(col => selectedKeys.value.includes(col.prop))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// 列配置确认
|
|
104
|
+
const onColumnConfirm = (keys: string[]) => {
|
|
105
|
+
selectedKeys.value = keys
|
|
106
|
+
tableKey.value += 1 // 强制刷新表格
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 分页切换
|
|
110
|
+
const onPageChange = (p: number) => {
|
|
111
|
+
page.value = p
|
|
112
|
+
loadData()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 加载模拟数据
|
|
116
|
+
const loadData = () => {
|
|
117
|
+
const data: TableRow[] = []
|
|
118
|
+
for (let i = 0; i < pageSize.value; i++) {
|
|
119
|
+
const index = (page.value - 1) * pageSize.value + i + 1
|
|
120
|
+
data.push({
|
|
121
|
+
id: index,
|
|
122
|
+
name: `员工${index}`,
|
|
123
|
+
department: ['技术部', '产品部', '运营部', '市场部'][index % 4],
|
|
124
|
+
position: ['开发工程师', '产品经理', '运营专员', '市场专员'][index % 4],
|
|
125
|
+
phone: `138${String(index).padStart(8, '0')}`,
|
|
126
|
+
email: `employee${index}@example.com`,
|
|
127
|
+
joinDate: `2024-${String((index % 12) + 1).padStart(2, '0')}-${String((index % 28) + 1).padStart(2, '0')}`,
|
|
128
|
+
status: ['在职', '离职', '休假'][index % 3]
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
tableData.value = data
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onMounted(() => {
|
|
135
|
+
// 从 localStorage 读取配置,没有则默认全选
|
|
136
|
+
const storageKey = 'column-config-demo'
|
|
137
|
+
const stored = localStorage.getItem(storageKey)
|
|
138
|
+
if (stored) {
|
|
139
|
+
try {
|
|
140
|
+
selectedKeys.value = JSON.parse(stored)
|
|
141
|
+
} catch {
|
|
142
|
+
selectedKeys.value = columns.map(col => col.prop)
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
selectedKeys.value = columns.map(col => col.prop)
|
|
146
|
+
}
|
|
147
|
+
loadData()
|
|
148
|
+
})
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<style scoped>
|
|
152
|
+
.page.column-config-demo-page {
|
|
153
|
+
padding: 16px;
|
|
154
|
+
}
|
|
155
|
+
.title {
|
|
156
|
+
font-size: 18px;
|
|
157
|
+
margin-bottom: 12px;
|
|
158
|
+
}
|
|
159
|
+
.toolbar {
|
|
160
|
+
margin-bottom: 12px;
|
|
161
|
+
}
|
|
162
|
+
.pagination {
|
|
163
|
+
margin-top: 12px;
|
|
164
|
+
text-align: right;
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
@@ -6,6 +6,7 @@ import PositionReportPage from '../pages/PositionReportPage.vue'
|
|
|
6
6
|
import DateRangePage from '../pages/DateRangePage.vue'
|
|
7
7
|
import OrgTreeSelectPage from '../pages/OrgTreeSelectPage.vue'
|
|
8
8
|
import MoneyInputPage from '../pages/MoneyInputPage.vue'
|
|
9
|
+
import ColumnConfigDemoPage from '../pages/ColumnConfigDemoPage.vue'
|
|
9
10
|
|
|
10
11
|
export const routes = [
|
|
11
12
|
{
|
|
@@ -83,5 +84,14 @@ export const routes = [
|
|
|
83
84
|
title: '金额输入',
|
|
84
85
|
showInMenu: true
|
|
85
86
|
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
path: '/column-config-demo',
|
|
90
|
+
name: 'ColumnConfigDemo',
|
|
91
|
+
component: ColumnConfigDemoPage,
|
|
92
|
+
meta: {
|
|
93
|
+
title: '表格列配置演示',
|
|
94
|
+
showInMenu: true
|
|
95
|
+
}
|
|
86
96
|
}
|
|
87
97
|
]
|