vue2server7 7.0.108 → 7.0.110

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.
@@ -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,236 @@
1
+ <template>
2
+ <section class="page base-table-demo-page">
3
+ <h1 class="title">封装表格组件演示</h1>
4
+
5
+ <!-- 查询工具栏 -->
6
+ <div class="search-toolbar">
7
+ <el-input v-model="searchForm.keyword" placeholder="关键词搜索" clearable style="width: 200px" />
8
+ <el-select v-model="searchForm.department" placeholder="选择部门" clearable style="width: 140px; margin-left: 12px">
9
+ <el-option label="技术部" value="技术部" />
10
+ <el-option label="产品部" value="产品部" />
11
+ <el-option label="运营部" value="运营部" />
12
+ <el-option label="市场部" value="市场部" />
13
+ </el-select>
14
+ <el-button type="primary" @click="onSearch" style="margin-left: 12px">查询</el-button>
15
+ <el-button @click="onReset">重置</el-button>
16
+ </div>
17
+
18
+ <!-- 使用封装的 BaseTable 组件 -->
19
+ <BaseTable
20
+ ref="baseTableRef"
21
+ :data="tableData"
22
+ :columns="columns"
23
+ :show-index="true"
24
+ :show-toolbar="true"
25
+ :show-pagination="true"
26
+ v-model:current-page="page"
27
+ :page-size="pageSize"
28
+ :total="total"
29
+ storage-key="base-table-demo"
30
+ @page-change="onPageChange"
31
+ >
32
+ <!-- 自定义工具栏插槽(可选,不填则显示默认的列配置按钮) -->
33
+ <!-- <template #toolbar>
34
+ <el-button type="success">导出</el-button>
35
+ <TableColumnSettings
36
+ :columns="columns.map(c => ({ prop: c.prop, label: c.label, disabled: c.disabled }))"
37
+ :selected-keys="selectedKeys"
38
+ storage-key="base-table-demo"
39
+ @confirm="onColumnConfirm"
40
+ />
41
+ </template> -->
42
+
43
+ <!-- 自定义姓名列 -->
44
+ <template #column-name="{ row }">
45
+ <el-tag type="primary" size="small">{{ row.name }}</el-tag>
46
+ </template>
47
+
48
+ <!-- 自定义状态列 -->
49
+ <template #column-status="{ row }">
50
+ <el-tag :type="getStatusType(row.status)" size="small">
51
+ {{ row.status }}
52
+ </el-tag>
53
+ </template>
54
+
55
+ <!-- 操作列(通过插槽添加) -->
56
+ <template #action-column>
57
+ <el-table-column label="操作" width="180" align="center" fixed="right">
58
+ <template #default="{ row }">
59
+ <el-button type="primary" link size="small" @click="onView(row)">查看</el-button>
60
+ <el-button type="primary" link size="small" @click="onEdit(row)">编辑</el-button>
61
+ <el-button type="danger" link size="small" @click="onDelete(row)">删除</el-button>
62
+ </template>
63
+ </el-table-column>
64
+ </template>
65
+ </BaseTable>
66
+ </section>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import { ref, reactive, onMounted } from 'vue'
71
+ import { ElMessage, ElMessageBox } from 'element-plus'
72
+ import BaseTable from '../components/BaseTable.vue'
73
+
74
+ // 表格行数据接口
75
+ interface TableRow {
76
+ id: number
77
+ name: string
78
+ department: string
79
+ position: string
80
+ phone: string
81
+ email: string
82
+ joinDate: string
83
+ status: string
84
+ }
85
+
86
+ // 搜索表单
87
+ interface SearchForm {
88
+ keyword: string
89
+ department: string
90
+ }
91
+
92
+ // ==================== 响应式状态 ====================
93
+
94
+ const baseTableRef = ref()
95
+
96
+ // 搜索表单
97
+ const searchForm = reactive<SearchForm>({
98
+ keyword: '',
99
+ department: ''
100
+ })
101
+
102
+ // 表格数据
103
+ const tableData = ref<TableRow[]>([])
104
+
105
+ // 分页
106
+ const page = ref(1)
107
+ const pageSize = ref(10)
108
+ const total = ref(50)
109
+
110
+ // 列配置
111
+ const columns = [
112
+ { prop: 'name', label: '姓名', width: 120, disabled: true }, // 禁用 = 强制显示,不可取消
113
+ { prop: 'department', label: '部门', width: 120 },
114
+ { prop: 'position', label: '职位', width: 140 },
115
+ { prop: 'phone', label: '电话', width: 140 },
116
+ { prop: 'email', label: '邮箱', minWidth: 180 },
117
+ { prop: 'joinDate', label: '入职日期', width: 120, align: 'center' },
118
+ { prop: 'status', label: '状态', width: 100, align: 'center' }
119
+ ]
120
+
121
+ // ==================== 方法 ====================
122
+
123
+ /**
124
+ * 根据状态返回对应的 tag 类型
125
+ */
126
+ const getStatusType = (status: string) => {
127
+ const map: Record<string, string> = {
128
+ '在职': 'success',
129
+ '离职': 'info',
130
+ '休假': 'warning'
131
+ }
132
+ return map[status] || 'info'
133
+ }
134
+
135
+ /**
136
+ * 查询
137
+ */
138
+ const onSearch = () => {
139
+ page.value = 1
140
+ loadData()
141
+ ElMessage.success('查询成功')
142
+ }
143
+
144
+ /**
145
+ * 重置
146
+ */
147
+ const onReset = () => {
148
+ searchForm.keyword = ''
149
+ searchForm.department = ''
150
+ page.value = 1
151
+ loadData()
152
+ }
153
+
154
+ /**
155
+ * 分页切换
156
+ */
157
+ const onPageChange = (p: number) => {
158
+ page.value = p
159
+ loadData()
160
+ }
161
+
162
+ /**
163
+ * 查看
164
+ */
165
+ const onView = (row: TableRow) => {
166
+ ElMessageBox.alert(
167
+ `员工信息:\n姓名:${row.name}\n部门:${row.department}\n职位:${row.position}`,
168
+ '查看详情',
169
+ { confirmButtonText: '确定' }
170
+ )
171
+ }
172
+
173
+ /**
174
+ * 编辑
175
+ */
176
+ const onEdit = (row: TableRow) => {
177
+ ElMessage.info(`编辑员工:${row.name}`)
178
+ }
179
+
180
+ /**
181
+ * 删除
182
+ */
183
+ const onDelete = (row: TableRow) => {
184
+ ElMessageBox.confirm(
185
+ `确定删除员工「${row.name}」吗?`,
186
+ '提示',
187
+ {
188
+ confirmButtonText: '确定',
189
+ cancelButtonText: '取消',
190
+ type: 'warning'
191
+ }
192
+ ).then(() => {
193
+ ElMessage.success('删除成功')
194
+ }).catch(() => {})
195
+ }
196
+
197
+ /**
198
+ * 加载模拟数据
199
+ */
200
+ const loadData = () => {
201
+ const data: TableRow[] = []
202
+ for (let i = 0; i < pageSize.value; i++) {
203
+ const index = (page.value - 1) * pageSize.value + i + 1
204
+ data.push({
205
+ id: index,
206
+ name: `员工${index}`,
207
+ department: ['技术部', '产品部', '运营部', '市场部'][index % 4],
208
+ position: ['开发工程师', '产品经理', '运营专员', '市场专员'][index % 4],
209
+ phone: `138${String(index).padStart(8, '0')}`,
210
+ email: `employee${index}@example.com`,
211
+ joinDate: `2024-${String((index % 12) + 1).padStart(2, '0')}-${String((index % 28) + 1).padStart(2, '0')}`,
212
+ status: ['在职', '离职', '休假'][index % 3]
213
+ })
214
+ }
215
+ tableData.value = data
216
+ }
217
+
218
+ // ==================== 生命周期 ====================
219
+
220
+ onMounted(() => {
221
+ loadData()
222
+ })
223
+ </script>
224
+
225
+ <style scoped>
226
+ .page.base-table-demo-page {
227
+ padding: 16px;
228
+ }
229
+ .title {
230
+ font-size: 18px;
231
+ margin-bottom: 12px;
232
+ }
233
+ .search-toolbar {
234
+ margin-bottom: 12px;
235
+ }
236
+ </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>