vue3-smart-table 0.0.1

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/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # CX-SmartTable v0.0.1 使用文档
2
+
3
+ ## 概览
4
+
5
+ `SmartTable` 是一个 **基于 Vue 3 + Element Plus** 的高可复用表格组件,面向 **中后台系统** 场景设计,强调:
6
+
7
+ - 配置驱动(columns 即 schema)
8
+ - 权限解耦(不依赖 store / 登录体系)
9
+ - 操作列智能显示(无可见按钮 → 整列隐藏)
10
+ - 列显隐持久化(方案 A:只缓存 visible)
11
+ - 单元格渲染器体系(renderer 插件化)
12
+
13
+ ---
14
+
15
+ ## 目录结构
16
+
17
+ ```txt
18
+ SmartTable/
19
+ ├─ column/
20
+ │ ├─ index.vue # TableColumn 子组件
21
+ │ └─ renderer.ts # renderer 注册中心
22
+ ├─ hooks/
23
+ │ ├─ useOperationColumn.ts # 操作列按钮可见性 / 宽度逻辑
24
+ │ └─ useTableColumns.ts # 列显隐缓存(只缓存 visible)
25
+ ├─ index.vue # SmartTable 主组件
26
+ ├─ types.ts # ColumnConfig / ButtonConfig 类型
27
+ └─ README.md
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 1. SmartTable Props
33
+
34
+ | 属性 | 类型 | 默认值 | 说明 |
35
+ | --- | --- | --- | --- |
36
+ | data | `any[]` | `[]` | 表格数据 |
37
+ | columns | `ColumnConfig[]` | `[]` | 列配置(支持 v-model:columns) |
38
+ | rowKey | `string` | `'id'` | 行唯一 key |
39
+ | loading | `boolean` | `false` | loading 状态 |
40
+ | permissions | `string[]` | `[]` | 当前用户权限列表 |
41
+ | pageKey | `string` | - | 列缓存 pageKey(可选) |
42
+ | userId | `string \| number` | - | 列缓存 userId(可选) |
43
+
44
+ > 其余属性将 **透传给 el-table**。
45
+
46
+ ---
47
+
48
+ ## 2. ColumnConfig 列配置
49
+
50
+ ```ts
51
+ export interface ColumnConfig<R = any> {
52
+ type?: 'selection' | 'index' | 'operation'
53
+ key?: string
54
+ label?: string
55
+
56
+ visible?: boolean
57
+ inControl?: boolean
58
+
59
+ render?: string
60
+ editable?: boolean
61
+ editType?: 'input' | 'number' | 'select'
62
+
63
+ renderProps?: Record<string, any>
64
+ columnProps?: Record<string, any>
65
+ formatter?: (value: any, row: R) => any
66
+
67
+ /** operation 专用 */
68
+ buttons?: ButtonConfig<R>[]
69
+ maxbtn?: number
70
+
71
+ /** 内部字段(SmartTable 自动注入) */
72
+ __rows?: R[]
73
+ }
74
+ ```
75
+
76
+ ### 设计约定
77
+
78
+ - `selection / index / operation` 为 **核心列**
79
+ - 核心列必须:`inControl = false`
80
+ - 普通列通过 `visible` 控制显示 / 隐藏
81
+
82
+ ---
83
+
84
+ ## 3. 操作列 ButtonConfig
85
+
86
+ ```ts
87
+ export interface ButtonConfig<R = any> {
88
+ permission?: string | string[]
89
+ label: string
90
+ type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
91
+ action: (row: R) => void
92
+
93
+ /** 行级可见性 */
94
+ visible?: (row: R) => boolean
95
+
96
+ /** 用于宽度计算 */
97
+ width?: number
98
+ }
99
+ ```
100
+
101
+ ### 操作列显示规则
102
+
103
+ > **整个操作列是否显示 = 是否存在“至少一个行 + 至少一个按钮可见”**
104
+
105
+ - 按钮无权限 → 不显示
106
+ - `visible(row) === false` → 不显示
107
+ - **所有行所有按钮都不可见 → 整列隐藏**
108
+ - 避免出现「空白操作列」
109
+
110
+ ---
111
+
112
+ ## 4. 内置 Renderer
113
+
114
+ | renderer | 说明 |
115
+ | --- | --- |
116
+ | `html` | 原生 HTML(ellipsis) |
117
+ | `copy` | 可复制文本(hover 显示按钮 + ElMessage) |
118
+ | `img` | 图片预览 |
119
+ | `dict` | 字典映射 |
120
+ | `map` | key-value 映射 |
121
+ | `formatter` | 自定义格式化 |
122
+ | `editable` | 可编辑单元格(input / number / select) |
123
+ | `icon` | iconfont / svg / url |
124
+
125
+ ### copy 示例
126
+
127
+ ```ts
128
+ {
129
+ key: 'username',
130
+ label: '用户名',
131
+ render: 'copy'
132
+ }
133
+ ```
134
+
135
+ 特性:
136
+ - hover 显示复制按钮
137
+ - 复制成功 / 失败统一使用 `ElMessage`
138
+
139
+ ---
140
+ ### img 示例
141
+ ```ts
142
+ {
143
+ key: 'avatar',
144
+ label: '头像',
145
+ render: 'img',
146
+ renderProps: { style: 'width:50px;height:50px' }
147
+ }
148
+
149
+ ```
150
+ - 支持图片预览
151
+ - 可传入 previewSrcList、fit、style 等 renderProps
152
+
153
+ ### map 示例
154
+
155
+ ```ts
156
+ const providerMap = {
157
+ aa: 'xxx',
158
+ bb: 'xxxx',
159
+ cc: 'xxxxxx'
160
+ }
161
+ {
162
+ visible: true,
163
+ key: "status",
164
+ label: "状态",
165
+ render: "map",
166
+ renderProps: {
167
+ options: providerMap,
168
+ },
169
+ columnProps: { width: 80, sortable: true}
170
+ },
171
+
172
+ ```
173
+ - 根据值映射显示文本
174
+ - 不匹配则显示空字符串
175
+
176
+ ### dict 示例
177
+
178
+ ```ts
179
+ const Enables = [
180
+ { label: '启用', value: 1, listClass: 'primary' },
181
+ { label: '禁用', value: 0, listClass: 'warning' }
182
+ ]
183
+
184
+ {
185
+ key: "status",
186
+ label: "状态",
187
+ visible: true,
188
+ render: "dict",
189
+ renderProps: {
190
+ options: Enables,
191
+ },
192
+ columnProps: { width: 80, sortable: true}
193
+ },
194
+ ```
195
+ ---
196
+ - 支持多选值
197
+ - 可通过 showValue 显示未匹配的值
198
+ - 可自定义 tag 类型(listClass)
199
+
200
+ ### formatter 示例
201
+ ```ts
202
+ {
203
+ key: 'price',
204
+ label: '价格',
205
+ render: 'formatter',
206
+ formatter: (val) => `$${val}`
207
+ }
208
+ ```
209
+ - 使用自定义函数格式化显示内容
210
+
211
+ ### editable 渲染器
212
+
213
+ ```ts
214
+ {
215
+ key: 'age',
216
+ label: '年龄',
217
+ render: 'editable',
218
+ editable: true,
219
+ editType: 'number',
220
+ renderProps: { min: 0, max: 120 }
221
+ }
222
+ ```
223
+ - 支持类型:input / number / select
224
+ - 支持事件:
225
+ - cellChange(row, key) 值变化
226
+ - cellBlur(row, key) 失去焦点
227
+ - cellEnter(row, key) 回车事件(input)
228
+ ### icon 示例
229
+ ```ts
230
+ {
231
+ key: 'icon',
232
+ label: '图标',
233
+ render: 'icon',
234
+ renderProps: { style: 'color:red;font-size:24px' }
235
+ }
236
+ ```
237
+ - 支持网络图片 URL
238
+ - 支持 svg 字符串
239
+ - 支持 iconfont class
240
+ ## 5. useTableColumns(列显隐缓存)
241
+
242
+ ### 设计原则
243
+
244
+ - ✅ **顺序永远以 defaultColumns 为准**
245
+ - ✅ **只缓存 visible**
246
+ - ❌ 不缓存 render / action / 函数
247
+ - ❌ 不侵入 store / 登录体系
248
+
249
+ ```ts
250
+ const { columns } = useTableColumns(defaultColumns, {
251
+ pageKey: 'user-list',
252
+ userId: currentUserId
253
+ })
254
+ ```
255
+
256
+ - `userId` / `pageKey` **由调用方决定**
257
+ - 不传则不启用缓存
258
+
259
+ ---
260
+
261
+ ## 6. 使用示例
262
+
263
+ ```vue
264
+ <SmartTable
265
+ :data="tableData"
266
+ v-model:columns="columns"
267
+ :permissions="userPermissions"
268
+ :user-id="userId"
269
+ page-key="user-list"
270
+ @cellChange="onCellChange"
271
+ />
272
+ ```
273
+
274
+ ## 7. 设计边界说明
275
+
276
+ - SmartTable **不关心权限系统如何实现**
277
+ - permission 只是 string 比对
278
+ - renderer 只负责 UI,不处理权限
279
+ - 操作列是否显示由 SmartTable 统一决策
280
+
281
+
282
+
283
+
284
+
285
+
@@ -0,0 +1,11 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),C=require("element-plus"),V=e.defineComponent({__name:"input",props:{row:{},col:{},onCellBlur:{type:Function},onCellEnter:{type:Function}},setup(n){const t=n,l=e.ref(t.row[t.col.key]);e.watch(l,s=>{t.row[t.col.key]=s});const r=()=>{var s;return(s=t.onCellBlur)==null?void 0:s.call(t,t.row,t.col)},o=()=>{var s;return(s=t.onCellEnter)==null?void 0:s.call(t,t.row,t.col)};return(s,c)=>{const m=e.resolveComponent("el-input");return e.openBlock(),e.createBlock(m,e.mergeProps({modelValue:l.value,"onUpdate:modelValue":c[0]||(c[0]=d=>l.value=d)},{placeholder:"",size:"small",clearable:!0,...n.col.renderProps},{onBlur:r,onKeyup:e.withKeys(o,["enter"])}),null,16,["modelValue"])}}}),F=e.defineComponent({__name:"inputNumber",props:{row:{},col:{},onCellChange:{type:Function},onCellBlur:{type:Function},onCellEnter:{type:Function}},setup(n){const t=n,l=e.ref(t.row[t.col.key]);e.watch(l,s=>{var c;t.row[t.col.key]=s,(c=t.onCellChange)==null||c.call(t,t.row,t.col)});const r=()=>{var s;return(s=t.onCellBlur)==null?void 0:s.call(t,t.row,t.col)},o=()=>{var s;return(s=t.onCellEnter)==null?void 0:s.call(t,t.row,t.col)};return(s,c)=>{const m=e.resolveComponent("el-input-number");return e.openBlock(),e.createBlock(m,e.mergeProps({modelValue:l.value,"onUpdate:modelValue":c[0]||(c[0]=d=>l.value=d)},{min:0,max:99999,controls:!1,size:"small",...n.col.renderProps},{onBlur:r,onKeyup:e.withKeys(o,["enter"])}),null,16,["modelValue"])}}}),A=e.defineComponent({__name:"select",props:{row:{},col:{},onCellChange:{type:Function},onCellBlur:{type:Function},onCellEnter:{type:Function}},setup(n){const t=n,l=e.ref(t.row[t.col.key]);e.watch(l,c=>{t.row[t.col.key]=c});const r=()=>{var c;return(c=t.onCellChange)==null?void 0:c.call(t,t.row,t.col)},o=()=>{var c;return(c=t.onCellBlur)==null?void 0:c.call(t,t.row,t.col)},s=()=>{var c;return(c=t.onCellEnter)==null?void 0:c.call(t,t.row,t.col)};return(c,m)=>{const d=e.resolveComponent("el-option"),i=e.resolveComponent("el-select");return e.openBlock(),e.createBlock(i,e.mergeProps({modelValue:l.value,"onUpdate:modelValue":m[0]||(m[0]=f=>l.value=f)},{placeholder:"请选择",size:"small",clearable:!0,...n.col.renderProps},{onChange:r,onBlur:o,onKeyup:e.withKeys(s,["enter"])}),{default:e.withCtx(()=>{var f;return[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(((f=n.col.renderProps)==null?void 0:f.options)||[],u=>(e.openBlock(),e.createBlock(d,{key:u.value,label:u.label,value:u.value},null,8,["label","value"]))),128))]}),_:1},16,["modelValue"])}}}),w=n=>e.defineComponent({props:["row","col","onCellChange","onCellBlur","onCellEnter","onClick"],setup(t){return()=>e.h(n,t)}});function K(n){return typeof n.formatter=="function"}function O(){return{input:w(V),"input-number":w(F),select:w(A),button:n=>{const t=n.col.renderProps||{};return e.h(C.ElButton,{type:t.type||"primary",...t,onClick:()=>{var l;return(l=n.onClick)==null?void 0:l.call(n,n.row,n.col)}},()=>t.label||n.row[n.col.key])},link:n=>{const t=n.col.renderProps||{};return e.h("a",{href:t.href||"#",target:t.blank?"_blank":"_self",style:t.style||"color:#409EFF;cursor:pointer;",onClick:l=>{var r;l.preventDefault(),(r=n.onClick)==null||r.call(n,n.row,n.col)}},t.label||n.row[n.col.key])},html:n=>{var t;return e.h("div",{class:"line-clamp-2",innerHTML:n.row[n.col.key]??"",...((t=n.col)==null?void 0:t.renderProps)||{}})},copy:n=>{const t=n.row[n.col.key]??"";return e.h("div",{class:"copy-wrapper",style:"position: relative; display: inline-block;"},[e.h("span",{class:"copy-text line-clamp-1",style:"padding-right: 20px;"},t),e.h("span",{class:"copy-btn",style:`
2
+ position: absolute;
3
+ right: 0;
4
+ top: 50%;
5
+ transform: translateY(-50%);
6
+ cursor: pointer;
7
+ display: none;
8
+ font-size: 12px;
9
+ color: #409EFF;
10
+ user-select: none;
11
+ `,onClick:()=>{if(t)try{if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(t).then(()=>{C.ElMessage.success("复制成功")}).catch(()=>{C.ElMessage.error("复制失败")});else{const l=document.createElement("textarea");l.value=t,l.style.position="fixed",l.style.opacity="0",document.body.appendChild(l),l.select();const r=document.execCommand("copy");document.body.removeChild(l),r?C.ElMessage.success("复制成功"):C.ElMessage.error("复制失败")}}catch{C.ElMessage.error("复制失败")}}},"📋")])},img:n=>{var r;const t=n.row[n.col.key]??"",l=((r=n.col)==null?void 0:r.renderProps)||{};return e.h(C.ElImage,{src:t,previewSrcList:(l==null?void 0:l.previewSrcList)??(Array.isArray(t)?t:[t]),fit:"contain",style:"width:80px;height:80px",...l})},dict:n=>{const t=n.row[n.col.key],l=n.col.renderProps||{},r=l.options??[],o=l.showValue??!1;if(t==null||t==="")return"";const s=Array.isArray(t)?t.map(String):[String(t)],c=r.filter(i=>s.includes(String(i.value))),m=s.filter(i=>!r.some(f=>String(f.value)===i)),d=c.map((i,f)=>e.h(C.ElTag,{key:i.value,type:i.listClass,class:i.cssClass,disableTransitions:!0},{default:()=>i.label+" "}));return o&&m.length>0&&d.push(e.h("span",{},m.join(" "))),e.h("div",{},d)},map:n=>{var r;const t=n.row[n.col.key],l=((r=n.col.renderProps)==null?void 0:r.options)??{};return t!=null?l[t]??"":""},formatter:n=>{var o;const{col:t,row:l}=n,r=l[t.key];return K(t)?(o=t.formatter)==null?void 0:o.call(t,r,l):r??""},icon:n=>{const t=n.row[n.col.key]??"",l=n.col.renderProps||{};return t?/^https?:\/\//.test(t)?e.h(C.ElImage,{src:t,previewSrcList:[t],fit:"contain",style:"width:40px;height:40px",...l}):/^\s*<svg[\s\S]*<\/svg>\s*$/.test(t)?e.h("div",{innerHTML:t,style:`width:40px;height:40px;display:inline-block;${l.style||""}`,...l}):e.h("i",{class:t,style:`font-size:20px;${l.style||""}`,...l}):""}}}function M(n,t=10,l=[]){const o="*:*:*",s=a=>{if(!a)return!0;const p=Array.isArray(a)?a:[a];return l.some(y=>y===o||p.includes(y))},c=e.computed(()=>n.some(a=>s(a.permission))),m=e.computed(()=>n.filter(p=>s(p.permission)).slice(0,t).reduce((p,y)=>p+(y.width??55),0)),d=(a,p)=>s(a.permission)&&(a.visible?a.visible(p):!0),i=a=>n.filter(y=>d(y,a)).slice(0,t).reduce((y,_)=>y+(_.width??55),0);return{hasAnyButton:c,optWidth:m,hasAnyVisibleButton:a=>a!=null&&a.length?a.some(p=>n.some(y=>d(y,p))):!1,getMaxOptWidth:a=>a!=null&&a.length?a.reduce((p,y)=>Math.max(p,i(y)),0):m.value,getVisibleButtons:a=>n.filter(p=>d(p,a)).slice(0,t)}}const I=e.defineComponent({__name:"index",props:{col:{type:Object,required:!0},permissions:{type:Array,default:()=>[]}},emits:["cellBlur","cellEnter","cellChange","cellClick"],setup(n,{emit:t}){const l=n,r=t,{col:o}=e.toRefs(l),s=(h,k)=>r("cellChange",h,k),c=(h,k)=>r("cellBlur",h,k),m=(h,k)=>r("cellEnter",h,k),d=(h,k)=>r("cellClick",h,k),i=O(),{hasAnyButton:f,hasAnyVisibleButton:u,optWidth:g,getMaxOptWidth:a,getVisibleButtons:p}=M(o.value.buttons||[],o.value.maxbtn??10,l.permissions||[]),y=e.computed(()=>(o.value.buttons||[]).length?(o.value.__rows||[]).length?u(o.value.__rows||[]):f.value:!1),_=e.computed(()=>o.value.__rows?a(o.value.__rows):g.value);function P(h){return!(h.type==="selection"||h.type==="index"||h.type==="operation"&&!y.value||h.visible===!1)}return(h,k)=>{const b=e.resolveComponent("el-table-column"),S=e.resolveComponent("el-button");return e.unref(o).type==="selection"?(e.openBlock(),e.createBlock(b,e.mergeProps({key:0,type:"selection"},e.unref(o).columnProps),null,16)):e.unref(o).type==="index"?(e.openBlock(),e.createBlock(b,e.mergeProps({key:1,type:"index",label:e.unref(o).label||"#",align:"center"},e.unref(o).columnProps),null,16,["label"])):e.unref(o).type==="operation"&&y.value?(e.openBlock(),e.createBlock(b,e.mergeProps({key:2,label:e.unref(o).label||"操作",align:"center"},{...e.unref(o).columnProps,width:_.value}),{default:e.withCtx(({row:v})=>[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(p)(v),B=>(e.openBlock(),e.createBlock(S,{key:B.label,type:B.type||"primary",link:"",onClick:N=>B.action(v)},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(B.label),1)]),_:2},1032,["type","onClick"]))),128))]),_:1},16,["label"])):P(e.unref(o))?(e.openBlock(),e.createBlock(b,e.mergeProps({key:3,prop:e.unref(o).key,label:e.unref(o).label,align:"center"},e.unref(o).columnProps||{}),{default:e.withCtx(({row:v})=>[e.unref(o).render&&e.unref(i)[e.unref(o).render]?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(e.unref(i)[e.unref(o).render]),{key:0,row:v,col:e.unref(o),onCellChange:s,onCellBlur:c,onCellEnter:m,onClick:d},null,40,["row","col"])):(e.openBlock(),e.createElementBlock(e.Fragment,{key:1},[e.createTextVNode(e.toDisplayString(v[e.unref(o).key]),1)],64))]),_:1},16,["prop","label"])):e.createCommentVNode("",!0)}}}),T="table_columns_";function $(n,t){return`${T}${n}_${t}`}function x(n,t){if(!(t!=null&&t.length))return n;const l=new Map(t.map(r=>[r.key,r]));return n.map(r=>{const o=l.get(r.key);return o?{...r,visible:typeof o.visible=="boolean"?o.visible:r.visible}:r})}function W(n,t){const{pageKey:l,userId:r,storage:o=localStorage}=t||{},c=r?$(r,l||""):null,m=c?o.getItem(c):null,d=e.ref(x(n,m?JSON.parse(m):[]));return e.watch(d,i=>{if(!c)return;const f=i.map(u=>({key:u.key,visible:u.visible,columnOpts:u.columnOpts}));o.setItem(c,JSON.stringify(f))},{deep:!0}),{columns:d,setColumns(i){d.value=x(n,i),c&&o.setItem(c,JSON.stringify(i))},resetColumns(){d.value=n,c&&o.removeItem(c)}}}const D=e.defineComponent({__name:"index",props:{data:{type:Array,default:()=>[]},columns:{type:Array,default:()=>[]},pageKey:String,rowKey:{type:String,default:"id"},loading:{type:Boolean,default:!1},permissions:{type:Array,default:()=>[]},userId:{type:[String,Number],default:""}},emits:["update:columns","cellChange","cellBlur","cellEnter","cell-click"],setup(n,{expose:t,emit:l}){const r=n,o=l,{columns:s}=W(r.columns,{pageKey:r.pageKey??"",userId:r.userId??""});e.watch(s,u=>o("update:columns",u),{deep:!0,immediate:!0});const c=(u,g)=>o("cellChange",u,g),m=(u,g)=>{o("cellBlur",u,g)},d=(u,g)=>{console.log("enter"),o("cellEnter",u,g)},i=(u,g)=>{g&&o("cell-click",u,g)},f=e.ref();return t({tableRef:f}),(u,g)=>{const a=e.resolveComponent("el-table"),p=e.resolveDirective("loading");return e.withDirectives((e.openBlock(),e.createBlock(a,e.mergeProps({ref_key:"tableRef",ref:f},u.$attrs,{data:n.data,"row-key":n.rowKey,class:"smart-table"}),{default:e.withCtx(()=>[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(s),y=>(e.openBlock(),e.createBlock(I,{key:y.key,col:y,permissions:n.permissions,onCellChange:c,onCellBlur:m,onCellEnter:d,onCellClick:i},null,8,["col","permissions"]))),128))]),_:1},16,["data","row-key"])),[[p,n.loading]])}}}),L=(n,t)=>{const l=n.__vccOpts||n;for(const[r,o]of t)l[r]=o;return l},E=L(D,[["__scopeId","data-v-338b77db"]]);exports.SmartTable=E;exports.default=E;
@@ -0,0 +1 @@
1
+ .copy-wrapper:hover .copy-btn{display:inline-block!important}.smart-table[data-v-338b77db]{width:100%}
@@ -0,0 +1,518 @@
1
+ import { defineComponent as B, ref as V, watch as S, resolveComponent as w, createBlock as b, openBlock as f, mergeProps as _, withKeys as M, withCtx as P, createElementBlock as O, Fragment as K, renderList as T, h as g, computed as $, toRefs as U, createCommentVNode as j, unref as d, createTextVNode as D, toDisplayString as N, resolveDynamicComponent as H, resolveDirective as q, withDirectives as X } from "vue";
2
+ import { ElImage as L, ElTag as Y, ElMessage as E, ElButton as G } from "element-plus";
3
+ const Q = /* @__PURE__ */ B({
4
+ __name: "input",
5
+ props: {
6
+ row: {},
7
+ col: {},
8
+ onCellBlur: { type: Function },
9
+ onCellEnter: { type: Function }
10
+ },
11
+ setup(t) {
12
+ const e = t, n = V(e.row[e.col.key]);
13
+ S(n, (c) => {
14
+ e.row[e.col.key] = c;
15
+ });
16
+ const r = () => {
17
+ var c;
18
+ return (c = e.onCellBlur) == null ? void 0 : c.call(e, e.row, e.col);
19
+ }, l = () => {
20
+ var c;
21
+ return (c = e.onCellEnter) == null ? void 0 : c.call(e, e.row, e.col);
22
+ };
23
+ return (c, o) => {
24
+ const y = w("el-input");
25
+ return f(), b(y, _({
26
+ modelValue: n.value,
27
+ "onUpdate:modelValue": o[0] || (o[0] = (u) => n.value = u)
28
+ }, { placeholder: "", size: "small", clearable: !0, ...t.col.renderProps }, {
29
+ onBlur: r,
30
+ onKeyup: M(l, ["enter"])
31
+ }), null, 16, ["modelValue"]);
32
+ };
33
+ }
34
+ }), Z = /* @__PURE__ */ B({
35
+ __name: "inputNumber",
36
+ props: {
37
+ row: {},
38
+ col: {},
39
+ onCellChange: { type: Function },
40
+ onCellBlur: { type: Function },
41
+ onCellEnter: { type: Function }
42
+ },
43
+ setup(t) {
44
+ const e = t, n = V(e.row[e.col.key]);
45
+ S(n, (c) => {
46
+ var o;
47
+ e.row[e.col.key] = c, (o = e.onCellChange) == null || o.call(e, e.row, e.col);
48
+ });
49
+ const r = () => {
50
+ var c;
51
+ return (c = e.onCellBlur) == null ? void 0 : c.call(e, e.row, e.col);
52
+ }, l = () => {
53
+ var c;
54
+ return (c = e.onCellEnter) == null ? void 0 : c.call(e, e.row, e.col);
55
+ };
56
+ return (c, o) => {
57
+ const y = w("el-input-number");
58
+ return f(), b(y, _({
59
+ modelValue: n.value,
60
+ "onUpdate:modelValue": o[0] || (o[0] = (u) => n.value = u)
61
+ }, { min: 0, max: 99999, controls: !1, size: "small", ...t.col.renderProps }, {
62
+ onBlur: r,
63
+ onKeyup: M(l, ["enter"])
64
+ }), null, 16, ["modelValue"]);
65
+ };
66
+ }
67
+ }), ee = /* @__PURE__ */ B({
68
+ __name: "select",
69
+ props: {
70
+ row: {},
71
+ col: {},
72
+ onCellChange: { type: Function },
73
+ onCellBlur: { type: Function },
74
+ onCellEnter: { type: Function }
75
+ },
76
+ setup(t) {
77
+ const e = t, n = V(e.row[e.col.key]);
78
+ S(n, (o) => {
79
+ e.row[e.col.key] = o;
80
+ });
81
+ const r = () => {
82
+ var o;
83
+ return (o = e.onCellChange) == null ? void 0 : o.call(e, e.row, e.col);
84
+ }, l = () => {
85
+ var o;
86
+ return (o = e.onCellBlur) == null ? void 0 : o.call(e, e.row, e.col);
87
+ }, c = () => {
88
+ var o;
89
+ return (o = e.onCellEnter) == null ? void 0 : o.call(e, e.row, e.col);
90
+ };
91
+ return (o, y) => {
92
+ const u = w("el-option"), a = w("el-select");
93
+ return f(), b(a, _({
94
+ modelValue: n.value,
95
+ "onUpdate:modelValue": y[0] || (y[0] = (h) => n.value = h)
96
+ }, { placeholder: "请选择", size: "small", clearable: !0, ...t.col.renderProps }, {
97
+ onChange: r,
98
+ onBlur: l,
99
+ onKeyup: M(c, ["enter"])
100
+ }), {
101
+ default: P(() => {
102
+ var h;
103
+ return [
104
+ (f(!0), O(K, null, T(((h = t.col.renderProps) == null ? void 0 : h.options) || [], (i) => (f(), b(u, {
105
+ key: i.value,
106
+ label: i.label,
107
+ value: i.value
108
+ }, null, 8, ["label", "value"]))), 128))
109
+ ];
110
+ }),
111
+ _: 1
112
+ }, 16, ["modelValue"]);
113
+ };
114
+ }
115
+ }), W = (t) => B({
116
+ props: ["row", "col", "onCellChange", "onCellBlur", "onCellEnter", "onClick"],
117
+ setup(e) {
118
+ return () => g(t, e);
119
+ }
120
+ });
121
+ function te(t) {
122
+ return typeof t.formatter == "function";
123
+ }
124
+ function ne() {
125
+ return {
126
+ input: W(Q),
127
+ "input-number": W(Z),
128
+ select: W(ee),
129
+ button: (t) => {
130
+ const e = t.col.renderProps || {};
131
+ return g(G, {
132
+ type: e.type || "primary",
133
+ ...e,
134
+ onClick: () => {
135
+ var n;
136
+ return (n = t.onClick) == null ? void 0 : n.call(t, t.row, t.col);
137
+ }
138
+ }, () => e.label || t.row[t.col.key]);
139
+ },
140
+ link: (t) => {
141
+ const e = t.col.renderProps || {};
142
+ return g("a", {
143
+ href: e.href || "#",
144
+ target: e.blank ? "_blank" : "_self",
145
+ style: e.style || "color:#409EFF;cursor:pointer;",
146
+ onClick: (n) => {
147
+ var r;
148
+ n.preventDefault(), (r = t.onClick) == null || r.call(t, t.row, t.col);
149
+ }
150
+ }, e.label || t.row[t.col.key]);
151
+ },
152
+ html: (t) => {
153
+ var e;
154
+ return g("div", {
155
+ class: "line-clamp-2",
156
+ innerHTML: t.row[t.col.key] ?? "",
157
+ ...((e = t.col) == null ? void 0 : e.renderProps) || {}
158
+ });
159
+ },
160
+ copy: (t) => {
161
+ const e = t.row[t.col.key] ?? "";
162
+ return g(
163
+ "div",
164
+ {
165
+ class: "copy-wrapper",
166
+ style: "position: relative; display: inline-block;"
167
+ },
168
+ [
169
+ g("span", {
170
+ class: "copy-text line-clamp-1",
171
+ style: "padding-right: 20px;"
172
+ }, e),
173
+ g(
174
+ "span",
175
+ {
176
+ class: "copy-btn",
177
+ style: `
178
+ position: absolute;
179
+ right: 0;
180
+ top: 50%;
181
+ transform: translateY(-50%);
182
+ cursor: pointer;
183
+ display: none;
184
+ font-size: 12px;
185
+ color: #409EFF;
186
+ user-select: none;
187
+ `,
188
+ onClick: () => {
189
+ if (e)
190
+ try {
191
+ if (navigator.clipboard && navigator.clipboard.writeText)
192
+ navigator.clipboard.writeText(e).then(() => {
193
+ E.success("复制成功");
194
+ }).catch(() => {
195
+ E.error("复制失败");
196
+ });
197
+ else {
198
+ const n = document.createElement("textarea");
199
+ n.value = e, n.style.position = "fixed", n.style.opacity = "0", document.body.appendChild(n), n.select();
200
+ const r = document.execCommand("copy");
201
+ document.body.removeChild(n), r ? E.success("复制成功") : E.error("复制失败");
202
+ }
203
+ } catch {
204
+ E.error("复制失败");
205
+ }
206
+ }
207
+ },
208
+ "📋"
209
+ // 复制图标,可用 Element Plus 图标替换
210
+ )
211
+ ]
212
+ );
213
+ },
214
+ img: (t) => {
215
+ var r;
216
+ const e = t.row[t.col.key] ?? "", n = ((r = t.col) == null ? void 0 : r.renderProps) || {};
217
+ return g(L, {
218
+ src: e,
219
+ previewSrcList: (n == null ? void 0 : n.previewSrcList) ?? (Array.isArray(e) ? e : [e]),
220
+ fit: "contain",
221
+ style: "width:80px;height:80px",
222
+ ...n
223
+ });
224
+ },
225
+ dict: (t) => {
226
+ const e = t.row[t.col.key], n = t.col.renderProps || {}, r = n.options ?? [], l = n.showValue ?? !1;
227
+ if (e == null || e === "") return "";
228
+ const c = Array.isArray(e) ? e.map(String) : [String(e)], o = r.filter((a) => c.includes(String(a.value))), y = c.filter((a) => !r.some((h) => String(h.value) === a)), u = o.map((a, h) => g(
229
+ Y,
230
+ { key: a.value, type: a.listClass, class: a.cssClass, disableTransitions: !0 },
231
+ { default: () => a.label + " " }
232
+ ));
233
+ return l && y.length > 0 && u.push(g("span", {}, y.join(" "))), g("div", {}, u);
234
+ },
235
+ map: (t) => {
236
+ var r;
237
+ const e = t.row[t.col.key], n = ((r = t.col.renderProps) == null ? void 0 : r.options) ?? {};
238
+ return e != null ? n[e] ?? "" : "";
239
+ },
240
+ formatter: (t) => {
241
+ var l;
242
+ const { col: e, row: n } = t, r = n[e.key];
243
+ return te(e) ? (l = e.formatter) == null ? void 0 : l.call(e, r, n) : r ?? "";
244
+ },
245
+ icon: (t) => {
246
+ const e = t.row[t.col.key] ?? "", n = t.col.renderProps || {};
247
+ return e ? /^https?:\/\//.test(e) ? g(L, {
248
+ src: e,
249
+ previewSrcList: [e],
250
+ fit: "contain",
251
+ style: "width:40px;height:40px",
252
+ ...n
253
+ }) : /^\s*<svg[\s\S]*<\/svg>\s*$/.test(e) ? g("div", {
254
+ innerHTML: e,
255
+ style: `width:40px;height:40px;display:inline-block;${n.style || ""}`,
256
+ ...n
257
+ }) : g("i", {
258
+ class: e,
259
+ // val 直接当 className
260
+ style: `font-size:20px;${n.style || ""}`,
261
+ ...n
262
+ }) : "";
263
+ }
264
+ };
265
+ }
266
+ function le(t, e = 10, n = []) {
267
+ const l = "*:*:*", c = (s) => {
268
+ if (!s) return !0;
269
+ const m = Array.isArray(s) ? s : [s];
270
+ return n.some(
271
+ (p) => p === l || m.includes(p)
272
+ );
273
+ }, o = $(() => t.some((s) => c(s.permission))), y = $(() => t.filter((m) => c(m.permission)).slice(0, e).reduce(
274
+ (m, p) => m + (p.width ?? 55),
275
+ 0
276
+ )), u = (s, m) => c(s.permission) && (s.visible ? s.visible(m) : !0), a = (s) => t.filter((p) => u(p, s)).slice(0, e).reduce(
277
+ (p, I) => p + (I.width ?? 55),
278
+ 0
279
+ );
280
+ return {
281
+ hasAnyButton: o,
282
+ optWidth: y,
283
+ hasAnyVisibleButton: (s) => s != null && s.length ? s.some(
284
+ (m) => t.some((p) => u(p, m))
285
+ ) : !1,
286
+ getMaxOptWidth: (s) => s != null && s.length ? s.reduce(
287
+ (m, p) => Math.max(m, a(p)),
288
+ 0
289
+ ) : y.value,
290
+ getVisibleButtons: (s) => t.filter((m) => u(m, s)).slice(0, e)
291
+ };
292
+ }
293
+ const re = /* @__PURE__ */ B({
294
+ __name: "index",
295
+ props: {
296
+ col: { type: Object, required: !0 },
297
+ permissions: { type: Array, default: () => [] }
298
+ },
299
+ emits: ["cellBlur", "cellEnter", "cellChange", "cellClick"],
300
+ setup(t, { emit: e }) {
301
+ const n = t, r = e, { col: l } = U(n), c = (v, k) => r("cellChange", v, k), o = (v, k) => r("cellBlur", v, k), y = (v, k) => r("cellEnter", v, k), u = (v, k) => r("cellClick", v, k), a = ne(), {
302
+ hasAnyButton: h,
303
+ hasAnyVisibleButton: i,
304
+ optWidth: C,
305
+ getMaxOptWidth: s,
306
+ getVisibleButtons: m
307
+ } = le(
308
+ l.value.buttons || [],
309
+ l.value.maxbtn ?? 10,
310
+ n.permissions || []
311
+ ), p = $(() => (l.value.buttons || []).length ? (l.value.__rows || []).length ? i(l.value.__rows || []) : h.value : !1), I = $(() => l.value.__rows ? s(l.value.__rows) : C.value);
312
+ function z(v) {
313
+ return !(v.type === "selection" || v.type === "index" || v.type === "operation" && !p.value || v.visible === !1);
314
+ }
315
+ return (v, k) => {
316
+ const A = w("el-table-column"), J = w("el-button");
317
+ return d(l).type === "selection" ? (f(), b(A, _({
318
+ key: 0,
319
+ type: "selection"
320
+ }, d(l).columnProps), null, 16)) : d(l).type === "index" ? (f(), b(A, _({
321
+ key: 1,
322
+ type: "index",
323
+ label: d(l).label || "#",
324
+ align: "center"
325
+ }, d(l).columnProps), null, 16, ["label"])) : d(l).type === "operation" && p.value ? (f(), b(A, _({
326
+ key: 2,
327
+ label: d(l).label || "操作",
328
+ align: "center"
329
+ }, {
330
+ ...d(l).columnProps,
331
+ width: I.value
332
+ }), {
333
+ default: P(({ row: x }) => [
334
+ (f(!0), O(K, null, T(d(m)(x), (F) => (f(), b(J, {
335
+ key: F.label,
336
+ type: F.type || "primary",
337
+ link: "",
338
+ onClick: (ue) => F.action(x)
339
+ }, {
340
+ default: P(() => [
341
+ D(N(F.label), 1)
342
+ ]),
343
+ _: 2
344
+ }, 1032, ["type", "onClick"]))), 128))
345
+ ]),
346
+ _: 1
347
+ }, 16, ["label"])) : z(d(l)) ? (f(), b(A, _({
348
+ key: 3,
349
+ prop: d(l).key,
350
+ label: d(l).label,
351
+ align: "center"
352
+ }, d(l).columnProps || {}), {
353
+ default: P(({ row: x }) => [
354
+ d(l).render && d(a)[d(l).render] ? (f(), b(H(d(a)[d(l).render]), {
355
+ key: 0,
356
+ row: x,
357
+ col: d(l),
358
+ onCellChange: c,
359
+ onCellBlur: o,
360
+ onCellEnter: y,
361
+ onClick: u
362
+ }, null, 40, ["row", "col"])) : (f(), O(K, { key: 1 }, [
363
+ D(N(x[d(l).key]), 1)
364
+ ], 64))
365
+ ]),
366
+ _: 1
367
+ }, 16, ["prop", "label"])) : j("", !0);
368
+ };
369
+ }
370
+ }), oe = "table_columns_";
371
+ function ce(t, e) {
372
+ return `${oe}${t}_${e}`;
373
+ }
374
+ function R(t, e) {
375
+ if (!(e != null && e.length)) return t;
376
+ const n = new Map(
377
+ e.map((r) => [r.key, r])
378
+ );
379
+ return t.map((r) => {
380
+ const l = n.get(r.key);
381
+ return l ? {
382
+ ...r,
383
+ visible: typeof l.visible == "boolean" ? l.visible : r.visible
384
+ } : r;
385
+ });
386
+ }
387
+ function se(t, e) {
388
+ const {
389
+ pageKey: n,
390
+ userId: r,
391
+ storage: l = localStorage
392
+ } = e || {}, o = r ? ce(r, n || "") : null, y = o ? l.getItem(o) : null, u = V(
393
+ R(
394
+ t,
395
+ y ? JSON.parse(y) : []
396
+ )
397
+ );
398
+ return S(
399
+ u,
400
+ (a) => {
401
+ if (!o) return;
402
+ const h = a.map((i) => ({
403
+ key: i.key,
404
+ visible: i.visible,
405
+ columnOpts: i.columnOpts
406
+ }));
407
+ l.setItem(
408
+ o,
409
+ JSON.stringify(h)
410
+ );
411
+ },
412
+ { deep: !0 }
413
+ ), {
414
+ /** 当前列配置(响应式) */
415
+ columns: u,
416
+ /**
417
+ * 主动设置列配置
418
+ * 常用于:列设置弹窗 / 拖拽排序完成
419
+ */
420
+ setColumns(a) {
421
+ u.value = R(
422
+ t,
423
+ a
424
+ ), o && l.setItem(
425
+ o,
426
+ JSON.stringify(a)
427
+ );
428
+ },
429
+ /**
430
+ * 重置为默认列配置
431
+ */
432
+ resetColumns() {
433
+ u.value = t, o && l.removeItem(o);
434
+ }
435
+ };
436
+ }
437
+ const ae = /* @__PURE__ */ B({
438
+ __name: "index",
439
+ props: {
440
+ data: { type: Array, default: () => [] },
441
+ columns: { type: Array, default: () => [] },
442
+ // v-model:columns
443
+ pageKey: String,
444
+ rowKey: { type: String, default: "id" },
445
+ loading: { type: Boolean, default: !1 },
446
+ permissions: {
447
+ type: Array,
448
+ default: () => []
449
+ },
450
+ userId: {
451
+ /** 当前用户标识(可选,用于列缓存) */
452
+ type: [String, Number],
453
+ default: ""
454
+ }
455
+ },
456
+ emits: [
457
+ "update:columns",
458
+ "cellChange",
459
+ "cellBlur",
460
+ "cellEnter",
461
+ "cell-click"
462
+ ],
463
+ setup(t, { expose: e, emit: n }) {
464
+ const r = t, l = n, { columns: c } = se(r.columns, {
465
+ pageKey: r.pageKey ?? "",
466
+ userId: r.userId ?? ""
467
+ });
468
+ S(
469
+ c,
470
+ (i) => l("update:columns", i),
471
+ { deep: !0, immediate: !0 }
472
+ );
473
+ const o = (i, C) => l("cellChange", i, C), y = (i, C) => {
474
+ l("cellBlur", i, C);
475
+ }, u = (i, C) => {
476
+ console.log("enter"), l("cellEnter", i, C);
477
+ }, a = (i, C) => {
478
+ C && l("cell-click", i, C);
479
+ }, h = V();
480
+ return e({
481
+ tableRef: h
482
+ }), (i, C) => {
483
+ const s = w("el-table"), m = q("loading");
484
+ return X((f(), b(s, _({
485
+ ref_key: "tableRef",
486
+ ref: h
487
+ }, i.$attrs, {
488
+ data: t.data,
489
+ "row-key": t.rowKey,
490
+ class: "smart-table"
491
+ }), {
492
+ default: P(() => [
493
+ (f(!0), O(K, null, T(d(c), (p) => (f(), b(re, {
494
+ key: p.key,
495
+ col: p,
496
+ permissions: t.permissions,
497
+ onCellChange: o,
498
+ onCellBlur: y,
499
+ onCellEnter: u,
500
+ onCellClick: a
501
+ }, null, 8, ["col", "permissions"]))), 128))
502
+ ]),
503
+ _: 1
504
+ }, 16, ["data", "row-key"])), [
505
+ [m, t.loading]
506
+ ]);
507
+ };
508
+ }
509
+ }), ie = (t, e) => {
510
+ const n = t.__vccOpts || t;
511
+ for (const [r, l] of e)
512
+ n[r] = l;
513
+ return n;
514
+ }, me = /* @__PURE__ */ ie(ae, [["__scopeId", "data-v-338b77db"]]);
515
+ export {
516
+ me as SmartTable,
517
+ me as default
518
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "vue3-smart-table",
3
+ "version": "0.0.1",
4
+ "description": "基于 Vue 3 + Element Plus 的高可复用表格组件",
5
+ "main": "dist/vue3-smart-table.cjs.js",
6
+ "module": "dist/vue3-smart-table.es.js",
7
+ "types": "dist/types/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "keywords": [
12
+ "vue3",
13
+ "element-plus",
14
+ "table",
15
+ "smart-table"
16
+ ],
17
+ "author": "xiechen",
18
+ "license": "MIT",
19
+ "scripts": {
20
+ "build": "vue-tsc --noEmit && vite build",
21
+ "dev:demo": "cd demo && vite",
22
+ "preview:demo": "vite preview demo"
23
+ },
24
+ "peerDependencies": {
25
+ "element-plus": "^2.9.0",
26
+ "vue": "3.5.13"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.0.2",
30
+ "@vitejs/plugin-vue": "^5.2.4",
31
+ "@vitejs/plugin-vue-jsx": "^5.1.2",
32
+ "element-plus": "^2.9.0",
33
+ "typescript": "^5.0.0",
34
+ "vite": "^6.4.1",
35
+ "vue": "^3.5.25",
36
+ "vue-tsc": "^3.1.8"
37
+ },
38
+ "dependencies": {
39
+ "vue-router": "^4.6.4"
40
+ }
41
+ }