docs/packages/component-migration-baseline.md 自动同步生成。本文冻结第一阶段 UI 迁移的规范来源和兼容目标,避免生成器任意选择某个应用目录作为唯一基准。
# 1. 审计范围
2026-09-09 对以下仓库的 src/components 进行了逐文件 SHA-256 对比:
g2rain-app-templateg2rain-cms-appg2rain-department-appg2rain-infra-appg2rain-manager-appg2rain-member-app
结论:公共目录高度重复,但 QueryForm、SortDialog、SortManagerButton 和 RemoteSelect 已有分叉。迁移采用逐文件基准,并在公共包中去除应用耦合。
# 2. 逐文件规范来源
| 能力 | 规范来源 | 选择理由 | 生成时处理 |
|---|---|---|---|
QueryForm | g2rain-cms-app | CMS、Department、Infra、Manager 四个仓库一致,包含较新的国际化文案 | 移除全局 $t 假设,改用 UI Translator 注入 |
TableSort/useTableSort.ts | 任一六仓库版本 | 六个仓库完全一致 | 保持算法和公开类型 |
TableSort/SortableTable.vue | 任一六仓库版本 | 六个仓库完全一致 | 将硬编码主色替换为 --g2-color-primary |
TableSort/TableColumn.vue | 任一六仓库版本 | 六个仓库完全一致 | 将字符串注入键改为导出的 Symbol 键 |
SortDialog.vue、SortManagerButton.vue | g2rain-cms-app | CMS、Department、Infra、Manager 四个仓库一致,包含国际化文案 | 使用 Translator,不依赖应用 i18n 实例 |
RemoteSelect/types.ts | 任一六仓库版本 | 六个仓库完全一致 | 用 unknown 和泛型替换公开 any |
RemoteSelect/index.vue | g2rain-manager-app | 包含其他版本能力并新增 focus、openDropdown | 保留 Expose;移除 Locale Store 和平台 i18n 导入 |
ApiSelect.vue、DictSelect.vue | g2rain-cms-app | CMS、Department、Infra、Manager 四个仓库一致 | 使用 Translator;API 由 Props 注入 |
以下为最初第一阶段的排除记录;后续决定将前三项公共化,当前实现与迁移行为以 平台数据组件 为准:
| 组件 | 原因 |
|---|---|
OrganSelect | 直接依赖 AccessToken Store 和组织领域语义 |
DictText | 直接依赖 Locale Store 与字典领域 API |
StatusSwitch | 直接依赖 Locale、i18n 和字典更新 API |
UserSelect | 当前仅 Department 存在,属于用户领域适配 |
OrganSelect、DictSelect、DictText、StatusSwitch 从 @g2rain/ui/platform 导出,通过数据 Provider 和回调隔离应用依赖;UserSelect 尚待后续实现。
# 3. UI 能力注入
公共组件不能依赖全局 $t、Pinia 或具体 i18n 实例。@g2rain/ui 只导出通用插件契约:
import type { App, InjectionKey } from 'vue'
export type G2rainTranslator = (key: string, fallback: string) => string
export interface G2rainUiOptions {
translate?: G2rainTranslator
locale?: () => string | undefined
}
export interface G2rainUiContext {
translate: G2rainTranslator
locale?: () => string | undefined
}
export const G2RAIN_UI_CONTEXT: InjectionKey<G2rainUiContext>
export const G2rainUi: { install(app: App, options?: G2rainUiOptions): void }
组织、字典和状态的数据 Provider 从 @g2rain/ui/platform 注入,不进入根插件:
import type { G2rainDataProviders } from '@g2rain/ui/platform'
export interface G2rainPlatformUiOptions {
dataProviders?: G2rainDataProviders
onMissingProvider?: (name: 'organ' | 'dict') => void
}
默认 Translator 返回 fallback。应用在组合根分别注入通用能力和平台数据:
import { G2rainUi } from '@g2rain/ui'
import { G2rainPlatformUi } from '@g2rain/ui/platform'
app.use(G2rainUi, {
translate: (key, fallback) => t(key, fallback),
locale: () => localeStore.locale,
})
app.use(G2rainPlatformUi, {
dataProviders: {
organ: {
loadOptions: params => organApi.select(params),
getPolicy: () => ({
defaultValue: tokenStore.isAdminCompany ? null : tokenStore.organId,
clearable: tokenStore.isAdminCompany,
autoSelectFirstWhenEmpty: !tokenStore.isAdminCompany && tokenStore.organId == null,
}),
},
dict: {
loadOptions: params => dictService.select(params),
},
},
})
dataProviders 与平台组件行为见平台数据组件。通用组件通过 useG2rainUi() 读取语言。useG2rainUi() 未安装插件时必须返回默认上下文,按需导入组件不能因此报错。平台组件通过 useG2rainPlatformUi() 读取 Provider;未安装时不发请求。
# 4. QueryForm 契约
第一版保持现有公开行为:
export interface QueryFormData {
id?: number
createTime?: [string, string]
updateTime?: [string, string]
sorts?: string[]
[key: string]: unknown
}
export interface QueryFormExpose {
updateSorts(sortParams: Record<string, string>): void
updateSortFromTable(sort: {
prop?: string
order?: 'ascending' | 'descending' | null
}): void
reset(): void
}
公开契约:
- 必填
v-model: QueryFormData。 - 事件:
search。 - Slots:默认 Slot、
actions。 - Reset 只清理基础字段,保留扩展字段和默认 Slot 管理的业务状态。
- 同时兼容
reactive对象和普通 Ref,不因整对象替换导致父级响应失效。 - 所有内置文案通过 Translator 获取并提供中文 fallback。
# 5. TableSort 契约
export type SortOrder = 'ascending' | 'descending'
export interface SortItem {
prop: string
order: SortOrder
}
export interface SortColumn {
prop: string
label: string
sortable?: boolean
}
export interface SortableTableExpose {
openSortConfig(): void
getSortParams(): Record<string, string>
getSortString(): string
tableRef: unknown
}
SortableTable:
- Props:
enableMultiSort?: boolean,默认true;initialSort?: SortItem[],默认空数组。 - 事件:
sort-change(sortParams: Record<string, string>)。 - Slot:默认 Slot。
- Expose:
openSortConfig、getSortParams、getSortString、tableRef。
TableColumn:
- 透传 Element Plus TableColumn Attributes 和默认/Header Slots。
- Props:
sortable?: boolean | 'custom'、sortBy?: string | ((row: unknown) => unknown)、sortOrders?: Array<SortOrder | null>。 - 默认排序顺序为 ascending、descending、null。
内部 Provide/Inject 必须使用包内导出的 Symbol,不使用 registerColumn、tableSort 等裸字符串,防止宿主冲突。
# 6. RemoteSelect 契约
export interface RemoteSelectFetchParams {
key?: string
value?: number
signal?: AbortSignal
}
export type RemoteSelectOption = Record<string, unknown>
export type FetchDataFunction<T extends RemoteSelectOption = RemoteSelectOption> = (
params: RemoteSelectFetchParams,
) => Promise<readonly T[]>
export interface RemoteSelectExpose {
focus(): Promise<void>
openDropdown(): Promise<void>
}
RemoteSelect:
- Props:
modelValue、fetchData、valueKey、labelKey、placeholder、clearable、disabled、width、debounceDelay、prefetchOnOpen、autoSelectFirstWhenEmpty。 - 默认值保持 Manager 基准:
valueKey=value、labelKey=label、宽度 200px、防抖 300ms,两个布尔增强项默认 false。 - 事件:
update:modelValue、change、clear。 - Expose:
focus、openDropdown。 - 新请求开始或组件卸载时中止旧请求;不支持 AbortSignal 的旧 API 可以忽略可选参数。
- 仅最后一次未取消请求可以更新 options 和 loading。
- Placeholder 通过 Translator 获取,显式 Prop 优先。
ApiSelect:
- Props 兼容 CMS 基准,并保留
allowEmptyKeyword和prefetchOnOpen。 - 默认
valueKey=id、labelKey=name、allowEmptyKeyword=false。 - API 方法适配为
FetchDataFunction,不导入具体 API。
DictSelect:
modelValue规范化为string | null | undefined。- API 方法由 Prop 注入,并允许附加
usageCode。 - 默认
valueKey=code、labelKey=name。
# 7. 生成与验证规则
- 生成前记录上述基准文件当时的 Git commit;若工作区未提交,记录
working-tree并在差异报告中说明。 - 不整目录复制;每个文件按表中来源提取。
- 先建立 characterization tests,冻结现有行为,再做能力注入和类型收紧。
- 去耦重构不得改变公开事件时序和默认值。
- 与基准有意不同的行为必须记录在迁移说明和 Changelog。
- 试点应用使用兼容转发入口,验证通过后再删除本地副本。