feat(web): 前端插槽设施,作品页与整理记录行挂「补字幕」
- lib/slots.ts 按位置注册(侧栏入口、作品页操作区 / 面板区、整理记录行操作、设置分区),默认直接显示; components/slots 挂载点:面板类直接渲染,动作类渲染成按钮 / 菜单项、对话框在菜单外渲染 - 作品页操作区与面板区、整理记录行菜单接入插槽;补字幕改由 features/subtitles.tsx 注册, 作品页按钮行为不变,整理记录行菜单新增「补字幕」(成功 / 部分成功的入库类整理) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
079c3eafe9
commit
62119a9b30
7 files changed
+313
-11
No files matched your search
@@ -0,0 +1,91 @@
|
||||
import type { MediaKind, PublicOrganizationRecord, SlotPosition } from "@umt/contracts";
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
// 前端插槽(ADR-006):功能模块按位置注册组件,页面在挂载点渲染。默认直接显示,不做开关(内部用,做了就是要用)。
|
||||
// 注册写在 app/features/*.tsx,由 app/features/index.ts 汇总;挂载点组件在 components/slots/slot-mounts.tsx。
|
||||
// 位置分两类:
|
||||
// - 动作类(entity.actions、organize.record.actions):一个按钮 / 菜单项,点了打开功能自己的对话框;
|
||||
// - 面板类(entity.panels、settings.sections、sidebar.entry):直接渲染组件。
|
||||
|
||||
/** 插槽里拿到的作品(作品页) */
|
||||
export interface SlotEntity {
|
||||
id: string;
|
||||
title: string;
|
||||
kind: MediaKind;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface SlotProps {
|
||||
"sidebar.entry": Record<string, never>;
|
||||
"entity.actions": { entity: SlotEntity };
|
||||
"entity.panels": { entity: SlotEntity };
|
||||
"organize.record.actions": { record: PublicOrganizationRecord };
|
||||
"settings.sections": Record<string, never>;
|
||||
}
|
||||
|
||||
export type ActionPosition = "entity.actions" | "organize.record.actions";
|
||||
export type PanelPosition = Exclude<SlotPosition, ActionPosition>;
|
||||
|
||||
interface SlotBase {
|
||||
/** 全局唯一,建议 <功能>.<位置> */
|
||||
id: string;
|
||||
/** 所属功能(contracts FEATURES 的 id) */
|
||||
feature: string;
|
||||
/** 越小越靠前,缺省 100;同序按注册顺序 */
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export interface ActionSlot<P extends ActionPosition = ActionPosition> extends SlotBase {
|
||||
label: string;
|
||||
icon?: ComponentType<{ className?: string }>;
|
||||
/** 不显示时返回 false(缺省显示) */
|
||||
visible?: (props: SlotProps[P]) => boolean;
|
||||
/** 点了之后渲染的对话框;关闭时调 onClose */
|
||||
Dialog: ComponentType<SlotProps[P] & { onClose: () => void }>;
|
||||
}
|
||||
|
||||
export interface PanelSlot<P extends PanelPosition = PanelPosition> extends SlotBase {
|
||||
Component: ComponentType<SlotProps[P]>;
|
||||
}
|
||||
|
||||
export type SlotEntry<P extends SlotPosition> = P extends ActionPosition
|
||||
? ActionSlot<P>
|
||||
: P extends PanelPosition
|
||||
? PanelSlot<P>
|
||||
: never;
|
||||
|
||||
export interface SlotRegistry {
|
||||
register<P extends SlotPosition>(position: P, entry: SlotEntry<P>): void;
|
||||
entries<P extends SlotPosition>(position: P): SlotEntry<P>[];
|
||||
}
|
||||
|
||||
export function createSlotRegistry(): SlotRegistry {
|
||||
const byPos = new Map<SlotPosition, SlotBase[]>();
|
||||
return {
|
||||
register(position, entry) {
|
||||
const list = byPos.get(position) ?? [];
|
||||
if (list.some((e) => e.id === entry.id)) throw new Error(`插槽 ${position} 已有 ${entry.id}`);
|
||||
byPos.set(position, [...list, entry]);
|
||||
},
|
||||
entries<P extends SlotPosition>(position: P) {
|
||||
const list = (byPos.get(position) ?? []) as SlotEntry<P>[];
|
||||
// 稳定排序:order 相同保持注册顺序
|
||||
return list
|
||||
.map((e, i) => ({ e, i }))
|
||||
.sort((a, b) => (a.e.order ?? 100) - (b.e.order ?? 100) || a.i - b.i)
|
||||
.map((x) => x.e);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 应用里唯一的插槽表(features/index.ts 往这里注册) */
|
||||
export const slots: SlotRegistry = createSlotRegistry();
|
||||
|
||||
/** 动作类插槽里当前可见的项 */
|
||||
export function visibleActions<P extends ActionPosition>(
|
||||
registry: SlotRegistry,
|
||||
position: P,
|
||||
props: SlotProps[P],
|
||||
): ActionSlot<P>[] {
|
||||
return (registry.entries(position) as ActionSlot<P>[]).filter((a) => a.visible?.(props) ?? true);
|
||||
}
|
||||
Reference in new issue
Block a user