@liujitcn/kratos-taro-app-core 0.0.81 → 0.0.83

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
@@ -18,6 +18,8 @@
18
18
 
19
19
  ## 公开入口
20
20
 
21
+ 登录页返回会清理本次回跳并替换到首页;直接打开登录页清理旧 `lastRoute`,登录成功默认进入首页。主动登录、路由守卫和会话过期重登通过 `redirect=1` 标记保留本次业务目标及查询参数,成功跳转后才清理目标。登录页每次显示都会检查有效登录态,已登录则替换回首页,避免浏览器回退重新停留在登录页。H5 与微信小程序统一使用 `reLaunch` 替换登录入口并重置应用页面栈。
22
+
21
23
  - `@liujitcn/kratos-taro-app-core`:启动、模块、导航、状态和常用工具,包含动态路径使用的 `resolveBundledAsset()`。
22
24
  - `@liujitcn/kratos-taro-app-core/static/*`:页面图片等可由 Webpack 静态分析的打包资源。
23
25
  - `@liujitcn/kratos-taro-app-core/build`:构建期模块描述和双语构建错误文案格式化器。
package/dist/build.mjs CHANGED
@@ -7,7 +7,6 @@ var corePages = {
7
7
  "pages/index/index": {
8
8
  style: {
9
9
  navigationStyle: "custom",
10
- navigationBarTextStyle: "white",
11
10
  navigationBarTitleText: ""
12
11
  }
13
12
  },
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.81",
10
+ "version": "0.0.83",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Text, View } from '@tarojs/components'
3
+
4
+ /** 空态组件属性。 */
5
+ export interface KratosEmptyProps {
6
+ /** 图标节点(如 <UniIcon />),不传则不渲染图标位。 */
7
+ icon?: ReactNode
8
+ /** 主文案。 */
9
+ title?: string
10
+ /** 次级提示文案。 */
11
+ description?: string
12
+ /** 操作区内容(如引导按钮)。 */
13
+ children?: ReactNode
14
+ }
15
+
16
+ /** 标准空态组件:图标 + 主文案 + 提示文案 + 可选操作区。 */
17
+ export default function KratosEmpty({ icon, title, description, children }: KratosEmptyProps) {
18
+ return (
19
+ <View className='k-empty'>
20
+ {icon && <View className='k-empty__icon'>{icon}</View>}
21
+ {title && <Text className='k-empty__title'>{title}</Text>}
22
+ {description && <Text className='k-empty__desc'>{description}</Text>}
23
+ {children}
24
+ </View>
25
+ )
26
+ }
@@ -30,21 +30,8 @@
30
30
  line-height: 12PX;
31
31
  }
32
32
 
33
- /* 设计稿:激活项顶部霓虹发光条 */
34
- .kratos-tab-bar__item--active::before {
35
- position: absolute;
36
- top: 0;
37
- left: 50%;
38
- width: 26px;
39
- height: 3px;
40
- border-radius: 3px;
41
- background: var(--kratos-tab-bar-active-color);
42
- box-shadow: 0 0 12px var(--kratos-tab-bar-active-color);
43
- content: '';
44
- transform: translateX(-50%);
45
- }
46
-
47
33
  .kratos-tab-bar__icon {
34
+ flex-shrink: 0;
48
35
  width: 28PX;
49
36
  height: 28PX;
50
37
  margin-bottom: 4PX;
package/src/index.ts CHANGED
@@ -42,3 +42,5 @@ export * from './utils/navigation'
42
42
  export * from './utils/secretCrypto'
43
43
  export { default as PasswordVerifyDialog } from './components/PasswordVerifyDialog'
44
44
  export type { PasswordVerifyDialogProps } from './components/PasswordVerifyDialog'
45
+ export { default as KratosEmpty } from './components/KratosEmpty'
46
+ export type { KratosEmptyProps } from './components/KratosEmpty'
@@ -9,4 +9,4 @@
9
9
  | `en-US.json` | 英文 Core 语言包。 | Taro 切换为英文时使用。 |
10
10
  | `ja-JP.json` | 日文 Core 语言包。 | Taro 切换为日文时使用。 |
11
11
 
12
- 语言包是标准 JSON,不能在文件内直接写注释;公共键使用 `common.` 或 `core.` 命名空间。新增语言后执行仓库根目录的 `make i18n`。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化设计与语言扩展](../../../../../../docs/国际化设计与语言扩展.md)。
12
+ 语言包是标准 JSON,不能在文件内直接写注释;公共键使用 `common.` 或 `core.` 命名空间。新增语言后执行仓库根目录的 `make i18n`。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;语言迁移初始名称来自管理端 Core 的 `common.language.*`。新增语言的完整文件清单见[国际化设计与语言扩展](../../../../../../docs/国际化设计与语言扩展.md)。
@@ -10,16 +10,11 @@
10
10
  "common.action.save": "Save",
11
11
  "common.action.view": "View",
12
12
  "common.field.language": "Language",
13
- "common.language.en-US": "English",
14
- "common.language.zh-CN": "简体中文",
15
- "common.language.zh-TW": "Traditional Chinese",
16
- "common.language.ja-JP": "Japanese",
17
13
  "common.message.loading": "Loading",
18
14
  "common.message.network_error": "Network error. Check your connection and try again.",
19
15
  "common.message.request_error": "Request failed",
20
16
  "common.message.unknown": "Unable to process this request. Try again later.",
21
17
  "common.title.notice": "Notice",
22
- "core.auth.login_again": "Log in again",
23
18
  "core.auth.session_expired": "Your session has expired. Please log in again.",
24
19
  "core.config.missing": "Missing app configuration: {names}",
25
20
  "core.crypto.base64_decode_unsupported": "Base64 decoding is not supported in this environment",
@@ -34,18 +29,20 @@
34
29
  "core.file.temporary_path_required": "Use a temporary file path when uploading from a Mini Program",
35
30
  "core.file.upload_failed": "Upload failed",
36
31
  "core.home.account_capability": "Account features",
