@liujitcn/kratos-uni-app-core 0.0.20 → 0.0.21

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.
@@ -0,0 +1,110 @@
1
+ {
2
+ "common.action.cancel": "取消",
3
+ "common.action.confirm": "確定",
4
+ "common.action.delete": "刪除",
5
+ "common.action.login": "登入",
6
+ "common.action.logout": "退出登入",
7
+ "common.action.new": "新建",
8
+ "common.action.openInNewWindow": "新視窗開啟",
9
+ "common.action.retry": "重試",
10
+ "common.action.save": "儲存",
11
+ "common.action.search": "搜尋",
12
+ "common.field.language": "語言",
13
+ "common.language.en-US": "English",
14
+ "common.language.ja-JP": "日本語",
15
+ "common.language.zh-CN": "簡體中文",
16
+ "common.language.zh-TW": "繁體中文",
17
+ "common.language.ko-KR": "韓語",
18
+ "common.language.fr-FR": "法語",
19
+ "common.language.es-ES": "西班牙語",
20
+ "common.message.loading": "正在載入",
21
+ "common.message.networkError": "網路錯誤,換個網路試試",
22
+ "common.message.requestError": "請求錯誤",
23
+ "common.message.unknown": "暫時無法處理,請稍後重試",
24
+ "common.title.notice": "提示",
25
+ "core.auth.loginAgain": "重新登入",
26
+ "core.auth.sessionExpired": "當前頁面已失效,請重新登入",
27
+ "core.config.missing": "移動端配置缺失:{names}",
28
+ "core.crypto.base64DecodeUnsupported": "當前環境不支援 Base64 解碼",
29
+ "core.crypto.base64EncodeUnsupported": "當前環境不支援 Base64 編碼",
30
+ "core.crypto.encryptFailed": "密碼加密失敗",
31
+ "core.crypto.passwordRequired": "密碼不能為空",
32
+ "core.crypto.passwordTooLong": "密碼長度超出限制",
33
+ "core.crypto.publicKeyInvalid": "公鑰格式無效",
34
+ "core.crypto.randomFailed": "隨機數生成失敗",
35
+ "core.crypto.unsupported": "當前環境不支援密碼加密",
36
+ "core.file.uploadFailed": "上傳失敗",
37
+ "core.home.accountCapability": "賬戶能力",
38
+ "core.home.accountCapabilityDescription": "個人資料、應用設定與智慧助手入口已保留",
39
+ "core.home.crossPlatform": "跨端頁面",
40
+ "core.home.crossPlatformDescription": "同一套頁面程式碼適配 H5 和微信小程式",
41
+ "core.home.demoScope": "演示範圍",
42
+ "core.home.framework": "應用框架",
43
+ "core.home.language": "開發語言",
44
+ "core.home.mainTitle": "應用框架示例",
45
+ "core.home.stateManagement": "狀態管理",
46
+ "core.home.staticHome": "靜態首頁",
47
+ "core.home.staticHomeDescription": "當前首頁用於展示框架資訊,不依賴業務介面",
48
+ "core.home.styleSolution": "樣式方案",
49
+ "core.home.subTitle": "保留通用導航與個人中心體驗",
50
+ "core.home.techStack": "當前技術棧",
51
+ "core.login.agreePrefix": "我已閱讀並同意",
52
+ "core.login.agreeSeparator": "和",
53
+ "core.login.behaviorClick": "請按順序點選文字",
54
+ "core.login.behaviorLoading": "載入中...",
55
+ "core.login.behaviorPuzzle": "請拖動滑塊完成拼圖",
56
+ "core.login.behaviorRotate": "拖動滑塊,將內圈圖片轉正",
57
+ "core.login.bindWechat": "登入並繫結微信",
58
+ "core.login.captcha": "請輸入驗證碼",
59
+ "core.login.captchaLoadFailed": "驗證碼載入失敗",
60
+ "core.login.captchaTypeMissing": "登入驗證碼型別未配置",
61
+ "core.login.captchaTypeName": "登入驗證碼型別",
62
+ "core.login.captchaTypeResponseMissing": "驗證碼介面未返回型別",
63
+ "core.login.captchaWriteFailed": "驗證碼圖片寫入失敗",
64
+ "core.login.defaultSubTitle": "歡迎使用本應用",
65
+ "core.login.loginFailed": "登入失敗,請重試",
66
+ "core.login.loginSuccess": "登入成功",
67
+ "core.login.password": "請輸入密碼",
68
+ "core.login.privacy": "《隱私協議》",
69
+ "core.login.protocolMissing": "服務條款和隱私協議未配置",
70
+ "core.login.protocolPrompt": "請先閱讀並勾選協議內容,點選確定後將自動勾選並繼續登入",
71
+ "core.login.service": "《服務條款》",
72
+ "core.login.tenant": "請輸入租戶編號",
73
+ "core.login.userName": "請輸入使用者名稱或手機號",
74
+ "core.login.userNameMobile": "請輸入使用者名稱/手機號碼",
75
+ "core.login.userNamePassword": "請輸入使用者名稱和密碼",
76
+ "core.login.wechat": "微信一鍵登入",
77
+ "core.login.wechatBindTip": "該微信尚未繫結,請登入已有賬號完成繫結",
78
+ "core.login.wechatFailed": "微信登入失敗,請稍後重試",
79
+ "core.navigation.home": "首頁",
80
+ "core.navigation.error.access": "選單訪問模式無效:{access}",
81
+ "core.navigation.error.childTab": "只有移動端二級選單可以作為 tab:{name}",
82
+ "core.navigation.error.cycle": "移動端選單存在迴圈父級:{name}",
83
+ "core.navigation.error.identity": "選單 id、name 或 app/ 路徑無效",
84
+ "core.navigation.error.itemObject": "選單項不是物件",
85
+ "core.navigation.error.listMissing": "選單響應缺少扁平列表",
86
+ "core.navigation.error.namePrefix": "選單名稱必須使用 App 字首:{name}",
87
+ "core.navigation.error.parentInvalid": "移動端選單父級編號無效:{name}",
88
+ "core.navigation.error.parentMissing": "選單缺少父級編號:{name}",
89
+ "core.navigation.error.parentNotFound": "移動端選單父級不存在:{name}",
90
+ "core.navigation.error.responseObject": "選單響應不是物件",
91
+ "core.navigation.error.rootTab": "移動端二級選單必須作為 tab:{name}",
92
+ "core.navigation.error.tabCount": "tabBar 只能配置 0 或 2-5 項",
93
+ "core.navigation.error.unique": "選單 id、name、path 必須唯一",
94
+ "core.navigation.error.viewKey": "未註冊 viewKey:{viewKey}",
95
+ "core.navigation.my": "我的",
96
+ "core.navigation.protocol": "協議詳情",
97
+ "core.protocol.loadFailed": "移動端配置載入失敗",
98
+ "core.protocol.notConfigured": "{title}未配置,暫時無法檢視",
99
+ "core.protocol.privacy": "隱私協議",
100
+ "core.protocol.service": "服務條款",
101
+ "core.status.CONFIG_ERROR": "導航配置無效",
102
+ "core.status.FORBIDDEN": "暫無訪問許可權",
103
+ "core.status.NOT_FOUND": "頁面不存在",
104
+ "core.status.OFFLINE": "網路不可用",
105
+ "core.status.PAGE_UNAVAILABLE": "頁面暫不可用",
106
+ "core.status.loading": "正在載入",
107
+ "core.webview.embedBlocked": "當前 H5 頁面可能被目標站點限制嵌入",
108
+ "core.webview.invalidUrl": "缺少有效連結地址",
109
+ "core.webview.openFailed": "連結無法開啟"
110
+ }
package/src/module.ts CHANGED
@@ -1,4 +1,14 @@
1
1
  import { corePages } from './pages'
