Commit 00b6e86c authored by drigle's avatar drigle

feat: group model batches by model

parent d99022c1
{
"name": "model_analysis",
"version": "1.0.2",
"version": "1.0.3",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
"author": "yang"
"author": "yang",
"spa": true
}
......@@ -326,6 +326,21 @@ export default function Models() {
return Number(right) - Number(left);
}), [batchesByYear]);
const activeYearEntries = batchesByYear[activeBatchYear] || [];
const activeYearModelGroups = useMemo(() => {
const grouped = new Map();
activeYearEntries.forEach((entry) => {
const modelKey = `${entry.sourceModel.source_model_id || entry.sourceModel.model_code || entry.sourceModel.model_name}`;
if (!grouped.has(modelKey)) {
grouped.set(modelKey, {
key: modelKey,
model: entry.sourceModel,
entries: [],
});
}
grouped.get(modelKey).entries.push(entry);
});
return [...grouped.values()];
}, [activeYearEntries]);
const activeYearSelectableEntries = activeYearEntries.filter((entry) => !syncedBatchCodes.includes(entry.key));
const allActiveYearSelected = activeYearSelectableEntries.length > 0
&& activeYearSelectableEntries.every((entry) => selectedBatchCodes.includes(entry.key));
......@@ -337,8 +352,8 @@ export default function Models() {
: current.filter((code) => !activeCodes.includes(code)));
}
function handleActiveYearBatchSelection(keys) {
const selectableCodes = new Set(activeYearSelectableEntries.map((entry) => entry.key));
function handleBatchSelection(keys, entries) {
const selectableCodes = new Set(entries.filter((entry) => !syncedBatchCodes.includes(entry.key)).map((entry) => entry.key));
const nextCodes = keys === 'all' ? selectableCodes : new Set(keys);
setSelectedBatchCodes((current) => [
...current.filter((code) => !selectableCodes.has(code)),
......@@ -521,32 +536,51 @@ export default function Models() {
{activeYearEntries.length ? (
<ScrollShadow className="max-h-[48vh] overflow-y-auto" orientation="vertical" size={24}>
<ListView
aria-label="当前年份可同步批次"
disabledKeys={new Set(activeYearEntries.filter((entry) => syncedBatchCodes.includes(entry.key)).map((entry) => entry.key))}
selectedKeys={new Set(activeYearEntries.filter((entry) => selectedBatchCodes.includes(entry.key)).map((entry) => entry.key))}
selectionBehavior="toggle"
selectionMode="multiple"
variant="primary"
onSelectionChange={handleActiveYearBatchSelection}
>
{activeYearEntries.map((entry) => {
const synced = syncedBatchCodes.includes(entry.key);
<div className="space-y-3 pr-1">
{activeYearModelGroups.map((modelGroup) => {
const groupSelectableEntries = modelGroup.entries.filter((entry) => !syncedBatchCodes.includes(entry.key));
const groupSelectedCount = groupSelectableEntries.filter((entry) => selectedBatchCodes.includes(entry.key)).length;
return (
<ListView.Item id={entry.key} key={entry.key} textValue={entry.sourceBatch.batch_name}>
<ListView.ItemContent>
<span className="min-w-0 flex-1">
<ListView.Title className="block">{entry.sourceBatch.batch_name}</ListView.Title>
<ListView.Description className="block tabular-nums">{entry.sourceModel.model_name} · {entry.sourceModel.model_code} · {formatDateTime(entry.sourceBatch.source_batch_date)} · {entry.sourceBatch.batch_code}</ListView.Description>
<section className="overflow-hidden rounded-xl border border-default bg-surface" key={modelGroup.key}>
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-default bg-surface-secondary px-4 py-3">
<div className="min-w-0">
<h3 className="truncate text-sm font-semibold text-foreground">{modelGroup.model.model_name}</h3>
<p className="mt-0.5 truncate text-xs text-muted">{modelGroup.model.model_code}</p>
</div>
<span className="shrink-0 text-xs tabular-nums text-muted">
{modelGroup.entries.length} 个批次{groupSelectedCount ? ` · 已选 ${groupSelectedCount}` : ''}
</span>
</ListView.ItemContent>
<ListView.ItemAction>
{synced ? <Tag tone="emerald">已同步</Tag> : <Description className="tabular-nums">{Number(entry.sourceBatch.source_model_data_count || 0).toLocaleString('zh-CN')} 条</Description>}
</ListView.ItemAction>
</ListView.Item>
</div>
<ListView
aria-label={`${modelGroup.model.model_name} 批次`}
disabledKeys={new Set(modelGroup.entries.filter((entry) => syncedBatchCodes.includes(entry.key)).map((entry) => entry.key))}
selectedKeys={new Set(modelGroup.entries.filter((entry) => selectedBatchCodes.includes(entry.key)).map((entry) => entry.key))}
selectionBehavior="toggle"
selectionMode="multiple"
variant="primary"
onSelectionChange={(keys) => handleBatchSelection(keys, modelGroup.entries)}
>
{modelGroup.entries.map((entry) => {
const synced = syncedBatchCodes.includes(entry.key);
return (
<ListView.Item id={entry.key} key={entry.key} textValue={entry.sourceBatch.batch_name}>
<ListView.ItemContent>
<span className="min-w-0 flex-1">
<ListView.Title className="block">{entry.sourceBatch.batch_name}</ListView.Title>
<ListView.Description className="block tabular-nums">{formatDateTime(entry.sourceBatch.source_batch_date)} · {entry.sourceBatch.batch_code}</ListView.Description>
</span>
</ListView.ItemContent>
<ListView.ItemAction>
{synced ? <Tag tone="emerald">已同步</Tag> : <Description className="tabular-nums">{Number(entry.sourceBatch.source_model_data_count || 0).toLocaleString('zh-CN')} 条</Description>}
</ListView.ItemAction>
</ListView.Item>
);
})}
</ListView>
</section>
);
})}
</ListView>
</div>
</ScrollShadow>
) : <EmptyCollection title="该年份没有来源批次" description="请选择其他年份查看可同步批次。" />}
</div>
......
......@@ -60,7 +60,7 @@ loadSourceBatchData(sourceModel: SourceModel, sourceBatch: SourceBatch): Promise
`ANALYZE_MODEL_DATA_MX_*` 结果行不要求业务主键。每个批次只允许同步一次,结果行按来源读取顺序记录 `source_row_number` 并纯新增,不进行去重或更新。
MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql` 初始化。脚本只创建不存在的来源表,并按稳定键更新 3 个模型、9 个批次和 48 条结果样例;使用测试源启动前端时,必须把 `MODEL_MONITOR_CONNECTOR_NAME` 配置为对应的 MySQL Connector。该环境只验证通用同步流程,不替代 Oracle 驱动和方言的集成验收。
MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql` 初始化。脚本只创建不存在的来源表,并按稳定键更新 4 个模型、16 个批次和 80 条结果样例;每个模型包含 2025 年历史批次和 2026 年当前批次,便于验证按年份分组、年份切换、跨年份勾选和全选口径。使用测试源启动前端时,必须把 `MODEL_MONITOR_CONNECTOR_NAME` 配置为对应的 MySQL Connector;当前测试环境的 Connector 名称为 `oqJDeVZszjRM`,可用 `MODEL_MONITOR_CONNECTOR_NAME=oqJDeVZszjRM npm run dev` 启动。该环境只验证通用同步流程,不替代 Oracle 驱动和方言的集成验收。
未来若固定数据源配置变化,只替换构建常量和适配器实现,不修改页面和本地持久化服务。
......@@ -179,6 +179,6 @@ MySQL 联调数据可通过 `scripts/mysql/model-monitor-source-fixtures.sql`
- 勾选批次记录后在视口固定 ActionBar 中执行转发、隐藏或清空选择;所有按钮继续调用原处理函数。
- 模型同步、批次同步、隐藏管理、转发提交、审批详情和填写使用 HeroUI Modal/AlertDialog;关闭或取消不触发保存请求。
- 弹窗宽度按内容密度使用统一梯度:删除确认使用 `md`,短单列表单使用 `lg`,普通双列表单使用 `2xl`,内容型详情、转发、批量同步和填写流程使用 `4xl`(1088px);移动端保留 16px 安全边距,不改变关闭、校验、保存和审批回调。
- 批量同步模型批次弹窗使用紧凑步骤条、年份筛选与全选工具行,以及单一 HeroUI Pro ListView 原生多选;每个批次行展示来源模型信息,切换年份继续保留其它年份已选项,已同步批次继续禁用,选择和同步口径不变。
- 批量同步模型批次弹窗使用紧凑步骤条、年份筛选与全选工具行;年份下按来源模型分段展示批次,同一模型的批次集中在同一列表区,并在分段标题展示模型名称、编码和批次数量。各分段使用 HeroUI Pro ListView 原生多选,切换年份继续保留其它年份已选项,已同步批次继续禁用,选择和同步口径不变。
- 查看态详情统一放在单一 secondary Surface;编辑态字段直接位于 Modal Overlay,字段必填、枚举来源、主键禁用和完成填写校验保持原规则。
- Loading、错误和空状态由 HeroUI/HeroUI Pro 语义组件展示;不得生成测试记录或用伪造数值填充空页面。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment