@movk/nuxt 0.1.1 → 1.0.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 +11 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +29 -3
- package/dist/runtime/components/AutoForm.d.vue.ts +12 -6
- package/dist/runtime/components/AutoForm.vue +3 -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/StarRating.d.vue.ts +7 -7
- 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.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/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 +43 -0
- package/dist/runtime/composables/useAutoForm.d.ts +874 -54
- 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/useUploadWithProgress.d.ts +52 -0
- package/dist/runtime/composables/useUploadWithProgress.js +117 -0
- package/dist/runtime/plugins/api.factory.d.ts +2 -0
- package/dist/runtime/plugins/api.factory.js +188 -0
- package/dist/runtime/schemas/api.d.ts +354 -0
- package/dist/runtime/schemas/api.js +212 -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/types/api.d.ts +218 -0
- package/dist/runtime/types/api.js +8 -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 +64 -0
- package/dist/runtime/utils/api-utils.js +127 -0
- package/package.json +32 -25
|
@@ -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,52 @@
|
|
|
1
|
+
import type { ApiResponse, RequestToastOptions } from '../types/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* 上传选项(带进度监控)
|
|
4
|
+
*/
|
|
5
|
+
export interface UploadWithProgressOptions {
|
|
6
|
+
/** 文件字段名 @defaultValue 'file' */
|
|
7
|
+
fieldName?: string;
|
|
8
|
+
/** 额外的表单字段 */
|
|
9
|
+
fields?: Record<string, string | Blob>;
|
|
10
|
+
/** 额外的请求头 */
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
/** Toast 配置 */
|
|
13
|
+
toast?: RequestToastOptions | false;
|
|
14
|
+
/** 端点名称 */
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
/** 上传成功回调 */
|
|
17
|
+
onSuccess?: (response: ApiResponse) => void;
|
|
18
|
+
/** 上传失败回调 */
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 带进度监控的文件上传 composable
|
|
23
|
+
*
|
|
24
|
+
* 基于原生 XMLHttpRequest 实现,支持实时进度和取消上传
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const { progress, uploading, upload, abort } = useUploadWithProgress()
|
|
29
|
+
*
|
|
30
|
+
* const { data, error } = await upload('/api/upload', files, {
|
|
31
|
+
* fieldName: 'files',
|
|
32
|
+
* onSuccess: (res) => console.log('上传成功', res)
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function useUploadWithProgress<T = unknown>(): {
|
|
37
|
+
/** 上传进度 (0-100) */
|
|
38
|
+
progress: any;
|
|
39
|
+
/** 是否正在上传 */
|
|
40
|
+
uploading: any;
|
|
41
|
+
/** 上传结果 */
|
|
42
|
+
data: any;
|
|
43
|
+
/** 错误信息 */
|
|
44
|
+
error: any;
|
|
45
|
+
/** 执行上传 */
|
|
46
|
+
upload: (url: string, files: File | File[], options?: UploadWithProgressOptions) => Promise<{
|
|
47
|
+
data: ApiResponse<T> | null;
|
|
48
|
+
error: Error | null;
|
|
49
|
+
}>;
|
|
50
|
+
/** 中止上传 */
|
|
51
|
+
abort: () => void;
|
|
52
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ref, useNuxtApp } from "#imports";
|
|
2
|
+
import {
|
|
3
|
+
showToast,
|
|
4
|
+
isBusinessSuccess,
|
|
5
|
+
extractMessage,
|
|
6
|
+
extractToastMessage,
|
|
7
|
+
getAuthHeaders
|
|
8
|
+
} from "../utils/api-utils.js";
|
|
9
|
+
export function useUploadWithProgress() {
|
|
10
|
+
const { $api } = useNuxtApp();
|
|
11
|
+
const progress = ref(0);
|
|
12
|
+
const uploading = ref(false);
|
|
13
|
+
const data = ref(null);
|
|
14
|
+
const error = ref(null);
|
|
15
|
+
let currentXhr = null;
|
|
16
|
+
const abort = () => {
|
|
17
|
+
currentXhr?.abort();
|
|
18
|
+
currentXhr = null;
|
|
19
|
+
uploading.value = false;
|
|
20
|
+
progress.value = 0;
|
|
21
|
+
};
|
|
22
|
+
const upload = async (url, files, options = {}) => {
|
|
23
|
+
const { fieldName = "file", fields = {}, headers = {}, toast, endpoint, onSuccess, onError } = options;
|
|
24
|
+
const apiInstance = endpoint ? $api.use(endpoint) : $api;
|
|
25
|
+
const config = apiInstance.getConfig();
|
|
26
|
+
const fullUrl = `${config.baseURL || ""}${url}`;
|
|
27
|
+
const formData = new FormData();
|
|
28
|
+
const fileArray = Array.isArray(files) ? files : [files];
|
|
29
|
+
fileArray.forEach((file) => formData.append(fieldName, file));
|
|
30
|
+
Object.entries(fields).forEach(([key, value]) => formData.append(key, value));
|
|
31
|
+
progress.value = 0;
|
|
32
|
+
uploading.value = true;
|
|
33
|
+
data.value = null;
|
|
34
|
+
error.value = null;
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const xhr = new XMLHttpRequest();
|
|
37
|
+
currentXhr = xhr;
|
|
38
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
39
|
+
if (e.lengthComputable) {
|
|
40
|
+
progress.value = Math.round(e.loaded / e.total * 100);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
xhr.addEventListener("load", () => {
|
|
44
|
+
uploading.value = false;
|
|
45
|
+
currentXhr = null;
|
|
46
|
+
try {
|
|
47
|
+
const response = JSON.parse(xhr.responseText);
|
|
48
|
+
const isSuccess = isBusinessSuccess(response, config.success);
|
|
49
|
+
const message = extractMessage(response, config.success);
|
|
50
|
+
if (isSuccess) {
|
|
51
|
+
data.value = response;
|
|
52
|
+
if (import.meta.client && toast !== false) {
|
|
53
|
+
const msg = extractToastMessage(toast, "success", message || "\u4E0A\u4F20\u6210\u529F");
|
|
54
|
+
showToast("success", msg, toast, config.toast);
|
|
55
|
+
}
|
|
56
|
+
onSuccess?.(response);
|
|
57
|
+
resolve({ data: response, error: null });
|
|
58
|
+
} else {
|
|
59
|
+
const err = new Error(message || "\u4E0A\u4F20\u5931\u8D25");
|
|
60
|
+
error.value = err;
|
|
61
|
+
if (import.meta.client && toast !== false) {
|
|
62
|
+
const msg = extractToastMessage(toast, "error", message || "\u4E0A\u4F20\u5931\u8D25");
|
|
63
|
+
showToast("error", msg, toast, config.toast);
|
|
64
|
+
}
|
|
65
|
+
onError?.(err);
|
|
66
|
+
resolve({ data: null, error: err });
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const parseError = err instanceof Error ? err : new Error("\u54CD\u5E94\u89E3\u6790\u5931\u8D25");
|
|
70
|
+
error.value = parseError;
|
|
71
|
+
if (import.meta.client && toast !== false) {
|
|
72
|
+
showToast("error", "\u4E0A\u4F20\u5931\u8D25", toast, config.toast);
|
|
73
|
+
}
|
|
74
|
+
onError?.(parseError);
|
|
75
|
+
resolve({ data: null, error: parseError });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
xhr.addEventListener("error", () => {
|
|
79
|
+
uploading.value = false;
|
|
80
|
+
currentXhr = null;
|
|
81
|
+
const err = new Error("\u7F51\u7EDC\u9519\u8BEF");
|
|
82
|
+
error.value = err;
|
|
83
|
+
if (import.meta.client && toast !== false) {
|
|
84
|
+
showToast("error", "\u4E0A\u4F20\u5931\u8D25", toast, config.toast);
|
|
85
|
+
}
|
|
86
|
+
onError?.(err);
|
|
87
|
+
resolve({ data: null, error: err });
|
|
88
|
+
});
|
|
89
|
+
xhr.addEventListener("abort", () => {
|
|
90
|
+
uploading.value = false;
|
|
91
|
+
currentXhr = null;
|
|
92
|
+
error.value = new Error("\u4E0A\u4F20\u5DF2\u53D6\u6D88");
|
|
93
|
+
resolve({ data: null, error: error.value });
|
|
94
|
+
});
|
|
95
|
+
xhr.open("POST", fullUrl);
|
|
96
|
+
const authHeaders = getAuthHeaders(config);
|
|
97
|
+
Object.entries({ ...headers, ...authHeaders }).forEach(([key, value]) => {
|
|
98
|
+
xhr.setRequestHeader(key, value);
|
|
99
|
+
});
|
|
100
|
+
xhr.send(formData);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
/** 上传进度 (0-100) */
|
|
105
|
+
progress,
|
|
106
|
+
/** 是否正在上传 */
|
|
107
|
+
uploading,
|
|
108
|
+
/** 上传结果 */
|
|
109
|
+
data,
|
|
110
|
+
/** 错误信息 */
|
|
111
|
+
error,
|
|
112
|
+
/** 执行上传 */
|
|
113
|
+
upload,
|
|
114
|
+
/** 中止上传 */
|
|
115
|
+
abort
|
|
116
|
+
};
|
|
117
|
+
}
|