n-design-readonly-plugin 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.
package/.DS_Store ADDED
Binary file
package/.gitignore ADDED
@@ -0,0 +1,31 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ .DS_Store
12
+ dist
13
+ dist-ssr
14
+ coverage
15
+ *.local
16
+ package-lock.json
17
+ report.html
18
+
19
+ /cypress/videos/
20
+ /cypress/screenshots/
21
+
22
+ # Editor directories and files
23
+ .vscode/*
24
+ !.vscode/extensions.json
25
+ !.vscode/settings.json
26
+ .idea
27
+ *.suo
28
+ *.ntvs*
29
+ *.njsproj
30
+ *.sln
31
+ *.sw?
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # n-design-readonly-plugin
2
+
3
+ [![npm version](https://img.shields.io/npm/v/n-design-readonly-plugin?color=blue)](https://www.npmjs.com/package/n-design-readonly-plugin)
4
+ [![npm downloads](https://img.shields.io/npm/dm/n-design-readonly-plugin)](https://www.npmjs.com/package/n-design-readonly-plugin)
5
+ [![License](https://img.shields.io/npm/l/n-design-readonly-plugin)](LICENSE)
6
+
7
+ > 一个 Vue 3 高阶组件(HOC),用于将 `n-designv3` 表单组件一键切换为只读模式。
8
+
9
+ 在业务系统中,经常需要在「编辑态」和「详情态」之间切换。本插件通过 `:readonly="true"` 全局控制或组件级控制,自动将 `<NkInput>`、`<NkSelect>` 等组件渲染为不可编辑的静态文本,无需重复编写展示逻辑。
10
+
11
+ ## ✨ 特性
12
+
13
+ - ✅ 支持 `n-designv3` 所有表单组件(`Input`, `Select`, `DatePicker` 等)
14
+ - ✅ 全局控制:通过 `<NkForm :readonly="true">` 一键禁用整个表单
15
+ - ✅ 组件级控制:单个组件设置 `:readonly="true"`
16
+ - ✅ 自动提取显示文本(支持 `label` / `value` 映射)
17
+ - ✅ 完整 TypeScript 类型支持
18
+ - ✅ 零运行时依赖(仅依赖 `vue` 和 `n-designv3`)
19
+
20
+ ## 📦 安装
21
+
22
+ ```bash
23
+ npm install n-design-readonly-plugin
24
+ ```
25
+
26
+ ## 1.全局注册插件
27
+ ```ts
28
+ // main.ts
29
+ import { createApp } from 'vue';
30
+ import App from './App.vue';
31
+
32
+ // 引入 n-designv3(必须)
33
+ import NDesignV3 from 'n-designv3';
34
+ import 'n-designv3/dist/style.css';
35
+
36
+ // 引入本插件
37
+ import { NkReadonlyPlugin } from 'nk-readonly-hoc';
38
+
39
+ const app = createApp(App);
40
+ app.use(NDesignV3);
41
+ app.use(NkReadonlyPlugin); // 注册后自动提供 NkInput, NkSelect 等组件
42
+ app.mount('#app');
43
+
44
+ ```
45
+
46
+ ## 2.在模板中使用
47
+ ### 全局只读(推荐)
48
+ ```vue
49
+ <template>
50
+ <nk-form :readonly="isReadonly" :model="form">
51
+ <nk-form-item label="用户名">
52
+ <nk-input v-model:value="form.name" />
53
+ </nk-form-item>
54
+ <nk-form-item label="城市">
55
+ <nk-select
56
+ v-model:value="form.city"
57
+ :options="cityOptions"
58
+ />
59
+ </nk-form-item>
60
+ </nk-form>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import { ref } from 'vue';
65
+
66
+ const isReadonly = ref(true);
67
+ const form = reactive({ name: '张三', city: 'bj' });
68
+ const cityOptions = [
69
+ { label: '北京', value: 'bj' },
70
+ { label: '上海', value: 'sh' }
71
+ ];
72
+ </script>
73
+ ```
74
+
75
+ ## 3.组件映射
76
+ | 原始组件 (`n-designv3`) | 只读包装组件 |
77
+ |------------------------|-------------|
78
+ | `<n-input>` | `<nk-input>` |
79
+ | `<n-select>` | `<nk-select>` |
80
+ | `<n-date-picker>` | `<nk-date-picker>` |
81
+ | ... | ... |
82
+
83
+ 所有 n-* 表单组件均自动映射为 nk-*,命名规则一致。
84
+
85
+ ## 4.工作原理
86
+ 插件内部使用 provide/inject 传递 readonly 上下文
87
+ 每个 nk-* 组件是 withReadonly(WrappedComponent) 的高阶封装
88
+ 当 readonly === true 时,不再渲染原始组件,而是渲染一个带样式的 <span> 显示文本
89
+ 文本内容通过智能解析 value + options 自动获取(如 Select 的 label)
@@ -0,0 +1,4 @@
1
+ import { Plugin } from 'vue';
2
+ import { withReadonly } from './withReadonly';
3
+ export declare const NkReadonlyPlugin: Plugin;
4
+ export { withReadonly };
package/dist/index.mjs ADDED
@@ -0,0 +1,243 @@
1
+ import { defineComponent as E, inject as B, ref as T, computed as w, provide as U, useAttrs as z, h as j } from "vue";
2
+ import { Form as K, Checkbox as O, Radio as N, Select as M, Switch as q, TimePicker as J, DatePicker as Q, Cascader as W, TreeSelect as X, Textarea as Y, InputNumber as Z, Input as _ } from "n-designv3";
3
+ function A(e) {
4
+ if (e == null) return "";
5
+ if (typeof e == "string") return e;
6
+ if (Array.isArray(e)) return e.map(A).join("");
7
+ if (typeof e == "object") {
8
+ if (typeof e.children == "string")
9
+ return e.children;
10
+ if (e?.default && typeof e.default == "function") {
11
+ const n = e.default();
12
+ if (Array.isArray(n))
13
+ return A(n);
14
+ }
15
+ e.children && A(e.children);
16
+ }
17
+ return "";
18
+ }
19
+ function P(e, n = {}) {
20
+ const t = /* @__PURE__ */ new Map(), { value: l = "value", label: c = "label" } = n;
21
+ if (!e) return t;
22
+ const s = Array.isArray(e) ? e : [e];
23
+ for (const r of s)
24
+ if (!(!r || typeof r != "object")) {
25
+ if (r.type && (typeof r.type == "object" || typeof r.type == "function")) {
26
+ let u = r.type.name?.toLowerCase() || "";
27
+ if (typeof r.type == "function" ? u = r.type.displayName?.toLowerCase() || "" : typeof r.type == "string" && (u = r.type?.toLowerCase() || ""), /option|radio|checkbox/i.test(u)) {
28
+ const o = r.props || {}, a = o[l] ?? o.value, f = o[c] ?? o.label;
29
+ if (a != null) {
30
+ const x = A(r.children) || f || String(a);
31
+ t.set(a, x);
32
+ }
33
+ }
34
+ }
35
+ r.children && P(r.children, n).forEach((o, a) => {
36
+ t.has(a) || t.set(a, o);
37
+ });
38
+ }
39
+ return t;
40
+ }
41
+ function V(e, n, t) {
42
+ const { value: l = "value", label: c = "label", children: s = "children" } = t, r = [];
43
+ let u = e;
44
+ for (const o of n) {
45
+ const a = u.find((f) => f[l] === o);
46
+ if (a)
47
+ r.push(a[c] || a[l] || String(o)), u = a[s] || [];
48
+ else {
49
+ r.push(String(o));
50
+ break;
51
+ }
52
+ }
53
+ return r;
54
+ }
55
+ function ee(e, n, t) {
56
+ const { value: l = "value", label: c = "title", children: s = "children" } = t, r = [], u = new Set(Array.isArray(n) ? n : [n]);
57
+ function o(a) {
58
+ for (const f of a)
59
+ u.has(f[l]) && r.push(f[c] || f[l]), f[s] && o(f[s]);
60
+ }
61
+ return o(e), r;
62
+ }
63
+ function te({
64
+ modelValue: e,
65
+ options: n,
66
+ treeData: t,
67
+ valueToLabel: l,
68
+ fieldNames: c,
69
+ slots: s,
70
+ isSelect: r,
71
+ isRadioGroup: u,
72
+ isCheckboxGroup: o,
73
+ isCheckbox: a,
74
+ isRadio: f,
75
+ isCascader: x,
76
+ isTreeSelect: m,
77
+ isSwitch: R,
78
+ attrs: d,
79
+ emptyText: b
80
+ }) {
81
+ if (l)
82
+ try {
83
+ return l(e) || b;
84
+ } catch (i) {
85
+ console.warn("[ReadonlyHOC] valueToLabel error", i);
86
+ }
87
+ if (R) {
88
+ const i = d.checkedValue ?? d["checked-value"] ?? !0, y = d.uncheckedValue ?? d["un-checked-value"] ?? !1;
89
+ return e === i ? d.checkedChildren ?? d["checked-children"] ?? "开启" : e === y ? d.uncheckedChildren ?? d["un-checked-children"] ?? "关闭" : String(e || b);
90
+ }
91
+ if (x && Array.isArray(e) && n?.length)
92
+ return V(n, e, c).join(" / ") || b;
93
+ if (m && t?.length)
94
+ return ee(t, e, c).join(", ") || b;
95
+ if (n?.length) {
96
+ const i = c.value || "value", y = c.label || "label", g = (v) => {
97
+ const C = n.find((k) => k[i] === v);
98
+ return C ? C[y] || C[i] : v == null ? "" : String(v);
99
+ };
100
+ return Array.isArray(e) ? e.map(g).join(", ") || b : g(e) || b;
101
+ } else if ((r || u || o) && s?.default)
102
+ try {
103
+ const i = s.default(), y = P(i, c);
104
+ if (y.size > 0) {
105
+ const g = (v) => v == null ? "" : y.get(v) || String(v);
106
+ return Array.isArray(e) ? e.map(g).join(", ") || b : g(e) || b;
107
+ }
108
+ } catch (i) {
109
+ console.warn("[ReadonlyHOC] Failed to parse slot options", i);
110
+ }
111
+ else if (s?.default && (a || f) && typeof e == "boolean")
112
+ try {
113
+ const i = s.default(), y = A(i);
114
+ if (y) return y;
115
+ } catch {
116
+ }
117
+ return e == null || e === "" ? b : Array.isArray(e) ? e.join(", ") : String(e);
118
+ }
119
+ function re(e) {
120
+ const n = e.name || "", t = n.toLowerCase(), l = /select/i.test(t) && !/tree|cascader/i.test(t), c = /radio/i.test(t) && !/group/i.test(t), s = /radio.*group/i.test(t), r = /checkbox/i.test(t) && !/group/i.test(t), u = /checkbox.*group/i.test(t), o = /switch/i.test(t), a = /cascader/i.test(t), f = /tree.*select/i.test(t), m = r || o || c ? "checked" : "value", R = `update:${m}`, d = /form$/i.test(t), b = /form.*item/i.test(t);
121
+ return E({
122
+ name: n,
123
+ inheritAttrs: !1,
124
+ props: {
125
+ modelValue: { type: [String, Number, Boolean, Array, Object], default: void 0 },
126
+ [m]: { type: [String, Number, Boolean, Array, Object], default: void 0 },
127
+ readonly: { type: Boolean, default: void 0 },
128
+ emptyText: { type: String, default: "--" },
129
+ valueToLabel: { type: Function, default: null }
130
+ },
131
+ emits: ["update:modelValue", R],
132
+ setup(i, { emit: y, slots: g, expose: v }) {
133
+ const C = B("nkReadonly", T(!1)), k = w(() => i.readonly ?? C.value), F = w(() => i.readonly ?? C.value);
134
+ d && U("nkReadonly", F);
135
+ const L = w(() => i[m] !== void 0 ? i[m] : i.modelValue), G = (h) => {
136
+ y("update:modelValue", h), y(R, h);
137
+ }, p = z(), I = w(() => {
138
+ if (!k.value) return "";
139
+ const h = p.fieldNames ?? p["field-names"] ?? {}, $ = p.options, D = p.treeData ?? p["tree-data"];
140
+ return te({
141
+ modelValue: L.value,
142
+ options: $,
143
+ treeData: D,
144
+ valueToLabel: i.valueToLabel,
145
+ fieldNames: h,
146
+ slots: g,
147
+ isSelect: l,
148
+ isRadioGroup: s,
149
+ isCheckboxGroup: u,
150
+ isCheckbox: r,
151
+ isRadio: c,
152
+ isCascader: a,
153
+ isTreeSelect: f,
154
+ isSwitch: o,
155
+ attrs: p,
156
+ emptyText: i.emptyText
157
+ });
158
+ }), H = {
159
+ lineHeight: "32px",
160
+ padding: "0 6px",
161
+ display: "inline-block",
162
+ minHeight: "32px",
163
+ border: "none",
164
+ backgroundColor: "none",
165
+ cursor: "default",
166
+ wordBreak: "break-all",
167
+ color: "rgba(0, 0, 0, 0.65)"
168
+ }, S = T();
169
+ return d && v({
170
+ validate: () => k.value ? Promise.resolve({}) : S.value?.validate?.(),
171
+ validateFields: (h) => k.value ? Promise.resolve({}) : S.value?.validateFields?.(h),
172
+ resetFields: (h) => !k.value && S.value?.resetFields?.(h),
173
+ clearValidate: (h) => !k.value && S.value?.clearValidate?.(h),
174
+ scrollToField: (h) => !k.value && S.value?.scrollToField?.(h)
175
+ }), () => b ? j(
176
+ e,
177
+ {
178
+ ...p,
179
+ style: { ...p.style || {}, marginBottom: k.value ? "10px" : void 0 }
180
+ },
181
+ g
182
+ ) : d ? j(
183
+ e,
184
+ {
185
+ ...p,
186
+ rules: F.value ? null : p.rules,
187
+ disabled: F.value ? !0 : p.disabled,
188
+ ref: S
189
+ },
190
+ g
191
+ ) : k.value ? j(
192
+ "span",
193
+ {
194
+ class: "nk-readonly-wrapper",
195
+ style: H,
196
+ role: "text",
197
+ tabindex: 0,
198
+ "aria-readonly": "true"
199
+ },
200
+ I.value
201
+ ) : j(
202
+ e,
203
+ {
204
+ ...p,
205
+ [m]: L.value,
206
+ [`onUpdate:${m}`]: G,
207
+ ref: d ? S : void 0
208
+ },
209
+ g
210
+ );
211
+ }
212
+ });
213
+ }
214
+ const ne = {
215
+ Input: _,
216
+ InputNumber: Z,
217
+ Textarea: Y,
218
+ Select: M,
219
+ SelectOption: M.Option,
220
+ TreeSelect: X,
221
+ Cascader: W,
222
+ DatePicker: Q,
223
+ TimePicker: J,
224
+ Radio: N,
225
+ Checkbox: O,
226
+ RadioGroup: N.Group,
227
+ CheckboxGroup: O.Group,
228
+ Switch: q,
229
+ Form: K,
230
+ FormItem: K.Item
231
+ }, ae = {
232
+ install(e) {
233
+ for (const [n, t] of Object.entries(ne))
234
+ if (t) {
235
+ const l = re(t);
236
+ e.component(`Nk${n}`, l);
237
+ }
238
+ }
239
+ };
240
+ export {
241
+ ae as NkReadonlyPlugin,
242
+ re as withReadonly
243
+ };
@@ -0,0 +1 @@
1
+ (function(A,o){typeof exports=="object"&&typeof module<"u"?o(exports,require("vue"),require("n-designv3")):typeof define=="function"&&define.amd?define(["exports","vue","n-designv3"],o):(A=typeof globalThis<"u"?globalThis:A||self,o(A.NDesignReadolyPlugin={},A.Vue,A.nDesignv3))})(this,(function(A,o,c){"use strict";function w(e){if(e==null)return"";if(typeof e=="string")return e;if(Array.isArray(e))return e.map(w).join("");if(typeof e=="object"){if(typeof e.children=="string")return e.children;if(e?.default&&typeof e.default=="function"){const n=e.default();if(Array.isArray(n))return w(n)}e.children&&w(e.children)}return""}function L(e,n={}){const t=new Map,{value:s="value",label:u="label"}=n;if(!e)return t;const d=Array.isArray(e)?e:[e];for(const r of d)if(!(!r||typeof r!="object")){if(r.type&&(typeof r.type=="object"||typeof r.type=="function")){let f=r.type.name?.toLowerCase()||"";if(typeof r.type=="function"?f=r.type.displayName?.toLowerCase()||"":typeof r.type=="string"&&(f=r.type?.toLowerCase()||""),/option|radio|checkbox/i.test(f)){const a=r.props||{},l=a[s]??a.value,y=a[u]??a.label;if(l!=null){const j=w(r.children)||y||String(l);t.set(l,j)}}}r.children&&L(r.children,n).forEach((a,l)=>{t.has(l)||t.set(l,a)})}return t}function O(e,n,t){const{value:s="value",label:u="label",children:d="children"}=t,r=[];let f=e;for(const a of n){const l=f.find(y=>y[s]===a);if(l)r.push(l[u]||l[s]||String(a)),f=l[d]||[];else{r.push(String(a));break}}return r}function K(e,n,t){const{value:s="value",label:u="title",children:d="children"}=t,r=[],f=new Set(Array.isArray(n)?n:[n]);function a(l){for(const y of l)f.has(y[s])&&r.push(y[u]||y[s]),y[d]&&a(y[d])}return a(e),r}function M({modelValue:e,options:n,treeData:t,valueToLabel:s,fieldNames:u,slots:d,isSelect:r,isRadioGroup:f,isCheckboxGroup:a,isCheckbox:l,isRadio:y,isCascader:j,isTreeSelect:R,isSwitch:F,attrs:p,emptyText:m}){if(s)try{return s(e)||m}catch(i){console.warn("[ReadonlyHOC] valueToLabel error",i)}if(F){const i=p.checkedValue??p["checked-value"]??!0,h=p.uncheckedValue??p["un-checked-value"]??!1;return e===i?p.checkedChildren??p["checked-children"]??"开启":e===h?p.uncheckedChildren??p["un-checked-children"]??"关闭":String(e||m)}if(j&&Array.isArray(e)&&n?.length)return O(n,e,u).join(" / ")||m;if(R&&t?.length)return K(t,e,u).join(", ")||m;if(n?.length){const i=u.value||"value",h=u.label||"label",g=S=>{const T=n.find(C=>C[i]===S);return T?T[h]||T[i]:S==null?"":String(S)};return Array.isArray(e)?e.map(g).join(", ")||m:g(e)||m}else if((r||f||a)&&d?.default)try{const i=d.default(),h=L(i,u);if(h.size>0){const g=S=>S==null?"":h.get(S)||String(S);return Array.isArray(e)?e.map(g).join(", ")||m:g(e)||m}}catch(i){console.warn("[ReadonlyHOC] Failed to parse slot options",i)}else if(d?.default&&(l||y)&&typeof e=="boolean")try{const i=d.default(),h=w(i);if(h)return h}catch{}return e==null||e===""?m:Array.isArray(e)?e.join(", "):String(e)}function P(e){const n=e.name||"",t=n.toLowerCase(),s=/select/i.test(t)&&!/tree|cascader/i.test(t),u=/radio/i.test(t)&&!/group/i.test(t),d=/radio.*group/i.test(t),r=/checkbox/i.test(t)&&!/group/i.test(t),f=/checkbox.*group/i.test(t),a=/switch/i.test(t),l=/cascader/i.test(t),y=/tree.*select/i.test(t),R=r||a||u?"checked":"value",F=`update:${R}`,p=/form$/i.test(t),m=/form.*item/i.test(t);return o.defineComponent({name:n,inheritAttrs:!1,props:{modelValue:{type:[String,Number,Boolean,Array,Object],default:void 0},[R]:{type:[String,Number,Boolean,Array,Object],default:void 0},readonly:{type:Boolean,default:void 0},emptyText:{type:String,default:"--"},valueToLabel:{type:Function,default:null}},emits:["update:modelValue",F],setup(i,{emit:h,slots:g,expose:S}){const T=o.inject("nkReadonly",o.ref(!1)),C=o.computed(()=>i.readonly??T.value),v=o.computed(()=>i.readonly??T.value);p&&o.provide("nkReadonly",v);const N=o.computed(()=>i[R]!==void 0?i[R]:i.modelValue),H=k=>{h("update:modelValue",k),h(F,k)},b=o.useAttrs(),$=o.computed(()=>{if(!C.value)return"";const k=b.fieldNames??b["field-names"]??{},q=b.options,B=b.treeData??b["tree-data"];return M({modelValue:N.value,options:q,treeData:B,valueToLabel:i.valueToLabel,fieldNames:k,slots:g,isSelect:s,isRadioGroup:d,isCheckboxGroup:f,isCheckbox:r,isRadio:u,isCascader:l,isTreeSelect:y,isSwitch:a,attrs:b,emptyText:i.emptyText})}),E={lineHeight:"32px",padding:"0 6px",display:"inline-block",minHeight:"32px",border:"none",backgroundColor:"none",cursor:"default",wordBreak:"break-all",color:"rgba(0, 0, 0, 0.65)"},x=o.ref();return p&&S({validate:()=>C.value?Promise.resolve({}):x.value?.validate?.(),validateFields:k=>C.value?Promise.resolve({}):x.value?.validateFields?.(k),resetFields:k=>!C.value&&x.value?.resetFields?.(k),clearValidate:k=>!C.value&&x.value?.clearValidate?.(k),scrollToField:k=>!C.value&&x.value?.scrollToField?.(k)}),()=>m?o.h(e,{...b,style:{...b.style||{},marginBottom:C.value?"10px":void 0}},g):p?o.h(e,{...b,rules:v.value?null:b.rules,disabled:v.value?!0:b.disabled,ref:x},g):C.value?o.h("span",{class:"nk-readonly-wrapper",style:E,role:"text",tabindex:0,"aria-readonly":"true"},$.value):o.h(e,{...b,[R]:N.value,[`onUpdate:${R}`]:H,ref:p?x:void 0},g)}})}const I={Input:c.Input,InputNumber:c.InputNumber,Textarea:c.Textarea,Select:c.Select,SelectOption:c.Select.Option,TreeSelect:c.TreeSelect,Cascader:c.Cascader,DatePicker:c.DatePicker,TimePicker:c.TimePicker,Radio:c.Radio,Checkbox:c.Checkbox,RadioGroup:c.Radio.Group,CheckboxGroup:c.Checkbox.Group,Switch:c.Switch,Form:c.Form,FormItem:c.Form.Item},G={install(e){for(const[n,t]of Object.entries(I))if(t){const s=P(t);e.component(`Nk${n}`,s)}}};A.NkReadonlyPlugin=G,A.withReadonly=P,Object.defineProperty(A,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,71 @@
1
+ import { PropType, Component, VNode } from 'vue';
2
+ type FormModelValue = string | number | boolean | any[] | null | undefined;
3
+ type ValueToLabelFn = (value: FormModelValue) => string;
4
+ export declare function withReadonly(BaseComponent: Component): import('vue').DefineComponent<{
5
+ [x: string]: {
6
+ type: PropType<FormModelValue>;
7
+ default: undefined;
8
+ } | {
9
+ type: BooleanConstructor;
10
+ default: undefined;
11
+ } | {
12
+ type: StringConstructor;
13
+ default: string;
14
+ } | {
15
+ type: PropType<ValueToLabelFn>;
16
+ default: null;
17
+ };
18
+ modelValue: {
19
+ type: PropType<FormModelValue>;
20
+ default: undefined;
21
+ };
22
+ readonly: {
23
+ type: BooleanConstructor;
24
+ default: undefined;
25
+ };
26
+ emptyText: {
27
+ type: StringConstructor;
28
+ default: string;
29
+ };
30
+ valueToLabel: {
31
+ type: PropType<ValueToLabelFn>;
32
+ default: null;
33
+ };
34
+ }, () => VNode<import('vue').RendererNode, import('vue').RendererElement, {
35
+ [key: string]: any;
36
+ }>, unknown, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, string[], string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<import('vue').ExtractPropTypes<{
37
+ [x: string]: {
38
+ type: PropType<FormModelValue>;
39
+ default: undefined;
40
+ } | {
41
+ type: BooleanConstructor;
42
+ default: undefined;
43
+ } | {
44
+ type: StringConstructor;
45
+ default: string;
46
+ } | {
47
+ type: PropType<ValueToLabelFn>;
48
+ default: null;
49
+ };
50
+ modelValue: {
51
+ type: PropType<FormModelValue>;
52
+ default: undefined;
53
+ };
54
+ readonly: {
55
+ type: BooleanConstructor;
56
+ default: undefined;
57
+ };
58
+ emptyText: {
59
+ type: StringConstructor;
60
+ default: string;
61
+ };
62
+ valueToLabel: {
63
+ type: PropType<ValueToLabelFn>;
64
+ default: null;
65
+ };
66
+ }>> & {
67
+ [x: `on${Capitalize<string>}`]: ((...args: any[]) => any) | undefined;
68
+ }, {
69
+ [x: string]: FormModelValue | ValueToLabelFn;
70
+ }, {}>;
71
+ export {};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "n-design-readonly-plugin",
3
+ "version": "1.0.0",
4
+ "description": "只读模式插件 for n-designv3",
5
+ "main": "./dist/index.cjs.js",
6
+ "module": "./dist/index.es.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.es.js",
12
+ "require": "./dist/index.cjs.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ ""
18
+ ],
19
+ "scripts": {
20
+ "dev": "vite --config vite.config.playground.ts",
21
+ "build": "vite build",
22
+ "preview": "vite preview"
23
+ },
24
+ "keywords": [
25
+ "vue",
26
+ "n-design-readonly-plugin",
27
+ "vue",
28
+ "readonly",
29
+ "n-designv3",
30
+ "form",
31
+ "hoc"
32
+ ],
33
+ "author": "pangsh<psh2416623245@163.com>",
34
+ "license": "ISC",
35
+ "packageManager": "pnpm@10.21.0",
36
+ "devDependencies": {
37
+ "@types/node": "^25.2.0",
38
+ "@vitejs/plugin-vue": "^6.0.3",
39
+ "n-designv3": "1.1.42",
40
+ "typescript": "^5.9.3",
41
+ "vite": "^7.3.1",
42
+ "vite-plugin-dts": "^4.5.4",
43
+ "vue": "3.3.4"
44
+ }
45
+ }
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div class="form-wrapper">
3
+ <nk-form :readonly="false" :model="form" :rules="rules">
4
+ <nk-form-item label="用户名" name="name">
5
+ <nk-input placeholder="用户名" v-model:value="form.name"></nk-input>
6
+ </nk-form-item>
7
+ <n-row>
8
+ <n-col :span="24">
9
+ <nk-form-item label="下拉options" name="ep6ClusterObjId">
10
+ <nk-select
11
+ allowClear
12
+ placeholder="请选择"
13
+ :options="options"
14
+ :showSearch="false"
15
+ v-model:value="form.ep6ClusterObjId"
16
+ >
17
+ </nk-select>
18
+ </nk-form-item>
19
+ </n-col>
20
+ </n-row>
21
+ <n-row>
22
+ <n-col :span="24">
23
+ <nk-form-item label="下拉options" name="ep6ClusterObjId">
24
+ <n-select
25
+ allowClear
26
+ placeholder="请选择"
27
+ :options="options"
28
+ :showSearch="false"
29
+ v-model:value="form.ep6ClusterObjId"
30
+ >
31
+ </n-select>
32
+ </nk-form-item>
33
+ </n-col>
34
+ </n-row>
35
+ </nk-form>
36
+ </div>
37
+ </template>
38
+
39
+ <script lang="ts" setup>
40
+ import { reactive } from "vue";
41
+
42
+ const form = reactive({
43
+ name: "张三",
44
+ ep6ClusterObjId: "1",
45
+ });
46
+ const options = [
47
+ {
48
+ label: "选项1",
49
+ value: "1",
50
+ externalValue: "externalValue1",
51
+ },
52
+ {
53
+ label: "选项2",
54
+ value: "2",
55
+ externalValue: "externalValue2",
56
+ },
57
+ {
58
+ label: "选项3",
59
+ value: "3",
60
+ externalValue: "externalValue3",
61
+ },
62
+ ];
63
+ const rules = {
64
+ name: [{ required: true, message: "请输入用户名" }],
65
+ ep6ClusterObjId: [{ required: true, message: "请选择" }],
66
+ };
67
+ </script>
68
+ <style>
69
+ .form-wrapper {
70
+ width: 100%;
71
+ height: 100vh;
72
+ padding: 20px;
73
+ box-sizing: border-box;
74
+ position: relative;
75
+ background-color: bisque;
76
+ }
77
+ </style>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" href="/favicon.ico" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>NkReadonlyHOC Playground</title>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="./main.ts"></script>
14
+ </body>
15
+
16
+ </html>
@@ -0,0 +1,12 @@
1
+ // main.ts
2
+ import { createApp } from 'vue';
3
+ import App from './App.vue';
4
+ import { NkReadonlyPlugin } from '../src/index';
5
+ import NDesign from 'n-designv3';
6
+ import 'n-designv3/dist/nancal.variable.min.css';
7
+ import './reset.css'
8
+
9
+ const app = createApp(App);
10
+ app.use(NDesign);
11
+ app.use(NkReadonlyPlugin); // ← 一行注册!
12
+ app.mount('#app');
@@ -0,0 +1,126 @@
1
+ html,
2
+ body,
3
+ div,
4
+ span,
5
+ applet,
6
+ object,
7
+ iframe,
8
+ p,
9
+ blockquote,
10
+ pre,
11
+ a,
12
+ abbr,
13
+ acronym,
14
+ address,
15
+ big,
16
+ cite,
17
+ code,
18
+ del,
19
+ dfn,
20
+ em,
21
+ img,
22
+ ins,
23
+ kbd,
24
+ q,
25
+ s,
26
+ samp,
27
+ small,
28
+ strike,
29
+ strong,
30
+ sub,
31
+ sup,
32
+ tt,
33
+ var,
34
+ b,
35
+ u,
36
+ i,
37
+ center,
38
+ dl,
39
+ dt,
40
+ dd,
41
+ ol,
42
+ ul,
43
+ li,
44
+ fieldset,
45
+ form,
46
+ label,
47
+ legend,
48
+ table,
49
+ caption,
50
+ tbody,
51
+ tfoot,
52
+ thead,
53
+ tr,
54
+ th,
55
+ td,
56
+ article,
57
+ aside,
58
+ canvas,
59
+ details,
60
+ embed,
61
+ figure,
62
+ figcaption,
63
+ footer,
64
+ header,
65
+ hgroup,
66
+ menu,
67
+ nav,
68
+ output,
69
+ ruby,
70
+ section,
71
+ summary,
72
+ time,
73
+ mark,
74
+ audio,
75
+ video {
76
+ margin: 0;
77
+ padding: 0;
78
+ border: 0;
79
+ font-size: 100%;
80
+ font: inherit;
81
+ // vertical-align: baseline;
82
+ }
83
+ /* HTML5 display-role reset for older browsers */
84
+ article,
85
+ aside,
86
+ details,
87
+ figcaption,
88
+ figure,
89
+ footer,
90
+ header,
91
+ hgroup,
92
+ menu,
93
+ nav,
94
+ section {
95
+ display: block;
96
+ }
97
+ body {
98
+ line-height: 1;
99
+ }
100
+ ol,
101
+ ul {
102
+ list-style: none;
103
+ }
104
+ blockquote,
105
+ q {
106
+ quotes: none;
107
+ }
108
+ blockquote:before,
109
+ blockquote:after,
110
+ q:before,
111
+ q:after {
112
+ content: '';
113
+ content: none;
114
+ }
115
+ table {
116
+ border-collapse: collapse;
117
+ border-spacing: 0;
118
+ }
119
+ html,
120
+ body {
121
+ width: 100%;
122
+ height: 100%;
123
+ padding: 0;
124
+ margin: 0;
125
+ font-family: 'PingFang SC-Medium', 'PingFang SC-Regular', 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', 'Arial', 'sans-serif';
126
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ // src/index.ts
2
+ import type { App, Plugin } from 'vue';
3
+ import { withReadonly } from './withReadonly';
4
+
5
+ // 👇 直接从 n-designv3 导入组件(构建时 external,不打包)
6
+ import {
7
+ Input,
8
+ InputNumber,
9
+ Textarea,
10
+ Select,
11
+ TreeSelect,
12
+ Cascader,
13
+ DatePicker,
14
+ TimePicker,
15
+ Radio,
16
+ Checkbox,
17
+ Switch,
18
+ Form,
19
+ } from 'n-designv3';
20
+
21
+ const COMPONENTS = {
22
+ Input,
23
+ InputNumber,
24
+ Textarea,
25
+ Select,
26
+ SelectOption: Select.Option,
27
+ TreeSelect,
28
+ Cascader,
29
+ DatePicker,
30
+ TimePicker,
31
+ Radio,
32
+ Checkbox,
33
+ RadioGroup: Radio.Group,
34
+ CheckboxGroup: Checkbox.Group,
35
+ Switch,
36
+ Form,
37
+ FormItem: Form.Item,
38
+ };
39
+
40
+ export const NkReadonlyPlugin: Plugin = {
41
+ install(app: App) {
42
+ for (const [name, Comp] of Object.entries(COMPONENTS)) {
43
+ if (Comp) {
44
+ const ReadonlyComp = withReadonly(Comp);
45
+ app.component(`Nk${name}`, ReadonlyComp);
46
+ }
47
+ }
48
+ },
49
+ };
50
+
51
+ // 可选:导出 HOC 供用户按需使用
52
+ export { withReadonly };
@@ -0,0 +1,11 @@
1
+ // src/types/env.d.ts
2
+ interface ImportMetaEnv {
3
+ readonly DEV: boolean;
4
+ readonly PROD: boolean;
5
+ readonly SSR: boolean;
6
+ // 可以根据实际需要添加其他环境变量
7
+ }
8
+
9
+ interface ImportMeta {
10
+ readonly env: ImportMetaEnv;
11
+ }
@@ -0,0 +1,414 @@
1
+ // composables/withReadonly.ts
2
+ import { defineComponent, h, computed, inject, provide, ref, watch, useAttrs, type PropType, type Component, type VNode, Ref } from 'vue';
3
+
4
+ type FormModelValue = string | number | boolean | any[] | null | undefined;
5
+ type ValueToLabelFn = (value: FormModelValue) => string;
6
+
7
+ // ========================
8
+ // 工具函数:提取静态文本(用于单个 checkbox/radio/select )
9
+ // ========================
10
+ function extractStaticText(vnode: any): string {
11
+ if (vnode == null) return '';
12
+ if (typeof vnode === 'string') return vnode;
13
+ if (Array.isArray(vnode)) return vnode.map(extractStaticText).join('');
14
+ if (typeof vnode === 'object') {
15
+ if (typeof vnode.children === 'string') {
16
+ return vnode.children;
17
+ }
18
+ // 处理动态插槽内容
19
+ if (vnode?.default && typeof vnode.default === 'function') {
20
+ const dynamicContent = vnode.default();
21
+ if (Array.isArray(dynamicContent)) {
22
+ return extractStaticText(dynamicContent);
23
+ }
24
+ }
25
+ if (vnode.children) {
26
+ extractStaticText(vnode.children);
27
+ }
28
+ }
29
+ return '';
30
+ }
31
+
32
+ // ========================
33
+ // 从插槽中提取 value → label 映射(支持 n-option / n-radio / n-checkbox / n-select)
34
+ // ========================
35
+ function collectOptionLabelMapFromSlots(vnodes: VNode | VNode[] | undefined, fieldNames: Record<string, string> = {}): Map<any, string> {
36
+ const labelMap = new Map<any, string>();
37
+ const { value: valKey = 'value', label: labelKey = 'label' } = fieldNames;
38
+
39
+ if (!vnodes) return labelMap;
40
+
41
+ const nodes = Array.isArray(vnodes) ? vnodes : [vnodes];
42
+
43
+ for (const vnode of nodes) {
44
+ if (!vnode || typeof vnode !== 'object') continue;
45
+
46
+ // 处理组件节点
47
+ if (vnode.type && (typeof vnode.type === 'object' || typeof vnode.type === 'function')) {
48
+ let compName = (vnode.type as any).name?.toLowerCase() || '';
49
+ if (typeof vnode.type === 'function') {
50
+ compName = (vnode.type as any).displayName?.toLowerCase() || '';
51
+ } else if (typeof vnode.type === 'string') {
52
+ compName = (vnode.type as any)?.toLowerCase() || '';
53
+ }
54
+ if (/option|radio|checkbox/i.test(compName)) {
55
+ const props = vnode.props || {};
56
+ const value = props[valKey] ?? props.value;
57
+ const labelText = props[labelKey] ?? props.label;
58
+ if (value != null) {
59
+ const label = extractStaticText(vnode.children) || labelText || String(value);
60
+ labelMap.set(value, label);
61
+ }
62
+ }
63
+ }
64
+
65
+ // 递归子节点
66
+ if (vnode.children) {
67
+ const childMap = collectOptionLabelMapFromSlots(vnode.children as any, fieldNames);
68
+ childMap.forEach((label, val) => {
69
+ if (!labelMap.has(val)) labelMap.set(val, label);
70
+ });
71
+ }
72
+ }
73
+
74
+ return labelMap;
75
+ }
76
+
77
+ // ========================
78
+ // Cascader: 递归查找 label 路径
79
+ // ========================
80
+ function findCascaderLabels(options: any[], value: any[], fieldNames: Record<string, string>): string[] {
81
+ const { value: valKey = 'value', label: labelKey = 'label', children: childrenKey = 'children' } = fieldNames;
82
+ const labels: string[] = [];
83
+ let current = options;
84
+
85
+ for (const v of value) {
86
+ const node = current.find((item: any) => item[valKey] === v);
87
+ if (node) {
88
+ labels.push(node[labelKey] || node[valKey] || String(v));
89
+ current = node[childrenKey] || [];
90
+ } else {
91
+ labels.push(String(v));
92
+ break;
93
+ }
94
+ }
95
+ return labels;
96
+ }
97
+
98
+ // ========================
99
+ // TreeSelect: 从 treeData 递归查找 label
100
+ // ========================
101
+ function findTreeSelectLabels(treeData: any[], value: any, fieldNames: Record<string, string>): string[] {
102
+ const { value: valKey = 'value', label: labelKey = 'title', children: childrenKey = 'children' } = fieldNames;
103
+ const labels: string[] = [];
104
+ const targetValues = new Set(Array.isArray(value) ? value : [value]);
105
+
106
+ function dfs(nodes: any[]) {
107
+ for (const node of nodes) {
108
+ if (targetValues.has(node[valKey])) {
109
+ labels.push(node[labelKey] || node[valKey]);
110
+ }
111
+ if (node[childrenKey]) {
112
+ dfs(node[childrenKey]);
113
+ }
114
+ }
115
+ }
116
+
117
+ dfs(treeData);
118
+ return labels;
119
+ }
120
+
121
+ // ========================
122
+ // 获取只读显示文本
123
+ // ========================
124
+ function getDisplayText({
125
+ modelValue,
126
+ options,
127
+ treeData,
128
+ valueToLabel,
129
+ fieldNames,
130
+ slots,
131
+ isSelect,
132
+ isRadioGroup,
133
+ isCheckboxGroup,
134
+ isCheckbox,
135
+ isRadio,
136
+ isCascader,
137
+ isTreeSelect,
138
+ isSwitch,
139
+ attrs,
140
+ emptyText,
141
+ }: {
142
+ modelValue: FormModelValue;
143
+ options?: any[];
144
+ treeData?: any[];
145
+ valueToLabel?: ValueToLabelFn;
146
+ fieldNames: Record<string, string>;
147
+ slots?: any;
148
+ isSelect: boolean;
149
+ isRadioGroup: boolean;
150
+ isCheckboxGroup: boolean;
151
+ isCheckbox: boolean;
152
+ isRadio: boolean;
153
+ isCascader: boolean;
154
+ isTreeSelect: boolean;
155
+ isSwitch: boolean;
156
+ attrs: Record<string, any>;
157
+ emptyText: string;
158
+ }): string {
159
+ // 1. 自定义映射
160
+ if (valueToLabel) {
161
+ try {
162
+ return valueToLabel(modelValue) || emptyText;
163
+ } catch (e) {
164
+ console.warn('[ReadonlyHOC] valueToLabel error', e);
165
+ }
166
+ }
167
+
168
+ // 2. Switch 特殊处理
169
+ if (isSwitch) {
170
+ const checkedVal = attrs.checkedValue ?? attrs['checked-value'] ?? true;
171
+ const uncheckedVal = attrs.uncheckedValue ?? attrs['un-checked-value'] ?? false;
172
+ if (modelValue === checkedVal) return attrs.checkedChildren ?? attrs['checked-children'] ?? '开启';
173
+ if (modelValue === uncheckedVal) return attrs.uncheckedChildren ?? attrs['un-checked-children'] ?? '关闭';
174
+ return String(modelValue || emptyText);
175
+ }
176
+
177
+ // 3. Cascader
178
+ if (isCascader && Array.isArray(modelValue) && options?.length) {
179
+ return findCascaderLabels(options, modelValue, fieldNames).join(' / ') || emptyText;
180
+ }
181
+
182
+ // 4. TreeSelect
183
+ if (isTreeSelect && treeData?.length) {
184
+ return findTreeSelectLabels(treeData, modelValue, fieldNames).join(', ') || emptyText;
185
+ }
186
+
187
+ // 5. 动态 options(Select / RadioGroup / CheckboxGroup)
188
+ if (options?.length) {
189
+ const valKey = fieldNames.value || 'value';
190
+ const labelKey = fieldNames.label || 'label';
191
+ const getLabel = (val: any) => {
192
+ const opt = options.find(item => item[valKey] === val);
193
+ return opt ? opt[labelKey] || opt[valKey] : val === undefined || val === null ? '' : String(val);
194
+ };
195
+ if (Array.isArray(modelValue)) {
196
+ return modelValue.map(getLabel).join(', ') || emptyText;
197
+ } else {
198
+ return getLabel(modelValue) || emptyText;
199
+ }
200
+ }
201
+
202
+ // 6. 静态插槽 fallback(Select / RadioGroup / CheckboxGroup)
203
+ else if ((isSelect || isRadioGroup || isCheckboxGroup) && slots?.default) {
204
+ try {
205
+ const slotContent = slots.default();
206
+
207
+ const labelMap = collectOptionLabelMapFromSlots(slotContent, fieldNames);
208
+ if (labelMap.size > 0) {
209
+ const getLabel = (val: any) => (val === undefined || val === null ? '' : labelMap.get(val) || String(val));
210
+ if (Array.isArray(modelValue)) {
211
+ return modelValue.map(getLabel).join(', ') || emptyText;
212
+ } else {
213
+ return getLabel(modelValue) || emptyText;
214
+ }
215
+ }
216
+ } catch (e) {
217
+ console.warn('[ReadonlyHOC] Failed to parse slot options', e);
218
+ }
219
+ }
220
+
221
+ // 7. 单个 Checkbox / Radio 静态文本兜底
222
+ else if (slots?.default && (isCheckbox || isRadio) && typeof modelValue === 'boolean') {
223
+ try {
224
+ const content = slots.default();
225
+ const text = extractStaticText(content);
226
+ if (text) return text;
227
+ } catch (e) {
228
+ /* ignore */
229
+ }
230
+ }
231
+
232
+ // 8. 最终兜底
233
+ if (modelValue == null || modelValue === '') return emptyText;
234
+ return Array.isArray(modelValue) ? modelValue.join(', ') : String(modelValue);
235
+ }
236
+
237
+ // ========================
238
+ // 高阶组件
239
+ // ========================
240
+ export function withReadonly(BaseComponent: Component) {
241
+ const baseName = BaseComponent.name || '';
242
+ const componentName = baseName.toLowerCase();
243
+
244
+ const isSelect = /select/i.test(componentName) && !/tree|cascader/i.test(componentName);
245
+ const isRadio = /radio/i.test(componentName) && !/group/i.test(componentName);
246
+ const isRadioGroup = /radio.*group/i.test(componentName);
247
+ const isCheckbox = /checkbox/i.test(componentName) && !/group/i.test(componentName);
248
+ const isCheckboxGroup = /checkbox.*group/i.test(componentName);
249
+ const isSwitch = /switch/i.test(componentName);
250
+ const isCascader = /cascader/i.test(componentName);
251
+ const isTreeSelect = /tree.*select/i.test(componentName);
252
+
253
+ const isCheckType = isCheckbox || isSwitch || isRadio;
254
+ const nativeProp = isCheckType ? 'checked' : 'value';
255
+ const updateEvent = `update:${nativeProp}`;
256
+
257
+ const isForm = /form$/i.test(componentName);
258
+ const isFormItem = /form.*item/i.test(componentName);
259
+
260
+ return defineComponent({
261
+ name: baseName,
262
+ inheritAttrs: false,
263
+ props: {
264
+ modelValue: { type: [String, Number, Boolean, Array, Object] as PropType<FormModelValue>, default: undefined },
265
+ [nativeProp]: { type: [String, Number, Boolean, Array, Object] as PropType<FormModelValue>, default: undefined },
266
+ readonly: { type: Boolean, default: undefined },
267
+ emptyText: { type: String, default: '--' },
268
+ valueToLabel: { type: Function as PropType<ValueToLabelFn>, default: null },
269
+ },
270
+ emits: ['update:modelValue', updateEvent],
271
+ setup(props, { emit, slots, expose }) {
272
+ const globalReadonly = inject<Ref<boolean>>('nkReadonly', ref(false));
273
+ const isReadonly = computed(() => props.readonly ?? globalReadonly.value);
274
+ const formReadonly = computed(() => props.readonly ?? globalReadonly.value);
275
+
276
+ if (isForm) provide('nkReadonly', formReadonly);
277
+
278
+ const finalValue = computed(() => (props[nativeProp] !== undefined ? props[nativeProp] : props.modelValue));
279
+
280
+ const emitUpdate = (val: FormModelValue) => {
281
+ emit('update:modelValue', val);
282
+ emit(updateEvent, val);
283
+ };
284
+
285
+ // ✅ 使用 useAttrs() 保证响应式
286
+ const attrs = useAttrs();
287
+
288
+ const displayText = computed(() => {
289
+ if (!isReadonly.value) return '';
290
+
291
+ const fieldNames = attrs.fieldNames ?? attrs['field-names'] ?? {};
292
+ const options = attrs.options as any[] | undefined;
293
+ const treeData = (attrs.treeData ?? attrs['tree-data']) as any[] | undefined;
294
+
295
+ return getDisplayText({
296
+ modelValue: finalValue.value as FormModelValue,
297
+ options,
298
+ treeData,
299
+ valueToLabel: props.valueToLabel as ValueToLabelFn,
300
+ fieldNames: fieldNames as Record<string, string>,
301
+ slots,
302
+ isSelect,
303
+ isRadioGroup,
304
+ isCheckboxGroup,
305
+ isCheckbox,
306
+ isRadio,
307
+ isCascader,
308
+ isTreeSelect,
309
+ isSwitch,
310
+ attrs,
311
+ emptyText: props.emptyText as string,
312
+ });
313
+ });
314
+
315
+ // ⚠️ 开发警告
316
+ if (import.meta?.env?.DEV) {
317
+ watch(
318
+ () => isReadonly.value,
319
+ readonly => {
320
+ if (
321
+ readonly &&
322
+ !attrs.options &&
323
+ !attrs.treeData &&
324
+ !props.valueToLabel &&
325
+ slots?.default &&
326
+ (isSelect || isRadioGroup || isCheckboxGroup || isCascader || isTreeSelect)
327
+ ) {
328
+ console.warn(
329
+ `[ReadonlyHOC] Detected slot-only usage for ${baseName} in readonly mode. ` +
330
+ `This works for static content, but will fail for async/dynamic data. ` +
331
+ `✅ Recommended: Use \`options\` or \`treeData\` prop instead.`
332
+ );
333
+ }
334
+ },
335
+ { immediate: true }
336
+ );
337
+ }
338
+
339
+ const readonlyStyle = {
340
+ lineHeight: '32px',
341
+ padding: '0 6px',
342
+ display: 'inline-block',
343
+ minHeight: '32px',
344
+ border: 'none',
345
+ backgroundColor: 'none',
346
+ cursor: 'default',
347
+ wordBreak: 'break-all',
348
+ color: 'rgba(0, 0, 0, 0.65)',
349
+ };
350
+
351
+ const formRef = ref<any>();
352
+ if (isForm) {
353
+ expose({
354
+ validate: () => (isReadonly.value ? Promise.resolve({}) : formRef.value?.validate?.()),
355
+ validateFields: (names?: string[]) => (isReadonly.value ? Promise.resolve({}) : formRef.value?.validateFields?.(names)),
356
+ resetFields: (names?: string[]) => !isReadonly.value && formRef.value?.resetFields?.(names),
357
+ clearValidate: (names?: string[]) => !isReadonly.value && formRef.value?.clearValidate?.(names),
358
+ scrollToField: (name: string) => !isReadonly.value && formRef.value?.scrollToField?.(name),
359
+ });
360
+ }
361
+
362
+ return () => {
363
+ if (isFormItem) {
364
+ return h(
365
+ BaseComponent,
366
+ {
367
+ ...attrs,
368
+ style: { ...(attrs.style || {}), marginBottom: isReadonly.value ? '10px' : undefined },
369
+ },
370
+ slots
371
+ );
372
+ }
373
+
374
+ if (isForm) {
375
+ return h(
376
+ BaseComponent,
377
+ {
378
+ ...attrs,
379
+ rules: formReadonly.value ? null : attrs.rules,
380
+ disabled: formReadonly.value ? true : attrs.disabled,
381
+ ref: formRef,
382
+ },
383
+ slots
384
+ );
385
+ }
386
+
387
+ if (isReadonly.value) {
388
+ return h(
389
+ 'span',
390
+ {
391
+ class: 'nk-readonly-wrapper',
392
+ style: readonlyStyle,
393
+ role: 'text',
394
+ tabindex: 0,
395
+ 'aria-readonly': 'true',
396
+ },
397
+ displayText.value
398
+ );
399
+ }
400
+
401
+ return h(
402
+ BaseComponent,
403
+ {
404
+ ...attrs,
405
+ [nativeProp]: finalValue.value,
406
+ [`onUpdate:${nativeProp}`]: emitUpdate,
407
+ ref: isForm ? formRef : undefined,
408
+ },
409
+ slots
410
+ );
411
+ };
412
+ },
413
+ });
414
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM"],
7
+ "moduleResolution": "bundler",
8
+ "strict": true,
9
+ "jsx": "preserve",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "declaration": true,
13
+ "declarationDir": "./dist",
14
+ "outDir": "./dist",
15
+ "allowSyntheticDefaultImports": true,
16
+ "forceConsistentCasingInFileNames": true
17
+ },
18
+ "include": ["src"]
19
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import { resolve } from 'path'
4
+
5
+ export default defineConfig({
6
+ root: 'playground',
7
+ plugins: [vue()],
8
+ resolve: {
9
+ alias: {
10
+ '@': resolve(__dirname, 'src'),
11
+ },
12
+ },
13
+ server: {
14
+ host: '0.0.0.0',
15
+ port: 5200,
16
+ },
17
+ })
package/vite.config.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { resolve } from 'path';
2
+ import { defineConfig } from 'vite';
3
+ import dts from 'vite-plugin-dts';
4
+
5
+ export default defineConfig({
6
+ build: {
7
+ lib: {
8
+ entry: resolve(__dirname, 'src/index.ts'),
9
+ name: 'NDesignReadolyPlugin',
10
+ fileName: 'index',
11
+ },
12
+ rollupOptions: {
13
+ // external 排除 vue 和 n-designv3
14
+ external: ['vue','n-designv3'],
15
+ output: { globals: { vue: 'Vue' } }
16
+ }
17
+ },
18
+ plugins: [dts({ insertTypesEntry: true })]
19
+ });