37
- "core.home.account_capability_description": "Profile, app settings, and AI assistant access are included",
32
+ "core.home.account_capability_description": "Sign-in, profile, multi-factor authentication, and app settings out of the box",
38
33
  "core.home.cross_platform": "Cross-platform pages",
39
34
  "core.home.cross_platform_description": "One codebase for H5 and WeChat Mini Program",
40
35
  "core.home.demo_scope": "Overview",
36
+ "core.home.dynamic_config": "Dynamic config",
37
+ "core.home.dynamic_config_description": "Home logo and tagline configurable from the admin console",
41
38
  "core.home.framework": "App framework",
42
39
  "core.home.language": "Language",
43
40
  "core.home.main_title": "Application Home",
41
+ "core.home.message_center": "Message center",
42
+ "core.home.message_center_description": "Unread messages in one inbox with badge reminders",
44
43
  "core.home.state_management": "State management",
45
- "core.home.static_home": "Static home",
46
- "core.home.static_home_description": "This page presents framework details without business APIs",
47
44
  "core.home.style_solution": "Styling",
48
- "core.home.sub_title": "Shared navigation and account experience",
45
+ "core.home.sub_title": "Accounts, messages, and AI assistant in one place",
49
46
  "core.home.tech_stack": "Technology stack",
50
47
  "core.login.agree_prefix": "I have read and agree to the",
51
48
  "core.login.agree_separator": "and",
@@ -121,12 +118,5 @@
121
118
  "core.webview.embed_blocked": "The destination may block embedding on H5",
122
119
  "core.webview.invalid_url": "No valid link was provided",
123
120
  "core.webview.open_failed": "Unable to open link",
124
- "common.value.top_level": "Top level",
125
- "common.resource.default": "{resource}",
126
- "common.action.delete_resource": "Delete {resource}",
127
- "common.action.set_field_status": "Set {field}",
128
- "common.action.load_more": "Load more",
129
- "common.status.loading": "Loading…",
130
- "core.navigation.analytics": "Analytics",
131
- "core.navigation.report": "Reports"
121
+ "common.action.load_more": "Load more"
132
122
  }
@@ -10,16 +10,11 @@
10
10
  "common.action.save": "保存",
11
11
  "common.action.view": "表示",
12
12
  "common.field.language": "言語",
13
- "common.language.en-US": "英語",
14
- "common.language.zh-CN": "簡体字中国語",
15
- "common.language.zh-TW": "繁体字中国語",
16
- "common.language.ja-JP": "日本語",
17
13
  "common.message.loading": "をロード中です",
18
14
  "common.message.network_error": "ネットワーク エラー。別のネットワークを試してください",
19
15
  "common.message.request_error": "リクエストエラー",
20
16
  "common.message.unknown": "は一時的に処理できません。後でもう一度試してください。",
21
17
  "common.title.notice": "ヒント",
22
- "core.auth.login_again": "再度ログインしてください",
23
18
  "core.auth.session_expired": "現在のページは無効です。再度ログインしてください。",
24
19
  "core.config.missing": "モバイル端末の設定がありません: {names}",
25
20
  "core.crypto.base64_decode_unsupported": "現在の環境は Base64 デコードをサポートしていません",
@@ -34,18 +29,20 @@
34
29
  "core.file.temporary_path_required": "ミニ プログラムをアップロードするには、一時ファイル パスを使用してください。",
35
30
  "core.file.upload_failed": "アップロードに失敗しました",
36
31
  "core.home.account_capability": "アカウントの機能",
37
- "core.home.account_capability_description": "個人情報、アプリケーション設定、スマートアシスタントの入り口が予約されています",
32
+ "core.home.account_capability_description": "ログイン、プロフィール、多要素認証、アプリ設定を同梱",
38
33
  "core.home.cross_platform": "クロスエンド ページ",
39
34
  "core.home.cross_platform_description": "同じページ コード セットが H5 および WeChat ミニ プログラムに適応します",
40
35
  "core.home.demo_scope": "機能概要",
36
+ "core.home.dynamic_config": "動的設定",
37
+ "core.home.dynamic_config_description": "ホームのロゴとキャッチコピーは管理画面から変更可能",
41
38
  "core.home.framework": "アプリケーション フレームワーク",
42
39
  "core.home.language": "開発言語",
43
40
  "core.home.main_title": "アプリケーションホーム",
41
+ "core.home.message_center": "メッセージセンター",
42
+ "core.home.message_center_description": "未読メッセージを一括管理し、バッジでお知らせ",
44
43
  "core.home.state_management": "ステータス管理",
45
- "core.home.static_home": "静的ホームページ",
46
- "core.home.static_home_description": "現在のホームページはフレーム情報を表示するために使用されており、ビジネス インターフェイスには依存していません",
47
44
  "core.home.style_solution": "スタイル スキーム",
48
- "core.home.sub_title": "ユニバーサル ナビゲーションとパーソナル センターのエクスペリエンスを維持します",
45
+ "core.home.sub_title": "アカウント・メッセージ・スマートアシスタントをひとつに",
49
46
  "core.home.tech_stack": "現在のテクノロジースタック",
50
47
  "core.login.agree_prefix": "読んで同意します",
51
48
  "core.login.agree_separator": "および",
@@ -121,12 +118,5 @@
121
118
  "core.webview.embed_blocked": "現在の H5 ページは、ターゲット サイトによって埋め込みが制限されている可能性があります",
122
119
  "core.webview.invalid_url": "有効なリンク アドレスがありません",
123
120
  "core.webview.open_failed": "リンクを開けません",
124
- "common.value.top_level": "トップレベル",
125
- "common.resource.default": "{resource}",
126
- "common.action.delete_resource": "{resource}を削除",
127
- "common.action.set_field_status": "{field}を設定",
128
- "common.action.load_more": "さらに読み込む",
129
- "common.status.loading": "読み込み中…",
130
- "core.navigation.analytics": "分析",
131
- "core.navigation.report": "レポート"
121
+ "common.action.load_more": "さらに読み込む"
132
122
  }
