vue3-smart-table 0.0.4 → 1.0.0

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,131 @@
1
+ /**
2
+ * 全局配置管理
3
+ */
4
+ import type { SmartTableConfig } from './types'
5
+ import { getRendererManager } from './renderer'
6
+
7
+ /**
8
+ * 默认配置
9
+ */
10
+ const defaultConfig: SmartTableConfig = {
11
+ defaultPagination: {
12
+ page: 1,
13
+ size: 10,
14
+ total: 0
15
+ },
16
+ defaultTableProps: {},
17
+ defaultColumnProps: {},
18
+ theme: {
19
+ primaryColor: '#409EFF',
20
+ successColor: '#67C23A',
21
+ warningColor: '#E6A23C',
22
+ dangerColor: '#F56C6C',
23
+ infoColor: '#909399'
24
+ }
25
+ }
26
+
27
+ /**
28
+ * 全局配置类
29
+ */
30
+ class ConfigManager {
31
+ private config: SmartTableConfig = { ...defaultConfig }
32
+
33
+ /**
34
+ * 获取所有配置
35
+ */
36
+ getConfig(): SmartTableConfig {
37
+ return { ...this.config }
38
+ }
39
+
40
+ /**
41
+ * 设置配置
42
+ */
43
+ setConfig(config: Partial<SmartTableConfig>): void {
44
+ this.config = this.mergeConfig(this.config, config)
45
+
46
+ // 如果有自定义渲染器,自动注册
47
+ if (config.renderers) {
48
+ const manager = getRendererManager()
49
+ manager.registerMultiple(config.renderers)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 获取特定配置项
55
+ */
56
+ get<K extends keyof SmartTableConfig>(key: K): SmartTableConfig[K] {
57
+ return this.config[key]
58
+ }
59
+
60
+ /**
61
+ * 重置为默认配置
62
+ */
63
+ reset(): void {
64
+ this.config = { ...defaultConfig }
65
+ }
66
+
67
+ /**
68
+ * 深度合并配置
69
+ */
70
+ private mergeConfig(target: any, source: any): any {
71
+ const result = { ...target }
72
+
73
+ for (const key in source) {
74
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
75
+ result[key] = this.mergeConfig(target[key] || {}, source[key])
76
+ } else {
77
+ result[key] = source[key]
78
+ }
79
+ }
80
+
81
+ return result
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 全局配置管理器单例
87
+ */
88
+ let globalConfigManager: ConfigManager | null = null
89
+
90
+ /**
91
+ * 获取全局配置管理器
92
+ */
93
+ export function getConfigManager(): ConfigManager {
94
+ if (!globalConfigManager) {
95
+ globalConfigManager = new ConfigManager()
96
+ }
97
+ return globalConfigManager
98
+ }
99
+
100
+ /**
101
+ * 安装插件(用于 Vue.use())
102
+ */
103
+ export interface SmartTablePlugin {
104
+ install: (options?: SmartTableConfig) => void
105
+ }
106
+
107
+ /**
108
+ * 创建插件实例
109
+ */
110
+ export function createSmartTablePlugin(defaultOptions?: SmartTableConfig): SmartTablePlugin {
111
+ return {
112
+ install(options?: SmartTableConfig) {
113
+ const manager = getConfigManager()
114
+ const config = { ...defaultOptions, ...options }
115
+ if (config) {
116
+ manager.setConfig(config)
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * 全局配置快捷方法
124
+ */
125
+ export function setSmartTableConfig(config: Partial<SmartTableConfig>): void {
126
+ getConfigManager().setConfig(config)
127
+ }
128
+
129
+ export function getSmartTableConfig(): SmartTableConfig {
130
+ return getConfigManager().getConfig()
131
+ }
@@ -0,0 +1,136 @@
1
+ import { computed } from 'vue'
2
+
3
+ import { ButtonConfig } from "../types"
4
+ /**
5
+ * useOperationColumn
6
+ *
7
+ * 操作列专用逻辑 Hook,负责:
8
+ * 1. 根据权限判断操作列是否需要显示
9
+ * 2. 计算操作列宽度(支持按钮自定义宽度)
10
+ * 3. 支持行级 visible 配置
11
+ * @param buttonConfigs 操作列按钮配置
12
+ * @param maxbtn 操作列最多显示按钮数量(超过的不参与宽度计算)
13
+ * @param userPermissions 当前用户权限列表
14
+ */
15
+ export function useOperationColumn(
16
+ buttonConfigs: ButtonConfig[],
17
+ maxbtn = 10,
18
+ userPermissions: string[] = []
19
+ ) {
20
+ /** 默认按钮宽度 */
21
+ const defaultWidth = 60
22
+
23
+ /** 超级权限标识 */
24
+ const all_permission = '*:*:*'
25
+
26
+ /** --------------------------
27
+ * 权限判断
28
+ * -------------------------- */
29
+
30
+ /**
31
+ * 判断是否具备按钮权限
32
+ *
33
+ * 规则:
34
+ * - permission 未配置 ⇒ 永远有权限
35
+ * - permission 为 string | string[] ⇒ 与用户权限匹配
36
+ */
37
+ const hasPermi = (value?: string | string[]) => {
38
+ if (!value) return true
39
+
40
+ const permArray = Array.isArray(value) ? value : [value]
41
+ return userPermissions.some(
42
+ p => p === all_permission || permArray.includes(p)
43
+ )
44
+ }
45
+
46
+ /** --------------------------
47
+ * 仅基于权限(不考虑行级 visible)
48
+ * 适用于:表格未加载数据时的判断
49
+ * -------------------------- */
50
+
51
+ /**
52
+ * 是否至少存在一个有权限的按钮
53
+ * 用于判断操作列是否需要渲染
54
+ */
55
+ const hasAnyButton = computed(() => {
56
+ return buttonConfigs.some(btn => hasPermi(btn.permission))
57
+ })
58
+
59
+ /**
60
+ * 操作列宽度(仅基于权限)
61
+ * 用于无行数据时的兜底宽度计算
62
+ */
63
+ const optWidth = computed(() => {
64
+ const permittedBtns = buttonConfigs
65
+ .filter(btn => hasPermi(btn.permission))
66
+ .slice(0, maxbtn)
67
+
68
+ return permittedBtns.reduce(
69
+ (sum, btn) => sum + (btn.width ?? defaultWidth),
70
+ 0
71
+ )
72
+ })
73
+
74
+ /** --------------------------
75
+ * 权限 + 行级 visible
76
+ * -------------------------- */
77
+
78
+ /**
79
+ * 判断某个按钮在某一行是否可见
80
+ */
81
+ const isButtonVisible = (btn: ButtonConfig, row: any) => {
82
+ return (
83
+ hasPermi(btn.permission) &&
84
+ (btn.visible ? btn.visible(row) : true)
85
+ )
86
+ }
87
+
88
+ /**
89
+ * 单行操作列宽度
90
+ */
91
+ const optRowWidth = (row: any) => {
92
+ const visibleBtns = buttonConfigs
93
+ .filter(btn => isButtonVisible(btn, row))
94
+ .slice(0, maxbtn)
95
+
96
+ return visibleBtns.reduce(
97
+ (sum, btn) => sum + (btn.width ?? defaultWidth),
98
+ 0
99
+ )
100
+ }
101
+
102
+ /**
103
+ * 遍历所有行,获取最大操作列宽度
104
+ */
105
+ const getMaxOptWidth = (rows: any[]) => {
106
+ if (!rows?.length) return optWidth.value
107
+ return rows.reduce(
108
+ (max, row) => Math.max(max, optRowWidth(row)),
109
+ 0
110
+ )
111
+ }
112
+
113
+ /**
114
+ * 判断是否至少有一行存在可见按钮
115
+ */
116
+ const hasAnyVisibleButton = (rows: any[]) => {
117
+ if (!rows?.length) return false
118
+ return rows.some(row =>
119
+ buttonConfigs.some(btn => isButtonVisible(btn, row))
120
+ )
121
+ }
122
+
123
+ const getVisibleButtons = (row: any) => {
124
+ return buttonConfigs
125
+ .filter(btn => isButtonVisible(btn, row))
126
+ .slice(0, maxbtn)
127
+ }
128
+
129
+ return {
130
+ hasAnyButton,
131
+ optWidth,
132
+ hasAnyVisibleButton,
133
+ getMaxOptWidth,
134
+ getVisibleButtons
135
+ }
136
+ }
@@ -0,0 +1,143 @@
1
+ import { ref, watch } from 'vue'
2
+
3
+
4
+ /**
5
+ * 合并默认列配置和缓存配置
6
+ *
7
+ * 设计原则:
8
+ * 1️⃣ 列顺序:以 defaultColumns 为准
9
+ * 2️⃣ 列增减:以 defaultColumns 为准
10
+ * 3️⃣ 缓存只覆盖用户可配置字段(如 visible)
11
+ */
12
+ function mergeColumns(
13
+ defaultColumns: any[],
14
+ cacheColumns: Array<{ key: string; visible?: boolean }>
15
+ ) {
16
+ if (!cacheColumns?.length) return defaultColumns
17
+
18
+ // 建立 key => cache 映射表,提升查找效率
19
+ const cacheMap = new Map(
20
+ cacheColumns.map(c => [c.key, c])
21
+ )
22
+
23
+ return defaultColumns.map(col => {
24
+ const cacheCol = cacheMap.get(col.key)
25
+
26
+ // 只允许缓存覆盖「可配置字段」
27
+ if (!cacheCol) return col
28
+
29
+ return {
30
+ ...col,
31
+ visible:
32
+ typeof cacheCol.visible === 'boolean'
33
+ ? cacheCol.visible
34
+ : col.visible
35
+ }
36
+ })
37
+ }
38
+
39
+
40
+ /**
41
+ * useTableColumns
42
+ *
43
+ * 表格列管理 Hook
44
+ *
45
+ * 职责:
46
+ * - 管理表格列顺序
47
+ * - 管理列显示 / 隐藏
48
+ * - 持久化用户配置
49
+ */
50
+ export function useTableColumns(
51
+ defaultColumns: any[],
52
+ options?: {
53
+ /** 缓存唯一标识 */
54
+ cacheKey?: string
55
+ /** 存储介质,默认 localStorage */
56
+ storage?: Storage
57
+ }
58
+ ) {
59
+
60
+ /** 解构参数并设置默认值 */
61
+ const { cacheKey, storage = localStorage } = options || {}
62
+
63
+ /**
64
+ * 如果没有 cacheKey,则不启用缓存
65
+ * (例如公共页面、未登录页面)
66
+ * 从缓存中读取列配置
67
+ */
68
+ const cache = cacheKey ? storage.getItem(cacheKey) : null
69
+
70
+ /**
71
+ * 响应式列配置
72
+ * 初始化时合并默认列和缓存列
73
+ */
74
+ const columns = ref(
75
+ mergeColumns(
76
+ defaultColumns,
77
+ cache ? JSON.parse(cache) : []
78
+ )
79
+ )
80
+
81
+ /**
82
+ * 监听列变化,自动写入缓存
83
+ */
84
+ watch(
85
+ columns,
86
+ (newVal: any) => {
87
+ if (!cacheKey) return
88
+
89
+ /**
90
+ * ⚠️ 只保存“轻量配置”
91
+ * 避免把 render / action / 函数序列化进 localStorage
92
+ */
93
+ const lightColumns = newVal.map((col: any) => ({
94
+ key: col.key,
95
+ visible: col.visible,
96
+ columnOpts: col.columnOpts
97
+ }))
98
+
99
+ storage.setItem(
100
+ cacheKey,
101
+ JSON.stringify(lightColumns)
102
+ )
103
+ },
104
+ { deep: true }
105
+ )
106
+
107
+ /**
108
+ * 对外暴露的 API
109
+ */
110
+ return {
111
+ /** 当前列配置(响应式) */
112
+ columns,
113
+
114
+ /**
115
+ * 主动设置列配置
116
+ * 常用于:列设置弹窗 / 拖拽排序完成
117
+ */
118
+ setColumns(newColumns: any[]) {
119
+ columns.value = mergeColumns(
120
+ defaultColumns,
121
+ newColumns
122
+ )
123
+
124
+ if (cacheKey) {
125
+ storage.setItem(
126
+ cacheKey,
127
+ JSON.stringify(newColumns)
128
+ )
129
+ }
130
+ },
131
+
132
+ /**
133
+ * 重置为默认列配置
134
+ */
135
+ resetColumns() {
136
+ columns.value = defaultColumns
137
+
138
+ if (cacheKey) {
139
+ storage.removeItem(cacheKey)
140
+ }
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <el-table ref="tableRef"
3
+ v-bind="$attrs"
4
+ :data="data"
5
+ :row-key="rowKey"
6
+ class="smart-table"
7
+ v-loading="loading">
8
+ <TableColumn
9
+ v-for="col in cachedColumns"
10
+ :key="col.key"
11
+ :col="col"
12
+ :permissions="permissions"
13
+ :pagination="pagination"
14
+ @cell-change="handleCellChange"
15
+ @cell-blur="handleCellBlur"
16
+ @cell-enter="handleCellEnter"
17
+ @cell-click="handleCellClick">
18
+ <template v-for="col in cachedColumns" #[col.key]="slotProps">
19
+ <slot :name="col.key" v-bind="slotProps" />
20
+ </template>
21
+ </TableColumn>
22
+ </el-table>
23
+ </template>
24
+
25
+ <script setup lang="ts" name="SmartTable">
26
+ import { PropType, ref, watch } from 'vue'
27
+ import TableColumn from './column/index.vue'
28
+ import type { BaseColumn, ColumnConfig } from './types'
29
+ import { useTableColumns } from "./hooks/useTableColumns"
30
+
31
+ const props = defineProps({
32
+ data: { type: Array, default: () => [] },
33
+ columns: { type: Array, default: () => [] },
34
+ rowKey: { type: String, default: 'id' },
35
+ loading: { type: Boolean, default: false },
36
+ permissions: {
37
+ type: Array as PropType<string[]>,
38
+ default: () => []
39
+ },
40
+ cacheKey: String,
41
+ pagination: { type: Object, default: () => ({}) },
42
+
43
+ })
44
+
45
+ const emit = defineEmits([
46
+ 'update:columns',
47
+ 'cellChange',
48
+ 'cellBlur',
49
+ 'cellEnter',
50
+ 'cell-click',
51
+ ])
52
+
53
+ // ------------------ columns 处理 ------------------
54
+ const { columns: cachedColumns } = useTableColumns(props.columns, {
55
+ cacheKey: props.cacheKey ?? '',
56
+ })
57
+ watch(
58
+ cachedColumns,
59
+ (val: ColumnConfig[]) => emit("update:columns", val),
60
+ { deep: true, immediate: true },
61
+ )
62
+
63
+ // ----------------事件封装 ------------------
64
+ const handleCellChange = (row: any, key: string) => emit('cellChange', row, key)
65
+ const handleCellBlur = (row: any, key: string) => {
66
+ emit('cellBlur', row, key)
67
+ }
68
+ const handleCellEnter = (row: any, key: string) => {
69
+ console.log('enter')
70
+ emit('cellEnter', row, key)
71
+ }
72
+
73
+ // SmartTable
74
+ const handleCellClick = (row: any, col: any) => {
75
+ if(!col) return
76
+ emit('cell-click', row, col)
77
+ }
78
+
79
+ // el-table
80
+ const tableRef = ref();
81
+ defineExpose({
82
+ tableRef,
83
+ });
84
+
85
+ </script>
86
+
87
+ <style scoped>
88
+ .smart-table {
89
+ width: 100%;
90
+ }
91
+ </style>
@@ -0,0 +1,173 @@
1
+ /**
2
+ * SmartTable 内部渲染器管理系统
3
+ * 移动到组件内部,保证组件的自包含性
4
+ */
5
+ import { defineComponent, h, Component } from 'vue'
6
+ import type { Renderer } from './types'
7
+
8
+ /**
9
+ * 渲染器注册表接口
10
+ */
11
+ export interface RendererRegistry {
12
+ register(name: string, renderer: Renderer): void
13
+ registerMultiple(renderers: Record<string, Renderer>): void
14
+ get(name: string): Renderer | undefined
15
+ has(name: string): boolean
16
+ unregister(name: string): boolean
17
+ clear(): void
18
+ names(): string[]
19
+ }
20
+
21
+ /**
22
+ * 渲染器管理器类
23
+ */
24
+ class RendererManager implements RendererRegistry {
25
+ private renderers: Map<string, Renderer> = new Map()
26
+
27
+ register(name: string, renderer: Renderer): void {
28
+ if (this.renderers.has(name)) {
29
+ // 批量注册时不警告,只在单独注册时警告
30
+ if (process.env.NODE_ENV === 'development') {
31
+ console.debug(`[SmartTable] Renderer "${name}" already registered, skipping.`)
32
+ }
33
+ }
34
+ this.renderers.set(name, renderer)
35
+ }
36
+
37
+ registerMultiple(renderers: Record<string, Renderer>): void {
38
+ Object.entries(renderers).forEach(([name, renderer]) => {
39
+ if (!this.renderers.has(name)) {
40
+ this.renderers.set(name, renderer)
41
+ }
42
+ })
43
+ }
44
+
45
+ get(name: string): Renderer | undefined {
46
+ return this.renderers.get(name)
47
+ }
48
+
49
+ has(name: string): boolean {
50
+ return this.renderers.has(name)
51
+ }
52
+
53
+ unregister(name: string): boolean {
54
+ return this.renderers.delete(name)
55
+ }
56
+
57
+ clear(): void {
58
+ this.renderers.clear()
59
+ }
60
+
61
+ names(): string[] {
62
+ return Array.from(this.renderers.keys())
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 全局渲染器管理器单例
68
+ */
69
+ let globalRendererManager: RendererManager | null = null
70
+
71
+ /**
72
+ * 获取渲染器管理器
73
+ */
74
+ export function getRendererManager(): RendererManager {
75
+ if (!globalRendererManager) {
76
+ globalRendererManager = new RendererManager()
77
+ }
78
+ return globalRendererManager
79
+ }
80
+
81
+ /**
82
+ * 包装 SFC 组件为渲染器
83
+ */
84
+ export function wrapSFCComponent(comp: Component): Renderer {
85
+ return defineComponent({
86
+ props: ['row', 'col', 'onCellChange', 'onCellBlur', 'onCellEnter', 'onClick'],
87
+ setup(props) {
88
+ return () => h(comp, props)
89
+ }
90
+ })
91
+ }
92
+
93
+ /**
94
+ * 创建函数式渲染器
95
+ */
96
+ export function createFunctionalRenderer(
97
+ render: (props: {
98
+ row: any
99
+ col: any
100
+ onCellChange?: (row: any, col: any) => void
101
+ onCellBlur?: (row: any, col: any) => void
102
+ onCellEnter?: (row: any, col: any) => void
103
+ onClick?: (row: any, col: any) => void
104
+ }) => any
105
+ ): Renderer {
106
+ return defineComponent({
107
+ props: ['row', 'col', 'onCellChange', 'onCellBlur', 'onCellEnter', 'onClick'],
108
+ setup(props) {
109
+ return () => render(props)
110
+ }
111
+ })
112
+ }
113
+
114
+ /**
115
+ * 验证渲染器配置
116
+ * 在开发环境下验证 renderProps 的正确性
117
+ */
118
+ export function validateRendererProps(
119
+ rendererName: string,
120
+ renderProps: Record<string, any> | undefined
121
+ ): void {
122
+ if (process.env.NODE_ENV !== 'production' && renderProps) {
123
+ switch (rendererName) {
124
+ case 'dict':
125
+ if (!renderProps.options || !Array.isArray(renderProps.options)) {
126
+ console.warn(
127
+ `[SmartTable] 'dict' renderer requires 'options' array, received:`,
128
+ renderProps.options
129
+ )
130
+ }
131
+ break
132
+
133
+ case 'select':
134
+ if (!renderProps.options || !Array.isArray(renderProps.options)) {
135
+ console.warn(
136
+ `[SmartTable] 'select' renderer requires 'options' array, received:`,
137
+ renderProps.options
138
+ )
139
+ } else if (renderProps.options.length === 0) {
140
+ console.warn(`[SmartTable] 'select' renderer 'options' array is empty`)
141
+ }
142
+ break
143
+
144
+ case 'map':
145
+ if (!renderProps.options || typeof renderProps.options !== 'object') {
146
+ console.warn(
147
+ `[SmartTable] 'map' renderer requires 'options' object, received:`,
148
+ renderProps.options
149
+ )
150
+ }
151
+ break
152
+
153
+ case 'link':
154
+ if (!renderProps.href || typeof renderProps.href !== 'string') {
155
+ console.warn(
156
+ `[SmartTable] 'link' renderer requires 'href' string, received:`,
157
+ renderProps.href
158
+ )
159
+ }
160
+ break
161
+
162
+ case 'input-number':
163
+ if (renderProps.min !== undefined && renderProps.max !== undefined) {
164
+ if (renderProps.min > renderProps.max) {
165
+ console.warn(
166
+ `[SmartTable] 'input-number' renderer: min (${renderProps.min}) should not be greater than max (${renderProps.max})`
167
+ )
168
+ }
169
+ }
170
+ break
171
+ }
172
+ }
173
+ }