@liujitcn/kratos-taro-app-system 0.0.19 → 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.
@@ -3,8 +3,11 @@ import Taro, { getCurrentPages, useLoad } from '@tarojs/taro'
3
3
  import { useEffect, useMemo, useRef, useState } from 'react'
4
4
  import {
5
5
  formatSrc,
6
+ getCurrentLocale,
6
7
  navigateAppRoute,
8
+ t,
7
9
  uploadFile,
10
+ useI18n,
8
11
  } from '@liujitcn/kratos-taro-app-core'
9
12
  import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
10
13
  import { defAiMessageService, StreamAiMessageByChunkedRequest } from '../../../api/base/ai_message'
@@ -50,21 +53,14 @@ type StreamTask = { abort: () => void; aborted: boolean; finished: boolean; succ
50
53
  type MessageMap = Record<string, ChatMessageItem[]>
51
54
 
52
55
  const AI_TERMINAL = Terminal.TERMINAL_APP
53
- const THINKING_MESSAGE_CONTENT = '正在回复'
54
56
  const LOCAL_USER_MESSAGE_PREFIX = 'ai-user-local'
55
57
  const PENDING_MESSAGE_ID = 'pending'
56
58
  const MAX_ATTACHMENT_COUNT = 6
57
59
  const STARTER_PROMPT_PAGE_SIZE = 4
58
60
 
59
- const DEFAULT_SHORTCUTS: AiShortcut[] = [
60
- { key: 'summarize', title: '帮我总结一段内容', prompt: '请帮我总结以下内容', action: undefined, required_tools: [], sort: 1, group: '文本助手' },
61
- { key: 'rewrite', title: '帮我优化一段文字', prompt: '请帮我优化以下文字', action: undefined, required_tools: [], sort: 2, group: '文本助手' },
62
- { key: 'plan', title: '帮我制定一个计划', prompt: '请帮我制定一个清晰的执行计划', action: undefined, required_tools: [], sort: 3, group: '效率助手' },
63
- { key: 'ideas', title: '给我一些灵感', prompt: '请围绕这个主题给我一些新想法', action: undefined, required_tools: [], sort: 4, group: '效率助手' },
64
- ]
65
-
66
61
  /** AI 助手页面。 */
67
62
  export default function AiPage() {
63
+ const { locale } = useI18n()
68
64
  const windowInfo = Taro.getWindowInfo() as ReturnType<typeof Taro.getWindowInfo> & {
69
65
  safeAreaInsets?: { top?: number; bottom?: number }
70
66
  }
@@ -95,7 +91,7 @@ export default function AiPage() {
95
91
  const [sessions, setSessionsState] = useState<AiSession[]>([])
96
92
  const [messages, setMessagesState] = useState<MessageMap>({})
97
93
  const [selectedAttachments, setSelectedAttachments] = useState<AiAttachment[]>([])
98
- const [starterShortcuts, setStarterShortcuts] = useState<AiShortcut[]>(DEFAULT_SHORTCUTS)
94
+ const [starterShortcuts, setStarterShortcuts] = useState<AiShortcut[]>(createDefaultShortcuts)
99
95
 
100
96
  const activeSessionIDRef = useRef('')
101
97
  const sessionsRef = useRef<AiSession[]>([])
@@ -122,15 +118,21 @@ export default function AiPage() {
122
118
  const starterPromptStart = (starterPromptGroupIndex % starterPromptPageCount) * STARTER_PROMPT_PAGE_SIZE
123
119
  const starterPrompts = starterShortcuts.slice(starterPromptStart, starterPromptStart + STARTER_PROMPT_PAGE_SIZE)
124
120
  const hour = new Date().getHours()
125
- const greetingPeriod = hour < 11 ? '上午' : hour < 14 ? '中午' : hour < 18 ? '下午' : '晚上'
126
- const aiGreetingMessage = `您好,${greetingPeriod}好!今天有什么需要我协助的吗?`
121
+ const greetingPeriod = hour < 11
122
+ ? t('system.ai.period.morning')
123
+ : hour < 14
124
+ ? t('system.ai.period.noon')
125
+ : hour < 18
126
+ ? t('system.ai.period.afternoon')
127
+ : t('system.ai.period.evening')
128
+ const aiGreetingMessage = t('system.ai.greeting', { period: greetingPeriod })
127
129
  const composerPlaceholder = isRecording
128
- ? '正在听...'
130
+ ? t('system.ai.recording')
129
131
  : uploadingAttachment
130
- ? '附件上传中...'
132
+ ? t('system.ai.attachmentUploading')
131
133
  : hasMessages
132
- ? '继续输入问题'
133
- : '输入你想了解的内容'
134
+ ? t('system.ai.placeholder.continue')
135
+ : t('system.ai.placeholder.default')
134
136
  const isSubmitDisabled =
135
137
  uploadingAttachment ||
136
138
  currentSessionSending ||
@@ -276,7 +278,10 @@ export default function AiPage() {
276
278
  }
277
279
 
278
280
  const createRemoteSession = async () => {
279
- const response = await defAiSessionService.CreateAiSession({ title: '新会话', terminal: AI_TERMINAL })
281
+ const response = await defAiSessionService.CreateAiSession({
282
+ title: t('system.ai.newSession'),
283
+ terminal: AI_TERMINAL,
284
+ })
280
285
  const session = response.session ? normalizeSession(response.session) : undefined
281
286
  if (!session) return ''
282
287
  upsertSession(session)
@@ -307,7 +312,7 @@ export default function AiPage() {
307
312
  if (activeSessionIDRef.current === sessionID) scrollChatToBottom()
308
313
  } catch (error) {
309
314
  if (loadingSessionIDRef.current === sessionID) updateSessionMessages(sessionID, [])
310
- showError(error, '加载消息失败')
315
+ showError(error, t('system.ai.loadMessagesFailed'))
311
316
  } finally {
312
317
  if (loadingSessionIDRef.current === sessionID) setLoadingSessionID('')
313
318
  }
@@ -322,7 +327,7 @@ export default function AiPage() {
322
327
  const sessionID = await ensureActiveSession()
323
328
  if (sessionID) await loadMessages(sessionID)
324
329
  } catch (error) {
325
- showError(error, '加载会话失败')
330
+ showError(error, t('system.ai.loadSessionsFailed'))
326
331
  } finally {
327
332
  setLoadingSessions(false)
328
333
  }
@@ -339,7 +344,7 @@ export default function AiPage() {
339
344
  setStarterPromptGroupIndex(0)
340
345
  }
341
346
  } catch (error) {
342
- showError(error, '加载快捷助手失败')
347
+ showError(error, t('system.ai.loadShortcutsFailed'))
343
348
  } finally {
344
349
  setLoadingShortcuts(false)
345
350
  }
@@ -368,7 +373,7 @@ export default function AiPage() {
368
373
  handledByStream = true
369
374
  await chunkedTask.promise
370
375
  parser.flush()
371
- if (!task.finished && !task.aborted) throw new Error('AI 助手流式响应未完整返回')
376
+ if (!task.finished && !task.aborted) throw new Error(t('system.ai.responseIncomplete'))
372
377
  }
373
378
  if (
374
379
  process.env.TARO_ENV === 'h5' &&
@@ -388,15 +393,15 @@ export default function AiPage() {
388
393
  }
389
394
  runningStreamTaskMap.current.set(sessionID, task)
390
395
  const response = await defAiMessageService.StreamAiMessage(request, { signal: controller.signal })
391
- if (!response.body) throw new Error('AI 助手流式响应为空')
396
+ if (!response.body) throw new Error(t('system.ai.streamEmpty'))
392
397
  await readAiEventStream(response.body, (event) => handleAiStreamEvent(event, task), controller.signal)
393
- if (!task.finished && !task.aborted) throw new Error('AI 助手流式响应未完整返回')
398
+ if (!task.finished && !task.aborted) throw new Error(t('system.ai.responseIncomplete'))
394
399
  handledByStream = true
395
400
  }
396
401
  if (!handledByStream) {
397
402
  const response = await defAiMessageService.SendAiMessage(request)
398
403
  const nextMessages = normalizeNonStreamMessages(response)
399
- if (!nextMessages.length) throw new Error('AI 助手响应为空')
404
+ if (!nextMessages.length) throw new Error(t('system.ai.responseEmpty'))
400
405
  updateSessionMessages(sessionID, (current) => replacePendingMessages(current, nextMessages))
401
406
  scrollChatToBottom()
402
407
  if (response.session) upsertSession(normalizeSession(response.session))
@@ -407,7 +412,7 @@ export default function AiPage() {
407
412
  if (task?.aborted) return false
408
413
  updateSessionMessages(sessionID, markThinkingMessageFailed)
409
414
  scrollChatToBottom()
410
- showError(error, 'AI 助手请求失败')
415
+ showError(error, t('system.ai.requestFailed'))
411
416
  return false
412
417
  } finally {
413
418
  if (task && runningStreamTaskMap.current.get(sessionID) === task) {
@@ -433,6 +438,12 @@ export default function AiPage() {
433
438
  void ensureSessionsLoaded()
434
439
  })
435
440
 
441
+ useEffect(() => {
442
+ void Taro.setNavigationBarTitle({ title: t('system.ai.chatTitle') })
443
+ setStarterShortcuts(createDefaultShortcuts())
444
+ setStarterPromptGroupIndex(0)
445
+ }, [locale])
446
+
436
447
  useEffect(() => {
437
448
  const tasks = runningStreamTaskMap.current
438
449
  const deltaMap = pendingDeltaMap.current
@@ -468,16 +479,18 @@ export default function AiPage() {
468
479
  setSessionKeyword('')
469
480
  setShowSessionDrawer(false)
470
481
  } catch (error) {
471
- showError(error, '创建会话失败')
482
+ showError(error, t('system.ai.createSessionFailed'))
472
483
  }
473
484
  }
474
485
 
475
486
  const deleteSession = async (sessionID: string) => {
476
487
  const session = sessionsRef.current.find((item) => item.id === sessionID)
477
488
  const result = await Taro.showModal({
478
- title: '删除会话',
479
- content: `是否删除「${session?.title || '当前会话'}」?`,
480
- confirmText: '删除',
489
+ title: t('system.ai.deleteSession'),
490
+ content: t('system.ai.deleteSessionConfirm', {
491
+ title: session?.title || t('system.ai.currentSession'),
492
+ }),
493
+ confirmText: t('common.action.delete'),
481
494
  confirmColor: '#cf4444',
482
495
  })
483
496
  if (!result.confirm) return
@@ -492,13 +505,13 @@ export default function AiPage() {
492
505
  await ensureActiveSession()
493
506
  }
494
507
  } catch (error) {
495
- showError(error, '删除会话失败')
508
+ showError(error, t('system.ai.deleteSessionFailed'))
496
509
  }
497
510
  }
498
511
 
499
512
  const handleSessionAction = async (session: AiSession) => {
500
513
  try {
501
- const result = await Taro.showActionSheet({ itemList: ['删除会话'] })
514
+ const result = await Taro.showActionSheet({ itemList: [t('system.ai.deleteSession')] })
502
515
  if (result.tapIndex === 0) await deleteSession(session.id)
503
516
  } catch {
504
517
  // 用户取消操作无需提示。
@@ -507,7 +520,7 @@ export default function AiPage() {
507
520
 
508
521
  const copyMessage = async (item: ChatMessageItem) => {
509
522
  await Taro.setClipboardData({ data: item.content })
510
- await Taro.showToast({ icon: 'none', title: '消息已复制' })
523
+ await Taro.showToast({ icon: 'none', title: t('system.ai.copySuccess') })
511
524
  }
512
525
 
513
526
  const deleteMessage = async (item: ChatMessageItem) => {
@@ -519,7 +532,7 @@ export default function AiPage() {
519
532
  }
520
533
  updateSessionMessages(sessionID, (current) => current.filter((message) => message.messageID !== item.messageID))
521
534
  } catch (error) {
522
- showError(error, '删除消息失败')
535
+ showError(error, t('system.ai.deleteMessageFailed'))
523
536
  }
524
537
  }
525
538
 
@@ -532,7 +545,7 @@ export default function AiPage() {
532
545
  updateSessionMessages(sessionID, normalizeMessageList(response.messages))
533
546
  if (response.session) upsertSession(normalizeSession(response.session))
534
547
  } catch (error) {
535
- showError(error, '重新生成失败')
548
+ showError(error, t('system.ai.regenerateFailed'))
536
549
  } finally {
537
550
  setSessionSending(sessionID, false)
538
551
  }
@@ -540,7 +553,9 @@ export default function AiPage() {
540
553
 
541
554
  const handleMessageAction = async (item: ChatMessageItem) => {
542
555
  try {
543
- const itemList = item.role === 'ai' ? ['复制', '删除', '重新生成'] : ['复制', '删除']
556
+ const itemList = item.role === 'ai'
557
+ ? [t('system.ai.action.copy'), t('common.action.delete'), t('system.ai.action.regenerate')]
558
+ : [t('system.ai.action.copy'), t('common.action.delete')]
544
559
  const { tapIndex } = await Taro.showActionSheet({ itemList })
545
560
  if (tapIndex === 0) await copyMessage(item)
546
561
  else if (tapIndex === 1) await deleteMessage(item)
@@ -560,7 +575,7 @@ export default function AiPage() {
560
575
 
561
576
  const handleSend = async () => {
562
577
  if (isSubmitDisabled) return
563
- const text = inputText.trim() || '请结合附件内容回答我的问题'
578
+ const text = inputText.trim() || t('system.ai.attachmentAnswer')
564
579
  const attachments = [...selectedAttachments]
565
580
  setInputText('')
566
581
  setSelectedAttachments([])
@@ -570,7 +585,10 @@ export default function AiPage() {
570
585
  const handleAttachment = async () => {
571
586
  if (uploadingAttachment || currentSessionSending) return
572
587
  if (selectedAttachments.length >= MAX_ATTACHMENT_COUNT) {
573
- await Taro.showToast({ icon: 'none', title: `最多上传 ${MAX_ATTACHMENT_COUNT} 个附件` })
588
+ await Taro.showToast({
589
+ icon: 'none',
590
+ title: t('system.ai.attachmentLimit', { count: MAX_ATTACHMENT_COUNT }),
591
+ })
574
592
  return
575
593
  }
576
594
  try {
@@ -580,7 +598,9 @@ export default function AiPage() {
580
598
  })
581
599
  const files: AttachmentUpload[] = result.tempFilePaths.map((path, index) => ({
582
600
  path,
583
- name: result.tempFiles[index]?.originalFileObj?.name || `图片${index + 1}`,
601
+ name:
602
+ result.tempFiles[index]?.originalFileObj?.name ||
603
+ t('system.ai.imageName', { index: index + 1 }),
584
604
  size: Number(result.tempFiles[index]?.size || 0),
585
605
  }))
586
606
  setUploadingAttachment(true)
@@ -595,7 +615,7 @@ export default function AiPage() {
595
615
  setSelectedAttachments((current) => [...current, ...attachments].slice(0, MAX_ATTACHMENT_COUNT))
596
616
  } catch (error) {
597
617
  const message = error instanceof Error ? error.message : String(error)
598
- if (!/cancel/i.test(message)) showError(error, '附件上传失败')
618
+ if (!/cancel/i.test(message)) showError(error, t('system.ai.uploadFailed'))
599
619
  } finally {
600
620
  setUploadingAttachment(false)
601
621
  }
@@ -613,7 +633,7 @@ export default function AiPage() {
613
633
  <Button className='nav-back-button' hoverClass='none' onClick={navigateBack}>
614
634
  <UniIcon type='left' size={24} color='#111' />
615
635
  </Button>
616
- <View className='ai-navbar__title'>AI 助手</View>
636
+ <View className='ai-navbar__title'>{t('system.ai.chatTitle')}</View>
617
637
  <Button className='nav-menu-button' hoverClass='none' onClick={() => setShowSessionDrawer((open) => !open)}>
618
638
  <UniIcon type='bars' size={24} color='#111' />
619
639
  </Button>
@@ -651,7 +671,7 @@ export default function AiPage() {
651
671
  onLongPress={() => void handleMessageAction(item)}
652
672
  >
653
673
  {item.role === 'ai' && item.model ? (
654
- <View className='reply-meta'><Text className='reply-tag'>模型回复</Text><Text className='reply-model'>{item.model}</Text></View>
674
+ <View className='reply-meta'><Text className='reply-tag'>{t('system.ai.modelReply')}</Text><Text className='reply-model'>{item.model}</Text></View>
655
675
  ) : null}
656
676
  <View className='bubble-content'>{item.content}</View>
657
677
  {item.attachments.length ? (
@@ -662,7 +682,7 @@ export default function AiPage() {
662
682
  className='attachment-card'
663
683
  onClick={() => previewAttachment(attachment, item.attachments)}
664
684
  >
665
- <View className='attachment-icon'>{isImageAttachment(attachment) ? '图' : '件'}</View>
685
+ <View className='attachment-icon'>{isImageAttachment(attachment) ? t('system.ai.imageAttachment') : t('system.ai.attachment')}</View>
666
686
  <View className='attachment-info'>
667
687
  <View className='attachment-name'>{attachment.name}</View>
668
688
  <View className='attachment-meta'>{formatAttachmentMeta(attachment)}</View>
@@ -671,14 +691,14 @@ export default function AiPage() {
671
691
  ))}
672
692
  </View>
673
693
  ) : null}
674
- {item.tools.length ? <View className='tool-row'>已调用:{formatTools(item.tools)}</View> : null}
694
+ {item.tools.length ? <View className='tool-row'>{t('system.ai.toolCalled', { tools: formatTools(item.tools) })}</View> : null}
675
695
  </View>
676
696
  </View>
677
697
  ))}
678
698
  <View id='chat-bottom' className='chat-bottom' />
679
699
  </View>
680
700
  )}
681
- {loadingSessionID ? <View className='loading-session'>正在加载消息...</View> : null}
701
+ {loadingSessionID ? <View className='loading-session'>{t('system.ai.loadMessages')}</View> : null}
682
702
  </ScrollView>
683
703
  <Composer
684
704
  value={inputText}
@@ -692,7 +712,10 @@ export default function AiPage() {
692
712
  onAttach={() => void handleAttachment()}
693
713
  onRecord={() => {
694
714
  setIsRecording((recording) => {
695
- void Taro.showToast({ icon: 'none', title: !recording ? '正在识别语音' : '已停止语音输入' })
715
+ void Taro.showToast({
716
+ icon: 'none',
717
+ title: !recording ? t('system.ai.recognizing') : t('system.ai.speechStopped'),
718
+ })
696
719
  return !recording
697
720
  })
698
721
  }}
@@ -719,7 +742,7 @@ export default function AiPage() {
719
742
  function normalizeSession(session?: Partial<AiSession> | null): AiSession {
720
743
  return {
721
744
  id: String(session?.id ?? ''),
722
- title: String(session?.title ?? '新会话'),
745
+ title: String(session?.title ?? t('system.ai.newSession')),
723
746
  summary: String(session?.summary ?? ''),
724
747
  updated_at: session?.updated_at,
725
748
  terminal: Number(session?.terminal ?? AI_TERMINAL),
@@ -821,7 +844,7 @@ function createThinkingMessage(options?: { sessionID?: string; messageID?: strin
821
844
  id: streamKey || `ai-thinking-${now}`,
822
845
  input_content: undefined,
823
846
  output_content: {
824
- kind: 'text', content: THINKING_MESSAGE_CONTENT, reply_source: '', model: '', fallback: false,
847
+ kind: 'text', content: t('system.ai.thinking'), reply_source: '', model: '', fallback: false,
825
848
  fallback_reason: '', flow: '', step: '', blocks_json: '',
826
849
  },
827
850
  attachments: [],
@@ -859,7 +882,7 @@ function appendStreamingDelta(current: ChatMessageItem[], payload: AiStreamPaylo
859
882
  const streamKey = buildStreamMessageKey(payload.session_id, payload.message_id)
860
883
  return current.map((item) => {
861
884
  if (item.streamKey !== streamKey || item.role === 'user') return item
862
- const baseContent = item.content === THINKING_MESSAGE_CONTENT ? '' : item.content
885
+ const baseContent = item.content === t('system.ai.thinking') ? '' : item.content
863
886
  return { ...item, content: `${baseContent}${payload.delta}`, status: AiMessageStatus.GENERATING_AMS }
864
887
  })
865
888
  }
@@ -870,7 +893,7 @@ function markThinkingMessageFailed(current: ChatMessageItem[]) {
870
893
  ? {
871
894
  ...item,
872
895
  status: AiMessageStatus.FAILED_AMS,
873
- content: item.role === 'ai' ? '这次回复没有成功返回,你可以直接重试刚才的问题。' : item.content,
896
+ content: item.role === 'ai' ? t('system.ai.failedResponse') : item.content,
874
897
  }
875
898
  : item,
876
899
  )
@@ -880,7 +903,7 @@ function markStreamingError(current: ChatMessageItem[], payload: AiStreamPayload
880
903
  const streamKey = buildStreamMessageKey(payload.session_id, payload.message_id)
881
904
  return current.map((item) =>
882
905
  item.localOnly && item.streamKey === streamKey
883
- ? { ...item, status: AiMessageStatus.FAILED_AMS, content: '这次回复没有成功返回,你可以直接重试刚才的问题。' }
906
+ ? { ...item, status: AiMessageStatus.FAILED_AMS, content: t('system.ai.failedResponse') }
884
907
  : item,
885
908
  )
886
909
  }
@@ -908,7 +931,9 @@ function normalizeNonStreamMessages(response: unknown) {
908
931
  const events = parseAiEventStreamText(response)
909
932
  const finishEvent = [...events].reverse().find((item) => item.event === 'finish')
910
933
  if (finishEvent) return normalizeMessageList(finishEvent.payload.messages)
911
- if ([...events].reverse().some((item) => item.event === 'error')) throw new Error('AI 助手请求失败')
934
+ if ([...events].reverse().some((item) => item.event === 'error')) {
935
+ throw new Error(t('system.ai.requestFailed'))
936
+ }
912
937
  return []
913
938
  }
914
939
 
@@ -917,7 +942,7 @@ function sortMessages(list: ChatMessageItem[]) {
917
942
  const difference = resolveTimestamp(left.created_at) - resolveTimestamp(right.created_at)
918
943
  if (difference) return difference
919
944
  if (left.role !== right.role) return left.role === 'user' ? -1 : 1
920
- return left.messageID.localeCompare(right.messageID, 'zh-Hans-CN', { numeric: true })
945
+ return left.messageID.localeCompare(right.messageID, getCurrentLocale(), { numeric: true })
921
946
  })
922
947
  }
923
948
 
@@ -938,7 +963,9 @@ function isImageAttachment(attachment: AiAttachment) {
938
963
  }
939
964
 
940
965
  function formatAttachmentMeta(attachment: AiAttachment) {
941
- return attachment.size ? `${Math.max(1, Math.round(attachment.size / 1024))} KB` : '附件'
966
+ return attachment.size
967
+ ? `${Math.max(1, Math.round(attachment.size / 1024))} KB`
968
+ : t('system.ai.attachment')
942
969
  }
943
970
 
944
971
  function formatTools(tools: AiToolCall[]) {
@@ -949,3 +976,44 @@ function showError(error: unknown, fallback: string) {
949
976
  const message = error instanceof Error ? error.message : fallback
950
977
  void Taro.showToast({ icon: 'none', title: message || fallback })
951
978
  }
979
+
980
+ function createDefaultShortcuts(): AiShortcut[] {
981
+ return [
982
+ {
983
+ key: 'summarize',
984
+ title: t('system.ai.prompt.summary'),
985
+ prompt: t('system.ai.prompt.summaryContent'),
986
+ action: undefined,
987
+ required_tools: [],
988
+ sort: 1,
989
+ group: t('system.ai.textAssistant'),
990
+ },
991
+ {
992
+ key: 'rewrite',
993
+ title: t('system.ai.prompt.optimize'),
994
+ prompt: t('system.ai.prompt.optimizeContent'),
995
+ action: undefined,
996
+ required_tools: [],
997
+ sort: 2,
998
+ group: t('system.ai.textAssistant'),
999
+ },
1000
+ {
1001
+ key: 'plan',
1002
+ title: t('system.ai.prompt.plan'),
1003
+ prompt: t('system.ai.prompt.planContent'),
1004
+ action: undefined,
1005
+ required_tools: [],
1006
+ sort: 3,
1007
+ group: t('system.ai.efficiencyAssistant'),
1008
+ },
1009
+ {
1010
+ key: 'ideas',
1011
+ title: t('system.ai.prompt.idea'),
1012
+ prompt: t('system.ai.prompt.ideaContent'),
1013
+ action: undefined,
1014
+ required_tools: [],
1015
+ sort: 4,
1016
+ group: t('system.ai.efficiencyAssistant'),
1017
+ },
1018
+ ]
1019
+ }
@@ -9,7 +9,7 @@ import {
9
9
  View,
10
10
  } from '@tarojs/components'
11
11
  import Taro, { useLoad } from '@tarojs/taro'
12
- import { useState } from 'react'
12
+ import { useEffect, useState } from 'react'
13
13
  import defaultAvatar from '@liujitcn/kratos-taro-app-core/static/images/avatar.png'
14
14
  import navigatorBackground from '@liujitcn/kratos-taro-app-core/static/images/navigator_bg.png'
15
15
  import { defAuthService } from '@liujitcn/kratos-taro-app-core/api/system/auth'
@@ -20,6 +20,7 @@ import {
20
20
  formatSrc,
21
21
  navigateToLogin,
22
22
  uploadFile,
23
+ useI18n,
23
24
  useUserStore,
24
25
  } from '@liujitcn/kratos-taro-app-core'
25
26
  import './profile.scss'
@@ -28,6 +29,7 @@ const IMG_MAX_SIZE = 1024 * 1024
28
29
 
29
30
  /** 用户个人资料编辑页。 */
30
31
  export default function ProfilePage() {
32
+ const { locale, t } = useI18n()
31
33
  const [userInfo, setUserInfo] = useState<UserProfileForm>({
32
34
  user_name: '',
33
35
  nick_name: '',
@@ -39,6 +41,10 @@ export default function ProfilePage() {
39
41
  const ensureAuthenticated = useUserStore((state) => state.ensureAuthenticated)
40
42
  const safeTop = Taro.getWindowInfo().safeArea?.top || 0
41
43
 
44
+ useEffect(() => {
45
+ void Taro.setNavigationBarTitle({ title: t('system.profile.title') })
46
+ }, [locale, t])
47
+
42
48
  const requireAuth = () => {
43
49
  if (ensureAuthenticated()) return true
44
50
  navigateToLogin()
@@ -66,9 +72,9 @@ export default function ProfilePage() {
66
72
  await defAuthService.UpdateUserProfile(nextProfile)
67
73
  setUserInfo(nextProfile)
68
74
  await useUserStore.getState().getUserProfile()
69
- await Taro.showToast({ icon: 'success', title: '更新成功' })
75
+ await Taro.showToast({ icon: 'success', title: t('system.profile.updateSuccess') })
70
76
  } catch {
71
- await Taro.showToast({ icon: 'error', title: '上传头像失败' })
77
+ await Taro.showToast({ icon: 'error', title: t('system.profile.avatarUploadFailed') })
72
78
  }
73
79
  }
74
80
 
@@ -81,7 +87,7 @@ export default function ProfilePage() {
81
87
  const file = selected.tempFiles[0]
82
88
  const filePath = 'tempFilePath' in file ? file.tempFilePath : file.path
83
89
  if (file.size > IMG_MAX_SIZE) {
84
- await Taro.showToast({ title: '请上传小于1M的照片', icon: 'none', duration: 1500 })
90
+ await Taro.showToast({ title: t('system.profile.photoLimit'), icon: 'none', duration: 1500 })
85
91
  return
86
92
  }
87
93
  await uploadAvatar(filePath)
@@ -92,14 +98,14 @@ export default function ProfilePage() {
92
98
  const response = await defAuthService.BindUserPhone({ code: event.detail.code || '' })
93
99
  setUserInfo((current) => ({ ...current, phone: response.phone }))
94
100
  await useUserStore.getState().getUserProfile()
95
- await Taro.showToast({ icon: 'success', title: '授权成功' })
101
+ await Taro.showToast({ icon: 'success', title: t('system.profile.phoneAuthorizationSuccess') })
96
102
  }
97
103
 
98
104
  const onSubmit = async () => {
99
105
  if (!requireAuth()) return
100
106
  await defAuthService.UpdateUserProfile(userInfo)
101
107
  await useUserStore.getState().getUserProfile()
102
- await Taro.showToast({ icon: 'success', title: '保存成功' })
108
+ await Taro.showToast({ icon: 'success', title: t('system.profile.saveSuccess') })
103
109
  setTimeout(() => void Taro.navigateBack(), 400)
104
110
  }
105
111
 
@@ -107,40 +113,40 @@ export default function ProfilePage() {
107
113
  <View className='profile-viewport' style={{ backgroundImage: `url(${navigatorBackground})` }}>
108
114
  <View className='profile-navbar' style={{ paddingTop: `${safeTop}px` }}>
109
115
  <View className='profile-back' onClick={() => void Taro.navigateBack()}>‹</View>
110
- <View className='profile-navbar__title'>个人信息</View>
116
+ <View className='profile-navbar__title'>{t('system.profile.title')}</View>
111
117
  </View>
112
118
  <View className='profile-avatar'>
113
119
  <View className='profile-avatar__content' onClick={() => void onAvatarChange()}>
114
120
  <Image className='profile-avatar__image' src={userInfo.avatar ? formatSrc(userInfo.avatar) : defaultAvatar} mode='aspectFill' />
115
- <Text className='profile-avatar__text'>点击修改头像</Text>
121
+ <Text className='profile-avatar__text'>{t('system.profile.avatarChange')}</Text>
116
122
  </View>
117
123
  </View>
118
124
  <View className='profile-form'>
119
125
  <View className='profile-form__content'>
120
126
  {userInfo.user_name ? (
121
127
  <View className='profile-form__item'>
122
- <Text className='profile-label'>账号</Text>
128
+ <Text className='profile-label'>{t('system.profile.account')}</Text>
123
129
  <Text className='profile-account profile-placeholder'>{userInfo.user_name}</Text>
124
130
  </View>
125
131
  ) : null}
126
132
  {process.env.TARO_ENV === 'weapp' ? (
127
133
  <View className='profile-form__item'>
128
- <Text className='profile-label'>手机号</Text>
134
+ <Text className='profile-label'>{t('system.profile.mobile')}</Text>
129
135
  <View className='profile-input'>
130
136
  {userInfo.phone ? (
131
137
  <Text className='profile-account'>{userInfo.phone}</Text>
132
138
  ) : (
133
- <Button className='profile-auth-button' openType='getPhoneNumber' onGetPhoneNumber={onGetPhoneNumber}>微信授权手机号</Button>
139
+ <Button className='profile-auth-button' openType='getPhoneNumber' onGetPhoneNumber={onGetPhoneNumber}>{t('system.profile.phoneAuthorization')}</Button>
134
140
  )}
135
141
  </View>
136
142
  </View>
137
143
  ) : null}
138
144
  <View className='profile-form__item'>
139
- <Text className='profile-label'>昵称</Text>
140
- <Input className='profile-input' placeholder='请填写昵称' value={userInfo.nick_name} onInput={(event) => setUserInfo((current) => ({ ...current, nick_name: event.detail.value }))} />
145
+ <Text className='profile-label'>{t('system.profile.nickName')}</Text>
146
+ <Input className='profile-input' placeholder={t('system.profile.nickNamePlaceholder')} value={userInfo.nick_name} onInput={(event) => setUserInfo((current) => ({ ...current, nick_name: event.detail.value }))} />
141
147
  </View>
142
148
  <View className='profile-form__item'>
143
- <Text className='profile-label'>性别</Text>
149
+ <Text className='profile-label'>{t('system.profile.gender')}</Text>
144
150
  <RadioGroup onChange={(event) => setUserInfo((current) => ({ ...current, gender: Number(event.detail.value) }))}>
145
151
  {genderList.map((item) => (
146
152
  <Label className='profile-radio' key={item.value}>
@@ -151,7 +157,7 @@ export default function ProfilePage() {
151
157
  </RadioGroup>
152
158
  </View>
153
159
  </View>
154
- <Button className='profile-form__button' onClick={() => void onSubmit()}>保 存</Button>
160
+ <Button className='profile-form__button' onClick={() => void onSubmit()}>{t('common.action.save')}</Button>
155
161
  </View>
156
162
  </View>
157
163
  )
@@ -49,6 +49,16 @@
49
49
  text-align: center;
50
50
  }
51
51
 
52
+ .settings-locale {
53
+ display: flex;
54
+ justify-content: space-between;
55
+ padding-right: 42px;
56
+ }
57
+
58
+ .settings-locale__value {
59
+ color: #888;
60
+ }
61
+
52
62
  .settings-button {
53
63
  margin-bottom: 20px;
54
64
  border-radius: 10px;