@@ -10,16 +10,11 @@
10
10
  "common.action.save": "保存",
11
11
  "common.action.view": "查看",
12
12
  "common.field.language": "语言",
13
- "common.language.en-US": "English",
14
- "common.language.zh-CN": "简体中文",
15
- "common.language.zh-TW": "繁体中文",
16
- "common.language.ja-JP": "日语",
17
13
  "common.message.loading": "正在加载",
18
14
  "common.message.network_error": "网络错误,换个网络试试",
19
15
  "common.message.request_error": "请求错误",
20
16
  "common.message.unknown": "暂时无法处理,请稍后重试",
21
17
  "common.title.notice": "提示",
22
- "core.auth.login_again": "重新登录",
23
18
  "core.auth.session_expired": "当前页面已失效,请重新登录",
24
19
  "core.config.missing": "移动端配置缺失:{names}",
25
20
  "core.crypto.base64_decode_unsupported": "当前环境不支持 Base64 解码",
@@ -34,18 +29,20 @@
34
29
  "core.file.temporary_path_required": "小程序端请使用文件临时路径上传",
35
30
  "core.file.upload_failed": "上传失败",
36
31
  "core.home.account_capability": "账户能力",
37
- "core.home.account_capability_description": "个人资料、应用设置与智能助手入口已保留",
32
+ "core.home.account_capability_description": "登录、个人资料、多因素认证与应用设置开箱即用",
38
33
  "core.home.cross_platform": "跨端页面",
39
34
  "core.home.cross_platform_description": "同一套页面代码适配 H5 和微信小程序",
40
35
  "core.home.demo_scope": "功能概览",
36
+ "core.home.dynamic_config": "动态配置",
37
+ "core.home.dynamic_config_description": "首页 Logo 与标语支持后台远程配置",
41
38
  "core.home.framework": "应用框架",
42
39
  "core.home.language": "开发语言",
43
40
  "core.home.main_title": "应用首页",
41
+ "core.home.message_center": "消息中心",
42
+ "core.home.message_center_description": "未读消息统一收纳,红点提醒不错过",
44
43
  "core.home.state_management": "状态管理",
45
- "core.home.static_home": "静态首页",
46
- "core.home.static_home_description": "当前首页用于展示框架信息,不依赖业务接口",
47
44
  "core.home.style_solution": "样式方案",
48
- "core.home.sub_title": "保留通用导航与个人中心体验",
45
+ "core.home.sub_title": "账号、消息与智能助手,一站触达",
49
46
  "core.home.tech_stack": "当前技术栈",
50
47
  "core.login.agree_prefix": "我已阅读并同意",
51
48
  "core.login.agree_separator": "和",
@@ -121,12 +118,5 @@
121
118
  "core.webview.embed_blocked": "当前 H5 页面可能被目标站点限制嵌入",
122
119
  "core.webview.invalid_url": "缺少有效链接地址",
123
120
  "core.webview.open_failed": "链接无法打开",
124
- "common.value.top_level": "顶级节点",
125
- "common.resource.default": "{resource}",
126
- "common.action.delete_resource": "删除{resource}",
127
- "common.action.set_field_status": "设置{field}",
128
- "common.action.load_more": "加载更多",
129
- "common.status.loading": "加载中…",
130
- "core.navigation.analytics": "分析",
131
- "core.navigation.report": "报表"
121
+ "common.action.load_more": "加载更多"
132
122
  }
@@ -10,16 +10,11 @@
10
10
  "common.action.save": "儲存",
11
11
  "common.action.view": "檢視",
12
12
  "common.field.language": "語言",
13
- "common.language.en-US": "English",
14
- "common.language.zh-CN": "簡體中文",
15
- "common.language.zh-TW": "繁體中文",
16
- "common.language.ja-JP": "日語",
17
13
  "common.message.loading": "正在載入",
18
14
  "common.message.network_error": "網路錯誤,換個網路試試",
19
15
  "common.message.request_error": "請求錯誤",
20
16
  "common.message.unknown": "暫時無法處理,請稍後重試",
21
17
  "common.title.notice": "提示",
22
- "core.auth.login_again": "重新登入",
23
18
  "core.auth.session_expired": "當前頁面已失效,請重新登入",
24
19
  "core.config.missing": "移動端配置缺失:{names}",
25
20
  "core.crypto.base64_decode_unsupported": "當前環境不支援 Base64 解碼",
@@ -34,18 +29,20 @@
34
29
  "core.file.temporary_path_required": "小程式端請使用檔案臨時路徑上傳",
35
30
  "core.file.upload_failed": "上傳失敗",
36
31
  "core.home.account_capability": "賬戶能力",
37
- "core.home.account_capability_description": "個人資料、應用設定與智慧助手入口已保留",
32
+ "core.home.account_capability_description": "登入、個人資料、多因素認證與應用設定開箱即用",
38
33
  "core.home.cross_platform": "跨端頁面",
39
34
  "core.home.cross_platform_description": "同一套頁面程式碼適配 H5 和微信小程式",
40
35
  "core.home.demo_scope": "功能概覽",
36
+ "core.home.dynamic_config": "動態設定",
37
+ "core.home.dynamic_config_description": "首頁 Logo 與標語支援後台遠端設定",
41
38
  "core.home.framework": "應用框架",
42
39
  "core.home.language": "開發語言",
43
40
  "core.home.main_title": "應用首頁",
41
+ "core.home.message_center": "訊息中心",
42
+ "core.home.message_center_description": "未讀訊息統一查看,紅點提醒不錯過",
44
43
  "core.home.state_management": "狀態管理",
45
- "core.home.static_home": "靜態首頁",
46
- "core.home.static_home_description": "當前首頁用於展示框架資訊,不依賴業務介面",
47
44
  "core.home.style_solution": "樣式方案",
