@svadmin/core 0.29.0 → 0.31.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/package.json +3 -3
- package/src/auth-hooks.svelte.ts +18 -1
- package/src/data-transfer.svelte.ts +46 -38
- package/src/export-format.ts +115 -0
- package/src/i18n.svelte.ts +258 -276
- package/src/index.ts +6 -4
- package/src/permissions.svelte.ts +4 -3
- package/src/query-hooks.svelte.ts +19 -2
- package/src/routing-hooks.svelte.ts +9 -0
- package/src/types.ts +3 -0
- package/src/useCan.ts +3 -2
- package/src/useParsed.svelte.ts +23 -4
- package/src/utility-hooks.svelte.ts +111 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"author": "zuohuadong",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"default": "./src/i18n.svelte.ts"
|
|
39
39
|
},
|
|
40
40
|
"./permissions": {
|
|
41
|
-
"types": "./src/permissions.ts",
|
|
42
|
-
"default": "./src/permissions.ts"
|
|
41
|
+
"types": "./src/permissions.svelte.ts",
|
|
42
|
+
"default": "./src/permissions.svelte.ts"
|
|
43
43
|
},
|
|
44
44
|
"./toast": {
|
|
45
45
|
"types": "./src/toast.svelte.ts",
|
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -114,6 +114,24 @@ export function useUpdatePassword() {
|
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// ─── useUpdateIdentity / useUpdateProfile ────────────────────
|
|
118
|
+
|
|
119
|
+
export function useUpdateIdentity() {
|
|
120
|
+
return createAuthMutation({
|
|
121
|
+
method: 'updateIdentity',
|
|
122
|
+
successMessage: t('common.operationSuccess'),
|
|
123
|
+
onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function useUpdateProfile() {
|
|
128
|
+
return createAuthMutation({
|
|
129
|
+
method: 'updateProfile',
|
|
130
|
+
successMessage: t('common.operationSuccess'),
|
|
131
|
+
onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
117
135
|
// ─── useGetIdentity ──────────────────────────────────────────
|
|
118
136
|
|
|
119
137
|
export function useGetIdentity() {
|
|
@@ -297,4 +315,3 @@ export function usePermissions<T = unknown>() {
|
|
|
297
315
|
can: (resource: string, action: string): boolean => hasFn(`${resource}:${action}`),
|
|
298
316
|
};
|
|
299
317
|
}
|
|
300
|
-
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { getDataProviderForResource } from './context.svelte';
|
|
2
2
|
import { useParsed } from './useParsed.svelte';
|
|
3
3
|
import type { Sort, Filter, BaseRecord } from './types';
|
|
4
|
+
import { downloadData } from './export-format';
|
|
5
|
+
import type { ExportFormat } from './export-format';
|
|
6
|
+
export { downloadData, toCsv, toJson, toXlsx, escapeCsvField } from './export-format';
|
|
7
|
+
export type { ExportFormat } from './export-format';
|
|
4
8
|
|
|
5
9
|
export interface UseExportOptions<TData extends BaseRecord = BaseRecord> {
|
|
6
10
|
resource?: string;
|
|
@@ -10,13 +14,15 @@ export interface UseExportOptions<TData extends BaseRecord = BaseRecord> {
|
|
|
10
14
|
maxItemCount?: number;
|
|
11
15
|
pageSize?: number;
|
|
12
16
|
download?: boolean;
|
|
17
|
+
/** 导出格式,默认 csv(兼容旧版) */
|
|
18
|
+
format?: ExportFormat;
|
|
13
19
|
meta?: Record<string, unknown>;
|
|
14
20
|
dataProviderName?: string;
|
|
15
21
|
onError?: (error: Error) => void;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
|
-
* useExport — export records from a resource as CSV download
|
|
25
|
+
* useExport — export records from a resource as CSV/JSON/XLSX download
|
|
20
26
|
* Supports mapData, sorters, filters, maxItemCount, pageSize
|
|
21
27
|
*/
|
|
22
28
|
export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExportOptions<TData> = {}) {
|
|
@@ -57,29 +63,7 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
|
|
|
57
63
|
: allRecords.map(r => r as unknown as Record<string, unknown>);
|
|
58
64
|
|
|
59
65
|
if (options.download !== false && typeof document !== 'undefined') {
|
|
60
|
-
|
|
61
|
-
const escapeCsvField = (s: string) => {
|
|
62
|
-
let val = s;
|
|
63
|
-
if (/^[=+@\t\r]/.test(val)) val = "'" + val;
|
|
64
|
-
return val.includes(',') || val.includes('"') || val.includes('\n') || val.includes('\r') ? `"${val.replace(/"/g, '""')}"` : val;
|
|
65
|
-
};
|
|
66
|
-
const header = fields.map(escapeCsvField).join(',');
|
|
67
|
-
const rows = mapped.map(record =>
|
|
68
|
-
fields.map(f => {
|
|
69
|
-
const val = record[f];
|
|
70
|
-
const str = val === null || val === undefined ? '' : typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
71
|
-
return escapeCsvField(str);
|
|
72
|
-
}).join(',')
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const csv = [header, ...rows].join('\n');
|
|
76
|
-
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
|
77
|
-
const url = URL.createObjectURL(blob);
|
|
78
|
-
const a = document.createElement('a');
|
|
79
|
-
a.href = url;
|
|
80
|
-
a.download = `${exportResource}_export.csv`;
|
|
81
|
-
a.click();
|
|
82
|
-
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
66
|
+
downloadData(mapped, exportResource, options.format ?? 'csv');
|
|
83
67
|
}
|
|
84
68
|
|
|
85
69
|
return allRecords;
|
|
@@ -99,6 +83,8 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
|
|
|
99
83
|
|
|
100
84
|
export interface UseImportOptions<TData = Record<string, unknown>> {
|
|
101
85
|
resource?: string;
|
|
86
|
+
/** 指定导入文件格式;默认 auto = 根据文件扩展名推断。注意:import 仅支持 csv 和 json,不支持 xlsx 解析 */
|
|
87
|
+
format?: 'auto' | 'csv' | 'json';
|
|
102
88
|
mapData?: (item: Record<string, unknown>) => TData;
|
|
103
89
|
batchSize?: number;
|
|
104
90
|
onFinish?: (result: { succeeded: unknown[]; errored: { request: unknown; error: unknown }[] }) => void;
|
|
@@ -131,21 +117,40 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
131
117
|
text = text.slice(1);
|
|
132
118
|
}
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
options.onFinish?.({ succeeded: [], errored: [] });
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
120
|
+
// 自动检测或使用指定格式
|
|
121
|
+
const format = options.format ?? 'auto';
|
|
122
|
+
const isJson = format === 'json' || (format === 'auto' && info.file.name.toLowerCase().endsWith('.json'));
|
|
140
123
|
|
|
141
|
-
|
|
142
|
-
const records: Record<string, unknown>[] = [];
|
|
124
|
+
let records: Record<string, unknown>[];
|
|
143
125
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
126
|
+
if (isJson) {
|
|
127
|
+
// JSON 格式:期望数组对象
|
|
128
|
+
const parsed = JSON.parse(text);
|
|
129
|
+
if (!Array.isArray(parsed)) {
|
|
130
|
+
mutationResult = { succeeded: [], errored: [{ request: null, error: new Error('JSON import requires an array of objects') }] };
|
|
131
|
+
options.onFinish?.(mutationResult);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
records = parsed.map((item: unknown) =>
|
|
135
|
+
options.mapData ? (options.mapData(item as Record<string, unknown>) as unknown as Record<string, unknown>) : (item as Record<string, unknown>)
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
// CSV 格式
|
|
139
|
+
const rows = parseCSV(text);
|
|
140
|
+
if (rows.length < 2) {
|
|
141
|
+
mutationResult = { succeeded: [], errored: [] };
|
|
142
|
+
options.onFinish?.({ succeeded: [], errored: [] });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const headers = rows[0];
|
|
147
|
+
records = [];
|
|
148
|
+
for (let i = 1; i < rows.length; i++) {
|
|
149
|
+
const values = rows[i];
|
|
150
|
+
const record: Record<string, unknown> = {};
|
|
151
|
+
headers.forEach((h, idx) => { record[h] = values[idx] ?? ''; });
|
|
152
|
+
records.push(options.mapData ? (options.mapData(record) as unknown as Record<string, unknown>) : record);
|
|
153
|
+
}
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
const batchSize = options.batchSize ?? 1;
|
|
@@ -198,13 +203,16 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
return {
|
|
201
|
-
inputProps: { type: 'file' as const, accept: '.csv' },
|
|
206
|
+
inputProps: { type: 'file' as const, accept: '.csv,.json' },
|
|
202
207
|
handleChange,
|
|
203
208
|
get isLoading() { return isLoading; },
|
|
204
209
|
get mutationResult() { return mutationResult; },
|
|
205
210
|
};
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
|
|
214
|
+
// ─── 解析工具 ────────────────────────────────────────────────────
|
|
215
|
+
|
|
208
216
|
/** Parse full CSV text respecting quoted multi-line fields */
|
|
209
217
|
function parseCSV(text: string): string[][] {
|
|
210
218
|
const rows: string[][] = [];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// 纯函数导出格式化工具 — 不依赖 Svelte runes 或浏览器 API(downloadData 除外)
|
|
2
|
+
// 可被 bun:test 直接测试
|
|
3
|
+
|
|
4
|
+
/** 支持的导出/导入格式 */
|
|
5
|
+
export type ExportFormat = 'csv' | 'json' | 'xlsx';
|
|
6
|
+
|
|
7
|
+
/** CSV 转义,含公式注入防护 */
|
|
8
|
+
export function escapeCsvField(s: string): string {
|
|
9
|
+
let val = s;
|
|
10
|
+
if (/^[=+@\t\r]/.test(val)) val = "'" + val;
|
|
11
|
+
return val.includes(',') || val.includes('"') || val.includes('\n') || val.includes('\r')
|
|
12
|
+
? `"${val.replace(/"/g, '""')}"`
|
|
13
|
+
: val;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 将记录数组转为 CSV 字符串 */
|
|
17
|
+
export function toCsv(records: Record<string, unknown>[]): string {
|
|
18
|
+
if (records.length === 0) return '';
|
|
19
|
+
const fields = Object.keys(records[0]);
|
|
20
|
+
const header = fields.map(escapeCsvField).join(',');
|
|
21
|
+
const rows = records.map(record =>
|
|
22
|
+
fields.map(f => {
|
|
23
|
+
const val = record[f];
|
|
24
|
+
const str = val === null || val === undefined ? '' : typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
25
|
+
return escapeCsvField(str);
|
|
26
|
+
}).join(',')
|
|
27
|
+
);
|
|
28
|
+
return [header, ...rows].join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 将记录数组转为格式化的 JSON 字符串 */
|
|
32
|
+
export function toJson(records: Record<string, unknown>[]): string {
|
|
33
|
+
return JSON.stringify(records, null, 2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 转义 XML 特殊字符 */
|
|
37
|
+
export function escapeXml(s: string): string {
|
|
38
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 生成最小可用的 XLSX(SpreadsheetML)——零依赖。
|
|
43
|
+
*/
|
|
44
|
+
export function toXlsx(records: Record<string, unknown>[]): string {
|
|
45
|
+
if (records.length === 0) return '';
|
|
46
|
+
const fields = Object.keys(records[0]);
|
|
47
|
+
|
|
48
|
+
const headerCells = fields.map(f => `<Cell><Data ss:Type="String">${escapeXml(f)}</Data></Cell>`).join('');
|
|
49
|
+
const dataRows = records.map(record =>
|
|
50
|
+
`<Row>${fields.map(f => {
|
|
51
|
+
const val = record[f];
|
|
52
|
+
if (val === null || val === undefined) return '<Cell><Data ss:Type="String"></Data></Cell>';
|
|
53
|
+
if (typeof val === 'number') return `<Cell><Data ss:Type="Number">${val}</Data></Cell>`;
|
|
54
|
+
if (typeof val === 'boolean') return `<Cell><Data ss:Type="Boolean">${val ? 1 : 0}</Data></Cell>`;
|
|
55
|
+
const str = typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
56
|
+
return `<Cell><Data ss:Type="String">${escapeXml(str)}</Data></Cell>`;
|
|
57
|
+
}).join('')}</Row>`
|
|
58
|
+
).join('\n');
|
|
59
|
+
|
|
60
|
+
return `<?xml version="1.0"?>
|
|
61
|
+
<?mso-application progid="Excel.Sheet"?>
|
|
62
|
+
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
|
63
|
+
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
|
64
|
+
<Worksheet ss:Name="Export">
|
|
65
|
+
<Table>
|
|
66
|
+
<Row>${headerCells}</Row>
|
|
67
|
+
${dataRows}
|
|
68
|
+
</Table>
|
|
69
|
+
</Worksheet>
|
|
70
|
+
</Workbook>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 将记录以指定格式下载为文件。
|
|
75
|
+
* 依赖浏览器 document API;在 Node/bun:test 中需 mock。
|
|
76
|
+
*/
|
|
77
|
+
export function downloadData(
|
|
78
|
+
records: Record<string, unknown>[],
|
|
79
|
+
resource: string,
|
|
80
|
+
format: ExportFormat = 'csv',
|
|
81
|
+
): void {
|
|
82
|
+
if (records.length === 0) return;
|
|
83
|
+
if (typeof document === 'undefined') return;
|
|
84
|
+
|
|
85
|
+
let content: string;
|
|
86
|
+
let mimeType: string;
|
|
87
|
+
let ext: string;
|
|
88
|
+
|
|
89
|
+
switch (format) {
|
|
90
|
+
case 'json':
|
|
91
|
+
content = toJson(records);
|
|
92
|
+
mimeType = 'application/json;charset=utf-8;';
|
|
93
|
+
ext = 'json';
|
|
94
|
+
break;
|
|
95
|
+
case 'xlsx':
|
|
96
|
+
content = toXlsx(records);
|
|
97
|
+
mimeType = 'application/vnd.ms-excel;charset=utf-8;';
|
|
98
|
+
ext = 'xls';
|
|
99
|
+
break;
|
|
100
|
+
case 'csv':
|
|
101
|
+
default:
|
|
102
|
+
content = toCsv(records);
|
|
103
|
+
mimeType = 'text/csv;charset=utf-8;';
|
|
104
|
+
ext = 'csv';
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const blob = new Blob(['\uFEFF' + content], { type: mimeType });
|
|
109
|
+
const url = URL.createObjectURL(blob);
|
|
110
|
+
const a = document.createElement('a');
|
|
111
|
+
a.href = url;
|
|
112
|
+
a.download = `${resource}_export.${ext}`;
|
|
113
|
+
a.click();
|
|
114
|
+
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
115
|
+
}
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -122,6 +122,7 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
122
122
|
'auth.hasAccount': '已有账号?',
|
|
123
123
|
'auth.forgotPasswordLink': '忘记密码?',
|
|
124
124
|
'auth.forgotPasswordDescription': '输入您的邮箱地址,我们将发送重置密码的链接。',
|
|
125
|
+
'auth.resetPasswordDescription': '设置一个新的登录密码。',
|
|
125
126
|
'auth.resetLinkSent': '重置链接已发送,请查看您的邮箱。',
|
|
126
127
|
'auth.passwordMismatch': '两次输入的密码不一致',
|
|
127
128
|
'auth.emailRequired': '请输入邮箱',
|
|
@@ -144,19 +145,6 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
144
145
|
'devtools.title': '开发者工具 (Ctrl+Shift+D)',
|
|
145
146
|
// Validation
|
|
146
147
|
// Chat
|
|
147
|
-
'chat.title': 'AI 助手',
|
|
148
|
-
'chat.typing': '输入中...',
|
|
149
|
-
'chat.clear': '清空',
|
|
150
|
-
'chat.welcome': '我可以帮你做什么?',
|
|
151
|
-
'chat.welcomeDesc': '可以询问当前后台、资源和库存运营数据。',
|
|
152
|
-
'chat.thinking': '思考中...',
|
|
153
|
-
'chat.placeholder': '输入消息...',
|
|
154
|
-
'chat.shortcutHint': '切换',
|
|
155
|
-
'chat.error': '抱歉,处理时出现问题,请重试。',
|
|
156
|
-
'chat.approvalRequired': '需要确认',
|
|
157
|
-
'chat.suggestion1': '如何新建一条资源记录?',
|
|
158
|
-
'chat.suggestion2': '解释当前数据模型',
|
|
159
|
-
'chat.suggestion3': '帮我写一个筛选条件',
|
|
160
148
|
'chat.contextInfo': '当前上下文:{resource}',
|
|
161
149
|
'chat.actionButton': '执行',
|
|
162
150
|
'chat.historyRestored': '已恢复聊天记录',
|
|
@@ -172,14 +160,11 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
172
160
|
// Settings
|
|
173
161
|
'settings.title': '设置',
|
|
174
162
|
'settings.profile': '个人资料',
|
|
175
|
-
'settings.accountPreferences': '账户偏好',
|
|
176
163
|
'settings.appearance': '外观设置',
|
|
177
164
|
'settings.about': '关于',
|
|
178
165
|
'settings.general': '通用',
|
|
179
166
|
'settings.security': '安全设置',
|
|
180
|
-
'settings.accessSecurity': '安全与权限',
|
|
181
167
|
'settings.integrations': '第三方集成',
|
|
182
|
-
'settings.integrationApi': '集成与 API',
|
|
183
168
|
'settings.notifications': '通知设置',
|
|
184
169
|
'settings.api': 'API 设置',
|
|
185
170
|
'settings.developer': '开发者',
|
|
@@ -325,6 +310,114 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
325
310
|
'profile.currentPassword': '当前密码',
|
|
326
311
|
'profile.passwordChanged': '密码已修改',
|
|
327
312
|
'profile.updatePassword': '更新密码',
|
|
313
|
+
'profile.newPassword': '新密码',
|
|
314
|
+
'profile.company': '企业',
|
|
315
|
+
'profile.gamer': '玩家',
|
|
316
|
+
'profile.companyName': '公司名称',
|
|
317
|
+
'profile.industry': '行业',
|
|
318
|
+
'profile.website': '网站',
|
|
319
|
+
'profile.employees': '员工',
|
|
320
|
+
'profile.founded': '成立于',
|
|
321
|
+
'profile.companyDescription': '公司介绍',
|
|
322
|
+
'profile.level': '等级',
|
|
323
|
+
'profile.gamesPlayed': '游戏场次',
|
|
324
|
+
'profile.winRate': '胜率',
|
|
325
|
+
'profile.rank': '段位',
|
|
326
|
+
// Public profile
|
|
327
|
+
'publicProfile.projects': '项目',
|
|
328
|
+
'publicProfile.projects2Col': '两栏项目',
|
|
329
|
+
'publicProfile.projects3Col': '三栏项目',
|
|
330
|
+
'publicProfile.activity': '活动',
|
|
331
|
+
'publicProfile.teams': '团队',
|
|
332
|
+
'publicProfile.followers': '关注者',
|
|
333
|
+
'publicProfile.following': '正在关注',
|
|
334
|
+
'publicProfile.editProfile': '编辑资料',
|
|
335
|
+
'publicProfile.sendMessage': '发送消息',
|
|
336
|
+
'publicProfile.joinedDate': '{date} 加入',
|
|
337
|
+
'publicProfile.noProjects': '暂无项目',
|
|
338
|
+
'publicProfile.noActivity': '暂无活动',
|
|
339
|
+
'publicProfile.noTeams': '暂无团队',
|
|
340
|
+
'publicProfile.projectMembers': '{count} 名成员',
|
|
341
|
+
'publicProfile.projectTasks': '{count} 个任务',
|
|
342
|
+
'publicProfile.viewProfile': '查看资料',
|
|
343
|
+
'publicProfile.activityPosted': '发布了动态',
|
|
344
|
+
'publicProfile.activityCommented': '评论了',
|
|
345
|
+
'publicProfile.activityJoined': '加入了',
|
|
346
|
+
'publicProfile.activityCreated': '创建了',
|
|
347
|
+
// Account demo pages
|
|
348
|
+
'account.getStarted': '开始使用',
|
|
349
|
+
'account.getStartedDescription': '按照步骤补齐资料、连接服务并邀请团队成员。',
|
|
350
|
+
'account.uploadAvatar': '上传头像',
|
|
351
|
+
'account.setupProfile': '完善个人资料',
|
|
352
|
+
'account.connectProvider': '连接身份提供方',
|
|
353
|
+
'account.setupTeam': '创建团队空间',
|
|
354
|
+
'account.completeSetup': '完成设置',
|
|
355
|
+
'account.skipSetup': '跳过',
|
|
356
|
+
'account.step': '第 {current} / {total} 步',
|
|
357
|
+
'account.companyProfile': '公司资料',
|
|
358
|
+
'account.companyProfileDescription': '维护组织基础信息、行业属性和公开展示内容。',
|
|
359
|
+
'account.settingsPlain': '纯净设置',
|
|
360
|
+
'account.settingsPlainDescription': '以单页表单方式管理账号、安全与通知偏好。',
|
|
361
|
+
'account.settingsSidebar': '侧边栏设置',
|
|
362
|
+
'account.settingsSidebarDescription': '调整导航密度、默认折叠和菜单显示偏好。',
|
|
363
|
+
'account.settingsEnterprise': '企业设置',
|
|
364
|
+
'account.settingsEnterpriseDescription': '配置 SSO、会话策略、审计日志和合规保留周期。',
|
|
365
|
+
'account.membersStarter': '成员入门',
|
|
366
|
+
'account.membersStarterDescription': '快速邀请核心成员并检查初始角色配置。',
|
|
367
|
+
'account.teamMembers': '团队成员',
|
|
368
|
+
'account.teamMembersDescription': '按部门、角色、状态和最近活跃时间管理团队名册。',
|
|
369
|
+
'account.inviteMember': '邀请成员',
|
|
370
|
+
'account.active': '活跃',
|
|
371
|
+
'account.invited': '已邀请',
|
|
372
|
+
'account.inactive': '未活跃',
|
|
373
|
+
'account.member': '成员',
|
|
374
|
+
'account.role': '角色',
|
|
375
|
+
'account.status': '状态',
|
|
376
|
+
'account.joined': '加入时间',
|
|
377
|
+
'account.importMembers': '导入成员',
|
|
378
|
+
'account.importMembersDescription': '上传 CSV 或 Excel 文件,批量创建成员并预分配角色。',
|
|
379
|
+
'account.importComplete': '导入完成',
|
|
380
|
+
'account.imported': '已成功导入 {count} 位成员。',
|
|
381
|
+
'account.dragOrClick': '拖拽文件到这里,或点击选择文件',
|
|
382
|
+
'account.supportedFormats': '支持 CSV、XLSX,建议不超过 5MB。',
|
|
383
|
+
'account.importProgress': '导入中',
|
|
384
|
+
'account.startImport': '开始导入',
|
|
385
|
+
'account.downloadTemplate': '下载模板',
|
|
386
|
+
'account.securityLog': '安全日志',
|
|
387
|
+
'account.securityLogDescription': '查看登录、密钥、2FA 和会话变更记录。',
|
|
388
|
+
// Security log
|
|
389
|
+
'securityLog.event': '事件',
|
|
390
|
+
'securityLog.ip': 'IP 地址',
|
|
391
|
+
'securityLog.device': '设备',
|
|
392
|
+
'securityLog.location': '位置',
|
|
393
|
+
'securityLog.time': '时间',
|
|
394
|
+
'securityLog.loginSuccess': '登录成功',
|
|
395
|
+
'securityLog.loginFailed': '登录失败',
|
|
396
|
+
'securityLog.apiKeyCreated': '创建 API 密钥',
|
|
397
|
+
'securityLog.apiKeyRevoked': '撤销 API 密钥',
|
|
398
|
+
'securityLog.twoFactorEnabled': '启用两步验证',
|
|
399
|
+
'securityLog.passwordChanged': '修改密码',
|
|
400
|
+
'securityLog.sessionRevoked': '撤销会话',
|
|
401
|
+
// Network
|
|
402
|
+
'network.userCards': '用户卡片',
|
|
403
|
+
'network.userCardsDescription': '以收藏卡片方式展示社区、资产和连接状态。',
|
|
404
|
+
'network.teamCrew': '团队成员表',
|
|
405
|
+
'network.teamCrewDescription': '用表格方式查看跨部门成员、在线状态和项目投入。',
|
|
406
|
+
'network.nftCollection': '藏品',
|
|
407
|
+
'network.floorPrice': '底价',
|
|
408
|
+
'network.volume': '交易量',
|
|
409
|
+
'network.connected': '已连接',
|
|
410
|
+
'network.connect': '连接',
|
|
411
|
+
// Two-factor authentication
|
|
412
|
+
'auth.twoFactorSetup': '两步验证设置',
|
|
413
|
+
'auth.twoFactorScanQR': '扫描二维码',
|
|
414
|
+
'auth.twoFactorEnterCode': '输入验证码',
|
|
415
|
+
'auth.twoFactorSaveRecovery': '保存恢复码',
|
|
416
|
+
'auth.twoFactorRecoveryHint': '请把恢复码保存在安全位置,手机不可用时可以用它登录。',
|
|
417
|
+
'auth.twoFactorVerify': '验证代码',
|
|
418
|
+
'auth.twoFactorRecovery': '恢复码',
|
|
419
|
+
'auth.twoFactorVerifySuccess': '两步验证已启用',
|
|
420
|
+
'auth.twoFactorDisable': '停用两步验证',
|
|
328
421
|
// Tasks
|
|
329
422
|
'task.drawerTitle': '任务中心',
|
|
330
423
|
'task.listTitle': '任务列表',
|
|
@@ -388,127 +481,6 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
388
481
|
'task.status.dead_lettered': '死信',
|
|
389
482
|
'task.status.retry_scheduled': '等待重试',
|
|
390
483
|
'task.status.leased': '已领取',
|
|
391
|
-
// Public Profile
|
|
392
|
-
'publicProfile.title': '公开资料',
|
|
393
|
-
'publicProfile.projects': '项目',
|
|
394
|
-
'publicProfile.activity': '动态',
|
|
395
|
-
'publicProfile.teams': '团队',
|
|
396
|
-
'publicProfile.profile': '资料',
|
|
397
|
-
'publicProfile.viewProfile': '查看资料',
|
|
398
|
-
'publicProfile.followers': '关注者',
|
|
399
|
-
'publicProfile.following': '关注中',
|
|
400
|
-
'publicProfile.editProfile': '编辑资料',
|
|
401
|
-
'publicProfile.sendMessage': '发消息',
|
|
402
|
-
'publicProfile.addFriend': '添加好友',
|
|
403
|
-
'publicProfile.projects2Col': '项目 (两列)',
|
|
404
|
-
'publicProfile.projects3Col': '项目 (三列)',
|
|
405
|
-
'publicProfile.noProjects': '暂无项目',
|
|
406
|
-
'publicProfile.noActivity': '暂无动态',
|
|
407
|
-
'publicProfile.noTeams': '暂无团队',
|
|
408
|
-
'publicProfile.joinedDate': '加入于 {date}',
|
|
409
|
-
'publicProfile.projectMembers': '{count} 名成员',
|
|
410
|
-
'publicProfile.projectTasks': '{count} 个任务',
|
|
411
|
-
'publicProfile.activityPosted': '发布了内容',
|
|
412
|
-
'publicProfile.activityCommented': '评论了',
|
|
413
|
-
'publicProfile.activityJoined': '加入了',
|
|
414
|
-
'publicProfile.activityCreated': '创建了',
|
|
415
|
-
// Profile variants
|
|
416
|
-
'profile.company': '公司资料',
|
|
417
|
-
'profile.companyDescription': '公司简介和统计信息',
|
|
418
|
-
'profile.gamer': '玩家资料',
|
|
419
|
-
'profile.gamerDescription': '游戏成就和统计',
|
|
420
|
-
'profile.default': '个人资料',
|
|
421
|
-
'profile.defaultDescription': '个人简介和活动',
|
|
422
|
-
'profile.companyName': '公司名称',
|
|
423
|
-
'profile.industry': '行业',
|
|
424
|
-
'profile.employees': '员工数',
|
|
425
|
-
'profile.founded': '成立时间',
|
|
426
|
-
'profile.website': '网站',
|
|
427
|
-
'profile.gamerTag': '玩家标签',
|
|
428
|
-
'profile.level': '等级',
|
|
429
|
-
'profile.rank': '排名',
|
|
430
|
-
'profile.gamesPlayed': '游戏场次',
|
|
431
|
-
'profile.winRate': '胜率',
|
|
432
|
-
// Account extensions
|
|
433
|
-
'account.getStarted': '入门指南',
|
|
434
|
-
'account.getStartedDescription': '按照步骤完成初始设置,快速上手平台功能。',
|
|
435
|
-
'account.companyProfile': '公司信息',
|
|
436
|
-
'account.companyProfileDescription': '管理公司基本信息和品牌设置。',
|
|
437
|
-
'account.settingsPlain': '基本设置',
|
|
438
|
-
'account.settingsPlainDescription': '管理基本账户设置。',
|
|
439
|
-
'account.settingsSidebar': '侧栏设置',
|
|
440
|
-
'account.settingsSidebarDescription': '配置侧边栏布局和行为。',
|
|
441
|
-
'account.settingsEnterprise': '企业设置',
|
|
442
|
-
'account.settingsEnterpriseDescription': '管理企业级配置和策略。',
|
|
443
|
-
'account.importMembers': '导入成员',
|
|
444
|
-
'account.importMembersDescription': '从 CSV 文件批量导入团队成员。',
|
|
445
|
-
'account.membersStarter': '成员概览',
|
|
446
|
-
'account.membersStarterDescription': '查看和管理工作区成员。',
|
|
447
|
-
'account.securityLog': '安全日志',
|
|
448
|
-
'account.securityLogDescription': '查看账户安全相关的操作记录。',
|
|
449
|
-
'account.step': '第 {current} 步(共 {total} 步)',
|
|
450
|
-
'account.completeSetup': '完成设置',
|
|
451
|
-
'account.uploadAvatar': '上传头像',
|
|
452
|
-
'account.connectProvider': '连接服务',
|
|
453
|
-
'account.setupProfile': '完善资料',
|
|
454
|
-
'account.setupTeam': '邀请团队',
|
|
455
|
-
'account.skipSetup': '跳过设置',
|
|
456
|
-
'account.nextStep': '下一步',
|
|
457
|
-
'account.prevStep': '上一步',
|
|
458
|
-
'account.finishSetup': '完成设置',
|
|
459
|
-
'account.dragOrClick': '拖拽文件或点击上传',
|
|
460
|
-
'account.supportedFormats': '支持 CSV、XLSX 格式',
|
|
461
|
-
'account.downloadTemplate': '下载模板',
|
|
462
|
-
'account.startImport': '开始导入',
|
|
463
|
-
'account.importProgress': '导入进度',
|
|
464
|
-
'account.importComplete': '导入完成',
|
|
465
|
-
'account.imported': '已导入 {count} 条记录',
|
|
466
|
-
'account.member': '成员',
|
|
467
|
-
'account.role': '角色',
|
|
468
|
-
'account.status': '状态',
|
|
469
|
-
'account.joined': '加入时间',
|
|
470
|
-
'account.active': '活跃',
|
|
471
|
-
'account.invited': '已邀请',
|
|
472
|
-
'account.inactive': '未激活',
|
|
473
|
-
'account.inviteMember': '邀请成员',
|
|
474
|
-
'account.removeMember': '移除成员',
|
|
475
|
-
// Network
|
|
476
|
-
'network.userCards': '用户卡片',
|
|
477
|
-
'network.userCardsDescription': '以卡片形式浏览用户列表。',
|
|
478
|
-
'network.teamCrew': '团队花名册',
|
|
479
|
-
'network.teamCrewDescription': '以表格形式查看团队成员信息。',
|
|
480
|
-
'network.connect': '关注',
|
|
481
|
-
'network.connected': '已关注',
|
|
482
|
-
'network.nftCollection': 'NFT 收藏',
|
|
483
|
-
'network.nftCount': '{count} 件藏品',
|
|
484
|
-
'network.floorPrice': '地板价',
|
|
485
|
-
'network.volume': '交易量',
|
|
486
|
-
'network.viewCollection': '查看收藏',
|
|
487
|
-
// Auth
|
|
488
|
-
'auth.twoFactorSetup': '设置双重验证',
|
|
489
|
-
'auth.twoFactorVerify': '验证身份',
|
|
490
|
-
'auth.twoFactorRecovery': '恢复代码',
|
|
491
|
-
'auth.twoFactorScanQR': '使用验证器应用扫描二维码',
|
|
492
|
-
'auth.twoFactorEnterCode': '输入 6 位验证码',
|
|
493
|
-
'auth.twoFactorVerifySuccess': '双重验证已成功启用',
|
|
494
|
-
'auth.twoFactorSaveRecovery': '请妥善保存以下恢复代码',
|
|
495
|
-
'auth.twoFactorRecoveryHint': '当您无法使用验证器时,可使用恢复代码登录。每个代码只能使用一次。',
|
|
496
|
-
'auth.twoFactorDisable': '关闭双重验证',
|
|
497
|
-
'auth.twoFactorDisableConfirm': '确认要关闭双重验证吗?这将降低账户安全性。',
|
|
498
|
-
// Security Log
|
|
499
|
-
'securityLog.event': '事件',
|
|
500
|
-
'securityLog.ip': 'IP 地址',
|
|
501
|
-
'securityLog.device': '设备',
|
|
502
|
-
'securityLog.location': '位置',
|
|
503
|
-
'securityLog.time': '时间',
|
|
504
|
-
'securityLog.loginSuccess': '登录成功',
|
|
505
|
-
'securityLog.loginFailed': '登录失败',
|
|
506
|
-
'securityLog.passwordChanged': '密码已修改',
|
|
507
|
-
'securityLog.twoFactorEnabled': '已启用双重验证',
|
|
508
|
-
'securityLog.twoFactorDisabled': '已关闭双重验证',
|
|
509
|
-
'securityLog.apiKeyCreated': 'API 密钥已创建',
|
|
510
|
-
'securityLog.apiKeyRevoked': 'API 密钥已吊销',
|
|
511
|
-
'securityLog.sessionRevoked': '会话已注销',
|
|
512
484
|
},
|
|
513
485
|
'en': {
|
|
514
486
|
'common.list': 'List',
|
|
@@ -629,6 +601,7 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
629
601
|
'auth.hasAccount': 'Already have an account?',
|
|
630
602
|
'auth.forgotPasswordLink': 'Forgot password?',
|
|
631
603
|
'auth.forgotPasswordDescription': 'Enter your email address and we will send you a reset link.',
|
|
604
|
+
'auth.resetPasswordDescription': 'Set a new sign-in password.',
|
|
632
605
|
'auth.resetLinkSent': 'Reset link sent! Check your email.',
|
|
633
606
|
'auth.passwordMismatch': 'Passwords do not match',
|
|
634
607
|
'auth.emailRequired': 'Email is required',
|
|
@@ -651,19 +624,6 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
651
624
|
'devtools.title': 'DevTools (Ctrl+Shift+D)',
|
|
652
625
|
// Validation
|
|
653
626
|
// Chat
|
|
654
|
-
'chat.title': 'AI Assistant',
|
|
655
|
-
'chat.typing': 'Typing...',
|
|
656
|
-
'chat.clear': 'Clear',
|
|
657
|
-
'chat.welcome': 'How can I help?',
|
|
658
|
-
'chat.welcomeDesc': 'Ask me anything about your admin panel, resources, and inventory operations.',
|
|
659
|
-
'chat.thinking': 'Thinking...',
|
|
660
|
-
'chat.placeholder': 'Type a message...',
|
|
661
|
-
'chat.shortcutHint': 'to toggle',
|
|
662
|
-
'chat.error': 'Sorry, something went wrong. Please try again.',
|
|
663
|
-
'chat.approvalRequired': 'Approval required',
|
|
664
|
-
'chat.suggestion1': 'How do I create a new resource?',
|
|
665
|
-
'chat.suggestion2': 'Explain the data model',
|
|
666
|
-
'chat.suggestion3': 'Help me write a filter query',
|
|
667
627
|
'chat.contextInfo': 'Current context: {resource}',
|
|
668
628
|
'chat.actionButton': 'Execute',
|
|
669
629
|
'chat.historyRestored': 'Chat history restored',
|
|
@@ -679,14 +639,11 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
679
639
|
// Settings
|
|
680
640
|
'settings.title': 'Settings',
|
|
681
641
|
'settings.profile': 'Profile',
|
|
682
|
-
'settings.accountPreferences': 'Account preferences',
|
|
683
642
|
'settings.appearance': 'Appearance',
|
|
684
643
|
'settings.about': 'About',
|
|
685
644
|
'settings.general': 'General',
|
|
686
645
|
'settings.security': 'Security',
|
|
687
|
-
'settings.accessSecurity': 'Security and roles',
|
|
688
646
|
'settings.integrations': 'Integrations',
|
|
689
|
-
'settings.integrationApi': 'Integrations and API',
|
|
690
647
|
'settings.notifications': 'Notifications',
|
|
691
648
|
'settings.api': 'API Settings',
|
|
692
649
|
'settings.developer': 'Developer',
|
|
@@ -832,6 +789,114 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
832
789
|
'profile.currentPassword': 'Current Password',
|
|
833
790
|
'profile.passwordChanged': 'Password updated successfully',
|
|
834
791
|
'profile.updatePassword': 'Update Password',
|
|
792
|
+
'profile.newPassword': 'New Password',
|
|
793
|
+
'profile.company': 'Company',
|
|
794
|
+
'profile.gamer': 'Gamer',
|
|
795
|
+
'profile.companyName': 'Company Name',
|
|
796
|
+
'profile.industry': 'Industry',
|
|
797
|
+
'profile.website': 'Website',
|
|
798
|
+
'profile.employees': 'Employees',
|
|
799
|
+
'profile.founded': 'Founded',
|
|
800
|
+
'profile.companyDescription': 'Company Description',
|
|
801
|
+
'profile.level': 'Level',
|
|
802
|
+
'profile.gamesPlayed': 'Games Played',
|
|
803
|
+
'profile.winRate': 'Win Rate',
|
|
804
|
+
'profile.rank': 'Rank',
|
|
805
|
+
// Public profile
|
|
806
|
+
'publicProfile.projects': 'Projects',
|
|
807
|
+
'publicProfile.projects2Col': 'Projects - 2 Columns',
|
|
808
|
+
'publicProfile.projects3Col': 'Projects - 3 Columns',
|
|
809
|
+
'publicProfile.activity': 'Activity',
|
|
810
|
+
'publicProfile.teams': 'Teams',
|
|
811
|
+
'publicProfile.followers': 'Followers',
|
|
812
|
+
'publicProfile.following': 'Following',
|
|
813
|
+
'publicProfile.editProfile': 'Edit Profile',
|
|
814
|
+
'publicProfile.sendMessage': 'Send Message',
|
|
815
|
+
'publicProfile.joinedDate': 'Joined {date}',
|
|
816
|
+
'publicProfile.noProjects': 'No projects yet',
|
|
817
|
+
'publicProfile.noActivity': 'No activity yet',
|
|
818
|
+
'publicProfile.noTeams': 'No teams yet',
|
|
819
|
+
'publicProfile.projectMembers': '{count} members',
|
|
820
|
+
'publicProfile.projectTasks': '{count} tasks',
|
|
821
|
+
'publicProfile.viewProfile': 'View Profile',
|
|
822
|
+
'publicProfile.activityPosted': 'posted',
|
|
823
|
+
'publicProfile.activityCommented': 'commented on',
|
|
824
|
+
'publicProfile.activityJoined': 'joined',
|
|
825
|
+
'publicProfile.activityCreated': 'created',
|
|
826
|
+
// Account demo pages
|
|
827
|
+
'account.getStarted': 'Get Started',
|
|
828
|
+
'account.getStartedDescription': 'Complete profile basics, connect services, and invite your team.',
|
|
829
|
+
'account.uploadAvatar': 'Upload Avatar',
|
|
830
|
+
'account.setupProfile': 'Set Up Profile',
|
|
831
|
+
'account.connectProvider': 'Connect Identity Provider',
|
|
832
|
+
'account.setupTeam': 'Set Up Team',
|
|
833
|
+
'account.completeSetup': 'Complete Setup',
|
|
834
|
+
'account.skipSetup': 'Skip',
|
|
835
|
+
'account.step': 'Step {current} of {total}',
|
|
836
|
+
'account.companyProfile': 'Company Profile',
|
|
837
|
+
'account.companyProfileDescription': 'Maintain organization details, industry fields, and public profile content.',
|
|
838
|
+
'account.settingsPlain': 'Settings - Plain',
|
|
839
|
+
'account.settingsPlainDescription': 'Manage account, security, and notification preferences in one page.',
|
|
840
|
+
'account.settingsSidebar': 'Settings - Sidebar',
|
|
841
|
+
'account.settingsSidebarDescription': 'Adjust navigation density, default collapse, and menu display preferences.',
|
|
842
|
+
'account.settingsEnterprise': 'Enterprise Settings',
|
|
843
|
+
'account.settingsEnterpriseDescription': 'Configure SSO, session policy, audit logs, and compliance retention.',
|
|
844
|
+
'account.membersStarter': 'Members Starter',
|
|
845
|
+
'account.membersStarterDescription': 'Invite core members and check initial role setup.',
|
|
846
|
+
'account.teamMembers': 'Team Members',
|
|
847
|
+
'account.teamMembersDescription': 'Manage the team roster by department, role, status, and recent activity.',
|
|
848
|
+
'account.inviteMember': 'Invite Member',
|
|
849
|
+
'account.active': 'Active',
|
|
850
|
+
'account.invited': 'Invited',
|
|
851
|
+
'account.inactive': 'Inactive',
|
|
852
|
+
'account.member': 'Member',
|
|
853
|
+
'account.role': 'Role',
|
|
854
|
+
'account.status': 'Status',
|
|
855
|
+
'account.joined': 'Joined',
|
|
856
|
+
'account.importMembers': 'Import Members',
|
|
857
|
+
'account.importMembersDescription': 'Upload a CSV or Excel file to create members and pre-assign roles in bulk.',
|
|
858
|
+
'account.importComplete': 'Import Complete',
|
|
859
|
+
'account.imported': 'Imported {count} members successfully.',
|
|
860
|
+
'account.dragOrClick': 'Drag a file here, or click to select one',
|
|
861
|
+
'account.supportedFormats': 'CSV and XLSX supported, up to 5MB recommended.',
|
|
862
|
+
'account.importProgress': 'Importing',
|
|
863
|
+
'account.startImport': 'Start Import',
|
|
864
|
+
'account.downloadTemplate': 'Download Template',
|
|
865
|
+
'account.securityLog': 'Security Log',
|
|
866
|
+
'account.securityLogDescription': 'Review login, key, 2FA, and session change events.',
|
|
867
|
+
// Security log
|
|
868
|
+
'securityLog.event': 'Event',
|
|
869
|
+
'securityLog.ip': 'IP Address',
|
|
870
|
+
'securityLog.device': 'Device',
|
|
871
|
+
'securityLog.location': 'Location',
|
|
872
|
+
'securityLog.time': 'Time',
|
|
873
|
+
'securityLog.loginSuccess': 'Login successful',
|
|
874
|
+
'securityLog.loginFailed': 'Login failed',
|
|
875
|
+
'securityLog.apiKeyCreated': 'API key created',
|
|
876
|
+
'securityLog.apiKeyRevoked': 'API key revoked',
|
|
877
|
+
'securityLog.twoFactorEnabled': 'Two-factor authentication enabled',
|
|
878
|
+
'securityLog.passwordChanged': 'Password changed',
|
|
879
|
+
'securityLog.sessionRevoked': 'Session revoked',
|
|
880
|
+
// Network
|
|
881
|
+
'network.userCards': 'User Cards',
|
|
882
|
+
'network.userCardsDescription': 'Showcase community profiles, asset collections, and connection status.',
|
|
883
|
+
'network.teamCrew': 'Team Crew',
|
|
884
|
+
'network.teamCrewDescription': 'Review cross-functional members, presence, and project commitment in a table.',
|
|
885
|
+
'network.nftCollection': 'Collection',
|
|
886
|
+
'network.floorPrice': 'Floor Price',
|
|
887
|
+
'network.volume': 'Volume',
|
|
888
|
+
'network.connected': 'Connected',
|
|
889
|
+
'network.connect': 'Connect',
|
|
890
|
+
// Two-factor authentication
|
|
891
|
+
'auth.twoFactorSetup': 'Two-Factor Authentication Setup',
|
|
892
|
+
'auth.twoFactorScanQR': 'Scan QR Code',
|
|
893
|
+
'auth.twoFactorEnterCode': 'Enter Verification Code',
|
|
894
|
+
'auth.twoFactorSaveRecovery': 'Save Recovery Codes',
|
|
895
|
+
'auth.twoFactorRecoveryHint': 'Keep recovery codes somewhere safe so you can sign in if your device is unavailable.',
|
|
896
|
+
'auth.twoFactorVerify': 'Verify Code',
|
|
897
|
+
'auth.twoFactorRecovery': 'Recovery Codes',
|
|
898
|
+
'auth.twoFactorVerifySuccess': 'Two-factor authentication is enabled',
|
|
899
|
+
'auth.twoFactorDisable': 'Disable Two-Factor Authentication',
|
|
835
900
|
// Tasks
|
|
836
901
|
'task.drawerTitle': 'Task Center',
|
|
837
902
|
'task.listTitle': 'Tasks',
|
|
@@ -895,127 +960,6 @@ const baseLocales: Record<string, Record<string, string>> = {
|
|
|
895
960
|
'task.status.dead_lettered': 'Dead lettered',
|
|
896
961
|
'task.status.retry_scheduled': 'Retry scheduled',
|
|
897
962
|
'task.status.leased': 'Leased',
|
|
898
|
-
// Public Profile
|
|
899
|
-
'publicProfile.title': 'Public Profile',
|
|
900
|
-
'publicProfile.projects': 'Projects',
|
|
901
|
-
'publicProfile.activity': 'Activity',
|
|
902
|
-
'publicProfile.teams': 'Teams',
|
|
903
|
-
'publicProfile.profile': 'Profile',
|
|
904
|
-
'publicProfile.viewProfile': 'View Profile',
|
|
905
|
-
'publicProfile.followers': 'Followers',
|
|
906
|
-
'publicProfile.following': 'Following',
|
|
907
|
-
'publicProfile.editProfile': 'Edit Profile',
|
|
908
|
-
'publicProfile.sendMessage': 'Send Message',
|
|
909
|
-
'publicProfile.addFriend': 'Add Friend',
|
|
910
|
-
'publicProfile.projects2Col': 'Projects (2 Columns)',
|
|
911
|
-
'publicProfile.projects3Col': 'Projects (3 Columns)',
|
|
912
|
-
'publicProfile.noProjects': 'No projects yet',
|
|
913
|
-
'publicProfile.noActivity': 'No activity yet',
|
|
914
|
-
'publicProfile.noTeams': 'No teams yet',
|
|
915
|
-
'publicProfile.joinedDate': 'Joined {date}',
|
|
916
|
-
'publicProfile.projectMembers': '{count} members',
|
|
917
|
-
'publicProfile.projectTasks': '{count} tasks',
|
|
918
|
-
'publicProfile.activityPosted': 'posted',
|
|
919
|
-
'publicProfile.activityCommented': 'commented on',
|
|
920
|
-
'publicProfile.activityJoined': 'joined',
|
|
921
|
-
'publicProfile.activityCreated': 'created',
|
|
922
|
-
// Profile variants
|
|
923
|
-
'profile.company': 'Company Profile',
|
|
924
|
-
'profile.companyDescription': 'Company overview and statistics',
|
|
925
|
-
'profile.gamer': 'Gamer Profile',
|
|
926
|
-
'profile.gamerDescription': 'Gaming achievements and stats',
|
|
927
|
-
'profile.default': 'Default Profile',
|
|
928
|
-
'profile.defaultDescription': 'Personal overview and activity',
|
|
929
|
-
'profile.companyName': 'Company Name',
|
|
930
|
-
'profile.industry': 'Industry',
|
|
931
|
-
'profile.employees': 'Employees',
|
|
932
|
-
'profile.founded': 'Founded',
|
|
933
|
-
'profile.website': 'Website',
|
|
934
|
-
'profile.gamerTag': 'Gamer Tag',
|
|
935
|
-
'profile.level': 'Level',
|
|
936
|
-
'profile.rank': 'Rank',
|
|
937
|
-
'profile.gamesPlayed': 'Games Played',
|
|
938
|
-
'profile.winRate': 'Win Rate',
|
|
939
|
-
// Account extensions
|
|
940
|
-
'account.getStarted': 'Get Started',
|
|
941
|
-
'account.getStartedDescription': 'Follow the steps to complete your initial setup and get up to speed quickly.',
|
|
942
|
-
'account.companyProfile': 'Company Profile',
|
|
943
|
-
'account.companyProfileDescription': 'Manage company information and branding.',
|
|
944
|
-
'account.settingsPlain': 'Basic Settings',
|
|
945
|
-
'account.settingsPlainDescription': 'Manage basic account settings.',
|
|
946
|
-
'account.settingsSidebar': 'Sidebar Settings',
|
|
947
|
-
'account.settingsSidebarDescription': 'Configure sidebar layout and behavior.',
|
|
948
|
-
'account.settingsEnterprise': 'Enterprise Settings',
|
|
949
|
-
'account.settingsEnterpriseDescription': 'Manage enterprise-level configuration and policies.',
|
|
950
|
-
'account.importMembers': 'Import Members',
|
|
951
|
-
'account.importMembersDescription': 'Bulk import team members from a CSV file.',
|
|
952
|
-
'account.membersStarter': 'Members Overview',
|
|
953
|
-
'account.membersStarterDescription': 'View and manage workspace members.',
|
|
954
|
-
'account.securityLog': 'Security Log',
|
|
955
|
-
'account.securityLogDescription': 'View security-related activity logs for your account.',
|
|
956
|
-
'account.step': 'Step {current} of {total}',
|
|
957
|
-
'account.completeSetup': 'Complete Setup',
|
|
958
|
-
'account.uploadAvatar': 'Upload Avatar',
|
|
959
|
-
'account.connectProvider': 'Connect Provider',
|
|
960
|
-
'account.setupProfile': 'Set Up Profile',
|
|
961
|
-
'account.setupTeam': 'Invite Team',
|
|
962
|
-
'account.skipSetup': 'Skip Setup',
|
|
963
|
-
'account.nextStep': 'Next Step',
|
|
964
|
-
'account.prevStep': 'Previous Step',
|
|
965
|
-
'account.finishSetup': 'Finish Setup',
|
|
966
|
-
'account.dragOrClick': 'Drag file or click to upload',
|
|
967
|
-
'account.supportedFormats': 'Supports CSV, XLSX formats',
|
|
968
|
-
'account.downloadTemplate': 'Download Template',
|
|
969
|
-
'account.startImport': 'Start Import',
|
|
970
|
-
'account.importProgress': 'Import Progress',
|
|
971
|
-
'account.importComplete': 'Import Complete',
|
|
972
|
-
'account.imported': 'Imported {count} records',
|
|
973
|
-
'account.member': 'Member',
|
|
974
|
-
'account.role': 'Role',
|
|
975
|
-
'account.status': 'Status',
|
|
976
|
-
'account.joined': 'Joined',
|
|
977
|
-
'account.active': 'Active',
|
|
978
|
-
'account.invited': 'Invited',
|
|
979
|
-
'account.inactive': 'Inactive',
|
|
980
|
-
'account.inviteMember': 'Invite Member',
|
|
981
|
-
'account.removeMember': 'Remove Member',
|
|
982
|
-
// Network
|
|
983
|
-
'network.userCards': 'User Cards',
|
|
984
|
-
'network.userCardsDescription': 'Browse users in card layout.',
|
|
985
|
-
'network.teamCrew': 'Team Crew',
|
|
986
|
-
'network.teamCrewDescription': 'View team members in table layout.',
|
|
987
|
-
'network.connect': 'Follow',
|
|
988
|
-
'network.connected': 'Following',
|
|
989
|
-
'network.nftCollection': 'NFT Collection',
|
|
990
|
-
'network.nftCount': '{count} items',
|
|
991
|
-
'network.floorPrice': 'Floor Price',
|
|
992
|
-
'network.volume': 'Volume',
|
|
993
|
-
'network.viewCollection': 'View Collection',
|
|
994
|
-
// Auth
|
|
995
|
-
'auth.twoFactorSetup': 'Set Up Two-Factor Auth',
|
|
996
|
-
'auth.twoFactorVerify': 'Verify Identity',
|
|
997
|
-
'auth.twoFactorRecovery': 'Recovery Codes',
|
|
998
|
-
'auth.twoFactorScanQR': 'Scan the QR code with your authenticator app',
|
|
999
|
-
'auth.twoFactorEnterCode': 'Enter 6-digit verification code',
|
|
1000
|
-
'auth.twoFactorVerifySuccess': 'Two-factor authentication enabled successfully',
|
|
1001
|
-
'auth.twoFactorSaveRecovery': 'Save these recovery codes in a safe place',
|
|
1002
|
-
'auth.twoFactorRecoveryHint': 'You can use recovery codes to log in when you cannot access your authenticator. Each code can only be used once.',
|
|
1003
|
-
'auth.twoFactorDisable': 'Disable Two-Factor Auth',
|
|
1004
|
-
'auth.twoFactorDisableConfirm': 'Are you sure you want to disable two-factor authentication? This will reduce your account security.',
|
|
1005
|
-
// Security Log
|
|
1006
|
-
'securityLog.event': 'Event',
|
|
1007
|
-
'securityLog.ip': 'IP Address',
|
|
1008
|
-
'securityLog.device': 'Device',
|
|
1009
|
-
'securityLog.location': 'Location',
|
|
1010
|
-
'securityLog.time': 'Time',
|
|
1011
|
-
'securityLog.loginSuccess': 'Login Successful',
|
|
1012
|
-
'securityLog.loginFailed': 'Login Failed',
|
|
1013
|
-
'securityLog.passwordChanged': 'Password Changed',
|
|
1014
|
-
'securityLog.twoFactorEnabled': '2FA Enabled',
|
|
1015
|
-
'securityLog.twoFactorDisabled': '2FA Disabled',
|
|
1016
|
-
'securityLog.apiKeyCreated': 'API Key Created',
|
|
1017
|
-
'securityLog.apiKeyRevoked': 'API Key Revoked',
|
|
1018
|
-
'securityLog.sessionRevoked': 'Session Revoked',
|
|
1019
963
|
},
|
|
1020
964
|
};
|
|
1021
965
|
|
|
@@ -1037,19 +981,56 @@ function detectLocale(): string {
|
|
|
1037
981
|
|
|
1038
982
|
let currentLocale = $state(detectLocale());
|
|
1039
983
|
|
|
984
|
+
/**
|
|
985
|
+
* I18nProvider — pluggable translation boundary.
|
|
986
|
+
* When set, all t/getLocale/getAvailableLocales delegates to it first,
|
|
987
|
+
* falling back to the built-in locale tables. This lets apps integrate
|
|
988
|
+
* existing i18n systems (svelte-i18n, paraglide, etc.) without rewriting callsites.
|
|
989
|
+
*/
|
|
990
|
+
export interface I18nProvider {
|
|
991
|
+
translate: (key: string, params?: Record<string, string | number>) => string;
|
|
992
|
+
getLocale: () => string;
|
|
993
|
+
setLocale: (locale: string) => void;
|
|
994
|
+
getAvailableLocales?: () => string[];
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
let i18nProvider: I18nProvider | undefined;
|
|
998
|
+
|
|
999
|
+
export function setI18nProvider(provider: I18nProvider | undefined): void {
|
|
1000
|
+
i18nProvider = provider;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export function getI18nProvider(): I18nProvider | undefined {
|
|
1004
|
+
return i18nProvider;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1040
1007
|
export function setLocale(locale: string): void {
|
|
1008
|
+
if (i18nProvider) {
|
|
1009
|
+
i18nProvider.setLocale(locale);
|
|
1010
|
+
}
|
|
1011
|
+
// 同步更新 local state,确保 Svelte $state 依赖追踪触发模板刷新
|
|
1041
1012
|
currentLocale = locale;
|
|
1042
1013
|
}
|
|
1043
1014
|
|
|
1044
1015
|
export function getLocale(): string {
|
|
1016
|
+
// 读取 currentLocale 以保持 Svelte $state 响应式追踪
|
|
1017
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1018
|
+
currentLocale;
|
|
1019
|
+
if (i18nProvider) return i18nProvider.getLocale();
|
|
1045
1020
|
return currentLocale;
|
|
1046
1021
|
}
|
|
1047
1022
|
|
|
1048
1023
|
export function getAvailableLocales(): string[] {
|
|
1024
|
+
if (i18nProvider?.getAvailableLocales) return i18nProvider.getAvailableLocales();
|
|
1049
1025
|
return Object.keys(locales);
|
|
1050
1026
|
}
|
|
1051
1027
|
|
|
1052
1028
|
export function t(key: string, params?: Record<string, string | number>): string {
|
|
1029
|
+
// 读取 currentLocale 以建立 Svelte $state 依赖追踪,
|
|
1030
|
+
// 确保无论是否使用 provider,语言切换都能触发模板刷新。
|
|
1031
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1032
|
+
currentLocale;
|
|
1033
|
+
if (i18nProvider) return i18nProvider.translate(key, params);
|
|
1053
1034
|
const locale = locales[currentLocale];
|
|
1054
1035
|
let text = locale?.[key] ?? locales['en']?.[key] ?? key;
|
|
1055
1036
|
|
|
@@ -1068,6 +1049,7 @@ export function addTranslations(locale: string, translations: Record<string, str
|
|
|
1068
1049
|
export function resetI18n(): void {
|
|
1069
1050
|
currentLocale = detectLocale();
|
|
1070
1051
|
locales = initialLocales;
|
|
1052
|
+
i18nProvider = undefined;
|
|
1071
1053
|
}
|
|
1072
1054
|
|
|
1073
1055
|
/**
|
|
@@ -1079,9 +1061,9 @@ export function useTranslation() {
|
|
|
1079
1061
|
return t(key, params);
|
|
1080
1062
|
};
|
|
1081
1063
|
|
|
1082
|
-
return {
|
|
1064
|
+
return {
|
|
1083
1065
|
get t() { return translate; },
|
|
1084
|
-
get locale() { return
|
|
1066
|
+
get locale() { return getLocale(); },
|
|
1085
1067
|
setLocale,
|
|
1086
1068
|
getAvailableLocales
|
|
1087
1069
|
};
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export {
|
|
|
23
23
|
useNavigation, useGo, useBack,
|
|
24
24
|
useGetToPath, useLink,
|
|
25
25
|
useResource,
|
|
26
|
-
useModalForm, useDrawerForm, useModal,
|
|
26
|
+
useModalForm, useDrawerForm, useModal, useCheckboxGroup, useRadioGroup, useAutocomplete,
|
|
27
27
|
useOvertime, useRelation,
|
|
28
28
|
useNotification, useDataProvider,
|
|
29
29
|
useMenu, useBreadcrumb, useThemedLayoutContext,
|
|
@@ -36,7 +36,8 @@ export { setAccessControlProvider, getAccessControlProvider, getAccessControlOpt
|
|
|
36
36
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
37
37
|
export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, resetToast } from './toast.svelte';
|
|
38
38
|
export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
|
|
39
|
-
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation } from './i18n.svelte';
|
|
39
|
+
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, resetI18n } from './i18n.svelte';
|
|
40
|
+
export type { I18nProvider } from './i18n.svelte';
|
|
40
41
|
export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
41
42
|
export type { AuditLogProvider } from './audit';
|
|
42
43
|
export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
|
|
@@ -79,11 +80,12 @@ export type { UseCanOptions, UseCanResult } from './useCan';
|
|
|
79
80
|
export { createCaslAccessControl } from './adapters/casl';
|
|
80
81
|
export { createCasbinAccessControl } from './adapters/casbin';
|
|
81
82
|
export type { CasbinAdapterOptions } from './adapters/casbin';
|
|
82
|
-
export { useExport, useImport } from './data-transfer.svelte';
|
|
83
|
-
export type { UseExportOptions, UseImportOptions } from './data-transfer.svelte';
|
|
83
|
+
export { useExport, useImport, downloadData } from './data-transfer.svelte';
|
|
84
|
+
export type { UseExportOptions, UseImportOptions, ExportFormat } from './data-transfer.svelte';
|
|
84
85
|
export {
|
|
85
86
|
useLogin, useLogout,
|
|
86
87
|
useRegister, useForgotPassword, useUpdatePassword,
|
|
88
|
+
useUpdateIdentity, useUpdateProfile,
|
|
87
89
|
useGetIdentity, useIsAuthenticated,
|
|
88
90
|
useOnError, usePermissions,
|
|
89
91
|
getLogoutVersion, resetLogoutVersion,
|
|
@@ -9,6 +9,7 @@ export interface CanParams {
|
|
|
9
9
|
resource: string;
|
|
10
10
|
action: Action;
|
|
11
11
|
params?: { id?: string | number; [key: string]: unknown };
|
|
12
|
+
meta?: Record<string, unknown>;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface CanResult {
|
|
@@ -81,8 +82,8 @@ export function getAccessControlOptions() {
|
|
|
81
82
|
* Supports both single capability requests or an array of batched checks.
|
|
82
83
|
*/
|
|
83
84
|
export async function canAccessAsync(params: CanParams[]): Promise<CanResult[]>;
|
|
84
|
-
export async function canAccessAsync(resource: string, action: Action, params?: Record<string, unknown>): Promise<CanResult>;
|
|
85
|
-
export async function canAccessAsync(resourceOrBatch: string | CanParams[], action?: Action, params?: Record<string, unknown>): Promise<CanResult | CanResult[]> {
|
|
85
|
+
export async function canAccessAsync(resource: string, action: Action, params?: Record<string, unknown>, meta?: Record<string, unknown>): Promise<CanResult>;
|
|
86
|
+
export async function canAccessAsync(resourceOrBatch: string | CanParams[], action?: Action, params?: Record<string, unknown>, meta?: Record<string, unknown>): Promise<CanResult | CanResult[]> {
|
|
86
87
|
if (!provider) {
|
|
87
88
|
return Array.isArray(resourceOrBatch)
|
|
88
89
|
? resourceOrBatch.map(() => ({ can: true }))
|
|
@@ -93,7 +94,7 @@ export async function canAccessAsync(resourceOrBatch: string | CanParams[], acti
|
|
|
93
94
|
return provider.can(resourceOrBatch);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
return provider.can({ resource: resourceOrBatch, action: action as string, params }) as Promise<CanResult>;
|
|
97
|
+
return provider.can({ resource: resourceOrBatch, action: action as string, params, meta }) as Promise<CanResult>;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// ─── Feature Gate ─────────────────────────────────────────────
|
|
@@ -87,9 +87,26 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
87
87
|
const queryOptions = opts.queryOptions;
|
|
88
88
|
const pagination = cloneQueryKeyPart(opts.pagination);
|
|
89
89
|
const sorters = cloneQueryKeyPart(opts.sorters);
|
|
90
|
-
const
|
|
90
|
+
const explicitFilters = cloneQueryKeyPart(opts.filters);
|
|
91
91
|
const meta = cloneQueryKeyPart(opts.meta);
|
|
92
|
-
|
|
92
|
+
|
|
93
|
+
// 自动注入嵌套资源 parent filter
|
|
94
|
+
// 仅消费路径解析出的 parentParams(如 /teams/123/users 的 teamId),
|
|
95
|
+
// 不再从 query params 中误取,避免 ?tenantId=1 等被静默转成 filter。
|
|
96
|
+
//
|
|
97
|
+
// 安全边界:仅在 useList 未显式指定 resource(回退到 parsed.resource),
|
|
98
|
+
// 或显式 resource 与当前路由资源一致时注入。
|
|
99
|
+
const parentFilters: typeof explicitFilters = [];
|
|
100
|
+
const injectParent = !opts.resource || opts.resource === parsed.resource;
|
|
101
|
+
if (injectParent && parsed.parentParams) {
|
|
102
|
+
for (const [paramKey, paramVal] of Object.entries(parsed.parentParams)) {
|
|
103
|
+
if (paramKey.endsWith('Id') && paramVal) {
|
|
104
|
+
parentFilters.push({ field: paramKey, operator: 'eq' as const, value: paramVal });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const filters = [...parentFilters, ...(explicitFilters ?? [])];
|
|
109
|
+
|
|
93
110
|
return {
|
|
94
111
|
queryKey: [opts.dataProviderName, resource, 'list', pagination, sorters, filters, meta],
|
|
95
112
|
queryFn: async () => provider.getList<TData>({
|
|
@@ -99,6 +99,15 @@ export function useResource(resourceName?: string) {
|
|
|
99
99
|
get resource() { return resolvedResource; },
|
|
100
100
|
get resources() { return getResources(); },
|
|
101
101
|
get identifier() { return resolvedResource?.identifier ?? resolvedResource?.name; },
|
|
102
|
+
/** 当前 URL 解析出的 parent 资源信息(嵌套资源路由) */
|
|
103
|
+
get parent() {
|
|
104
|
+
const parentParams: Record<string, string> = {};
|
|
105
|
+
// 只取路径解析出的 parent params,排除 query params 的误判
|
|
106
|
+
for (const [k, v] of Object.entries(parsed.parentParams)) {
|
|
107
|
+
if (v) parentParams[k] = v;
|
|
108
|
+
}
|
|
109
|
+
return Object.keys(parentParams).length > 0 ? parentParams : undefined;
|
|
110
|
+
},
|
|
102
111
|
/** Resolve a resource by name — useful for dynamic lookups inside callbacks */
|
|
103
112
|
select: (name: string) => {
|
|
104
113
|
try {
|
package/src/types.ts
CHANGED
|
@@ -72,6 +72,7 @@ export interface GetListParams {
|
|
|
72
72
|
export interface GetListResult<TData extends BaseRecord = BaseRecord> {
|
|
73
73
|
data: TData[];
|
|
74
74
|
total: number;
|
|
75
|
+
[key: string]: unknown;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export interface GetOneParams {
|
|
@@ -315,6 +316,8 @@ export interface AuthProvider {
|
|
|
315
316
|
register?: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
316
317
|
forgotPassword?: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
317
318
|
updatePassword?: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
319
|
+
/** Update current user's identity fields such as name or email. */
|
|
320
|
+
updateIdentity?: (params: Partial<Identity> & Record<string, unknown>) => Promise<AuthActionResult>;
|
|
318
321
|
/** Update user profile (name, avatar, etc.) */
|
|
319
322
|
updateProfile?: (params: { name?: string; avatar?: string | File; [key: string]: unknown }) => Promise<AuthActionResult>;
|
|
320
323
|
/** Get list of system roles for RBAC mapping */
|
package/src/useCan.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface UseCanOptions {
|
|
|
8
8
|
resource: string;
|
|
9
9
|
action: Action;
|
|
10
10
|
params?: Record<string, unknown>;
|
|
11
|
+
meta?: Record<string, unknown>;
|
|
11
12
|
queryOptions?: {
|
|
12
13
|
enabled?: boolean;
|
|
13
14
|
staleTime?: number;
|
|
@@ -34,8 +35,8 @@ export function useCan(options: () => UseCanOptions): UseCanResult {
|
|
|
34
35
|
const query = createQuery<CanResult>(() => {
|
|
35
36
|
const opts = options();
|
|
36
37
|
return {
|
|
37
|
-
queryKey: ['useCan', opts.resource, opts.action, opts.params] as const,
|
|
38
|
-
queryFn: () => canAccessAsync(opts.resource, opts.action, opts.params),
|
|
38
|
+
queryKey: ['useCan', opts.resource, opts.action, opts.params, opts.meta] as const,
|
|
39
|
+
queryFn: () => canAccessAsync(opts.resource, opts.action, opts.params, opts.meta),
|
|
39
40
|
enabled: opts.queryOptions?.enabled ?? true,
|
|
40
41
|
staleTime: opts.queryOptions?.staleTime ?? 5 * 60 * 1000,
|
|
41
42
|
};
|
package/src/useParsed.svelte.ts
CHANGED
|
@@ -10,7 +10,10 @@ interface ParsedRoute {
|
|
|
10
10
|
resourcePath?: string;
|
|
11
11
|
action?: 'list' | 'create' | 'edit' | 'show' | 'clone';
|
|
12
12
|
id?: string;
|
|
13
|
+
/** Query params parsed from the URL search string (e.g. ?page=2). */
|
|
13
14
|
params: Record<string, string>;
|
|
15
|
+
/** Parent path params derived from nested routes (e.g. /teams/123/users -> teamId). */
|
|
16
|
+
parentParams: Record<string, string>;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
let globalPath = $state('/');
|
|
@@ -21,6 +24,16 @@ export function resetGlobalPath(): void {
|
|
|
21
24
|
export function syncGlobalPath(): void {
|
|
22
25
|
if (typeof window !== 'undefined') globalPath = currentPath();
|
|
23
26
|
}
|
|
27
|
+
|
|
28
|
+
/** 简单的英文名词单数化,处理常见复数模式(-ies, -ses, -es, -s) */
|
|
29
|
+
function singularize(word: string): string {
|
|
30
|
+
if (word.endsWith('ies') && word.length > 3) return word.slice(0, -3) + 'y';
|
|
31
|
+
if (word.endsWith('ses') && word.length > 3) return word.slice(0, -2);
|
|
32
|
+
if (word.endsWith('es') && word.length > 2) return word.slice(0, -2);
|
|
33
|
+
if (word.endsWith('s') && word.length > 1) return word.slice(0, -1);
|
|
34
|
+
return word;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
registerRouterSync(syncGlobalPath);
|
|
25
38
|
|
|
26
39
|
if (typeof window !== 'undefined') {
|
|
@@ -41,13 +54,15 @@ if (typeof window !== 'undefined') {
|
|
|
41
54
|
* // parsed.resource === 'posts'
|
|
42
55
|
* // parsed.action === 'edit'
|
|
43
56
|
* // parsed.id === '123'
|
|
57
|
+
* // parsed.params contains query params only
|
|
58
|
+
* // parsed.parentParams contains nested route params such as teamId
|
|
44
59
|
*/
|
|
45
60
|
export function useParsed(): ParsedRoute {
|
|
46
61
|
const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
|
|
47
62
|
|
|
48
63
|
const parsed = $derived.by(() => {
|
|
49
64
|
const p = globalPath;
|
|
50
|
-
const result: ParsedRoute = { params: {} };
|
|
65
|
+
const result: ParsedRoute = { params: {}, parentParams: {} };
|
|
51
66
|
|
|
52
67
|
// Parse query params from hash
|
|
53
68
|
const [pathname, queryString] = p.split('?');
|
|
@@ -75,12 +90,15 @@ export function useParsed(): ParsedRoute {
|
|
|
75
90
|
result.resource = segments[resourceIndex];
|
|
76
91
|
result.resourcePath = segments.slice(0, resourceIndex + 1).join('/');
|
|
77
92
|
|
|
78
|
-
// Add parent path params based on `<parentResource>/<parentId>` structure
|
|
93
|
+
// Add parent path params based on `<parentResource>/<parentId>` structure.
|
|
94
|
+
// These are kept separate from query params so that downstream filter
|
|
95
|
+
// injection only applies to path-derived parent params, never to
|
|
96
|
+
// arbitrary query params like ?tenantId=1.
|
|
79
97
|
for (let i = 0; i < resourceIndex; i += 2) {
|
|
80
98
|
if (segments[i] && segments[i + 1]) {
|
|
81
99
|
const parentName = segments[i];
|
|
82
|
-
const singular =
|
|
83
|
-
result.
|
|
100
|
+
const singular = singularize(parentName);
|
|
101
|
+
result.parentParams[`${singular}Id`] = segments[i + 1];
|
|
84
102
|
}
|
|
85
103
|
}
|
|
86
104
|
|
|
@@ -136,5 +154,6 @@ export function useParsed(): ParsedRoute {
|
|
|
136
154
|
get action() { return parsed.action; },
|
|
137
155
|
get id() { return parsed.id; },
|
|
138
156
|
get params() { return parsed.params; },
|
|
157
|
+
get parentParams() { return parsed.parentParams; },
|
|
139
158
|
};
|
|
140
159
|
}
|
|
@@ -8,6 +8,8 @@ import { t } from './i18n.svelte';
|
|
|
8
8
|
import type { BaseRecord, HttpError, Filter, KnownResources, ResourceDefinition } from './types';
|
|
9
9
|
import { useList, type UseListOptions } from './query-hooks.svelte';
|
|
10
10
|
import { toast } from './toast.svelte';
|
|
11
|
+
import type { UseSelectOptions } from './hooks.svelte';
|
|
12
|
+
import { useSelect as useSelectImpl } from './hooks.svelte';
|
|
11
13
|
|
|
12
14
|
// ─── useModal ─────────────────────────────────────────────────
|
|
13
15
|
export function useModal(options?: { defaultVisible?: boolean }) {
|
|
@@ -181,3 +183,112 @@ export function useRelation<TData extends BaseRecord = BaseRecord, TError = Http
|
|
|
181
183
|
export function useNotification() {
|
|
182
184
|
return { open: toast.info, success: toast.success, error: toast.error, warning: toast.warning };
|
|
183
185
|
}
|
|
186
|
+
|
|
187
|
+
// ─── Field-specific hooks (refine-compatible) ─────────────────────
|
|
188
|
+
// useCheckboxGroup / useRadioGroup / useAutocomplete are convenience
|
|
189
|
+
// wrappers around useSelect for common form field patterns.
|
|
190
|
+
|
|
191
|
+
export interface UseCheckboxGroupOptions<TData extends BaseRecord = BaseRecord> extends Omit<UseSelectOptions<TData>, 'optionLabel' | 'optionValue' | 'defaultValue'> {
|
|
192
|
+
/** Field on the record used as the option label (default: 'title') */
|
|
193
|
+
optionLabel?: string | ((item: TData) => string);
|
|
194
|
+
/** Field on the record used as the checkbox value (default: 'id') */
|
|
195
|
+
optionValue?: string | ((item: TData) => string | number);
|
|
196
|
+
/** Currently selected values — controlled mode */
|
|
197
|
+
defaultValue?: (string | number)[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* useCheckboxGroup — multi-select field state backed by a resource query.
|
|
202
|
+
* Provides a list of checkbox options plus helpers to toggle individual values.
|
|
203
|
+
*/
|
|
204
|
+
export function useCheckboxGroup<TData extends BaseRecord = BaseRecord>(options: UseCheckboxGroupOptions<TData>) {
|
|
205
|
+
const select = useSelectImpl<TData>(options);
|
|
206
|
+
let selected = $state<(string | number)[]>(options.defaultValue ?? []);
|
|
207
|
+
|
|
208
|
+
function toggle(value: string | number) {
|
|
209
|
+
const idx = selected.findIndex(v => String(v) === String(value));
|
|
210
|
+
if (idx >= 0) {
|
|
211
|
+
selected = selected.filter(v => String(v) !== String(value));
|
|
212
|
+
} else {
|
|
213
|
+
selected = [...selected, value];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isSelected(value: string | number): boolean {
|
|
218
|
+
return selected.some(v => String(v) === String(value));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
query: select.query,
|
|
223
|
+
get options() { return select.options; },
|
|
224
|
+
onSearchChange: select.onSearchChange,
|
|
225
|
+
get selected() { return selected; },
|
|
226
|
+
toggle,
|
|
227
|
+
isSelected,
|
|
228
|
+
setSelected: (values: (string | number)[]) => { selected = values; },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface UseRadioGroupOptions<TData extends BaseRecord = BaseRecord> extends Omit<UseSelectOptions<TData>, 'optionLabel' | 'optionValue' | 'defaultValue'> {
|
|
233
|
+
optionLabel?: string | ((item: TData) => string);
|
|
234
|
+
optionValue?: string | ((item: TData) => string | number);
|
|
235
|
+
defaultValue?: string | number;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* useRadioGroup — single-select field state backed by a resource query.
|
|
240
|
+
* Provides a list of radio options plus a setter for the currently selected value.
|
|
241
|
+
*/
|
|
242
|
+
export function useRadioGroup<TData extends BaseRecord = BaseRecord>(options: UseRadioGroupOptions<TData>) {
|
|
243
|
+
const { defaultValue: _dv, ...selectOpts } = options;
|
|
244
|
+
// useSelect 的 defaultValue 是数组类型;透传单值以便远程选项回填当前已选值
|
|
245
|
+
const select = useSelectImpl<TData>({
|
|
246
|
+
...selectOpts,
|
|
247
|
+
defaultValue: _dv !== undefined ? [_dv] : [],
|
|
248
|
+
});
|
|
249
|
+
let value = $state<string | number | undefined>(_dv);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
query: select.query,
|
|
253
|
+
get options() { return select.options; },
|
|
254
|
+
onSearchChange: select.onSearchChange,
|
|
255
|
+
get value() { return value; },
|
|
256
|
+
setValue: (v: string | number) => { value = v; },
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface UseAutocompleteOptions<TData extends BaseRecord = BaseRecord> extends Omit<UseSelectOptions<TData>, 'onSearch' | 'defaultValue'> {
|
|
261
|
+
optionLabel?: string | ((item: TData) => string);
|
|
262
|
+
optionValue?: string | ((item: TData) => string | number);
|
|
263
|
+
/** Field name to search against (default: optionLabel) */
|
|
264
|
+
searchField?: string;
|
|
265
|
+
defaultValue?: string | number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* useAutocomplete — typeahead search field backed by a resource query.
|
|
270
|
+
* Builds a 'contains' filter on the search field and debounces input.
|
|
271
|
+
*/
|
|
272
|
+
export function useAutocomplete<TData extends BaseRecord = BaseRecord>(options: UseAutocompleteOptions<TData>) {
|
|
273
|
+
const { searchField, defaultValue: _dv, ...selectOpts } = options;
|
|
274
|
+
const labelField = typeof options.optionLabel === 'string' ? options.optionLabel : 'title';
|
|
275
|
+
|
|
276
|
+
const select = useSelectImpl<TData>({
|
|
277
|
+
...selectOpts,
|
|
278
|
+
defaultValue: _dv !== undefined ? [_dv] : [],
|
|
279
|
+
onSearch: (term) => [{
|
|
280
|
+
field: searchField ?? labelField,
|
|
281
|
+
operator: 'contains' as const,
|
|
282
|
+
value: term,
|
|
283
|
+
}],
|
|
284
|
+
});
|
|
285
|
+
let value = $state<string | number | undefined>(_dv);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
query: select.query,
|
|
289
|
+
get options() { return select.options; },
|
|
290
|
+
onSearchChange: select.onSearchChange,
|
|
291
|
+
get value() { return value; },
|
|
292
|
+
setValue: (v: string | number) => { value = v; },
|
|
293
|
+
};
|
|
294
|
+
}
|