fix: 全面 QA 审计修复 — 安全加固/代码质量/跨平台一致性/测试覆盖
Phase 0 安全热修复 (CRITICAL): - 外部化微信 appid/secret 到 ERP__WECHAT__APPID/SECRET 环境变量 - 正确连接 HealthCrypto 到 ERP__HEALTH__AES_KEY/HMAC_KEY 环境变量 - 外部化小程序加密密钥到 TARO_APP_ENCRYPTION_KEY 环境变量 - 移除小程序 auth store 中的敏感信息 console.log Phase 1 安全加固: - 微信自动注册 display_name 添加 sanitize 防止 XSS - 测试数据库凭据改为从 TEST_DB_URL 环境变量读取 Phase 2 代码质量: - 提取 useThemeMode hook 消除 22 处重复暗色模式检测 - 提取共享健康常量到 constants/health.ts - 拆分 patient_service.rs 脱敏函数到 masking.rs - 移除未使用的 i18next/react-i18next 依赖 - 移除未使用的 api/errors.ts 和 erp-auth/anyhow 依赖 Phase 3 测试覆盖: - 新增 5 个患者模块集成测试 (CRUD/租户隔离/验证/软删除) Phase 4 跨平台一致性: - 统一小程序 Patient.birthday → birth_date 匹配后端 - 统一小程序 Appointment.time_slot → start_time/end_time 匹配后端 Phase 5 架构: - 微信登录添加多租户 TODO 注释 - 更新 wiki/infrastructure.md 环境变量文档
This commit is contained in:
@@ -19,15 +19,15 @@
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"antd": "^6.3.5",
|
||||
"axios": "^1.15.0",
|
||||
"i18next": "^26.0.5",
|
||||
"dayjs": "^1.11.20",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^17.0.4",
|
||||
"react-router-dom": "^7.14.0",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -40,7 +40,6 @@
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.4",
|
||||
"@playwright/test": "^1.52.0"
|
||||
"vite": "^8.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/web/pnpm-lock.yaml
generated
3
apps/web/pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.15.0
|
||||
version: 1.15.0
|
||||
dayjs:
|
||||
specifier: ^1.11.20
|
||||
version: 1.11.20
|
||||
i18next:
|
||||
specifier: ^26.0.5
|
||||
version: 26.0.5(typescript@6.0.2)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export function extractErrorMessage(err: unknown, fallback = '操作失败'): string {
|
||||
if (err && typeof err === 'object' && 'response' in err) {
|
||||
const resp = (err as { response?: { data?: { message?: string } } }).response;
|
||||
if (resp?.data?.message) return resp.data.message;
|
||||
}
|
||||
if (err instanceof Error) return err.message;
|
||||
return fallback;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Badge, List, Popover, Button, Empty, Typography, theme } from 'antd';
|
||||
import { Badge, List, Popover, Button, Empty, Typography } from 'antd';
|
||||
import { BellOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMessageStore } from '../stores/message';
|
||||
import { useThemeMode } from '../hooks/useThemeMode';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -12,8 +13,7 @@ export default function NotificationPanel() {
|
||||
const unreadCount = useMessageStore((s) => s.unreadCount);
|
||||
const recentMessages = useMessageStore((s) => s.recentMessages);
|
||||
const markAsRead = useMessageStore((s) => s.markAsRead);
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
26
apps/web/src/constants/health.ts
Normal file
26
apps/web/src/constants/health.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 健康管理模块共享常量
|
||||
*
|
||||
* 集中定义性别、血型、患者状态等下拉选项,
|
||||
* 供 PatientList / PatientDetail 等页面复用。
|
||||
*/
|
||||
|
||||
export const GENDER_OPTIONS = [
|
||||
{ value: 'male', label: '男' },
|
||||
{ value: 'female', label: '女' },
|
||||
{ value: 'other', label: '其他' },
|
||||
];
|
||||
|
||||
export const BLOOD_TYPE_OPTIONS = [
|
||||
{ value: 'A', label: 'A 型' },
|
||||
{ value: 'B', label: 'B 型' },
|
||||
{ value: 'AB', label: 'AB 型' },
|
||||
{ value: 'O', label: 'O 型' },
|
||||
];
|
||||
|
||||
export const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'active', label: '活跃' },
|
||||
{ value: 'inactive', label: '停用' },
|
||||
{ value: 'deceased', label: '已故' },
|
||||
];
|
||||
15
apps/web/src/hooks/useThemeMode.ts
Normal file
15
apps/web/src/hooks/useThemeMode.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { theme } from 'antd';
|
||||
|
||||
/**
|
||||
* 判断当前是否处于暗色主题模式。
|
||||
*
|
||||
* 通过 antd design token 的 colorBgContainer 色值检测,
|
||||
* 统一替代各页面中重复的 `token.colorBgContainer === '#111827'` 内联判断。
|
||||
*/
|
||||
export function useThemeMode(): boolean {
|
||||
const { token } = theme.useToken();
|
||||
return (
|
||||
token.colorBgContainer === '#111827' ||
|
||||
token.colorBgContainer === 'rgb(17, 24, 39)'
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import zhCN from './locales/zh-CN.json';
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: { 'zh-CN': { translation: zhCN } },
|
||||
lng: 'zh-CN',
|
||||
fallbackLng: 'zh-CN',
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"create": "新建",
|
||||
"search": "搜索",
|
||||
"confirm": "确认",
|
||||
"loading": "加载中...",
|
||||
"success": "操作成功",
|
||||
"error": "操作失败"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "登录",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"submit": "登录",
|
||||
"success": "登录成功",
|
||||
"failed": "用户名或密码错误"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"users": "用户管理",
|
||||
"roles": "角色管理",
|
||||
"organizations": "组织管理",
|
||||
"workflow": "工作流",
|
||||
"messages": "消息中心",
|
||||
"settings": "系统设置",
|
||||
"plugins": "插件管理"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './i18n'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Row, Col, Spin, theme } from 'antd';
|
||||
import { Row, Col, Spin } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} 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';
|
||||
|
||||
interface DashboardStats {
|
||||
@@ -108,10 +109,9 @@ export default function Home() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const unreadCount = useMessageStore((s) => s.unreadCount);
|
||||
const fetchUnreadCount = useMessageStore((s) => s.fetchUnreadCount);
|
||||
const { token } = theme.useToken();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
message,
|
||||
Empty,
|
||||
Tag,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
ApartmentOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { useThemeMode } from '../hooks/useThemeMode';
|
||||
import {
|
||||
listOrgTree,
|
||||
createOrg,
|
||||
@@ -38,8 +38,7 @@ import {
|
||||
} from '../api/orgs';
|
||||
|
||||
export default function Organizations() {
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const cardStyle = {
|
||||
background: isDark ? '#111827' : '#FFFFFF',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Row, Col, Empty, Select, theme } from 'antd';
|
||||
import { Row, Col, Empty, Select } from 'antd';
|
||||
import { DashboardOutlined } from '@ant-design/icons';
|
||||
import { countPluginData, aggregatePluginData, listPluginData } from '../api/pluginData';
|
||||
import {
|
||||
@@ -19,12 +19,12 @@ import {
|
||||
SkeletonBreakdownCard,
|
||||
WidgetRenderer,
|
||||
} from './dashboard/DashboardWidgets';
|
||||
import { useThemeMode } from '../hooks/useThemeMode';
|
||||
|
||||
// ── 主组件 ──
|
||||
|
||||
export function PluginDashboardPage() {
|
||||
const { pluginId } = useParams<{ pluginId: string }>();
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [schemaLoading, setSchemaLoading] = useState(false);
|
||||
@@ -37,9 +37,7 @@ export function PluginDashboardPage() {
|
||||
const [widgets, setWidgets] = useState<DashboardWidget[]>([]);
|
||||
const [widgetData, setWidgetData] = useState<WidgetData[]>([]);
|
||||
const [widgetsLoading, setWidgetsLoading] = useState(false);
|
||||
const isDark =
|
||||
themeToken.colorBgContainer === '#111827' ||
|
||||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
// 加载 schema
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Popconfirm,
|
||||
Checkbox,
|
||||
message,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
type RoleInfo,
|
||||
type PermissionInfo,
|
||||
} from '../api/roles';
|
||||
import { useThemeMode } from '../hooks/useThemeMode';
|
||||
|
||||
export default function Roles() {
|
||||
const [roles, setRoles] = useState<RoleInfo[]>([]);
|
||||
@@ -35,8 +35,7 @@ export default function Roles() {
|
||||
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
|
||||
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchRoles = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Popconfirm,
|
||||
Checkbox,
|
||||
message,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
} from '../api/users';
|
||||
import { listRoles, type RoleInfo } from '../api/roles';
|
||||
import type { UserInfo } from '../api/auth';
|
||||
import { useThemeMode } from '../hooks/useThemeMode';
|
||||
|
||||
const STATUS_COLOR_MAP: Record<string, string> = {
|
||||
active: '#059669',
|
||||
@@ -65,8 +65,7 @@ export default function Users() {
|
||||
const [allRoles, setAllRoles] = useState<RoleInfo[]>([]);
|
||||
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchUsers = useCallback(async (p = page) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Button, Input, Spin, Popconfirm, message, theme, Typography } from 'antd';
|
||||
import { Button, Input, Spin, Popconfirm, message, Typography } from 'antd';
|
||||
import { SendOutlined, CloseCircleOutlined, ArrowUpOutlined } from '@ant-design/icons';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { consultationApi, type Session, type Message } from '../../api/health/consultations';
|
||||
import { StatusTag } from './components/StatusTag';
|
||||
import { ImagePreview } from './components/ImagePreview';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const PAGE_SIZE = 30;
|
||||
|
||||
@@ -53,10 +54,7 @@ export default function ConsultationDetail() {
|
||||
const chatEndRef = useRef<HTMLDivElement>(null);
|
||||
const shouldScrollRef = useRef(true);
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
const isDark =
|
||||
themeToken.colorBgContainer === '#111827' ||
|
||||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
// --- Fetch session info ---
|
||||
const fetchSession = useCallback(async () => {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Space,
|
||||
Popconfirm,
|
||||
message,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
@@ -18,6 +17,7 @@ import { StatusTag } from './components/StatusTag';
|
||||
import { PatientSelect } from './components/PatientSelect';
|
||||
import { DoctorSelect } from './components/DoctorSelect';
|
||||
import { ExportButton } from './components/ExportButton';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'waiting', label: '等待中' },
|
||||
@@ -70,10 +70,7 @@ export default function ConsultationList() {
|
||||
const [patientLabels, setPatientLabels] = useState<Record<string, string>>({});
|
||||
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
const isDark =
|
||||
themeToken.colorBgContainer === '#111827' ||
|
||||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
// --- Data fetching ---
|
||||
const fetchSessions = useCallback(async (params: { page: number; page_size: number; status?: string }) => {
|
||||
@@ -84,8 +81,9 @@ export default function ConsultationList() {
|
||||
setTotal(result.total);
|
||||
} catch {
|
||||
message.error('加载咨询列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, DatePicker, message, theme } from 'antd';
|
||||
import { Table, DatePicker, message } from 'antd';
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { followUpApi, type FollowUpRecord } from '../../api/health/followUp';
|
||||
import { PatientSelect } from './components/PatientSelect';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const RESULT_MAP: Record<string, string> = {
|
||||
normal: '正常',
|
||||
@@ -28,10 +29,7 @@ export default function FollowUpRecordList() {
|
||||
const [query, setQuery] = useState<QueryParams>({ page: 1, page_size: 20 });
|
||||
const [selectedPatient, setSelectedPatient] = useState<string | undefined>();
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
const isDark =
|
||||
themeToken.colorBgContainer === '#111827' ||
|
||||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
// --- Data fetching ---
|
||||
const fetchRecords = useCallback(async (params: QueryParams) => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Space,
|
||||
Popconfirm,
|
||||
message,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
@@ -19,6 +18,7 @@ import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type Update
|
||||
import { StatusTag } from './components/StatusTag';
|
||||
import { PatientSelect } from './components/PatientSelect';
|
||||
import { DoctorSelect } from './components/DoctorSelect';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'pending', label: '待处理' },
|
||||
@@ -94,10 +94,7 @@ export default function FollowUpTaskList() {
|
||||
const [patientLabels, setPatientLabels] = useState<Record<string, string>>({});
|
||||
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
const isDark =
|
||||
themeToken.colorBgContainer === '#111827' ||
|
||||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
// --- Data fetching ---
|
||||
const fetchTasks = useCallback(async (params: { page: number; page_size: number; status?: string }) => {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Tag,
|
||||
message,
|
||||
Spin,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
@@ -36,19 +35,8 @@ import { followUpApi } from '../../api/health/followUp';
|
||||
import type { FollowUpRecord } from '../../api/health/followUp';
|
||||
import { StatusTag } from './components/StatusTag';
|
||||
import { VitalSignsChart } from './components/VitalSignsChart';
|
||||
|
||||
const GENDER_OPTIONS = [
|
||||
{ value: 'male', label: '男' },
|
||||
{ value: 'female', label: '女' },
|
||||
{ value: 'other', label: '其他' },
|
||||
];
|
||||
|
||||
const BLOOD_TYPE_OPTIONS = [
|
||||
{ value: 'A', label: 'A 型' },
|
||||
{ value: 'B', label: 'B 型' },
|
||||
{ value: 'AB', label: 'AB 型' },
|
||||
{ value: 'O', label: 'O 型' },
|
||||
];
|
||||
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS } from '../../constants/health';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const GENDER_LABEL: Record<string, string> = {
|
||||
male: '男',
|
||||
@@ -63,10 +51,7 @@ export default function PatientDetail() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const { token } = theme.useToken();
|
||||
const isDark =
|
||||
token.colorBgContainer === '#111827' ||
|
||||
token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
// 健康数据子 tab 的状态
|
||||
const [vitalSigns, setVitalSigns] = useState<VitalSigns[]>([]);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Popconfirm,
|
||||
DatePicker,
|
||||
message,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -26,26 +25,8 @@ import type {
|
||||
UpdatePatientReq,
|
||||
} from '../../api/health/patients';
|
||||
import { StatusTag } from './components/StatusTag';
|
||||
|
||||
const GENDER_OPTIONS = [
|
||||
{ value: 'male', label: '男' },
|
||||
{ value: 'female', label: '女' },
|
||||
{ value: 'other', label: '其他' },
|
||||
];
|
||||
|
||||
const BLOOD_TYPE_OPTIONS = [
|
||||
{ value: 'A', label: 'A 型' },
|
||||
{ value: 'B', label: 'B 型' },
|
||||
{ value: 'AB', label: 'AB 型' },
|
||||
{ value: 'O', label: 'O 型' },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'active', label: '活跃' },
|
||||
{ value: 'inactive', label: '停用' },
|
||||
{ value: 'deceased', label: '已故' },
|
||||
];
|
||||
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS, STATUS_OPTIONS } from '../../constants/health';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
export default function PatientList() {
|
||||
const [patients, setPatients] = useState<PatientListItem[]>([]);
|
||||
@@ -57,10 +38,7 @@ export default function PatientList() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPatient, setEditingPatient] = useState<PatientListItem | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const { token } = theme.useToken();
|
||||
const isDark =
|
||||
token.colorBgContainer === '#111827' ||
|
||||
token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchPatients = useCallback(
|
||||
@@ -77,8 +55,9 @@ export default function PatientList() {
|
||||
setTotal(result.total);
|
||||
} catch {
|
||||
message.error('加载患者列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[page, searchText, statusFilter],
|
||||
);
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
Tag,
|
||||
Card,
|
||||
message,
|
||||
theme,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { TagsOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||
import { patientApi } from '../../api/health/patients';
|
||||
import type { PatientListItem } from '../../api/health/patients';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
export default function PatientTagManage() {
|
||||
const [patients, setPatients] = useState<PatientListItem[]>([]);
|
||||
@@ -24,10 +24,7 @@ export default function PatientTagManage() {
|
||||
const [selectedPatient, setSelectedPatient] = useState<PatientListItem | null>(null);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const isDark =
|
||||
token.colorBgContainer === '#111827' ||
|
||||
token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchPatients = useCallback(
|
||||
async (p = page) => {
|
||||
@@ -38,8 +35,9 @@ export default function PatientTagManage() {
|
||||
setTotal(result.total);
|
||||
} catch {
|
||||
message.error('加载患者列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[page],
|
||||
);
|
||||
@@ -69,8 +67,9 @@ export default function PatientTagManage() {
|
||||
fetchPatients();
|
||||
} catch {
|
||||
message.error('标签更新失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Table, Button, Modal, Form, Input, Select, message, theme, Tag } from 'antd';
|
||||
import { Table, Button, Modal, Form, Input, Select, message, Tag } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { listTemplates, createTemplate, type MessageTemplateInfo } from '../../api/messageTemplates';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const channelMap: Record<string, { label: string; color: string }> = {
|
||||
in_app: { label: '站内', color: '#2563eb' },
|
||||
@@ -18,8 +19,7 @@ export default function MessageTemplates() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchData = useCallback(async (p = page) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { Table, Button, Tag, Space, Modal, Typography, message, theme } from 'antd';
|
||||
import { Table, Button, Tag, Space, Modal, Typography, message } from 'antd';
|
||||
import { CheckOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { listMessages, markRead, markAllRead, deleteMessage, type MessageInfo, type MessageQuery } from '../../api/messages';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
@@ -21,8 +22,7 @@ export default function NotificationList({ queryFilter }: Props) {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchData = useCallback(async (p = page, filter?: MessageQuery) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, Switch, TimePicker, Button, message, theme } from 'antd';
|
||||
import { Form, Switch, TimePicker, Button, message } from 'antd';
|
||||
import { BellOutlined } from '@ant-design/icons';
|
||||
import client from '../../api/client';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
interface PreferencesData {
|
||||
dnd_enabled: boolean;
|
||||
@@ -13,8 +14,7 @@ export default function NotificationPreferences() {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dndEnabled, setDndEnabled] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ dnd_enabled: false });
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, Select, Input, Tag, message, theme } from 'antd';
|
||||
import { Table, Select, Input, Tag, message } from 'antd';
|
||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||
import { listAuditLogs, type AuditLogItem, type AuditLogQuery } from '../../api/auditLogs';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const RESOURCE_TYPE_OPTIONS = [
|
||||
{ value: 'user', label: '用户' },
|
||||
@@ -38,8 +39,7 @@ export default function AuditLogViewer() {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [query, setQuery] = useState<AuditLogQuery>({ page: 1, page_size: 20 });
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchLogs = useCallback(async (params: AuditLogQuery) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Table,
|
||||
Modal,
|
||||
Tag,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, SearchOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
updateSetting,
|
||||
deleteSetting,
|
||||
} from '../../api/settings';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
interface SettingEntry {
|
||||
key: string;
|
||||
@@ -29,8 +29,7 @@ export default function SystemSettings() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editEntry, setEditEntry] = useState<SettingEntry | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchKey.trim()) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { Table, Tag, theme } from 'antd';
|
||||
import { Table, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { listCompletedTasks, type TaskInfo } from '../../api/workflowTasks';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const outcomeStyles: Record<string, { bg: string; color: string; text: string }> = {
|
||||
approved: { bg: '#ECFDF5', color: '#059669', text: '同意' },
|
||||
@@ -14,8 +15,7 @@ export default function CompletedTasks() {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { Button, message, Modal, Table, Tag, theme } from 'antd';
|
||||
import { Button, message, Modal, Table, Tag } from 'antd';
|
||||
import { EyeOutlined, PauseCircleOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../../api/workflowInstances';
|
||||
import { getProcessDefinition, type NodeDef, type EdgeDef } from '../../api/workflowDefinitions';
|
||||
import ProcessViewer from './ProcessViewer';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const statusStyles: Record<string, { bg: string; color: string; text: string }> = {
|
||||
running: { bg: '#eff6ff', color: '#2563eb', text: '运行中' },
|
||||
@@ -30,8 +31,7 @@ export default function InstanceMonitor() {
|
||||
const [viewerEdges, setViewerEdges] = useState<EdgeDef[]>([]);
|
||||
const [activeNodeIds, setActiveNodeIds] = useState<string[]>([]);
|
||||
const [viewerLoading, setViewerLoading] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { Button, Input, message, Modal, Space, Table, Tag, theme } from 'antd';
|
||||
import { Button, Input, message, Modal, Space, Table, Tag } from 'antd';
|
||||
import { CheckOutlined, CloseOutlined, SendOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
delegateTask,
|
||||
type TaskInfo,
|
||||
} from '../../api/workflowTasks';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
export default function PendingTasks() {
|
||||
const [data, setData] = useState<TaskInfo[]>([]);
|
||||
@@ -18,8 +19,7 @@ export default function PendingTasks() {
|
||||
const [outcome, setOutcome] = useState('approved');
|
||||
const [delegateModal, setDelegateModal] = useState<TaskInfo | null>(null);
|
||||
const [delegateTo, setDelegateTo] = useState('');
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Button, message, Modal, Space, Table, Tag, theme } from 'antd';
|
||||
import { Button, message, Modal, Space, Table, Tag } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type CreateProcessDefinitionRequest,
|
||||
} from '../../api/workflowDefinitions';
|
||||
import ProcessDesigner from './ProcessDesigner';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const statusColors: Record<string, { bg: string; color: string; text: string }> = {
|
||||
draft: { bg: '#f8fafc', color: '#475569', text: '草稿' },
|
||||
@@ -25,8 +26,7 @@ export default function ProcessDefinitions() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [designerOpen, setDesignerOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const { token } = theme.useToken();
|
||||
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchData = useCallback(async (p = page) => {
|
||||
setLoading(true);
|
||||
|
||||
Reference in New Issue
Block a user