@movk/nuxt 0.1.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +84 -9
  2. package/dist/module.d.mts +19 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +77 -8
  5. package/dist/runtime/components/AutoForm.d.vue.ts +12 -6
  6. package/dist/runtime/components/AutoForm.vue +4 -1
  7. package/dist/runtime/components/AutoForm.vue.d.ts +12 -6
  8. package/dist/runtime/components/ColorChooser.d.vue.ts +11 -5
  9. package/dist/runtime/components/ColorChooser.vue.d.ts +11 -5
  10. package/dist/runtime/components/DatePicker.d.vue.ts +14 -5
  11. package/dist/runtime/components/DatePicker.vue.d.ts +14 -5
  12. package/dist/runtime/components/SlideVerify.d.vue.ts +107 -0
  13. package/dist/runtime/components/SlideVerify.vue +147 -0
  14. package/dist/runtime/components/SlideVerify.vue.d.ts +107 -0
  15. package/dist/runtime/components/StarRating.d.vue.ts +7 -7
  16. package/dist/runtime/components/StarRating.vue +1 -0
  17. package/dist/runtime/components/StarRating.vue.d.ts +7 -7
  18. package/dist/runtime/components/auto-form-renderer/AutoFormRendererArray.d.vue.ts +6 -4
  19. package/dist/runtime/components/auto-form-renderer/AutoFormRendererArray.vue +1 -1
  20. package/dist/runtime/components/auto-form-renderer/AutoFormRendererArray.vue.d.ts +6 -4
  21. package/dist/runtime/components/auto-form-renderer/AutoFormRendererField.d.vue.ts +6 -4
  22. package/dist/runtime/components/auto-form-renderer/AutoFormRendererField.vue.d.ts +6 -4
  23. package/dist/runtime/components/auto-form-renderer/AutoFormRendererLayout.d.vue.ts +6 -4
  24. package/dist/runtime/components/auto-form-renderer/AutoFormRendererLayout.vue.d.ts +6 -4
  25. package/dist/runtime/components/auto-form-renderer/AutoFormRendererNested.d.vue.ts +6 -4
  26. package/dist/runtime/components/auto-form-renderer/AutoFormRendererNested.vue.d.ts +6 -4
  27. package/dist/runtime/components/input/WithCharacterLimit.d.vue.ts +11 -5
  28. package/dist/runtime/components/input/WithCharacterLimit.vue.d.ts +11 -5
  29. package/dist/runtime/components/input/WithClear.d.vue.ts +12 -5
  30. package/dist/runtime/components/input/WithClear.vue.d.ts +12 -5
  31. package/dist/runtime/components/input/WithCopy.d.vue.ts +12 -5
  32. package/dist/runtime/components/input/WithCopy.vue.d.ts +12 -5
  33. package/dist/runtime/components/input/WithPasswordToggle.d.vue.ts +11 -5
  34. package/dist/runtime/components/input/WithPasswordToggle.vue.d.ts +11 -5
  35. package/dist/runtime/components/theme-picker/ThemePicker.d.vue.ts +3 -0
  36. package/dist/runtime/components/theme-picker/ThemePicker.vue +235 -0
  37. package/dist/runtime/components/theme-picker/ThemePicker.vue.d.ts +3 -0
  38. package/dist/runtime/components/theme-picker/ThemePickerButton.d.vue.ts +18 -0
  39. package/dist/runtime/components/theme-picker/ThemePickerButton.vue +34 -0
  40. package/dist/runtime/components/theme-picker/ThemePickerButton.vue.d.ts +18 -0
  41. package/dist/runtime/composables/useApiAuth.d.ts +47 -0
  42. package/dist/runtime/composables/useApiAuth.js +66 -0
  43. package/dist/runtime/composables/useApiFetch.d.ts +42 -0
  44. package/dist/runtime/composables/useApiFetch.js +41 -0
  45. package/dist/runtime/composables/useAutoForm.d.ts +81 -605
  46. package/dist/runtime/composables/useAutoForm.js +3 -1
  47. package/dist/runtime/composables/useClientApiFetch.d.ts +24 -0
  48. package/dist/runtime/composables/useClientApiFetch.js +8 -0
  49. package/dist/runtime/composables/useDateFormatter.d.ts +21 -7
  50. package/dist/runtime/composables/useDateFormatter.js +92 -57
  51. package/dist/runtime/composables/useDownloadWithProgress.d.ts +48 -0
  52. package/dist/runtime/composables/useDownloadWithProgress.js +85 -0
  53. package/dist/runtime/composables/useTheme.d.ts +21 -0
  54. package/dist/runtime/composables/useTheme.js +143 -0
  55. package/dist/runtime/composables/useUploadWithProgress.d.ts +52 -0
  56. package/dist/runtime/composables/useUploadWithProgress.js +117 -0
  57. package/dist/runtime/internal/useAutoFormProvider.js +2 -2
  58. package/dist/runtime/plugins/api.factory.d.ts +2 -0
  59. package/dist/runtime/plugins/api.factory.js +186 -0
  60. package/dist/runtime/plugins/theme.d.ts +2 -0
  61. package/dist/runtime/plugins/theme.js +89 -0
  62. package/dist/runtime/schemas/api.d.ts +590 -0
  63. package/dist/runtime/schemas/api.js +228 -0
  64. package/dist/runtime/server/api/_movk/session.post.d.ts +10 -0
  65. package/dist/runtime/server/api/_movk/session.post.js +18 -0
  66. package/dist/runtime/style.css +1 -0
  67. package/dist/runtime/types/api.d.ts +218 -0
  68. package/dist/runtime/types/api.js +0 -0
  69. package/dist/runtime/types/auth.d.ts +34 -0
  70. package/dist/runtime/types/auto-form-renderer.d.ts +14 -22
  71. package/dist/runtime/types/auto-form-renderer.js +0 -0
  72. package/dist/runtime/types/components.d.ts +29 -41
  73. package/dist/runtime/types/components.js +0 -0
  74. package/dist/runtime/types/index.d.ts +1 -0
  75. package/dist/runtime/types/index.js +3 -2
  76. package/dist/runtime/utils/api-utils.d.ts +79 -0
  77. package/dist/runtime/utils/api-utils.js +127 -0
  78. package/package.json +38 -31