2
+ import type { LocaleMessages, SupportedLocale } from './locales'
3
+
4
+ // Core 语言包随 coreModule 注册,供 uni-app 公共页面、请求工具和基础组件使用。
5
+ import coreEnUS from './locales/en-US.json'
6
+ import coreJaJP from './locales/ja-JP.json'
7
+ import coreZhCN from './locales/zh-CN.json'
8
+ import coreZhTW from './locales/zh-TW.json'
9
+ import coreKoKR from './locales/ko-KR.json'
10
+ import coreFrFR from './locales/fr-FR.json'
11
+ import coreEsES from './locales/es-ES.json'
2
12
 
3
13
  /** bootstrap 可见状态键。 */
4
14
  export type BootstrapViewKey =
@@ -21,6 +31,8 @@ export interface KratosAppModule {
21
31
  pages: Record<string, KratosAppPageConfig>
22
32
  views: Record<string, string>
23
33
  icons?: Record<string, string>
34
+ /** 模块按语言区域贡献的扁平语言包。 */
35
+ messages?: Partial<Record<SupportedLocale, LocaleMessages>>
24
36
  }
25
37
 
26
38
  /** 声明 uni-app 模块。 */
@@ -82,4 +94,13 @@ export const coreModule = defineKratosAppModule({
82
94
  USER_DEFAULT: 'static/tabs/user_default.png',
83
95
  USER_SELECTED: 'static/tabs/user_selected.png',
84
96
  },
97
+ messages: {
98
+ 'zh-CN': coreZhCN,
99
+ 'zh-TW': coreZhTW,
100
+ 'ko-KR': coreKoKR,
101
+ 'fr-FR': coreFrFR,
102
+ 'es-ES': coreEsES,
103
+ 'en-US': coreEnUS,
104
+ 'ja-JP': coreJaJP,
105
+ },
85
106
  })
