@liujitcn/kratos-taro-app-system 0.0.31 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  | 页面 | 视图键 | 用途 |
8
8
  | --- | --- | --- |
9
9
  | `pages/my/my` | `PROFILE_HOME` | 我的 tab 与登录概览。 |
10
- | `pagesMember/profile/profile` | `PROFILE` | 头像、昵称、性别和地区资料。 |
10
+ | `pagesMember/profile/profile` | `PROFILE` | 头像、昵称、性别、邮箱和证件资料。 |
11
11
  | `pagesMember/settings/settings` | `SETTINGS` | MFA 状态、设置与退出登录。 |
12
12
  | `pagesMember/ai/index` | `AI` | AI 会话、流式消息、附件和快捷入口。 |
13
13
 
@@ -21,7 +21,7 @@ AI 页面支持会话创建与切换、历史会话搜索、消息加载、附
21
21
 
22
22
  ## MFA
23
23
 
24
- 设置页通过 `@liujitcn/kratos-taro-app-core/api/base/mfa` 查询状态并发起绑定或禁用;页面私有的 `components/PasswordVerifyDialog.tsx` 提供 `setup`、`disable` 两种模式。未启用 MFA 时组件只显示密码输入,已启用 MFA 时在同一组件内同时完成密码和 MFA 校验,绑定成功展示一次性恢复码,禁用成功后清理会话并重新登录。
24
+ 设置页通过 `@liujitcn/kratos-taro-app-core/api/base/v1/mfa` 查询状态并发起绑定或禁用;页面私有的 `components/PasswordVerifyDialog.tsx` 提供 `setup`、`disable` 两种模式。未启用 MFA 时组件只显示密码输入,已启用 MFA 时在同一组件内同时完成密码和 MFA 校验,绑定成功展示一次性恢复码,禁用成功后清理会话并重新登录。
25
25
 
26
26
  ## 验证
27
27
 
package/dist/build.mjs CHANGED
@@ -26,7 +26,12 @@ var systemPages = {
26
26
  navigationBarTitleText: ""
27
27
  }
28
28
  },
29
- "pagesMember/message/index": { style: { navigationBarTitleText: "" } },
29
+ "pagesMember/message/index": {
30
+ style: {
31
+ navigationStyle: "custom",
32
+ navigationBarTitleText: ""
33
+ }
34
+ },
30
35
  "pagesMember/message/detail": { style: { navigationBarTitleText: "" } }
31
36
  };
32
37
 
package/dist/module.mjs CHANGED
@@ -24,7 +24,12 @@ var systemPages = {
24
24
  navigationBarTitleText: ""
25
25
  }
26
26
  },
27
- "pagesMember/message/index": { style: { navigationBarTitleText: "" } },
27
+ "pagesMember/message/index": {
28
+ style: {
29
+ navigationStyle: "custom",
30
+ navigationBarTitleText: ""
31
+ }
32
+ },
28
33
  "pagesMember/message/detail": { style: { navigationBarTitleText: "" } }
29
34
  };
30
35
 