@@ -6,6 +6,7 @@ import WithCharacterLimit from "../components/input/WithCharacterLimit.vue";
6
6
  import DatePicker from "../components/DatePicker.vue";
7
7
  import ColorChooser from "../components/ColorChooser.vue";
8
8
  import StarRating from "../components/StarRating.vue";
9
+ import SlideVerify from "../components/SlideVerify.vue";
9
10
  import {
10
11
  UInput,
11
12
  UInputNumber,
@@ -193,7 +194,8 @@ const DEFAULT_CONTROLS = {
193
194
  withCopy: defineControl({ component: WithCopy, controlProps: DEFAULT_CONTROL_PROPS }),
194
195
  withCharacterLimit: defineControl({ component: WithCharacterLimit, controlProps: DEFAULT_CONTROL_PROPS }),
195
196
  colorChooser: defineControl({ component: ColorChooser, controlProps: DEFAULT_CONTROL_PROPS }),
196
- starRating: defineControl({ component: StarRating, controlProps: DEFAULT_CONTROL_PROPS })
197
+ starRating: defineControl({ component: StarRating, controlProps: DEFAULT_CONTROL_PROPS }),
198
+ slideVerify: defineControl({ component: SlideVerify, controlProps: DEFAULT_CONTROL_PROPS })
197
199
  };
198
200
  export function useAutoForm(controls) {
199
201
  function createZodFactory(_controls) {
@@ -0,0 +1,24 @@
1
+ import type { UseApiFetchOptions, UseApiFetchReturn } from '../types/api.js';
2
+ /**
3
+ * 仅客户端执行的 useApiFetch
4
+ *
5
+ * 设置 `server: false, lazy: true`
6
+ * 适合非 SEO 敏感数据,需手动调用 execute() 触发请求
7
+ *
8
+ * @typeParam ResT - API 响应 data 字段的原始类型
9
+ * @typeParam DataT - transform 转换后的最终类型(默认等于 ResT)
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const { data, execute } = useClientApiFetch<User>('/user/preferences')
14
+ *
15
+ * // 在 onMounted 或用户操作时触发
16
+ * onMounted(() => execute())
17
+ *
18
+ * // 使用 transform 转换数据(接收解包后的数据)
19
+ * const { data } = useClientApiFetch<{ content: User[] }, User[]>('/users', {
20
+ * transform: ({ content }) => content ?? []
21
+ * })
22
+ * ```
23
+ */
24
+ export declare function useClientApiFetch<ResT = unknown, DataT = ResT>(url: string | (() => string), options?: UseApiFetchOptions<ResT, DataT>): UseApiFetchReturn<DataT>;
@@ -0,0 +1,8 @@
1
+ import { useApiFetch } from "./useApiFetch.js";
2
+ export function useClientApiFetch(url, options = {}) {
3
+ return useApiFetch(url, {
4
+ ...options,
5
+ lazy: true,
6
+ server: false
7
+ });
8
+ }
@@ -1,26 +1,40 @@
1
1
  import type { DateValue } from '@internationalized/date';
2
2
  import type { DateRange } from 'reka-ui';
3
+ /**
4
+ * 日期格式化器配置选项
5
+ */
3
6
  export interface DateFormatterOptions {
4
7
  /**
5
8
  * 语言区域
6
- * @default 'zh-CN'
9
+ * @defaultValue 'zh-CN'
7
10
  */
8
11
  locale?: string;
9
12
  /**
10
13
  * 日期格式化选项
11
- * @see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
14
+ * @see {@link https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat | Intl.DateTimeFormat}
12
15
  */
13
16
  formatOptions?: Intl.DateTimeFormatOptions;
14
17
  /**
15
- * 时区标识符,默认使用本地时区
16
- * @see https://react-spectrum.adobe.com/internationalized/date/index.html#timezones
18
+ * 时区标识符,默认使用本地时区
19
+ * @see {@link https://react-spectrum.adobe.com/internationalized/date/index.html#timezones | 时区文档}
17
20
  */
18
21
  timeZone?: string;
19
22
  }
23
+ /**
24
+ * 检查值是否为 DateValue 类型
25
+ */
20
26
  declare function isDateValue(value: unknown): value is DateValue;
27
+ /**
28
+ * 检查值是否为 DateRange 类型
29
+ */
21
30
  declare function isDateRange(value: unknown): value is DateRange;
22
31
  /**
23
- * 日期格式化工具
32
+ * 日期格式化工具 Composable
33
+ *
34
+ * 提供日期格式化、转换、解析和查询功能,基于 `@internationalized/date` 库实现国际化支持。
35
+ *
36
+ * @param options - 配置选项
37
+ * @returns 日期格式化工具集
24
38
  *
25
39
  * @example
26
40
  * ```ts
@@ -29,7 +43,6 @@ declare function isDateRange(value: unknown): value is DateRange;
29
43
  * formatter.format(date) // "2025年11月6日"
30
44
  * formatter.toISO(date) // "2025-11-06"
31
45
  * formatter.toTimestamp(date) // 1730822400000
32
- * formatter.toUnixTimestamp(date) // 1730822400
33
46
  * formatter.getToday() // 今天的日期
34
47
  * formatter.isWeekend(date) // 是否周末
35
48
  * ```
@@ -43,7 +56,7 @@ export declare function useDateFormatter(options?: DateFormatterOptions): {
43
56
  toTimestamp: (date: DateValue | undefined | null) => number | null;
44
57
  toUnixTimestamp: (date: DateValue | undefined | null) => number | null;
45
58
  parse: (value: string) => import("@internationalized/date").CalendarDate | import("@internationalized/date").CalendarDateTime | import("@internationalized/date").ZonedDateTime | null;
46
- convertData: <T>(data: T, converter: (value: DateValue) => any) => T;
59
+ convertData: <T>(data: T, converter: (value: DateValue) => unknown) => T;
47
60
  getToday: () => import("@internationalized/date").CalendarDate;
48
61
  getNow: () => import("@internationalized/date").ZonedDateTime;
49
62
  getStartOfWeek: (date: DateValue) => DateValue;
@@ -53,6 +66,7 @@ export declare function useDateFormatter(options?: DateFormatterOptions): {
53
66
  getStartOfYear: (date: DateValue) => DateValue;
54
67
  getEndOfYear: (date: DateValue) => DateValue;
55
68
  getDayOfWeek: (date: DateValue) => number;
69
+ getDayOfWeekName: (date: DateValue, style?: "narrow" | "short" | "long") => string;
56
70
  getWeeksInMonth: (date: DateValue) => number;
57
71
  isWeekday: (date: DateValue) => boolean;
58
72
  isWeekend: (date: DateValue) => boolean;
@@ -33,89 +33,123 @@ export function useDateFormatter(options = {}) {
33
33
  const locale = options.locale ?? DEFAULT_LOCALE;
34
34
  const formatOptions = options.formatOptions ?? DEFAULT_FORMAT_OPTIONS;
35
35
  const timeZone = options.timeZone ?? getLocalTimeZone();
36
- const formatter = new DateFormatter(locale, {
37
- ...formatOptions,
38
- timeZone
39
- });
40
- const format = (date) => {
36
+ const formatter = new DateFormatter(locale, { ...formatOptions, timeZone });
37
+ function format(date) {
41
38
  if (!date) return "";
42
39
  try {
43
40
  return formatter.format(date.toDate(timeZone));
44
- } catch (error) {
45
- console.error("[useDateFormatter] Format error:", error);
41
+ } catch {
46
42
  return "";
47
43
  }
48
- };
49
- const formatRange = (start, end, separator = " - ") => {
44
+ }
45
+ function formatRange(start, end, separator = " - ") {
50
46
  if (!start || !end) return "";
51
47
  return `${format(start)}${separator}${format(end)}`;
52
- };
53
- const formatArray = (dates, separator = ", ") => {
48
+ }
49
+ function formatArray(dates, separator = ", ") {
54
50
  if (!dates?.length) return "";
55
51
  return dates.map(format).join(separator);
56
- };
57
- const toISO = (date) => {
52
+ }
53
+ function toISO(date) {
58
54
  if (!date) return "";
59
55
  try {
60
56
  return date.toString();
61
- } catch (error) {
62
- console.error("[useDateFormatter] toISO error:", error);
57
+ } catch {
63
58
  return "";
64
59
  }
65
- };
66
- const toDate = (date) => {
60
+ }
61
+ function toDate(date) {
67
62
  if (!date) return null;
68
63
  try {
69
64
  return new Date(Date.UTC(date.year, date.month - 1, date.day));
70
- } catch (error) {
71
- console.error("[useDateFormatter] toDate error:", error);
65
+ } catch {
72
66
  return null;
73
67
  }
74
- };
75
- const toTimestamp = (date) => {
68
+ }
69
+ function toTimestamp(date) {
76
70
  const jsDate = toDate(date);
77
71
  return jsDate ? jsDate.getTime() : null;
78
- };
79
- const toUnixTimestamp = (date) => {
72
+ }
73
+ function toUnixTimestamp(date) {
80
74
  const timestamp = toTimestamp(date);
81
75
  return timestamp ? Math.floor(timestamp / 1e3) : null;
82
- };
83
- const getToday = () => today(timeZone);
84
- const getNow = () => now(timeZone);
85
- const parse = (value) => {
76
+ }
77
+ function getToday() {
78
+ return today(timeZone);
79
+ }
80
+ function getNow() {
81
+ return now(timeZone);
82
+ }
83
+ function parse(value) {
86
84
  try {
87
85
  if (value.includes("T")) {
88
86
  return value.includes("[") ? parseZonedDateTime(value) : parseDateTime(value);
89
87
  }
90
88
  return parseDate(value);
91
- } catch (error) {
92
- console.error("[useDateFormatter] Parse error:", error);
89
+ } catch {
93
90
  return null;
94
91
  }
95
- };
96
- const getStartOfWeek = (date) => startOfWeek(date, locale);
97
- const getEndOfWeek = (date) => endOfWeek(date, locale);
98
- const getStartOfMonth = (date) => startOfMonth(date);
99
- const getEndOfMonth = (date) => endOfMonth(date);
100
- const getStartOfYear = (date) => startOfYear(date);
101
- const getEndOfYear = (date) => endOfYear(date);
102
- const getDayOfWeekNumber = (date) => getDayOfWeek(date, locale);
103
- const getWeeksInMonthNumber = (date) => getWeeksInMonth(date, locale);
104
- const checkIsWeekday = (date) => isWeekday(date, locale);
105
- const checkIsWeekend = (date) => isWeekend(date, locale);
106
- const checkIsSameDay = (a, b) => isSameDay(a, b);
107
- const checkIsSameMonth = (a, b) => isSameMonth(a, b);
108
- const checkIsSameYear = (a, b) => isSameYear(a, b);
109
- const checkIsToday = (date) => isToday(date, timeZone);
110
- const convertData = (data, converter) => {
92
+ }
93
+ function getStartOfWeek(date) {
94
+ return startOfWeek(date, locale);
95
+ }
96
+ function getEndOfWeek(date) {
97
+ return endOfWeek(date, locale);
98
+ }
99
+ function getStartOfMonth(date) {
100
+ return startOfMonth(date);
101
+ }
102
+ function getEndOfMonth(date) {
103
+ return endOfMonth(date);
104
+ }
105
+ function getStartOfYear(date) {
106
+ return startOfYear(date);
107
+ }
108
+ function getEndOfYear(date) {
109
+ return endOfYear(date);
110
+ }
111
+ function getDayOfWeekNumber(date) {
112
+ return getDayOfWeek(date, locale);
113
+ }
114
+ function getDayOfWeekName(date, style = "long") {
115
+ try {
116
+ const weekdayFormatter = new DateFormatter(locale, { weekday: style, timeZone });
117
+ return weekdayFormatter.format(date.toDate(timeZone));
118
+ } catch {
119
+ return "";
120
+ }
121
+ }
122
+ function getWeeksInMonthNumber(date) {
123
+ return getWeeksInMonth(date, locale);
124
+ }
125
+ function checkIsWeekday(date) {
126
+ return isWeekday(date, locale);
127
+ }
128
+ function checkIsWeekend(date) {
129
+ return isWeekend(date, locale);
130
+ }
131
+ function checkIsSameDay(a, b) {
132
+ return isSameDay(a, b);
133
+ }
134
+ function checkIsSameMonth(a, b) {
135
+ return isSameMonth(a, b);
136
+ }
137
+ function checkIsSameYear(a, b) {
138
+ return isSameYear(a, b);
139
+ }
140
+ function checkIsToday(date) {
141
+ return isToday(date, timeZone);
142
+ }
143
+ function convertData(data, converter) {
111
144
  if (!data) return data;
112
145
  if (isDateValue(data)) {
113
146
  return converter(data);
114
147
  }
115
148
  if (isDateRange(data)) {
149
+ const range = data;
116
150
  return {
117
- start: data.start ? converter(data.start) : data.start,
118
- end: data.end ? converter(data.end) : data.end
151
+ start: range.start ? converter(range.start) : range.start,
152
+ end: range.end ? converter(range.end) : range.end
119
153
  };
120
154
  }
121
155
  if (Array.isArray(data)) {
@@ -131,23 +165,26 @@ export function useDateFormatter(options = {}) {
131
165
  return result;
132
166
  }
133
167
  return data;
134
- };
135
- const convertToISO = (data) => convertData(data, toISO);
136
- const convertToFormatted = (data) => convertData(data, format);
137
- const convertToDate = (data) => convertData(data, toDate);
168
+ }
169
+ function convertToISO(data) {
170
+ return convertData(data, toISO);
171
+ }
172
+ function convertToFormatted(data) {
173
+ return convertData(data, format);
174
+ }
175
+ function convertToDate(data) {
176
+ return convertData(data, toDate);
177
+ }
138
178
  return {
139
- // 格式化
140
179
  format,
141
180
  formatRange,
142
181
  formatArray,
143
- // 转换
144
182
  toISO,
145
183
  toDate,
146
184
  toTimestamp,
147
185
  toUnixTimestamp,
148
186
  parse,
149
187
  convertData,
150
- // 工具方法 - 获取日期
151
188
  getToday,
152
189
  getNow,
153
190
  getStartOfWeek,
@@ -156,8 +193,8 @@ export function useDateFormatter(options = {}) {
156
193
  getEndOfMonth,
157
194
  getStartOfYear,
158
195
  getEndOfYear,
159
- // 工具方法 - 查询
160
196
  getDayOfWeek: getDayOfWeekNumber,
197
+ getDayOfWeekName,
161
198
  getWeeksInMonth: getWeeksInMonthNumber,
162
199
  isWeekday: checkIsWeekday,
163
200
  isWeekend: checkIsWeekend,
@@ -167,11 +204,9 @@ export function useDateFormatter(options = {}) {
167
204
  isToday: checkIsToday,
168
205
  isDateValue,
169
206
  isDateRange,
170
- // 批量转换
171
207
  convertToISO,
172
208
  convertToFormatted,
173
209
  convertToDate,
174
- // 导出配置供外部使用
175
210
  locale,
176
211
  timeZone
177
212
  };
@@ -0,0 +1,48 @@
1
+ import type { RequestToastOptions } from '../types/api.js';
2
+ /**
3
+ * 下载选项(带进度监控)
4
+ */
5
+ export interface DownloadWithProgressOptions {
6
+ /** 自定义文件名,不提供时从响应头提取 */
7
+ filename?: string;
8
+ /** 额外的请求头 */
9
+ headers?: Record<string, string>;
10
+ /** Toast 配置 */
11
+ toast?: RequestToastOptions | false;
12
+ /** 端点名称 */
13
+ endpoint?: string;
14
+ /** 下载成功回调 */
15
+ onSuccess?: (filename: string) => void;
16
+ /** 下载失败回调 */
17
+ onError?: (error: Error) => void;
18
+ }
19
+ /**
20
+ * 带进度监控的文件下载 composable
21
+ *
22
+ * 基于原生 fetch + ReadableStream 实现,支持实时进度和取消下载
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const { progress, downloading, download, abort } = useDownloadWithProgress()
27
+ *
28
+ * await download('/api/export', {
29
+ * filename: 'report.pdf',
30
+ * onSuccess: (name) => console.log('下载成功:', name)
31
+ * })
32
+ * ```
33
+ */
34
+ export declare function useDownloadWithProgress(): {
35
+ /** 下载进度 (0-100) */
36
+ progress: any;
37
+ /** 是否正在下载 */
38
+ downloading: any;
39
+ /** 错误信息 */
40
+ error: any;
41
+ /** 执行下载 */
42
+ download: (url: string, options?: DownloadWithProgressOptions) => Promise<{
43
+ success: boolean;
44
+ error: Error | null;
45
+ }>;
46
+ /** 中止下载 */
47
+ abort: () => void;
48
+ };
@@ -0,0 +1,85 @@
1
+ import { ref, useNuxtApp } from "#imports";
2
+ import { extractFilename, triggerDownload } from "@movk/core";
3
+ import { showToast, extractToastMessage, getAuthHeaders } from "../utils/api-utils.js";
4
+ export function useDownloadWithProgress() {
5
+ const { $api } = useNuxtApp();
6
+ const progress = ref(0);
7
+ const downloading = ref(false);
8
+ const error = ref(null);
9
+ let abortController = null;
10
+ const abort = () => {
11
+ abortController?.abort();
12
+ abortController = null;
13
+ downloading.value = false;
14
+ progress.value = 0;
15
+ };
16
+ const download = async (url, options = {}) => {
17
+ const { filename, headers = {}, toast, endpoint, onSuccess, onError } = options;
18
+ const apiInstance = endpoint ? $api.use(endpoint) : $api;
19
+ const config = apiInstance.getConfig();
20
+ const fullUrl = `${config.baseURL || ""}${url}`;
21
+ progress.value = 0;
22
+ downloading.value = true;
23
+ error.value = null;
24
+ abortController = new AbortController();
25
+ try {
26
+ const authHeaders = getAuthHeaders(config);
27
+ const response = await fetch(fullUrl, {
28
+ method: "GET",
29
+ headers: { ...headers, ...authHeaders },
30
+ signal: abortController.signal
31
+ });
32
+ if (!response.ok) {
33
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25: ${response.status} ${response.statusText}`);
34
+ }
35
+ const contentLength = response.headers.get("content-length");
36
+ const total = contentLength ? Number.parseInt(contentLength, 10) : 0;
37
+ const finalFilename = filename || extractFilename(response.headers, url.split("/").pop() || "download");
38
+ const reader = response.body?.getReader();
39
+ if (!reader) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u54CD\u5E94\u6D41");
40
+ const chunks = [];
41
+ let received = 0;
42
+ while (true) {
43
+ const { done, value } = await reader.read();
44
+ if (done) break;
45
+ chunks.push(value);
46
+ received += value.length;
47
+ if (total > 0) progress.value = Math.round(received / total * 100);
48
+ }
49
+ triggerDownload(new Blob(chunks), finalFilename);
50
+ downloading.value = false;
51
+ abortController = null;
52
+ progress.value = 100;
53
+ if (import.meta.client && toast !== false) {
54
+ const message = extractToastMessage(toast, "success", `\u4E0B\u8F7D\u6210\u529F: ${finalFilename}`);
55
+ showToast("success", message, toast, config.toast);
56
+ }
57
+ onSuccess?.(finalFilename);
58
+ return { success: true, error: null };
59
+ } catch (err) {
60
+ downloading.value = false;
61
+ abortController = null;
62
+ const downloadError = err instanceof Error ? err : new Error("\u4E0B\u8F7D\u5931\u8D25");
63
+ error.value = downloadError;
64
+ const isAborted = downloadError.name === "AbortError";
65
+ if (import.meta.client && !isAborted && toast !== false) {
66
+ const message = extractToastMessage(toast, "error", downloadError.message || "\u4E0B\u8F7D\u5931\u8D25");
67
+ showToast("error", message, toast, config.toast);
68
+ }
69
+ if (!isAborted) onError?.(downloadError);
70
+ return { success: false, error: downloadError };
71
+ }
72
+ };
73
+ return {
74
+ /** 下载进度 (0-100) */
75
+ progress,
76
+ /** 是否正在下载 */
77
+ downloading,
78
+ /** 错误信息 */
79
+ error,
80
+ /** 执行下载 */
81
+ download,
82
+ /** 中止下载 */
83
+ abort
84
+ };
85
+ }
@@ -0,0 +1,21 @@
1
+ export declare function useTheme(): {
2
+ neutralColors: readonly ["slate", "gray", "zinc", "neutral", "stone"];
3
+ neutral: import("vue").WritableComputedRef<any, any>;
4
+ primaryColors: string[];
5
+ primary: import("vue").WritableComputedRef<any, any>;
6
+ setBlackAsPrimary: (value: boolean) => void;
7
+ radiuses: number[];
8
+ radius: import("vue").WritableComputedRef<any, any>;
9
+ fonts: string[];
10
+ font: import("vue").WritableComputedRef<any, any>;
11
+ modes: {
12
+ label: string;
13
+ icon: any;
14
+ }[];
15
+ mode: import("vue").WritableComputedRef<any, any>;
16
+ hasCSSChanges: import("vue").ComputedRef<any>;
17
+ hasAppConfigChanges: import("vue").ComputedRef<boolean>;
18
+ exportCSS: () => string;
19
+ exportAppConfig: () => string;
20
+ resetTheme: () => void;
21
+ };
@@ -0,0 +1,143 @@
1
+ import { useAppConfig, useColorMode, useSiteConfig } from "#imports";
2
+ import { omit } from "@movk/core";
3
+ import colors from "tailwindcss/colors";
4
+ import { computed } from "vue";
5
+ export function useTheme() {
6
+ const appConfig = useAppConfig();
7
+ const colorMode = useColorMode();
8
+ const site = useSiteConfig();
9
+ const neutralColors = ["slate", "gray", "zinc", "neutral", "stone"];
10
+ const neutral = computed({
11
+ get() {
12
+ return appConfig.ui.colors.neutral;
13
+ },
14
+ set(option) {
15
+ appConfig.ui.colors.neutral = option;
16
+ window.localStorage.setItem(`${site.name}-ui-neutral`, appConfig.ui.colors.neutral);
17
+ }
18
+ });
19
+ const colorsToOmit = ["inherit", "current", "transparent", "black", "white", ...neutralColors];
20
+ const primaryColors = Object.keys(omit(colors, [...colorsToOmit]));
21
+ const primary = computed({
22
+ get() {
23
+ return appConfig.ui.colors.primary;
24
+ },
25
+ set(option) {
26
+ appConfig.ui.colors.primary = option;
27
+ window.localStorage.setItem(`${site.name}-ui-primary`, appConfig.ui.colors.primary);
28
+ setBlackAsPrimary(false);
29
+ }
30
+ });
31
+ const radiuses = [0, 0.125, 0.25, 0.375, 0.5];
32
+ const radius = computed({
33
+ get() {
34
+ return appConfig.theme.radius;
35
+ },
36
+ set(option) {
37
+ appConfig.theme.radius = option;
38
+ window.localStorage.setItem(`${site.name}-ui-radius`, String(appConfig.theme.radius));
39
+ }
40
+ });
41
+ const fonts = ["Public Sans", "DM Sans", "Geist", "Inter", "Poppins", "Outfit", "Raleway"];
42
+ const font = computed({
43
+ get() {
44
+ return appConfig.theme.font;
45
+ },
46
+ set(option) {
47
+ appConfig.theme.font = option;
48
+ if (appConfig.theme.font) {
49
+ window.localStorage.setItem(`${site.name}-ui-font`, appConfig.theme.font);
50
+ }
51
+ }
52
+ });
53
+ const modes = [
54
+ { label: "light", icon: appConfig.ui.icons.light },
55
+ { label: "dark", icon: appConfig.ui.icons.dark },
56
+ { label: "system", icon: appConfig.ui.icons.system }
57
+ ];
58
+ const mode = computed({
59
+ get() {
60
+ return colorMode.value;
61
+ },
62
+ set(option) {
63
+ colorMode.preference = option;
64
+ }
65
+ });
66
+ function setBlackAsPrimary(value) {
67
+ appConfig.theme.blackAsPrimary = value;
68
+ window.localStorage.setItem(`${site.name}-ui-black-as-primary`, String(value));
69
+ }
70
+ const hasCSSChanges = computed(() => {
71
+ return appConfig.theme.radius !== 0.25 || appConfig.theme.blackAsPrimary || appConfig.theme.font !== "Public Sans";
72
+ });
73
+ const hasAppConfigChanges = computed(() => {
74
+ return appConfig.ui.colors.primary !== "green" || appConfig.ui.colors.neutral !== "slate";
75
+ });
76
+ function exportCSS() {
77
+ const lines = [
78
+ '@import "tailwindcss";',
79
+ '@import "@nuxt/ui";'
80
+ ];
81
+ if (appConfig.theme.font !== "Public Sans") {
82
+ lines.push("", "@theme {", ` --font-sans: '${appConfig.theme.font}', sans-serif;`, "}");
83
+ }
84
+ const rootLines = [];
85
+ if (appConfig.theme.radius !== 0.25) {
86
+ rootLines.push(` --ui-radius: ${appConfig.theme.radius}rem;`);
87
+ }
88
+ if (appConfig.theme.blackAsPrimary) {
89
+ rootLines.push(" --ui-primary: black;");
90
+ }
91
+ if (rootLines.length) {
92
+ lines.push("", ":root {", ...rootLines, "}");
93
+ }
94
+ if (appConfig.theme.blackAsPrimary) {
95
+ lines.push("", ".dark {", " --ui-primary: white;", "}");
96
+ }
97
+ return lines.join("\n");
98
+ }
99
+ function exportAppConfig() {
100
+ const config = {};
101
+ if (appConfig.ui.colors.primary !== "green" || appConfig.ui.colors.neutral !== "slate") {
102
+ config.ui = { colors: {} };
103
+ if (appConfig.ui.colors.primary !== "green") {
104
+ config.ui.colors.primary = appConfig.ui.colors.primary;
105
+ }
106
+ if (appConfig.ui.colors.neutral !== "slate") {
107
+ config.ui.colors.neutral = appConfig.ui.colors.neutral;
108
+ }
109
+ }
110
+ const configString = JSON.stringify(config, null, 2).replace(/"([^"]+)":/g, "$1:").replace(/"/g, "'");
111
+ return `export default defineAppConfig(${configString})`;
112
+ }
113
+ function resetTheme() {
114
+ appConfig.ui.colors.primary = "green";
115
+ window.localStorage.removeItem(`${site.name}-ui-primary`);
116
+ appConfig.ui.colors.neutral = "slate";
117
+ window.localStorage.removeItem(`${site.name}-ui-neutral`);
118
+ appConfig.theme.radius = 0.25;
119
+ window.localStorage.removeItem(`${site.name}-ui-radius`);
120
+ appConfig.theme.font = "Public Sans";
121
+ window.localStorage.removeItem(`${site.name}-ui-font`);
122
+ appConfig.theme.blackAsPrimary = false;
123
+ window.localStorage.removeItem(`${site.name}-ui-black-as-primary`);
124
+ }
125
+ return {
126
+ neutralColors,
127
+ neutral,
128
+ primaryColors,
129
+ primary,
130
+ setBlackAsPrimary,
131
+ radiuses,
132
+ radius,
133
+ fonts,
134
+ font,
135
+ modes,
136
+ mode,
137
+ hasCSSChanges,
138
+ hasAppConfigChanges,
139
+ exportCSS,
140
+ exportAppConfig,
141
+ resetTheme
142
+ };
143
+ }