@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.
- package/README.md +84 -9
- package/dist/module.d.mts +19 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +77 -8
- package/dist/runtime/components/AutoForm.d.vue.ts +12 -6
- package/dist/runtime/components/AutoForm.vue +4 -1
- package/dist/runtime/components/AutoForm.vue.d.ts +12 -6
- package/dist/runtime/components/ColorChooser.d.vue.ts +11 -5
- package/dist/runtime/components/ColorChooser.vue.d.ts +11 -5
- package/dist/runtime/components/DatePicker.d.vue.ts +14 -5
- package/dist/runtime/components/DatePicker.vue.d.ts +14 -5
- package/dist/runtime/components/SlideVerify.d.vue.ts +107 -0
- package/dist/runtime/components/SlideVerify.vue +147 -0
- package/dist/runtime/components/SlideVerify.vue.d.ts +107 -0
- package/dist/runtime/components/StarRating.d.vue.ts +7 -7
- package/dist/runtime/components/StarRating.vue +1 -0
- package/dist/runtime/components/StarRating.vue.d.ts +7 -7
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererArray.d.vue.ts +6 -4
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererArray.vue +1 -1
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererArray.vue.d.ts +6 -4
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererField.d.vue.ts +6 -4
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererField.vue.d.ts +6 -4
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererLayout.d.vue.ts +6 -4
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererLayout.vue.d.ts +6 -4
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererNested.d.vue.ts +6 -4
- package/dist/runtime/components/auto-form-renderer/AutoFormRendererNested.vue.d.ts +6 -4
- package/dist/runtime/components/input/WithCharacterLimit.d.vue.ts +11 -5
- package/dist/runtime/components/input/WithCharacterLimit.vue.d.ts +11 -5
- package/dist/runtime/components/input/WithClear.d.vue.ts +12 -5
- package/dist/runtime/components/input/WithClear.vue.d.ts +12 -5
- package/dist/runtime/components/input/WithCopy.d.vue.ts +12 -5
- package/dist/runtime/components/input/WithCopy.vue.d.ts +12 -5
- package/dist/runtime/components/input/WithPasswordToggle.d.vue.ts +11 -5
- package/dist/runtime/components/input/WithPasswordToggle.vue.d.ts +11 -5
- package/dist/runtime/components/theme-picker/ThemePicker.d.vue.ts +3 -0
- package/dist/runtime/components/theme-picker/ThemePicker.vue +235 -0
- package/dist/runtime/components/theme-picker/ThemePicker.vue.d.ts +3 -0
- package/dist/runtime/components/theme-picker/ThemePickerButton.d.vue.ts +18 -0
- package/dist/runtime/components/theme-picker/ThemePickerButton.vue +34 -0
- package/dist/runtime/components/theme-picker/ThemePickerButton.vue.d.ts +18 -0
- package/dist/runtime/composables/useApiAuth.d.ts +47 -0
- package/dist/runtime/composables/useApiAuth.js +66 -0
- package/dist/runtime/composables/useApiFetch.d.ts +42 -0
- package/dist/runtime/composables/useApiFetch.js +41 -0
- package/dist/runtime/composables/useAutoForm.d.ts +81 -605
- package/dist/runtime/composables/useAutoForm.js +3 -1
- package/dist/runtime/composables/useClientApiFetch.d.ts +24 -0
- package/dist/runtime/composables/useClientApiFetch.js +8 -0
- package/dist/runtime/composables/useDateFormatter.d.ts +21 -7
- package/dist/runtime/composables/useDateFormatter.js +92 -57
- package/dist/runtime/composables/useDownloadWithProgress.d.ts +48 -0
- package/dist/runtime/composables/useDownloadWithProgress.js +85 -0
- package/dist/runtime/composables/useTheme.d.ts +21 -0
- package/dist/runtime/composables/useTheme.js +143 -0
- package/dist/runtime/composables/useUploadWithProgress.d.ts +52 -0
- package/dist/runtime/composables/useUploadWithProgress.js +117 -0
- package/dist/runtime/internal/useAutoFormProvider.js +2 -2
- package/dist/runtime/plugins/api.factory.d.ts +2 -0
- package/dist/runtime/plugins/api.factory.js +186 -0
- package/dist/runtime/plugins/theme.d.ts +2 -0
- package/dist/runtime/plugins/theme.js +89 -0
- package/dist/runtime/schemas/api.d.ts +590 -0
- package/dist/runtime/schemas/api.js +228 -0
- package/dist/runtime/server/api/_movk/session.post.d.ts +10 -0
- package/dist/runtime/server/api/_movk/session.post.js +18 -0
- package/dist/runtime/style.css +1 -0
- package/dist/runtime/types/api.d.ts +218 -0
- package/dist/runtime/types/api.js +0 -0
- package/dist/runtime/types/auth.d.ts +34 -0
- package/dist/runtime/types/auto-form-renderer.d.ts +14 -22
- package/dist/runtime/types/auto-form-renderer.js +0 -0
- package/dist/runtime/types/components.d.ts +29 -41
- package/dist/runtime/types/components.js +0 -0
- package/dist/runtime/types/index.d.ts +1 -0
- package/dist/runtime/types/index.js +3 -2
- package/dist/runtime/utils/api-utils.d.ts +79 -0
- package/dist/runtime/utils/api-utils.js +127 -0
- 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>;
|
|
@@ -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
|
-
* @
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
45
|
-
console.error("[useDateFormatter] Format error:", error);
|
|
41
|
+
} catch {
|
|
46
42
|
return "";
|
|
47
43
|
}
|
|
48
|
-
}
|
|
49
|
-
|
|
44
|
+
}
|
|
45
|
+
function formatRange(start, end, separator = " - ") {
|
|
50
46
|
if (!start || !end) return "";
|
|
51
47
|
return `${format(start)}${separator}${format(end)}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
48
|
+
}
|
|
49
|
+
function formatArray(dates, separator = ", ") {
|
|
54
50
|
if (!dates?.length) return "";
|
|
55
51
|
return dates.map(format).join(separator);
|
|
56
|
-
}
|
|
57
|
-
|
|
52
|
+
}
|
|
53
|
+
function toISO(date) {
|
|
58
54
|
if (!date) return "";
|
|
59
55
|
try {
|
|
60
56
|
return date.toString();
|
|
61
|
-
} catch
|
|
62
|
-
console.error("[useDateFormatter] toISO error:", error);
|
|
57
|
+
} catch {
|
|
63
58
|
return "";
|
|
64
59
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
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
|
|
71
|
-
console.error("[useDateFormatter] toDate error:", error);
|
|
65
|
+
} catch {
|
|
72
66
|
return null;
|
|
73
67
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
68
|
+
}
|
|
69
|
+
function toTimestamp(date) {
|
|
76
70
|
const jsDate = toDate(date);
|
|
77
71
|
return jsDate ? jsDate.getTime() : null;
|
|
78
|
-
}
|
|
79
|
-
|
|
72
|
+
}
|
|
73
|
+
function toUnixTimestamp(date) {
|
|
80
74
|
const timestamp = toTimestamp(date);
|
|
81
75
|
return timestamp ? Math.floor(timestamp / 1e3) : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
92
|
-
console.error("[useDateFormatter] Parse error:", error);
|
|
89
|
+
} catch {
|
|
93
90
|
return null;
|
|
94
91
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
118
|
-
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|