@manohub/ui 0.6.1 → 0.7.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 +12 -4
- package/dist/components/button/index.d.ts +8 -8
- package/dist/components/capsule/index.d.ts +13 -9
- package/dist/components/capsule/index.js +1 -1
- package/dist/components/card/index.js +7 -4
- package/dist/components/checkbox/index.js +1 -0
- package/dist/components/dialog/index.d.ts +58 -6
- package/dist/components/dialog/index.js +45 -8
- package/dist/components/drawer/index.d.ts +18 -6
- package/dist/components/drawer/index.js +11 -5
- package/dist/components/form/index.d.ts +115 -17
- package/dist/components/form/index.js +39 -9
- package/dist/components/input/index.d.ts +4 -4
- package/dist/components/input/index.js +1 -1
- package/dist/components/layout/index.d.ts +43 -1
- package/dist/components/layout/index.js +22 -2
- package/dist/components/notice/index.d.ts +24 -11
- package/dist/components/notice/index.js +11 -5
- package/dist/components/notification/index.d.ts +2 -2
- package/dist/components/page/index.js +4 -4
- package/dist/components/panel/index.js +4 -4
- package/dist/components/part-padding.d.ts +9 -6
- package/dist/components/part-padding.js +2 -1
- package/dist/components/radio/index.d.ts +1 -1
- package/dist/components/radio/index.js +1 -0
- package/dist/components/search/index.d.ts +26 -6
- package/dist/components/search/index.js +22 -4
- package/dist/components/steps/index.d.ts +232 -6
- package/dist/components/steps/index.js +84 -26
- package/dist/components/switch/index.d.ts +29 -5
- package/dist/components/switch/index.js +8 -2
- package/dist/components/tabbar/index.d.ts +184 -0
- package/dist/components/tabbar/index.js +161 -0
- package/dist/components/table/index.d.ts +74 -8
- package/dist/components/table/index.js +69 -16
- package/dist/components/tabs/index.d.ts +145 -125
- package/dist/components/tabs/index.js +93 -117
- package/dist/components/tag/index.d.ts +2 -2
- package/dist/components/text/index.d.ts +120 -0
- package/dist/components/text/index.js +55 -0
- package/dist/components/toast/index.d.ts +8 -8
- package/dist/components/toast/index.js +11 -2
- package/dist/components/upload/index.d.ts +30 -0
- package/dist/components/upload/index.js +7 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +6 -3
- package/dist/services/dialog.d.ts +20 -5
- package/dist/services/dialog.js +8 -1
- package/dist/styles/components/capsule.css +4 -4
- package/dist/styles/components/dialog.css +40 -1
- package/dist/styles/components/form.css +206 -26
- package/dist/styles/components/input.css +29 -2
- package/dist/styles/components/layout.css +2 -0
- package/dist/styles/components/select.css +11 -0
- package/dist/styles/components/steps.css +48 -0
- package/dist/styles/components/{tabs.css → tabbar.css} +66 -65
- package/dist/styles/components/tabbar.tokens.css +30 -0
- package/dist/styles/components/table.css +50 -1
- package/dist/styles/components/table.tokens.css +3 -0
- package/dist/styles/components/text.css +101 -0
- package/dist/styles/components/toast.css +12 -6
- package/dist/styles/index.css +8 -4
- package/dist/styles/operations/tabs.css +73 -0
- package/dist/styles/reset.css +205 -0
- package/package.json +2 -2
- package/dist/components/tabset/index.d.ts +0 -105
- package/dist/components/tabset/index.js +0 -68
- package/dist/styles/components/tabs.tokens.css +0 -30
- package/dist/styles/operations/tabset.css +0 -57
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, createVNode, Fragment } from "vue";
|
|
1
|
+
import { defineComponent, computed, createVNode, mergeProps, Fragment } from "vue";
|
|
2
2
|
import { Icon } from "@manohub/icon";
|
|
3
3
|
const Steps = /* @__PURE__ */ defineComponent({
|
|
4
4
|
name: "Steps",
|
|
@@ -22,27 +22,81 @@ const Steps = /* @__PURE__ */ defineComponent({
|
|
|
22
22
|
size: {
|
|
23
23
|
type: String,
|
|
24
24
|
default: "md"
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* 步骤可点:点某步即切到该步(**节点与标题整块**都是点击区)。
|
|
28
|
+
*
|
|
29
|
+
* 缺省 `false` —— 规范未演示跳步,件不凭空长交互。开了之后:
|
|
30
|
+
* - 步渲染成**原生 `<button type="button">`** ⇒ 键盘 Tab/Enter 可达、`:focus-visible` 有焦点环。
|
|
31
|
+
* 不要再靠 `title` 插槽自绘可点元素:那会嵌进 `<button>` 形成嵌套交互,且拿不到键盘支持;
|
|
32
|
+
* - 「哪几步能跳」由 `canJump` 决定(缺省全可跳);
|
|
33
|
+
* - 点击发 `update:current`(可写 `v-model:current`)与 `onChange`。
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ 「跳转前验证」写在**渲染期**(`disabled`),不要写成点击时的事件拦截 —— 见文件头。
|
|
36
|
+
*/
|
|
37
|
+
clickable: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: false
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* 跳转验证:**能不能跳到第 `index` 步**(第二参是当前步)。缺省全可跳。
|
|
43
|
+
*
|
|
44
|
+
* 返回 `false` 的步会被画成禁用态(置灰 + `not-allowed`)、拦掉点击、**也不进键盘序列** ——
|
|
45
|
+
* 判定跑在**渲染期**,不是「点了再拦」(那样只能得到「看着可点、点了没反应」)。
|
|
46
|
+
* 需要异步验证才能决定能不能跳时,把结果先存进状态、再据此返回。
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ 它只回答「能不能跳」。若某一步是「本身不可用」(不论从哪来),也用它统一表达 ——
|
|
49
|
+
* 本件不另设行级 `disabled`,免得同一条规则散在两处。
|
|
50
|
+
*/
|
|
51
|
+
canJump: {
|
|
52
|
+
type: Function,
|
|
53
|
+
default: void 0
|
|
54
|
+
},
|
|
55
|
+
/** 点击换步(`clickable` 档下生效);`prevIndex` 为点击前的当前步 */
|
|
56
|
+
onChange: {
|
|
57
|
+
type: Function,
|
|
58
|
+
default: void 0
|
|
25
59
|
}
|
|
26
60
|
},
|
|
61
|
+
emits: {
|
|
62
|
+
"update:current": (index) => true
|
|
63
|
+
},
|
|
27
64
|
slots: Object,
|
|
28
65
|
setup(props, {
|
|
29
66
|
slots,
|
|
30
|
-
attrs
|
|
67
|
+
attrs,
|
|
68
|
+
emit
|
|
31
69
|
}) {
|
|
70
|
+
const jumpable = computed(() => props.steps.map((_, index) => props.canJump?.(index, props.current) ?? true));
|
|
71
|
+
const selectStep = (index) => {
|
|
72
|
+
if (!jumpable.value[index]) return;
|
|
73
|
+
emit("update:current", index);
|
|
74
|
+
props.onChange?.(index, props.current);
|
|
75
|
+
};
|
|
76
|
+
const stepAction = (index) => props.clickable ? {
|
|
77
|
+
type: "button",
|
|
78
|
+
disabled: !jumpable.value[index],
|
|
79
|
+
onClick: () => selectStep(index)
|
|
80
|
+
} : {};
|
|
32
81
|
return () => {
|
|
82
|
+
const PartTag = props.clickable ? "button" : props.variant === "bar" ? "span" : "div";
|
|
33
83
|
const root = ["mh-steps", props.size === "md" ? "" : `mh-steps--${props.size}`, attrs.class].filter(Boolean).join(" ");
|
|
34
84
|
const style = attrs.style;
|
|
35
85
|
if (props.variant === "bar") {
|
|
36
86
|
return createVNode("div", {
|
|
37
87
|
"class": ["mh-steps__bar", attrs.class].filter(Boolean).join(" "),
|
|
38
88
|
"style": style
|
|
39
|
-
}, [props.steps.map((step, index) => createVNode(
|
|
89
|
+
}, [props.steps.map((step, index) => createVNode(PartTag, mergeProps({
|
|
40
90
|
"key": `${step.title}-${index}`,
|
|
41
|
-
"class": ["mh-steps__seg", index === props.current ? "mh-steps__seg--active" : ""].filter(Boolean).join(" ")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
index
|
|
45
|
-
|
|
91
|
+
"class": ["mh-steps__seg", index === props.current ? "mh-steps__seg--active" : "", props.clickable ? "mh-steps__seg--clickable" : "", props.clickable && !jumpable.value[index] ? "mh-steps__seg--disabled" : ""].filter(Boolean).join(" "),
|
|
92
|
+
"aria-current": index === props.current ? "step" : void 0
|
|
93
|
+
}, stepAction(index)), {
|
|
94
|
+
default: () => [createVNode("span", null, [index + 1]), slots.title ? slots.title({
|
|
95
|
+
item: step,
|
|
96
|
+
index,
|
|
97
|
+
disabled: !jumpable.value[index]
|
|
98
|
+
}) : step.title]
|
|
99
|
+
}))]);
|
|
46
100
|
}
|
|
47
101
|
return createVNode("div", {
|
|
48
102
|
"class": root,
|
|
@@ -57,24 +111,28 @@ const Steps = /* @__PURE__ */ defineComponent({
|
|
|
57
111
|
// 连线在「步 index-1 之后」:当前步之后那一段才转蓝
|
|
58
112
|
index - 1 === props.current ? "mh-steps__connector--active" : ""
|
|
59
113
|
].filter(Boolean).join(" ")
|
|
60
|
-
}, null) : null, createVNode(
|
|
61
|
-
"class": ["mh-steps__step", state].filter(Boolean).join(" ")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
114
|
+
}, null) : null, createVNode(PartTag, mergeProps({
|
|
115
|
+
"class": ["mh-steps__step", state, props.clickable ? "mh-steps__step--clickable" : "", props.clickable && !jumpable.value[index] ? "mh-steps__step--disabled" : ""].filter(Boolean).join(" "),
|
|
116
|
+
"aria-current": index === props.current ? "step" : void 0
|
|
117
|
+
}, stepAction(index)), {
|
|
118
|
+
default: () => [createVNode("span", {
|
|
119
|
+
"class": "mh-steps__node"
|
|
120
|
+
}, [index < props.current ? createVNode(Icon, {
|
|
121
|
+
"name": "check",
|
|
122
|
+
"class": "mh-steps__node-icon",
|
|
123
|
+
"size": 16
|
|
124
|
+
}, null) : step.icon ? createVNode(Icon, {
|
|
125
|
+
"name": step.icon,
|
|
126
|
+
"class": "mh-steps__node-icon",
|
|
127
|
+
"size": 16
|
|
128
|
+
}, null) : index + 1]), createVNode("span", {
|
|
129
|
+
"class": "mh-steps__title"
|
|
130
|
+
}, [slots.title ? slots.title({
|
|
131
|
+
item: step,
|
|
132
|
+
index,
|
|
133
|
+
disabled: !jumpable.value[index]
|
|
134
|
+
}) : step.title])]
|
|
135
|
+
})]);
|
|
78
136
|
})]);
|
|
79
137
|
};
|
|
80
138
|
}
|
|
@@ -13,8 +13,22 @@ import type { ControlSize } from '../control-size';
|
|
|
13
13
|
* - 选中态三源等价:原生 `:checked` / 组件下发 `data-state` / 静态档 `.is-checked`;
|
|
14
14
|
* - `labelPosition` 用 flex `order` 翻转,**不改 DOM 顺序** —— 否则那条 `~` 兄弟规则立刻失效。
|
|
15
15
|
*
|
|
16
|
+
* ## 与 `Form.Item` 的关系(**同一个词别写两遍**)
|
|
17
|
+
*
|
|
18
|
+
* `label` 是**控件自己的文案** —— 它长在根 `<label>` 里,**点它也切换开关**;
|
|
19
|
+
* 而 `Form.Item` 的 `label` 是**字段名**(点它只是聚焦控件)。两者不是一回事。
|
|
20
|
+
*
|
|
21
|
+
* 所以放进表单行时**不要再传一次**:字段名归 `Form.Item`,开关只放控件本身。
|
|
22
|
+
*
|
|
23
|
+
* - 正确:`<Form.Item label="启用"><Switch v-model={on} /></Form.Item>`
|
|
24
|
+
* - 错误:`<Form.Item label="启用"><Switch v-model={on} label="启用" /></Form.Item>`
|
|
25
|
+
* —— 同一行会显示成「启用 启用 (开关)」,同一个词出现两遍。
|
|
26
|
+
*
|
|
27
|
+
* 只有**没有表单行**的场合(表格行内 / 工具栏 / 设置面板)才需要 `label` 自己带文字;
|
|
28
|
+
* 那种场景若要把文字放到开关右侧,给 `labelPosition="right"`。
|
|
29
|
+
*
|
|
16
30
|
* @example
|
|
17
|
-
* <Switch modelValue={on}
|
|
31
|
+
* <Switch modelValue={on} onChange={(v) => (on = v)} />
|
|
18
32
|
*
|
|
19
33
|
* @example 轨道内文案 + 尺寸 + 加载
|
|
20
34
|
* <Switch v-model={auto} checkedText="开" uncheckedText="关" size="lg" loading={saving} />
|
|
@@ -25,7 +39,12 @@ export declare const Switch: import("vue").DefineComponent<import("vue").Extract
|
|
|
25
39
|
type: BooleanConstructor;
|
|
26
40
|
default: boolean;
|
|
27
41
|
};
|
|
28
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* 开关**自己的文案**(点它也切换;复杂内容走默认插槽)。
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ 放进 `Form.Item` 时**不要传** —— 字段名由 `Form.Item` 的 `label` 给,
|
|
46
|
+
* 两处写同一个词会显示成「启用 启用 (开关)」。独立使用(表格行内 / 工具栏 / 设置面板)才用它。
|
|
47
|
+
*/
|
|
29
48
|
label: {
|
|
30
49
|
type: StringConstructor;
|
|
31
50
|
default: undefined;
|
|
@@ -44,7 +63,7 @@ export declare const Switch: import("vue").DefineComponent<import("vue").Extract
|
|
|
44
63
|
type: BooleanConstructor;
|
|
45
64
|
default: boolean;
|
|
46
65
|
};
|
|
47
|
-
/**
|
|
66
|
+
/** 文字相对开关的位置(缺省在左)—— 表单行内一般不传 `label`,这条主要服务独立使用的场景 */
|
|
48
67
|
labelPosition: {
|
|
49
68
|
type: PropType<"left" | "right">;
|
|
50
69
|
default: string;
|
|
@@ -82,7 +101,12 @@ export declare const Switch: import("vue").DefineComponent<import("vue").Extract
|
|
|
82
101
|
type: BooleanConstructor;
|
|
83
102
|
default: boolean;
|
|
84
103
|
};
|
|
85
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* 开关**自己的文案**(点它也切换;复杂内容走默认插槽)。
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ 放进 `Form.Item` 时**不要传** —— 字段名由 `Form.Item` 的 `label` 给,
|
|
108
|
+
* 两处写同一个词会显示成「启用 启用 (开关)」。独立使用(表格行内 / 工具栏 / 设置面板)才用它。
|
|
109
|
+
*/
|
|
86
110
|
label: {
|
|
87
111
|
type: StringConstructor;
|
|
88
112
|
default: undefined;
|
|
@@ -101,7 +125,7 @@ export declare const Switch: import("vue").DefineComponent<import("vue").Extract
|
|
|
101
125
|
type: BooleanConstructor;
|
|
102
126
|
default: boolean;
|
|
103
127
|
};
|
|
104
|
-
/**
|
|
128
|
+
/** 文字相对开关的位置(缺省在左)—— 表单行内一般不传 `label`,这条主要服务独立使用的场景 */
|
|
105
129
|
labelPosition: {
|
|
106
130
|
type: PropType<"left" | "right">;
|
|
107
131
|
default: string;
|
|
@@ -8,7 +8,12 @@ const Switch = /* @__PURE__ */ defineComponent({
|
|
|
8
8
|
type: Boolean,
|
|
9
9
|
default: false
|
|
10
10
|
},
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* 开关**自己的文案**(点它也切换;复杂内容走默认插槽)。
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ 放进 `Form.Item` 时**不要传** —— 字段名由 `Form.Item` 的 `label` 给,
|
|
15
|
+
* 两处写同一个词会显示成「启用 启用 (开关)」。独立使用(表格行内 / 工具栏 / 设置面板)才用它。
|
|
16
|
+
*/
|
|
12
17
|
label: {
|
|
13
18
|
type: String,
|
|
14
19
|
default: void 0
|
|
@@ -27,7 +32,7 @@ const Switch = /* @__PURE__ */ defineComponent({
|
|
|
27
32
|
type: Boolean,
|
|
28
33
|
default: false
|
|
29
34
|
},
|
|
30
|
-
/**
|
|
35
|
+
/** 文字相对开关的位置(缺省在左)—— 表单行内一般不传 `label`,这条主要服务独立使用的场景 */
|
|
31
36
|
labelPosition: {
|
|
32
37
|
type: String,
|
|
33
38
|
default: "left"
|
|
@@ -76,6 +81,7 @@ const Switch = /* @__PURE__ */ defineComponent({
|
|
|
76
81
|
};
|
|
77
82
|
return () => createVNode("label", {
|
|
78
83
|
"class": ["mh-switch", resolvedSize.value === "md" ? "" : `mh-switch--${resolvedSize.value}`, props.form ? "mh-switch--form" : "", hasTrackText.value ? "mh-switch--with-text" : "", props.labelPosition === "right" ? "mh-switch--label-right" : "", props.loading ? "is-loading" : "", props.disabled ? "is-disabled" : "", props.modelValue ? "is-checked" : "", attrs.class].filter(Boolean).join(" "),
|
|
84
|
+
"data-inline-control": "true",
|
|
79
85
|
"style": attrs.style
|
|
80
86
|
}, [createVNode("input", {
|
|
81
87
|
"class": "mh-switch__input",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
2
|
+
import type { ControlSize } from '../control-size';
|
|
3
|
+
export interface TabBarOption {
|
|
4
|
+
/** 项文字 */
|
|
5
|
+
label: string;
|
|
6
|
+
/** 项值(受控值就是它) */
|
|
7
|
+
value: string | number;
|
|
8
|
+
/** 禁用项:不可选 */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** 单项可关(`closable` 打开时用它例外掉某些项) */
|
|
11
|
+
closable?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type TabBarValue = string | number;
|
|
14
|
+
/** 页签形态:`line` 下划线指示条 / `card` 选项卡(与内容区连成一体) */
|
|
15
|
+
export type TabBarType = 'line' | 'card';
|
|
16
|
+
/**
|
|
17
|
+
* 页签条(**自绘**;下划线或选项卡两种形态 + 三档尺寸 + 居中 + 可关闭 + 可新增 + 「更多」收纳)。
|
|
18
|
+
*
|
|
19
|
+
* ## 与 `Tabs` / `Capsule` 的分工(三者都能"切换",判据在**可见形态**)
|
|
20
|
+
*
|
|
21
|
+
* - 本件:**页签条**(`line` 下划线 / `card` 选项卡,页面级分区切换)—— 就是这一族的皮肤,
|
|
22
|
+
* 也是 `Tabs` **不传 `selector` 插槽时的缺省皮肤**(只给 `options` + 面板就够);
|
|
23
|
+
* - `Capsule`:胶囊/分段按钮(块状选中底),用于筛选项那种"按钮感"切换 —— 同样能塞进 `Tabs` 的 `selector`;
|
|
24
|
+
* - `Tabs`:**骨架**(把某组互斥项与面板显隐接起来),它不含皮肤:缺省借本件,传了 `selector` 就整块换掉。
|
|
25
|
+
*
|
|
26
|
+
* ## 指示条为什么不是一个滑块
|
|
27
|
+
*
|
|
28
|
+
* 每个项的指示条是它自己的伪元素、常驻但未选时 `opacity:0; scale:.75` —— "滑过去"的观感来自这两个属性的
|
|
29
|
+
* 过渡。做成一条共享滑块要 JS 量位置,且首帧会跳。**别在页签区加轨线**(会扣走区高 1px,规范明确警告)。
|
|
30
|
+
* (`card` 档不画指示条:选中由底色与顶边表达。)
|
|
31
|
+
*
|
|
32
|
+
* ## 超过一屏的页签怎么放
|
|
33
|
+
*
|
|
34
|
+
* 给 `more` 插槽即出「更多 ▾」页签 + 操作层浮层(原生 popover,零 JS 开合);哪些页签收进"更多"由调用方
|
|
35
|
+
* 决定 —— 收多久、收哪些是业务判断,不是皮肤能定的。
|
|
36
|
+
*
|
|
37
|
+
* @example 基本
|
|
38
|
+
* <TabBar :options="[{label:'全部',value:'all'},{label:'进行中',value:'doing'}]" v-model:value="current" />
|
|
39
|
+
*
|
|
40
|
+
* @example 选项卡形态 + 可关闭可新增
|
|
41
|
+
* <TabBar type="card" closable addable :options="tabs" v-model:value="current"
|
|
42
|
+
* :on-close="remove" :on-add="add" />
|
|
43
|
+
*/
|
|
44
|
+
export declare const TabBar: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
45
|
+
options: {
|
|
46
|
+
type: PropType<TabBarOption[]>;
|
|
47
|
+
required: true;
|
|
48
|
+
};
|
|
49
|
+
/** 受控选中值;不传即非受控(由 `defaultValue` 起) */
|
|
50
|
+
value: {
|
|
51
|
+
type: PropType<TabBarValue | undefined>;
|
|
52
|
+
default: undefined;
|
|
53
|
+
};
|
|
54
|
+
/** 非受控初始选中值,缺省第一项 */
|
|
55
|
+
defaultValue: {
|
|
56
|
+
type: PropType<TabBarValue | undefined>;
|
|
57
|
+
default: undefined;
|
|
58
|
+
};
|
|
59
|
+
/** 选中变化回调(受控与非受控都会调) */
|
|
60
|
+
onChange: {
|
|
61
|
+
type: PropType<(value: TabBarValue) => void>;
|
|
62
|
+
default: undefined;
|
|
63
|
+
};
|
|
64
|
+
/** 形态:`line` 下划线 / `card` 选项卡 */
|
|
65
|
+
type: {
|
|
66
|
+
type: PropType<TabBarType>;
|
|
67
|
+
default: string;
|
|
68
|
+
};
|
|
69
|
+
/** 整体居中(页签少、要撑满整行时用) */
|
|
70
|
+
centered: {
|
|
71
|
+
type: BooleanConstructor;
|
|
72
|
+
default: boolean;
|
|
73
|
+
};
|
|
74
|
+
/** 尺寸档(24 / 32 / 40,只覆盖令牌) */
|
|
75
|
+
size: {
|
|
76
|
+
type: PropType<ControlSize>;
|
|
77
|
+
default: string;
|
|
78
|
+
};
|
|
79
|
+
/** 每项带关闭位(单项可用 `option.closable` 覆写) */
|
|
80
|
+
closable: {
|
|
81
|
+
type: BooleanConstructor;
|
|
82
|
+
default: boolean;
|
|
83
|
+
};
|
|
84
|
+
/** 尾部「新增」按钮 */
|
|
85
|
+
addable: {
|
|
86
|
+
type: BooleanConstructor;
|
|
87
|
+
default: boolean;
|
|
88
|
+
};
|
|
89
|
+
/** 「更多」页签的文字 */
|
|
90
|
+
moreLabel: {
|
|
91
|
+
type: StringConstructor;
|
|
92
|
+
default: string;
|
|
93
|
+
};
|
|
94
|
+
/** 关闭某项(只回调,**不从 `options` 里删** —— 数据归调用方) */
|
|
95
|
+
onClose: {
|
|
96
|
+
type: PropType<(value: TabBarValue) => void>;
|
|
97
|
+
default: undefined;
|
|
98
|
+
};
|
|
99
|
+
/** 点新增(只回调) */
|
|
100
|
+
onAdd: {
|
|
101
|
+
type: PropType<() => void>;
|
|
102
|
+
default: undefined;
|
|
103
|
+
};
|
|
104
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
105
|
+
'update:value': (value: TabBarValue) => true;
|
|
106
|
+
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
107
|
+
options: {
|
|
108
|
+
type: PropType<TabBarOption[]>;
|
|
109
|
+
required: true;
|
|
110
|
+
};
|
|
111
|
+
/** 受控选中值;不传即非受控(由 `defaultValue` 起) */
|
|
112
|
+
value: {
|
|
113
|
+
type: PropType<TabBarValue | undefined>;
|
|
114
|
+
default: undefined;
|
|
115
|
+
};
|
|
116
|
+
/** 非受控初始选中值,缺省第一项 */
|
|
117
|
+
defaultValue: {
|
|
118
|
+
type: PropType<TabBarValue | undefined>;
|
|
119
|
+
default: undefined;
|
|
120
|
+
};
|
|
121
|
+
/** 选中变化回调(受控与非受控都会调) */
|
|
122
|
+
onChange: {
|
|
123
|
+
type: PropType<(value: TabBarValue) => void>;
|
|
124
|
+
default: undefined;
|
|
125
|
+
};
|
|
126
|
+
/** 形态:`line` 下划线 / `card` 选项卡 */
|
|
127
|
+
type: {
|
|
128
|
+
type: PropType<TabBarType>;
|
|
129
|
+
default: string;
|
|
130
|
+
};
|
|
131
|
+
/** 整体居中(页签少、要撑满整行时用) */
|
|
132
|
+
centered: {
|
|
133
|
+
type: BooleanConstructor;
|
|
134
|
+
default: boolean;
|
|
135
|
+
};
|
|
136
|
+
/** 尺寸档(24 / 32 / 40,只覆盖令牌) */
|
|
137
|
+
size: {
|
|
138
|
+
type: PropType<ControlSize>;
|
|
139
|
+
default: string;
|
|
140
|
+
};
|
|
141
|
+
/** 每项带关闭位(单项可用 `option.closable` 覆写) */
|
|
142
|
+
closable: {
|
|
143
|
+
type: BooleanConstructor;
|
|
144
|
+
default: boolean;
|
|
145
|
+
};
|
|
146
|
+
/** 尾部「新增」按钮 */
|
|
147
|
+
addable: {
|
|
148
|
+
type: BooleanConstructor;
|
|
149
|
+
default: boolean;
|
|
150
|
+
};
|
|
151
|
+
/** 「更多」页签的文字 */
|
|
152
|
+
moreLabel: {
|
|
153
|
+
type: StringConstructor;
|
|
154
|
+
default: string;
|
|
155
|
+
};
|
|
156
|
+
/** 关闭某项(只回调,**不从 `options` 里删** —— 数据归调用方) */
|
|
157
|
+
onClose: {
|
|
158
|
+
type: PropType<(value: TabBarValue) => void>;
|
|
159
|
+
default: undefined;
|
|
160
|
+
};
|
|
161
|
+
/** 点新增(只回调) */
|
|
162
|
+
onAdd: {
|
|
163
|
+
type: PropType<() => void>;
|
|
164
|
+
default: undefined;
|
|
165
|
+
};
|
|
166
|
+
}>> & Readonly<{
|
|
167
|
+
"onUpdate:value"?: ((value: TabBarValue) => any) | undefined;
|
|
168
|
+
}>, {
|
|
169
|
+
size: ControlSize;
|
|
170
|
+
value: TabBarValue | undefined;
|
|
171
|
+
type: TabBarType;
|
|
172
|
+
onChange: (value: TabBarValue) => void;
|
|
173
|
+
onClose: (value: TabBarValue) => void;
|
|
174
|
+
defaultValue: TabBarValue | undefined;
|
|
175
|
+
centered: boolean;
|
|
176
|
+
closable: boolean;
|
|
177
|
+
addable: boolean;
|
|
178
|
+
moreLabel: string;
|
|
179
|
+
onAdd: () => void;
|
|
180
|
+
}, SlotsType<{
|
|
181
|
+
more?: (context: {
|
|
182
|
+
options: TabBarOption[];
|
|
183
|
+
}) => VNodeChild;
|
|
184
|
+
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { defineComponent, ref, computed, createVNode, Fragment, mergeProps } from "vue";
|
|
2
|
+
import { Icon } from "@manohub/icon";
|
|
3
|
+
const TabBar = /* @__PURE__ */ defineComponent({
|
|
4
|
+
name: "TabBar",
|
|
5
|
+
inheritAttrs: false,
|
|
6
|
+
props: {
|
|
7
|
+
options: {
|
|
8
|
+
type: Array,
|
|
9
|
+
required: true
|
|
10
|
+
},
|
|
11
|
+
/** 受控选中值;不传即非受控(由 `defaultValue` 起) */
|
|
12
|
+
value: {
|
|
13
|
+
type: [String, Number],
|
|
14
|
+
default: void 0
|
|
15
|
+
},
|
|
16
|
+
/** 非受控初始选中值,缺省第一项 */
|
|
17
|
+
defaultValue: {
|
|
18
|
+
type: [String, Number],
|
|
19
|
+
default: void 0
|
|
20
|
+
},
|
|
21
|
+
/** 选中变化回调(受控与非受控都会调) */
|
|
22
|
+
onChange: {
|
|
23
|
+
type: Function,
|
|
24
|
+
default: void 0
|
|
25
|
+
},
|
|
26
|
+
/** 形态:`line` 下划线 / `card` 选项卡 */
|
|
27
|
+
type: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: "line"
|
|
30
|
+
},
|
|
31
|
+
/** 整体居中(页签少、要撑满整行时用) */
|
|
32
|
+
centered: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: false
|
|
35
|
+
},
|
|
36
|
+
/** 尺寸档(24 / 32 / 40,只覆盖令牌) */
|
|
37
|
+
size: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: "md"
|
|
40
|
+
},
|
|
41
|
+
/** 每项带关闭位(单项可用 `option.closable` 覆写) */
|
|
42
|
+
closable: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
default: false
|
|
45
|
+
},
|
|
46
|
+
/** 尾部「新增」按钮 */
|
|
47
|
+
addable: {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
default: false
|
|
50
|
+
},
|
|
51
|
+
/** 「更多」页签的文字 */
|
|
52
|
+
moreLabel: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: "更多"
|
|
55
|
+
},
|
|
56
|
+
/** 关闭某项(只回调,**不从 `options` 里删** —— 数据归调用方) */
|
|
57
|
+
onClose: {
|
|
58
|
+
type: Function,
|
|
59
|
+
default: void 0
|
|
60
|
+
},
|
|
61
|
+
/** 点新增(只回调) */
|
|
62
|
+
onAdd: {
|
|
63
|
+
type: Function,
|
|
64
|
+
default: void 0
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
/* 受控值必须 emit 回 `update:value`,否则 `v-model:value="x"` 是**静默失效**的:
|
|
68
|
+
受控时 `props.value !== undefined` ⇒ 内部状态不更新,而外部又收不到新值 ⇒ 点 tab 毫无反应
|
|
69
|
+
(不报错、不警告)。本件 0.6.x 就漏了这条 —— 文档示例教 `v-model:value` 却点了不动。 */
|
|
70
|
+
emits: {
|
|
71
|
+
"update:value": (value) => true
|
|
72
|
+
},
|
|
73
|
+
slots: Object,
|
|
74
|
+
setup(props, {
|
|
75
|
+
slots,
|
|
76
|
+
attrs,
|
|
77
|
+
emit
|
|
78
|
+
}) {
|
|
79
|
+
const innerValue = ref(props.value ?? props.defaultValue ?? props.options[0]?.value);
|
|
80
|
+
const current = computed(() => props.value ?? innerValue.value);
|
|
81
|
+
const moreId = `mh-tabbar-${Math.random().toString(36).slice(2, 10)}`;
|
|
82
|
+
const select = (value) => {
|
|
83
|
+
const target = props.options.find((option) => option.value === value);
|
|
84
|
+
if (target?.disabled) return;
|
|
85
|
+
if (props.value === void 0) innerValue.value = value;
|
|
86
|
+
emit("update:value", value);
|
|
87
|
+
props.onChange?.(value);
|
|
88
|
+
};
|
|
89
|
+
const isClosable = (option) => option.closable ?? props.closable;
|
|
90
|
+
return () => createVNode("nav", {
|
|
91
|
+
"class": [
|
|
92
|
+
"mh-tabbar",
|
|
93
|
+
/* `mh-popup-host` 是操作层展开转向的**宿主标记**(popup.css 靠它 :has 反推面板开合):
|
|
94
|
+
没有它,「更多」的 caret 展开时不会翻向 —— 与 Select / Tooltip 同一分工。 */
|
|
95
|
+
"mh-popup-host",
|
|
96
|
+
props.type === "card" ? "mh-tabbar--card" : "",
|
|
97
|
+
props.centered ? "mh-tabbar--centered" : "",
|
|
98
|
+
props.size === "md" ? "" : `mh-tabbar--${props.size}`,
|
|
99
|
+
attrs.class
|
|
100
|
+
].filter(Boolean).join(" "),
|
|
101
|
+
"style": attrs.style,
|
|
102
|
+
"role": "tablist"
|
|
103
|
+
}, [props.options.map((option) => {
|
|
104
|
+
const active = option.value === current.value;
|
|
105
|
+
return createVNode("button", {
|
|
106
|
+
"key": option.value,
|
|
107
|
+
"type": "button",
|
|
108
|
+
"role": "tab",
|
|
109
|
+
"class": ["mh-tabbar__tab", active ? "is-selected" : "", option.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
110
|
+
"data-state": active ? "active" : "inactive",
|
|
111
|
+
"aria-selected": active,
|
|
112
|
+
"disabled": option.disabled,
|
|
113
|
+
"onClick": () => select(option.value)
|
|
114
|
+
}, [option.label, isClosable(option) && !option.disabled ? (
|
|
115
|
+
/* 关闭位不是 `<button>`:页签自己是按钮,按钮里嵌按钮是非法 HTML。
|
|
116
|
+
用 `role="button"` + `stopPropagation` 表达同样的语义与行为。 */
|
|
117
|
+
createVNode("span", {
|
|
118
|
+
"class": "mh-tabbar__close",
|
|
119
|
+
"role": "button",
|
|
120
|
+
"tabindex": -1,
|
|
121
|
+
"aria-label": `关闭${option.label}`,
|
|
122
|
+
"onClick": (event) => {
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
props.onClose?.(option.value);
|
|
126
|
+
}
|
|
127
|
+
}, [createVNode(Icon, {
|
|
128
|
+
"name": "close"
|
|
129
|
+
}, null)])
|
|
130
|
+
) : null]);
|
|
131
|
+
}), props.addable ? createVNode("button", {
|
|
132
|
+
"type": "button",
|
|
133
|
+
"class": "mh-tabbar__add",
|
|
134
|
+
"aria-label": "新增页签",
|
|
135
|
+
"onClick": () => props.onAdd?.()
|
|
136
|
+
}, [createVNode(Icon, {
|
|
137
|
+
"name": "plus"
|
|
138
|
+
}, null)]) : null, slots.more ? createVNode(Fragment, null, [createVNode("button", mergeProps({
|
|
139
|
+
"type": "button",
|
|
140
|
+
"class": "mh-tabbar__tab mh-tabbar__tab--more",
|
|
141
|
+
"data-state": "inactive"
|
|
142
|
+
}, {
|
|
143
|
+
popovertarget: moreId
|
|
144
|
+
}), [props.moreLabel, createVNode(Icon, {
|
|
145
|
+
"name": "chevron-down",
|
|
146
|
+
"class": "mh-popup-caret"
|
|
147
|
+
}, null)]), createVNode("div", mergeProps({
|
|
148
|
+
"id": moreId,
|
|
149
|
+
"class": "mh-popup",
|
|
150
|
+
"data-side": "bottom",
|
|
151
|
+
"data-align": "end"
|
|
152
|
+
}, {
|
|
153
|
+
popover: "auto"
|
|
154
|
+
}), [slots.more({
|
|
155
|
+
options: props.options
|
|
156
|
+
})])]) : null]);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
export {
|
|
160
|
+
TabBar
|
|
161
|
+
};
|