@@ -99,6 +104,20 @@ var zh_CN_default = {
99
104
  "system.profile.avatar_upload_failed": "\u4E0A\u4F20\u5934\u50CF\u5931\u8D25",
100
105
  "system.profile.avatar_update": "\u66F4\u65B0\u5934\u50CF\u6635\u79F0",
101
106
  "system.profile.gender": "\u6027\u522B",
107
+ "system.profile.email": "\u90AE\u7BB1",
108
+ "system.profile.email_placeholder": "\u8BF7\u8F93\u5165\u90AE\u7BB1",
109
+ "system.profile.email_invalid": "\u8BF7\u8F93\u5165\u6B63\u786E\u7684\u90AE\u7BB1",
110
+ "system.profile.id_type": "\u8BC1\u4EF6\u7C7B\u578B",
111
+ "system.profile.id_code": "\u8BC1\u4EF6\u53F7",
112
+ "system.profile.id_code_placeholder": "\u8BF7\u8F93\u5165\u8BC1\u4EF6\u53F7",
113
+ "system.profile.id_code_invalid": "\u8BF7\u8F93\u5165\u6B63\u786E\u7684\u8BC1\u4EF6\u53F7",
114
+ "system.profile.id_type.unspecified": "\u672A\u6307\u5B9A",
115
+ "system.profile.id_type.id_card": "\u5C45\u6C11\u8EAB\u4EFD\u8BC1",
116
+ "system.profile.id_type.passport": "\u62A4\u7167",
117
+ "system.profile.id_type.hk_macao_permit": "\u6E2F\u6FB3\u5C45\u6C11\u6765\u5F80\u5185\u5730\u901A\u884C\u8BC1",
118
+ "system.profile.id_type.taiwan_permit": "\u53F0\u6E7E\u5C45\u6C11\u6765\u5F80\u5927\u9646\u901A\u884C\u8BC1",
119
+ "system.profile.id_type.foreign_permanent_residence": "\u5916\u56FD\u4EBA\u6C38\u4E45\u5C45\u7559\u8EAB\u4EFD\u8BC1",
120
+ "system.profile.id_type.other": "\u5176\u4ED6\u8BC1\u4EF6",
102
121
  "system.profile.login_prompt": "\u70B9\u51FB\u767B\u5F55\u8D26\u53F7",
103
122
  "system.profile.mobile": "\u624B\u673A\u53F7",
104
123
  "system.profile.nick_name": "\u6635\u79F0",
@@ -216,6 +235,20 @@ var en_US_default = {
216
235
  "system.profile.avatar_upload_failed": "Avatar upload failed",
217
236
  "system.profile.avatar_update": "Update avatar and name",
218
237
  "system.profile.gender": "Gender",
238
+ "system.profile.email": "Email",
239
+ "system.profile.email_placeholder": "Enter email",
240
+ "system.profile.email_invalid": "Enter a valid email",
241
+ "system.profile.id_type": "ID type",
242
+ "system.profile.id_code": "ID number",
243
+ "system.profile.id_code_placeholder": "Enter ID number",
244
+ "system.profile.id_code_invalid": "Enter a valid ID number",
245
+ "system.profile.id_type.unspecified": "Unspecified",
246
+ "system.profile.id_type.id_card": "Resident ID card",
247
+ "system.profile.id_type.passport": "Passport",
248
+ "system.profile.id_type.hk_macao_permit": "Mainland travel permit for Hong Kong/Macao residents",
249
+ "system.profile.id_type.taiwan_permit": "Mainland travel permit for Taiwan residents",
250
+ "system.profile.id_type.foreign_permanent_residence": "Foreign permanent residence ID",
251
+ "system.profile.id_type.other": "Other ID",
219
252
  "system.profile.login_prompt": "Tap to log in",
220
253
  "system.profile.mobile": "Mobile",
221
254
  "system.profile.nick_name": "Nickname",
@@ -333,6 +366,20 @@ var ja_JP_default = {
333
366
  "system.profile.avatar_upload_failed": "\u30A2\u30D0\u30BF\u30FC\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
334
367
  "system.profile.avatar_update": "\u30A2\u30D0\u30BF\u30FC\u306E\u30CB\u30C3\u30AF\u30CD\u30FC\u30E0\u3092\u66F4\u65B0",
335
368
  "system.profile.gender": "\u6027\u5225",
369
+ "system.profile.email": "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9",
370
+ "system.profile.email_placeholder": "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
371
+ "system.profile.email_invalid": "\u6B63\u3057\u3044\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
372
+ "system.profile.id_type": "\u8EAB\u5206\u8A3C\u660E\u66F8\u306E\u7A2E\u985E",
373
+ "system.profile.id_code": "\u8EAB\u5206\u8A3C\u660E\u66F8\u756A\u53F7",
374
+ "system.profile.id_code_placeholder": "\u8EAB\u5206\u8A3C\u660E\u66F8\u756A\u53F7\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
375
+ "system.profile.id_code_invalid": "\u6B63\u3057\u3044\u8EAB\u5206\u8A3C\u660E\u66F8\u756A\u53F7\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
376
+ "system.profile.id_type.unspecified": "\u672A\u6307\u5B9A",
377
+ "system.profile.id_type.id_card": "\u8EAB\u5206\u8A3C\u660E\u66F8",
378
+ "system.profile.id_type.passport": "\u30D1\u30B9\u30DD\u30FC\u30C8",
379
+ "system.profile.id_type.hk_macao_permit": "\u9999\u6E2F\u30FB\u30DE\u30AB\u30AA\u4F4F\u6C11\u5185\u5730\u901A\u884C\u8A3C",
380
+ "system.profile.id_type.taiwan_permit": "\u53F0\u6E7E\u4F4F\u6C11\u5927\u9678\u901A\u884C\u8A3C",
381
+ "system.profile.id_type.foreign_permanent_residence": "\u5916\u56FD\u4EBA\u6C38\u4E45\u5C45\u7559\u8EAB\u5206\u8A3C\u660E\u66F8",
382
+ "system.profile.id_type.other": "\u305D\u306E\u4ED6",
336
383
  "system.profile.login_prompt": "\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u30A2\u30AB\u30A6\u30F3\u30C8\u306B\u30ED\u30B0\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044",
337
384
  "system.profile.mobile": "\u643A\u5E2F\u96FB\u8A71\u756A\u53F7",
338
385
  "system.profile.nick_name": "\u30CB\u30C3\u30AF\u30CD\u30FC\u30E0",
@@ -450,6 +497,20 @@ var zh_TW_default = {
450
497
  "system.profile.avatar_upload_failed": "\u4E0A\u50B3\u982D\u50CF\u5931\u6557",
451
498
  "system.profile.avatar_update": "\u66F4\u65B0\u982D\u50CF\u66B1\u7A31",
452
499
  "system.profile.gender": "\u6027\u5225",
500
+ "system.profile.email": "\u96FB\u5B50\u90F5\u4EF6",
501
+ "system.profile.email_placeholder": "\u8ACB\u8F38\u5165\u96FB\u5B50\u90F5\u4EF6",
502
+ "system.profile.email_invalid": "\u8ACB\u8F38\u5165\u6B63\u78BA\u7684\u96FB\u5B50\u90F5\u4EF6",
503
+ "system.profile.id_type": "\u8B49\u4EF6\u985E\u578B",
504
+ "system.profile.id_code": "\u8B49\u4EF6\u865F",
505
+ "system.profile.id_code_placeholder": "\u8ACB\u8F38\u5165\u8B49\u4EF6\u865F",
506
+ "system.profile.id_code_invalid": "\u8ACB\u8F38\u5165\u6B63\u78BA\u7684\u8B49\u4EF6\u865F",
507
+ "system.profile.id_type.unspecified": "\u672A\u6307\u5B9A",
508
+ "system.profile.id_type.id_card": "\u5C45\u6C11\u8EAB\u5206\u8B49",
509
+ "system.profile.id_type.passport": "\u8B77\u7167",
510
+ "system.profile.id_type.hk_macao_permit": "\u6E2F\u6FB3\u5C45\u6C11\u4F86\u5F80\u5167\u5730\u901A\u884C\u8B49",
511
+ "system.profile.id_type.taiwan_permit": "\u81FA\u7063\u5C45\u6C11\u4F86\u5F80\u5927\u9678\u901A\u884C\u8B49",
512
+ "system.profile.id_type.foreign_permanent_residence": "\u5916\u570B\u4EBA\u6C38\u4E45\u5C45\u7559\u8EAB\u5206\u8B49",
513
+ "system.profile.id_type.other": "\u5176\u4ED6\u8B49\u4EF6",
453
514
  "system.profile.login_prompt": "\u9EDE\u9078\u767B\u5165\u8CEC\u865F",
454
515
  "system.profile.mobile": "\u624B\u6A5F\u865F",
455
516
  "system.profile.nick_name": "\u66B1\u7A31",
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "type": "git",
8
8
  "url": "git@github.com:liujitcn/kratos-admin.git"
9
9
  },
10
- "version": "0.0.31",
10
+ "version": "0.0.33",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -38,8 +38,8 @@
38
38
  "@tarojs/components": "4.2.1",
39
39
  "@tarojs/taro": "4.2.1",
40
40
  "react": "18.3.1",
41
- "@liujitcn/kratos-taro-app-core": "^0.0.31",
42
- "@liujitcn/kratos-taro-app-ui": "^0.0.31"
41
+ "@liujitcn/kratos-taro-app-core": "^0.0.33",
42
+ "@liujitcn/kratos-taro-app-ui": "^0.0.33"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "pnpm run build:entries && pnpm pack --pack-destination ../../../dist/npm",
@@ -6,7 +6,7 @@ import {
6
6
  } from '@liujitcn/kratos-taro-app-core/utils/http'
7
7
  import { getLocaleRequestHeaders, t } from '@liujitcn/kratos-taro-app-core'
8
8
  import Taro from '@tarojs/taro'
9
- import type { ListAiMessageRequest, ListAiMessageResponse } from '../../rpc/base/v1/ai_session'
9
+ import type { ListAiMessageRequest, ListAiMessageResponse } from '../../../rpc/base/v1/ai_session'
10
10
  import type {
11
11
  AiMessageService,
12
12
  DeleteAiMessageRequest,
@@ -16,7 +16,7 @@ import type {
16
16
  SendAiMessageRequest,
17
17
  SendAiMessageResponse,
18
18
  UpdateAiMessageRequest,
19
- } from '../../rpc/base/v1/ai_message'
19
+ } from '../../../rpc/base/v1/ai_message'
20
20
 
21
21
  const AI_SESSION_URL = '/v1/base/ai/session'
22
22
 
@@ -13,7 +13,7 @@ import type {
13
13
  ListAiSessionResponse,
14
14
  UpdateAiSessionRequest,
15
15
  UpdateAiSessionResponse,
16
- } from '../../rpc/base/v1/ai_session'
16
+ } from '../../../rpc/base/v1/ai_session'
17
17
 
18
18
  const AI_SESSION_URL = '/v1/base/ai/session'
19
19
 
@@ -3,7 +3,7 @@ import type {
3
3
  AiToolService,
4
4
  ListAiShortcutRequest,
5
5
  ListAiShortcutResponse,
6
- } from '../../rpc/base/v1/ai_tool'
6
+ } from '../../../rpc/base/v1/ai_tool'
7
7
 
8
8
  const AI_SHORTCUT_URL = '/v1/base/ai/shortcut'
9
9
 
@@ -4,6 +4,8 @@ import type {
4
4
  DeleteNotificationRequest,
5
5
  GetNotificationRequest,
6
6
  GetNotificationSummaryRequest,
7
+ ListNotificationCategoriesRequest,
8
+ ListNotificationCategoriesResponse,
7
9
  MarkAllNotificationReadRequest,
8
10
  MarkNotificationReadRequest,
9
11
  MarkNotificationUnreadRequest,
@@ -13,8 +15,8 @@ import type {
13
15
  PageNotificationRequest,
14
16
  PageNotificationResponse,
15
17
  RestoreNotificationRequest,
16
- } from '../../rpc/base/v1/notification'
17
- import type { Empty } from '../../rpc/google/protobuf/empty'
18
+ } from '../../../rpc/base/v1/notification'
19
+ import type { Empty } from '../../../rpc/google/protobuf/empty'
18
20
 
19
21
  const NOTIFICATION_URL = '/v1/base/notification'
20
22
 
@@ -22,7 +24,35 @@ const NOTIFICATION_URL = '/v1/base/notification'
22
24
  export class NotificationServiceImpl implements NotificationService {
23
25
  /** 分页查询收件箱。 */
24
26
  PageNotification(request: PageNotificationRequest): Promise<PageNotificationResponse> {
25
- return http({ url: NOTIFICATION_URL, method: 'GET', authMode: 'required', data: request })
27
+ const data = Object.fromEntries(
28
+ Object.entries(request).filter(([, value]) => value !== undefined),
29
+ )
30
+ return http<Partial<PageNotificationResponse>>({
31
+ url: NOTIFICATION_URL,
32
+ method: 'GET',
33
+ authMode: 'required',
34
+ data,
35
+ }).then((response) => ({
36
+ ...response,
37
+ notifications: Array.isArray(response.notifications) ? response.notifications : [],
38
+ total: response.total ?? 0,
39
+ next_cursor_id: response.next_cursor_id ?? 0,
40
+ has_more: response.has_more ?? false,
41
+ }))
42
+ }
43
+ /** 查询当前可用的消息分类。 */
44
+ ListNotificationCategories(
45
+ request: ListNotificationCategoriesRequest,
46
+ ): Promise<ListNotificationCategoriesResponse> {
47
+ return http<Partial<ListNotificationCategoriesResponse>>({
48
+ url: `${NOTIFICATION_URL}/categories`,
49
+ method: 'GET',
50
+ authMode: 'required',
51
+ data: request,
52
+ }).then((response) => ({
53
+ ...response,
54
+ categories: Array.isArray(response.categories) ? response.categories : [],
55
+ }))
26
56
  }
27
57
  /** 查询消息详情。 */
28
58
  GetNotification(request: GetNotificationRequest): Promise<Notification> {
@@ -30,7 +60,17 @@ export class NotificationServiceImpl implements NotificationService {
30
60
  }
31
61
  /** 查询未读汇总。 */
32
62
  GetNotificationSummary(request: GetNotificationSummaryRequest): Promise<NotificationSummary> {
33
- return http({ url: `${NOTIFICATION_URL}/summary`, method: 'GET', authMode: 'required', data: request })
63
+ return http<Partial<NotificationSummary>>({
64
+ url: `${NOTIFICATION_URL}/summary`,
65
+ method: 'GET',
66
+ authMode: 'required',
67
+ data: request,
68
+ }).then((response) => ({
69
+ ...response,
70
+ unread_total: response.unread_total ?? 0,
71
+ latest_delivery_id: response.latest_delivery_id ?? 0,
72
+ category_unread: Array.isArray(response.category_unread) ? response.category_unread : [],
73
+ }))
34
74
  }
35
75
  /** 标记消息已读。 */
36
76
  MarkNotificationRead(request: MarkNotificationReadRequest): Promise<Empty> {
@@ -68,6 +68,20 @@
68
68
  "system.profile.avatar_upload_failed": "Avatar upload failed",
69
69
  "system.profile.avatar_update": "Update avatar and name",
70
70
  "system.profile.gender": "Gender",
71
+ "system.profile.email": "Email",
72
+ "system.profile.email_placeholder": "Enter email",
73
+ "system.profile.email_invalid": "Enter a valid email",
74
+ "system.profile.id_type": "ID type",
75
+ "system.profile.id_code": "ID number",
76
+ "system.profile.id_code_placeholder": "Enter ID number",
77
+ "system.profile.id_code_invalid": "Enter a valid ID number",
78
+ "system.profile.id_type.unspecified": "Unspecified",
79
+ "system.profile.id_type.id_card": "Resident ID card",
80
+ "system.profile.id_type.passport": "Passport",
81
+ "system.profile.id_type.hk_macao_permit": "Mainland travel permit for Hong Kong/Macao residents",
82
+ "system.profile.id_type.taiwan_permit": "Mainland travel permit for Taiwan residents",
83
+ "system.profile.id_type.foreign_permanent_residence": "Foreign permanent residence ID",
84
+ "system.profile.id_type.other": "Other ID",
71
85
  "system.profile.login_prompt": "Tap to log in",
72
86
  "system.profile.mobile": "Mobile",
73
87
  "system.profile.nick_name": "Nickname",
@@ -68,6 +68,20 @@
68
68
  "system.profile.avatar_upload_failed": "アバターのアップロードに失敗しました",
69
69
  "system.profile.avatar_update": "アバターのニックネームを更新",
70
70
  "system.profile.gender": "性別",
71
+ "system.profile.email": "メールアドレス",
72
+ "system.profile.email_placeholder": "メールアドレスを入力してください",
73
+ "system.profile.email_invalid": "正しいメールアドレスを入力してください",
74
+ "system.profile.id_type": "身分証明書の種類",
75
+ "system.profile.id_code": "身分証明書番号",
76
+ "system.profile.id_code_placeholder": "身分証明書番号を入力してください",
77
+ "system.profile.id_code_invalid": "正しい身分証明書番号を入力してください",
78
+ "system.profile.id_type.unspecified": "未指定",
79
+ "system.profile.id_type.id_card": "身分証明書",
80
+ "system.profile.id_type.passport": "パスポート",
81
+ "system.profile.id_type.hk_macao_permit": "香港・マカオ住民内地通行証",
82
+ "system.profile.id_type.taiwan_permit": "台湾住民大陸通行証",
83
+ "system.profile.id_type.foreign_permanent_residence": "外国人永久居留身分証明書",
84
+ "system.profile.id_type.other": "その他",
71
85
  "system.profile.login_prompt": "クリックしてアカウントにログインしてください",
72
86
  "system.profile.mobile": "携帯電話番号",
73
87
  "system.profile.nick_name": "ニックネーム",
@@ -68,6 +68,20 @@
68
68
  "system.profile.avatar_upload_failed": "上传头像失败",
69
69
  "system.profile.avatar_update": "更新头像昵称",
70
70
  "system.profile.gender": "性别",
71
+ "system.profile.email": "邮箱",
72
+ "system.profile.email_placeholder": "请输入邮箱",
73
+ "system.profile.email_invalid": "请输入正确的邮箱",
74
+ "system.profile.id_type": "证件类型",
75
+ "system.profile.id_code": "证件号",
76
+ "system.profile.id_code_placeholder": "请输入证件号",
77
+ "system.profile.id_code_invalid": "请输入正确的证件号",
78
+ "system.profile.id_type.unspecified": "未指定",
79
+ "system.profile.id_type.id_card": "居民身份证",
80
+ "system.profile.id_type.passport": "护照",
81
+ "system.profile.id_type.hk_macao_permit": "港澳居民来往内地通行证",
82
+ "system.profile.id_type.taiwan_permit": "台湾居民来往大陆通行证",
83
+ "system.profile.id_type.foreign_permanent_residence": "外国人永久居留身份证",
84
+ "system.profile.id_type.other": "其他证件",
71
85
  "system.profile.login_prompt": "点击登录账号",
72
86
  "system.profile.mobile": "手机号",
73
87
  "system.profile.nick_name": "昵称",
@@ -68,6 +68,20 @@
68
68
  "system.profile.avatar_upload_failed": "上傳頭像失敗",
69
69
  "system.profile.avatar_update": "更新頭像暱稱",
70
70
  "system.profile.gender": "性別",
71
+ "system.profile.email": "電子郵件",
72
+ "system.profile.email_placeholder": "請輸入電子郵件",
73
+ "system.profile.email_invalid": "請輸入正確的電子郵件",
74
+ "system.profile.id_type": "證件類型",
75
+ "system.profile.id_code": "證件號",
76
+ "system.profile.id_code_placeholder": "請輸入證件號",
77
+ "system.profile.id_code_invalid": "請輸入正確的證件號",
78
+ "system.profile.id_type.unspecified": "未指定",
79
+ "system.profile.id_type.id_card": "居民身分證",
80
+ "system.profile.id_type.passport": "護照",
81
+ "system.profile.id_type.hk_macao_permit": "港澳居民來往內地通行證",
82
+ "system.profile.id_type.taiwan_permit": "臺灣居民來往大陸通行證",
83
+ "system.profile.id_type.foreign_permanent_residence": "外國人永久居留身分證",
84
+ "system.profile.id_type.other": "其他證件",
71
85
  "system.profile.login_prompt": "點選登入賬號",
72
86
  "system.profile.mobile": "手機號",
73
87
  "system.profile.nick_name": "暱稱",
@@ -1,6 +1,7 @@
1
1
  import { setAppMenuBadge } from '@liujitcn/kratos-taro-app-core/navigation'
2
+ import { getToken } from '@liujitcn/kratos-taro-app-core/utils/auth'
2
3
  import { getRequestAccessToken } from '@liujitcn/kratos-taro-app-core/utils/http'
3
- import { defNotificationService } from './api/base/notification'
4
+ import { defNotificationService } from './api/base/v1/notification'
4
5
 
5
6
  /** System 模块共享的站内信未读数。 */
6
7
  let unreadTotal = 0
@@ -10,6 +11,7 @@ let notificationPaused = false
10
11
 
11
12
  /** 从服务端刷新未读汇总并同步消息入口 badge。 */
12
13
  export async function refreshNotificationSummary(): Promise<number> {
14
+ if (!getToken()) return unreadTotal
13
15
  try {
14
16
  const summary = await defNotificationService.GetNotificationSummary({})
15
17
  unreadTotal = summary.unread_total
@@ -23,6 +25,7 @@ export async function refreshNotificationSummary(): Promise<number> {
23
25
  /** 启动应用端站内信定时回源。 */
24
26
  export function startNotificationPolling(): void {
25
27
  stopNotificationPolling()
28
+ if (!getToken()) return
26
29
  notificationPaused = false
27
30
  void refreshNotificationSummary()
28
31
  notificationTimer = setInterval(() => void refreshNotificationSummary(), 30_000)
package/src/pages.ts CHANGED
@@ -23,6 +23,11 @@ export const systemPages: Record<string, KratosTaroPageConfig> = {
23
23
  navigationBarTitleText: '',
24
24
  },
25
25
  },
26
- 'pagesMember/message/index': { style: { navigationBarTitleText: '' } },
26
+ 'pagesMember/message/index': {
27
+ style: {
28
+ navigationStyle: 'custom',
29
+ navigationBarTitleText: '',
30
+ },
31
+ },
27
32
  'pagesMember/message/detail': { style: { navigationBarTitleText: '' } },
28
33
  }
@@ -184,6 +184,32 @@ export interface PageNotificationResponse {
184
184
  has_more: boolean;
185
185
  }
186
186
 
187
+ /** 消息分类列表查询条件。 */
188
+ export interface ListNotificationCategoriesRequest {
189
+ }
190
+
191
+ /** 消息分类列表响应。 */
192
+ export interface ListNotificationCategoriesResponse {
193
+ /** 消息分类列表 */
194
+ categories: NotificationCategory[];
195
+ }
196
+
197
+ /** 当前可用的消息分类。 */
198
+ export interface NotificationCategory {
199
+ /** 消息分类ID */
200
+ id: number;
201
+ /** 分类编码 */
202
+ code: string;
203
+ /** 分类名称 */
204
+ name: string;
205
+ /** 分类图标 */
206
+ icon: string;
207
+ /** 分类颜色 */
208
+ color: string;
209
+ /** 排序 */
210
+ sort: number;
211
+ }
212
+
187
213
  /** 消息详情查询条件。 */
188
214
  export interface GetNotificationRequest {
189
215
  /** 投递ID */
@@ -296,6 +322,8 @@ export interface Notification {
296
322
  export interface NotificationService {
297
323
  /** 分页查询当前用户收件箱。 */
298
324
  PageNotification(request: PageNotificationRequest): Promise<PageNotificationResponse>;
325
+ /** 查询消息分类列表。 */
326
+ ListNotificationCategories(request: ListNotificationCategoriesRequest): Promise<ListNotificationCategoriesResponse>;
299
327
  /** 查询当前用户未读汇总。 */
300
328
  GetNotificationSummary(request: GetNotificationSummaryRequest): Promise<NotificationSummary>;
301
329
  /** 查询当前用户消息详情。 */
@@ -10,9 +10,9 @@ import {
10
10
  useI18n,
11
11
  } from '@liujitcn/kratos-taro-app-core'
12
12
  import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
13
- import { defAiMessageService, StreamAiMessageByChunkedRequest } from '../../../api/base/ai_message'
14
- import { defAiSessionService } from '../../../api/base/ai_session'
15
- import { defAiToolService } from '../../../api/base/ai_tool'
13
+ import { defAiMessageService, StreamAiMessageByChunkedRequest } from '../../../api/base/v1/ai_message'
14
+ import { defAiSessionService } from '../../../api/base/v1/ai_session'
15
+ import { defAiToolService } from '../../../api/base/v1/ai_tool'
16
16
  import type { AiAttachment, AiMessage, AiSession } from '../../../rpc/base/v1/ai_session'
17
17
  import type { AiShortcut, AiToolCall } from '../../../rpc/base/v1/ai_tool'
18
18
  import { AiMessageStatus } from '../../../rpc/base/v1/ai_session'
@@ -2,9 +2,11 @@ import { useLoad } from '@tarojs/taro'
2
2
  import { Button, RichText, Text, View } from '@tarojs/components'
3
3
  import { useState } from 'react'
4
4
  import { navigateAppView, t } from '@liujitcn/kratos-taro-app-core'
5
- import { defNotificationService } from '../../../api/base/notification'
5
+ import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
6
+ import { defNotificationService } from '../../../api/base/v1/notification'
6
7
  import type { Notification } from '../../../rpc/base/v1/notification'
7
8
  import { MessageActionType, MessageContentFormat } from '../../../rpc/base/v1/notification'
9
+ import { resolveMessageCategoryIcon } from './icons'
8
10
  import './message.scss'
9
11
 
10
12
  /** Taro 站内信详情页面。 */
@@ -13,7 +15,7 @@ export default function MessageDetailPage() {
13
15
 
14
16
  useLoad((options) => {
15
17
  const id = Number(options?.id)
16
- if (id > 0) void loadDetail(id)
18
+ if (id > 0) void loadDetail(id).catch(() => undefined)
17
19
  })
18
20
 
19
21
  /** 加载站内信详情。 */
@@ -31,7 +33,11 @@ export default function MessageDetailPage() {
31
33
  try {
32
34
  const value = JSON.parse(detail.action_params) as Record<string, unknown>
33
35
  if (value && typeof value === 'object' && !Array.isArray(value)) {
34
- params = Object.fromEntries(Object.entries(value).filter(([, item]) => typeof item === 'string' || typeof item === 'number').map(([key, item]) => [key, String(item)]))
36
+ params = Object.fromEntries(
37
+ Object.entries(value)
38
+ .filter(([, item]) => typeof item === 'string' || typeof item === 'number')
39
+ .map(([key, item]) => [key, String(item)]),
40
+ )
35
41
  }
36
42
  } catch {
37
43
  params = {}
@@ -45,12 +51,28 @@ export default function MessageDetailPage() {
45
51
  <View className='detail-page'>
46
52
  <Text className='detail-title'>{detail.title}</Text>
47
53
  <View className='detail-meta'>
48
- <Text>{detail.category_name}</Text><Text>{detail.sender_name}</Text><Text>{detail.received_at}</Text>
54
+ <View className='detail-category' style={{ color: detail.category_color || undefined }}>
55
+ <UniIcon
56
+ className='detail-category__icon'
57
+ type={resolveMessageCategoryIcon(detail.category_icon)}
58
+ size={18}
59
+ color={detail.category_color || '#64748b'}
60
+ />
61
+ <Text>{detail.category_name}</Text>
62
+ </View>
63
+ <Text>{detail.sender_name}</Text>
64
+ <Text>{detail.received_at}</Text>
49
65
  </View>
50
- {detail.content_format === MessageContentFormat.MESSAGE_CONTENT_FORMAT_RICH_TEXT
51
- ? <RichText className='detail-content-rich' nodes={detail.content} />
52
- : <Text className='detail-content'>{detail.content}</Text>}
53
- {detail.action_type === MessageActionType.MESSAGE_ACTION_TYPE_VIEW_KEY && <Button className='detail-action' onClick={openAction}>{t('common.action.view')}</Button>}
66
+ {detail.content_format === MessageContentFormat.MESSAGE_CONTENT_FORMAT_RICH_TEXT ? (
67
+ <RichText className='detail-content-rich' nodes={detail.content} />
68
+ ) : (
69
+ <Text className='detail-content'>{detail.content}</Text>
70
+ )}
71
+ {detail.action_type === MessageActionType.MESSAGE_ACTION_TYPE_VIEW_KEY && (
72
+ <Button className='detail-action' onClick={openAction}>
73
+ {t('common.action.view')}
74
+ </Button>
75
+ )}
54
76
  </View>
55
77
  )
56
78
  }
@@ -0,0 +1,26 @@
1
+ import type { UniIconName } from '@liujitcn/kratos-taro-app-ui'
2
+
3
+ /** 消息分类在 Taro 中可渲染的图标名称。 */
4
+ export type MessageCategoryIcon = UniIconName
5
+
6
+ const categoryIconMap: Record<string, MessageCategoryIcon> = {
7
+ Bell: 'notification-filled',
8
+ Lock: 'locked-filled',
9
+ List: 'list',
10
+ Message: 'chatboxes-filled',
11
+ InfoFilled: 'info-filled',
12
+ WarningFilled: 'flag-filled',
13
+ CircleCheckFilled: 'checkbox-filled',
14
+ Promotion: 'paperplane-filled',
15
+ Setting: 'settings-filled',
16
+ User: 'person-filled',
17
+ Calendar: 'calendar-filled',
18
+ ChatDotRound: 'chatbubble-filled',
19
+ DataAnalysis: 'bars',
20
+ CollectionTag: 'list',
21
+ }
22
+
23
+ /** 将管理端维护的 Element Plus 图标名转换为 Taro 图标名。 */
24
+ export function resolveMessageCategoryIcon(icon?: string): MessageCategoryIcon {
25
+ return categoryIconMap[icon ?? ''] ?? 'notification'
26
+ }
@@ -1,11 +1,13 @@
1
1
  import { useDidShow } from '@tarojs/taro'
2
- import { Button, Input, Text, View } from '@tarojs/components'
2
+ import { Button, ScrollView, Text, View } from '@tarojs/components'
3
3
  import { useState } from 'react'
4
- import { navigateAppRoute, t } from '@liujitcn/kratos-taro-app-core'
5
- import { defNotificationService } from '../../../api/base/notification'
6
- import type { Notification } from '../../../rpc/base/v1/notification'
4
+ import { navigateAppView, t } from '@liujitcn/kratos-taro-app-core'
5
+ import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
6
+ import { defNotificationService } from '../../../api/base/v1/notification'
7
+ import type { Notification, NotificationCategory } from '../../../rpc/base/v1/notification'
7
8
  import { NotificationView } from '../../../rpc/base/v1/notification'
8
9
  import { refreshNotificationSummary } from '../../../notification'
10
+ import { resolveMessageCategoryIcon } from './icons'
9
11
  import './message.scss'
10
12
 
11
13
  /** Taro 站内信收件箱页面。 */
@@ -16,8 +18,10 @@ export default function MessageInboxPage() {
16
18
  const [finished, setFinished] = useState(false)
17
19
  const [unreadTotal, setUnreadTotal] = useState(0)
18
20
  const [selectedView, setSelectedView] = useState(NotificationView.NOTIFICATION_VIEW_INBOX)
19
- const [categoryInput, setCategoryInput] = useState('')
20
21
  const [categoryId, setCategoryId] = useState<number>()
22
+ const [categoryOptions, setCategoryOptions] = useState<
23
+ NotificationCategory[]
24
+ >([])
21
25
  const viewOptions = [
22
26
  { value: NotificationView.NOTIFICATION_VIEW_INBOX, key: 'system.notification.view.inbox' },
23
27
  { value: NotificationView.NOTIFICATION_VIEW_UNREAD, key: 'system.notification.view.unread' },
@@ -28,12 +32,24 @@ export default function MessageInboxPage() {
28
32
  ]
29
33
 
30
34
  useDidShow(() => {
31
- void refresh()
32
- void refreshNotificationSummary().then(setUnreadTotal)
35
+ void loadCategories().catch(() => undefined)
36
+ void refresh().catch(() => undefined)
37
+ void refreshNotificationSummary()
38
+ .then(setUnreadTotal)
39
+ .catch(() => undefined)
33
40
  })
34
41
 
42
+ /** 加载管理端维护的消息分类。 */
43
+ async function loadCategories() {
44
+ const result = await defNotificationService.ListNotificationCategories({})
45
+ setCategoryOptions(result.categories ?? [])
46
+ }
47
+
35
48
  /** 刷新站内信列表。 */
36
49
  async function refresh(view = selectedView, category = categoryId) {
50
+ setItems([])
51
+ setCursorId(0)
52
+ setFinished(false)
37
53
  setLoading(true)
38
54
  try {
39
55
  const result = await defNotificationService.PageNotification({
@@ -77,15 +93,13 @@ export default function MessageInboxPage() {
77
93
  /** 切换收件箱视图。 */
78
94
  function changeView(view: NotificationView) {
79
95
  setSelectedView(view)
80
- void refresh(view, categoryId)
96
+ void refresh(view, categoryId).catch(() => undefined)
81
97
  }
82
98
 
83
- /** 应用分类筛选。 */
84
- function applyCategoryFilter() {
85
- const value = Number(categoryInput)
86
- const next = Number.isInteger(value) && value > 0 ? value : undefined
87
- setCategoryId(next)
88
- void refresh(selectedView, next)
99
+ /** 切换消息分类。 */
100
+ function changeCategory(id?: number) {
101
+ setCategoryId(id)
102
+ void refresh(selectedView, id).catch(() => undefined)
89
103
  }
90
104
 
91
105
  /** 标记当前水位线之前的消息为已读。 */
@@ -116,18 +130,53 @@ export default function MessageInboxPage() {
116
130
  /** 打开站内信详情。 */
117
131
  async function openDetail(item: Notification) {
118
132
  if (!item.read_at) await defNotificationService.MarkNotificationRead({ ids: [item.id] })
119
- navigateAppRoute(`app/message/detail?id=${item.id}`)
133
+ navigateAppView('MESSAGE_DETAIL', { id: String(item.id) })
120
134
  }
121
135
 
122
136
  return (
123
137
  <View className='message-page'>
124
138
  <View className='message-header'>
125
- <Text className='message-title'>{t('system.notification.title')}</Text>
126
- {unreadTotal > 0 && (
127
- <Text className='message-unread'>{unreadTotal > 99 ? '99+' : unreadTotal}</Text>
139
+ <View className='message-header__title'>
140
+ <Text className='message-title'>{t('system.notification.title')}</Text>
141
+ {unreadTotal > 0 && <View className='message-unread-dot' aria-hidden='true' />}
142
+ </View>
143
+ {selectedView !== NotificationView.NOTIFICATION_VIEW_ARCHIVED && unreadTotal > 0 && (
144
+ <Button
145
+ className='message-action message-action--read message-header__action'
146
+ onClick={() => void markAllRead().catch(() => undefined)}
147
+ >
148
+ {t('system.notification.mark_all_read')}
149
+ </Button>
128
150
  )}
129
151
  </View>
130
152
  <View className='message-controls'>
153
+ <ScrollView className='message-category-scroll' scrollX showScrollbar={false}>
154
+ <View className='message-category-list'>
155
+ <Button
156
+ className={categoryId === undefined ? 'message-category active' : 'message-category'}
157
+ onClick={() => changeCategory()}
158
+ >
159
+ {t('system.notification.view.inbox')}
160
+ </Button>
161
+ {categoryOptions.map((category) => (
162
+ <Button
163
+ key={category.id}
164
+ className={
165
+ categoryId === category.id ? 'message-category active' : 'message-category'
166
+ }
167
+ onClick={() => changeCategory(category.id)}
168
+ >
169
+ <UniIcon
170
+ className='message-category__icon'
171
+ type={resolveMessageCategoryIcon(category.icon)}
172
+ size={16}
173
+ color={categoryId === category.id ? '#fff' : category.color || '#64748b'}
174
+ />
175
+ {category.name}
176
+ </Button>
177
+ ))}
178
+ </View>
179
+ </ScrollView>
131
180
  <View className='message-tabs'>
132
181
  {viewOptions.map((option) => (
133
182
  <Button
@@ -139,40 +188,35 @@ export default function MessageInboxPage() {
139
188
  </Button>
140
189
  ))}
141
190
  </View>
142
- <View className='message-filter'>
143
- <Input
144
- type='number'
145
- value={categoryInput}
146
- placeholder={t('system.notification.category_filter')}
147
- onInput={(event) => setCategoryInput(event.detail.value)}
148
- onConfirm={applyCategoryFilter}
149
- />
150
- <Button size='mini' onClick={applyCategoryFilter}>
151
- {t('common.action.confirm')}
152
- </Button>
153
- {selectedView !== NotificationView.NOTIFICATION_VIEW_ARCHIVED && unreadTotal > 0 && (
154
- <Button size='mini' onClick={() => void markAllRead()}>
155
- {t('system.notification.mark_all_read')}
156
- </Button>
157
- )}
158
- </View>
159
191
  </View>
160
192
  {items.map((item) => (
161
- <View className='message-item' key={item.id} onClick={() => void openDetail(item)}>
193
+ <View
194
+ className='message-item'
195
+ key={item.id}
196
+ onClick={() => void openDetail(item).catch(() => undefined)}
197
+ >
162
198
  <View className='message-item__main'>
163
199
  <Text className={`message-item__title ${item.read_at ? '' : 'unread'}`}>
164
200
  {item.title}
165
201
  </Text>
166
202
  <Text className='message-item__time'>{item.received_at}</Text>
167
203
  </View>
168
- <Text className='message-item__category'>{item.category_name}</Text>
204
+ <View className='message-item__category'>
205
+ <UniIcon
206
+ className='message-item__category-icon'
207
+ type={resolveMessageCategoryIcon(item.category_icon)}
208
+ size={16}
209
+ color={item.category_color || '#64748b'}
210
+ />
211
+ <Text>{item.category_name}</Text>
212
+ </View>
169
213
  <View className='message-item__actions'>
170
214
  {selectedView !== NotificationView.NOTIFICATION_VIEW_ARCHIVED && item.allow_archive && (
171
215
  <Button
172
216
  size='mini'
173
217
  onClick={(event) => {
174
218
  event.stopPropagation()
175
- void toggleArchive(item)
219
+ void toggleArchive(item).catch(() => undefined)
176
220
  }}
177
221
  >
178
222
  {t('system.notification.action.archive')}
@@ -183,7 +227,7 @@ export default function MessageInboxPage() {
183
227
  size='mini'
184
228
  onClick={(event) => {
185
229
  event.stopPropagation()
186
- void toggleArchive(item)
230
+ void toggleArchive(item).catch(() => undefined)
187
231
  }}
188
232
  >
189
233
  {t('system.notification.action.restore')}
@@ -194,7 +238,7 @@ export default function MessageInboxPage() {
194
238
  size='mini'
195
239
  onClick={(event) => {
196
240
  event.stopPropagation()
197
- void deleteItem(item)
241
+ void deleteItem(item).catch(() => undefined)
198
242
  }}
199
243
  >
200
244
  {t('system.notification.action.delete')}
@@ -204,7 +248,11 @@ export default function MessageInboxPage() {
204
248
  </View>
205
249
  ))}
206
250
  {!finished && (
207
- <Button className='load-more' loading={loading} onClick={() => void loadMore()}>
251
+ <Button
252
+ className='load-more'
253
+ loading={loading}
254
+ onClick={() => void loadMore().catch(() => undefined)}
255
+ >
208
256
  {t('common.action.load_more')}
209
257
  </Button>
210
258
  )}
@@ -1,79 +1,155 @@
1
1
  .message-page {
2
+ box-sizing: border-box;
2
3
  min-height: 100vh;
3
- padding: 24px;
4
- background: var(--kratos-color-background);
4
+ padding: 16px 24px calc(120px + env(safe-area-inset-bottom));
5
+ background: var(--kratos-color-background, #f7f8fa);
6
+ color: var(--kratos-color-text, #1f2937);
5
7
  }
6
8
 
7
- .message-header {
8
- padding: 20px 4px 28px;
9
- }
10
9
  .message-header {
11
10
  display: flex;
12
11
  align-items: center;
13
12
  justify-content: space-between;
13
+ gap: 20px;
14
+ padding: 20px 4px 24px;
15
+ }
16
+ .message-header__title {
17
+ display: flex;
18
+ align-items: center;
19
+ min-width: 0;
14
20
  }
15
21
  .message-title {
16
- color: var(--kratos-color-text);
22
+ display: block;
23
+ color: var(--kratos-color-text, #1f2937);
17
24
  font-size: 40px;
18
25
  font-weight: 700;
26
+ line-height: 1.35;
19
27
  }
20
- .message-unread {
21
- min-width: 36px;
22
- padding: 4px 10px;
23
- border-radius: 18px;
28
+ .message-unread-dot {
29
+ width: 12px;
30
+ height: 12px;
31
+ margin-left: 8px;
32
+ border-radius: 50%;
24
33
  background: #e5484d;
25
- color: #fff;
26
- font-size: 22px;
27
- line-height: 28px;
28
- text-align: center;
29
34
  }
30
35
  .message-controls {
31
- margin-bottom: 18px;
36
+ margin-bottom: 24px;
37
+ }
38
+ .message-category-scroll {
39
+ width: 100%;
40
+ white-space: nowrap;
41
+ }
42
+ .message-category-list {
43
+ display: flex;
44
+ gap: 12px;
45
+ width: max-content;
46
+ padding-bottom: 4px;
32
47
  }
48
+ .message-category,
33
49
  .message-tabs,
34
- .message-filter,
35
50
  .message-item__actions {
36
51
  display: flex;
37
52
  align-items: center;
53
+ }
54
+ .message-category {
55
+ flex-shrink: 0;
56
+ width: auto;
57
+ height: 64px;
58
+ margin: 0;
59
+ padding: 0 22px;
60
+ border: 1px solid var(--kratos-color-border, #e2e8f0);
61
+ border-radius: 32px;
62
+ background: #fff;
63
+ color: var(--kratos-color-text-muted, #6b7280);
64
+ font-size: 24px;
65
+ line-height: 64px;
66
+ white-space: nowrap;
67
+ }
68
+ .message-category__icon,
69
+ .message-item__category-icon {
70
+ flex: 0 0 auto;
71
+ margin-right: 8px;
72
+ }
73
+ .message-category.active {
74
+ border-color: var(--kratos-color-primary, #27ba9b);
75
+ background: var(--kratos-color-primary, #27ba9b);
76
+ color: #fff;
77
+ }
78
+ .message-tabs {
79
+ gap: 12px;
80
+ margin-top: 18px;
81
+ padding: 6px;
82
+ border-radius: 12px;
83
+ background: #eef1f3;
84
+ }
85
+ .message-tab::after,
86
+ .message-category::after,
87
+ .message-action::after,
88
+ .message-item__actions button::after,
89
+ .load-more::after {
90
+ border: 0;
91
+ }
92
+ .message-item__actions {
93
+ flex-wrap: wrap;
94
+ justify-content: flex-end;
38
95
  gap: 12px;
39
96
  }
40
97
  .message-tab {
98
+ box-sizing: border-box;
41
99
  flex: 1;
100
+ min-width: 0;
101
+ height: 68px;
42
102
  margin: 0;
43
- padding: 8px 12px;
103
+ padding: 0 12px;
104
+ border: 0;
105
+ border-radius: 8px;
44
106
  background: transparent;
45
- color: var(--kratos-color-text-muted);
107
+ color: var(--kratos-color-text-muted, #6b7280);
46
108
  font-size: 26px;
109
+ line-height: 68px;
110
+ white-space: nowrap;
47
111
  }
48
112
  .message-tab.active {
49
- background: var(--kratos-color-primary);
113
+ background: var(--kratos-color-primary, #27ba9b);
50
114
  color: #fff;
51
115
  }
52
- .message-filter {
53
- margin-top: 14px;
54
- }
55
- .message-filter input {
56
- min-width: 0;
57
- flex: 1;
58
- padding: 12px 16px;
59
- border: 1px solid var(--kratos-color-border);
60
- border-radius: 8px;
61
- }
62
- .message-filter button,
116
+ .message-action,
63
117
  .message-item__actions button {
118
+ box-sizing: border-box;
119
+ flex-shrink: 0;
120
+ width: auto;
121
+ height: 68px;
64
122
  margin: 0;
65
- padding: 0 14px;
123
+ padding: 0 20px;
124
+ border: 0;
125
+ border-radius: 8px;
126
+ background: #eef1f3;
127
+ color: var(--kratos-color-text, #1f2937);
66
128
  font-size: 24px;
129
+ line-height: 68px;
130
+ white-space: nowrap;
67
131
  }
68
- .message-item__actions {
69
- justify-content: flex-end;
70
- margin-top: 14px;
132
+ .message-action--read {
133
+ background: #e8f8f4;
134
+ color: var(--kratos-color-primary, #16806d);
135
+ }
136
+ .message-header__action {
137
+ height: 56px;
138
+ padding: 0 18px;
139
+ line-height: 56px;
140
+ }
141
+ .message-item__actions button {
142
+ height: 56px;
143
+ padding: 0 16px;
144
+ font-size: 22px;
145
+ line-height: 56px;
71
146
  }
72
147
  .message-item {
73
148
  margin-bottom: 16px;
74
149
  padding: 28px;
75
150
  border-radius: 12px;
76
151
  background: #fff;
152
+ box-shadow: 0 4px 16px rgba(31, 41, 55, 0.05);
77
153
  }
78
154
  .message-item__main {
79
155
  display: flex;
@@ -82,43 +158,58 @@
82
158
  gap: 20px;
83
159
  }
84
160
  .message-item__title {
161
+ display: block;
85
162
  min-width: 0;
86
163
  flex: 1;
87
164
  overflow: hidden;
88
- color: var(--kratos-color-text);
165
+ color: var(--kratos-color-text, #1f2937);
89
166
  text-overflow: ellipsis;
90
167
  white-space: nowrap;
91
168
  }
92
169
  .message-item__title.unread {
93
- color: var(--kratos-color-text);
170
+ color: var(--kratos-color-text, #1f2937);
94
171
  font-weight: 700;
95
172
  }
96
173
  .message-item__time,
97
174
  .message-item__category {
98
- color: var(--kratos-color-text-muted);
175
+ color: var(--kratos-color-text-muted, #6b7280);
99
176
  font-size: 24px;
100
177
  }
178
+ .message-item__time {
179
+ flex-shrink: 0;
180
+ }
101
181
  .message-item__category {
102
- display: block;
182
+ display: flex;
183
+ align-items: center;
103
184
  margin-top: 14px;
104
185
  }
105
186
  .load-more {
187
+ box-sizing: border-box;
188
+ width: 100%;
189
+ height: 72px;
106
190
  margin-top: 24px;
191
+ padding: 0 24px;
192
+ border: 0;
193
+ border-radius: 8px;
194
+ background: #eef1f3;
195
+ color: var(--kratos-color-text, #1f2937);
107
196
  font-size: 28px;
197
+ line-height: 72px;
108
198
  }
109
199
  .empty {
110
- padding-top: 180px;
111
- color: var(--kratos-color-text-muted);
200
+ padding: 120px 0;
201
+ color: var(--kratos-color-text-muted, #6b7280);
112
202
  text-align: center;
113
203
  }
114
204
  .detail-page {
205
+ box-sizing: border-box;
115
206
  min-height: 100vh;
116
- padding: 36px 30px;
117
- background: var(--kratos-color-background);
207
+ padding: 24px 30px calc(96px + env(safe-area-inset-bottom));
208
+ background: var(--kratos-color-background, #f7f8fa);
118
209
  }
119
210
  .detail-title {
120
211
  display: block;
121
- color: var(--kratos-color-text);
212
+ color: var(--kratos-color-text, #1f2937);
122
213
  font-size: 42px;
123
214
  font-weight: 700;
124
215
  line-height: 1.35;
@@ -128,11 +219,18 @@
128
219
  flex-wrap: wrap;
129
220
  gap: 18px;
130
221
  margin: 24px 0 36px;
131
- color: var(--kratos-color-text-muted);
222
+ color: var(--kratos-color-text-muted, #6b7280);
132
223
  font-size: 24px;
133
224
  }
225
+ .detail-category {
226
+ display: inline-flex;
227
+ align-items: center;
228
+ }
229
+ .detail-category__icon {
230
+ margin-right: 8px;
231
+ }
134
232
  .detail-content {
135
- color: var(--kratos-color-text);
233
+ color: var(--kratos-color-text, #1f2937);
136
234
  font-size: 30px;
137
235
  line-height: 1.8;
138
236
  white-space: pre-wrap;
@@ -140,7 +238,7 @@
140
238
  }
141
239
  .detail-content-rich {
142
240
  display: block;
143
- color: var(--kratos-color-text);
241
+ color: var(--kratos-color-text, #1f2937);
144
242
  font-size: 30px;
145
243
  line-height: 1.8;
146
244
  overflow-wrap: anywhere;
@@ -3,6 +3,7 @@ import {
3
3
  Image,
4
4
  Input,
5
5
  Label,
6
+ Picker,
6
7
  Radio,
7
8
  RadioGroup,
8
9
  Text,
@@ -12,10 +13,11 @@ import Taro, { useLoad } from '@tarojs/taro'
12
13
  import { useEffect, useState } from 'react'
13
14
  import defaultAvatar from '@liujitcn/kratos-taro-app-core/static/images/avatar.png'
14
15
  import navigatorBackground from '@liujitcn/kratos-taro-app-core/static/images/navigator_bg.png'
15
- import { defAuthService } from '@liujitcn/kratos-taro-app-core/api/system/auth'
16
- import { defBaseDictService } from '@liujitcn/kratos-taro-app-core/api/system/base_dict'
16
+ import { defAuthService } from '@liujitcn/kratos-taro-app-core/api/system/app/v1/auth'
17
+ import { defBaseDictService } from '@liujitcn/kratos-taro-app-core/api/system/app/v1/base_dict'
17
18
  import type { BaseDictForm_DictItem } from '@liujitcn/kratos-taro-app-core/rpc/system/app/v1/base_dict'
18
19
  import type { UserProfileForm } from '@liujitcn/kratos-taro-app-core/rpc/system/app/v1/auth'
20
+ import { BaseUserIDType } from '@liujitcn/kratos-taro-app-core/rpc/system/common/v1/common'
19
21
  import {
20
22
  formatSrc,
21
23
  navigateToLogin,
@@ -26,6 +28,8 @@ import {
26
28
  import './profile.scss'
27
29
 
28
30
  const IMG_MAX_SIZE = 1024 * 1024
31
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
32
+ const ID_CODE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/
29
33
 
30
34
  /** 用户个人资料编辑页。 */
31
35
  export default function ProfilePage() {
@@ -35,11 +39,27 @@ export default function ProfilePage() {
35
39
  nick_name: '',
36
40
  gender: 0,
37
41
  phone: '',
42
+ email: '',
43
+ id_type: BaseUserIDType.BASE_USER_ID_TYPE_UNSPECIFIED,
44
+ id_code: '',
38
45
  avatar: '',
39
46
  })
40
47
  const [genderList, setGenderList] = useState<BaseDictForm_DictItem[]>([])
41
48
  const ensureAuthenticated = useUserStore((state) => state.ensureAuthenticated)
42
49
  const safeTop = Taro.getWindowInfo().safeArea?.top || 0
50
+ const idTypeOptions = [
51
+ { label: t('system.profile.id_type.unspecified'), value: BaseUserIDType.BASE_USER_ID_TYPE_UNSPECIFIED },
52
+ { label: t('system.profile.id_type.id_card'), value: BaseUserIDType.BASE_USER_ID_TYPE_ID_CARD },
53
+ { label: t('system.profile.id_type.passport'), value: BaseUserIDType.BASE_USER_ID_TYPE_PASSPORT },
54
+ { label: t('system.profile.id_type.hk_macao_permit'), value: BaseUserIDType.BASE_USER_ID_TYPE_HK_MACAO_PERMIT },
55
+ { label: t('system.profile.id_type.taiwan_permit'), value: BaseUserIDType.BASE_USER_ID_TYPE_TAIWAN_PERMIT },
56
+ {
57
+ label: t('system.profile.id_type.foreign_permanent_residence'),
58
+ value: BaseUserIDType.BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE,
59
+ },
60
+ { label: t('system.profile.id_type.other'), value: BaseUserIDType.BASE_USER_ID_TYPE_OTHER },
61
+ ]
62
+ const idTypeIndex = Math.max(0, idTypeOptions.findIndex((item) => item.value === userInfo.id_type))
43
63
 
44
64
  useEffect(() => {
45
65
  void Taro.setNavigationBarTitle({ title: t('system.profile.title') })
@@ -54,7 +74,7 @@ export default function ProfilePage() {
54
74
  const loadData = async () => {
55
75
  const [profile, gender] = await Promise.all([
56
76
  useUserStore.getState().getUserProfile(),
57
- defBaseDictService.GetBaseDict({ value: 'base_user_gender' }),
77
+ defBaseDictService.GetBaseDict({ code: 'base_user_gender' }),
58
78
  ])
59
79
  setUserInfo(profile)
60
80
  setGenderList(gender.items || [])
@@ -69,7 +89,7 @@ export default function ProfilePage() {
69
89
  try {
70
90
  const fileInfo = await uploadFile('avatar', filePath)
71
91
  const nextProfile = { ...userInfo, avatar: fileInfo.url }
72
- await defAuthService.UpdateUserProfile(nextProfile)
92
+ await defAuthService.UpdateUserProfile({ user_profile: nextProfile })
73
93
  setUserInfo(nextProfile)
74
94
  await useUserStore.getState().getUserProfile()
75
95
  await Taro.showToast({ icon: 'success', title: t('system.profile.update_success') })
@@ -103,7 +123,15 @@ export default function ProfilePage() {
103
123
 
104
124
  const onSubmit = async () => {
105
125
  if (!requireAuth()) return
106
- await defAuthService.UpdateUserProfile(userInfo)
126
+ if (userInfo.email && !EMAIL_PATTERN.test(userInfo.email)) {
127
+ await Taro.showToast({ icon: 'none', title: t('system.profile.email_invalid') })
128
+ return
129
+ }
130
+ if (userInfo.id_code && !ID_CODE_PATTERN.test(userInfo.id_code)) {
131
+ await Taro.showToast({ icon: 'none', title: t('system.profile.id_code_invalid') })
132
+ return
133
+ }
134
+ await defAuthService.UpdateUserProfile({ user_profile: userInfo })
107
135
  await useUserStore.getState().getUserProfile()
108
136
  await Taro.showToast({ icon: 'success', title: t('system.profile.save_success') })
109
137
  setTimeout(() => void Taro.navigateBack(), 400)
@@ -145,6 +173,25 @@ export default function ProfilePage() {
145
173
  <Text className='profile-label'>{t('system.profile.nick_name')}</Text>
146
174
  <Input className='profile-input' placeholder={t('system.profile.nick_name_placeholder')} value={userInfo.nick_name} onInput={(event) => setUserInfo((current) => ({ ...current, nick_name: event.detail.value }))} />
147
175
  </View>
176
+ <View className='profile-form__item'>
177
+ <Text className='profile-label'>{t('system.profile.email')}</Text>
178
+ <Input className='profile-input' type='text' placeholder={t('system.profile.email_placeholder')} value={userInfo.email} onInput={(event) => setUserInfo((current) => ({ ...current, email: event.detail.value }))} />
179
+ </View>
180
+ <Picker
181
+ mode='selector'
182
+ range={idTypeOptions.map((item) => item.label)}
183
+ value={idTypeIndex}
184
+ onChange={(event) => setUserInfo((current) => ({ ...current, id_type: idTypeOptions[Number(event.detail.value)]?.value ?? BaseUserIDType.BASE_USER_ID_TYPE_UNSPECIFIED }))}
185
+ >
186
+ <View className='profile-form__item'>
187
+ <Text className='profile-label'>{t('system.profile.id_type')}</Text>
188
+ <Text className='profile-input profile-account'>{idTypeOptions[idTypeIndex]?.label}</Text>
189
+ </View>
190
+ </Picker>
191
+ <View className='profile-form__item'>
192
+ <Text className='profile-label'>{t('system.profile.id_code')}</Text>
193
+ <Input className='profile-input' type='text' placeholder={t('system.profile.id_code_placeholder')} value={userInfo.id_code} onInput={(event) => setUserInfo((current) => ({ ...current, id_code: event.detail.value }))} />
194
+ </View>
148
195
  <View className='profile-form__item'>
149
196
  <Text className='profile-label'>{t('system.profile.gender')}</Text>
150
197
  <RadioGroup onChange={(event) => setUserInfo((current) => ({ ...current, gender: Number(event.detail.value) }))}>
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'
3
3
  import { useI18n } from '@liujitcn/kratos-taro-app-core'
4
4
  import MfaRecoveryCodesDialog from '@liujitcn/kratos-taro-app-core/components/MfaRecoveryCodesDialog'
5
5
  import MfaSetupPanel from '@liujitcn/kratos-taro-app-core/components/MfaSetupPanel'
6
- import { defMfaService } from '@liujitcn/kratos-taro-app-core/api/base/mfa'
6
+ import { defMfaService } from '@liujitcn/kratos-taro-app-core/api/base/v1/mfa'
7
7
  import {
8
8
  createWebAuthnCredential,
9
9
  getWebAuthnAssertion,
@@ -8,7 +8,7 @@ import {
8
8
  useI18n,
9
9
  useUserStore,
10
10
  } from '@liujitcn/kratos-taro-app-core'
11
- import { defMfaService } from '@liujitcn/kratos-taro-app-core/api/base/mfa'
11
+ import { defMfaService } from '@liujitcn/kratos-taro-app-core/api/base/v1/mfa'
12
12
  import PasswordVerifyDialog from './components/PasswordVerifyDialog'
13
13
  import './settings.scss'
14
14