fix(web,plugin): 前端审计修复 — 401 消除 + 统计卡片 crash + 销售漏斗 500 + antd 6 废弃 API
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- API client: proactive token refresh(请求前 30s 检查过期,提前刷新避免 401)
- Plugin store: fetchPlugins promise 去重,防止 StrictMode 并发重复请求
- Home stats: 简化 useEffect 加载逻辑,修复 tagColor undefined crash
- PluginGraphPage: valueStyle → styles.content, Spin tip → description(antd 6)
- DashboardWidgets: trailColor → railColor(antd 6)
- data_service: build_scope_sql 参数索引修复(硬编码 $100 → 动态 values.len()+1)
- erp-core error: Internal 错误添加 tracing::error 日志输出
This commit is contained in:
iven
2026-04-18 20:31:49 +08:00
parent 790991f77c
commit 5ba11f985f
12 changed files with 308 additions and 100 deletions

View File

@@ -127,8 +127,15 @@ export default function Home() {
if (cancelled) return;
const extractTotal = (res: PromiseSettledResult<{ data: { data?: { total?: number } } }>) =>
res.status === 'fulfilled' ? (res.value.data?.data?.total ?? 0) : 0;
const extractTotal = (res: PromiseSettledResult<{ data: { data?: { total?: number } } }>) => {
if (res.status !== 'fulfilled') return 0;
const body = res.value.data;
if (body && typeof body === 'object' && 'data' in body) {
const inner = (body as { data?: { total?: number } }).data;
return inner?.total ?? 0;
}
return 0;
};
setStats({
userCount: extractTotal(usersRes),
@@ -147,7 +154,8 @@ export default function Home() {
loadStats();
return () => { cancelled = true; };
}, [fetchUnreadCount]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleNavigate = useCallback((path: string) => {
navigate(path);
@@ -220,10 +228,10 @@ export default function Home() {
];
const recentActivities: ActivityItem[] = [
{ id: '1', text: '系统管理员 创建了 <strong>管理员角色</strong>', time: '刚刚', icon: <TeamOutlined /> },
{ id: '2', text: '系统管理员 配置了 <strong>工作流模板</strong>', time: '5 分钟前', icon: <FileProtectOutlined /> },
{ id: '3', text: '系统管理员 更新了 <strong>组织架构</strong>', time: '10 分钟前', icon: <ApartmentOutlined /> },
{ id: '4', text: '系统管理员 设置了 <strong>消息通知偏好</strong>', time: '30 分钟前', icon: <BellOutlined /> },
{ id: '1', text: '系统管理员 创建了 管理员角色', time: '刚刚', icon: <TeamOutlined /> },
{ id: '2', text: '系统管理员 配置了 工作流模板', time: '5 分钟前', icon: <FileProtectOutlined /> },
{ id: '3', text: '系统管理员 更新了 组织架构', time: '10 分钟前', icon: <ApartmentOutlined /> },
{ id: '4', text: '系统管理员 设置了 消息通知偏好', time: '30 分钟前', icon: <BellOutlined /> },
];
const priorityLabel: Record<string, string> = { high: '紧急', medium: '一般', low: '低' };
@@ -351,7 +359,7 @@ export default function Home() {
<div key={activity.id} className="erp-activity-item">
<div className="erp-activity-dot">{activity.icon}</div>
<div className="erp-activity-content">
<div className="erp-activity-text" dangerouslySetInnerHTML={{ __html: activity.text }} />
<div className="erp-activity-text">{activity.text}</div>
<div className="erp-activity-time">{activity.time}</div>
</div>
</div>

View File

@@ -11,7 +11,7 @@ import {
type DashboardWidget,
} from '../api/plugins';
import type { EntityStat, FieldBreakdown, WidgetData } from './dashboard/dashboardTypes';
import { ENTITY_PALETTE, DEFAULT_PALETTE, ENTITY_ICONS, getDelayClass } from './dashboard/dashboardConstants';
import { getEntityPalette, getEntityIcon, getDelayClass } from './dashboard/dashboardConstants';
import {
StatCard,
SkeletonStatCard,
@@ -89,27 +89,28 @@ export function PluginDashboardPage() {
const abortController = new AbortController();
async function loadAllCounts() {
const results: EntityStat[] = [];
for (const entity of entities) {
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
if (abortController.signal.aborted) return;
const palette = getEntityPalette(entity.name, i);
const icon = getEntityIcon(entity.name);
try {
const count = await countPluginData(pluginId!, entity.name);
if (abortController.signal.aborted) return;
const palette = ENTITY_PALETTE[entity.name] || DEFAULT_PALETTE;
results.push({
name: entity.name,
displayName: entity.display_name || entity.name,
count,
icon: ENTITY_ICONS[entity.name] || <DashboardOutlined />,
icon,
gradient: palette.gradient,
iconBg: palette.iconBg,
});
} catch {
const palette = ENTITY_PALETTE[entity.name] || DEFAULT_PALETTE;
results.push({
name: entity.name,
displayName: entity.display_name || entity.name,
count: 0,
icon: ENTITY_ICONS[entity.name] || <DashboardOutlined />,
icon,
gradient: palette.gradient,
iconBg: palette.iconBg,
});
@@ -206,8 +207,8 @@ export function PluginDashboardPage() {
);
// 当前实体的色板
const currentPalette = useMemo(
() => ENTITY_PALETTE[selectedEntity] || DEFAULT_PALETTE,
[selectedEntity],
() => getEntityPalette(selectedEntity, entities.findIndex((e) => e.name === selectedEntity)),
[selectedEntity, entities],
);
// ── 渲染 ──
if (schemaLoading) {
@@ -257,7 +258,7 @@ export function PluginDashboardPage() {
margin: 0,
}}
>
CRM
{pluginId ? `${pluginId.toUpperCase()} 数据统计` : '数据统计'}
</p>
</div>
<Select

View File

@@ -36,7 +36,7 @@ import {
import type { GraphNode, GraphEdge, GraphConfig, NodePosition, HoverState } from './graph/graphTypes';
import { computeCircularLayout } from './graph/graphLayout';
import {
RELATIONSHIP_COLORS,
getEdgeColor,
NODE_HOVER_SCALE,
getRelColor,
getEdgeTypeLabel,
@@ -327,8 +327,7 @@ export function PluginGraphPage() {
}
} else {
const idx = nodes.indexOf(node);
const palette = Object.values(RELATIONSHIP_COLORS);
const pick = palette[idx % palette.length];
const pick = getEdgeColor(`_node_${idx}`);
nodeColorBase = pick.base;
nodeColorLight = pick.light;
nodeColorGlow = pick.glow;
@@ -475,7 +474,7 @@ export function PluginGraphPage() {
if (loading) {
return (
<div style={{ padding: 24, textAlign: 'center' }}>
<Spin size="large" tip="加载图谱数据中..." />
<Spin size="large" description="加载图谱数据中..." />
</div>
);
}
@@ -497,7 +496,7 @@ export function PluginGraphPage() {
</Text>
}
value={customers.length}
valueStyle={{ color: token.colorPrimary, fontWeight: 600 }}
styles={{ content: { color: token.colorPrimary, fontWeight: 600 } }}
/>
</Card>
</Col>
@@ -514,7 +513,7 @@ export function PluginGraphPage() {
</Text>
}
value={relationships.length}
valueStyle={{ color: token.colorSuccess, fontWeight: 600 }}
styles={{ content: { color: token.colorSuccess, fontWeight: 600 } }}
/>
</Card>
</Col>
@@ -531,10 +530,12 @@ export function PluginGraphPage() {
</Text>
}
value={centerNode?.label || '未选择'}
valueStyle={{
fontSize: 20,
color: centerNode ? token.colorWarning : token.colorTextDisabled,
fontWeight: 600,
styles={{
content: {
fontSize: 20,
color: centerNode ? token.colorWarning : token.colorTextDisabled,
fontWeight: 600,
},
}}
/>
{selectedCenter && (

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useCallback } from 'react';
import { useEffect, useState, useCallback, useRef } from 'react';
import {
Table,
Button,
@@ -80,6 +80,15 @@ export default function Users() {
setLoading(false);
}, [page, searchText]);
// 搜索防抖:输入后 300ms 才触发查询
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedSearch = useCallback((_text: string) => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
debounceTimer.current = setTimeout(() => {
setPage(1);
}, 300);
}, []);
const fetchRoles = useCallback(async () => {
try {
const result = await listRoles();
@@ -349,7 +358,10 @@ export default function Users() {
placeholder="搜索用户名..."
prefix={<SearchOutlined style={{ color: '#94A3B8' }} />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => {
setSearchText(e.target.value);
debouncedSearch(e.target.value);
}}
allowClear
style={{ width: 220, borderRadius: 8 }}
/>

View File

@@ -169,7 +169,7 @@ export function BreakdownCard({
percent={barPercent}
showInfo={false}
strokeColor={tagStrokeColor(color)}
trailColor="var(--erp-border-light)"
railColor="var(--erp-border-light)"
size="small"
style={{ marginBottom: 0 }}
/>

View File

@@ -9,37 +9,25 @@ import {
PieChartOutlined,
LineChartOutlined,
FunnelPlotOutlined,
AppstoreOutlined,
UserOutlined,
ShoppingOutlined,
FileTextOutlined,
DatabaseOutlined,
} from '@ant-design/icons';
// ── 色板配置 ──
// ── 通用调色板 ──
export const ENTITY_PALETTE: Record<string, { gradient: string; iconBg: string; tagColor: string }> = {
customer: {
gradient: 'linear-gradient(135deg, #4F46E5, #6366F1)',
iconBg: 'rgba(79, 70, 229, 0.12)',
tagColor: 'purple',
},
contact: {
gradient: 'linear-gradient(135deg, #059669, #10B981)',
iconBg: 'rgba(5, 150, 105, 0.12)',
tagColor: 'green',
},
communication: {
gradient: 'linear-gradient(135deg, #D97706, #F59E0B)',
iconBg: 'rgba(217, 119, 6, 0.12)',
tagColor: 'orange',
},
customer_tag: {
gradient: 'linear-gradient(135deg, #7C3AED, #A78BFA)',
iconBg: 'rgba(124, 58, 237, 0.12)',
tagColor: 'volcano',
},
customer_relationship: {
gradient: 'linear-gradient(135deg, #E11D48, #F43F5E)',
iconBg: 'rgba(225, 29, 72, 0.12)',
tagColor: 'red',
},
};
const UNIVERSAL_COLORS = [
{ gradient: 'linear-gradient(135deg, #4F46E5, #6366F1)', iconBg: 'rgba(79, 70, 229, 0.12)', tagColor: 'purple' },
{ gradient: 'linear-gradient(135deg, #059669, #10B981)', iconBg: 'rgba(5, 150, 105, 0.12)', tagColor: 'green' },
{ gradient: 'linear-gradient(135deg, #D97706, #F59E0B)', iconBg: 'rgba(217, 119, 6, 0.12)', tagColor: 'orange' },
{ gradient: 'linear-gradient(135deg, #7C3AED, #A78BFA)', iconBg: 'rgba(124, 58, 237, 0.12)', tagColor: 'volcano' },
{ gradient: 'linear-gradient(135deg, #E11D48, #F43F5E)', iconBg: 'rgba(225, 29, 72, 0.12)', tagColor: 'red' },
{ gradient: 'linear-gradient(135deg, #0891B2, #06B6D4)', iconBg: 'rgba(8, 145, 178, 0.12)', tagColor: 'cyan' },
{ gradient: 'linear-gradient(135deg, #EA580C, #F97316)', iconBg: 'rgba(234, 88, 12, 0.12)', tagColor: 'orange' },
{ gradient: 'linear-gradient(135deg, #DB2777, #EC4899)', iconBg: 'rgba(219, 39, 119, 0.12)', tagColor: 'magenta' },
];
export const DEFAULT_PALETTE = {
gradient: 'linear-gradient(135deg, #2563EB, #3B82F6)',
@@ -52,16 +40,42 @@ export const TAG_COLORS = [
'magenta', 'gold', 'lime', 'geekblue', 'volcano',
];
// ── 图标映射 ──
// ── 按实体顺序自动分配颜色 ──
export const ENTITY_ICONS: Record<string, React.ReactNode> = {
customer: <TeamOutlined />,
contact: <TeamOutlined />,
communication: <PhoneOutlined />,
customer_tag: <TagsOutlined />,
customer_relationship: <RiseOutlined />,
const paletteCache = new Map<string, { gradient: string; iconBg: string; tagColor: string }>();
export function getEntityPalette(entityName: string, index: number) {
const cached = paletteCache.get(entityName);
if (cached) return cached;
const safeIndex = index >= 0 ? index : 0;
const palette = UNIVERSAL_COLORS[safeIndex % UNIVERSAL_COLORS.length];
paletteCache.set(entityName, palette);
return palette;
}
// ── 通用图标映射 ──
const ICON_MAP: Record<string, React.ReactNode> = {
team: <TeamOutlined />,
user: <UserOutlined />,
users: <TeamOutlined />,
message: <PhoneOutlined />,
phone: <PhoneOutlined />,
tags: <TagsOutlined />,
tag: <TagsOutlined />,
apartment: <RiseOutlined />,
dashboard: <DashboardOutlined />,
shopping: <ShoppingOutlined />,
file: <FileTextOutlined />,
database: <DatabaseOutlined />,
appstore: <AppstoreOutlined />,
};
export function getEntityIcon(iconName?: string): React.ReactNode {
if (!iconName) return <AppstoreOutlined />;
return ICON_MAP[iconName.toLowerCase().replace('outlined', '')] ?? <AppstoreOutlined />;
}
export const WIDGET_ICON_MAP: Record<string, React.ReactNode> = {
stat_card: <DashboardOutlined />,
bar_chart: <BarChartOutlined />,

View File

@@ -9,26 +9,45 @@ import type { GraphEdge } from './graphTypes';
// ── 常量 ──
/** 关系类型对应的色板 (base / light / glow) */
export const RELATIONSHIP_COLORS: Record<string, { base: string; light: string; glow: string }> = {
parent_child: { base: '#4F46E5', light: '#818CF8', glow: 'rgba(79,70,229,0.3)' },
sibling: { base: '#059669', light: '#34D399', glow: 'rgba(5,150,105,0.3)' },
partner: { base: '#D97706', light: '#FBBF24', glow: 'rgba(217,119,6,0.3)' },
supplier: { base: '#0891B2', light: '#22D3EE', glow: 'rgba(8,145,178,0.3)' },
competitor: { base: '#DC2626', light: '#F87171', glow: 'rgba(220,38,38,0.3)' },
};
/** 关系类型对应的色板 (base / light / glow) — 通用调色板自动分配 */
const EDGE_PALETTE: Array<{ base: string; light: string; glow: string }> = [
{ base: '#4F46E5', light: '#818CF8', glow: 'rgba(79,70,229,0.3)' },
{ base: '#059669', light: '#34D399', glow: 'rgba(5,150,105,0.3)' },
{ base: '#D97706', light: '#FBBF24', glow: 'rgba(217,119,6,0.3)' },
{ base: '#0891B2', light: '#22D3EE', glow: 'rgba(8,145,178,0.3)' },
{ base: '#DC2626', light: '#F87171', glow: 'rgba(220,38,38,0.3)' },
{ base: '#7C3AED', light: '#A78BFA', glow: 'rgba(124,58,237,0.3)' },
{ base: '#EA580C', light: '#FB923C', glow: 'rgba(234,88,12,0.3)' },
{ base: '#DB2777', light: '#F472B6', glow: 'rgba(219,39,119,0.3)' },
];
/** 未匹配到已知关系类型时的默认色 */
export const DEFAULT_REL_COLOR = { base: '#7C3AED', light: '#A78BFA', glow: 'rgba(124,58,237,0.3)' };
/** 关系类型 → 中文标签 */
export const REL_LABEL_MAP: Record<string, string> = {
parent_child: '母子',
sibling: '兄弟',
partner: '伙伴',
supplier: '供应商',
competitor: '竞争',
};
const edgeColorCache = new Map<string, { base: string; light: string; glow: string }>();
export function getEdgeColorGlobal(label: string) {
const cached = edgeColorCache.get(label);
if (cached) return cached;
const color = EDGE_PALETTE[edgeColorCache.size % EDGE_PALETTE.length];
edgeColorCache.set(label, color);
return color;
}
/** @deprecated 使用 getEdgeColorGlobal */
export const RELATIONSHIP_COLORS = new Proxy({} as Record<string, { base: string; light: string; glow: string }>, {
get(_, prop: string) { return getEdgeColorGlobal(prop); },
});
/** @deprecated 标签直接使用原始值 */
export const REL_LABEL_MAP = new Proxy({} as Record<string, string>, {
get(_, prop: string) { return prop; },
});
/** 通用边颜色函数 — 兼容旧路径导入 */
export function getEdgeColor(label: string) {
return getEdgeColorGlobal(label);
}
/** 普通节点基础半径 */
export const NODE_BASE_RADIUS = 18;