fix(web): 前端功能验证修复 — 移除硬编码假数据/修正系统信息/修复dev.ps1环境变量
- refactor(Home): 待办任务改为从工作流API获取真实数据 - refactor(Home): 最近动态改为从审计日志API获取真实操作记录 - refactor(Home): 移除硬编码的sparkline趋势图和假统计数据 - fix(Home): 系统信息 PostgreSQL 16→18,模块数量 5→6 - fix(Login): 移除硬编码版本号 v0.1.0 - fix(MainLayout): Footer 更新为 "HMS 健康管理平台" - fix(dev.ps1): 添加缺失的 WECHAT/HEALTH 环境变量
This commit is contained in:
@@ -397,7 +397,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
|
||||
{/* 底部 */}
|
||||
<Footer className={`erp-footer ${isDark ? 'erp-footer-dark' : 'erp-footer-light'}`}>
|
||||
ERP Platform v0.1.0
|
||||
HMS 健康管理平台
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Row, Col, Spin } from 'antd';
|
||||
import { Row, Col, Spin, Empty } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
@@ -11,16 +11,14 @@ import {
|
||||
ClockCircleOutlined,
|
||||
ApartmentOutlined,
|
||||
CheckCircleOutlined,
|
||||
TeamOutlined,
|
||||
FileProtectOutlined,
|
||||
RiseOutlined,
|
||||
FallOutlined,
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import client from '../api/client';
|
||||
import { useThemeMode } from '../hooks/useThemeMode';
|
||||
import { useMessageStore } from '../stores/message';
|
||||
import { listAuditLogs, type AuditLogItem } from '../api/auditLogs';
|
||||
import { listPendingTasks, type TaskInfo } from '../api/workflowTasks';
|
||||
|
||||
interface DashboardStats {
|
||||
userCount: number;
|
||||
@@ -29,12 +27,6 @@ interface DashboardStats {
|
||||
unreadMessages: number;
|
||||
}
|
||||
|
||||
interface TrendData {
|
||||
value: string;
|
||||
direction: 'up' | 'down' | 'neutral';
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface StatCardConfig {
|
||||
key: string;
|
||||
title: string;
|
||||
@@ -43,29 +35,9 @@ interface StatCardConfig {
|
||||
gradient: string;
|
||||
iconBg: string;
|
||||
delay: string;
|
||||
trend: TrendData;
|
||||
sparkline: number[];
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface TaskItem {
|
||||
id: string;
|
||||
title: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
assignee: string;
|
||||
dueText: string;
|
||||
color: string;
|
||||
icon: React.ReactNode;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface ActivityItem {
|
||||
id: string;
|
||||
text: string;
|
||||
time: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
function useCountUp(end: number, duration = 800) {
|
||||
const [count, setCount] = useState(0);
|
||||
const prevEnd = useRef(end);
|
||||
@@ -99,6 +71,54 @@ function StatValue({ value, loading }: { value: number; loading: boolean }) {
|
||||
return <span className="erp-count-up">{animatedValue.toLocaleString()}</span>;
|
||||
}
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
create: '创建', update: '更新', delete: '删除',
|
||||
login: '登录', user_login: '登录', 'user.login': '登录',
|
||||
'user.create': '创建', 'user.update': '更新', 'user.delete': '删除',
|
||||
'role.create': '创建', 'role.update': '更新', 'role.delete': '删除',
|
||||
'patient.create': '创建', 'patient.update': '更新',
|
||||
'appointment.create': '创建', 'appointment.update': '更新',
|
||||
};
|
||||
const RESOURCE_LABELS: Record<string, string> = {
|
||||
user: '用户', role: '角色', permission: '权限',
|
||||
organization: '组织', department: '部门', position: '岗位',
|
||||
dictionary: '字典', menu: '菜单', setting: '设置',
|
||||
process_definition: '流程定义', process_instance: '流程实例',
|
||||
message: '消息', plugin: '插件',
|
||||
patient: '患者', doctor: '医护', appointment: '预约',
|
||||
follow_up_task: '随访', consultation_session: '咨询',
|
||||
auth: '认证',
|
||||
};
|
||||
const RESOURCE_ICONS: Record<string, React.ReactNode> = {
|
||||
user: <UserOutlined />, role: <SafetyCertificateOutlined />,
|
||||
organization: <ApartmentOutlined />, process_definition: <PartitionOutlined />,
|
||||
process_instance: <FileTextOutlined />, message: <BellOutlined />,
|
||||
patient: <UserOutlined />, doctor: <UserOutlined />,
|
||||
};
|
||||
|
||||
function formatTimeAgo(dateStr: string): string {
|
||||
const diff = Date.now() - new Date(dateStr).getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
if (minutes < 1) return '刚刚';
|
||||
if (minutes < 60) return `${minutes} 分钟前`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours} 小时前`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} 天前`;
|
||||
}
|
||||
|
||||
function formatActionLabel(action: string): string {
|
||||
if (ACTION_LABELS[action]) return ACTION_LABELS[action];
|
||||
const lastPart = action.split('.').pop() || action;
|
||||
return ACTION_LABELS[lastPart] || lastPart;
|
||||
}
|
||||
|
||||
function formatResourceLabel(resource: string): string {
|
||||
if (RESOURCE_LABELS[resource]) return RESOURCE_LABELS[resource];
|
||||
const lastPart = resource.split('.').pop() || resource;
|
||||
return RESOURCE_LABELS[lastPart] || lastPart;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
userCount: 0,
|
||||
@@ -107,6 +127,9 @@ export default function Home() {
|
||||
unreadMessages: 0,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pendingTasks, setPendingTasks] = useState<TaskInfo[]>([]);
|
||||
const [recentActivities, setRecentActivities] = useState<AuditLogItem[]>([]);
|
||||
const [activitiesLoading, setActivitiesLoading] = useState(true);
|
||||
const unreadCount = useMessageStore((s) => s.unreadCount);
|
||||
const fetchUnreadCount = useMessageStore((s) => s.fetchUnreadCount);
|
||||
const navigate = useNavigate();
|
||||
@@ -150,8 +173,31 @@ export default function Home() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
try {
|
||||
const result = await listPendingTasks(1, 5);
|
||||
if (!cancelled) setPendingTasks(result.data);
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
async function loadActivities() {
|
||||
setActivitiesLoading(true);
|
||||
try {
|
||||
const result = await listAuditLogs({ page: 1, page_size: 5 });
|
||||
if (!cancelled) setRecentActivities(result.data);
|
||||
} catch {
|
||||
// 静默处理
|
||||
} finally {
|
||||
if (!cancelled) setActivitiesLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchUnreadCount();
|
||||
loadStats();
|
||||
loadTasks();
|
||||
loadActivities();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -170,8 +216,6 @@ export default function Home() {
|
||||
gradient: 'linear-gradient(135deg, #2563eb, #60a5fa)',
|
||||
iconBg: 'rgba(79, 70, 229, 0.12)',
|
||||
delay: 'erp-fade-in erp-fade-in-delay-1',
|
||||
trend: { value: '+2', direction: 'up', label: '较上周' },
|
||||
sparkline: [30, 45, 35, 50, 40, 55, 60, 50, 65, 70],
|
||||
onClick: () => handleNavigate('/users'),
|
||||
},
|
||||
{
|
||||
@@ -182,8 +226,6 @@ export default function Home() {
|
||||
gradient: 'linear-gradient(135deg, #059669, #10B981)',
|
||||
iconBg: 'rgba(5, 150, 105, 0.12)',
|
||||
delay: 'erp-fade-in erp-fade-in-delay-2',
|
||||
trend: { value: '+1', direction: 'up', label: '较上月' },
|
||||
sparkline: [20, 25, 30, 28, 35, 40, 38, 42, 45, 50],
|
||||
onClick: () => handleNavigate('/roles'),
|
||||
},
|
||||
{
|
||||
@@ -194,8 +236,6 @@ export default function Home() {
|
||||
gradient: 'linear-gradient(135deg, #d97706, #F59E0B)',
|
||||
iconBg: 'rgba(217, 119, 6, 0.12)',
|
||||
delay: 'erp-fade-in erp-fade-in-delay-3',
|
||||
trend: { value: '0', direction: 'neutral', label: '较昨日' },
|
||||
sparkline: [10, 15, 12, 20, 18, 25, 22, 28, 24, 20],
|
||||
onClick: () => handleNavigate('/workflow'),
|
||||
},
|
||||
{
|
||||
@@ -206,8 +246,6 @@ export default function Home() {
|
||||
gradient: 'linear-gradient(135deg, #E11D48, #F43F5E)',
|
||||
iconBg: 'rgba(225, 29, 72, 0.12)',
|
||||
delay: 'erp-fade-in erp-fade-in-delay-4',
|
||||
trend: { value: '0', direction: 'neutral', label: '全部已读' },
|
||||
sparkline: [5, 8, 3, 10, 6, 12, 8, 4, 7, 5],
|
||||
onClick: () => handleNavigate('/messages'),
|
||||
},
|
||||
];
|
||||
@@ -221,21 +259,6 @@ export default function Home() {
|
||||
{ icon: <SettingOutlined />, label: '系统设置', path: '/settings', color: '#475569' },
|
||||
];
|
||||
|
||||
const pendingTasks: TaskItem[] = [
|
||||
{ id: '1', title: '审核新用户注册申请', priority: 'high', assignee: '系统', dueText: '待处理', color: '#dc2626', icon: <UserOutlined />, path: '/users' },
|
||||
{ id: '2', title: '配置工作流审批节点', priority: 'medium', assignee: '管理员', dueText: '进行中', color: '#d97706', icon: <PartitionOutlined />, path: '/workflow' },
|
||||
{ id: '3', title: '更新角色权限策略', priority: 'low', assignee: '管理员', dueText: '计划中', color: '#059669', icon: <SafetyCertificateOutlined />, path: '/roles' },
|
||||
];
|
||||
|
||||
const recentActivities: ActivityItem[] = [
|
||||
{ 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: '低' };
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 欢迎语 */}
|
||||
@@ -256,50 +279,29 @@ export default function Home() {
|
||||
|
||||
{/* 统计卡片行 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{statCards.map((card) => {
|
||||
const maxSpark = Math.max(...card.sparkline, 1);
|
||||
return (
|
||||
<Col xs={24} sm={12} lg={6} key={card.key}>
|
||||
<div
|
||||
className={`erp-stat-card ${card.delay}`}
|
||||
style={{ '--card-gradient': card.gradient, '--card-icon-bg': card.iconBg } as React.CSSProperties}
|
||||
onClick={card.onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') card.onClick?.(); }}
|
||||
>
|
||||
<div className="erp-stat-card-bar" />
|
||||
<div className="erp-stat-card-body">
|
||||
<div className="erp-stat-card-info">
|
||||
<div className="erp-stat-card-title">{card.title}</div>
|
||||
<div className="erp-stat-card-value">
|
||||
<StatValue value={card.value} loading={loading} />
|
||||
</div>
|
||||
<div className={`erp-stat-card-trend erp-stat-card-trend-${card.trend.direction}`}>
|
||||
{card.trend.direction === 'up' && <RiseOutlined />}
|
||||
{card.trend.direction === 'down' && <FallOutlined />}
|
||||
<span>{card.trend.value}</span>
|
||||
<span className="erp-stat-card-trend-label">{card.trend.label}</span>
|
||||
</div>
|
||||
{statCards.map((card) => (
|
||||
<Col xs={24} sm={12} lg={6} key={card.key}>
|
||||
<div
|
||||
className={`erp-stat-card ${card.delay}`}
|
||||
style={{ '--card-gradient': card.gradient, '--card-icon-bg': card.iconBg } as React.CSSProperties}
|
||||
onClick={card.onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') card.onClick?.(); }}
|
||||
>
|
||||
<div className="erp-stat-card-bar" />
|
||||
<div className="erp-stat-card-body">
|
||||
<div className="erp-stat-card-info">
|
||||
<div className="erp-stat-card-title">{card.title}</div>
|
||||
<div className="erp-stat-card-value">
|
||||
<StatValue value={card.value} loading={loading} />
|
||||
</div>
|
||||
<div className="erp-stat-card-icon">{card.icon}</div>
|
||||
</div>
|
||||
<div className="erp-stat-card-sparkline">
|
||||
{card.sparkline.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="erp-stat-card-sparkline-bar"
|
||||
style={{
|
||||
height: `${(v / maxSpark) * 100}%`,
|
||||
background: card.gradient,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="erp-stat-card-icon">{card.icon}</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 待办任务 + 最近活动 */}
|
||||
@@ -319,30 +321,32 @@ export default function Home() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="erp-task-list">
|
||||
{pendingTasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="erp-task-item"
|
||||
style={{ '--task-color': task.color } as React.CSSProperties}
|
||||
onClick={() => handleNavigate(task.path)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(task.path); }}
|
||||
>
|
||||
<div className="erp-task-item-icon">{task.icon}</div>
|
||||
<div className="erp-task-item-content">
|
||||
<div className="erp-task-item-title">{task.title}</div>
|
||||
<div className="erp-task-item-meta">
|
||||
<span>{task.assignee}</span>
|
||||
<span>{task.dueText}</span>
|
||||
{pendingTasks.length === 0 ? (
|
||||
<Empty description="暂无待办任务" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
) : (
|
||||
pendingTasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="erp-task-item"
|
||||
style={{ '--task-color': '#2563eb' } as React.CSSProperties}
|
||||
onClick={() => handleNavigate('/workflow')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate('/workflow'); }}
|
||||
>
|
||||
<div className="erp-task-item-icon"><PartitionOutlined /></div>
|
||||
<div className="erp-task-item-content">
|
||||
<div className="erp-task-item-title">{task.node_name || task.definition_name || '流程任务'}</div>
|
||||
<div className="erp-task-item-meta">
|
||||
<span>{task.definition_name || '工作流'}</span>
|
||||
<span>{task.status === 'pending' ? '待处理' : task.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="erp-task-priority erp-task-priority-medium">一般</span>
|
||||
<RightOutlined style={{ color: isDark ? '#475569' : '#CBD5E1', fontSize: 12 }} />
|
||||
</div>
|
||||
<span className={`erp-task-priority erp-task-priority-${task.priority}`}>
|
||||
{priorityLabel[task.priority]}
|
||||
</span>
|
||||
<RightOutlined style={{ color: isDark ? '#475569' : '#CBD5E1', fontSize: 12 }} />
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -355,15 +359,25 @@ export default function Home() {
|
||||
<span className="erp-section-title">最近动态</span>
|
||||
</div>
|
||||
<div className="erp-activity-list">
|
||||
{recentActivities.map((activity) => (
|
||||
<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">{activity.text}</div>
|
||||
<div className="erp-activity-time">{activity.time}</div>
|
||||
{activitiesLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: 24 }}><Spin /></div>
|
||||
) : recentActivities.length === 0 ? (
|
||||
<Empty description="暂无动态" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
) : (
|
||||
recentActivities.map((log) => (
|
||||
<div key={log.id} className="erp-activity-item">
|
||||
<div className="erp-activity-dot">
|
||||
{RESOURCE_ICONS[log.resource_type] || <FileTextOutlined />}
|
||||
</div>
|
||||
<div className="erp-activity-content">
|
||||
<div className="erp-activity-text">
|
||||
{formatActionLabel(log.action)}了{formatResourceLabel(log.resource_type)}
|
||||
</div>
|
||||
<div className="erp-activity-time">{formatTimeAgo(log.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -407,10 +421,10 @@ export default function Home() {
|
||||
{[
|
||||
{ label: '系统版本', value: 'v0.1.0' },
|
||||
{ label: '后端框架', value: 'Axum 0.8 + Tokio' },
|
||||
{ label: '数据库', value: 'PostgreSQL 16' },
|
||||
{ label: '数据库', value: 'PostgreSQL 18' },
|
||||
{ label: '缓存', value: 'Redis 7' },
|
||||
{ label: '前端框架', value: 'React 19 + Ant Design 6' },
|
||||
{ label: '模块数量', value: '5 个业务模块' },
|
||||
{ label: '模块数量', value: '6 个业务模块' },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="erp-system-info-item">
|
||||
<span className="erp-system-info-label">{item.label}</span>
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function Login() {
|
||||
|
||||
<div style={{ marginTop: 32, textAlign: 'center' }}>
|
||||
<p style={{ fontSize: 12, color: '#475569', margin: 0 }}>
|
||||
ERP Platform v0.1.0 · Powered by Rust + React
|
||||
ERP Platform · Powered by Rust + React
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
12
dev.ps1
12
dev.ps1
@@ -19,6 +19,16 @@ $BackendPort = 3000
|
||||
$FrontendPort = 5174
|
||||
$LogDir = ".logs"
|
||||
|
||||
# --- environment variables ---
|
||||
$env:ERP__DATABASE__URL = "postgres://postgres:123123@localhost:5432/erp"
|
||||
$env:ERP__JWT__SECRET = "dev-secret-key-change-in-prod"
|
||||
$env:ERP__AUTH__SUPER_ADMIN_PASSWORD = "Admin@2026"
|
||||
$env:ERP__REDIS__URL = "redis://:redis_KBCYJk@129.204.154.246:6379"
|
||||
$env:ERP__WECHAT__APPID = "wx20f4ef9cc2ec66c5"
|
||||
$env:ERP__WECHAT__SECRET = "placeholder_wechat_secret"
|
||||
$env:ERP__HEALTH__AES_KEY = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
||||
$env:ERP__HEALTH__HMAC_KEY = "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5"
|
||||
|
||||
# --- find PID using port ---
|
||||
function Find-PortPid([int]$Port) {
|
||||
try {
|
||||
@@ -135,7 +145,9 @@ function Start-Services {
|
||||
$backendLog = Join-Path $LogDir "backend.log"
|
||||
$backendErr = Join-Path $LogDir "backend.err"
|
||||
|
||||
$backendDir = Join-Path $PSScriptRoot "crates\erp-server"
|
||||
$proc = Start-Process -FilePath "cargo" -ArgumentList "run","-p","erp-server" `
|
||||
-WorkingDirectory $backendDir `
|
||||
-RedirectStandardOutput $backendLog -RedirectStandardError $backendErr `
|
||||
-WindowStyle Hidden -PassThru
|
||||
|
||||
|
||||
Reference in New Issue
Block a user