@zerodep-dev/vue-tsx-pro-ui 0.4.0 → 0.4.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/dist/config/createUi.d.ts +21 -2
- package/dist/config/createUi.js +18 -1
- package/dist/config/createUi.js.map +1 -1
- package/dist/config/theme/preference.js +2 -1
- package/dist/config/theme/preference.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/utils/request/use-request.d.ts +25 -1
- package/dist/utils/request/use-request.js +35 -3
- package/dist/utils/request/use-request.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,7 +10,7 @@ import { type ComponentStyles } from './styles';
|
|
|
10
10
|
import { type Lang } from './lang/lang';
|
|
11
11
|
import type { WorkCalendar } from '../utils/data/workdays';
|
|
12
12
|
import type { HttpClient } from '../utils/request/http';
|
|
13
|
-
import { type ThemeInput } from './theme/theme';
|
|
13
|
+
import { type Theme, type ThemeInput } from './theme/theme';
|
|
14
14
|
/** 应用级配置;theme 与 lang 可以传 ref 或 getter,运行时切换,其余在安装时确定 */
|
|
15
15
|
export interface UiOptions {
|
|
16
16
|
/** 主题;不写时用亮色主题,也不改动页面根元素 */
|
|
@@ -49,11 +49,21 @@ export declare const documentStyles: Readonly<{
|
|
|
49
49
|
}>;
|
|
50
50
|
/** 服务端渲染时 `<html>` 与 `<body>` 要带的属性(createUi 在浏览器里写到页面根元素上的那些) */
|
|
51
51
|
export interface DocumentAttrs {
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* `<html>` 的属性:主题的类、变量(style)、data-theme,传了 lang 时还有 lang、dir。
|
|
54
|
+
* data-theme-classes 记下主题的类,防闪烁脚本和浏览器接管时据此换掉,不留下旧主题的类
|
|
55
|
+
*/
|
|
53
56
|
html: Record<string, string>;
|
|
54
57
|
/** `<body>` 的属性:paintDocument 时去掉外边距的类 */
|
|
55
58
|
body: Record<string, string>;
|
|
56
59
|
}
|
|
60
|
+
/** themeClassBoot 的结果 */
|
|
61
|
+
export interface ThemeClassBoot {
|
|
62
|
+
/** 内联到 <head> 的脚本,放在 themeBootScript 之后 */
|
|
63
|
+
script: string;
|
|
64
|
+
/** 候选主题的类:交给 renderStyleTag 的 include,它们的规则才在首屏样式里 */
|
|
65
|
+
classes: readonly string[];
|
|
66
|
+
}
|
|
57
67
|
/** createUi 的结果:Vue 插件,另外能给出服务端渲染时页面根元素的属性 */
|
|
58
68
|
export interface UiPlugin extends ObjectPlugin {
|
|
59
69
|
/**
|
|
@@ -61,6 +71,15 @@ export interface UiPlugin extends ObjectPlugin {
|
|
|
61
71
|
* 服务端不知道系统的明暗,跟随系统的主题按亮色给;`themeBootScript` 在页面加载前按用户的选择改过来
|
|
62
72
|
*/
|
|
63
73
|
documentAttrs(): DocumentAttrs;
|
|
74
|
+
/**
|
|
75
|
+
* 服务端渲染的防闪烁:`themeBootScript` 只设底色和文字色,正文里的组件读的是主题变量,要把 <html> 上的主题类也换掉,
|
|
76
|
+
* 否则暗色系统上会先看到暗底配亮色主题的深色字,要等水合才对。主题按这里给的候选表选,规则随首屏样式一起输出:
|
|
77
|
+
*
|
|
78
|
+
* const boot = ui.themeClassBoot(builtinThemes);
|
|
79
|
+
* const styles = renderStyleTag(page, { include: boot.classes });
|
|
80
|
+
* // <head>:<script>themeBootScript(…)</script> <script>boot.script</script> styles
|
|
81
|
+
*/
|
|
82
|
+
themeClassBoot(themes: Readonly<Record<string, Theme>>): ThemeClassBoot;
|
|
64
83
|
}
|
|
65
84
|
/**
|
|
66
85
|
* 应用级配置的 Vue 插件:`app.use(createUi({ theme: { light: lightTheme, dark: darkTheme } }))`。
|
package/dist/config/createUi.js
CHANGED
|
@@ -39,7 +39,7 @@ function createUi(options = {}) {
|
|
|
39
39
|
if (options.theme || options.paintDocument) {
|
|
40
40
|
const theme = resolveTheme(toValue(options.theme) ?? defaultConfig.theme, false);
|
|
41
41
|
const { classes, variables } = rootThemeAttrs(theme, options.paintDocument ?? false);
|
|
42
|
-
if (classes.length) html.class = classes.join(" ");
|
|
42
|
+
if (classes.length) html.class = html["data-theme-classes"] = classes.join(" ");
|
|
43
43
|
const style = Object.entries(variables).map(([name, value]) => `${name}:${value}`).join(";");
|
|
44
44
|
if (style) html.style = style;
|
|
45
45
|
html["data-theme"] = theme.name;
|
|
@@ -50,6 +50,13 @@ function createUi(options = {}) {
|
|
|
50
50
|
body
|
|
51
51
|
};
|
|
52
52
|
},
|
|
53
|
+
themeClassBoot(themes) {
|
|
54
|
+
const table = Object.fromEntries(Object.entries(themes).map(([name, theme]) => [name, rootThemeAttrs(theme, options.paintDocument ?? false)]));
|
|
55
|
+
return {
|
|
56
|
+
script: themeClassBootScript(table),
|
|
57
|
+
classes: [...new Set(Object.values(table).flatMap(({ classes }) => classes))]
|
|
58
|
+
};
|
|
59
|
+
},
|
|
53
60
|
install(app) {
|
|
54
61
|
const scope = effectScope();
|
|
55
62
|
app.onUnmount(() => scope.stop());
|
|
@@ -90,6 +97,9 @@ function createUi(options = {}) {
|
|
|
90
97
|
const root = document.documentElement;
|
|
91
98
|
if (options.paintDocument) root.style.removeProperty("background-color");
|
|
92
99
|
if (options.paintDocument) root.style.removeProperty("color");
|
|
100
|
+
const rendered = root.dataset.themeClasses;
|
|
101
|
+
if (rendered) root.classList.remove(...rendered.split(" "));
|
|
102
|
+
delete root.dataset.themeClasses;
|
|
93
103
|
watchEffect((onCleanup) => onCleanup(applyTheme(root, theme.value, options.paintDocument ?? false)));
|
|
94
104
|
if (options.paintDocument && document.body) {
|
|
95
105
|
const body = document.body;
|
|
@@ -109,6 +119,13 @@ function rootThemeAttrs(theme, paint) {
|
|
|
109
119
|
variables: themeVariables(theme)
|
|
110
120
|
};
|
|
111
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* 防闪烁脚本的第二段:themeBootScript 已按用户的偏好写好 data-theme,这里把服务端写的主题类换成那套主题的。
|
|
124
|
+
* data-theme 不在候选表里时不动(用户自己定制的主题,水合后才有)
|
|
125
|
+
*/
|
|
126
|
+
function themeClassBootScript(themes) {
|
|
127
|
+
return `(()=>{const c=${JSON.stringify(themes).replaceAll("<", "\\u003c")};const r=document.documentElement;const t=c[r.dataset.theme];if(!t)return;const o=r.dataset.themeClasses;if(o)r.classList.remove(...o.split(' '));r.classList.add(...t.classes);r.dataset.themeClasses=t.classes.join(' ');for(const n in t.variables)r.style.setProperty(n,t.variables[n])})()`;
|
|
128
|
+
}
|
|
112
129
|
/** 把主题的 class、CSS 变量与 data-theme 写到元素上,返回撤销函数 */
|
|
113
130
|
function applyTheme(element, theme, paint) {
|
|
114
131
|
const { classes, variables } = rootThemeAttrs(theme, paint);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createUi.js","names":[],"sources":["../../src/config/createUi.ts"],"sourcesContent":["/**\n * 应用级配置:在宿主入口里 `app.use(createUi({ theme, lang }))`,所有 TSX 页面共享,不必逐页包裹 Provider。\n * 传入 ref 或 getter 即可在运行时切换主题与语言。\n */\nimport {\n computed,\n effectScope,\n toValue,\n watchEffect,\n type MaybeRefOrGetter,\n type ObjectPlugin,\n} from 'vue';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport {\n ConfigContext,\n defaultConfig,\n type Highlighter,\n type TextMatcher,\n type UiPermissions,\n type UiRouter,\n} from './context';\nimport type { ComponentDefaults } from './defaults';\nimport { mergeStyles, type ComponentStyles } from './styles';\nimport { defineLang, type Lang } from './lang/lang';\nimport { usePrefersDark } from '../utils/dom/media-query';\nimport type { WorkCalendar } from '../utils/data/workdays';\nimport type { HttpClient } from '../utils/request/http';\nimport { resolveTheme, themeVariables, type Theme, type ThemeInput } from './theme/theme';\n\n/** 应用级配置;theme 与 lang 可以传 ref 或 getter,运行时切换,其余在安装时确定 */\nexport interface UiOptions {\n /** 主题;不写时用亮色主题,也不改动页面根元素 */\n theme?: MaybeRefOrGetter<ThemeInput>;\n /** 只写要改的字段,其余用简体中文的默认值 */\n lang?: MaybeRefOrGetter<Partial<Lang>>;\n /** 代码高亮器(CodeBlock 用),如 `@zerodep-dev/vue-tsx-pro-ui/shiki` 的 createShikiHighlighter();不写时显示纯文本 */\n highlighter?: Highlighter;\n /** 额外的文字匹配,如 `@zerodep-dev/vue-tsx-pro-ui/pinyin` 的 createPinyinMatcher():选项过滤、命令面板认拼音;不写时只按字面匹配 */\n textMatcher?: TextMatcher;\n /** 组件默认值,如 `{ Button: { variant: 'outline' } }`(见 defaults.ts) */\n defaults?: ComponentDefaults;\n /** 组件的主题定制:追加样式、新增档位,如 `{ Button: { base: { root: { letterSpacing: '0.02em' } } } }`(见 styles.ts) */\n styles?: ComponentStyles;\n /** 路由适配:站内链接不刷新页面地跳转、导航组件判断当前页(见 UiRouter) */\n router?: UiRouter;\n /** 权限判断:`(权限点) => 用户有没有`,Can、useCan 据此控制按钮等界面;不写时一律放行(见 UiPermissions) */\n permissions?: UiPermissions;\n /** 请求客户端:createHttpClient({ baseURL, onRequest, onResponse }),或包一层 axios、ky(见 HttpClient) */\n http?: HttpClient;\n /** 工作日历:createWorkCalendar({ holidays, workdays }) 带上法定节假日与调休;不写时只按周末算 */\n workCalendar?: WorkCalendar;\n /** 按下时的波纹效果,默认 true;关掉后组件的 ripple prop 仍可单独打开 */\n ripple?: boolean;\n /**\n * 页面根元素也用主题的背景色、文字色与字体,并去掉 body 的默认外边距,默认 false。\n * 整页都由组件库主题管理的站点(如文档站)打开:页面边缘、回弹滚动露出的地方不再是浏览器的默认色,\n * AppShell 贴满窗口、四周不留 8px 的白边;配合 themeBootScript 时接手它写的行内颜色\n */\n paintDocument?: boolean;\n}\n\n/** paintDocument 时页面根元素的样式 */\nexport const documentStyles = css.create({\n // 字号不设:根元素的字号是 rem 的基准,留给浏览器(用户的字号设置)\n page: {\n backgroundColor: '_background',\n color: '_text',\n fontFamily: '_sans',\n lineHeight: '_normal',\n // macOS 上用灰度抗锯齿,深色底上的浅色文字不发虚;其他系统忽略\n WebkitFontSmoothing: 'antialiased',\n MozOsxFontSmoothing: 'grayscale',\n },\n // 浏览器给 body 的 8px 外边距:整页的外壳(AppShell)要贴满窗口\n body: { margin: 0 },\n});\n\n/** 服务端渲染时 `<html>` 与 `<body>` 要带的属性(createUi 在浏览器里写到页面根元素上的那些) */\nexport interface DocumentAttrs {\n /** `<html>` 的属性:主题的类、变量(style)、data-theme,传了 lang 时还有 lang、dir */\n html: Record<string, string>;\n /** `<body>` 的属性:paintDocument 时去掉外边距的类 */\n body: Record<string, string>;\n}\n\n/** createUi 的结果:Vue 插件,另外能给出服务端渲染时页面根元素的属性 */\nexport interface UiPlugin extends ObjectPlugin {\n /**\n * 服务端渲染:`<html>`、`<body>` 要带的属性,首屏就是这套主题与语言,不等客户端接管后再变。\n * 服务端不知道系统的明暗,跟随系统的主题按亮色给;`themeBootScript` 在页面加载前按用户的选择改过来\n */\n documentAttrs(): DocumentAttrs;\n}\n\n/**\n * 应用级配置的 Vue 插件:`app.use(createUi({ theme: { light: lightTheme, dark: darkTheme } }))`。\n * 主题的 CSS 变量写在页面根元素上,所有 TSX 页面共享;应用卸载时撤销。\n */\nexport function createUi(options: UiOptions = {}): UiPlugin {\n return {\n documentAttrs() {\n const html: Record<string, string> = {};\n const body: Record<string, string> = {};\n if (options.lang) {\n const lang = defineLang(defaultConfig.lang, toValue(options.lang));\n html.lang = lang.code;\n html.dir = lang.direction;\n }\n if (options.theme || options.paintDocument) {\n const theme = resolveTheme(toValue(options.theme) ?? defaultConfig.theme, false);\n const { classes, variables } = rootThemeAttrs(theme, options.paintDocument ?? false);\n if (classes.length) html.class = classes.join(' ');\n const style = Object.entries(variables)\n .map(([name, value]) => `${name}:${value}`)\n .join(';');\n if (style) html.style = style;\n html['data-theme'] = theme.name;\n if (options.paintDocument) body.class = css.attrs(documentStyles.body).class ?? '';\n }\n return { html, body };\n },\n install(app) {\n const scope = effectScope();\n app.onUnmount(() => scope.stop());\n scope.run(() => {\n const prefersDark = usePrefersDark();\n const theme = computed(() =>\n resolveTheme(toValue(options.theme) ?? defaultConfig.theme, prefersDark.value),\n );\n const lang = computed(() => defineLang(defaultConfig.lang, toValue(options.lang) ?? {}));\n app.provide(ConfigContext.key, {\n get theme() {\n return theme.value;\n },\n get lang() {\n return lang.value;\n },\n highlighter: options.highlighter,\n textMatcher: options.textMatcher,\n defaults: options.defaults ?? {},\n styles: mergeStyles({}, options.styles),\n router: options.router,\n permissions: options.permissions,\n http: options.http,\n workCalendar: options.workCalendar,\n ripple: options.ripple ?? true,\n });\n // 应用级的语言也写在页面根元素上(Provider 写在自己的元素上):读屏按 lang 选发音,\n // 从右到左的语言靠 dir 镜像排版与滚动条;没传 lang 时不动,宿主自己管\n if (options.lang && typeof document !== 'undefined') {\n const root = document.documentElement;\n const previous = { lang: root.lang, dir: root.dir };\n watchEffect(() => {\n root.lang = lang.value.code;\n root.dir = lang.value.direction;\n });\n app.onUnmount(() => Object.assign(root, previous));\n }\n // 应用级没有容器元素,主题变量挂在页面根元素上;变量名带 vtp- 前缀,不会与宿主冲突。\n // 只开 paintDocument、不传主题时用默认主题(整页也要有底色、字体,body 去掉外边距)\n if ((options.theme || options.paintDocument) && typeof document !== 'undefined') {\n const root = document.documentElement;\n // themeBootScript 在应用加载前写的行内颜色:交给主题的 class\n if (options.paintDocument) root.style.removeProperty('background-color');\n if (options.paintDocument) root.style.removeProperty('color');\n watchEffect((onCleanup) =>\n onCleanup(applyTheme(root, theme.value, options.paintDocument ?? false)),\n );\n if (options.paintDocument && document.body) {\n const body = document.body;\n const classes = css.attrs(documentStyles.body).class?.split(' ') ?? [];\n body.classList.add(...classes);\n app.onUnmount(() => body.classList.remove(...classes));\n }\n }\n });\n },\n };\n}\n\n/** 主题挂到页面根元素上的类与 CSS 变量;paint 时加上整页的底色与字体(浏览器里写到元素上,服务端写进 HTML) */\nfunction rootThemeAttrs(theme: Theme, paint: boolean) {\n const classes =\n css\n .attrs(...theme.styles, paint && documentStyles.page)\n .class?.split(' ')\n .filter(Boolean) ?? [];\n return { classes, variables: themeVariables(theme) };\n}\n\n/** 把主题的 class、CSS 变量与 data-theme 写到元素上,返回撤销函数 */\nfunction applyTheme(element: HTMLElement, theme: Theme, paint: boolean) {\n const { classes, variables } = rootThemeAttrs(theme, paint);\n element.classList.add(...classes);\n for (const [name, value] of Object.entries(variables)) element.style.setProperty(name, value);\n element.dataset.theme = theme.name;\n return () => {\n element.classList.remove(...classes);\n for (const name of Object.keys(variables)) element.style.removeProperty(name);\n delete element.dataset.theme;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createUi.js","names":[],"sources":["../../src/config/createUi.ts"],"sourcesContent":["/**\n * 应用级配置:在宿主入口里 `app.use(createUi({ theme, lang }))`,所有 TSX 页面共享,不必逐页包裹 Provider。\n * 传入 ref 或 getter 即可在运行时切换主题与语言。\n */\nimport {\n computed,\n effectScope,\n toValue,\n watchEffect,\n type MaybeRefOrGetter,\n type ObjectPlugin,\n} from 'vue';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport {\n ConfigContext,\n defaultConfig,\n type Highlighter,\n type TextMatcher,\n type UiPermissions,\n type UiRouter,\n} from './context';\nimport type { ComponentDefaults } from './defaults';\nimport { mergeStyles, type ComponentStyles } from './styles';\nimport { defineLang, type Lang } from './lang/lang';\nimport { usePrefersDark } from '../utils/dom/media-query';\nimport type { WorkCalendar } from '../utils/data/workdays';\nimport type { HttpClient } from '../utils/request/http';\nimport { resolveTheme, themeVariables, type Theme, type ThemeInput } from './theme/theme';\n\n/** 应用级配置;theme 与 lang 可以传 ref 或 getter,运行时切换,其余在安装时确定 */\nexport interface UiOptions {\n /** 主题;不写时用亮色主题,也不改动页面根元素 */\n theme?: MaybeRefOrGetter<ThemeInput>;\n /** 只写要改的字段,其余用简体中文的默认值 */\n lang?: MaybeRefOrGetter<Partial<Lang>>;\n /** 代码高亮器(CodeBlock 用),如 `@zerodep-dev/vue-tsx-pro-ui/shiki` 的 createShikiHighlighter();不写时显示纯文本 */\n highlighter?: Highlighter;\n /** 额外的文字匹配,如 `@zerodep-dev/vue-tsx-pro-ui/pinyin` 的 createPinyinMatcher():选项过滤、命令面板认拼音;不写时只按字面匹配 */\n textMatcher?: TextMatcher;\n /** 组件默认值,如 `{ Button: { variant: 'outline' } }`(见 defaults.ts) */\n defaults?: ComponentDefaults;\n /** 组件的主题定制:追加样式、新增档位,如 `{ Button: { base: { root: { letterSpacing: '0.02em' } } } }`(见 styles.ts) */\n styles?: ComponentStyles;\n /** 路由适配:站内链接不刷新页面地跳转、导航组件判断当前页(见 UiRouter) */\n router?: UiRouter;\n /** 权限判断:`(权限点) => 用户有没有`,Can、useCan 据此控制按钮等界面;不写时一律放行(见 UiPermissions) */\n permissions?: UiPermissions;\n /** 请求客户端:createHttpClient({ baseURL, onRequest, onResponse }),或包一层 axios、ky(见 HttpClient) */\n http?: HttpClient;\n /** 工作日历:createWorkCalendar({ holidays, workdays }) 带上法定节假日与调休;不写时只按周末算 */\n workCalendar?: WorkCalendar;\n /** 按下时的波纹效果,默认 true;关掉后组件的 ripple prop 仍可单独打开 */\n ripple?: boolean;\n /**\n * 页面根元素也用主题的背景色、文字色与字体,并去掉 body 的默认外边距,默认 false。\n * 整页都由组件库主题管理的站点(如文档站)打开:页面边缘、回弹滚动露出的地方不再是浏览器的默认色,\n * AppShell 贴满窗口、四周不留 8px 的白边;配合 themeBootScript 时接手它写的行内颜色\n */\n paintDocument?: boolean;\n}\n\n/** paintDocument 时页面根元素的样式 */\nexport const documentStyles = css.create({\n // 字号不设:根元素的字号是 rem 的基准,留给浏览器(用户的字号设置)\n page: {\n backgroundColor: '_background',\n color: '_text',\n fontFamily: '_sans',\n lineHeight: '_normal',\n // macOS 上用灰度抗锯齿,深色底上的浅色文字不发虚;其他系统忽略\n WebkitFontSmoothing: 'antialiased',\n MozOsxFontSmoothing: 'grayscale',\n },\n // 浏览器给 body 的 8px 外边距:整页的外壳(AppShell)要贴满窗口\n body: { margin: 0 },\n});\n\n/** 服务端渲染时 `<html>` 与 `<body>` 要带的属性(createUi 在浏览器里写到页面根元素上的那些) */\nexport interface DocumentAttrs {\n /**\n * `<html>` 的属性:主题的类、变量(style)、data-theme,传了 lang 时还有 lang、dir。\n * data-theme-classes 记下主题的类,防闪烁脚本和浏览器接管时据此换掉,不留下旧主题的类\n */\n html: Record<string, string>;\n /** `<body>` 的属性:paintDocument 时去掉外边距的类 */\n body: Record<string, string>;\n}\n\n/** themeClassBoot 的结果 */\nexport interface ThemeClassBoot {\n /** 内联到 <head> 的脚本,放在 themeBootScript 之后 */\n script: string;\n /** 候选主题的类:交给 renderStyleTag 的 include,它们的规则才在首屏样式里 */\n classes: readonly string[];\n}\n\n/** createUi 的结果:Vue 插件,另外能给出服务端渲染时页面根元素的属性 */\nexport interface UiPlugin extends ObjectPlugin {\n /**\n * 服务端渲染:`<html>`、`<body>` 要带的属性,首屏就是这套主题与语言,不等客户端接管后再变。\n * 服务端不知道系统的明暗,跟随系统的主题按亮色给;`themeBootScript` 在页面加载前按用户的选择改过来\n */\n documentAttrs(): DocumentAttrs;\n /**\n * 服务端渲染的防闪烁:`themeBootScript` 只设底色和文字色,正文里的组件读的是主题变量,要把 <html> 上的主题类也换掉,\n * 否则暗色系统上会先看到暗底配亮色主题的深色字,要等水合才对。主题按这里给的候选表选,规则随首屏样式一起输出:\n *\n * const boot = ui.themeClassBoot(builtinThemes);\n * const styles = renderStyleTag(page, { include: boot.classes });\n * // <head>:<script>themeBootScript(…)</script> <script>boot.script</script> styles\n */\n themeClassBoot(themes: Readonly<Record<string, Theme>>): ThemeClassBoot;\n}\n\n/**\n * 应用级配置的 Vue 插件:`app.use(createUi({ theme: { light: lightTheme, dark: darkTheme } }))`。\n * 主题的 CSS 变量写在页面根元素上,所有 TSX 页面共享;应用卸载时撤销。\n */\nexport function createUi(options: UiOptions = {}): UiPlugin {\n return {\n documentAttrs() {\n const html: Record<string, string> = {};\n const body: Record<string, string> = {};\n if (options.lang) {\n const lang = defineLang(defaultConfig.lang, toValue(options.lang));\n html.lang = lang.code;\n html.dir = lang.direction;\n }\n if (options.theme || options.paintDocument) {\n const theme = resolveTheme(toValue(options.theme) ?? defaultConfig.theme, false);\n const { classes, variables } = rootThemeAttrs(theme, options.paintDocument ?? false);\n if (classes.length) html.class = html['data-theme-classes'] = classes.join(' ');\n const style = Object.entries(variables)\n .map(([name, value]) => `${name}:${value}`)\n .join(';');\n if (style) html.style = style;\n html['data-theme'] = theme.name;\n if (options.paintDocument) body.class = css.attrs(documentStyles.body).class ?? '';\n }\n return { html, body };\n },\n themeClassBoot(themes) {\n const table = Object.fromEntries(\n Object.entries(themes).map(([name, theme]) => [\n name,\n rootThemeAttrs(theme, options.paintDocument ?? false),\n ]),\n );\n return {\n script: themeClassBootScript(table),\n classes: [...new Set(Object.values(table).flatMap(({ classes }) => classes))],\n };\n },\n install(app) {\n const scope = effectScope();\n app.onUnmount(() => scope.stop());\n scope.run(() => {\n const prefersDark = usePrefersDark();\n const theme = computed(() =>\n resolveTheme(toValue(options.theme) ?? defaultConfig.theme, prefersDark.value),\n );\n const lang = computed(() => defineLang(defaultConfig.lang, toValue(options.lang) ?? {}));\n app.provide(ConfigContext.key, {\n get theme() {\n return theme.value;\n },\n get lang() {\n return lang.value;\n },\n highlighter: options.highlighter,\n textMatcher: options.textMatcher,\n defaults: options.defaults ?? {},\n styles: mergeStyles({}, options.styles),\n router: options.router,\n permissions: options.permissions,\n http: options.http,\n workCalendar: options.workCalendar,\n ripple: options.ripple ?? true,\n });\n // 应用级的语言也写在页面根元素上(Provider 写在自己的元素上):读屏按 lang 选发音,\n // 从右到左的语言靠 dir 镜像排版与滚动条;没传 lang 时不动,宿主自己管\n if (options.lang && typeof document !== 'undefined') {\n const root = document.documentElement;\n const previous = { lang: root.lang, dir: root.dir };\n watchEffect(() => {\n root.lang = lang.value.code;\n root.dir = lang.value.direction;\n });\n app.onUnmount(() => Object.assign(root, previous));\n }\n // 应用级没有容器元素,主题变量挂在页面根元素上;变量名带 vtp- 前缀,不会与宿主冲突。\n // 只开 paintDocument、不传主题时用默认主题(整页也要有底色、字体,body 去掉外边距)\n if ((options.theme || options.paintDocument) && typeof document !== 'undefined') {\n const root = document.documentElement;\n // themeBootScript 在应用加载前写的行内颜色:交给主题的 class\n if (options.paintDocument) root.style.removeProperty('background-color');\n if (options.paintDocument) root.style.removeProperty('color');\n // 服务端或防闪烁脚本写的主题类交给 applyTheme:留着的话,旧主题的类和新主题的类同时生效,谁赢全看规则的先后\n const rendered = root.dataset.themeClasses;\n if (rendered) root.classList.remove(...rendered.split(' '));\n delete root.dataset.themeClasses;\n watchEffect((onCleanup) =>\n onCleanup(applyTheme(root, theme.value, options.paintDocument ?? false)),\n );\n if (options.paintDocument && document.body) {\n const body = document.body;\n const classes = css.attrs(documentStyles.body).class?.split(' ') ?? [];\n body.classList.add(...classes);\n app.onUnmount(() => body.classList.remove(...classes));\n }\n }\n });\n },\n };\n}\n\n/** 主题挂到页面根元素上的类与 CSS 变量;paint 时加上整页的底色与字体(浏览器里写到元素上,服务端写进 HTML) */\nfunction rootThemeAttrs(theme: Theme, paint: boolean) {\n const classes =\n css\n .attrs(...theme.styles, paint && documentStyles.page)\n .class?.split(' ')\n .filter(Boolean) ?? [];\n return { classes, variables: themeVariables(theme) };\n}\n\n/**\n * 防闪烁脚本的第二段:themeBootScript 已按用户的偏好写好 data-theme,这里把服务端写的主题类换成那套主题的。\n * data-theme 不在候选表里时不动(用户自己定制的主题,水合后才有)\n */\nfunction themeClassBootScript(themes: Record<string, ReturnType<typeof rootThemeAttrs>>): string {\n // 主题名来自调用方,写进内联脚本要防 </script>\n const table = JSON.stringify(themes).replaceAll('<', '\\\\u003c');\n return `(()=>{const c=${table};const r=document.documentElement;const t=c[r.dataset.theme];if(!t)return;const o=r.dataset.themeClasses;if(o)r.classList.remove(...o.split(' '));r.classList.add(...t.classes);r.dataset.themeClasses=t.classes.join(' ');for(const n in t.variables)r.style.setProperty(n,t.variables[n])})()`;\n}\n\n/** 把主题的 class、CSS 变量与 data-theme 写到元素上,返回撤销函数 */\nfunction applyTheme(element: HTMLElement, theme: Theme, paint: boolean) {\n const { classes, variables } = rootThemeAttrs(theme, paint);\n element.classList.add(...classes);\n for (const [name, value] of Object.entries(variables)) element.style.setProperty(name, value);\n element.dataset.theme = theme.name;\n return () => {\n element.classList.remove(...classes);\n for (const name of Object.keys(variables)) element.style.removeProperty(name);\n delete element.dataset.theme;\n };\n}\n"],"mappings":"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"}
|
|
@@ -11,7 +11,8 @@ import { computed, effectScope, shallowRef } from "vue";
|
|
|
11
11
|
* app.use(createUi({ theme: () => preference.theme, paintDocument: true }));
|
|
12
12
|
* preference.set('paper', { origin: { x: event.clientX, y: event.clientY } }); // 从点击处圆形展开
|
|
13
13
|
*
|
|
14
|
-
* 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts
|
|
14
|
+
* 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts);
|
|
15
|
+
* 服务端渲染的页面还要加上 ui.themeClassBoot(…) 的脚本(见 createUi.ts)。
|
|
15
16
|
*/
|
|
16
17
|
/** 创建主题偏好;在应用的生命周期里一直有效(跟随系统与其他标签页的监听不撤销) */
|
|
17
18
|
function createThemePreference(options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preference.js","names":[],"sources":["../../../src/config/theme/preference.ts"],"sourcesContent":["/**\n * 主题偏好:用户选“跟随系统”或某一套主题,记在本地存储里,下次打开、其他标签页都沿用。\n * 在创建应用前调用,把 theme 交给 createUi:\n *\n * const preference = createThemePreference({ themes: { light: lightTheme, dark: darkTheme, paper: paperTheme } });\n * app.use(createUi({ theme: () => preference.theme, paintDocument: true }));\n * preference.set('paper', { origin: { x: event.clientX, y: event.clientY } }); // 从点击处圆形展开\n *\n * 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts)。\n */\nimport { computed, effectScope, shallowRef } from 'vue';\nimport { revealTransition, type RevealOrigin } from '../../utils/motion/reveal';\nimport { usePrefersDark } from '../../utils/dom/media-query';\nimport { DEFAULT_THEME_STORAGE_KEY, SYSTEM_THEME } from './boot';\nimport type { Theme } from './theme';\n\n/** createThemePreference 的选项 */\nexport interface ThemePreferenceOptions<Name extends string> {\n /** 可选的主题:名字 → 主题 */\n themes: Record<Name, Theme>;\n /** 跟随系统时,亮色与暗色各用哪一套;默认 light、dark */\n system?: { light: NoInfer<Name>; dark: NoInfer<Name> };\n /** 记住选择的 localStorage 键,默认 'vue-tsx-pro-theme';false 不记 */\n storageKey?: string | false;\n /** 没记过(或记的主题已经没有了)时的偏好,默认跟随系统 */\n fallback?: NoInfer<Name> | typeof SYSTEM_THEME;\n}\n\n/** 主题偏好:当前的选择、解析出的主题,以及切换 */\nexport interface ThemePreference<Name extends string> {\n /** 可选的主题名,按 themes 里的顺序(ThemeSwitcher 据此列出选项) */\n readonly names: readonly Name[];\n /** 当前的选择:某一套主题,或 'system' */\n readonly preference: Name | typeof SYSTEM_THEME;\n /** 解析出的主题名(跟随系统时按系统明暗) */\n readonly name: Name;\n /** 解析出的主题,交给 createUi 或 Provider */\n readonly theme: Theme;\n /** 切换;给了 origin 时从那里圆形展开新主题(见 revealTransition) */\n set(preference: Name | typeof SYSTEM_THEME, options?: { origin?: RevealOrigin }): Promise<void>;\n}\n\n/** 创建主题偏好;在应用的生命周期里一直有效(跟随系统与其他标签页的监听不撤销) */\nexport function createThemePreference<Name extends string>(\n options: ThemePreferenceOptions<Name>,\n): ThemePreference<Name> {\n const names = Object.keys(options.themes) as Name[];\n const system =\n options.system ?? ({ light: 'light', dark: 'dark' } as { light: Name; dark: Name });\n const storageKey = options.storageKey ?? DEFAULT_THEME_STORAGE_KEY;\n const fallback = options.fallback ?? SYSTEM_THEME;\n const isChoice = (value: unknown): value is Name | typeof SYSTEM_THEME =>\n value === SYSTEM_THEME || names.includes(value as Name);\n\n const read = () => {\n const stored = storageKey ? safeStorage()?.getItem(storageKey) : null;\n return isChoice(stored) ? stored : fallback;\n };\n const preference = shallowRef<Name | typeof SYSTEM_THEME>(read());\n\n // 跟随系统的监听不属于哪个组件,放在自己的作用域里\n const scope = effectScope(true);\n const name = scope.run(() => {\n const prefersDark = usePrefersDark();\n return computed<Name>(() =>\n preference.value === SYSTEM_THEME\n ? prefersDark.value\n ? system.dark\n : system.light\n : preference.value,\n );\n })!;\n\n // 其他标签页改了偏好:跟着变\n if (storageKey && typeof window !== 'undefined') {\n window.addEventListener('storage', (event) => {\n if (event.key === storageKey) preference.value = read();\n });\n }\n\n return {\n names,\n get preference() {\n return preference.value;\n },\n get name() {\n return name.value;\n },\n get theme() {\n return options.themes[name.value];\n },\n set(next, setOptions) {\n if (!isChoice(next)) throw new Error(`没有名为 ${String(next)} 的主题`);\n if (storageKey) safeStorage()?.setItem(storageKey, next);\n return revealTransition(() => (preference.value = next), setOptions?.origin);\n },\n };\n}\n\n/** 隐私模式、禁用存储时 localStorage 会抛错:当作没有存储 */\nfunction safeStorage(): Storage | undefined {\n try {\n return globalThis.localStorage;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"preference.js","names":[],"sources":["../../../src/config/theme/preference.ts"],"sourcesContent":["/**\n * 主题偏好:用户选“跟随系统”或某一套主题,记在本地存储里,下次打开、其他标签页都沿用。\n * 在创建应用前调用,把 theme 交给 createUi:\n *\n * const preference = createThemePreference({ themes: { light: lightTheme, dark: darkTheme, paper: paperTheme } });\n * app.use(createUi({ theme: () => preference.theme, paintDocument: true }));\n * preference.set('paper', { origin: { x: event.clientX, y: event.clientY } }); // 从点击处圆形展开\n *\n * 页面加载时先闪一下默认主题:在 HTML 的 <head> 里内联 themeBootScript(…) 生成的脚本,应用加载前就把底色设对(见 boot.ts);\n * 服务端渲染的页面还要加上 ui.themeClassBoot(…) 的脚本(见 createUi.ts)。\n */\nimport { computed, effectScope, shallowRef } from 'vue';\nimport { revealTransition, type RevealOrigin } from '../../utils/motion/reveal';\nimport { usePrefersDark } from '../../utils/dom/media-query';\nimport { DEFAULT_THEME_STORAGE_KEY, SYSTEM_THEME } from './boot';\nimport type { Theme } from './theme';\n\n/** createThemePreference 的选项 */\nexport interface ThemePreferenceOptions<Name extends string> {\n /** 可选的主题:名字 → 主题 */\n themes: Record<Name, Theme>;\n /** 跟随系统时,亮色与暗色各用哪一套;默认 light、dark */\n system?: { light: NoInfer<Name>; dark: NoInfer<Name> };\n /** 记住选择的 localStorage 键,默认 'vue-tsx-pro-theme';false 不记 */\n storageKey?: string | false;\n /** 没记过(或记的主题已经没有了)时的偏好,默认跟随系统 */\n fallback?: NoInfer<Name> | typeof SYSTEM_THEME;\n}\n\n/** 主题偏好:当前的选择、解析出的主题,以及切换 */\nexport interface ThemePreference<Name extends string> {\n /** 可选的主题名,按 themes 里的顺序(ThemeSwitcher 据此列出选项) */\n readonly names: readonly Name[];\n /** 当前的选择:某一套主题,或 'system' */\n readonly preference: Name | typeof SYSTEM_THEME;\n /** 解析出的主题名(跟随系统时按系统明暗) */\n readonly name: Name;\n /** 解析出的主题,交给 createUi 或 Provider */\n readonly theme: Theme;\n /** 切换;给了 origin 时从那里圆形展开新主题(见 revealTransition) */\n set(preference: Name | typeof SYSTEM_THEME, options?: { origin?: RevealOrigin }): Promise<void>;\n}\n\n/** 创建主题偏好;在应用的生命周期里一直有效(跟随系统与其他标签页的监听不撤销) */\nexport function createThemePreference<Name extends string>(\n options: ThemePreferenceOptions<Name>,\n): ThemePreference<Name> {\n const names = Object.keys(options.themes) as Name[];\n const system =\n options.system ?? ({ light: 'light', dark: 'dark' } as { light: Name; dark: Name });\n const storageKey = options.storageKey ?? DEFAULT_THEME_STORAGE_KEY;\n const fallback = options.fallback ?? SYSTEM_THEME;\n const isChoice = (value: unknown): value is Name | typeof SYSTEM_THEME =>\n value === SYSTEM_THEME || names.includes(value as Name);\n\n const read = () => {\n const stored = storageKey ? safeStorage()?.getItem(storageKey) : null;\n return isChoice(stored) ? stored : fallback;\n };\n const preference = shallowRef<Name | typeof SYSTEM_THEME>(read());\n\n // 跟随系统的监听不属于哪个组件,放在自己的作用域里\n const scope = effectScope(true);\n const name = scope.run(() => {\n const prefersDark = usePrefersDark();\n return computed<Name>(() =>\n preference.value === SYSTEM_THEME\n ? prefersDark.value\n ? system.dark\n : system.light\n : preference.value,\n );\n })!;\n\n // 其他标签页改了偏好:跟着变\n if (storageKey && typeof window !== 'undefined') {\n window.addEventListener('storage', (event) => {\n if (event.key === storageKey) preference.value = read();\n });\n }\n\n return {\n names,\n get preference() {\n return preference.value;\n },\n get name() {\n return name.value;\n },\n get theme() {\n return options.themes[name.value];\n },\n set(next, setOptions) {\n if (!isChoice(next)) throw new Error(`没有名为 ${String(next)} 的主题`);\n if (storageKey) safeStorage()?.setItem(storageKey, next);\n return revealTransition(() => (preference.value = next), setOptions?.origin);\n },\n };\n}\n\n/** 隐私模式、禁用存储时 localStorage 会抛错:当作没有存储 */\nfunction safeStorage(): Storage | undefined {\n try {\n return globalThis.localStorage;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4CA,SAAgB,sBACd,SACuB;CACvB,MAAM,QAAQ,OAAO,KAAK,QAAQ,MAAM;CACxC,MAAM,SACJ,QAAQ,UAAW;EAAE,OAAO;EAAS,MAAM;CAAO;CACpD,MAAM,aAAa,QAAQ,cAAA;CAC3B,MAAM,WAAW,QAAQ,YAAA;CACzB,MAAM,YAAY,UAChB,UAAA,YAA0B,MAAM,SAAS,KAAa;CAExD,MAAM,aAAa;EACjB,MAAM,SAAS,aAAa,YAAY,CAAC,EAAE,QAAQ,UAAU,IAAI;EACjE,OAAO,SAAS,MAAM,IAAI,SAAS;CACrC;CACA,MAAM,aAAa,WAAuC,KAAK,CAAC;CAIhE,MAAM,OADQ,YAAY,IACb,CAAA,CAAM,UAAU;EAC3B,MAAM,cAAc,eAAe;EACnC,OAAO,eACL,WAAW,UAAA,WACP,YAAY,QACV,OAAO,OACP,OAAO,QACT,WAAW,KACjB;CACF,CAAC;CAGD,IAAI,cAAc,OAAO,WAAW,aAClC,OAAO,iBAAiB,YAAY,UAAU;EAC5C,IAAI,MAAM,QAAQ,YAAY,WAAW,QAAQ,KAAK;CACxD,CAAC;CAGH,OAAO;EACL;EACA,IAAI,aAAa;GACf,OAAO,WAAW;EACpB;EACA,IAAI,OAAO;GACT,OAAO,KAAK;EACd;EACA,IAAI,QAAQ;GACV,OAAO,QAAQ,OAAO,KAAK;EAC7B;EACA,IAAI,MAAM,YAAY;GACpB,IAAI,CAAC,SAAS,IAAI,GAAG,MAAM,IAAI,MAAM,QAAQ,OAAO,IAAI,EAAE,KAAK;GAC/D,IAAI,YAAY,YAAY,CAAC,EAAE,QAAQ,YAAY,IAAI;GACvD,OAAO,uBAAwB,WAAW,QAAQ,MAAO,YAAY,MAAM;EAC7E;CACF;AACF;;AAGA,SAAS,cAAmC;CAC1C,IAAI;EACF,OAAO,WAAW;CACpB,QAAQ;EACN;CACF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -186,7 +186,7 @@ export { dataTableRecipe, dataTableStateStyles, type DataTableDensities, type Da
|
|
|
186
186
|
export { tableParams, useTableQuery, type TablePage, type TableQuery, type TableQueryOptions, type TableQueryState, } from './table/use-table-query';
|
|
187
187
|
export { ConfigScope, Provider, providerStyles, type ConfigScopeProps, type ProviderProps, } from './config/Provider';
|
|
188
188
|
export { ConfigContext, defaultConfig, useCan, useConfig, useLang, useTextMatcher, type Highlighter, type QueryPatch, type QueryValues, type TextMatcher, type UiConfig, type UiPermissions, type UiRouter, } from './config/context';
|
|
189
|
-
export { createUi, documentStyles, type DocumentAttrs, type UiOptions, type UiPlugin, } from './config/createUi';
|
|
189
|
+
export { createUi, documentStyles, type DocumentAttrs, type ThemeClassBoot, type UiOptions, type UiPlugin, } from './config/createUi';
|
|
190
190
|
export { mergeDefaults, useDefaults, type ComponentDefaults, type ResolvedDefaults, } from './config/defaults';
|
|
191
191
|
export { queryCodecs, useQueryState, useQueryStates, type QueryCodec, type QueryCodecs, } from './config/query-state';
|
|
192
192
|
export { mergeStyles, partAttrs, useRecipe, type ComponentStyles, type PartAttrs, type PartLayer, type StyleLayers, } from './config/styles';
|
|
@@ -278,7 +278,7 @@ export { computeMeasurements, defaultRangeExtractor, findVisibleRange, isWindow,
|
|
|
278
278
|
export { codeText, errorCodeOf, errorCodeText, errorMessage, useErrorMessage, } from './utils/request/error-message';
|
|
279
279
|
export { createHttpClient, HTTP_ERROR_CODES, HttpError, isAbortError, OK_CODE, scopeHttp, useHttp, type ApiEnvelope, type ApiErrorDetail, type FetchClient, type HttpClient, type HttpClientOptions, type HttpErrorCode, type HttpErrorContext, type HttpMethod, type HttpRequest, type HttpWire, type QueryValue, } from './utils/request/http';
|
|
280
280
|
export { useAutosave, type Autosave, type AutosaveOptions, type AutosaveStatus, } from './utils/request/use-autosave';
|
|
281
|
-
export { useRequest, type RequestOptions, type RequestState } from './utils/request/use-request';
|
|
281
|
+
export { useRequest, type RequestCache, type RequestOptions, type RequestState, } from './utils/request/use-request';
|
|
282
282
|
export { fromWire, toWire, wireText, type WireReviveOptions, type WireScalar, type WireType, type WireTypes, } from './utils/request/wire';
|
|
283
283
|
export { useActiveIndicator, type ActiveIndicatorOptions, type IndicatorAxes, } from './utils/motion/active-indicator';
|
|
284
284
|
export { parseDuration, themeTransition, themeTransitionOrInstant, useAnimate, type ThemeAnimate, type ThemeAnimateOptions, type ThemeTiming, type ThemeTransitionOptions, } from './utils/motion/animation';
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/** 请求结果的缓存:@zerodep-dev/storage 的 createCache 满足这个形状(按形状对接,组件库不依赖它) */
|
|
2
|
+
export interface RequestCache {
|
|
3
|
+
/** 缓存的值;fresh 为 false 是过期但还能先显示的 */
|
|
4
|
+
read(key: string): Promise<{
|
|
5
|
+
value: unknown;
|
|
6
|
+
fresh: boolean;
|
|
7
|
+
} | undefined>;
|
|
8
|
+
/** 写回请求的结果;tags 是这条依赖的东西(后端的表名),按它失效 */
|
|
9
|
+
set(key: string, value: unknown, options?: {
|
|
10
|
+
tags?: readonly string[];
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
/** 缓存的键失效或被别处改了时回调(keys 为 undefined 表示全清了);返回取消收听 */
|
|
13
|
+
watch?(listener: (keys: readonly string[] | undefined) => void): () => void;
|
|
14
|
+
}
|
|
1
15
|
/** useRequest 的选项 */
|
|
2
16
|
export interface RequestOptions<P, T> {
|
|
3
17
|
/** 请求参数:读到的响应式值变了、且内容确实不同时重新请求;不写时只在开始与 refresh 时请求 */
|
|
@@ -13,6 +27,16 @@ export interface RequestOptions<P, T> {
|
|
|
13
27
|
* 变回 true 时不自动补一次,需要时调用 refresh
|
|
14
28
|
*/
|
|
15
29
|
enabled?: () => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* 结果的缓存(先显示旧值、后台刷新):有缓存先显示;新鲜的不再请求(refresh 照样请求),过期的接着请求、到了替换并写回。
|
|
32
|
+
* key 由参数算出,不同参数各存各的;tags 是写回时标上的标签(依赖的表)。缓存读写出错时当作没有缓存,命中新鲜缓存时不调 onSuccess。
|
|
33
|
+
* 当前参数的键失效了(本页或别的标签页)自动再请求一次;别的标签页写进了新值时直接用它,不再请求
|
|
34
|
+
*/
|
|
35
|
+
cache?: {
|
|
36
|
+
store: RequestCache;
|
|
37
|
+
key: (params: P) => string;
|
|
38
|
+
tags?: (params: P) => readonly string[];
|
|
39
|
+
};
|
|
16
40
|
/** 成功后调用 */
|
|
17
41
|
onSuccess?: (data: T, params: P) => void;
|
|
18
42
|
/** 失败后调用(取消的不算) */
|
|
@@ -26,7 +50,7 @@ export interface RequestState<T> {
|
|
|
26
50
|
readonly error: unknown;
|
|
27
51
|
/** 正在请求(防抖等待时还不算) */
|
|
28
52
|
readonly loading: boolean;
|
|
29
|
-
/**
|
|
53
|
+
/** 按当前参数立即请求(缓存新鲜也请求),取消进行中的;返回结果,失败或被后来的请求顶掉时为 undefined */
|
|
30
54
|
refresh(): Promise<T | undefined>;
|
|
31
55
|
/** 取消进行中的请求与等着的防抖 */
|
|
32
56
|
cancel(): void;
|
|
@@ -13,12 +13,21 @@ import { onScopeDispose, shallowRef, watch } from "vue";
|
|
|
13
13
|
* );
|
|
14
14
|
* users.data?.rows; users.loading; users.error; users.refresh();
|
|
15
15
|
*
|
|
16
|
+
* 结果可以经 cache 选项存进 IndexedDB、localStorage(@zerodep-dev/storage 的 createCache):
|
|
17
|
+
*
|
|
18
|
+
* const cache = createCache(indexedDbDriver({ name: 'app' }), { ttl: 60_000, stale: 86_400_000 });
|
|
19
|
+
* useRequest(fetchCities, {
|
|
20
|
+
* params: () => province,
|
|
21
|
+
* cache: { store: cache, key: (p) => `cities:${p}`, tags: () => ['t_city'] }, // 标上依赖的表
|
|
22
|
+
* });
|
|
23
|
+
* // t_city 改了(kit 的失效推送、或自己保存后):await cache.invalidateTag('t_city'),正显示着的这个请求自动重新请求
|
|
24
|
+
*
|
|
16
25
|
* 只在浏览器里自动请求:服务端渲染时 data 留空,等页面在浏览器里接管后再请求。
|
|
17
26
|
* 一开始就请求时 loading 从 true 开始,服务端也按“加载中”渲染:水合时两边一致,浏览器接着发请求。
|
|
18
27
|
*/
|
|
19
28
|
/** 请求的状态(在 setup 里调用;见文件开头) */
|
|
20
29
|
function useRequest(fetcher, options = {}) {
|
|
21
|
-
const { params, debounce = 0, immediate = true, keepPrevious = true } = options;
|
|
30
|
+
const { params, cache, debounce = 0, immediate = true, keepPrevious = true } = options;
|
|
22
31
|
const data = shallowRef();
|
|
23
32
|
const error = shallowRef();
|
|
24
33
|
const starts = immediate && options.enabled?.() !== false;
|
|
@@ -35,7 +44,11 @@ function useRequest(fetcher, options = {}) {
|
|
|
35
44
|
controller = void 0;
|
|
36
45
|
loading.value = false;
|
|
37
46
|
}
|
|
38
|
-
|
|
47
|
+
/** 读缓存;没配缓存时 undefined(不 await,没缓存的请求照旧同步发出) */
|
|
48
|
+
function readCache(current) {
|
|
49
|
+
return cache?.store.read(cache.key(current)).catch(() => void 0);
|
|
50
|
+
}
|
|
51
|
+
async function run(current, force = false) {
|
|
39
52
|
cancel();
|
|
40
53
|
last = current;
|
|
41
54
|
const id = latest;
|
|
@@ -43,10 +56,21 @@ function useRequest(fetcher, options = {}) {
|
|
|
43
56
|
loading.value = true;
|
|
44
57
|
if (!keepPrevious) data.value = void 0;
|
|
45
58
|
try {
|
|
59
|
+
const reading = readCache(current);
|
|
60
|
+
const cached = reading && await reading;
|
|
61
|
+
if (id !== latest) return void 0;
|
|
62
|
+
if (cached) {
|
|
63
|
+
data.value = cached.value;
|
|
64
|
+
if (cached.fresh && !force) return cached.value;
|
|
65
|
+
}
|
|
46
66
|
const result = await fetcher(current, own.signal);
|
|
47
67
|
if (id !== latest) return void 0;
|
|
48
68
|
data.value = result;
|
|
49
69
|
error.value = void 0;
|
|
70
|
+
if (cache) {
|
|
71
|
+
const tags = cache.tags?.(current);
|
|
72
|
+
cache.store.set(cache.key(current), result, tags ? { tags } : void 0).catch(() => void 0);
|
|
73
|
+
}
|
|
50
74
|
options.onSuccess?.(result, current);
|
|
51
75
|
return result;
|
|
52
76
|
} catch (reason) {
|
|
@@ -70,6 +94,14 @@ function useRequest(fetcher, options = {}) {
|
|
|
70
94
|
else run(next);
|
|
71
95
|
});
|
|
72
96
|
if (starts && typeof window !== "undefined") run(readParams());
|
|
97
|
+
if (cache?.store.watch && typeof window !== "undefined") {
|
|
98
|
+
const stop = cache.store.watch((keys) => {
|
|
99
|
+
if (latest === 0 || options.enabled?.() === false) return;
|
|
100
|
+
const current = readParams();
|
|
101
|
+
if (!keys || keys.includes(cache.key(current))) run(current);
|
|
102
|
+
});
|
|
103
|
+
onScopeDispose(stop);
|
|
104
|
+
}
|
|
73
105
|
onScopeDispose(cancel);
|
|
74
106
|
return {
|
|
75
107
|
get data() {
|
|
@@ -81,7 +113,7 @@ function useRequest(fetcher, options = {}) {
|
|
|
81
113
|
get loading() {
|
|
82
114
|
return loading.value;
|
|
83
115
|
},
|
|
84
|
-
refresh: () => run(readParams()),
|
|
116
|
+
refresh: () => run(readParams(), true),
|
|
85
117
|
cancel,
|
|
86
118
|
mutate: (value) => data.value = value
|
|
87
119
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-request.js","names":[],"sources":["../../../src/utils/request/use-request.ts"],"sourcesContent":["/**\n * 一个请求的状态(参考 TanStack Query、alova 的 useWatcher):只采用最后一次的结果,发新请求时取消上一次,\n * 参数变化防抖,重新请求时保留旧数据(表格不闪成空的)。\n *\n * const http = useHttp();\n * const users = useRequest(\n * ({ keyword, page }, signal) => http.request<Page<User>>({ url: '/users', query: { keyword, page }, signal }),\n * { params: () => ({ keyword, page }), debounce: 300 },\n * );\n * users.data?.rows; users.loading; users.error; users.refresh();\n *\n * 只在浏览器里自动请求:服务端渲染时 data 留空,等页面在浏览器里接管后再请求。\n * 一开始就请求时 loading 从 true 开始,服务端也按“加载中”渲染:水合时两边一致,浏览器接着发请求。\n */\nimport { onScopeDispose, shallowRef, watch } from 'vue';\nimport { sameValue } from '../data/equality';\nimport { isAbortError } from './http';\n\n/** useRequest 的选项 */\nexport interface RequestOptions<P, T> {\n /** 请求参数:读到的响应式值变了、且内容确实不同时重新请求;不写时只在开始与 refresh 时请求 */\n params?: () => P;\n /** 参数变了以后等多久再请求(毫秒),默认 0;refresh 不等 */\n debounce?: number;\n /** 一开始就请求,默认 true;false 时等调用 refresh(或参数变化) */\n immediate?: boolean;\n /** 重新请求时保留上一次的数据,默认 true;false 时请求一开始就清空 */\n keepPrevious?: boolean;\n /**\n * 自动请求的开关(弹层打开时才请求这类):为 false 时 immediate 与参数变化都不请求,refresh 照常。\n * 变回 true 时不自动补一次,需要时调用 refresh\n */\n enabled?: () => boolean;\n /** 成功后调用 */\n onSuccess?: (data: T, params: P) => void;\n /** 失败后调用(取消的不算) */\n onError?: (error: unknown, params: P) => void;\n}\n\n/** useRequest 的返回值 */\nexport interface RequestState<T> {\n /** 最后一次成功的结果 */\n readonly data: T | undefined;\n /** 最后一次失败的原因;成功后清空 */\n readonly error: unknown;\n /** 正在请求(防抖等待时还不算) */\n readonly loading: boolean;\n /**
|
|
1
|
+
{"version":3,"file":"use-request.js","names":[],"sources":["../../../src/utils/request/use-request.ts"],"sourcesContent":["/**\n * 一个请求的状态(参考 TanStack Query、alova 的 useWatcher):只采用最后一次的结果,发新请求时取消上一次,\n * 参数变化防抖,重新请求时保留旧数据(表格不闪成空的)。\n *\n * const http = useHttp();\n * const users = useRequest(\n * ({ keyword, page }, signal) => http.request<Page<User>>({ url: '/users', query: { keyword, page }, signal }),\n * { params: () => ({ keyword, page }), debounce: 300 },\n * );\n * users.data?.rows; users.loading; users.error; users.refresh();\n *\n * 结果可以经 cache 选项存进 IndexedDB、localStorage(@zerodep-dev/storage 的 createCache):\n *\n * const cache = createCache(indexedDbDriver({ name: 'app' }), { ttl: 60_000, stale: 86_400_000 });\n * useRequest(fetchCities, {\n * params: () => province,\n * cache: { store: cache, key: (p) => `cities:${p}`, tags: () => ['t_city'] }, // 标上依赖的表\n * });\n * // t_city 改了(kit 的失效推送、或自己保存后):await cache.invalidateTag('t_city'),正显示着的这个请求自动重新请求\n *\n * 只在浏览器里自动请求:服务端渲染时 data 留空,等页面在浏览器里接管后再请求。\n * 一开始就请求时 loading 从 true 开始,服务端也按“加载中”渲染:水合时两边一致,浏览器接着发请求。\n */\nimport { onScopeDispose, shallowRef, watch } from 'vue';\nimport { sameValue } from '../data/equality';\nimport { isAbortError } from './http';\n\n/** 请求结果的缓存:@zerodep-dev/storage 的 createCache 满足这个形状(按形状对接,组件库不依赖它) */\nexport interface RequestCache {\n /** 缓存的值;fresh 为 false 是过期但还能先显示的 */\n read(key: string): Promise<{ value: unknown; fresh: boolean } | undefined>;\n /** 写回请求的结果;tags 是这条依赖的东西(后端的表名),按它失效 */\n set(key: string, value: unknown, options?: { tags?: readonly string[] }): Promise<void>;\n /** 缓存的键失效或被别处改了时回调(keys 为 undefined 表示全清了);返回取消收听 */\n watch?(listener: (keys: readonly string[] | undefined) => void): () => void;\n}\n\n/** useRequest 的选项 */\nexport interface RequestOptions<P, T> {\n /** 请求参数:读到的响应式值变了、且内容确实不同时重新请求;不写时只在开始与 refresh 时请求 */\n params?: () => P;\n /** 参数变了以后等多久再请求(毫秒),默认 0;refresh 不等 */\n debounce?: number;\n /** 一开始就请求,默认 true;false 时等调用 refresh(或参数变化) */\n immediate?: boolean;\n /** 重新请求时保留上一次的数据,默认 true;false 时请求一开始就清空 */\n keepPrevious?: boolean;\n /**\n * 自动请求的开关(弹层打开时才请求这类):为 false 时 immediate 与参数变化都不请求,refresh 照常。\n * 变回 true 时不自动补一次,需要时调用 refresh\n */\n enabled?: () => boolean;\n /**\n * 结果的缓存(先显示旧值、后台刷新):有缓存先显示;新鲜的不再请求(refresh 照样请求),过期的接着请求、到了替换并写回。\n * key 由参数算出,不同参数各存各的;tags 是写回时标上的标签(依赖的表)。缓存读写出错时当作没有缓存,命中新鲜缓存时不调 onSuccess。\n * 当前参数的键失效了(本页或别的标签页)自动再请求一次;别的标签页写进了新值时直接用它,不再请求\n */\n cache?: {\n store: RequestCache;\n key: (params: P) => string;\n tags?: (params: P) => readonly string[];\n };\n /** 成功后调用 */\n onSuccess?: (data: T, params: P) => void;\n /** 失败后调用(取消的不算) */\n onError?: (error: unknown, params: P) => void;\n}\n\n/** useRequest 的返回值 */\nexport interface RequestState<T> {\n /** 最后一次成功的结果 */\n readonly data: T | undefined;\n /** 最后一次失败的原因;成功后清空 */\n readonly error: unknown;\n /** 正在请求(防抖等待时还不算) */\n readonly loading: boolean;\n /** 按当前参数立即请求(缓存新鲜也请求),取消进行中的;返回结果,失败或被后来的请求顶掉时为 undefined */\n refresh(): Promise<T | undefined>;\n /** 取消进行中的请求与等着的防抖 */\n cancel(): void;\n /** 直接改数据(乐观更新、本地删掉一行),不发请求 */\n mutate(data: T | undefined): void;\n}\n\n/** 请求的状态(在 setup 里调用;见文件开头) */\nexport function useRequest<T, P = undefined>(\n fetcher: (params: P, signal: AbortSignal) => Promise<T>,\n options: RequestOptions<P, T> = {},\n): RequestState<T> {\n const { params, cache, debounce = 0, immediate = true, keepPrevious = true } = options;\n const data = shallowRef<T>();\n const error = shallowRef<unknown>();\n const starts = immediate && options.enabled?.() !== false;\n const loading = shallowRef(starts);\n let controller: AbortController | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n // 每次请求一个编号:结果回来时不是最后一次的就丢掉(fetcher 没理会 signal 时也不会覆盖)\n let latest = 0;\n const readParams = () => params?.() as P;\n // 上一次请求(或开始时)的参数:参数变了但与它相同时不再请求(refresh 之后侦听器又看到同一组参数)\n let last = params ? readParams() : undefined;\n\n function cancel() {\n clearTimeout(timer);\n latest++;\n controller?.abort();\n controller = undefined;\n loading.value = false;\n }\n\n /** 读缓存;没配缓存时 undefined(不 await,没缓存的请求照旧同步发出) */\n function readCache(current: P) {\n return cache?.store.read(cache.key(current)).catch(() => undefined);\n }\n\n async function run(current: P, force = false): Promise<T | undefined> {\n cancel();\n last = current;\n const id = latest;\n const own = (controller = new AbortController());\n loading.value = true;\n if (!keepPrevious) data.value = undefined;\n try {\n const reading = readCache(current);\n const cached = reading && (await reading);\n if (id !== latest) return undefined;\n if (cached) {\n data.value = cached.value as T;\n if (cached.fresh && !force) return cached.value as T;\n }\n const result = await fetcher(current, own.signal);\n if (id !== latest) return undefined;\n data.value = result;\n error.value = undefined;\n if (cache) {\n const tags = cache.tags?.(current);\n void cache.store\n .set(cache.key(current), result, tags ? { tags } : undefined)\n .catch(() => undefined);\n }\n options.onSuccess?.(result, current);\n return result;\n } catch (reason) {\n if (id !== latest || isAbortError(reason)) return undefined;\n error.value = reason;\n options.onError?.(reason, current);\n return undefined;\n } finally {\n if (id === latest) {\n loading.value = false;\n controller = undefined;\n }\n }\n }\n\n if (params) {\n watch(readParams, (next) => {\n if (options.enabled?.() === false) return;\n if (sameValue(next, last)) return;\n last = next;\n clearTimeout(timer);\n if (debounce > 0) timer = setTimeout(() => void run(next), debounce);\n else void run(next);\n });\n }\n if (starts && typeof window !== 'undefined') {\n void run(readParams());\n }\n if (cache?.store.watch && typeof window !== 'undefined') {\n // 不强制:失效了缓存里没有、照常请求;别的标签页刚写进新值时直接读到它。还没请求过(immediate: false)的不管\n const stop = cache.store.watch((keys) => {\n if (latest === 0 || options.enabled?.() === false) return;\n const current = readParams();\n if (!keys || keys.includes(cache.key(current))) void run(current);\n });\n onScopeDispose(stop);\n }\n onScopeDispose(cancel);\n\n return {\n get data() {\n return data.value;\n },\n get error() {\n return error.value;\n },\n get loading() {\n return loading.value;\n },\n refresh: () => run(readParams(), true),\n cancel,\n mutate: (value) => (data.value = value),\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerodep-dev/vue-tsx-pro-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "vue-tsx-pro 组件库",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"@floating-ui/dom": "1.8.0",
|
|
118
118
|
"@internationalized/date": "3.12.4",
|
|
119
119
|
"@radix-ui/colors": "3.0.0",
|
|
120
|
-
"@zerodep-dev/vue-tsx-pro-tsx": "^0.4.
|
|
120
|
+
"@zerodep-dev/vue-tsx-pro-tsx": "^0.4.1",
|
|
121
121
|
"animejs": "4.5.0",
|
|
122
122
|
"culori": "4.0.2",
|
|
123
123
|
"decimal.js": "10.6.0",
|