Commit a46c3e8d authored by David Yang's avatar David Yang

feat: sync models

parent 5a3fb30d
{
"0 debug pnpm:scope": {
"selected": 1
},
"1 error pnpm": {
"code": "ELIFECYCLE",
"errno": "ENOENT",
"syscall": "spawn",
"file": "sh",
"pkgid": "@",
"stage": "dev",
"script": "max dev",
"err": {
"name": "pnpm",
"message": "@ dev: `max dev`\nspawn ENOENT",
"code": "ELIFECYCLE",
"stack": "pnpm: @ dev: `max dev`\nspawn ENOENT\n at ChildProcess.<anonymous> (/snapshot/dist/pnpm.cjs)\n at ChildProcess.emit (events.js:400:28)\n at maybeClose (internal/child_process.js:1088:16)\n at Process.ChildProcess._handle.onexit (internal/child_process.js:296:5)"
}
},
"2 warn pnpm:global": " Local package.json exists, but node_modules missing, did you mean to install?"
}
\ No newline at end of file
......@@ -7,10 +7,9 @@ function resolvePackageRoot(packageName: string) {
const ROUTE_PATHS = {
login: '/login',
analysisOverview: '/analysis/overview',
analysisViews: '/analysis/views',
analysisForwards: '/analysis/forwards',
analysisBoards: '/analysis/boards',
modelOverview: '/models/overview',
models: '/models',
modelForwards: '/models/forwards',
} as const;
export default defineConfig({
......@@ -35,39 +34,59 @@ export default defineConfig({
routes: [
{
path: '/',
redirect: ROUTE_PATHS.analysisOverview,
redirect: ROUTE_PATHS.modelOverview,
},
{
path: '/analysis',
redirect: ROUTE_PATHS.analysisOverview,
redirect: ROUTE_PATHS.modelOverview,
},
{
path: '/resource',
redirect: ROUTE_PATHS.analysisOverview,
redirect: ROUTE_PATHS.modelOverview,
},
{
path: '/resource/stat',
redirect: ROUTE_PATHS.analysisOverview,
redirect: ROUTE_PATHS.modelOverview,
},
{
path: '/resource/booking',
redirect: ROUTE_PATHS.analysisViews,
redirect: ROUTE_PATHS.models,
},
{
path: ROUTE_PATHS.analysisOverview,
component: './project_manager_xy/AnalysisOverview',
path: ROUTE_PATHS.modelOverview,
component: './project_manager_xy/ModelOverview',
},
{
path: ROUTE_PATHS.analysisViews,
component: './project_manager_xy/AnalysisViews',
path: ROUTE_PATHS.modelForwards,
component: './project_manager_xy/ForwardApprovals',
},
{
path: ROUTE_PATHS.analysisForwards,
component: './project_manager_xy/ForwardApprovals',
path: '/models/:modelId/batches/:batchId/data',
component: './project_manager_xy/ModelBatchData',
},
{
path: '/models/:modelId/batches',
component: './project_manager_xy/ModelBatches',
},
{
path: ROUTE_PATHS.models,
component: './project_manager_xy/Models',
},
{
path: '/analysis/overview',
redirect: ROUTE_PATHS.modelOverview,
},
{
path: '/analysis/views',
redirect: ROUTE_PATHS.models,
},
{
path: '/analysis/forwards',
redirect: ROUTE_PATHS.modelForwards,
},
{
path: ROUTE_PATHS.analysisBoards,
component: './project_manager_xy/DashboardBoards',
path: '/analysis/boards',
redirect: ROUTE_PATHS.models,
},
],
},
......
......@@ -104,7 +104,7 @@ AI 在编写 API 调用或数据转换逻辑时,必须遵守:
## 业务文档索引
- 审计材料中心模块 - 请阅读 `/src/pages/project_manager_xy/audit-material-center.business-flow.md`
- 模型监控模块 - 请阅读 `/src/pages/project_manager_xy/model-monitor.business-flow.md`
- 模板来源说明 - 如需确认旧项目上下文,可参考 `/src/pages/project_manager_xy/project-management.business-flow.md`
## 获取当前登录用户信息
......
This diff is collapsed.
{
"name": "model_analysis",
"version": "1.0.1",
"version": "1.0.2",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
......
import { UserContext } from '@/wrapper/Auth';
import {
ChartCard,
MetricCard,
NoticeBanner,
} from './AnalysisShared';
import {
ClockIcon,
ShareIcon,
} from '@heroicons/react/24/outline';
import {
buildOverviewCharts,
buildMetricCards,
formatDateTime,
loadAnalysisSubjects,
loadAnalysisViews,
loadDashboardBoards,
loadForwardRecords,
} from './analysisService';
import { useContext, useEffect, useMemo, useState } from 'react';
function OverviewStatusItem({ icon: Icon, label, value, hint }) {
return (
<div className="flex min-w-0 items-start gap-3 rounded-lg border border-zinc-200 bg-white px-4 py-3 shadow-sm">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-rose-50 text-rose-600">
<Icon className="size-[18px]" />
</div>
<div className="min-w-0">
<div className="text-xs font-medium text-zinc-500">{label}</div>
<div className="mt-1 truncate text-lg font-semibold leading-6 text-zinc-950">
{value}
</div>
<div className="mt-0.5 truncate text-xs text-zinc-500">{hint}</div>
</div>
</div>
);
}
function OverviewHeader({ welcomeName, lastUpdatedAt, sharedViewCount }) {
return (
<section className="overflow-hidden rounded-lg border border-zinc-200/80 bg-white shadow-sm">
<div className="grid gap-5 bg-[linear-gradient(135deg,rgba(255,241,242,0.78),rgba(255,255,255,0.96)_42%,rgba(248,250,252,0.98))] p-5 lg:grid-cols-[minmax(0,1fr)_minmax(360px,480px)] lg:items-center lg:p-6">
<div className="min-w-0">
<div className="inline-flex rounded-full border border-rose-200 bg-white/80 px-3 py-1 text-[11px] font-semibold tracking-[0.16em] text-rose-700">
ANALYSIS OVERVIEW
</div>
<h1 className="mt-4 max-w-4xl text-2xl font-semibold leading-tight text-zinc-950 sm:text-3xl">
{welcomeName},这里是新分析工具的统一驾驶舱
</h1>
<p className="mt-3 max-w-4xl text-sm leading-6 text-zinc-600">
聚合视图配置、转发审批和图表看板三条主链路,先判断配置成熟度,再进入具体分析任务。
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
<OverviewStatusItem
icon={ClockIcon}
label="最近更新时间"
value={lastUpdatedAt}
hint="配置或审批变更"
/>
<OverviewStatusItem
icon={ShareIcon}
label="共享视图数"
value={sharedViewCount}
hint="团队开放配置"
/>
</div>
</div>
</section>
);
}
export default function AnalysisOverview() {
const userInfo = useContext(UserContext);
const [subjects, setSubjects] = useState([]);
const [views, setViews] = useState([]);
const [forwards, setForwards] = useState([]);
const [boards, setBoards] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
let cancelled = false;
(async () => {
setLoading(true);
try {
const [subjectList, viewList, forwardList, boardList] = await Promise.all([
loadAnalysisSubjects(),
loadAnalysisViews(),
loadForwardRecords(),
loadDashboardBoards(),
]);
if (cancelled) {
return;
}
setSubjects(subjectList);
setViews(viewList);
setForwards(forwardList);
setBoards(boardList);
setError('');
} catch (loadError) {
if (!cancelled) {
setError(loadError?.message || '分析总览加载失败');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, []);
const metricCards = useMemo(
() => buildMetricCards({ subjects, views, forwards, boards }),
[subjects, views, forwards, boards],
);
const overviewCharts = useMemo(
() => buildOverviewCharts({ views, forwards, boards }),
[views, forwards, boards],
);
const welcomeName =
userInfo?.display_name || userInfo?.displayName || userInfo?.username || '业务同事';
const lastUpdatedAt = useMemo(
() =>
formatDateTime(
[...views, ...forwards, ...boards]
.map((item) => item.updatedAt || item.createdAt)
.sort()
.pop(),
),
[boards, forwards, views],
);
const sharedViewCount = useMemo(
() => views.filter((item) => item.scope === 'shared').length,
[views],
);
const viewChartOption = useMemo(
() => ({
grid: {
top: 24,
right: 24,
bottom: 24,
left: 24,
containLabel: true,
},
xAxis: {
type: 'category',
data: overviewCharts.viewGroups.map((item) => item.label),
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
color: '#e4e4e7',
type: 'dashed',
},
},
},
series: [
{
type: 'bar',
barMaxWidth: 28,
data: overviewCharts.viewGroups.map((item) => item.count),
},
],
}),
[overviewCharts.viewGroups],
);
const approvalChartOption = useMemo(
() => ({
tooltip: {
trigger: 'item',
},
series: [
{
type: 'pie',
radius: ['42%', '72%'],
center: ['50%', '46%'],
itemStyle: {
borderColor: '#fff',
borderWidth: 3,
},
data: overviewCharts.approvalGroups.map((item) => ({
name: item.label,
value: item.count,
})),
},
],
}),
[overviewCharts.approvalGroups],
);
const boardChartOption = useMemo(
() => ({
xAxis: {
type: 'category',
data: overviewCharts.boardGroups.map((item) => item.label),
},
yAxis: {
type: 'value',
},
series: [
{
type: 'line',
smooth: true,
areaStyle: {
opacity: 0.1,
},
data: overviewCharts.boardGroups.map((item) => item.count),
},
],
}),
[overviewCharts.boardGroups],
);
return (
<div className="space-y-6">
<OverviewHeader
welcomeName={welcomeName}
lastUpdatedAt={lastUpdatedAt}
sharedViewCount={sharedViewCount}
/>
{loading ? <NoticeBanner>正在加载分析总览...</NoticeBanner> : null}
{!loading && error ? <NoticeBanner tone="error">{error}</NoticeBanner> : null}
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{metricCards.map((card) => (
<MetricCard
key={card.label}
label={card.label}
value={card.value}
hint={card.hint}
/>
))}
</div>
<div className="grid gap-6 xl:grid-cols-3">
<ChartCard
title="对象视图分布"
description="看当前有哪些对象已经沉淀出了正式分析视图。"
option={viewChartOption}
emptyText="还没有分析视图配置"
/>
<ChartCard
title="审批状态分布"
description="优先识别仍卡在审批中的转发记录。"
option={approvalChartOption}
emptyText="还没有转发审批记录"
/>
<ChartCard
title="看板共享情况"
description="快速判断看板建设是偏个人使用还是团队共享。"
option={boardChartOption}
emptyText="还没有图表看板"
/>
</div>
</div>
);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -6,42 +6,41 @@ import { SiteContext } from '@/wrapper/Site';
import {
ArrowsRightLeftIcon,
Bars3Icon,
ChartBarSquareIcon,
ChartPieIcon,
ClipboardDocumentListIcon,
CircleStackIcon,
CubeIcon,
Squares2X2Icon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import {
Dialog as HeadlessDialog,
DialogBackdrop,
DialogPanel,
} from '@headlessui/react';
import clsx from 'clsx';
import { useContext, useEffect, useMemo, useState } from 'react';
import { Link, Outlet, useLocation } from 'umi';
const APP_NAME = '数据分析工具';
const APP_NAME = '模型监控';
const menuGroups = [
{
title: '分析驾驶舱',
title: '模型监控',
items: [
{
name: '分析总览',
path: '/analysis/overview',
icon: ChartPieIcon,
name: '模型总览',
path: '/models/overview',
icon: CircleStackIcon,
},
{
name: '分析视图',
path: '/analysis/views',
icon: ChartBarSquareIcon,
name: '全部模型',
path: '/models',
icon: CubeIcon,
},
{
name: '转发审批',
path: '/analysis/forwards',
path: '/models/forwards',
icon: ArrowsRightLeftIcon,
},
{
name: '图表看板',
path: '/analysis/boards',
icon: ClipboardDocumentListIcon,
},
],
},
];
......@@ -158,19 +157,11 @@ function ExpandSidebarIcon() {
}
function MobileSidebar({ open, close, children }) {
if (!open) {
return null;
}
return (
<div className="relative z-[140] lg:hidden">
<div
className="fixed inset-0 z-[140] bg-black/30"
onClick={close}
aria-hidden="true"
/>
<div className="fixed inset-y-0 z-[150] w-full max-w-80 p-2">
<div className="flex h-full flex-col rounded-lg bg-zinc-900 shadow-xs ring-1 ring-white/10">
<HeadlessDialog open={open} onClose={close} className="relative z-[140] lg:hidden">
<DialogBackdrop transition className="fixed inset-0 bg-black/40 transition-opacity duration-200 data-closed:opacity-0" />
<div className="fixed inset-0 overflow-hidden">
<DialogPanel transition className="flex h-full w-full max-w-80 flex-col bg-zinc-900 shadow-xl transition duration-200 data-closed:-translate-x-full">
<div className="-mb-3 flex justify-end px-4 pt-3">
<NavbarItem
onClick={close}
......@@ -181,9 +172,9 @@ function MobileSidebar({ open, close, children }) {
</NavbarItem>
</div>
{children}
</div>
</DialogPanel>
</div>
</div>
</HeadlessDialog>
);
}
......@@ -228,10 +219,28 @@ export default function ProjectManagerLayout() {
}, []);
const sidebarCollapsed = isMobile ? true : isSidebarCollapsed;
const isNavItemActive = (path) => {
if (path === '/models') {
return (
location.pathname === '/models' ||
(location.pathname.startsWith('/models/') &&
!location.pathname.startsWith('/models/overview') &&
!location.pathname.startsWith('/models/forwards'))
);
}
return location.pathname === path;
};
const currentGroupTitle = useMemo(() => {
return (
menuGroups.find((group) =>
group.items.some((item) => item.path === location.pathname),
group.items.some((item) =>
item.path === '/models'
? location.pathname === '/models' ||
(location.pathname.startsWith('/models/') &&
!location.pathname.startsWith('/models/overview') &&
!location.pathname.startsWith('/models/forwards'))
: item.path === location.pathname,
),
)?.title || ''
);
}, [location.pathname]);
......@@ -285,7 +294,7 @@ export default function ProjectManagerLayout() {
{APP_NAME}
</h1>
<div className="mt-1 text-xs text-zinc-400">
配置驱动的数据分析工具
模型批次同步与协作处置
</div>
</div>
{applicationSwitcherButton}
......@@ -307,7 +316,7 @@ export default function ProjectManagerLayout() {
key={item.path}
name={item.name}
path={item.path}
active={location.pathname === item.path}
active={isNavItemActive(item.path)}
collapsed={sidebarCollapsed}
icon={item.icon}
/>
......@@ -323,7 +332,7 @@ export default function ProjectManagerLayout() {
{userInfo?.display_name || userInfo?.username || '当前用户'}
</div>
<div className="mt-1 text-xs text-zinc-500">
当前正在查看数据分析工具
当前正在查看模型监控
</div>
</div>
) : null}
......@@ -366,7 +375,7 @@ export default function ProjectManagerLayout() {
</h1>
</div>
<div className="mt-1 text-xs text-zinc-500">
配置驱动分析工作区
模型同步工作区
</div>
</div>
</div>
......@@ -383,7 +392,7 @@ export default function ProjectManagerLayout() {
key={item.path}
name={item.name}
path={item.path}
active={location.pathname === item.path}
active={isNavItemActive(item.path)}
icon={item.icon}
onNavigate={() => setIsMobileSidebarOpen(false)}
/>
......
This diff is collapsed.
This diff is collapsed.
const MOCK_MODELS = [
{
model_code: 'AML_CUSTOMER_RISK',
model_name: '客户洗钱风险模型',
primary_key_fields: ['customer_id', 'risk_date'],
},
{
model_code: 'TRANSACTION_ALERT',
model_name: '异常交易预警模型',
primary_key_fields: ['alert_id'],
},
{
model_code: 'CREDIT_EXPOSURE',
model_name: '授信敞口监测模型',
primary_key_fields: ['institution_code', 'reporting_period'],
},
];
const MOCK_BATCHES = {
AML_CUSTOMER_RISK: [
{ batch_code: '2026-06', batch_name: '2026 年 6 月批次' },
{ batch_code: '2026-05', batch_name: '2026 年 5 月批次' },
],
TRANSACTION_ALERT: [
{ batch_code: '2026-W29', batch_name: '2026 年第 29 周' },
{ batch_code: '2026-W28', batch_name: '2026 年第 28 周' },
],
CREDIT_EXPOSURE: [
{ batch_code: '2026-Q2', batch_name: '2026 年第二季度' },
{ batch_code: '2026-Q1', batch_name: '2026 年第一季度' },
],
};
const DATA_BUILDERS = {
AML_CUSTOMER_RISK(batchCode) {
const columns = [
{ name: 'customer_id', label: '客户编号', type: 'text' },
{ name: 'risk_date', label: '风险日期', type: 'date' },
{ name: 'customer_name', label: '客户名称', type: 'text' },
{ name: 'risk_level', label: '风险等级', type: 'text' },
{ name: 'risk_score', label: '风险分值', type: 'number' },
{ name: 'is_focus_customer', label: '是否重点客户', type: 'boolean' },
];
const month = batchCode === '2026-05' ? '05' : '06';
const names = ['华远实业', '海川商贸', '恒信科技', '嘉禾供应链', '远景投资'];
return {
columns,
rows: names.map((name, index) => ({
customer_id: `C${String(index + 1).padStart(4, '0')}`,
risk_date: `2026-${month}-${String(index + 3).padStart(2, '0')}T00:00:00.000Z`,
customer_name: name,
risk_level: index < 2 ? '高' : index < 4 ? '中' : '低',
risk_score: 91 - index * 9 + (month === '06' ? 2 : 0),
is_focus_customer: index < 2,
})),
};
},
TRANSACTION_ALERT(batchCode) {
const columns = [
{ name: 'alert_id', label: '预警编号', type: 'text' },
{ name: 'account_name', label: '账户名称', type: 'text' },
{ name: 'transaction_amount', label: '交易金额', type: 'number' },
{ name: 'transaction_time', label: '交易时间', type: 'date' },
{ name: 'alert_reason', label: '预警原因', type: 'text' },
];
const week = batchCode.endsWith('28') ? 28 : 29;
return {
columns,
rows: Array.from({ length: 7 }, (_, index) => ({
alert_id: `AL${week}${String(index + 1).padStart(3, '0')}`,
account_name: `监测账户 ${index + 1}`,
transaction_amount: 180000 + index * 73500,
transaction_time: `2026-07-${String((week === 29 ? 13 : 6) + index).padStart(2, '0')}T0${index % 9}:30:00.000Z`,
alert_reason: index % 2 === 0 ? '短期高频大额交易' : '交易对手集中度异常',
})),
};
},
CREDIT_EXPOSURE(batchCode) {
const columns = [
{ name: 'institution_code', label: '机构编码', type: 'text' },
{ name: 'reporting_period', label: '报告期间', type: 'text' },
{ name: 'institution_name', label: '机构名称', type: 'text' },
{ name: 'credit_limit', label: '授信额度', type: 'number' },
{ name: 'used_amount', label: '已用额度', type: 'number' },
{ name: 'over_limit', label: '是否超限', type: 'boolean' },
];
const multiplier = batchCode === '2026-Q1' ? 0.94 : 1;
return {
columns,
rows: ['总行营业部', '华东分行', '华南分行', '西部中心'].map((name, index) => {
const limit = (12000000 + index * 3500000) * multiplier;
const used = limit * (0.72 + index * 0.1);
return {
institution_code: `ORG${index + 1}`,
reporting_period: batchCode,
institution_name: name,
credit_limit: Math.round(limit),
used_amount: Math.round(used),
over_limit: used > limit,
};
}),
};
},
};
function wait(duration = 280) {
return new Promise((resolve) => window.setTimeout(resolve, duration));
}
export async function listSourceModels() {
await wait();
return MOCK_MODELS.map((item) => ({ ...item }));
}
export async function listSourceBatches(modelName) {
await wait();
const model = MOCK_MODELS.find((item) => item.model_name === modelName);
return (MOCK_BATCHES[model?.model_code] || []).map((item) => ({ ...item }));
}
export async function loadSourceBatchData(modelName, batchCode) {
await wait(420);
const model = MOCK_MODELS.find((item) => item.model_name === modelName);
const builder = DATA_BUILDERS[model?.model_code];
if (!builder) {
throw new Error('未找到所选模型的 Mock 数据。');
}
return builder(batchCode);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -4,6 +4,6 @@
请改为阅读:
- `src/pages/project_manager_xy/audit-material-center.business-flow.md`
- `src/pages/project_manager_xy/model-monitor.business-flow.md`
保留本文件仅用于说明模板来源,避免后续开发继续误引用旧的“项目管理”业务语义。
This diff is collapsed.
This diff is collapsed.
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