@mtn-ui-z/utils 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 +161 -0
- package/package.json +23 -0
- package/src/common.d.ts +3 -0
- package/src/common.ts +16 -0
- package/src/date.ts +34 -0
- package/src/dict.ts +193 -0
- package/src/dom.d.ts +8 -0
- package/src/dom.ts +20 -0
- package/src/function.d.ts +1 -0
- package/src/function.ts +9 -0
- package/src/index.d.ts +36 -0
- package/src/index.ts +61 -0
- package/src/interaction.d.ts +1 -0
- package/src/interaction.ts +10 -0
- package/src/media.d.ts +6 -0
- package/src/media.ts +11 -0
- package/src/modalRegistry.ts +37 -0
- package/src/permission.d.ts +27 -0
- package/src/permission.ts +165 -0
- package/src/request-types.ts +57 -0
- package/src/request.ts +178 -0
- package/src/state.d.ts +6 -0
- package/src/state.ts +11 -0
- package/src/storage.ts +143 -0
- package/src/theme.d.ts +107 -0
- package/src/theme.ts +502 -0
- package/src/types.d.ts +6 -0
- package/src/types.ts +11 -0
- package/src/useModal.ts +110 -0
- package/src/useTable.ts +307 -0
package/src/theme.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行时主题 / 密度 / 色板
|
|
3
|
+
*
|
|
4
|
+
* - CSS 变量由 @mtn-ui-z/theme 在 :root 提供默认值;业务未分层样式写在最后可覆盖 theme(见 theme README 的 @layer 说明)。
|
|
5
|
+
* - 亮/暗:`setThemeMode` 维护 `data-theme="dark"`(亮为移除属性),与 `[data-theme="dark"]` 令牌对齐。
|
|
6
|
+
* - 紧凑:`setDensityMode` 维护 `data-mtn-density="compact"`(舒适为移除属性)。
|
|
7
|
+
* - Ant:根组件使用 ConfigProvider,`:theme` 绑定含 `antThemeConfigRef` 与 `algorithm`(随 themeModeRef),见官方定制主题文档。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { nextTick, ref } from 'vue'
|
|
11
|
+
|
|
12
|
+
/** 可覆盖的主题色(与 theme tokens 中变量对应) */
|
|
13
|
+
export interface ThemeColorOverrides {
|
|
14
|
+
primary?: string
|
|
15
|
+
primaryHover?: string
|
|
16
|
+
primaryActive?: string
|
|
17
|
+
success?: string
|
|
18
|
+
successHover?: string
|
|
19
|
+
warning?: string
|
|
20
|
+
warningHover?: string
|
|
21
|
+
danger?: string
|
|
22
|
+
dangerHover?: string
|
|
23
|
+
dangerActive?: string
|
|
24
|
+
info?: string
|
|
25
|
+
infoHover?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 亮色 / 暗色模式 */
|
|
29
|
+
export type ThemeMode = 'light' | 'dark'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 全局紧凑程度(与 html 上 data-mtn-density 一致)
|
|
33
|
+
* - default:舒适,不写属性
|
|
34
|
+
* - compact:紧凑,写入 data-mtn-density="compact"
|
|
35
|
+
*/
|
|
36
|
+
export type DensityMode = 'default' | 'compact'
|
|
37
|
+
|
|
38
|
+
/** 色板名:`default` 表示不设置 data-mtn-preset;其余需 @mtn-ui-z/theme 或业务提供对应样式 */
|
|
39
|
+
export const MTN_THEME_PRESETS = ['default', 'ocean'] as const
|
|
40
|
+
|
|
41
|
+
/** documentElement 上 data-mtn-preset 的合法值 */
|
|
42
|
+
export type MtnThemePreset = (typeof MTN_THEME_PRESETS)[number]
|
|
43
|
+
|
|
44
|
+
const MTN_PRESET_ATTR = 'data-mtn-preset'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Ant Design Vue 4 ConfigProvider 的 theme.token 子集
|
|
48
|
+
* 颜色/圆角来自 CSS 变量;尺寸类来自 --mtn-control-height、--mtn-padding-* 等(随密度变化)
|
|
49
|
+
*/
|
|
50
|
+
export interface MtnAntThemeToken {
|
|
51
|
+
colorPrimary?: string
|
|
52
|
+
colorPrimaryHover?: string
|
|
53
|
+
colorPrimaryActive?: string
|
|
54
|
+
/** Link 按钮/文本色(如 ant-btn-link、a 链接) */
|
|
55
|
+
colorLink?: string
|
|
56
|
+
colorLinkHover?: string
|
|
57
|
+
colorLinkActive?: string
|
|
58
|
+
borderRadius?: number
|
|
59
|
+
colorBgBase?: string
|
|
60
|
+
colorText?: string
|
|
61
|
+
colorTextSecondary?: string
|
|
62
|
+
colorBorder?: string
|
|
63
|
+
colorSuccess?: string
|
|
64
|
+
colorWarning?: string
|
|
65
|
+
colorError?: string
|
|
66
|
+
colorInfo?: string
|
|
67
|
+
/** 控件高度(px 数值),对应 --mtn-control-height */
|
|
68
|
+
controlHeight?: number
|
|
69
|
+
fontSize?: number
|
|
70
|
+
padding?: number
|
|
71
|
+
paddingXS?: number
|
|
72
|
+
paddingSM?: number
|
|
73
|
+
paddingLG?: number
|
|
74
|
+
margin?: number
|
|
75
|
+
marginXS?: number
|
|
76
|
+
marginSM?: number
|
|
77
|
+
marginLG?: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 当前主题模式(亮/暗),供 ConfigProvider 与 algorithm 组合使用,并同步到 document.documentElement 的 data-theme */
|
|
81
|
+
export const themeModeRef = ref<ThemeMode>('light')
|
|
82
|
+
|
|
83
|
+
/** 当前密度(舒适 / 紧凑),与 data-mtn-density 同步 */
|
|
84
|
+
export const densityModeRef = ref<DensityMode>('default')
|
|
85
|
+
|
|
86
|
+
const THEME_VAR_MAP: Record<keyof ThemeColorOverrides, string> = {
|
|
87
|
+
primary: '--color-primary',
|
|
88
|
+
primaryHover: '--color-primary-hover',
|
|
89
|
+
primaryActive: '--color-primary-active',
|
|
90
|
+
success: '--color-success',
|
|
91
|
+
successHover: '--color-success-hover',
|
|
92
|
+
warning: '--color-warning',
|
|
93
|
+
warningHover: '--color-warning-hover',
|
|
94
|
+
danger: '--color-danger',
|
|
95
|
+
dangerHover: '--color-danger-hover',
|
|
96
|
+
dangerActive: '--color-danger-active',
|
|
97
|
+
info: '--color-info',
|
|
98
|
+
infoHover: '--color-info-hover'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hexLighten(hex: string, percent: number): string {
|
|
102
|
+
const n = Math.round((percent / 100) * 255)
|
|
103
|
+
const r = Math.min(255, parseInt(hex.slice(1, 3), 16) + n)
|
|
104
|
+
const g = Math.min(255, parseInt(hex.slice(3, 5), 16) + n)
|
|
105
|
+
const b = Math.min(255, parseInt(hex.slice(5, 7), 16) + n)
|
|
106
|
+
return '#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hexDarken(hex: string, percent: number): string {
|
|
110
|
+
const n = Math.round((percent / 100) * 255)
|
|
111
|
+
const r = Math.max(0, parseInt(hex.slice(1, 3), 16) - n)
|
|
112
|
+
const g = Math.max(0, parseInt(hex.slice(3, 5), 16) - n)
|
|
113
|
+
const b = Math.max(0, parseInt(hex.slice(5, 7), 16) - n)
|
|
114
|
+
return '#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const DEFAULT_PRIMARY = '#409eff'
|
|
118
|
+
const DEFAULT_HOVER_LIGHTEN = 15
|
|
119
|
+
const DEFAULT_ACTIVE_DARKEN = 10
|
|
120
|
+
const DEFAULT_BORDER_RADIUS = 4
|
|
121
|
+
const DEFAULT_CONTROL_HEIGHT = 32
|
|
122
|
+
const DEFAULT_FONT_SIZE = 14
|
|
123
|
+
const DEFAULT_PADDING_MD = 16
|
|
124
|
+
const DEFAULT_PADDING_SM = 12
|
|
125
|
+
const DEFAULT_PADDING_XS = 8
|
|
126
|
+
const DEFAULT_PADDING_LG = 24
|
|
127
|
+
const DEFAULT_MARGIN_MD = 16
|
|
128
|
+
const DEFAULT_MARGIN_SM = 12
|
|
129
|
+
const DEFAULT_MARGIN_XS = 8
|
|
130
|
+
const DEFAULT_MARGIN_LG = 24
|
|
131
|
+
|
|
132
|
+
function readCssVar(name: string): string {
|
|
133
|
+
if (typeof document === 'undefined') return ''
|
|
134
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 解析 "14px" / "14" → 整数 px,供 Ant token 使用 */
|
|
138
|
+
function parsePxInt(value: string, fallback: number): number {
|
|
139
|
+
const v = value.trim()
|
|
140
|
+
if (!v) return fallback
|
|
141
|
+
const n = parseFloat(v)
|
|
142
|
+
return Number.isFinite(n) ? Math.round(n) : fallback
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 供 Ant Design Vue ConfigProvider 使用的主题配置(响应式),绑定 :theme="antThemeConfigRef" 即可 */
|
|
146
|
+
export const antThemeConfigRef = ref<{ token: MtnAntThemeToken }>({
|
|
147
|
+
token: {
|
|
148
|
+
colorPrimary: DEFAULT_PRIMARY,
|
|
149
|
+
colorPrimaryHover: hexLighten(DEFAULT_PRIMARY, DEFAULT_HOVER_LIGHTEN),
|
|
150
|
+
colorPrimaryActive: hexDarken(DEFAULT_PRIMARY, DEFAULT_ACTIVE_DARKEN),
|
|
151
|
+
borderRadius: DEFAULT_BORDER_RADIUS,
|
|
152
|
+
controlHeight: DEFAULT_CONTROL_HEIGHT,
|
|
153
|
+
fontSize: DEFAULT_FONT_SIZE,
|
|
154
|
+
padding: DEFAULT_PADDING_MD,
|
|
155
|
+
paddingXS: DEFAULT_PADDING_XS,
|
|
156
|
+
paddingSM: DEFAULT_PADDING_SM,
|
|
157
|
+
paddingLG: DEFAULT_PADDING_LG,
|
|
158
|
+
margin: DEFAULT_MARGIN_MD,
|
|
159
|
+
marginXS: DEFAULT_MARGIN_XS,
|
|
160
|
+
marginSM: DEFAULT_MARGIN_SM,
|
|
161
|
+
marginLG: DEFAULT_MARGIN_LG
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 从当前 document 计算样式读取 CSS 变量,写入 antThemeConfigRef(亮/暗、密度、预设、setThemePrimary 后由内部 nextTick 调用)
|
|
167
|
+
*/
|
|
168
|
+
export function syncAntTokensFromCssVars(): void {
|
|
169
|
+
if (typeof document === 'undefined') return
|
|
170
|
+
const g = readCssVar
|
|
171
|
+
const rb = g('--radius-base')
|
|
172
|
+
const borderRadius = Math.round(parseFloat(rb)) || DEFAULT_BORDER_RADIUS
|
|
173
|
+
antThemeConfigRef.value = {
|
|
174
|
+
token: {
|
|
175
|
+
...antThemeConfigRef.value.token,
|
|
176
|
+
colorPrimary: g('--color-primary') || antThemeConfigRef.value.token.colorPrimary,
|
|
177
|
+
colorPrimaryHover: g('--color-primary-hover') || antThemeConfigRef.value.token.colorPrimaryHover,
|
|
178
|
+
colorPrimaryActive: g('--color-primary-active') || antThemeConfigRef.value.token.colorPrimaryActive,
|
|
179
|
+
colorLink: g('--color-primary') || antThemeConfigRef.value.token.colorLink,
|
|
180
|
+
colorLinkHover: g('--color-primary-hover') || antThemeConfigRef.value.token.colorLinkHover,
|
|
181
|
+
colorLinkActive: g('--color-primary-active') || antThemeConfigRef.value.token.colorLinkActive,
|
|
182
|
+
borderRadius,
|
|
183
|
+
colorBgBase: g('--color-bg-base'),
|
|
184
|
+
colorText: g('--color-text-primary'),
|
|
185
|
+
colorTextSecondary: g('--color-text-secondary'),
|
|
186
|
+
colorBorder: g('--color-border-base'),
|
|
187
|
+
colorSuccess: g('--color-success'),
|
|
188
|
+
colorWarning: g('--color-warning'),
|
|
189
|
+
colorError: g('--color-danger'),
|
|
190
|
+
colorInfo: g('--color-info'),
|
|
191
|
+
controlHeight: parsePxInt(g('--mtn-control-height'), DEFAULT_CONTROL_HEIGHT),
|
|
192
|
+
fontSize: parsePxInt(g('--mtn-font-size-base'), DEFAULT_FONT_SIZE),
|
|
193
|
+
padding: parsePxInt(g('--mtn-padding-md'), DEFAULT_PADDING_MD),
|
|
194
|
+
paddingXS: parsePxInt(g('--mtn-padding-xs'), DEFAULT_PADDING_XS),
|
|
195
|
+
paddingSM: parsePxInt(g('--mtn-padding-sm'), DEFAULT_PADDING_SM),
|
|
196
|
+
paddingLG: parsePxInt(g('--mtn-padding-lg'), DEFAULT_PADDING_LG),
|
|
197
|
+
margin: parsePxInt(g('--mtn-margin-md'), DEFAULT_MARGIN_MD),
|
|
198
|
+
marginXS: parsePxInt(g('--mtn-margin-xs'), DEFAULT_MARGIN_XS),
|
|
199
|
+
marginSM: parsePxInt(g('--mtn-margin-sm'), DEFAULT_MARGIN_SM),
|
|
200
|
+
marginLG: parsePxInt(g('--mtn-margin-lg'), DEFAULT_MARGIN_LG)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface ApplyMtnThemeOptions {
|
|
206
|
+
mode?: ThemeMode
|
|
207
|
+
/** 舒适 default / 紧凑 compact */
|
|
208
|
+
density?: DensityMode
|
|
209
|
+
preset?: MtnThemePreset | string | null
|
|
210
|
+
colors?: ThemeColorOverrides
|
|
211
|
+
/** 合并到 token,在 sync 之后应用,可覆盖从 CSS 读到的值 */
|
|
212
|
+
antToken?: Partial<MtnAntThemeToken>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 一键应用:模式 / 密度 / 预设 / 颜色 / 可选 Ant token,并在下一 tick 与 CSS 变量对齐 */
|
|
216
|
+
export function applyMtnTheme(opts: ApplyMtnThemeOptions): void {
|
|
217
|
+
if (opts.mode != null) setThemeMode(opts.mode)
|
|
218
|
+
if (opts.density != null) setDensityMode(opts.density)
|
|
219
|
+
if (opts.preset !== undefined) setThemePreset(opts.preset)
|
|
220
|
+
if (opts.colors != null && Object.keys(opts.colors).length > 0) {
|
|
221
|
+
setThemeColors(opts.colors)
|
|
222
|
+
}
|
|
223
|
+
nextTick(() => {
|
|
224
|
+
syncAntTokensFromCssVars()
|
|
225
|
+
if (opts.antToken != null && Object.keys(opts.antToken).length > 0) {
|
|
226
|
+
antThemeConfigRef.value = {
|
|
227
|
+
token: { ...antThemeConfigRef.value.token, ...opts.antToken }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 设置主题色(写 CSS 变量 + 同步 Ant ConfigProvider token)
|
|
235
|
+
* 会写入 document.documentElement.style,并更新 antThemeConfigRef
|
|
236
|
+
*
|
|
237
|
+
* @param colors 要覆盖的颜色,键为 camelCase(如 primary、primaryHover)
|
|
238
|
+
*/
|
|
239
|
+
export function setThemeColors(colors: ThemeColorOverrides): void {
|
|
240
|
+
if (colors.primary != null) {
|
|
241
|
+
const hover =
|
|
242
|
+
colors.primaryHover ?? hexLighten(colors.primary, DEFAULT_HOVER_LIGHTEN)
|
|
243
|
+
const active =
|
|
244
|
+
colors.primaryActive ?? hexDarken(colors.primary, DEFAULT_ACTIVE_DARKEN)
|
|
245
|
+
|
|
246
|
+
if (typeof document !== 'undefined') {
|
|
247
|
+
const root = document.documentElement
|
|
248
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
249
|
+
if (value == null) continue
|
|
250
|
+
const cssVar = THEME_VAR_MAP[key as keyof ThemeColorOverrides]
|
|
251
|
+
if (cssVar) root.style.setProperty(cssVar, value)
|
|
252
|
+
}
|
|
253
|
+
root.style.setProperty('--ant-primary-color', colors.primary)
|
|
254
|
+
root.style.setProperty('--ant-primary-color-hover', hover)
|
|
255
|
+
root.style.setProperty('--ant-primary-color-active', active)
|
|
256
|
+
root.style.setProperty('--ant-color-link', colors.primary)
|
|
257
|
+
root.style.setProperty('--ant-color-link-hover', hover)
|
|
258
|
+
root.style.setProperty('--ant-color-link-active', active)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
antThemeConfigRef.value = {
|
|
262
|
+
token: {
|
|
263
|
+
...antThemeConfigRef.value.token,
|
|
264
|
+
colorPrimary: colors.primary,
|
|
265
|
+
colorPrimaryHover: hover,
|
|
266
|
+
colorPrimaryActive: active,
|
|
267
|
+
colorLink: colors.primary,
|
|
268
|
+
colorLinkHover: hover,
|
|
269
|
+
colorLinkActive: active
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
273
|
+
} else if (typeof document !== 'undefined') {
|
|
274
|
+
const root = document.documentElement
|
|
275
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
276
|
+
if (value == null) continue
|
|
277
|
+
const cssVar = THEME_VAR_MAP[key as keyof ThemeColorOverrides]
|
|
278
|
+
if (cssVar) root.style.setProperty(cssVar, value)
|
|
279
|
+
}
|
|
280
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 仅设置主色,并自动生成 hover/active(简单变亮/变深)
|
|
286
|
+
* 会更新 antThemeConfigRef,需在根节点用 <a-config-provider :theme="antThemeConfigRef"> 包裹才能生效
|
|
287
|
+
*
|
|
288
|
+
* @param primary 主色,如 '#1890ff'
|
|
289
|
+
* @param hoverLighten hover 时亮度增加比例,默认 15
|
|
290
|
+
* @param activeDarken active 时亮度降低比例,默认 10
|
|
291
|
+
*/
|
|
292
|
+
export function setThemePrimary(
|
|
293
|
+
primary: string,
|
|
294
|
+
hoverLighten = DEFAULT_HOVER_LIGHTEN,
|
|
295
|
+
activeDarken = DEFAULT_ACTIVE_DARKEN
|
|
296
|
+
): void {
|
|
297
|
+
const isHex = /^#[0-9A-Fa-f]{6}$/.test(primary)
|
|
298
|
+
if (!isHex) {
|
|
299
|
+
if (typeof process !== 'undefined' ? process.env.NODE_ENV !== 'production' : true) {
|
|
300
|
+
console.warn('[mtn-ui] setThemePrimary 建议使用 6 位十六进制色,如 #1890ff')
|
|
301
|
+
}
|
|
302
|
+
setThemeColors({ primary })
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
setThemeColors({
|
|
306
|
+
primary,
|
|
307
|
+
primaryHover: hexLighten(primary, hoverLighten),
|
|
308
|
+
primaryActive: hexDarken(primary, activeDarken)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 切换亮色/暗色模式
|
|
314
|
+
* - 更新 themeModeRef,便于与 Ant Design Vue 的 theme.defaultAlgorithm / theme.darkAlgorithm 组合
|
|
315
|
+
* - 暗色:`data-theme="dark"`;亮色:移除 `data-theme`(与 SCSS theme-mixins 中 :root 亮主题一致)
|
|
316
|
+
* - Tailwind `dark:`:请配置 `darkMode: ['selector', '[data-theme="dark"]']` 或使用 class 策略
|
|
317
|
+
*/
|
|
318
|
+
export function setThemeMode(
|
|
319
|
+
mode: ThemeMode,
|
|
320
|
+
options?: { persist?: boolean; storageKey?: string }
|
|
321
|
+
): void {
|
|
322
|
+
if (typeof document === 'undefined') return
|
|
323
|
+
themeModeRef.value = mode
|
|
324
|
+
const root = document.documentElement
|
|
325
|
+
if (mode === 'dark') {
|
|
326
|
+
root.setAttribute('data-theme', 'dark')
|
|
327
|
+
} else {
|
|
328
|
+
root.removeAttribute('data-theme')
|
|
329
|
+
}
|
|
330
|
+
if (options?.persist) {
|
|
331
|
+
persistThemeMode(mode, { storageKey: options.storageKey })
|
|
332
|
+
}
|
|
333
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 在亮色与暗色之间切换,并返回切换后的模式
|
|
338
|
+
*/
|
|
339
|
+
export function toggleThemeMode(options?: { persist?: boolean; storageKey?: string }): ThemeMode {
|
|
340
|
+
const next: ThemeMode = themeModeRef.value === 'dark' ? 'light' : 'dark'
|
|
341
|
+
setThemeMode(next, options)
|
|
342
|
+
return next
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 当前亮/暗模式 */
|
|
346
|
+
export function getThemeMode(): ThemeMode {
|
|
347
|
+
return themeModeRef.value
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const THEME_MODE_STORAGE_KEY = 'mtn-ui-theme-mode'
|
|
351
|
+
const DENSITY_STORAGE_KEY = 'mtn-ui-density-mode'
|
|
352
|
+
|
|
353
|
+
const MTN_DENSITY_ATTR = 'data-mtn-density'
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 设置全局密度(紧凑 / 舒适),并可选持久化
|
|
357
|
+
*/
|
|
358
|
+
export function setDensityMode(
|
|
359
|
+
mode: DensityMode,
|
|
360
|
+
options?: { persist?: boolean; storageKey?: string }
|
|
361
|
+
): void {
|
|
362
|
+
if (typeof document === 'undefined') return
|
|
363
|
+
densityModeRef.value = mode
|
|
364
|
+
const root = document.documentElement
|
|
365
|
+
if (mode === 'compact') {
|
|
366
|
+
root.setAttribute(MTN_DENSITY_ATTR, 'compact')
|
|
367
|
+
} else {
|
|
368
|
+
root.removeAttribute(MTN_DENSITY_ATTR)
|
|
369
|
+
}
|
|
370
|
+
if (options?.persist) {
|
|
371
|
+
persistDensityMode(mode, { storageKey: options?.storageKey })
|
|
372
|
+
}
|
|
373
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function getDensityMode(): DensityMode {
|
|
377
|
+
return densityModeRef.value
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 在紧凑与舒适之间切换
|
|
382
|
+
*/
|
|
383
|
+
export function toggleDensityMode(options?: { persist?: boolean; storageKey?: string }): DensityMode {
|
|
384
|
+
const next: DensityMode = densityModeRef.value === 'compact' ? 'default' : 'compact'
|
|
385
|
+
setDensityMode(next, options)
|
|
386
|
+
return next
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 从 localStorage 恢复密度(与 initTheme 搭配,尽早调用)
|
|
391
|
+
*/
|
|
392
|
+
export function initDensityFromStorage(options?: { storageKey?: string }): DensityMode {
|
|
393
|
+
if (typeof window === 'undefined') return densityModeRef.value
|
|
394
|
+
const key = options?.storageKey ?? DENSITY_STORAGE_KEY
|
|
395
|
+
try {
|
|
396
|
+
const raw = window.localStorage.getItem(key)
|
|
397
|
+
if (raw === 'compact') {
|
|
398
|
+
setDensityMode('compact')
|
|
399
|
+
return 'compact'
|
|
400
|
+
}
|
|
401
|
+
if (raw === 'default') {
|
|
402
|
+
setDensityMode('default')
|
|
403
|
+
return 'default'
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
/* ignore */
|
|
407
|
+
}
|
|
408
|
+
return densityModeRef.value
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function persistDensityMode(mode: DensityMode, options?: { storageKey?: string }): void {
|
|
412
|
+
if (typeof window === 'undefined') return
|
|
413
|
+
const key = options?.storageKey ?? DENSITY_STORAGE_KEY
|
|
414
|
+
try {
|
|
415
|
+
window.localStorage.setItem(key, mode)
|
|
416
|
+
} catch {
|
|
417
|
+
/* ignore */
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 从 localStorage 恢复亮/暗(应用入口尽早调用以减少闪烁)
|
|
423
|
+
*/
|
|
424
|
+
export function initThemeFromStorage(options?: { storageKey?: string }): ThemeMode {
|
|
425
|
+
if (typeof window === 'undefined') return themeModeRef.value
|
|
426
|
+
const key = options?.storageKey ?? THEME_MODE_STORAGE_KEY
|
|
427
|
+
try {
|
|
428
|
+
const raw = window.localStorage.getItem(key)
|
|
429
|
+
if (raw === 'dark' || raw === 'light') {
|
|
430
|
+
setThemeMode(raw)
|
|
431
|
+
return raw
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
434
|
+
/* 私有模式等 */
|
|
435
|
+
}
|
|
436
|
+
return themeModeRef.value
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function persistThemeMode(mode: ThemeMode, options?: { storageKey?: string }): void {
|
|
440
|
+
if (typeof window === 'undefined') return
|
|
441
|
+
const key = options?.storageKey ?? THEME_MODE_STORAGE_KEY
|
|
442
|
+
try {
|
|
443
|
+
window.localStorage.setItem(key, mode)
|
|
444
|
+
} catch {
|
|
445
|
+
/* ignore */
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 初始化:从 localStorage 恢复亮/暗与密度;无记录时可选跟随 prefers-color-scheme;最后同步 Ant token
|
|
451
|
+
*/
|
|
452
|
+
export function initTheme(options?: {
|
|
453
|
+
storageKey?: string
|
|
454
|
+
densityStorageKey?: string
|
|
455
|
+
preferColorScheme?: boolean
|
|
456
|
+
}): ThemeMode {
|
|
457
|
+
if (typeof window === 'undefined') return themeModeRef.value
|
|
458
|
+
const key = options?.storageKey ?? THEME_MODE_STORAGE_KEY
|
|
459
|
+
const densityKey = options?.densityStorageKey ?? DENSITY_STORAGE_KEY
|
|
460
|
+
const modeAfterStorage = initThemeFromStorage({ storageKey: key })
|
|
461
|
+
initDensityFromStorage({ storageKey: densityKey })
|
|
462
|
+
let hadStored = false
|
|
463
|
+
try {
|
|
464
|
+
const raw = window.localStorage.getItem(key)
|
|
465
|
+
hadStored = raw === 'dark' || raw === 'light'
|
|
466
|
+
} catch {
|
|
467
|
+
/* ignore */
|
|
468
|
+
}
|
|
469
|
+
if (hadStored) {
|
|
470
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
471
|
+
return modeAfterStorage
|
|
472
|
+
}
|
|
473
|
+
if (options?.preferColorScheme && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
474
|
+
setThemeMode('dark')
|
|
475
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
476
|
+
return 'dark'
|
|
477
|
+
}
|
|
478
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
479
|
+
return themeModeRef.value
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 切换色板预设(仅设置 document 上 data-mtn-preset;样式需业务或未来 @mtn-ui-z/theme 提供)
|
|
484
|
+
* - 与亮/暗正交:可同时 data-theme="dark" + data-mtn-preset="ocean"
|
|
485
|
+
* - `default` / `null` / `''` 会移除 data-mtn-preset
|
|
486
|
+
*/
|
|
487
|
+
export function setThemePreset(preset: MtnThemePreset | string | null | undefined): void {
|
|
488
|
+
if (typeof document === 'undefined') return
|
|
489
|
+
const root = document.documentElement
|
|
490
|
+
if (preset == null || preset === '' || preset === 'default') {
|
|
491
|
+
root.removeAttribute(MTN_PRESET_ATTR)
|
|
492
|
+
} else {
|
|
493
|
+
root.setAttribute(MTN_PRESET_ATTR, preset)
|
|
494
|
+
}
|
|
495
|
+
nextTick(() => syncAntTokensFromCssVars())
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** 当前 data-mtn-preset,未设置时为 null */
|
|
499
|
+
export function getThemePreset(): string | null {
|
|
500
|
+
if (typeof document === 'undefined') return null
|
|
501
|
+
return document.documentElement.getAttribute(MTN_PRESET_ATTR)
|
|
502
|
+
}
|
package/src/types.d.ts
ADDED
package/src/types.ts
ADDED
package/src/useModal.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal 命令式调用 hook
|
|
3
|
+
* 用法:
|
|
4
|
+
* const modal = useModal(MyModalComponent)
|
|
5
|
+
* modal.show({ title: '标题', row: { id: 1 } })
|
|
6
|
+
*/
|
|
7
|
+
import { ref, type Component } from 'vue'
|
|
8
|
+
import { ModalRegistry } from './modalRegistry'
|
|
9
|
+
|
|
10
|
+
export interface UseModalOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Modal 唯一 ID(不传则自动生成)
|
|
13
|
+
*/
|
|
14
|
+
id?: string
|
|
15
|
+
/**
|
|
16
|
+
* Modal 关闭后是否销毁实例
|
|
17
|
+
*/
|
|
18
|
+
destroyOnClose?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseModalShowOptions {
|
|
22
|
+
/**
|
|
23
|
+
* 业务数据
|
|
24
|
+
*/
|
|
25
|
+
row?: unknown
|
|
26
|
+
/**
|
|
27
|
+
* 其他 a-modal props
|
|
28
|
+
*/
|
|
29
|
+
[key: string]: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UseModalReturn {
|
|
33
|
+
show: (options?: UseModalShowOptions) => void
|
|
34
|
+
hide: () => void
|
|
35
|
+
destroy: () => void
|
|
36
|
+
visible: ReturnType<typeof ref<boolean>>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getModalProvider() {
|
|
40
|
+
return (globalThis as any).__mtnModalProvider__ ?? null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function genId(prefix: string) {
|
|
44
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 命令式 Modal Hook
|
|
49
|
+
*/
|
|
50
|
+
export function useModal<T extends Component>(
|
|
51
|
+
modalComponent: T,
|
|
52
|
+
options: UseModalOptions = {}
|
|
53
|
+
): UseModalReturn {
|
|
54
|
+
// 用组件对象本身作为 id 前缀,保证唯一性
|
|
55
|
+
const id = options.id ?? genId((modalComponent as any).name ?? 'modal')
|
|
56
|
+
|
|
57
|
+
// 先注册组件(provider 会在渲染时从 registry 取)
|
|
58
|
+
ModalRegistry.register(id, modalComponent)
|
|
59
|
+
|
|
60
|
+
const visible = ref(false)
|
|
61
|
+
|
|
62
|
+
function show(opts: UseModalShowOptions = {}) {
|
|
63
|
+
const provider = getModalProvider()
|
|
64
|
+
if (!provider) {
|
|
65
|
+
console.warn('[useModal] ModalProvider 未挂载,请确保 App.vue 中使用了 <MtnModalProvider>')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 合并 props:open + 用户传入的 options + 关闭回调
|
|
70
|
+
provider.register(id, modalComponent, {
|
|
71
|
+
...opts,
|
|
72
|
+
open: true,
|
|
73
|
+
onCancel: (...args: unknown[]) => {
|
|
74
|
+
;(opts.onCancel as Function)?.(...args)
|
|
75
|
+
hide()
|
|
76
|
+
},
|
|
77
|
+
onOk: (...args: unknown[]) => {
|
|
78
|
+
;(opts.onOk as Function)?.(...args)
|
|
79
|
+
if (options.destroyOnClose) {
|
|
80
|
+
destroy()
|
|
81
|
+
} else {
|
|
82
|
+
hide()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
visible.value = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hide() {
|
|
91
|
+
const provider = getModalProvider()
|
|
92
|
+
if (provider) {
|
|
93
|
+
// 通知 antd modal 开始关闭动画(open: false 触发 @update:open 事件)
|
|
94
|
+
provider.update(id, { open: false })
|
|
95
|
+
}
|
|
96
|
+
visible.value = false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function destroy() {
|
|
100
|
+
const provider = getModalProvider()
|
|
101
|
+
if (provider) {
|
|
102
|
+
// 触发关闭动画,等 @update:open(false) + 300ms 动画结束后 confirmClose 会自动删
|
|
103
|
+
provider.update(id, { open: false })
|
|
104
|
+
}
|
|
105
|
+
visible.value = false
|
|
106
|
+
ModalRegistry.unregister(id)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { show, hide, destroy, visible }
|
|
110
|
+
}
|