48
- "core.home.sub_title": "保留通用導航與個人中心體驗",
45
+ "core.home.sub_title": "帳號、訊息與智慧助手,一站觸達",
49
46
  "core.home.tech_stack": "當前技術棧",
50
47
  "core.login.agree_prefix": "我已閱讀並同意",
51
48
  "core.login.agree_separator": "和",
@@ -121,12 +118,5 @@
121
118
  "core.webview.embed_blocked": "當前 H5 頁面可能被目標站點限制嵌入",
122
119
  "core.webview.invalid_url": "缺少有效連結地址",
123
120
  "core.webview.open_failed": "連結無法開啟",
124
- "common.value.top_level": "頂級節點",
125
- "common.resource.default": "{resource}",
126
- "common.action.delete_resource": "刪除{resource}",
127
- "common.action.set_field_status": "設定{field}",
128
- "common.action.load_more": "載入更多",
129
- "common.status.loading": "載入中…",
130
- "core.navigation.analytics": "分析",
131
- "core.navigation.report": "報表"
121
+ "common.action.load_more": "載入更多"
132
122
  }
package/src/navigation.ts CHANGED
@@ -101,6 +101,16 @@ export const defaultAppMenus: AppMenu[] = [
101
101
  icon: 'MESSAGE_DEFAULT',
102
102
  selectedIcon: 'MESSAGE_SELECTED',
103
103
  },