package/src/navigation.ts CHANGED
@@ -5,6 +5,7 @@ import { navigateToLogin } from './utils/navigation'
5
5
  import { resolveStaticView } from './module'
6
6
  import { matchLogicalPath, parseLogicalQuery } from './navigation-pattern.mjs'
7
7
  import { buildMenuTree } from './navigation-tree.mjs'
8
+ import { getCurrentLocale, t } from './locales'
8
9
 
9
10
  /** 移动菜单访问模式。 */
10
11
  export type AppMenuAccess = 'PUBLIC' | 'GUEST_ONLY' | 'AUTHENTICATED'
@@ -45,14 +46,14 @@ const ANONYMOUS_CACHE_KEY = 'kratos-uni-app:navigation:anonymous'
45
46
  const AUTHENTICATED_CACHE_KEY = 'kratos-uni-app:navigation:authenticated'
46
47
  /** 固定移动端菜单根目录编号。 */
47
48
  export const APP_MENU_ROOT_ID = 999
48
- const defaultMenus: AppMenu[] = [
49
+ const createDefaultMenus = (): AppMenu[] => [
49
50
  {
50
51
  id: 99901,
51
52
  parentId: APP_MENU_ROOT_ID,
52
53
  name: 'AppHome',
53
54
  path: 'app/home',
54
55
  viewKey: 'HOME',
55
- title: '首页',
56
+ title: t('core.navigation.home'),
56
57
  access: 'PUBLIC',
57
58
  inTabBar: true,
58
59
  icon: 'HOME_DEFAULT',
@@ -64,7 +65,7 @@ const defaultMenus: AppMenu[] = [
64
65
  name: 'AppMy',
65
66
  path: 'app/my',
66
67
  viewKey: 'PROFILE_HOME',
67
- title: '我的',
68
+ title: t('core.navigation.my'),
68
69
  access: 'PUBLIC',
69
70
  inTabBar: true,
70
71
  icon: 'USER_DEFAULT',
@@ -76,7 +77,7 @@ const defaultMenus: AppMenu[] = [
76
77
  name: 'AppLogin',
77
78
  path: 'app/login',
78
79
  viewKey: 'LOGIN',
79
- title: '登录',
80
+ title: t('common.action.login'),
80
81
  access: 'GUEST_ONLY',
81
82
  inTabBar: false,
82
83
  },
@@ -86,7 +87,7 @@ const defaultMenus: AppMenu[] = [
86
87
  name: 'AppProtocol',
87
88
  path: 'app/protocol/:type',
88
89
  viewKey: 'PROTOCOL',
89
- title: '协议详情',
90
+ title: t('core.navigation.protocol'),
90
91
  access: 'PUBLIC',
91
92
  inTabBar: false,
92
93
  },
@@ -96,7 +97,7 @@ const defaultMenus: AppMenu[] = [
96
97
  name: 'AppProfile',
97
98
  path: 'app/profile',
98
99
  viewKey: 'PROFILE',
99
- title: '个人信息',
100
+ title: t('system.profile.title'),
100
101
  access: 'AUTHENTICATED',
101
102
  inTabBar: false,
102
103
  },
@@ -106,7 +107,7 @@ const defaultMenus: AppMenu[] = [
106
107
  name: 'AppSettings',
107
108
  path: 'app/settings',
108
109
  viewKey: 'SETTINGS',
109
- title: '设置',
110
+ title: t('system.settings.title'),
110
111
  access: 'AUTHENTICATED',
111
112
  inTabBar: false,
112
113
  },
@@ -116,7 +117,7 @@ const defaultMenus: AppMenu[] = [
116
117
  name: 'AppAi',
117
118
  path: 'app/ai',
118
119
  viewKey: 'AI',
119
- title: 'AI 助手',
120
+ title: t('system.ai.chatTitle'),
120
121
  access: 'AUTHENTICATED',
121
122
  inTabBar: false,
122
123
  },
@@ -132,7 +133,7 @@ const defaultMenus: AppMenu[] = [
132
133
  },
133
134
  ]
134
135
 
135
- const menus = ref<AppMenu[]>(defaultMenus)
136
+ const menus = ref<AppMenu[]>([])
136
137
  const ready = ref(false)
137
138
  let adapter: AppNavigationAdapter = {
138
139
  list: () => defBaseMenuService.ListBaseMenu(),
@@ -147,7 +148,7 @@ export function setAppNavigationAdapter(nextAdapter: AppNavigationAdapter): void
147
148
  export async function initializeAppNavigation(): Promise<void> {
148
149
  const cacheKey = resolveCacheKey()
149
150
  const cached = readCachedMenus(cacheKey)
150
- menus.value = cached ?? defaultMenus
151
+ menus.value = cached ?? createDefaultMenus()
151
152
  try {
152
153
  const nextMenus = normalizeMenuResponse(await adapter.list())
153
154
  validateMenus(nextMenus)
@@ -235,7 +236,8 @@ export function launchAppStatus(state: import('./module').BootstrapViewKey, deta
235
236
  }
236
237
 
237
238
  function resolveCacheKey(): string {
238
- return hasValidToken() ? AUTHENTICATED_CACHE_KEY : ANONYMOUS_CACHE_KEY
239
+ const identity = hasValidToken() ? AUTHENTICATED_CACHE_KEY : ANONYMOUS_CACHE_KEY
240
+ return `${identity}:${getCurrentLocale()}`
239
241
  }
240
242
 
241
243
  function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
@@ -252,15 +254,16 @@ function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
252
254
 
253
255
  function normalizeMenuResponse(response: unknown): AppMenu[] {
254
256
  if (Array.isArray(response)) return response.map(normalizeMenu)
255
- if (!response || typeof response !== 'object') throw new Error('菜单响应不是对象')
257
+ if (!response || typeof response !== 'object')
258
+ throw new Error(t('core.navigation.error.responseObject'))
256
259
  const record = response as Record<string, unknown>
257
260
  const list = record.items ?? record.list ?? record.data
258
- if (!Array.isArray(list)) throw new Error('菜单响应缺少扁平列表')
261
+ if (!Array.isArray(list)) throw new Error(t('core.navigation.error.listMissing'))
259
262
  return list.map(normalizeMenu)
260
263
  }
261
264
 
262
265
  function normalizeMenu(value: unknown): AppMenu {
263
- if (!value || typeof value !== 'object') throw new Error('菜单项不是对象')
266
+ if (!value || typeof value !== 'object') throw new Error(t('core.navigation.error.itemObject'))
264
267
  const item = value as Record<string, unknown>
265
268
  const meta = (item.meta ?? {}) as Record<string, unknown>
266
269
  const app = (meta.app ?? item.app ?? {}) as Record<string, unknown>
@@ -292,15 +295,19 @@ function validateMenus(nextMenus: AppMenu[]): void {
292
295
  const paths = new Set<string>()
293
296
  for (const menu of nextMenus) {
294
297
  if (!Number.isSafeInteger(menu.id) || !menu.name || !menu.path.startsWith('app/')) {
295
- throw new Error('菜单 id、name 或 app/ 路径无效')
298
+ throw new Error(t('core.navigation.error.identity'))
299
+ }
300
+ if (!menu.name.startsWith('App')) {
301
+ throw new Error(t('core.navigation.error.namePrefix', { name: menu.name }))
296
302
  }
297
- if (!menu.name.startsWith('App')) throw new Error(`菜单名称必须使用 App 前缀:${menu.name}`)
298
303
  if (!['PUBLIC', 'GUEST_ONLY', 'AUTHENTICATED'].includes(menu.access)) {
299
- throw new Error(`菜单访问模式无效:${menu.access}`)
304
+ throw new Error(t('core.navigation.error.access', { access: menu.access }))
305
+ }
306
+ if (!resolveStaticView(menu.viewKey)) {
307
+ throw new Error(t('core.navigation.error.viewKey', { viewKey: menu.viewKey }))
300
308
  }
301
- if (!resolveStaticView(menu.viewKey)) throw new Error(`未注册 viewKey:${menu.viewKey}`)
302
309
  if (ids.has(menu.id) || names.has(menu.name) || paths.has(menu.path)) {
303
- throw new Error('菜单 id、name、path 必须唯一')
310
+ throw new Error(t('core.navigation.error.unique'))
304
311
  }
305
312
  ids.add(menu.id)
306
313
  names.add(menu.name)
@@ -308,23 +315,28 @@ function validateMenus(nextMenus: AppMenu[]): void {
308
315
  }
309
316
  const menuMap = new Map(nextMenus.map((menu) => [menu.id, menu]))
310
317
  for (const menu of nextMenus) {
311
- if (menu.parentId === undefined) throw new Error(`菜单缺少父级编号:${menu.name}`)
318
+ if (menu.parentId === undefined) {
319
+ throw new Error(t('core.navigation.error.parentMissing', { name: menu.name }))
320
+ }
312
321
  if (menu.parentId === APP_MENU_ROOT_ID) {
313
- if (!menu.inTabBar) throw new Error(`移动端二级菜单必须作为 tab:${menu.name}`)
322
+ if (!menu.inTabBar) throw new Error(t('core.navigation.error.rootTab', { name: menu.name }))
314
323
  continue
315
324
  }
316
- if (menu.inTabBar) throw new Error(`只有移动端二级菜单可以作为 tab:${menu.name}`)
325
+ if (menu.inTabBar) throw new Error(t('core.navigation.error.childTab', { name: menu.name }))
317
326
  const visited = new Set<number>([menu.id])
318
327
  let parentId = menu.parentId
319
328
  while (parentId !== APP_MENU_ROOT_ID) {
320
- if (visited.has(parentId)) throw new Error(`移动端菜单存在循环父级:${menu.name}`)
329
+ if (visited.has(parentId))
330
+ throw new Error(t('core.navigation.error.cycle', { name: menu.name }))
321
331
  visited.add(parentId)
322
332
  const parent = menuMap.get(parentId)
323
- if (!parent) throw new Error(`移动端菜单父级不存在:${menu.name}`)
324
- if (parent.parentId === undefined) throw new Error(`移动端菜单父级编号无效:${menu.name}`)
333
+ if (!parent) throw new Error(t('core.navigation.error.parentNotFound', { name: menu.name }))
334
+ if (parent.parentId === undefined) {
335
+ throw new Error(t('core.navigation.error.parentInvalid', { name: menu.name }))
336
+ }
325
337
  parentId = parent.parentId
326
338
  }
327
339
  }
328
340
  const tabCount = nextMenus.filter((menu) => menu.parentId === APP_MENU_ROOT_ID).length
329
- if (tabCount === 1 || tabCount > 5) throw new Error('tabBar 只能配置 0 或 2-5 项')
341
+ if (tabCount === 1 || tabCount > 5) throw new Error(t('core.navigation.error.tabCount'))
330
342
  }
package/src/pages.ts CHANGED
@@ -6,11 +6,11 @@ export const corePages: Record<string, KratosAppPageConfig> = {
6
6
  style: {
7
7
  navigationStyle: 'custom',
8
8
  navigationBarTextStyle: 'white',
9
- navigationBarTitleText: '首页',
9
+ navigationBarTitleText: '',
10
10
  },
11
11
  },
12
- 'pages/login/login': { style: { navigationBarTitleText: '登录' } },
13
- 'pages/login/protocal': { style: { navigationBarTitleText: '协议详情' } },
12
+ 'pages/login/login': { style: { navigationBarTitleText: '' } },
13
+ 'pages/login/protocal': { style: { navigationBarTitleText: '' } },
14
14
  'pages/webview/webview': {
15
15
  style: {
16
16
  navigationBarTitleText: '',
@@ -15,6 +15,8 @@ export interface GetConfigRequest {
15
15
 
16
16
  /** 系统配置项 */
17
17
  export interface ConfigItem {
18
+ /** 配置ID */
19
+ id: number;
18
20
  /** 配置key */
19
21
  key: string;
20
22
  /** 配置value */
@@ -0,0 +1,37 @@
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.11.8
4
+ // protoc unknown
5
+ // source: base/v1/language.proto
6
+
7
+ /* eslint-disable */
8
+
9
+ /** 查询语言请求参数。 */
10
+ export interface GetLanguageRequest {
11
+ }
12
+
13
+ /** 支持的语言。 */
14
+ export interface LanguageItem {
15
+ /** 标准语言代码 */
16
+ language_code: string;
17
+ /** 语言名称 */
18
+ language_name: string;
19
+ /** 本地语言名称 */
20
+ native_name: string;
21
+ /** 排序,值越小越靠前 */
22
+ sort: number;
23
+ }
24
+
25
+ /** 当前支持的语言查询结果。 */
26
+ export interface GetLanguageResponse {
27
+ /** 当前支持的语言列表 */
28
+ languages: LanguageItem[];
29
+ /** 主语言代码 */
30
+ primary_language_code: string;
31
+ }
32
+
33
+ /** 语言公共服务。 */
34
+ export interface LanguageService {
35
+ /** 查询当前支持的语言和主语言。 */
36
+ GetLanguage(request: GetLanguageRequest): Promise<GetLanguageResponse>;
37
+ }
@@ -12,3 +12,4 @@ export default pinia
12
12
  // 模块统一导出
13
13
  export * from './modules/user'
14
14
  export * from './modules/setting'
15
+ export * from './modules/locale'
@@ -0,0 +1,28 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed } from 'vue'
3
+ import {
4
+ getCurrentLocale,
5
+ getLanguageOptions,
6
+ getSupportedLocales,
7
+ localeState,
8
+ setCurrentLocale,
9
+ type SupportedLocale,
10
+ } from '../../locales'
11
+
12
+ /** 移动端语言状态。 */
13
+ export const useLocaleStore = defineStore('locale', () => {
14
+ const locale = computed(() => localeState.value)
15
+
16
+ /** 切换当前语言并持久化。 */
17
+ const setLocale = async (value: SupportedLocale) => {
18
+ await setCurrentLocale(value)
19
+ }
20
+
21
+ return {
22
+ locale,
23
+ currentLocale: getCurrentLocale,
24
+ languageOptions: computed(() => getLanguageOptions()),
25
+ supportedLocales: computed(() => getSupportedLocales()),
26
+ setLocale,
27
+ }
28
+ })
@@ -2,11 +2,12 @@ import { defConfigService } from '../../api/base/config'
2
2
  import { defineStore } from 'pinia'
3
3
  import { ref } from 'vue'
4
4
  import { BaseConfigSite } from '../../rpc/base/v1/enum'
5
+ import { t } from '../../locales'
5
6
 
6
7
  const REQUIRED_APP_CONFIGS = [
7
- { key: 'serviceProtocol', name: '服务条款' },
8
- { key: 'privacyProtocol', name: '隐私协议' },
9
- { key: 'captchaType', name: '登录验证码类型' },
8
+ { key: 'serviceProtocol', nameKey: 'core.protocol.service' },
9
+ { key: 'privacyProtocol', nameKey: 'core.protocol.privacy' },
10
+ { key: 'captchaType', nameKey: 'core.login.captchaTypeName' },
10
11
  ] as const
11
12
 
12
13
  export const useSettingStore = defineStore('setting', () => {
@@ -35,7 +36,11 @@ export const useSettingStore = defineStore('setting', () => {
35
36
 
36
37
  const missingConfigs = REQUIRED_APP_CONFIGS.filter(({ key }) => !nextData.get(key))
37
38
  if (missingConfigs.length) {
38
- throw new Error(`移动端配置缺失:${missingConfigs.map(({ name }) => name).join('、')}`)
39
+ throw new Error(
40
+ t('core.config.missing', {
41
+ names: missingConfigs.map(({ nameKey }) => t(nameKey)).join(', '),
42
+ }),
43
+ )
39
44
  }
40
45
 
41
46
  data.value = nextData
package/src/utils/file.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import type { FileInfo, MultiUploadFileResponse } from '../rpc/base/v1/file'
2
2
  import { formatSrc } from './index'
3
+ import { getLocaleRequestHeaders, t } from '../locales'
4
+ import { getRequestAccessToken } from './http'
3
5
 
4
6
  // 文件上传-兼容小程序端、H5端、App端
5
7
  export const uploadFile = async (fileType: string, filePath: string): Promise<FileInfo> => {
8
+ const token = await getRequestAccessToken()
6
9
  const res = await uni.uploadFile({
7
10
  url: '/v1/base/file',
8
11
  name: 'file',
@@ -10,11 +13,16 @@ export const uploadFile = async (fileType: string, filePath: string): Promise<Fi
10
13
  formData: {
11
14
  fileType: fileType,
12
15
  },
16
+ header: {
17
+ ...getLocaleRequestHeaders(),
18
+ 'source-client': 'miniapp',
19
+ ...(token ? { Authorization: token } : {}),
20
+ },
13
21
  })
14
22
  if (res.statusCode === 200) {
15
23
  return JSON.parse(res.data) as FileInfo
16
24
  } else {
17
- throw new Error('上传失败')
25
+ throw new Error(t('core.file.uploadFailed'))
18
26
  }
19
27
  }
20
28
 
@@ -23,35 +31,15 @@ export const uploadFileList = async (
23
31
  fileType: string,
24
32
  filePaths: string[],
25
33
  ): Promise<FileInfo[]> => {
26
- const fileInfoArr: FileInfo[] = []
27
- // 使用 Promise.all 等待所有上传完成
28
- await Promise.all(
29
- filePaths.map(async (filePath) => {
30
- try {
31
- const res = await uni.uploadFile({
32
- url: '/v1/base/file',
33
- name: 'file',
34
- filePath: filePath,
35
- formData: {
36
- fileType: fileType,
37
- },
38
- })
39
- if (res.statusCode === 200) {
40
- const data = JSON.parse(res.data) as FileInfo
41
- fileInfoArr.push(data)
42
- } else {
43
- console.error('上传失败,状态码:', res.statusCode)
44
- }
45
- } catch (error) {
46
- console.error('上传异常:', error)
47
- }
48
- }),
34
+ const results = await Promise.allSettled(
35
+ filePaths.map((filePath) => uploadFile(fileType, filePath)),
49
36
  )
50
- return fileInfoArr
37
+ return results.flatMap((result) => (result.status === 'fulfilled' ? [result.value] : []))
51
38
  }
52
39
 
53
40
  // 多文件上传-兼容小程序端、H5端、App端
54
41
  export const multiUploadFile = async (fileType: string, files: any): Promise<FileInfo[]> => {
42
+ const token = await getRequestAccessToken()
55
43
  const res = await uni.uploadFile({
56
44
  url: '/v1/base/file/multi',
57
45
  name: 'file',
@@ -60,12 +48,17 @@ export const multiUploadFile = async (fileType: string, files: any): Promise<Fil
60
48
  formData: {
61
49
  fileType: fileType,
62
50
  },
51
+ header: {
52
+ ...getLocaleRequestHeaders(),
53
+ 'source-client': 'miniapp',
54
+ ...(token ? { Authorization: token } : {}),
55
+ },
63
56
  })
64
57
  if (res.statusCode === 200) {
65
58
  const data = JSON.parse(res.data) as MultiUploadFileResponse
66
59
  return data.files
67
60
  } else {
68
- await uni.showToast({ icon: 'error', title: '上传失败' })
61
+ await uni.showToast({ icon: 'error', title: t('core.file.uploadFailed') })
69
62
  return []
70
63
  }
71
64
  }
package/src/utils/http.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  shouldRefreshToken,
17
17
  } from './auth'
18
18
  import { saveCurrentRoute } from './navigation'
19
+ import { getLocaleRequestHeaders, t } from '../locales'
19
20
 
20
21
  const apiBasePath = import.meta.env.VITE_APP_BASE_API || '/api'
21
22
  const apiTargetUrl = import.meta.env.VITE_APP_API_URL || ''
@@ -35,6 +36,7 @@ const SESSION_URL = '/v1/base/session'
35
36
  const REFRESH_TOKEN_URL = '/v1/base/token'
36
37
  const CAPTCHA_URL = '/v1/base/captcha'
37
38
  const CONFIG_URL = '/v1/base/config'
39
+ const LANGUAGE_URL = '/v1/base/language'
38
40
  const PASSWORD_PUBLIC_KEY_URL = '/v1/base/password-public-key'
39
41
  const LEGACY_AUTH_URL = '/auth'
40
42
  const LEGACY_REFRESH_TOKEN_URL = `${LEGACY_AUTH_URL}/token`
@@ -45,6 +47,7 @@ const NO_AUTH_URL_SET = new Set([
45
47
  REFRESH_TOKEN_URL,
46
48
  CAPTCHA_URL,
47
49
  CONFIG_URL,
50
+ LANGUAGE_URL,
48
51
  PASSWORD_PUBLIC_KEY_URL,
49
52
  LEGACY_AUTH_URL,
50
53
  LEGACY_CAPTCHA_URL,
@@ -54,6 +57,7 @@ const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
54
57
  SESSION_URL,
55
58
  CAPTCHA_URL,
56
59
  CONFIG_URL,
60
+ LANGUAGE_URL,
57
61
  PASSWORD_PUBLIC_KEY_URL,
58
62
  LEGACY_AUTH_URL,
59
63
  LEGACY_CAPTCHA_URL,
@@ -87,6 +91,7 @@ const httpInterceptor = {
87
91
  options.header = {
88
92
  ...options.header,
89
93
  'source-client': 'miniapp',
94
+ ...getLocaleRequestHeaders(),
90
95
  }
91
96
  // 4. 添加 token 请求头标识
92
97
  const accessToken = getToken()
@@ -219,7 +224,7 @@ export const http = <T>(options: HttpRequestOptions) => {
219
224
  // 其他错误 -> 根据后端错误信息轻提示
220
225
  void uni.showToast({
221
226
  icon: 'none',
222
- title: responseData.message || '请求错误',
227
+ title: responseData.message || t('common.message.requestError'),
223
228
  })
224
229
  reject(res)
225
230
  }
@@ -228,7 +233,7 @@ export const http = <T>(options: HttpRequestOptions) => {
228
233
  fail(err) {
229
234
  void uni.showToast({
230
235
  icon: 'none',
231
- title: '网络错误,换个网络试试',
236
+ title: t('common.message.networkError'),
232
237
  })
233
238
  reject(err)
234
239
  },
@@ -326,6 +331,7 @@ async function refreshAccessToken() {
326
331
  data: { refresh_token: refreshToken },
327
332
  header: {
328
333
  'source-client': 'miniapp',
334
+ ...getLocaleRequestHeaders(),
329
335
  },
330
336
  success: resolve,
331
337
  fail: reject,
@@ -377,10 +383,10 @@ async function promptRelogin() {
377
383
  isPromptingRelogin = true
378
384
  try {
379
385
  const modalRes = await uni.showModal({
380
- title: '提示',
381
- content: '当前页面已失效,请重新登录',
386
+ title: t('common.title.notice'),
387
+ content: t('core.auth.sessionExpired'),
382
388
  showCancel: false,
383
- confirmText: '重新登录',
389
+ confirmText: t('core.auth.loginAgain'),
384
390
  })
385
391
 
386
392
  if (!modalRes.confirm) {
@@ -405,7 +411,7 @@ function handleAuthExpiredByMode(authMode: AuthMode, url: string, responseData:
405
411
  if (authMode !== 'optional') {
406
412
  void uni.showToast({
407
413
  icon: 'none',
408
- title: responseData.message || '请求错误',
414
+ title: responseData.message || t('common.message.requestError'),
409
415
  })
410
416
  }
411
417
  }