逐文件精读源码生成。行号基于当前工作区版本,仅用于快速定位。
src/components/cards/<card-name>/ 下固定包含(11 张卡片 + _template 均如此;system-resource/、todo/ 为新增两卡):
| 文件 | 说明 |
|---|---|
<card-name>.meta.json |
元数据:id、name{zh,en}、description{zh,en}、icon、defaultSize{w,h}、version、可选 allowMultiple |
index.tsx |
默认导出组件,props 为 BaseCardProps(roleId/cardId/settings/isEditMode) |
locales/zh.json |
中文文案(由 loader.ts:21-24 的 import.meta.glob('../*/locales/*.json') 收集) |
locales/en.json |
英文文案(同上,与 zh 成对) |
其他 *.tsx |
可选的附属组件(弹窗、设置面板等) |
具体每张卡片的文件清单见:cards_character_list.md、cards_account_achievement.md、cards_attendance_domain_spaceship.md。
| 目录 | 文件(完整路径均以 src/components/cards/ 开头) |
|---|---|
registry/ |
src/components/cards/registry/index.ts、src/components/cards/registry/loader.ts、src/components/cards/registry/types.ts |
base/ |
src/components/cards/base/index.ts、src/components/cards/base/card-wrapper.tsx、src/components/cards/base/use-card-data.ts |
| (根) | src/components/cards/card-container.tsx、src/components/cards/card-context-menu.tsx |
_template/ |
src/components/cards/_template/_template.meta.json、src/components/cards/_template/index.tsx、src/components/cards/_template/README.md、src/components/cards/_template/locales/zh.json、src/components/cards/_template/locales/en.json |
character-list/ |
src/components/cards/character-list/character-list.meta.json、src/components/cards/character-list/index.tsx、src/components/cards/character-list/char-select-modal.tsx、src/components/cards/character-list/character-list-size-modal.tsx、src/components/cards/character-list/locales/zh.json、src/components/cards/character-list/locales/en.json |
account-info/ |
src/components/cards/account-info/account-info.meta.json、src/components/cards/account-info/index.tsx、src/components/cards/account-info/locales/zh.json、src/components/cards/account-info/locales/en.json |
account-progress/ |
src/components/cards/account-progress/account-progress.meta.json、src/components/cards/account-progress/index.tsx、src/components/cards/account-progress/locales/zh.json、src/components/cards/account-progress/locales/en.json |
achievement/ |
src/components/cards/achievement/achievement.meta.json、src/components/cards/achievement/index.tsx、src/components/cards/achievement/achievement-modal.tsx、src/components/cards/achievement/medal-browser.tsx、src/components/cards/achievement/locales/zh.json、src/components/cards/achievement/locales/en.json |
attendance/ |
src/components/cards/attendance/attendance.meta.json、src/components/cards/attendance/index.tsx、src/components/cards/attendance/attendance-settings-modal.tsx、src/components/cards/attendance/attendance-rewards.tsx、src/components/cards/attendance/attendance-rewards-modal.tsx、src/components/cards/attendance/locales/zh.json、src/components/cards/attendance/locales/en.json |
domain-info/ |
src/components/cards/domain-info/domain-info.meta.json、src/components/cards/domain-info/index.tsx、src/components/cards/domain-info/locales/zh.json、src/components/cards/domain-info/locales/en.json |
spaceship/ |
src/components/cards/spaceship/spaceship.meta.json、src/components/cards/spaceship/index.tsx、src/components/cards/spaceship/locales/zh.json、src/components/cards/spaceship/locales/en.json |
system-resource/ |
system-resource.meta.json、index.tsx、metrics.ts、metric-view.tsx、settings-modal.tsx、add-modal.tsx、locales/{zh,en}.json(详见 cards_system_todo.md) |
todo/ |
todo.meta.json、index.tsx、todo-detail.tsx、todo-modal.tsx、list-settings-modal.tsx、quick-add.ts、locales/{zh,en}.json(详见 cards_system_todo.md) |
职责:卡片系统唯一的类型契约文件,定义卡片元数据、组件 Props、本地化资源与卡片模块的标准结构。
导出:CardTag(interface)、CardMeta(interface)、BaseCardProps(interface)、CardLocales(interface)、CardModule(interface)。
主要依赖:无运行时依赖(纯类型文件);React.ComponentType 使用全局 React 类型(types.ts:35,未显式 import)。
| 符号 | 位置 | 说明 |
|---|---|---|
CardTag |
types.ts:2-5 | 卡片标签。字段:id: string(标签标识,用于筛选);label: Record<string, string>(多语言文案,如 {zh, en}) |
CardMeta |
types.ts:8-17 | 卡片元数据(对应 *.meta.json)。字段:id: string 卡片类型唯一标识(注册表 Map 的键);name: Record<string,string> 多语言名称;description: Record<string,string> 多语言描述;icon: string 图标名称或图标类名;defaultSize: {w:number; h:number} 默认网格尺寸;version: string 版本号;allowMultiple?: boolean 是否允许多实例(缺省视为 false);tags?: CardTag[] 多语言标签,用于添加卡片时搜索/筛选 |
BaseCardProps |
types.ts | 所有卡片组件必须接受的 Props。字段:roleId: string 当前角色 ID;cardId: string 卡片实例 ID;settings: any 卡片配置(由 dashboard 配置持久化);isEditMode?: boolean 仪表盘编辑模式标志;w?: number / h?: number 当前网格尺寸(2026-10 新增,由 CardContainer 传入,供卡片在设置中显示/修改尺寸) |
CardLocales |
types.ts:28-30 | 本地化资源容器:[language: string]: Record<string, string>(语言码 → 键值对) |
CardModule |
types.ts:33-38 | 卡片模块聚合体。字段:meta: CardMeta;component: React.ComponentType<BaseCardProps> 默认导出组件;startup?: (roleId: string) => Promise<void> 启动任务处理器;locales?: CardLocales 本地化资源 |
备注:
CardModule.locales仅为类型声明,loader.ts组装CardModule时从未写入该字段(见 loader.ts:71-81),本地化资源实际直接注入全局 i18n。- 类型文件中没有任何运行时校验逻辑;字段约束完全依赖 TypeScript。
职责:通过 Vite import.meta.glob 静态自动发现 cards/*/ 下的组件、元数据与本地化文件,构建并缓存卡片注册表(Map<meta.id, CardModule>),同时把卡片翻译注入 i18n 的 card 命名空间、把 startup 处理器注册到 CardStartupService。
导出:loadAllCards()、getAvailableCards()。
主要依赖:./types(CardModule/CardMeta/CardLocales 类型)、@/cards/startup-service(CardStartupService.register)、@/i18n(i18n.addResourceBundle)、Vite 的 import.meta.glob。
| 符号 | 位置 | 说明 |
|---|---|---|
cardModules |
loader.ts:7-13 | import.meta.glob('../*/index.tsx', { eager: true }),得到 路径 → { default: 组件, startup? } 的静态映射 |
cardMetas |
loader.ts:15-18 | import.meta.glob('../*/*.meta.json', { eager: true, import: 'default' }),直接得到已解析的 CardMeta 对象 |
cardLocaleFiles |
loader.ts:21-24 | import.meta.glob('../*/locales/*.json', { eager: true, import: 'default' }),得到 路径 → 翻译对象 |
cachedRegistry |
loader.ts:26 | 模块级缓存变量 Map<string, CardModule> | null,默认 null |
buildCardLocales()(私有) |
loader.ts:28-46 | 遍历 cardLocaleFiles,用正则 ../([^/]+)/locales/(\w+)\.json(:32)解析出目录名与语言码;跳过 _ 开头目录(:37);按语言 Object.assign 合并(:42),返回 CardLocales |
buildCardRegistry()(私有) |
loader.ts:48-86 | 先把 locales 注册进 i18n card 命名空间(:52-55),再遍历 cardMetas 组装注册表,见「备注」 |
loadAllCards() |
loader.ts:88-93 | 惰性单例:无缓存时调用 buildCardRegistry() 并缓存,之后始终返回同一 Map |
getAvailableCards() |
loader.ts:95-97 | Array.from(loadAllCards().values()).map(m => m.meta),返回所有已注册卡片的元数据数组(供「添加卡片」列表使用) |
备注:
- 自动发现机制:
- pattern 1
'../*/index.tsx'(:11)+eager: true—— 所有卡片目录的入口组件(含_template,无import选项故取整个模块 namespace)。 - pattern 2
'../*/*.meta.json'(:16)+eager: true, import: 'default'—— 所有元数据 JSON 的默认导出。 - pattern 3
'../*/locales/*.json'(:22)+eager: true, import: 'default'—— 所有卡片翻译文件。 - 三个 pattern 均以
../*/形式相对registry/展开,即覆盖src/components/cards/下所有一级子目录;文件头注释(:5-6)声称"排除 base、registry 和 _template",实际base/、registry/只是因不含index.tsx/*.meta.json而不匹配,_template则由_前缀判断显式跳过(:37、:63-65)。 eager: true意味着构建期同步打包:新卡片目录必须重启 dev server / 重新构建才会被发现,运行时无法动态注册。
- pattern 1
- meta 字段校验:没有任何 schema/字段校验。仅有两道过滤:路径正则不匹配则
continue(:33、:59),目录名以_开头则跳过(:37、:63-65)。后果:- JSON 语法错误 → Vite eager import 在构建/模块加载期直接抛错,无 try/catch,会导致应用启动失败(而非降级跳过)。
meta.id缺失 → 仍会执行cards.set(undefined, ...)(:81),注册表出现undefined键。name/icon/defaultSize等缺失 → 静默通过,在 UI 层才暴露问题。- 有
meta.json但同目录无index.tsx(或未 default 导出) →module取不到,该卡片被静默丢弃(:70、:82),无任何告警。
- 卡片列表缓存:
cachedRegistry(:26)在loadAllCards()首次调用时填充(:89-91),之后永久复用;副作用(i18n 注册 :54、CardStartupService.register:78)只在首次构建时执行一次,重复调用loadAllCards()不会重复注册。 - 本地化合并策略:按
glob返回的文件顺序Object.assign到同一语言对象(:42),不同卡片若使用相同 key 会互相覆盖(扁平合并且无卡片前缀)。 - 启动任务注册:
module.startup存在时写入cardModule.startup并调用CardStartupService.register(meta.id, module.startup)(:76-79),注册键是meta.id而非目录名。 - 三个 pattern 中的
*是 Vite glob 通配符(匹配任意一级子目录名),相对registry/目录解析。
职责:注册表的桶(barrel)出口,仅做聚合再导出。
导出:export * from './types'(index.ts:1)、export * from './loader'(index.ts:2),即对外暴露全部类型 + loadAllCards / getAvailableCards。
主要依赖:./types、./loader。
| 符号 | 位置 | 说明 |
|---|---|---|
| 类型再导出 | index.ts:1 | CardTag、CardMeta、BaseCardProps、CardLocales、CardModule |
| 函数再导出 | index.ts:2 | loadAllCards、getAvailableCards |
备注:实际调用方(如 card-container.tsx:22)多直接从 ./registry/loader 导入,本桶文件的使用率低。
职责:卡片通用数据加载 Hook,封装「请求 → data/isLoading/error 三态 + 手动 refetch + reloadKey 强制重载」的样板逻辑,供各卡片组件复用。
导出:useCardData<T>()。
主要依赖:react(useState/useEffect/useCallback/useRef)、@/utils/logger(logDebug/logError)。
| 符号 | 位置 | 说明 |
|---|---|---|
UseCardDataOptions<T>(接口,未导出) |
use-card-data.ts:4-10 | 字段:fetchData: () => Promise<T> 数据请求函数;defaultValue?: T 初始值;lazy?: boolean 懒加载开关(默认 false);reloadKey?: string | number | null 变化时强制重载(如 roleId 切换) |
useCardData<T>() |
use-card-data.ts:12-63 | 返回 { data, isLoading, error, refetch } |
─ data 初始值 |
use-card-data.ts:18 | 源码为 defaultValue || null:falsy 默认值(0/""/false)会被置为 null |
| ─ 初始 loading | use-card-data.ts:19 | useState(!lazy):非懒加载初始即 loading |
─ hasLoaded / hasLoadedRef |
use-card-data.ts:21-23 | 已加载标记,ref 供回调内读取最新值 |
─ fetchDataRef |
use-card-data.ts:25-26 | 保存最新 fetchData,避免回调依赖变动 |
─ loadData(force?) |
use-card-data.ts:28-45 | 守卫:hasLoaded && !force && !lazy 时直接返回(:29);否则置 loading → 执行请求 → 成功置 data/hasLoaded,失败置 error(logError),finally 收起 loading |
| ─ reloadKey effect | use-card-data.ts:47-55 | 用 prevReloadKeyRef 比对,检测到变化即重置 hasLoaded 并 loadData(true) |
| ─ 初始加载 effect | use-card-data.ts:57-61 | 仅 !lazy 时挂载即 loadData() |
| ─ 返回值 | use-card-data.ts:63 | { data, isLoading, error, refetch: loadData } |
备注:lazy: true 时守卫条件中的 !lazy 恒为假,refetch 永远会真正发起请求(即懒加载下手动 refetch 不会被去重拦截)。
职责:卡片通用容器组件,用错误边界 + Suspense 包裹卡片内容,崩溃时降级为「加载失败」占位、懒加载未就绪时显示骨架屏。
导出:CardWrapper()。
主要依赖:react(Component/Suspense/ErrorInfo/ReactNode)、@/components/ui/glass(GlassCard)、@/components/ui/loading-block(LoadingBlock)。
| 符号 | 位置 | 说明 |
|---|---|---|
CardWrapperProps(接口,未导出) |
card-wrapper.tsx:5-8 | children: ReactNode;fallback?: ReactNode 自定义错误占位 |
ErrorBoundaryState(接口,未导出) |
card-wrapper.tsx:10-12 | { hasError: boolean } |
CardErrorBoundary(类,未导出) |
card-wrapper.tsx:14-34 | getDerivedStateFromError 置 hasError(:20-22);componentDidCatch 仅 console.error(:24-26);render 出错时返回 props.fallback || <ErrorState/>(:28-33) |
CardWrapper() |
card-wrapper.tsx:36-42 | 结构:<CardErrorBoundary fallback><Suspense fallback={<LoadingSkeleton/>}>{children}</Suspense></CardErrorBoundary> |
LoadingSkeleton()(私有) |
card-wrapper.tsx:44-50 | GlassCard + LoadingBlock(minHeight={120}) |
ErrorState()(私有) |
card-wrapper.tsx:52-58 | GlassCard + 硬编码中文「加载失败」(:55) |
备注:错误兜底文案「加载失败」为硬编码中文、未走 i18n;全仓库未检索到对 CardWrapper 的 import(仅定义与导出),各卡片均直接用 useCardData 的 isLoading/error 自行渲染占位。
职责:base/ 目录的桶文件,聚合导出包装组件与数据 Hook。
导出:export * from './card-wrapper'(index.ts:1)、export * from './use-card-data'(index.ts:2)。
主要依赖:./card-wrapper、./use-card-data。
| 符号 | 位置 | 说明 |
|---|---|---|
| 再导出 | index.ts:1 | CardWrapper |
| 再导出 | index.ts:2 | useCardData |
备注:实际卡片均直接深路径导入(如 ../base/use-card-data),本桶文件当前无引用方。
职责:仪表盘卡片网格容器。负责卡片排序与默认坐标分配、dnd-kit 拖拽与网格吸附、落点高亮与碰撞检测、底部自动滚动/容器高度扩展、长按进入编辑模式、右键菜单、删除卡片、拖拽后布局持久化,以及按 card.type 从注册表取出组件并渲染(含拖拽浮层)。
导出:CONTAINER_HEIGHT(常量)、CardContainer()(组件)。
主要依赖:react、react-dom(createPortal)、react-i18next、@dnd-kit/core(DndContext/DragOverlay/PointerSensor/useSensor(s)/useDraggable)、@/components/ui/glass(GlassProgressCircle)、morphicons/react(MorphIcon)、lucide(MapPin/TriangleAlert)、@/types/dashboard(CardConfig)、@/utils/dashboardConfig(updateCardLayout)、@/hooks/useLongPressDrag、@/utils/logger、./registry/loader(loadAllCards)、./card-context-menu、@/components/ui/confirm-dialog(confirmDialog)。
| 区间 | 结构 / 职责 |
|---|---|
| 1-24 | import 声明 |
| 26-42 | 网格常量与背景:GRID_SIZE = 100(:27);generateGridSVG(isDragging) 生成虚线网格 data-URI(拖拽时透明度 0.3,否则 0.15,:30-42) |
| 43 | export const CONTAINER_HEIGHT = 2000(容器可用高度,被 src/pages/account.tsx:16 引用) |
| 45-51 | FreeDragCardProps(接口,未导出):card/roleId/isEditMode/onRemoveCard/onUpdatePosition |
| 53-445 | FreeDragCard()(私有组件):单张可拖拽卡片外壳,见下方细分行 |
| 447-456 | CardContainerProps(接口,未导出):roleId、tabId?、cards: CardConfig[]、onRemoveCard、isEditMode?、onEnterEditMode?、onExitEditMode?、highlightCardId? |
| 458-957 | CardContainer()(导出组件):容器主体,见下方细分行 |
| 行号 | 逻辑 |
|---|---|
| 71 | useTranslation() |
| 73-78 | useDraggable({ id: card.id, disabled: !isEditMode }),解构 setNodeRef/attributes/listeners/transform |
| 81-91 | 接入 useLongPressDrag(longPressProgress、handlePointerDown/Up/Leave、triggerDragStart) |
| 94-99 | isDraggingRef 同步最新拖拽状态 |
| 102-132 | 编辑模式退出判定:编辑态下 pointerup 且未拖拽 → 延时 50ms 检查 isDraggingRef,仍未拖拽则 onExitEditMode() |
| 135-147 | 进入编辑模式且用户仍按住时,补触发 triggerDragStart(listeners.onPointerDown)(长按直接进入拖拽) |
| 149-159 | 右键菜单状态:contextMenu {x,y} + handleContextMenu(preventDefault/stopPropagation 后记录坐标) |
| 161-283 | 右键菜单项构建 getContextMenuItems():character_list → view-list/change-role(:164-186);attendance → settings(:187-198);achievement → select-medals/change-role(:199-221);account_info/account_progress/domain_info/spaceship → change-role(:222-238),其中 domain_info 追加 view-list/switch-domain(:239-262);末尾统一追加危险项「删除」(:265-280),点击经 confirmDialog 确认后 onRemoveCard(card.id) |
| 285-287 | 网格坐标 → 像素坐标(x*GRID_SIZE+1、y*GRID_SIZE+1,留 1px 间隙) |
| 290-303 | 样式计算:position:absolute、宽 (w??3)*GRID_SIZE-2、高 (h??2)*GRID_SIZE-2(尺寸读取/默认 3×2 在 :294-295)、拖拽 transform、编辑态 cursor:grab、拖拽中原卡片 opacity:0 + visibility:hidden(防残影) |
| 305-342 | 卡片外壳 JSX:data-card-id、highlightCardId 命中时加 card-flash-highlight(:311);指针事件路由:编辑态交由 dnd-kit listeners(:317-320、:331-335),非编辑态走长按检测(:322-324、:337-341) |
| 344-362 | 长按进度环(GlassProgressCircle + 百分比文字),仅非编辑态且进度 > 0 时显示 |
| 365-371 | 拖拽中在卡片上方显示 Row/Col 网格坐标角标 |
| 374-405 | 删除按钮(编辑态且非拖拽中,卡片内右上角删除图标,避免被卡片外壳裁切):onClick → onRemoveCard,并在 pointer 事件上 stopPropagation 防止误触父级 |
| 407-432 | 卡片渲染:loadAllCards() → cardRegistry.get(card.type)?.component(:410-411);未注册类型渲染 Unknown card type 错误占位(:413-421);命中则以 roleId/cardId/settings/isEditMode 实例化组件(:424-430) |
| 435-442 | 渲染 <CardContextMenu>(坐标、菜单项、onClose) |
| 行号 | 逻辑 |
|---|---|
| 469-481 | 状态:sortedCards、activeId、isDragging、mousePosition、extraHeight、draggedViaLongPress、highlightGrid{x,y,w,h}、hasCollision |
| 484-505 | 排序与默认坐标分配:按 position 升序;缺 x/y 的卡片按每行 3 张、行高 2 网格排布,w ?? 3、h ?? 2(:491-499);依赖 cards.length 触发(:505) |
| 508-520 | containerHeight 计算:最大 ((y+h) * GRID_SIZE) + 100 基础留白 + extraHeight(空列表返回 100) |
| 523-529 | dnd-kit 传感器:PointerSensor,activationConstraint.distance = 5 |
| 532-534 | snapToGrid(value) = 四舍五入到 GRID_SIZE 倍数 |
| 537-541 | handleDragStart:记录 activeId、置 isDragging |
| 544-562 | 拖拽期间监听 mousemove + pointermove 更新鼠标坐标(window 级) |
| 565-585 | 鼠标距底边 < 150px 时按 50ms 定时器自动向下滚动(越近越快) |
| 588-684 | handleDragMove:底部 200px 内滚动并 setExtraHeight 扩容(:611-617);由 delta 换算目标网格并做矩形碰撞检测(:619-657);更新 highlightGrid/hasCollision(:657-683) |
| 687-780 | handleDragEnd:清理状态(activeId/isDragging/extraHeight/highlightGrid/hasCollision,:698-702);长按进入编辑的拖拽结束后退出编辑模式(:706-713);无 delta 提前返回(:716-719);吸附换算回网格坐标并 Math.max(0,…) 夹取(:728-733);碰撞则回退、不落盘(:739-763);否则本地更新 sortedCards(:766-770)并调用 updateCardLayout(tabId ?? roleId, card.id, {x,y,w,h}) 持久化(:773-779) |
| 783-785 | activeCard:为 DragOverlay 找到被拖拽卡片 |
| 788-794 | 渲染状态调试日志 effect |
| 796-823 | 空状态:cards.length === 0 时渲染 dashboard.no_cards 引导占位并提前 return |
| 825-831 | <DndContext sensors onDragStart onDragMove onDragEnd> |
| 832-839 | 容器背景:编辑态或拖拽中显示 generateGridSVG 网格 |
| 841-866 | 落点高亮格:碰撞时红色(hasCollision),否则主色;中心显示 (x, y) WxH 与 MapPin/TriangleAlert 图标,data-testid="highlight-grid" |
| 867-891 | 行列坐标标签(行号按 containerHeight/GRID_SIZE 数量,列号固定 20 列) |
| 893-917 | 卡片列表渲染:sortedCards.map 输出 FreeDragCard,传入 isDragging={activeId===card.id}、showGridCoords、highlightCardId、onLongPress(进入编辑并标记 draggedViaLongPress,:910-913)、onExitEditMode;onUpdatePosition 传空实现(:906) |
| 920-954 | 拖拽浮层:createPortal(..., document.body) 包裹 <DragOverlay dropAnimation={null}>,浮层内再次按 activeCard.type 查注册表渲染组件(:931-949),带 opacity-80 scale-105 rotate-2 与卡片尺寸(:924-929) |
| 955-957 | </DndContext> 收尾 |
备注:
- 编辑模式:由父级通过
isEditMode/onEnterEditMode/onExitEditMode控制;进入 = 长按卡片(onLongPress→ :910-913),退出 = 编辑态下点按未拖拽(:113-130)或长按触发的拖拽结束(:706-713)。编辑态下才挂载 dnd-kitattributes/listeners(:313)与删除按钮(:374)。 - 拖拽:网格吸附拖拽(非自由像素拖拽),1 格 = 100px;拖拽中原卡片隐藏,真实视觉由
DragOverlayportal 提供(:921-954)。 - 右键菜单:状态与菜单项在
FreeDragCard内,渲染走CardContextMenu,删除项带二次确认。除上表按类型分支外,另有两组:system_resource→system-resource-settings(打开设置弹窗)/ 派发manualRefresh(立即刷新);todo→todo-edit/todo-open-page/todo-rename/todo-duplicate/todo-sort/todo-hide-completed/todo-clear-completed/todo-color-icon/todo-delete-list,全部经cardAction事件由卡片自身处理。 - 尺寸调整:容器内没有交互式 resize 手柄;宽高完全来自
CardConfig.w/h(默认3×2),读取与兜底见 :294-295、:497-499、:636-637、:735-736、:927-928;拖拽只改变x/y,w/h仅在dragEnd的updateCardLayout中原样回写(:774-779)。 - 事件:文件内无 Tauri
invoke/emit/listen;对外通信使用window.dispatchEvent(new CustomEvent("cardAction", { detail: { cardId, action } })),共 8 处(:169、:180、:192、:204、:215、:232、:244、:255),action 取值view-list/change-role/settings/select-medals/switch-domain;监听方为各卡片自身的window.addEventListener("cardAction", ...)。另有拖拽期的window.addEventListener("mousemove"/"pointermove")(:556-557)。 - 持久化统一走
updateCardLayout(tabId, cardId, layout)(src/utils/dashboardConfig.ts:149-164:改x/y/w/h后saveTabCards),本文件不直接触碰存储/IPC。 - 已知冗余:
FreeDragCardProps.onUpdatePosition(:50)虽为必填 Props(形参未解构),但在FreeDragCard体内从未被调用,容器传入空实现(:906)。 - 调试日志较密集(
logger.info分布于 :103-146、:540、:659-675、:688-703、:789-794)。
职责:通用右键菜单组件,以 portal 挂到 document.body,支持点击外部 / Esc / 滚动自动关闭,菜单项点击后回调并关闭。
导出:ContextMenuItem(interface)、CardContextMenu()(组件)。
主要依赖:react(useEffect/useRef)、react-dom(createPortal)。
| 符号 | 位置 | 说明 |
|---|---|---|
ContextMenuItem |
card-context-menu.tsx:4-9 | 字段:key: string(React key);label: string 显示文案;danger?: boolean 危险项样式(红色);onPress: () => void 点击回调 |
CardContextMenuProps(接口,未导出) |
card-context-menu.tsx:11-16 | x: number、y: number 菜单坐标;items: ContextMenuItem[];onClose: () => void |
CardContextMenu() |
card-context-menu.tsx:18-72 | Portal 固定定位菜单;布局后根据实际宽高夹紧至视口内,过高时滚动;菜单项背景内缩并使用圆角 |
| ─ 关闭逻辑 effect | card-context-menu.tsx:26-44 | document mousedown 点击菜单外关闭(:27-31);document keydown Esc 关闭(:32-34);window scroll(capture)关闭(:35);清理时逐一移除(:39-43) |
| ─ 菜单项点击 | card-context-menu.tsx:61-64 | item.onPress() 后立即 onClose() |
| ─ portal | card-context-menu.tsx:46-71 | createPortal(..., document.body) |
备注:无 IPC、无全局事件;纯展示 + DOM 事件组件。
职责:新卡片开发模板(默认导出组件),演示 BaseCardProps 接入、useCardData 数据加载的三态渲染、settings 读取、编辑模式差异显示,并以注释形式给出 startup 导出约定。
导出:export default TemplateCard(index.tsx:38)。
主要依赖:../registry/types(BaseCardProps)、../base/use-card-data(useCardData)、react-i18next(useTranslation)、@/components/ui/glass(GlassCard/GlassProgressCircle)、@/utils/logger。
| 符号 | 位置 | 说明 |
|---|---|---|
startup 示例注释 |
_template/index.tsx:7-24 | 说明可选 startup(task) 导出的用法:CardStartupService.addTask 登记 → runAll() 匹配 cardType 调用 → subscribe 监听状态(仅注释,未实现) |
| 开发步骤注释 | _template/index.tsx:26-37 | 复制目录、重命名 meta.json、改 meta、实现组件、可选导出 startup |
TemplateCard(default) |
_template/index.tsx:38-115 | 解构 roleId/cardId/settings/isEditMode |
| ─ 数据加载 | _template/index.tsx:47-63 | useCardData({ fetchData: 模拟 500ms Promise, lazy: false }),logger.info 打印 roleId |
| ─ settings 读取 | _template/index.tsx:66 | settings.customKey || "default value" |
| ─ loading 分支 | _template/index.tsx:69-80 | GlassCard + 不确定进度的 GlassProgressCircle |
| ─ error 分支 | _template/index.tsx:83-91 | GlassCard + t("common.load_error") || "加载失败" |
| ─ 正常渲染 | _template/index.tsx:94-114 | 标题 t("template_card.title")、展示 roleId/cardId/customSetting(:101-105)、非编辑态追加交互提示 t("template_card.hint")(:107-111) |
备注:
- 模板不会被注册:loader 对
_前缀目录显式跳过(loader.ts:37、:63-65),其meta.json与locales/zh.json均不进入注册表。 t("template_card.title") || "Template Card"形式的兜底在 i18next 下基本无效——缺失键时t()返回键字符串本身(truthy),||分支不会命中;且模板键使用template_card.前缀,与 loader 注册的card命名空间(t("card:key"))不一致。- 模板自带的
locales/zh.json键为title/description,若目录不以_开头会被平铺注册为card:title/card:description。
职责:模板卡片的元数据样例,同时是所有 *.meta.json 的字段契约参考。
导出:(JSON 默认导出对象,经 import.meta.glob(..., import: 'default') 消费)
主要依赖:由 registry/loader.ts:15-18 加载。
| 符号 | 位置 | 说明 |
|---|---|---|
id |
_template.meta.json:2 | "_template" —— 全局唯一卡片类型标识,注册表 Map 的键 |
name |
_template.meta.json:3-6 | {zh: "卡片模板", en: "Card Template"} 多语言名称 |
description |
_template.meta.json:7-10 | {zh, en} 多语言描述 |
icon |
_template.meta.json:11 | "file" —— 图标类名 |
defaultSize |
_template.meta.json:12 | { w: 3, h: 2 } 默认网格尺寸 |
version |
_template.meta.json:13 | "1.0.0" |
allowMultiple |
_template.meta.json:14 | true —— 允许多实例 |
tags |
_template.meta.json:15-18 | 两个标签:{id:"template", label:{zh:"模板", en:"Template"}}、{id:"development", label:{zh:"开发", en:"Development"}} |
supportedGames |
_template.meta.json | 模板未声明 supportedGames(= 两个游戏都支持)。真实卡片见各自 meta.json:终末地独占的四张(character_list/achievement/domain_info/spaceship)声明 ["endfield"],与游戏无关的两张(system_resource/todo)声明 [] |
meta.json 完整字段契约:
| 字段 | 类型 | 必填 | 含义 / 缺失后果 |
|---|---|---|---|
id |
string |
是 | 卡片类型唯一标识与注册键;缺失会导致 cards.set(undefined, …) |
name |
Record<string,string> |
是 | 多语言名称(zh/en),添加卡片列表展示用 |
description |
Record<string,string> |
是 | 多语言描述 |
icon |
string |
是 | 图标名称或图标类名 |
defaultSize |
{ w: number, h: number } |
是 | 新增卡片时的默认网格占位 |
version |
string |
是 | 卡片版本号(当前无消费方做校验) |
allowMultiple |
boolean |
否(默认 false) | 是否允许多实例 |
tags |
CardTag[]({id, label{lang}}) |
否 | 搜索/筛选标签 |
supportedGames |
("endfield" | "arknights")[] |
否 | 该卡片支持的游戏。未声明 = 两个游戏都支持(新卡片默认不阻塞用户);[] = 与游戏无关(系统资源、待办);单个元素 = 该游戏独占。@/config/gameSupport 的 CARD_GAME_SUPPORT 为准,此字段仅作自描述文档(两处都写却只生效一处更容易踩坑) |
备注:
- 加载失败行为:loader 无运行时 schema 校验,也无 try/catch——
- JSON 语法错误 → Vite
eager: true在构建/模块加载期抛错,应用无法启动; - 有 meta 但缺
index.tsx→ 静默跳过该卡片(loader.ts:70、:82); - 目录名以
_开头 → 跳过(loader.ts:37、:63-65); - 路径不匹配正则 →
continue(loader.ts:33、:59); - 字段缺失(非
id)→ 无告警照常注册,由 UI 层暴露问题。
- JSON 语法错误 → Vite
职责:卡片开发流程说明文档(模板目录的一部分),指导复制模板、重命名元数据、实现组件、配置本地化与调试。 导出:(Markdown 文档,无代码导出) 主要依赖:无。
| 符号 | 位置 | 说明 |
|---|---|---|
| 「快速开始」 | README.md:3-36 | 6 步:复制目录(cp -r _template my-new-card,:7-9)、重命名 meta.json(:13-15)、编辑元数据(:17-21)、实现组件(:23-26)、创建 locales/zh.json+en.json(:28-32)、重启 dev server 验证(:34-36) |
| 「文件说明」 | README.md:38-45 | _template.meta.json、index.tsx、locales/、README.md 各自作用 |
| 「本地化翻译」 | README.md:47-68 | 翻译放卡片自己的 locales/ 而非全局 translation.json;键注册到 i18n card 命名空间,用 t("card:key") 访问(示例 :52-66) |
| 「注意事项」 | README.md:70-75 | _ 开头目录不注册;id 全局唯一;组件必须 export default;Props 必须符合 BaseCardProps |
| 「参考资源」 | README.md:77-81 | 指向 docs/card_development.md、registry/types.ts、character-list/ 示例 |
备注:文档描述与实现一致(_ 前缀跳过、card 命名空间注册均对应 loader.ts:37/54/63)。
职责:模板卡片的中文翻译资源样例(键平铺,无命名空间包裹)。
导出:(JSON 默认导出 Record<string, string>)
主要依赖:由 registry/loader.ts:21-24 扫描 ../*/locales/*.json 加载。
| 符号 | 位置 | 说明 |
|---|---|---|
title |
locales/zh.json:2 | "模板卡片" |
description |
locales/zh.json:3 | "这是一个卡片模板" |
备注:因目录名 _template 以 _ 开头,本文件实际会被 buildCardLocales()(loader.ts:37)跳过,仅作为格式示例;非 _ 目录下的同结构文件会被合并进 i18n card 命名空间,以 t("card:title") 访问。
- Tauri
invoke("xxx"):无。本组文件不含任何@tauri-apps/api调用;落盘统一经updateCardLayout()(src/utils/dashboardConfig.ts:149)间接完成。 - Tauri
emit/listen:无。 window.dispatchEvent(new CustomEvent("cardAction", { detail: { cardId, action } }))(card-container.tsx):action =view-list(:169、:244)、change-role(:180、:215、:232)、settings(:192)、select-medals(:204)、switch-domain(:255)。监听方位于各卡片组件(如character-list/index.tsx:187、achievement/index.tsx:223、domain-info/index.tsx:183、spaceship/index.tsx:252、account-info/index.tsx:142、account-progress/index.tsx:121、attendance/index.tsx:259)。cardAction新增动作(2026-10):system-resource-settings(system-resource 卡片打开设置弹窗)、todo-edit/todo-open-page/todo-rename/todo-duplicate/todo-sort/todo-hide-completed/todo-clear-completed/todo-color-icon/todo-delete-list(todo 卡片处理);另有manualRefresh全局事件用于系统资源卡片的「立即刷新」。详见 cards_system_todo.md。- 卡片改尺寸事件(2026-10):
CardContainer监听window事件cardResize(detail{cardId,w,h}),更新本地sortedCards并调用updateCardLayout(tabId ?? roleId, …)落盘,随后广播cardLayoutChanged(pages/dashboard.tsx监听后重新拉取当前标签页配置)。卡片组件自身不持有tabId,因此用事件解耦。 - DOM 监听:card-context-menu.tsx:36-38(
mousedown/keydown/scroll)、card-container.tsx:556-557(mousemove/pointermove)。