104
+ {
105
+ id: 99050100,
106
+ parentId: 99040000,
107
+ name: 'AppMessageDetail',
108
+ path: 'app/message/detail',
109
+ viewKey: 'MESSAGE_DETAIL',
110
+ title: '',
111
+ access: 'AUTHENTICATED',
112
+ inTabBar: false,
113
+ },
104
114
  {
105
115
  id: 99090000,
106
116
  parentId: APP_MENU_ROOT_ID,
@@ -222,8 +232,9 @@ export async function initializeAppNavigation(): Promise<void> {
222
232
  const cached = readCachedMenus(cacheKey)
223
233
  useAppNavigation.setState(createNavigationState(filterUnavailableMenus(cached ?? localizedDefaultAppMenus()), false))
224
234
  try {
225
- const nextMenus = filterUnavailableMenus(normalizeMenuResponse(await adapter.list()))
226
- validateMenus(nextMenus)
235
+ const nextMenus = validateMenus(
236
+ filterUnavailableMenus(normalizeMenuResponse(await adapter.list())),
237
+ )
227
238
  Taro.setStorageSync(cacheKey, nextMenus)
228
239
  useAppNavigation.setState(createNavigationState(nextMenus, true))
229
240
  } catch (error) {
@@ -240,8 +251,7 @@ function filterUnavailableMenus(nextMenus: AppMenu[]): AppMenu[] {
240
251
 
241
252
  /** 使用已经校验的整份配置进行原子切换。 */
242
253
  export function installAppNavigation(nextMenus: AppMenu[]): void {
243
- validateMenus(nextMenus)
244
- const snapshot = nextMenus.map((menu) => ({ ...menu }))
254
+ const snapshot = validateMenus(nextMenus).map((menu) => ({ ...menu }))
245
255
  Taro.setStorageSync(resolveCacheKey(), snapshot)
246
256
  useAppNavigation.setState(createNavigationState(snapshot, true))
247
257
  }
@@ -382,9 +392,16 @@ function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
382
392
  const cached = Taro.getStorageSync<unknown>(cacheKey)
383
393
  if (!Array.isArray(cached)) return
384
394
  try {
385
- const cachedMenus = cached as AppMenu[]
386
- validateMenus(cachedMenus)
387
- return cachedMenus
395
+ const cachedMenus = (cached as AppMenu[]).map((menu) => ({
396
+ ...menu,
397
+ title:
398
+ menu.viewKey === 'HOME'
399
+ ? t('core.navigation.home')
400
+ : menu.viewKey === 'MESSAGE_INBOX'
401
+ ? t('core.navigation.message')
402
+ : menu.title,
403
+ }))
404
+ return validateMenus(cachedMenus)
388
405
  } catch {
389
406
  Taro.removeStorageSync(cacheKey)
390
407
  }
@@ -403,7 +420,12 @@ function normalizeMenu(item: BaseMenu): AppMenu {
403
420
  name: item.name,
404
421
  path: item.path,
405
422
  viewKey: app?.view_key ?? '',
406
- title: meta?.title ?? '',
423
+ title:
424
+ app?.view_key === 'HOME'
425
+ ? t('core.navigation.home')
426
+ : app?.view_key === 'MESSAGE_INBOX'
427
+ ? t('core.navigation.message')
428
+ : meta?.title ?? '',
407
429
  access: (app?.access ?? 'AUTHENTICATED') as AppMenuAccess,
408
430
  inTabBar: app?.in_tab_bar ?? false,
409
431
  icon: meta?.icon,
@@ -411,11 +433,17 @@ function normalizeMenu(item: BaseMenu): AppMenu {
411
433
  }
412
434
  }
413
435
 
414
- function validateMenus(nextMenus: AppMenu[]): void {
436
+ function validateMenus(nextMenus: AppMenu[]): AppMenu[] {
437
+ // 多端共用后端时菜单可能携带本应用未注册的视图键,过滤跳过而不是让整份菜单失效。
438
+ const knownMenus = nextMenus.filter((menu) => {
439
+ if (resolveStaticView(menu.viewKey)) return true
440
+ console.warn(t('core.navigation.error.view_key', { viewKey: menu.viewKey }))
441
+ return false
442
+ })
415
443
  const ids = new Set<number>()
416
444
  const names = new Set<string>()
417
445
  const paths = new Set<string>()
418
- for (const menu of nextMenus) {
446
+ for (const menu of knownMenus) {
419
447
  if (!Number.isSafeInteger(menu.id) || !menu.name || !menu.path.startsWith('app/')) {
420
448
  throw new Error(t('core.navigation.error.identity'))
421
449
  }
@@ -425,9 +453,6 @@ function validateMenus(nextMenus: AppMenu[]): void {
425
453
  if (!['PUBLIC', 'GUEST_ONLY', 'AUTHENTICATED'].includes(menu.access)) {
426
454
  throw new Error(t('core.navigation.error.access', { access: menu.access }))
427
455
  }
428
- if (!resolveStaticView(menu.viewKey)) {
429
- throw new Error(t('core.navigation.error.view_key', { viewKey: menu.viewKey }))
430
- }
431
456
  if (ids.has(menu.id) || names.has(menu.name) || paths.has(menu.path)) {
432
457
  throw new Error(t('core.navigation.error.unique'))
433
458
  }
@@ -435,8 +460,8 @@ function validateMenus(nextMenus: AppMenu[]): void {
435
460
  names.add(menu.name)
436
461
  paths.add(menu.path)
437
462
  }
438
- const menuMap = new Map(nextMenus.map((menu) => [menu.id, menu]))
439
- for (const menu of nextMenus) {
463
+ const menuMap = new Map(knownMenus.map((menu) => [menu.id, menu]))
464
+ for (const menu of knownMenus) {
440
465
  if (menu.parentId === undefined) {
441
466
  throw new Error(t('core.navigation.error.parent_missing', { name: menu.name }))
442
467
  }
@@ -457,6 +482,7 @@ function validateMenus(nextMenus: AppMenu[]): void {
457
482
  parentId = parent.parentId
458
483
  }
459
484
  }
460
- const tabCount = nextMenus.filter((menu) => menu.parentId === APP_MENU_ROOT_ID).length
485
+ const tabCount = knownMenus.filter((menu) => menu.parentId === APP_MENU_ROOT_ID).length
461
486
  if (tabCount === 1 || tabCount > 5) throw new Error(t('core.navigation.error.tab_count'))
487
+ return knownMenus
462
488
  }
package/src/pages.ts CHANGED
@@ -5,7 +5,6 @@ export const corePages: Record<string, KratosTaroPageConfig> = {
5
5
  'pages/index/index': {
6
6
  style: {
7
7
  navigationStyle: 'custom',
8
- navigationBarTextStyle: 'white',
9
8
  navigationBarTitleText: '',
10
9
  },
11
10
  },
@@ -131,3 +131,16 @@ html.theme-dark .taro__toast > div:last-child {
131
131
  color: var(--kratos-color-text) !important;
132
132
  }
133
133
  /* #endif */
134
+
135
+ /* H5 隐藏滚动条但保留滚动能力,视觉对齐小程序;小程序端滚动条本就不存在,无副作用。 */
136
+ /* #ifdef h5 */
137
+ ::-webkit-scrollbar {
138
+ width: 0;
139
+ height: 0;
140
+ display: none;
141
+ }
142
+
143
+ * {
144
+ scrollbar-width: none;
145
+ }
146
+ /* #endif */
@@ -19,40 +19,6 @@
19
19
  box-shadow: var(--kratos-glow-soft);
20
20
  }
21
21
 
22
- /* 四角 L 形角标:渲染为真实 view 节点(weapp 伪元素仅支持 view) */
23
- .k-cnr {
24
- position: absolute;
25
- width: 24px;
26
- height: 24px;
27
- border: 3px solid var(--kratos-color-primary);
28
- opacity: 0.5;
29
- pointer-events: none;
30
- }
31
- .k-cnr--tl {
32
- top: 16px;
33
- left: 16px;
34
- border-right: 0;
35
- border-bottom: 0;
36
- }
37
- .k-cnr--tr {
38
- top: 16px;
39
- right: 16px;
40
- border-left: 0;
41
- border-bottom: 0;
42
- }
43
- .k-cnr--bl {
44
- bottom: 16px;
45
- left: 16px;
46
- border-right: 0;
47
- border-top: 0;
48
- }
49
- .k-cnr--br {
50
- bottom: 16px;
51
- right: 16px;
52
- border-left: 0;
53
- border-top: 0;
54
- }
55
-
56
22
  /* 装饰网格背景 */
57
23
  .k-grid-bg {
58
24
  position: absolute;
@@ -181,31 +147,44 @@
181
147
  max-width: calc(100% - 190px);
182
148
  overflow: hidden;
183
149
  color: var(--kratos-color-text);
184
- font-size: 36px;
185
- font-weight: 800;
150
+ font-size: 16PX;
151
+ font-weight: 700;
186
152
  text-overflow: ellipsis;
187
153
  transform: translateX(-50%);
188
154
  white-space: nowrap;
189
155
  }
190
156
  .k-top {
157
+ position: relative;
191
158
  display: flex;
192
159
  align-items: center;
193
- justify-content: space-between;
160
+ /* 标题绝对居中后,流内子元素(右侧动作按钮)统一靠右排布 */
161
+ justify-content: flex-end;
194
162
  gap: 16px;
195
163
  /* weapp 原生胶囊避让:页面通过 getMpCapsuleRightGap() 写入该变量,
196
164
  其余平台未设置时回退 0,不影响布局。 */
197
165
  padding-right: var(--mp-capsule-gap, 0px);
198
166
  }
167
+ /* #ifdef weapp */
168
+ /* 双写类名提高优先级:页面级 padding 简写(如 .message-header)不得压掉导航带 */
169
+ .k-top.k-top {
170
+ /* 小程序右上角是原生胶囊:操作按钮靠左,标题行与胶囊同行(同 k-page-nav--weapp 几何) */
171
+ justify-content: flex-start;
172
+ height: calc(88px + 44PX);
173
+ padding-top: 44PX;
174
+ padding-right: 0;
175
+ }
176
+ /* #endif */
199
177
  .k-top__title {
200
- font-size: 38px;
201
- font-weight: 800;
202
- letter-spacing: -0.3px;
178
+ position: absolute;
179
+ left: 50%;
180
+ max-width: calc(100% - 190px);
181
+ overflow: hidden;
203
182
  color: var(--kratos-color-text);
204
- }
205
- .k-top__t2 {
206
- font-size: 32px;
183
+ font-size: 16PX;
207
184
  font-weight: 700;
208
- color: var(--kratos-color-text);
185
+ text-overflow: ellipsis;
186
+ transform: translateX(-50%);
187
+ white-space: nowrap;
209
188
  }
210
189
 
211
190
  /* 方形图标按钮 + 角标 */
@@ -306,12 +285,20 @@
306
285
  box-shadow: var(--kratos-glow-soft);
307
286
  }
308
287
 
309
- /* 过滤芯片 */
288
+ /* 过滤芯片:单行横向滚动,超出隐藏滚动条 */
310
289
  .k-chips {
311
290
  display: flex;
312
- flex-wrap: wrap;
291
+ flex-wrap: nowrap;
313
292
  gap: 16px;
314
293
  margin-bottom: 24px;
294
+ overflow-x: auto;
295
+ scrollbar-width: none;
296
+ }
297
+ .k-chips::-webkit-scrollbar {
298
+ display: none;
299
+ }
300
+ .k-chips .k-ch {
301
+ flex: 0 0 auto;
315
302
  }
316
303
  .k-ch {
317
304
  display: inline-flex;
@@ -437,6 +424,8 @@
437
424
  align-items: center;
438
425
  justify-content: center;
439
426
  gap: 16px;
427
+ /* 各端 H5 原生 button 默认宽度不一,显式通栏保证双端一致 */
428
+ width: 100%;
440
429
  height: 100px;
441
430
  margin-top: 36px;
442
431
  border-radius: 20px;
@@ -734,15 +723,30 @@
734
723
  background: var(--kratos-color-primary-soft);
735
724
  }
736
725
  .k-msg-item__title {
726
+ display: -webkit-box;
727
+ overflow: hidden;
737
728
  font-size: 25px;
738
729
  font-weight: 700;
739
730
  color: var(--kratos-color-text);
731
+ -webkit-box-orient: vertical;
732
+ -webkit-line-clamp: 2;
733
+ word-break: break-word;
740
734
  }
741
- .k-msg-item__meta {
735
+ .k-msg-item__foot {
736
+ display: flex;
737
+ align-items: center;
738
+ gap: 24px;
742
739
  margin-top: 10px;
740
+ }
741
+ .k-msg-item__meta {
742
+ flex: 1;
743
+ min-width: 0;
744
+ overflow: hidden;
743
745
  font-size: 20px;
744
746
  color: var(--kratos-color-text-muted);
745
747
  font-family: var(--kratos-font-mono);
748
+ text-overflow: ellipsis;
749
+ white-space: nowrap;
746
750
  }
747
751
  .k-msg-item__time {
748
752
  flex-shrink: 0;
@@ -763,3 +767,48 @@
763
767
  color: var(--kratos-color-primary);
764
768
  font-family: var(--kratos-font-mono);
765
769
  }
770
+
771
+ /* ============================================================
772
+ * 标准空态词汇(列表无数据时统一使用)
773
+ * ============================================================ */
774
+ .k-empty {
775
+ display: flex;
776
+ flex-direction: column;
777
+ align-items: center;
778
+ padding: 140px 32px;
779
+ }
780
+ .k-empty__icon {
781
+ display: flex;
782
+ align-items: center;
783
+ justify-content: center;
784
+ width: 112px;
785
+ height: 112px;
786
+ border-radius: 26px;
787
+ border: 1PX solid var(--kratos-color-border-accent);
788
+ background: var(--kratos-color-primary-soft-2);
789
+ color: var(--kratos-color-primary);
790
+ box-shadow: var(--kratos-glow-soft);
791
+ }
792
+ .k-empty__title {
793
+ margin-top: 28px;
794
+ color: var(--kratos-color-text);
795
+ font-size: 27px;
796
+ font-weight: 700;
797
+ }
798
+ .k-empty__desc {
799
+ margin-top: 12px;
800
+ color: var(--kratos-color-text-muted);
801
+ font-size: 22px;
802
+ font-family: var(--kratos-font-mono);
803
+ text-align: center;
804
+ }
805
+ .k-empty__act {
806
+ margin-top: 32px;
807
+ padding: 16px 40px;
808
+ border-radius: 999px;
809
+ border: 1PX solid var(--kratos-color-border-accent);
810
+ background: var(--kratos-color-primary-soft-2);
811
+ color: var(--kratos-color-primary);
812
+ font-size: 24px;
813
+ font-family: var(--kratos-font-mono);
814
+ }
package/src/utils/http.ts CHANGED
@@ -1,4 +1,4 @@
1
- import Taro from '@tarojs/taro'
1
+ import Taro, { getCurrentPages } from '@tarojs/taro'
2
2
  import {
3
3
  clearToken,
4
4
  getRefreshToken,
@@ -262,23 +262,41 @@ async function refreshAccessToken(): Promise<void> {
262
262
  setTokenExpiresIn(data.expires_in)
263
263
  }
264
264
 
265
+ /** 会话过期弹窗等待时长:H5 端路由切换会自动隐藏模态且 Promise 永不结算,超时释放避免提示永久卡死。 */
266
+ const RELOGIN_PROMPT_TIMEOUT = 60_000
267
+
268
+ /** 提示会话过期并进入携带本次业务回跳标记的登录页。 */
265
269
  async function promptRelogin(): Promise<void> {
270
+ // 401 来自旧会话的在途请求而当前已是有效登录态时直接忽略,避免刚登录成功被弹窗打断。
271
+ if (hasValidToken()) return
272
+ const pages = getCurrentPages()
273
+ // 登录页自身的请求失效只清理残留登录态,避免确认后重载登录页丢失已填写的表单。
274
+ if (pages[pages.length - 1]?.route === 'pages/login/login') {
275
+ if (getToken() || getRefreshToken() || Taro.getStorageSync('user')) silentClearAuthData()
276
+ return
277
+ }
266
278
  if (isPromptingRelogin) return
267
279
  isPromptingRelogin = true
268
280
  try {
269
281
  const theme = useThemeStore.getState().theme
270
- const modal = await Taro.showModal({
271
- title: t('common.title.notice'),
272
- content: t('core.auth.session_expired'),
273
- showCancel: false,
274
- confirmText: t('core.auth.login_again'),
275
- confirmColor: theme === 'light' ? '#039e8c' : '#28f0d4',
276
- })
277
- if (!modal.confirm) return
282
+ const modal = await Promise.race([
283
+ // 按钮文案复用通用确认键:平台对 confirmText 限制 4 个汉字/8 字符,长文案会被整体拒绝。
284
+ Taro.showModal({
285
+ title: t('common.title.notice'),
286
+ content: t('core.auth.session_expired'),
287
+ showCancel: false,
288
+ confirmText: t('common.action.confirm'),
289
+ confirmColor: theme === 'light' ? '#039e8c' : '#28f0d4',
290
+ }).catch(() => undefined as undefined),
291
+ new Promise<undefined>((resolve) =>
292
+ setTimeout(() => resolve(undefined), RELOGIN_PROMPT_TIMEOUT),
293
+ ),
294
+ ])
295
+ if (!modal?.confirm) return
278
296
  await new Promise((resolve) => setTimeout(resolve, 80))
279
297
  silentClearAuthData()
280
298
  saveCurrentRoute()
281
- await Taro.reLaunch({ url: '/pages/login/login' })
299
+ await Taro.reLaunch({ url: '/pages/login/login?redirect=1' })
282
300
  } finally {
283
301
  isPromptingRelogin = false
284
302
  }
@@ -1,7 +1,10 @@
1
1
  import Taro, { getCurrentPages } from '@tarojs/taro'
2
+ import { hasValidToken } from './auth'
2
3
 
3
4
  const LAST_ROUTE_KEY = 'lastRoute'
4
5
  const LOGIN_PAGE = '/pages/login/login'
6
+ /** 携带本次回跳标记的登录入口。 */
7
+ const LOGIN_REDIRECT_PAGE = `${LOGIN_PAGE}?redirect=1`
5
8
  /** 首页 tab 页面路径。 */
6
9
  export const homeTabPage = '/pages/index/index'
7
10
 
@@ -57,10 +60,27 @@ export function consumeLoginRedirectUrl(): string {
57
60
  return url
58
61
  }
59
62
 
60
- /** 登录成功后恢复访问目标,并仅在跳转成功后清理保存的地址。 */
63
+ /** prepareLoginPage 清理直接访问的旧目标,已登录时替换回首页。 */
64
+ export function prepareLoginPage(redirect?: string): boolean {
65
+ if (redirect !== '1') Taro.removeStorageSync(LAST_ROUTE_KEY)
66
+ if (!hasValidToken()) return true
67
+ void returnFromLogin()
68
+ return false
69
+ }
70
+
71
+ /** returnFromLogin 结束本次登录入口并替换到首页,避免返回受保护页面。 */
72
+ export async function returnFromLogin(): Promise<void> {
73
+ Taro.removeStorageSync(LAST_ROUTE_KEY)
74
+ Taro.setStorageSync('SwitchTabIndex', true)
75
+ await Taro.reLaunch({ url: homeTabPage })
76
+ }
77
+
78
+ /** restoreLoginRedirect 替换到本次访问目标,跳转成功后才清理保存的地址。 */
61
79
  export async function restoreLoginRedirect(): Promise<void> {
62
80
  const savedRoute = Taro.getStorageSync<string>(LAST_ROUTE_KEY) || ''
63
- const targetRoute = savedRoute ? normalizeRouteUrl(savedRoute) : homeTabPage
81
+ const normalizedRoute = normalizeRouteUrl(savedRoute)
82
+ const targetRoute =
83
+ savedRoute && !normalizedRoute.startsWith(LOGIN_PAGE) ? normalizedRoute : homeTabPage
64
84
  if (targetRoute.startsWith(homeTabPage)) Taro.setStorageSync('SwitchTabIndex', true)
65
85
  await Taro.reLaunch({ url: targetRoute })
66
86
  if (savedRoute && Taro.getStorageSync<string>(LAST_ROUTE_KEY) === savedRoute) {
@@ -73,9 +93,9 @@ export function navigateToLogin(redirectUrl?: string): void {
73
93
  if (typeof redirectUrl === 'string' && redirectUrl) saveLoginRedirectUrl(redirectUrl)
74
94
  else saveCurrentRoute()
75
95
  void Taro.navigateTo({
76
- url: LOGIN_PAGE,
96
+ url: LOGIN_REDIRECT_PAGE,
77
97
  fail: () => {
78
- void Taro.reLaunch({ url: LOGIN_PAGE })
98
+ void Taro.reLaunch({ url: LOGIN_REDIRECT_PAGE })
79
99
  },
80
100
  })
81
101
  }
@@ -85,9 +105,9 @@ export function redirectToLogin(redirectUrl?: string): void {
85
105
  if (typeof redirectUrl === 'string' && redirectUrl) saveLoginRedirectUrl(redirectUrl)
86
106
  else saveCurrentRoute()
87
107
  void Taro.redirectTo({
88
- url: LOGIN_PAGE,
108
+ url: LOGIN_REDIRECT_PAGE,
89
109
  fail: () => {
90
- void Taro.reLaunch({ url: LOGIN_PAGE })
110
+ void Taro.reLaunch({ url: LOGIN_REDIRECT_PAGE })
91
111
  },
92
112
  })
93
113
  }
@@ -10,8 +10,21 @@ page {
10
10
  }
11
11
 
12
12
  .home-page--weapp .home-content {
13
- /* 44PX 是系统导航栏高度,88px 按设计稿转换为 44 物理像素。 */
14
- padding-top: calc(44PX + 88px);
13
+ padding-top: 0;
14
+ }
15
+
16
+ /* 小程序端首页标题占导航带:与 k-page-nav--weapp 同几何,标题与胶囊同行 */
17
+ .home-page--weapp .home-top {
18
+ box-sizing: border-box;
19
+ height: calc(88px + 44PX);
20
+ padding: 44PX 4px 0;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ }
25
+
26
+ .home-page--weapp .home-top__title {
27
+ margin-top: 0;
15
28
  }
16
29
 
17
30
  .home-content {
@@ -24,13 +37,13 @@ page {
24
37
  padding: 8px 4px 24px;
25
38
  }
26
39
 
27
- /* 页面大标题 */
40
+ /* 页面大标题:与其他页面导航标题同规格,向原生导航栏看齐 */
28
41
  .home-top__title {
29
42
  margin-top: 14px;
30
43
  color: var(--kratos-color-text);
31
- font-size: 44px;
32
- font-weight: 800;
33
- letter-spacing: -0.5px;
44
+ font-size: 16PX;
45
+ font-weight: 700;
46
+ text-align: center;
34
47
  }
35
48
 
36
49
  .home-hero {
@@ -13,7 +13,7 @@ export default function HomePage() {
13
13
  const themeStore = useThemeStore()
14
14
  const settings = useSettingStore((state) => state.data)
15
15
  const loadData = useSettingStore((state) => state.loadData)
16
- const mainTitle = settings?.get('mainTitle') || t('core.home.main_title')
16
+ const mainTitle = t('core.navigation.home')
17
17
  const subTitle = settings?.get('subTitle') || t('core.home.sub_title')
18
18
  const configuredAppLogo = settings?.get('appLogo')
19
19
  const configuredLogoUrl = configuredAppLogo ? formatSrc(configuredAppLogo) : defaultLogo
@@ -36,7 +36,8 @@ export default function HomePage() {
36
36
  const demos = [
37
37
  [t('core.home.cross_platform'), t('core.home.cross_platform_description')],
38
38
  [t('core.home.account_capability'), t('core.home.account_capability_description')],
39
- [t('core.home.static_home'), t('core.home.static_home_description')],
39
+ [t('core.home.message_center'), t('core.home.message_center_description')],
40
+ [t('core.home.dynamic_config'), t('core.home.dynamic_config_description')],
40
41
  ]
41
42
  return (
42
43
  <View
@@ -51,10 +52,6 @@ export default function HomePage() {
51
52
  </View>
52
53
 
53
54
  <View className='k-glass k-glass--lit home-hero'>
54
- <View className='k-cnr k-cnr--tl' aria-hidden />
55
- <View className='k-cnr k-cnr--tr' aria-hidden />
56
- <View className='k-cnr k-cnr--bl' aria-hidden />
57
- <View className='k-cnr k-cnr--br' aria-hidden />
58
55
  <View className='home-hero__main'>
59
56
  <Image
60
57
  className='home-logo'
@@ -1,5 +1,5 @@
1
1
  import { Button, Checkbox, Image, Input, Picker, Text, View } from '@tarojs/components'
2
- import Taro, { useLoad } from '@tarojs/taro'
2
+ import Taro, { useDidShow, useLoad } from '@tarojs/taro'
3
3
  import { useEffect, useMemo, useRef, useState } from 'react'
4
4
  import defaultLogo from '@liujitcn/kratos-taro-app-core/static/images/logo_icon.png'
5
5
  import { defLoginService } from '../../../api/base/v1/login'
@@ -8,7 +8,7 @@ import { navigateAppRoute } from '../../../navigation'
8
8
  import { LoginStatus, type LoginRequest, type LoginResponse } from '../../../rpc/base/v1/login'
9
9
  import { useSettingStore, useThemeStore, useUserStore } from '../../../stores'
10
10
  import { formatSrc } from '../../../utils'
11
- import { homeTabPage, restoreLoginRedirect } from '../../../utils/navigation'
11
+ import { prepareLoginPage, restoreLoginRedirect, returnFromLogin } from '../../../utils/navigation'
12
12
  import { encryptPassword } from '../../../utils/secretCrypto'
13
13
  import { createWebAuthnCredential, getWebAuthnAssertion } from '../../../utils/webauthn'
14
14
  import MfaRecoveryCodesDialog from '../../../components/MfaRecoveryCodesDialog'
@@ -84,11 +84,9 @@ export default function LoginPage() {
84
84
  const settings = useSettingStore((state) => state.data)
85
85
  const themeStore = useThemeStore()
86
86
 
87
- /** 返回上一页;登录页可能是冷启动首页,无上级页面时回主页。 */
87
+ /** 返回首页并结束本次登录回跳,避免上一页再次要求登录。 */
88
88
  const goBack = () => {
89
- Taro.navigateBack({
90
- fail: () => void Taro.reLaunch({ url: homeTabPage }),
91
- })
89
+ void returnFromLogin()
92
90
  }
93
91
  const loadSettings = useSettingStore((state) => state.loadData)
94
92
  const userStore = useUserStore.getState()
@@ -281,7 +279,12 @@ export default function LoginPage() {
281
279
  return settingsPromise.current
282
280
  }
283
281
 
284
- useLoad(() => {
282
+ const loginRedirect = useRef<string>()
283
+ useLoad((options) => {
284
+ loginRedirect.current = options.redirect
285
+ })
286
+ useDidShow(() => {
287
+ if (!prepareLoginPage(loginRedirect.current)) return
285
288
  void loadLoginSettings()
286
289
  })
287
290
 
@@ -639,10 +642,6 @@ export default function LoginPage() {
639
642
  </View>
640
643
  </View>
641
644
  <View className='k-glass k-glass--lit login-panel'>
642
- <View className='k-cnr k-cnr--tl' aria-hidden />
643
- <View className='k-cnr k-cnr--tr' aria-hidden />
644
- <View className='k-cnr k-cnr--bl' aria-hidden />
645
- <View className='k-cnr k-cnr--br' aria-hidden />
646
645
  {process.env.TARO_ENV === 'h5' ? (
647
646
  <View className='login-form'>
648
647
  {showTenantCode ? (