Commit 45960530 authored by yuguo's avatar yuguo

fix

parent caca4568
......@@ -57,7 +57,8 @@
"Bash(docker-compose ps:*)",
"Bash(cat web/src/app/\\\\\\(main\\\\\\)/admin/agents/page.tsx)",
"Bash(awk NR==68,NR==376:*)",
"Bash(LANG=en_US.UTF-8 sed:*)"
"Bash(LANG=en_US.UTF-8 sed:*)",
"Bash(echo:*)"
]
}
}
# UI Redesign Design — Modern Medical Style (Blue-White Minimalist)
## Overview
Full frontend UI redesign for the Internet Hospital platform, adopting a modern medical style inspired by platforms like DXY Doctor (丁香医生). The design emphasizes trust, professionalism, and clarity with a blue color palette.
## Color System
| Token | Value | Usage |
|-------|-------|-------|
| primary | `#2B6DE8` | Brand blue, buttons/links/selected |
| primary-light | `#5B8FF9` | Hover state |
| primary-dark | `#1A56C4` | Active state |
| primary-bg | `#F0F6FF` | Blue tag/badge backgrounds |
| success | `#52C41A` | Online/success/normal |
| warning | `#FAAD14` | Pending/alerts |
| danger | `#FF4D4F` | Error/urgent/delete |
| bg-base | `#F5F7FA` | Global page background |
| bg-card | `#FFFFFF` | Card/container background |
| sidebar-gradient | `#1A3B7A → #2B6DE8` | Sidebar background |
| text-primary | `#1D2129` | Title/body text |
| text-secondary | `#4E5969` | Description/helper text |
| text-disabled | `#A0A7B4` | Disabled/placeholder |
| border | `#E5E8EF` | Dividers/borders |
| hover-bg | `#F7F8FA` | List hover rows |
## Spacing System (4px multiples)
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon-text gap |
| sm | 8px | Compact element spacing |
| md | 12px | Form/list item spacing |
| base | 16px | Standard spacing/card padding |
| lg | 20px | Block spacing |
| xl | 24px | Page padding/large block spacing |
| 2xl | 32px | Section separation |
## Typography
| Level | Size | Weight | Usage |
|-------|------|--------|-------|
| H1 | 24px | 700 | Page title |
| H2 | 18px | 600 | Section/card title |
| H3 | 16px | 600 | Subtitle |
| Body | 14px | 400 | Body text |
| Small | 13px | 400 | Helper/description |
| Caption | 12px | 400 | Labels/timestamps |
## Shadow Levels
| Level | Value | Usage |
|-------|-------|-------|
| sm | `0 1px 3px rgba(0,0,0,0.04)` | Card default |
| md | `0 4px 12px rgba(0,0,0,0.08)` | Card hover / popover |
| lg | `0 8px 24px rgba(0,0,0,0.12)` | Modal / dropdown |
## Border Radius
| Element | Value |
|---------|-------|
| Button/Input | 8px |
| Card | 12px |
| Container/Modal | 16px |
| Avatar/Tag | full |
## Sidebar
- Background: `linear-gradient(180deg, #1A3B7A 0%, #2B6DE8 100%)`
- Logo: White icon + white text, bottom 1px rgba(255,255,255,0.1) divider
- Menu items: White text opacity 0.75, hover/selected: opacity 1 + left 3px white bar + bg rgba(255,255,255,0.1)
- Width: 220px expanded, 64px collapsed
## Header
- Height: 56px
- Background: white, bottom 1px solid #E5E8EF
- Left: Breadcrumb navigation
- Right: Search + notification bell (badge) + user avatar dropdown
## Card
- Background: white
- Border radius: 12px
- Shadow: sm level (no border)
- Padding: 20px standard, 16px compact
- Hover: shadow md + translateY(-1px) transition
## Stat Cards
- Left: 48x48 rounded(12px) icon container with light background
- Right: title(13px) + number(24px bold) + trend tag
- 4-column responsive grid
## Buttons
| Type | Style |
|------|-------|
| Primary | bg #2B6DE8, white text, hover #5B8FF9 |
| Secondary | white bg, #2B6DE8 border and text |
| Text | no border/bg, #2B6DE8 text |
| Danger | bg #FF4D4F, white text |
| Height | 36px standard, 32px compact, 40px large |
## Tables
- Header: #F7F8FA bg, 13px 600 weight
- Row height: 48px
- Hover: #F7F8FA bg
- Striping: odd white, even #FAFBFC
- Action column: text buttons, blue
- Pagination: right-aligned
## Role-Specific Variants
| Element | Patient | Doctor | Admin |
|---------|---------|--------|-------|
| Sidebar gradient | #1A3B7A → #2B6DE8 | #1A5C3A → #2B9E6E | #1A3B7A → #2B6DE8 |
| Primary color | #2B6DE8 | #2B9E6E | #2B6DE8 |
| Accent | #5B8FF9 | #52C41A | #5B8FF9 |
| Role tag | Blue | Green | Blue |
## Implementation Scope
Full redesign of all frontend pages:
- globals.css theme tokens
- Ant Design ConfigProvider theme
- All 3 role layouts (admin, doctor, patient)
- All dashboard pages
- All CRUD/list pages
- Auth pages (login/register)
- Consultation interfaces
- AI floating widget
- Charts color palette
This diff is collapsed.
......@@ -308,11 +308,9 @@ func main() {
authApi.POST("/user/verify-identity", userHandler.VerifyIdentity)
authApi.POST("/doctor/appointment", doctorHandler.MakeAppointment)
// 医生端路由(需要认证 + 医生角色)
doctorApi := authApi.Group("")
doctorApi.Use(middleware.RequireRole("doctor"))
// 医生端路由(需要认证,handler 内部通过 user_id 查 doctors 表验证医生身份)
doctorPortalHandler := doctorportal.NewHandler()
doctorPortalHandler.RegisterRoutes(doctorApi)
doctorPortalHandler.RegisterRoutes(authApi)
// 健康档案路由(需要认证)
healthHandler := health.NewHandler()
......
......@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"log"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
......@@ -14,6 +15,21 @@ import (
"internet-hospital/pkg/response"
)
// inferRoleFromAgent 根据 agentID 推断用户当前操作角色
// 优先使用 agentID 暗示的角色(因为前端根据页面路径选择了对应的 agent),回退到 JWT 角色
func inferRoleFromAgent(agentID, jwtRole string) string {
if strings.Contains(agentID, "doctor") {
return "doctor"
}
if strings.Contains(agentID, "admin") {
return "admin"
}
if strings.Contains(agentID, "patient") {
return "patient"
}
return jwtRole
}
// sendSSEEvent 发送 SSE 事件
func sendSSEEvent(c *gin.Context, flusher interface{ Flush() }, event string, data string) {
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, data)
......@@ -86,7 +102,8 @@ func (h *Handler) Chat(c *gin.Context) {
userID, _ := c.Get("user_id")
uid, _ := userID.(string)
role, _ := c.Get("role")
userRole, _ := role.(string)
jwtRole, _ := role.(string)
userRole := inferRoleFromAgent(agentID, jwtRole)
output, err := h.svc.Chat(c.Request.Context(), agentID, uid, userRole, req.SessionID, req.Message, req.Context)
if err != nil {
......@@ -128,7 +145,8 @@ func (h *Handler) ChatStream(c *gin.Context) {
userID, _ := c.Get("user_id")
uid, _ := userID.(string)
role, _ := c.Get("role")
userRole, _ := role.(string)
jwtRole, _ := role.(string)
userRole := inferRoleFromAgent(agentID, jwtRole)
emit := func(event, data string) {
sendSSEEvent(c, flusher, event, data)
......@@ -153,7 +171,8 @@ func (h *Handler) CreateSession(c *gin.Context) {
userID, _ := c.Get("user_id")
uid, _ := userID.(string)
role, _ := c.Get("role")
userRole, _ := role.(string)
jwtRole, _ := role.(string)
userRole := inferRoleFromAgent(req.AgentID, jwtRole)
sessionID := uuid.New().String()
session := model.AgentSession{
......
......@@ -8,6 +8,7 @@ type Role struct {
Code string `gorm:"uniqueIndex;size:50;not null" json:"code"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `gorm:"size:500" json:"description"`
HomePath string `gorm:"size:200" json:"home_path"` // 登录后默认首页路径
IsSystem bool `gorm:"default:false" json:"is_system"`
Status string `gorm:"size:20;default:active" json:"status"` // active | disabled
Sort int `gorm:"default:0" json:"sort"`
......
......@@ -94,7 +94,35 @@ func (h *Handler) UpdateMenu(c *gin.Context) {
}
updates := map[string]interface{}{}
if req.ParentID != nil {
updates["parent_id"] = *req.ParentID
newParentID := *req.ParentID
// 不能将自己设为自己的父菜单
if newParentID == menu.ID {
response.Error(c, 400, "不能将菜单设为自己的上级")
return
}
// 检查循环引用:新父节点不能是自己的后代
if newParentID != 0 {
var allMenus []model.Menu
db.Find(&allMenus)
menuMap := map[uint]uint{}
for _, m := range allMenus {
menuMap[m.ID] = m.ParentID
}
// 模拟修改后,从 newParentID 向上追溯是否会回到 menu.ID
menuMap[menu.ID] = newParentID
cur := newParentID
for i := 0; i < 100; i++ {
if cur == 0 {
break
}
if cur == menu.ID {
response.Error(c, 400, "不能将菜单设为其子菜单的下级,会产生循环引用")
return
}
cur = menuMap[cur]
}
}
updates["parent_id"] = newParentID
}
if req.Name != "" {
updates["name"] = req.Name
......
......@@ -26,13 +26,21 @@ func (h *Handler) ListRoles(c *gin.Context) {
func (h *Handler) SetUserRole(c *gin.Context) {
userID := c.Param("id")
var req struct {
Role string `json:"role" binding:"required,oneof=patient doctor admin"`
Role string `json:"role" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "role 必须为 patient、doctor 或 admin")
response.BadRequest(c, "role 不能为空")
return
}
db := database.GetDB()
// 从 DB 验证角色是否存在
var roleRecord model.Role
if err := db.Where("code = ? AND status = ?", req.Role, "active").First(&roleRecord).Error; err != nil {
response.BadRequest(c, "角色不存在: "+req.Role)
return
}
var user model.User
if err := db.Where("id = ?", userID).First(&user).Error; err != nil {
response.Error(c, 404, "用户不存在")
......@@ -102,23 +110,76 @@ type MenuTreeNode struct {
}
// GetMyMenus 基于 JWT role 返回对应端菜单树
// admin → /admin/* 菜单, patient → /patient/* 菜单, doctor → /doctor/* 菜单
// 优先从 role_menus 关联表获取该角色的菜单,若未配置则降级为路径前缀过滤
func (h *Handler) GetMyMenus(c *gin.Context) {
role, _ := c.Get("role")
roleStr, _ := role.(string)
if roleStr == "" {
response.Success(c, []MenuTreeNode{})
return
}
db := database.GetDB()
// 1. 查找角色记录
var roleRecord model.Role
if err := db.Where("code = ?", roleStr).First(&roleRecord).Error; err == nil {
// 2. 从 role_menus 获取关联的菜单ID
var menuIDs []uint
db.Model(&model.RoleMenu{}).Where("role_id = ?", roleRecord.ID).Pluck("menu_id", &menuIDs)
if len(menuIDs) > 0 {
// 3. 查询这些菜单(仅 visible + active)
var menus []model.Menu
db.Where("id IN ? AND visible = ? AND status = ?", menuIDs, true, "active").
Order("sort asc, id asc").Find(&menus)
// 4. 确保父节点也被包含(即使父节点未在 role_menus 中)
existIDs := map[uint]bool{}
for _, m := range menus {
existIDs[m.ID] = true
}
missingParentIDs := map[uint]bool{}
for _, m := range menus {
if m.ParentID != 0 && !existIDs[m.ParentID] {
missingParentIDs[m.ParentID] = true
}
}
// 递归补全所有父节点
for len(missingParentIDs) > 0 {
ids := make([]uint, 0, len(missingParentIDs))
for id := range missingParentIDs {
ids = append(ids, id)
}
var parents []model.Menu
db.Where("id IN ? AND visible = ? AND status = ?", ids, true, "active").Find(&parents)
missingParentIDs = map[uint]bool{}
for _, p := range parents {
if !existIDs[p.ID] {
menus = append(menus, p)
existIDs[p.ID] = true
if p.ParentID != 0 && !existIDs[p.ParentID] {
missingParentIDs[p.ParentID] = true
}
}
}
}
response.Success(c, buildMenuTree(menus, 0))
return
}
}
// 降级:role_menus 未配置时,按路径前缀过滤
prefix := rolePathPrefix(roleStr)
if prefix == "" {
response.Success(c, []MenuTreeNode{})
return
}
db := database.GetDB()
var menus []model.Menu
var allMenus []model.Menu
db.Where("visible = ? AND status = ?", true, "active").
Order("sort asc, id asc").Find(&menus)
filtered := filterMenusByPrefix(menus, prefix)
Order("sort asc, id asc").Find(&allMenus)
filtered := filterMenusByPrefix(allMenus, prefix)
response.Success(c, buildMenuTree(filtered, 0))
}
......
This diff is collapsed.
......@@ -3,6 +3,7 @@ package doctor
import (
"context"
"errors"
"time"
"gorm.io/gorm"
......@@ -43,7 +44,10 @@ func (s *Service) GetDepartments(ctx context.Context) ([]model.Department, error
type DoctorWithDepartment struct {
model.Doctor
DepartmentName string `json:"department_name"`
DepartmentName string `json:"department_name"`
NextAvailableDate *string `json:"next_available_date"`
NextAvailableTime *string `json:"next_available_time"`
AvailableSlots int `json:"available_slots"`
}
func (s *Service) GetDoctorList(ctx context.Context, params *DoctorListParams) (*PaginatedResponse, error) {
......@@ -54,46 +58,59 @@ func (s *Service) GetDoctorList(ctx context.Context, params *DoctorListParams) (
params.PageSize = 20
}
// 先查询医生列表
query := s.db.Model(&model.Doctor{}).Where("status = ?", "approved")
today := time.Now().Format("2006-01-02")
weekLater := time.Now().AddDate(0, 0, 7).Format("2006-01-02")
subQuery := s.db.Table("doctor_schedules").
Select("doctor_id, MIN(date) as next_available_date, MIN(start_time) as next_available_time, SUM(remaining) as available_slots").
Where("date >= ? AND date <= ? AND remaining > 0", today, weekLater).
Group("doctor_id")
query := s.db.Table("doctors").
Select("doctors.*, departments.name as department_name, sched.next_available_date, sched.next_available_time, sched.available_slots").
Joins("LEFT JOIN departments ON doctors.department_id = departments.id").
Joins("LEFT JOIN (?) as sched ON doctors.id = sched.doctor_id", subQuery).
Where("doctors.status = ? AND doctors.deleted_at IS NULL", "approved")
if params.DepartmentID != "" {
query = query.Where("department_id = ?", params.DepartmentID)
query = query.Where("doctors.department_id = ?", params.DepartmentID)
}
if params.Keyword != "" {
query = query.Where("name LIKE ?", "%"+params.Keyword+"%")
query = query.Where("doctors.name LIKE ?", "%"+params.Keyword+"%")
}
var total int64
query.Count(&total)
s.db.Table("doctors").Where("status = ? AND deleted_at IS NULL", "approved").
Scopes(func(db *gorm.DB) *gorm.DB {
if params.DepartmentID != "" {
db = db.Where("department_id = ?", params.DepartmentID)
}
if params.Keyword != "" {
db = db.Where("name LIKE ?", "%"+params.Keyword+"%")
}
return db
}).Count(&total)
// 有排班的排前面
baseOrder := "CASE WHEN sched.next_available_date IS NULL THEN 1 ELSE 0 END"
switch params.SortBy {
case "rating":
query = query.Order("rating DESC")
query = query.Order(baseOrder + ", doctors.rating DESC")
case "consult_count":
query = query.Order("consult_count DESC")
query = query.Order(baseOrder + ", doctors.consult_count DESC")
case "price":
query = query.Order("price ASC")
query = query.Order(baseOrder + ", doctors.price ASC")
default:
query = query.Order("rating DESC")
query = query.Order(baseOrder + ", doctors.rating DESC")
}
var doctors []model.Doctor
var result []DoctorWithDepartment
offset := (params.Page - 1) * params.PageSize
if err := query.Preload("Department").Offset(offset).Limit(params.PageSize).Find(&doctors).Error; err != nil {
if err := query.Offset(offset).Limit(params.PageSize).Scan(&result).Error; err != nil {
return nil, err
}
// 构建返回结果
var result []DoctorWithDepartment
for _, doctor := range doctors {
result = append(result, DoctorWithDepartment{
Doctor: doctor,
DepartmentName: doctor.Department.Name,
})
}
return &PaginatedResponse{
List: result,
Total: total,
......
......@@ -172,10 +172,15 @@ func (s *Service) CreatePrescription(ctx context.Context, doctorID string, req *
// 更新库存状态
tx.Exec("UPDATE medicines SET status = CASE WHEN stock <= 0 THEN 'out_of_stock' WHEN stock <= stock_warning THEN 'low_stock' ELSE 'normal' END WHERE deleted_at IS NULL")
// 如果有关联问诊,更新问诊的处方ID
// 如果有关联问诊,更新问诊的处方ID和诊断信息
if req.ConsultID != "" {
tx.Model(&model.Consultation{}).Where("id = ?", req.ConsultID).
Update("prescription_id", prescription.ID)
updates := map[string]interface{}{
"prescription_id": prescription.ID,
}
if req.Diagnosis != "" {
updates["diagnosis"] = req.Diagnosis
}
tx.Model(&model.Consultation{}).Where("id = ?", req.ConsultID).Updates(updates)
}
tx.Commit()
......
......@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"math/rand"
"strings"
"time"
"github.com/google/uuid"
......@@ -47,11 +46,12 @@ type LoginRequest struct {
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
User *model.User `json:"user"`
Menus []model.Menu `json:"menus"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
User *model.User `json:"user"`
HomePath string `json:"home_path"`
Menus []model.Menu `json:"menus"`
}
func (s *Service) SendCode(ctx context.Context, phone string) error {
......@@ -97,10 +97,13 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes
return nil, err
}
// 确定用户角色,默认为 patient
// 确定用户角色:验证请求的角色是否存在于 DB,默认为 patient
role := "patient"
if req.Role == "doctor" || req.Role == "admin" {
role = req.Role
if req.Role != "" {
var roleRecord model.Role
if err := s.db.Where("code = ? AND status = ?", req.Role, "active").First(&roleRecord).Error; err == nil {
role = req.Role
}
}
fmt.Printf("最终角色: %s\n", role)
......@@ -135,11 +138,16 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes
return nil, err
}
menus := s.getUserMenus(user.ID)
homePath := s.getRoleHomePath(user.Role)
return &LoginResponse{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
ExpiresIn: tokenPair.ExpiresIn,
User: user,
HomePath: homePath,
Menus: menus,
}, nil
}
......@@ -172,12 +180,14 @@ func (s *Service) Login(ctx context.Context, req *LoginRequest) (*LoginResponse,
}
menus := s.getUserMenus(user.ID)
homePath := s.getRoleHomePath(user.Role)
return &LoginResponse{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
ExpiresIn: tokenPair.ExpiresIn,
User: &user,
HomePath: homePath,
Menus: menus,
}, nil
}
......@@ -192,8 +202,9 @@ func (s *Service) GetUserByID(ctx context.Context, userID string) (*model.User,
// UserProfileResponse 用户完整信息响应(包含菜单)
type UserProfileResponse struct {
User *model.User `json:"user"`
Menus []model.Menu `json:"menus"`
User *model.User `json:"user"`
HomePath string `json:"home_path"`
Menus []model.Menu `json:"menus"`
}
// GetUserProfile 获取用户完整信息(包含菜单树)
......@@ -204,86 +215,80 @@ func (s *Service) GetUserProfile(ctx context.Context, userID string) (*UserProfi
}
menus := s.getUserMenus(userID)
homePath := s.getRoleHomePath(user.Role)
return &UserProfileResponse{
User: &user,
Menus: menus,
User: &user,
HomePath: homePath,
Menus: menus,
}, nil
}
// getUserMenus 基于 User.Role 返回对应端菜单树
// admin → /admin/* , patient → /patient/* , doctor → /doctor/*
// getRoleHomePath 查询角色的 home_path
func (s *Service) getRoleHomePath(roleCode string) string {
var role model.Role
if err := s.db.Select("home_path").Where("code = ?", roleCode).First(&role).Error; err != nil {
return ""
}
return role.HomePath
}
// getUserMenus 基于用户角色从 role_menus 关联表获取菜单树
func (s *Service) getUserMenus(userID string) []model.Menu {
var user model.User
if err := s.db.Select("role").Where("id = ?", userID).First(&user).Error; err != nil {
fmt.Printf("[getUserMenus] 查询用户失败: %v\n", err)
return nil
}
prefix := rolePathPrefix(user.Role)
fmt.Printf("[getUserMenus] userID=%s role=%s prefix=%s\n", userID, user.Role, prefix)
if prefix == "" {
return []model.Menu{}
// 查找角色记录
var roleRecord model.Role
if err := s.db.Where("code = ?", user.Role).First(&roleRecord).Error; err != nil {
return nil
}
var flatMenus []model.Menu
s.db.Where("visible = ? AND status = ?", true, "active").
Order("sort ASC, id ASC").Find(&flatMenus)
fmt.Printf("[getUserMenus] 查询到 %d 条可见菜单\n", len(flatMenus))
filtered := filterMenusByPrefix(flatMenus, prefix)
fmt.Printf("[getUserMenus] 过滤后 %d 条菜单 (prefix=%s)\n", len(filtered), prefix)
// 从 role_menus 获取关联的菜单ID
var menuIDs []uint
s.db.Model(&model.RoleMenu{}).Where("role_id = ?", roleRecord.ID).Pluck("menu_id", &menuIDs)
if len(menuIDs) == 0 {
return nil
}
tree := buildMenuTree(filtered, 0)
fmt.Printf("[getUserMenus] 构建菜单树 %d 个顶级节点\n", len(tree))
return tree
}
// 查询这些菜单(仅 visible + active)
var menus []model.Menu
s.db.Where("id IN ? AND visible = ? AND status = ?", menuIDs, true, "active").
Order("sort asc, id asc").Find(&menus)
// rolePathPrefix 返回角色对应的路由前缀
func rolePathPrefix(role string) string {
switch role {
case "admin":
return "/admin/"
case "patient":
return "/patient/"
case "doctor":
return "/doctor/"
default:
return ""
// 确保父节点也被包含
existIDs := map[uint]bool{}
for _, m := range menus {
existIDs[m.ID] = true
}
}
// filterMenusByPrefix 筛选属于指定路径前缀的菜单(包含父级分组节点)
func filterMenusByPrefix(menus []model.Menu, prefix string) []model.Menu {
matchIDs := map[uint]bool{}
parentIDs := map[uint]bool{}
missingParentIDs := map[uint]bool{}
for _, m := range menus {
if m.Path != "" && strings.HasPrefix(m.Path, prefix) {
matchIDs[m.ID] = true
if m.ParentID != 0 {
parentIDs[m.ParentID] = true
}
if m.ParentID != 0 && !existIDs[m.ParentID] {
missingParentIDs[m.ParentID] = true
}
}
for changed := true; changed; {
changed = false
for _, m := range menus {
if parentIDs[m.ID] && !matchIDs[m.ID] {
matchIDs[m.ID] = true
if m.ParentID != 0 {
parentIDs[m.ParentID] = true
for len(missingParentIDs) > 0 {
ids := make([]uint, 0, len(missingParentIDs))
for id := range missingParentIDs {
ids = append(ids, id)
}
var parents []model.Menu
s.db.Where("id IN ? AND visible = ? AND status = ?", ids, true, "active").Find(&parents)
missingParentIDs = map[uint]bool{}
for _, p := range parents {
if !existIDs[p.ID] {
menus = append(menus, p)
existIDs[p.ID] = true
if p.ParentID != 0 && !existIDs[p.ParentID] {
missingParentIDs[p.ParentID] = true
}
changed = true
}
}
}
var result []model.Menu
for _, m := range menus {
if matchIDs[m.ID] {
result = append(result, m)
}
}
return result
return buildMenuTree(menus, 0)
}
// buildMenuTree 将扁平菜单列表构建为树形结构
......
......@@ -161,9 +161,9 @@ func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, e
messages = append(messages, input.History...)
messages = append(messages, ai.ChatMessage{Role: "user", Content: input.Message})
// v15: 根据用户消息智能筛选工具
// v15: 根据用户消息智能筛选工具(按角色过滤参数,如 navigate_page 只展示对应角色的页面)
selectedTools := a.getFilteredTools(input.Message)
tools := a.getToolSchemas(selectedTools)
tools := a.getToolSchemasForRole(selectedTools, input.UserRole)
var toolCallResults []ToolCallResult
totalTokens := 0
......@@ -276,9 +276,9 @@ func (a *ReActAgent) RunStream(ctx context.Context, input AgentInput, onEvent fu
messages = append(messages, input.History...)
messages = append(messages, ai.ChatMessage{Role: "user", Content: input.Message})
// v15: 根据用户消息智能筛选工具
// v15: 根据用户消息智能筛选工具(按角色过滤参数,如 navigate_page 只展示对应角色的页面)
selectedTools := a.getFilteredTools(input.Message)
tools := a.getToolSchemas(selectedTools)
tools := a.getToolSchemasForRole(selectedTools, input.UserRole)
var toolCallResults []ToolCallResult
totalTokens := 0
......@@ -459,7 +459,11 @@ func (a *ReActAgent) getFilteredTools(userMessage string) []string {
}
func (a *ReActAgent) getToolSchemas(toolNames []string) []interface{} {
schemas := a.registry.GetSchemas(toolNames)
return a.getToolSchemasForRole(toolNames, "")
}
func (a *ReActAgent) getToolSchemasForRole(toolNames []string, role string) []interface{} {
schemas := a.registry.GetSchemasForRole(toolNames, role)
result := make([]interface{}, len(schemas))
for i, s := range schemas {
result[i] = s
......
......@@ -26,12 +26,17 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) {
}
func (r *ToolRegistry) GetSchemas(names []string) []ToolSchema {
return r.GetSchemasForRole(names, "")
}
// GetSchemasForRole 获取工具 Schema,支持按角色过滤参数(如 navigate_page 只展示对应角色的页面)
func (r *ToolRegistry) GetSchemasForRole(names []string, role string) []ToolSchema {
r.mu.RLock()
defer r.mu.RUnlock()
var schemas []ToolSchema
for _, name := range names {
if t, ok := r.tools[name]; ok {
schemas = append(schemas, ToSchema(t))
schemas = append(schemas, ToSchemaForRole(t, role))
}
}
return schemas
......
......@@ -10,6 +10,13 @@ type Tool interface {
Execute(ctx context.Context, params map[string]interface{}) (interface{}, error)
}
// RoleAwareTool 角色感知工具接口 — 工具参数可根据用户角色动态调整
// 例如 navigate_page 工具只向患者展示患者端页面,避免 LLM 生成跨角色导航
type RoleAwareTool interface {
Tool
ParametersForRole(role string) []ToolParameter
}
// ToolParameter 工具参数定义
type ToolParameter struct {
Name string `json:"name"`
......@@ -52,9 +59,25 @@ type PropertySchema struct {
// ToSchema 将 Tool 转换为 OpenAI Function Calling 格式
func ToSchema(t Tool) ToolSchema {
return ToSchemaForRole(t, "")
}
// ToSchemaForRole 将 Tool 转换为 OpenAI Function Calling 格式,支持按角色过滤参数
func ToSchemaForRole(t Tool, role string) ToolSchema {
// 如果工具实现了 RoleAwareTool 且传入了角色,使用角色感知的参数
var params []ToolParameter
if role != "" {
if rat, ok := t.(RoleAwareTool); ok {
params = rat.ParametersForRole(role)
}
}
if params == nil {
params = t.Parameters()
}
props := make(map[string]PropertySchema)
required := make([]string, 0) // 必须初始化为空数组,否则 JSON 序列化为 null 会导致 OpenAI 报错
for _, p := range t.Parameters() {
for _, p := range params {
props[p.Name] = PropertySchema{
Type: p.Type,
Description: p.Description,
......
......@@ -87,56 +87,33 @@ func (t *NavigatePageTool) Description() string {
return "为用户准备页面导航(不会自动打开页面)。调用此工具后,系统会在对话中显示一个'打开页面'按钮,用户点击后才会跳转。【重要】必须根据当前用户角色选择对应端的页面:admin用户选admin_*页面,doctor用户选doctor_*页面,patient用户选patient_*页面,否则会被拒绝访问。"
}
func (t *NavigatePageTool) Parameters() []agent.ToolParameter {
return t.ParametersForRole("")
}
// ParametersForRole 实现 RoleAwareTool 接口,按用户角色过滤可导航的页面
// 患者只能看到患者端页面,医生只能看到医生端页面,admin 可看到所有页面
func (t *NavigatePageTool) ParametersForRole(role string) []agent.ToolParameter {
cache := loadMenuCache()
// 构建 page_code 枚举和说明(按角色分组)
// 构建 page_code 枚举和说明(按角色过滤)
codes := make([]string, 0, len(cache))
adminPages := make([]string, 0)
doctorPages := make([]string, 0)
patientPages := make([]string, 0)
publicPages := make([]string, 0)
var pageDescs []string
for code, entry := range cache {
codes = append(codes, code)
pageInfo := fmt.Sprintf("%s (%s)", code, entry.Name)
switch entry.RoleScope {
case "admin":
adminPages = append(adminPages, pageInfo)
case "doctor":
doctorPages = append(doctorPages, pageInfo)
case "patient":
patientPages = append(patientPages, pageInfo)
default:
publicPages = append(publicPages, pageInfo)
// 按角色过滤:非 admin 用户只能看到自己角色对应的页面和公共页面
if role != "" && role != "admin" {
if entry.RoleScope != role && entry.RoleScope != "public" {
continue
}
}
codes = append(codes, code)
pageDescs = append(pageDescs, fmt.Sprintf("- %s (%s)", code, entry.Name))
}
// 构建按角色分组的说明
var desc strings.Builder
desc.WriteString("目标页面代码。【权限说明】只能导航到当前用户角色对应的页面,否则会被拒绝。")
if len(adminPages) > 0 {
desc.WriteString("\n\n【管理端页面 - 仅admin可访问】")
for _, p := range adminPages {
desc.WriteString("\n- " + p)
}
}
if len(doctorPages) > 0 {
desc.WriteString("\n\n【医生端页面 - 仅doctor可访问】")
for _, p := range doctorPages {
desc.WriteString("\n- " + p)
}
}
if len(patientPages) > 0 {
desc.WriteString("\n\n【患者端页面 - 仅patient可访问】")
for _, p := range patientPages {
desc.WriteString("\n- " + p)
}
}
if len(publicPages) > 0 {
desc.WriteString("\n\n【公共页面】")
for _, p := range publicPages {
desc.WriteString("\n- " + p)
}
desc.WriteString("目标页面代码。可导航的页面列表:")
for _, p := range pageDescs {
desc.WriteString("\n" + p)
}
return []agent.ToolParameter{
......
-- 清理不存在的菜单ID关联
DELETE FROM role_menus WHERE menu_id NOT IN (SELECT id FROM menus);
-- 管理端: 添加 AI日志(37)
INSERT INTO role_menus (role_id, menu_id) VALUES (1, 37) ON CONFLICT DO NOTHING;
-- 医生端(role_id=2): 补全缺失菜单
-- 现有: 24(工作台),25(问诊大厅),27(排班管理),28(收入统计),29(个人中心)
-- 缺失: 26(患者管理),38(AI辅助诊断),39(资质认证),40(慢病审核),41(待办任务)
INSERT INTO role_menus (role_id, menu_id) VALUES
(2, 26), (2, 38), (2, 39), (2, 40), (2, 41)
ON CONFLICT DO NOTHING;
-- 患者端(role_id=3): 补全所有菜单
-- 30(首页),31(找医生),32(我的问诊),33(慢病管理),34(我的处方),35(健康档案),36(个人中心),42(支付记录),43(药品订单)
INSERT INTO role_menus (role_id, menu_id) VALUES
(3, 30), (3, 31), (3, 32), (3, 33), (3, 34), (3, 35), (3, 36), (3, 42), (3, 43)
ON CONFLICT DO NOTHING;
INSERT INTO menus (parent_id, name, path, icon, type, sort, visible, status, created_at, updated_at) VALUES
(4, 'AI日志', '/admin/ai-logs', 'FileTextOutlined', 'menu', 9, true, 'active', NOW(), NOW()),
(0, 'AI辅助诊断', '/doctor/ai-assist', 'RobotOutlined', 'menu', 7, true, 'active', NOW(), NOW()),
(0, '资质认证', '/doctor/certification', 'SafetyCertificateOutlined', 'menu', 8, true, 'active', NOW(), NOW()),
(0, '慢病审核', '/doctor/chronic/review', 'HeartOutlined', 'menu', 9, true, 'active', NOW(), NOW()),
(0, '待办任务', '/doctor/tasks', 'CheckCircleOutlined', 'menu', 10, true, 'active', NOW(), NOW()),
(0, '支付记录', '/patient/payment', 'DollarOutlined', 'menu', 8, true, 'active', NOW(), NOW()),
(0, '药品订单', '/patient/pharmacy/order', 'ShoppingOutlined', 'menu', 9, true, 'active', NOW(), NOW());
......@@ -88,6 +88,7 @@ func main() {
{ParentID: 4, Name: "知识库", Path: "/admin/knowledge", Icon: "BookOutlined", Type: "menu", Sort: 6, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 4, Name: "内容安全", Path: "/admin/safety", Icon: "SafetyOutlined", Type: "menu", Sort: 7, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 4, Name: "AI运营中心", Path: "/admin/ai-center", Icon: "FundOutlined", Type: "menu", Sort: 8, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 4, Name: "AI日志", Path: "/admin/ai-logs", Icon: "FileTextOutlined", Type: "menu", Sort: 9, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
}
for i := range aiMenus {
db.Create(&aiMenus[i])
......@@ -102,11 +103,43 @@ func main() {
db.Create(&sysMenus[i])
}
// 医生端菜单
doctorMenus := []Menu{
{ParentID: 0, Name: "工作台", Path: "/doctor/workbench", Icon: "DashboardOutlined", Type: "menu", Sort: 1, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "问诊大厅", Path: "/doctor/consult", Icon: "MessageOutlined", Type: "menu", Sort: 2, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "患者管理", Path: "/doctor/patient", Icon: "UserOutlined", Type: "menu", Sort: 3, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "排班管理", Path: "/doctor/schedule", Icon: "CalendarOutlined", Type: "menu", Sort: 4, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "收入统计", Path: "/doctor/income", Icon: "DollarOutlined", Type: "menu", Sort: 5, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "个人中心", Path: "/doctor/profile", Icon: "UserOutlined", Type: "menu", Sort: 6, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "AI辅助诊断", Path: "/doctor/ai-assist", Icon: "RobotOutlined", Type: "menu", Sort: 7, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "资质认证", Path: "/doctor/certification", Icon: "SafetyCertificateOutlined", Type: "menu", Sort: 8, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "慢病审核", Path: "/doctor/chronic/review", Icon: "HeartOutlined", Type: "menu", Sort: 9, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "待办任务", Path: "/doctor/tasks", Icon: "CheckCircleOutlined", Type: "menu", Sort: 10, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
}
for i := range doctorMenus {
db.Create(&doctorMenus[i])
}
// 患者端菜单
patientMenus := []Menu{
{ParentID: 0, Name: "首页", Path: "/patient/home", Icon: "HomeOutlined", Type: "menu", Sort: 1, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "找医生", Path: "/patient/doctors", Icon: "TeamOutlined", Type: "menu", Sort: 2, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "我的问诊", Path: "/patient/consult", Icon: "MessageOutlined", Type: "menu", Sort: 3, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "慢病管理", Path: "/patient/chronic", Icon: "HeartOutlined", Type: "menu", Sort: 4, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "我的处方", Path: "/patient/prescription", Icon: "FileTextOutlined", Type: "menu", Sort: 5, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "健康档案", Path: "/patient/health-records", Icon: "FolderOpenOutlined", Type: "menu", Sort: 6, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "个人中心", Path: "/patient/profile", Icon: "UserOutlined", Type: "menu", Sort: 7, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "支付记录", Path: "/patient/payment", Icon: "DollarOutlined", Type: "menu", Sort: 8, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
{ParentID: 0, Name: "药品订单", Path: "/patient/pharmacy/order", Icon: "ShoppingOutlined", Type: "menu", Sort: 9, Visible: true, Status: "active", CreatedAt: now, UpdatedAt: now},
}
for i := range patientMenus {
db.Create(&patientMenus[i])
}
fmt.Println("✅ 菜单数据更新完成!")
fmt.Println("📋 菜单结构:")
fmt.Println(" 1. 运营大盘")
fmt.Println(" 2. 用户管理 (患者/医生/管理员)")
fmt.Println(" 3. 医疗业务 (科室/问诊/处方/药品/合规)")
fmt.Println(" 4. 智能体平台 (AI配置/Agent/工具/工作流/审核/知识库/安全/运营)")
fmt.Println(" 5. 系统管理 (角色/菜单)")
fmt.Println(" 管理端: 5个顶级菜单 + 19个子菜单")
fmt.Println(" 医生端: 10个菜单")
fmt.Println(" 患者端: 9个菜单")
fmt.Println(" 总计: 43个菜单")
}
......@@ -16,6 +16,9 @@ export interface Doctor {
price: number; // 问诊价格(分)
is_online: boolean;
schedule: DoctorSchedule[];
next_available_date: string | null;
next_available_time: string | null;
available_slots: number;
}
export interface DoctorSchedule {
......
......@@ -2,7 +2,7 @@ import { post, get, put } from './request';
import { MOCK_ENABLED, mockRegister, mockLogin } from './mock';
// 用户相关类型定义
export type UserRole = 'patient' | 'doctor' | 'admin';
export type UserRole = string;
export interface LoginParams {
phone?: string;
......@@ -16,7 +16,7 @@ export interface RegisterParams {
password: string;
code: string;
real_name?: string;
role?: 'patient' | 'doctor';
role?: string;
gender?: string;
age?: number;
}
......@@ -26,7 +26,7 @@ export interface UserInfo {
phone: string;
real_name: string;
avatar: string;
role: 'patient' | 'doctor' | 'admin';
role: string;
is_verified: boolean;
status: 'active' | 'disabled';
gender?: string;
......@@ -39,6 +39,7 @@ export interface LoginResponse {
refresh_token: string;
expires_in: number;
user: UserInfo;
home_path: string;
menus: Array<{
id: number;
name: string;
......@@ -91,6 +92,7 @@ export const userApi = {
// 获取用户完整信息(包含菜单)
getUserProfile: () => get<{
user: UserInfo;
home_path: string;
menus: Array<{
id: number;
parent_id: number;
......
This diff is collapsed.
......@@ -16,7 +16,7 @@ const { Title, Text, Paragraph } = Typography;
const RegisterPage: React.FC = () => {
const router = useRouter();
const { message } = App.useApp();
const { setUser, setTokens } = useUserStore();
const { setUser, setTokens, setHomePath } = useUserStore();
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [countdown, setCountdown] = useState(0);
......@@ -47,14 +47,13 @@ const RegisterPage: React.FC = () => {
try {
const response = await userApi.register({ ...values, code: values.code, role: registerType });
setTokens(response.data.access_token, response.data.refresh_token);
setUser(response.data.user);
const homePath = response.data.home_path || '/patient/home';
setUser(response.data.user, response.data.menus as any[], homePath);
setHomePath(homePath);
message.success('注册成功');
setCurrentStep(2);
const userRole = response.data.user.role;
setTimeout(() => {
if (userRole === 'doctor') router.push('/doctor/workbench');
else if (userRole === 'admin') router.push('/admin/dashboard');
else router.push('/patient/home');
router.push(homePath);
}, 2000);
} catch (error: any) {
message.error(error.message || '注册失败');
......@@ -70,7 +69,7 @@ const RegisterPage: React.FC = () => {
justifyContent: 'center',
overflow: 'auto',
padding: '32px 0',
background: 'linear-gradient(135deg, #042F2E 0%, #0F766E 40%, #0D9488 70%, #0D9488 100%)',
background: 'linear-gradient(135deg, #0A1E4E 0%, #1A56C4 40%, #2B6DE8 70%, #2B6DE8 100%)',
}}>
{/* 装饰背景 */}
<div style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}>
......@@ -93,7 +92,7 @@ const RegisterPage: React.FC = () => {
<div style={{ textAlign: 'center', marginBottom: 20 }}>
<div style={{
width: 48, height: 48, borderRadius: 12,
background: 'linear-gradient(135deg, #0D9488, #0F766E)',
background: 'linear-gradient(135deg, #2B6DE8, #1A56C4)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
margin: '0 auto 12px',
}}>
......@@ -118,7 +117,7 @@ const RegisterPage: React.FC = () => {
style={{
padding: 16, borderRadius: 12, cursor: 'pointer',
transition: 'all 0.2s',
border: `2px solid ${registerType === 'patient' ? '#0D9488' : '#e5e7eb'}`,
border: `2px solid ${registerType === 'patient' ? '#2B6DE8' : '#e5e7eb'}`,
backgroundColor: registerType === 'patient' ? '#f0f5ff' : 'transparent',
}}
onClick={() => setRegisterType('patient')}>
......@@ -129,7 +128,7 @@ const RegisterPage: React.FC = () => {
backgroundColor: '#eff6ff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<UserOutlined style={{ color: '#0D9488' }} />
<UserOutlined style={{ color: '#2B6DE8' }} />
</div>
<div>
<Text strong style={{ fontSize: 14 }}>我是患者</Text><br />
......@@ -142,7 +141,7 @@ const RegisterPage: React.FC = () => {
style={{
padding: 16, borderRadius: 12, cursor: 'pointer',
transition: 'all 0.2s',
border: `2px solid ${registerType === 'doctor' ? '#0D9488' : '#e5e7eb'}`,
border: `2px solid ${registerType === 'doctor' ? '#2B6DE8' : '#e5e7eb'}`,
backgroundColor: registerType === 'doctor' ? '#f0f5ff' : 'transparent',
}}
onClick={() => setRegisterType('doctor')}>
......@@ -153,7 +152,7 @@ const RegisterPage: React.FC = () => {
backgroundColor: '#eff6ff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<MedicineBoxOutlined style={{ color: '#0D9488' }} />
<MedicineBoxOutlined style={{ color: '#2B6DE8' }} />
</div>
<div>
<Text strong style={{ fontSize: 14 }}>我是医生</Text><br />
......@@ -168,8 +167,8 @@ const RegisterPage: React.FC = () => {
style={{
marginTop: 20, height: 40, borderRadius: 8,
fontWeight: 600, border: 'none',
background: 'linear-gradient(90deg, #0D9488, #0F766E)',
boxShadow: '0 4px 12px rgba(13,148,136,0.3)',
background: 'linear-gradient(90deg, #2B6DE8, #1A56C4)',
boxShadow: '0 4px 12px rgba(43,109,232,0.3)',
}}
onClick={() => setCurrentStep(1)}>下一步</Button>
</div>
......@@ -179,7 +178,7 @@ const RegisterPage: React.FC = () => {
<Form form={form} onFinish={handleRegister} size="middle">
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Space.Compact style={{ width: '100%' }}>
<Input prefix={<MobileOutlined style={{ color: 'rgba(13,148,136,0.6)' }} />} placeholder="手机号" style={{ flex: 1 }} />
<Input prefix={<MobileOutlined style={{ color: 'rgba(43,109,232,0.6)' }} />} placeholder="手机号" style={{ flex: 1 }} />
<Button
disabled={countdown > 0}
onClick={handleSendCode}
......@@ -190,13 +189,13 @@ const RegisterPage: React.FC = () => {
</Space.Compact>
</Form.Item>
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
<Input prefix={<SafetyOutlined style={{ color: 'rgba(13,148,136,0.6)' }} />} placeholder="请输入6位验证码" maxLength={6} />
<Input prefix={<SafetyOutlined style={{ color: 'rgba(43,109,232,0.6)' }} />} placeholder="请输入6位验证码" maxLength={6} />
</Form.Item>
<Form.Item name="real_name" rules={[{ required: true, message: '请输入真实姓名' }]}>
<Input prefix={<UserOutlined style={{ color: 'rgba(13,148,136,0.6)' }} />} placeholder="真实姓名" />
<Input prefix={<UserOutlined style={{ color: 'rgba(43,109,232,0.6)' }} />} placeholder="真实姓名" />
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password prefix={<LockOutlined style={{ color: 'rgba(13,148,136,0.6)' }} />} placeholder="设置密码" />
<Input.Password prefix={<LockOutlined style={{ color: 'rgba(43,109,232,0.6)' }} />} placeholder="设置密码" />
</Form.Item>
<Form.Item name="confirm_password" dependencies={['password']}
rules={[{ required: true, message: '请确认密码' }, ({ getFieldValue }) => ({
......@@ -205,7 +204,7 @@ const RegisterPage: React.FC = () => {
return Promise.reject(new Error('两次输入的密码不一致'));
},
})]}>
<Input.Password prefix={<LockOutlined style={{ color: 'rgba(13,148,136,0.6)' }} />} placeholder="确认密码" />
<Input.Password prefix={<LockOutlined style={{ color: 'rgba(43,109,232,0.6)' }} />} placeholder="确认密码" />
</Form.Item>
<Row gutter={12}>
<Col span={12}>
......@@ -227,8 +226,8 @@ const RegisterPage: React.FC = () => {
<Button type="primary" htmlType="submit" loading={loading} block
style={{
height: 40, borderRadius: 8, fontWeight: 600, border: 'none',
background: 'linear-gradient(90deg, #0D9488, #0F766E)',
boxShadow: '0 4px 12px rgba(13,148,136,0.3)',
background: 'linear-gradient(90deg, #2B6DE8, #1A56C4)',
boxShadow: '0 4px 12px rgba(43,109,232,0.3)',
}}>注册</Button>
<Button block style={{ borderRadius: 8 }} onClick={() => setCurrentStep(0)}>上一步</Button>
</Space>
......@@ -254,7 +253,7 @@ const RegisterPage: React.FC = () => {
{currentStep !== 2 && (
<div style={{ textAlign: 'center', marginTop: 16, fontSize: 14 }}>
<span style={{ color: '#8c8c8c' }}>已有账号?</span>
<Link href="/login" style={{ marginLeft: 4, fontWeight: 500, color: '#0D9488' }}>立即登录</Link>
<Link href="/login" style={{ marginLeft: 4, fontWeight: 500, color: '#2B6DE8' }}>立即登录</Link>
</div>
)}
</div>
......
......@@ -168,7 +168,7 @@ const AdminAdminsPage: React.FC = () => {
key: 'admin',
render: (_, record) => (
<Space>
<Avatar icon={<UserOutlined />} src={record.avatar} style={{ backgroundColor: '#0D9488' }} />
<Avatar icon={<UserOutlined />} src={record.avatar} style={{ backgroundColor: '#2B6DE8' }} />
<div>
<Text strong>{record.real_name || '未填写'}</Text>
<br />
......@@ -261,16 +261,8 @@ const AdminAdminsPage: React.FC = () => {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>管理员管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理后台管理员账号与权限</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {total} 条记录</span>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/用户名" prefix={<SearchOutlined />} value={keyword}
......@@ -282,7 +274,7 @@ const AdminAdminsPage: React.FC = () => {
</div>
</Card>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable
search={false}
columns={columns}
......
......@@ -60,7 +60,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
const columns = [
{
title: '工具名称', dataIndex: 'name', key: 'name',
render: (v: string) => <Space><CodeOutlined style={{ color: '#0D9488' }} /><Text code style={{ fontSize: 13 }}>{v}</Text></Space>,
render: (v: string) => <Space><CodeOutlined style={{ color: '#2B6DE8' }} /><Text code style={{ fontSize: 13 }}>{v}</Text></Space>,
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, width: 280 },
{
......@@ -100,13 +100,13 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
return (
<>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable search={false} dataSource={filtered} columns={columns} rowKey="name" loading={isLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: t => `共 ${t} 个工具` }} />
</Card>
<Modal
title={<Space><CodeOutlined style={{ color: '#0D9488' }} /><span>工具详情</span></Space>}
title={<Space><CodeOutlined style={{ color: '#2B6DE8' }} /><span>工具详情</span></Space>}
open={detailModal.open}
onCancel={() => setDetailModal({ open: false, tool: null })}
footer={detailModal.tool && (
......@@ -124,7 +124,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
{detailModal.tool && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="工具名称"><Text code style={{ color: '#0D9488' }}>{detailModal.tool.name}</Text></Descriptions.Item>
<Descriptions.Item label="工具名称"><Text code style={{ color: '#2B6DE8' }}>{detailModal.tool.name}</Text></Descriptions.Item>
<Descriptions.Item label="描述">{detailModal.tool.description}</Descriptions.Item>
<Descriptions.Item label="分类"><Tag color={(CATEGORY_CONFIG[detailModal.tool.category] || CATEGORY_CONFIG.other).color}>{(CATEGORY_CONFIG[detailModal.tool.category] || CATEGORY_CONFIG.other).label}</Tag></Descriptions.Item>
<Descriptions.Item label="状态"><Badge status={detailModal.tool.is_enabled ? 'success' : 'default'} text={detailModal.tool.is_enabled ? '已启用' : '已禁用'} /></Descriptions.Item>
......
......@@ -163,13 +163,13 @@ export default function HTTPToolsTab({ search }: Props) {
</Space>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable search={false} dataSource={tools} columns={columns} rowKey="id" loading={isLoading} size="small"
pagination={{ pageSize: 10, showTotal: t => `共 ${t} 个工具` }} />
</Card>
{/* Create/Edit Modal */}
<Modal title={<Space><ApiOutlined style={{ color: '#0D9488' }} />{editingId ? '编辑 HTTP 工具' : '新建 HTTP 工具'}</Space>}
<Modal title={<Space><ApiOutlined style={{ color: '#2B6DE8' }} />{editingId ? '编辑 HTTP 工具' : '新建 HTTP 工具'}</Space>}
open={modalOpen} onCancel={() => { setModalOpen(false); form.resetFields(); }}
onOk={handleSave} confirmLoading={saveMut.isPending} width={680} destroyOnHidden>
<Form form={form} layout="vertical" style={{ marginTop: 12 }}>
......
......@@ -121,7 +121,7 @@ export default function SkillsTab({ search }: Props) {
<Col key={skill.skill_id} xs={24} sm={12} md={8} lg={6}>
<Card
size="small"
style={{ borderRadius: 12, border: '1px solid #E0F2F1', transition: 'all 0.2s' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF', transition: 'all 0.2s' }}
styles={{ body: { padding: 16 } }}
hoverable
>
......
......@@ -240,7 +240,7 @@ function AgentsTab() {
title: '智能体', key: 'name',
render: (_: unknown, r: AgentDefinition) => (
<Space>
<RobotOutlined style={{ color: '#0D9488' }} />
<RobotOutlined style={{ color: '#2B6DE8' }} />
<div>
<Text strong>{r.name}</Text>
<br />
......@@ -380,7 +380,7 @@ function AgentsTab() {
<div key={i} style={{ marginBottom: 12, textAlign: m.role === 'user' ? 'right' : 'left' }}>
<div style={{
display: 'inline-block', padding: '8px 14px', borderRadius: 8, maxWidth: '85%',
background: m.role === 'user' ? '#0D9488' : '#f5f5f5',
background: m.role === 'user' ? '#2B6DE8' : '#f5f5f5',
color: m.role === 'user' ? '#fff' : '#333',
textAlign: 'left',
}}>
......@@ -493,16 +493,8 @@ export default function AgentManagementPage() {
const [activeTab, setActiveTab] = useState('agents');
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<RobotOutlined style={{ marginRight: 8, color: '#0D9488' }} />
智能体管理
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理智能体、工具与技能,支持在线编辑、热重载与对话测试</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
......
......@@ -23,7 +23,7 @@ export const CATEGORY_CONFIG: Record<string, { color: string; label: string; ico
export type CATEGORY_CONFIG_TYPE = typeof CATEGORY_CONFIG;
export const SOURCE_CONFIG = {
builtin: { label: '内置', color: '#0D9488' },
builtin: { label: '内置', color: '#2B6DE8' },
http: { label: 'HTTP', color: '#fa8c16' },
} as const;
......
......@@ -6,7 +6,7 @@ import {
Statistic, Tabs, Input, Select, Timeline, Spin, Empty,
} from 'antd';
import {
FundOutlined, SearchOutlined, ToolOutlined, RobotOutlined,
SearchOutlined, ToolOutlined, RobotOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
......@@ -176,49 +176,41 @@ export default function AICenterPage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<FundOutlined style={{ marginRight: 8, color: '#0D9488' }} />
AI 运营中心
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>全链路 AI 调用追踪、计量统计与性能监控</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={[16, 16]}>
<Col span={4}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="总调用次数" value={stats?.total_calls ?? 0} prefix={<ThunderboltOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="成功率" value={stats?.total_calls ? Math.round((stats.success_calls / stats.total_calls) * 100) : 0} suffix="%" valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={4}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="总 Tokens" value={stats?.total_tokens ?? 0} />
</Card>
</Col>
<Col span={4}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="Agent 执行" value={stats?.agent_execs ?? 0} prefix={<RobotOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="工具调用" value={stats?.tool_calls ?? 0} prefix={<ToolOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={isLoading} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="模拟调用" value={stats?.mock_calls ?? 0} valueStyle={{ color: '#fa8c16' }} />
</Card>
</Col>
</Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
......
......@@ -294,12 +294,7 @@ const AdminAIConfigPage: React.FC = () => {
};
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>AI 配置</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>配置平台 AI 模型与 Prompt 模板</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Tabs defaultActiveKey="model" items={[
{
key: 'model',
......@@ -308,7 +303,7 @@ const AdminAIConfigPage: React.FC = () => {
<Spin spinning={loading}>
<Row gutter={24}>
<Col span={16}>
<Card title="AI 模型配置" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card title="AI 模型配置" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Alert message="此配置将全局生效,影响所有 AI 功能" type="info" showIcon style={{ marginBottom: 20 }} />
<Form form={modelForm} layout="vertical">
<Row gutter={16}>
......@@ -360,7 +355,7 @@ const AdminAIConfigPage: React.FC = () => {
</Card>
</Col>
<Col span={8}>
<Card title="使用统计" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card title="使用统计" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<div>
<Text type="secondary">今日调用次数</Text>
......@@ -387,7 +382,7 @@ const AdminAIConfigPage: React.FC = () => {
key: 'prompts',
label: <span><FileTextOutlined /> Prompt 模板管理</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Text type="secondary">管理各场景的 AI Prompt 模板</Text>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddTemplate}>新建模板</Button>
......@@ -400,7 +395,7 @@ const AdminAIConfigPage: React.FC = () => {
key: 'safety',
label: <span><SafetyOutlined /> 安全词过滤</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Alert message="安全词过滤规则用于防止 AI 输出不安全或不合规的内容" type="info" showIcon style={{ marginBottom: 16 }} />
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}>
<ExclamationCircleOutlined style={{ fontSize: 48, marginBottom: 16 }} />
......
......@@ -7,7 +7,7 @@ import {
Badge, Collapse,
} from 'antd';
import {
FileTextOutlined, SearchOutlined, ToolOutlined, RobotOutlined,
SearchOutlined, ToolOutlined, RobotOutlined,
ThunderboltOutlined, CheckCircleOutlined, CloseCircleOutlined,
HistoryOutlined, FundOutlined,
} from '@ant-design/icons';
......@@ -246,50 +246,42 @@ export default function AILogsPage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<FileTextOutlined style={{ marginRight: 8, color: '#0D9488' }} />
AI 日志
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>智能体执行日志与 AI 调用追踪,默认查询当天数据</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 统计卡片 */}
<Row gutter={[16, 16]}>
<Col span={4}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="总调用" value={aiStats?.total_calls ?? 0} prefix={<ThunderboltOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="成功率" value={aiStats?.total_calls ? Math.round((aiStats.success_calls / aiStats.total_calls) * 100) : 0} suffix="%" valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={4}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="总 Tokens" value={aiStats?.total_tokens ?? 0} />
</Card>
</Col>
<Col span={4}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="Agent 执行" value={aiStats?.agent_execs ?? 0} prefix={<RobotOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="工具调用" value={aiStats?.tool_calls ?? 0} prefix={<ToolOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card loading={aiLoading} size="small" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="模拟调用" value={aiStats?.mock_calls ?? 0} valueStyle={{ color: '#fa8c16' }} />
</Card>
</Col>
</Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
......
......@@ -210,30 +210,25 @@ const AdminCompliancePage: React.FC = () => {
const generatedCount = reports.filter((r) => r.status === 'generated').length;
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>合规报表</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>审计日志、数据导出与监管上报</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #0D9488' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #2B6DE8' }}>
<Statistic title="审计日志" value={logsTotal} prefix={<FileSearchOutlined />} suffix="条" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="待处理" value={pendingCount} valueStyle={{ color: '#fa8c16' }} suffix="项" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #52c41a' }}>
<Statistic title="已上报" value={submittedCount} suffix="项" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #0891B2' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #0891B2' }}>
<Statistic title="已生成" value={generatedCount} suffix="项" />
</Card>
</Col>
......@@ -244,7 +239,7 @@ const AdminCompliancePage: React.FC = () => {
key: 'logs',
label: <span><AuditOutlined /> 审计日志</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input placeholder="搜索操作内容" style={{ width: 180, borderRadius: 8 }} allowClear
value={searchKeyword} onChange={e => setSearchKeyword(e.target.value)}
......@@ -262,16 +257,16 @@ const AdminCompliancePage: React.FC = () => {
key: 'export',
label: <span><DownloadOutlined /> 数据导出</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Row gutter={[16, 16]}>
{([
{ type: 'consultations', title: '问诊数据', desc: '问诊记录、诊断、处方', icon: <FileSearchOutlined style={{ fontSize: 24, color: '#0D9488' }} /> },
{ type: 'consultations', title: '问诊数据', desc: '问诊记录、诊断、处方', icon: <FileSearchOutlined style={{ fontSize: 24, color: '#2B6DE8' }} /> },
{ type: 'users', title: '用户数据', desc: '患者和医生基本信息', icon: <UserOutlined style={{ fontSize: 24, color: '#52c41a' }} /> },
{ type: 'prescriptions', title: '处方数据', desc: '处方记录、用药统计', icon: <AuditOutlined style={{ fontSize: 24, color: '#0891B2' }} /> },
{ type: 'logs', title: '系统日志', desc: '操作日志和审计记录', icon: <SettingOutlined style={{ fontSize: 24, color: '#fa8c16' }} /> },
] as const).map((item) => (
<Col span={6} key={item.type}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', textAlign: 'center', cursor: 'pointer' }} hoverable
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', textAlign: 'center', cursor: 'pointer' }} hoverable
onClick={() => { setExportType(item.type); setExportModalVisible(true); }}>
<div style={{ marginBottom: 10 }}>{item.icon}</div>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{item.title}</div>
......@@ -287,7 +282,7 @@ const AdminCompliancePage: React.FC = () => {
key: 'reports',
label: <span><CloudUploadOutlined /> 监管上报</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>管理合规报告,支持下载和上报</span>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setGenerateModalVisible(true)}>生成新报告</Button>
......
......@@ -111,7 +111,7 @@ const AdminConsultationsPage: React.FC = () => {
];
return (
<div style={{ padding: '20px 24px' }}>
<div style={{ padding: '12px 16px' }}>
<ProTable<AdminConsultItem>
headerTitle="问诊管理"
tooltip="查看和管理平台所有问诊记录"
......
......@@ -26,6 +26,22 @@ interface TrendItem {
revenue: number;
}
const sceneLabels: Record<string, string> = {
patient_agent: '患者智能助手',
doctor_agent: '医生智能助手',
admin_agent: '管理员智能助手',
patient_universal_agent: '患者智能助手',
doctor_universal_agent: '医生智能助手',
admin_universal_agent: '管理员智能助手',
agent_actions: '操作按钮格式',
pre_consult_chat: '预问诊对话',
pre_consult_analysis: '预问诊报告',
consult_diagnosis: '鉴别诊断',
consult_medication: '用药建议',
prescription_review: '处方审核',
lab_report_interpret: '检验报告解读',
};
const AdminDashboardPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState<DashboardStats>({
......@@ -104,41 +120,36 @@ const AdminDashboardPage: React.FC = () => {
};
// AI场景调用 TOP5 (水平柱状图)
const sceneCounts = (aiStats as Record<string, unknown>).scene_counts as Record<string, number> | undefined;
const sceneEntries = sceneCounts ? Object.entries(sceneCounts).sort((a, b) => b[1] - a[1]).slice(0, 5) : [];
// 后端返回 [{scene: "xxx", count: 5}, ...] 数组
const sceneCountsRaw = (aiStats as Record<string, unknown>).scene_counts as { scene: string; count: number }[] | undefined;
const sceneTop5 = (sceneCountsRaw || []).slice(0, 5);
const aiSceneOption = {
tooltip: { trigger: 'axis' as const },
xAxis: { type: 'value' as const, minInterval: 1 },
yAxis: { type: 'category' as const, data: sceneEntries.map(e => e[0]).reverse(), axisLabel: { fontSize: 11, width: 60, overflow: 'truncate' as const } },
yAxis: { type: 'category' as const, data: sceneTop5.map(e => sceneLabels[e.scene] || e.scene).reverse(), axisLabel: { fontSize: 11, width: 80, overflow: 'truncate' as const } },
grid: { left: 80, right: 20, top: 10, bottom: 20 },
series: [{ type: 'bar', data: sceneEntries.map(e => e[1]).reverse(), barWidth: 16, itemStyle: { borderRadius: [0, 4, 4, 0], color: CHART_COLORS[3] } }],
series: [{ type: 'bar', data: sceneTop5.map(e => e.count).reverse(), barWidth: 16, itemStyle: { borderRadius: [0, 4, 4, 0], color: CHART_COLORS[3] } }],
};
// AI 24h 分布柱状图
const hourlyData = (aiStats as Record<string, unknown>).hourly_distribution as number[] | undefined;
// 后端返回 [{hour: "09", count: 3}, ...] 数组
const hourlyTrend = (aiStats as Record<string, unknown>).hourly_trend as { hour: string; count: number }[] | undefined;
const hourlyDataMap = new Map((hourlyTrend || []).map(h => [parseInt(h.hour, 10), h.count]));
const hourlyData = Array.from({ length: 24 }, (_, i) => hourlyDataMap.get(i) || 0);
const ai24hOption = {
tooltip: { trigger: 'axis' as const },
xAxis: { type: 'category' as const, data: Array.from({ length: 24 }, (_, i) => `${i}:00`), axisLabel: { fontSize: 10, interval: 3 } },
yAxis: { type: 'value' as const, minInterval: 1 },
grid: { left: 40, right: 12, top: 10, bottom: 24 },
series: [{ type: 'bar', data: hourlyData || new Array(24).fill(0), barWidth: 10, itemStyle: { borderRadius: [3, 3, 0, 0], color: CHART_COLORS[4] } }],
series: [{ type: 'bar', data: hourlyData, barWidth: 10, itemStyle: { borderRadius: [3, 3, 0, 0], color: CHART_COLORS[4] } }],
};
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>运营大盘</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>平台实时运营数据概览</div>
</div>
<span style={{ fontSize: 12, color: '#bfbfbf' }}>数据实时更新</span>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Row1: 核心指标 */}
<Row gutter={[16, 16]}>
{[
{ title: '注册用户', value: stats.total_users, prefix: '', icon: <TeamOutlined style={{ fontSize: 18, color: '#0D9488' }} />, color: '#0D9488', bg: '#F0FDFA', extra: '' },
{ title: '注册用户', value: stats.total_users, prefix: '', icon: <TeamOutlined style={{ fontSize: 18, color: '#2B6DE8' }} />, color: '#2B6DE8', bg: '#F0F6FF', extra: '' },
{ title: '注册医生', value: stats.total_doctors, prefix: '', icon: <MedicineBoxOutlined style={{ fontSize: 18, color: '#52c41a' }} />, color: '#52c41a', bg: '#f6ffed', extra: '', suffix: <span style={{ fontSize: 12, color: '#bfbfbf', fontWeight: 400 }}>/ 在线 {stats.online_doctors}</span> },
{ title: '今日问诊', value: stats.today_consultations, prefix: '', icon: <MessageOutlined style={{ fontSize: 18, color: '#0891B2' }} />, color: '#0891B2', bg: '#ECFEFF', extra: '' },
{ title: '今日收入', value: (stats.revenue_today / 100), prefix: '¥', icon: <DollarOutlined style={{ fontSize: 18, color: '#fa8c16' }} />, color: '#fa8c16', bg: '#fff7e6', extra: '' },
......@@ -158,7 +169,7 @@ const AdminDashboardPage: React.FC = () => {
),
valueStyle: { fontSize: 24, fontWeight: 700, color: item.color },
}}
style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}
/>
</Col>
))}
......@@ -168,18 +179,18 @@ const AdminDashboardPage: React.FC = () => {
<EChartsCard title="问诊趋势(近7天)" option={trendOption} loading={loading} height={260} />
{/* Row3: 三栏图表 */}
<Row gutter={[16, 16]}>
<Col xs={24} md={8}>
<EChartsCard title="问诊状态分布" option={doughnutOption} loading={loading} height={220} />
<Row gutter={[16, 16]} align="stretch">
<Col xs={24} md={8} style={{ display: 'flex' }}>
<EChartsCard title="问诊状态分布" option={doughnutOption} loading={loading} height={220} style={{ flex: 1 }} />
</Col>
<Col xs={24} md={8}>
<EChartsCard title="AI 场景调用 TOP5" option={aiSceneOption} loading={loading} height={220} />
<Col xs={24} md={8} style={{ display: 'flex' }}>
<EChartsCard title="AI 场景调用 TOP5" option={aiSceneOption} loading={loading} height={220} style={{ flex: 1 }} />
</Col>
<Col xs={24} md={8}>
<Col xs={24} md={8} style={{ display: 'flex' }}>
<ProCard
title={<span style={{ fontSize: 14, fontWeight: 600 }}>平台概况</span>}
headerBordered
style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF', flex: 1 }}
bodyStyle={{ padding: '12px 16px' }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
......@@ -195,7 +206,7 @@ const AdminDashboardPage: React.FC = () => {
<Text style={{ fontSize: 12 }}>累计问诊</Text>
<Text strong style={{ fontSize: 12 }}>{stats.total_consultations.toLocaleString()}</Text>
</div>
<Progress percent={Math.min(100, stats.total_consultations)} strokeColor="#0D9488" showInfo={false} size="small" />
<Progress percent={Math.min(100, stats.total_consultations)} strokeColor="#2B6DE8" showInfo={false} size="small" />
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
......@@ -233,7 +244,7 @@ const AdminDashboardPage: React.FC = () => {
<ProCard
title={<span style={{ fontSize: 14, fontWeight: 600 }}>待审核医生 <Tag color="orange" style={{ marginLeft: 6 }}>{stats.pending_doctor_reviews}</Tag></span>}
headerBordered
style={{ borderRadius: 12, border: '1px solid #E0F2F1', height: '100%' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF', height: '100%' }}
>
<List
dataSource={recentReviews}
......@@ -261,11 +272,11 @@ const AdminDashboardPage: React.FC = () => {
{[
{ icon: <CheckCircleOutlined />, color: '#52c41a', bg: '#f6ffed', label: '累计问诊', value: stats.total_consultations.toLocaleString() },
{ icon: <RiseOutlined />, color: '#fa8c16', bg: '#fff7e6', label: '本月收入', value: `¥${(stats.revenue_month / 100).toLocaleString()}` },
{ icon: <ClockCircleOutlined />, color: '#0D9488', bg: '#F0FDFA', label: '平均响应', value: '3.2分钟' },
{ icon: <ClockCircleOutlined />, color: '#2B6DE8', bg: '#F0F6FF', label: '平均响应', value: '3.2分钟' },
{ icon: <AuditOutlined />, color: '#0891B2', bg: '#ECFEFF', label: '待审核', value: stats.pending_doctor_reviews },
].map((item, i) => (
<Col xs={12} sm={6} key={i}>
<div style={{ background: '#fff', borderRadius: 12, padding: '16px 20px', border: '1px solid #E0F2F1', textAlign: 'center' }}>
<div style={{ background: '#fff', borderRadius: 12, padding: '16px 20px', border: '1px solid #E5E8EF', textAlign: 'center' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: item.bg, color: item.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, margin: '0 auto 8px' }}>{item.icon}</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginBottom: 4 }}>{item.label}</div>
<div style={{ fontSize: 20, fontWeight: 700, color: item.color }}>{item.value}</div>
......
......@@ -113,12 +113,7 @@ const AdminDepartmentsPage: React.FC = () => {
);
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>科室管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理医院科室分类与排序</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<ProTable<Department>
headerTitle="科室列表"
rowKey="id"
......
......@@ -334,11 +334,6 @@ const AdminDoctorsPage: React.FC = () => {
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台注册医生,审核资质认证</div>
</div>
<ProTable<DoctorItem>
headerTitle="医生列表"
tooltip="管理平台注册医生,审核资质认证"
......
......@@ -84,7 +84,7 @@ export default function KnowledgePage() {
const colColumns = [
{
title: '集合名称', dataIndex: 'name', key: 'name',
render: (v: string) => <Space><BookOutlined style={{ color: '#0D9488' }} /><Text strong>{v}</Text></Space>,
render: (v: string) => <Space><BookOutlined style={{ color: '#2B6DE8' }} /><Text strong>{v}</Text></Space>,
},
{
title: '类别', dataIndex: 'category', key: 'category', width: 120,
......@@ -135,14 +135,9 @@ export default function KnowledgePage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>知识库管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理医学知识库集合和文档,支持语义向量检索</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 操作栏 */}
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Button icon={<ReloadOutlined />} onClick={() => { fetchCollections(); fetchDocuments(); }}>刷新</Button>
<Button icon={<SearchOutlined />} onClick={() => { setSearchResults([]); setSearchModal(true); }}>检索测试</Button>
......@@ -152,7 +147,7 @@ export default function KnowledgePage() {
</Card>
{/* 内容区 */}
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs
items={[
{
......
This diff is collapsed.
......@@ -3,13 +3,11 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import {
Card, Button, Modal, Tree, Tag, Space, Spin, App,
Dropdown,
} from 'antd';
import {
AppstoreOutlined, ReloadOutlined,
SaveOutlined, CheckSquareOutlined, MinusSquareOutlined,
PlusOutlined, EditOutlined, DeleteOutlined,
EyeOutlined, EyeInvisibleOutlined, MoreOutlined,
EyeOutlined, EyeInvisibleOutlined,
} from '@ant-design/icons';
import {
DrawerForm, ProFormText, ProFormDigit, ProFormSelect, ProFormSwitch,
......@@ -17,6 +15,7 @@ import {
} from '@ant-design/pro-components';
import { menuApi, roleApi } from '@/api/rbac';
import type { Menu, Role } from '@/api/rbac';
import { AVAILABLE_ICONS, getIconComponent } from '@/utils/iconMapper';
type TreeDataNode = {
key: number;
......@@ -42,31 +41,17 @@ function menusToTreeData(
key: m.id,
title: (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', paddingRight: 8 }}>
<span style={{ flex: 1 }}>
<span style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8 }}>
{m.icon && <span style={{ fontSize: 14 }}>{getIconComponent(m.icon)}</span>}
{m.name}
{m.path && <span style={{ color: '#999', fontSize: 12, marginLeft: 8 }}>{m.path}</span>}
{m.type && <Tag color={m.type === 'directory' ? 'blue' : m.type === 'button' ? 'orange' : 'green'} style={{ marginLeft: 6, fontSize: 11 }}>{m.type === 'directory' ? '目录' : m.type === 'button' ? '按钮' : '菜单'}</Tag>}
{!m.visible && <Tag color="default" style={{ marginLeft: 6, fontSize: 11 }}><EyeInvisibleOutlined /> 隐藏</Tag>}
{m.path && <span style={{ color: '#999', fontSize: 12 }}>{m.path}</span>}
{m.type && <Tag color={m.type === 'directory' ? 'blue' : m.type === 'button' ? 'orange' : 'green'} style={{ fontSize: 11 }}>{m.type === 'directory' ? '目录' : m.type === 'button' ? '按钮' : '菜单'}</Tag>}
{!m.visible && <Tag color="default" style={{ fontSize: 11 }}><EyeInvisibleOutlined /> 隐藏</Tag>}
</span>
<Space size={4} style={{ flexShrink: 0 }} onClick={(e) => e.stopPropagation()}>
<Dropdown
menu={{
items: [
{ key: 'addChild', icon: <PlusOutlined />, label: '添加子菜单' },
{ key: 'edit', icon: <EditOutlined />, label: '编辑' },
{ type: 'divider' },
{ key: 'delete', icon: <DeleteOutlined />, label: '删除', danger: true },
],
onClick: ({ key }) => {
if (key === 'edit') onEdit(m);
else if (key === 'delete') onDelete(m);
else if (key === 'addChild') onAddChild(m);
},
}}
trigger={['click']}
>
<Button type="text" size="small" icon={<MoreOutlined />} style={{ opacity: 0.6 }} />
</Dropdown>
<Button type="text" size="small" icon={<PlusOutlined />} onClick={() => onAddChild(m)} style={{ color: '#2B6DE8' }}>子菜单</Button>
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => onEdit(m)}>编辑</Button>
<Button type="text" size="small" icon={<DeleteOutlined />} onClick={() => onDelete(m)} danger>删除</Button>
</Space>
</div>
),
......@@ -74,13 +59,38 @@ function menusToTreeData(
}));
}
// 将菜单树转为 TreeSelect 的 treeData
function menusToSelectData(menus: Menu[]): any[] {
return menus.map((m) => ({
value: m.id,
title: m.name,
children: m.children?.length ? menusToSelectData(m.children) : undefined,
}));
// 收集某个菜单及其所有后代的 ID
function collectDescendantIds(menus: Menu[], id: number): Set<number> {
const ids = new Set<number>();
const addAllChildren = (children: Menu[]) => {
for (const c of children) {
ids.add(c.id);
if (c.children?.length) addAllChildren(c.children);
}
};
const find = (items: Menu[]) => {
for (const m of items) {
if (m.id === id) {
ids.add(m.id);
if (m.children?.length) addAllChildren(m.children);
return;
}
if (m.children?.length) find(m.children);
}
};
find(menus);
return ids;
}
// 将菜单树转为 TreeSelect 的 treeData,可排除指定 ID 集合
function menusToSelectData(menus: Menu[], excludeIds?: Set<number>): any[] {
return menus
.filter(m => !excludeIds || !excludeIds.has(m.id))
.map((m) => ({
value: m.id,
title: m.name,
children: m.children?.length ? menusToSelectData(m.children, excludeIds) : undefined,
}));
}
// 收集树中所有 key
......@@ -124,7 +134,13 @@ export default function MenusPage() {
const [defaultParentId, setDefaultParentId] = useState<number | undefined>(undefined);
const allMenuKeys = useMemo(() => collectAllKeys(menus), [menus]);
const parentTreeData = useMemo(() => menusToSelectData(menus), [menus]);
const topOption = { value: 0, title: '顶级菜单(一级)' };
const parentTreeData = useMemo(() => [topOption, ...menusToSelectData(menus)], [menus]);
const editParentTreeData = useMemo(() => {
if (!editingMenu) return parentTreeData;
const excludeIds = collectDescendantIds(menus, editingMenu.id);
return [topOption, ...menusToSelectData(menus, excludeIds)];
}, [menus, editingMenu, parentTreeData]);
const handleEdit = useCallback((m: Menu) => {
setEditingMenu(m);
......@@ -233,8 +249,8 @@ export default function MenusPage() {
const selectedRole = roles.find((r) => r.id === selectedRoleId);
// 表单字段
const menuFormFields = (
// 表单字段(接受 treeData 参数区分新增/编辑场景)
const renderFormFields = (treeData: any[]) => (
<>
<ProFormText
name="name"
......@@ -255,7 +271,7 @@ export default function MenusPage() {
placeholder="留空表示顶级菜单"
allowClear
fieldProps={{
treeData: parentTreeData,
treeData,
treeDefaultExpandAll: true,
}}
/>
......@@ -264,10 +280,23 @@ export default function MenusPage() {
label="路由路径"
placeholder="如 /admin/menus"
/>
<ProFormText
<ProFormSelect
name="icon"
label="图标"
placeholder="Ant Design 图标名称"
placeholder="请选择图标"
showSearch
options={AVAILABLE_ICONS.map(icon => ({
label: (
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{getIconComponent(icon)}
<span>{icon}</span>
</span>
),
value: icon,
}))}
fieldProps={{
optionLabelProp: 'value',
}}
/>
<ProFormText
name="component"
......@@ -295,32 +324,10 @@ export default function MenusPage() {
);
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<AppstoreOutlined style={{ marginRight: 8, color: '#1890ff' }} />菜单管理
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>
管理系统菜单结构,为角色分配菜单权限
</div>
</div>
<Space>
<Button icon={<ReloadOutlined />} onClick={fetchInitial}>刷新</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => { setDefaultParentId(undefined); setAddVisible(true); }}
>
添加菜单
</Button>
</Space>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Main content */}
<Spin spinning={loading}>
<div style={{ display: 'flex', gap: 16, minHeight: 500 }}>
<div style={{ display: 'flex', gap: 16 }}>
{/* Left: Role list */}
<Card
title="角色列表"
......@@ -358,6 +365,7 @@ export default function MenusPage() {
<Space>
<Button size="small" icon={<CheckSquareOutlined />} onClick={handleSelectAll}>全选</Button>
<Button size="small" icon={<MinusSquareOutlined />} onClick={handleDeselectAll}>全不选</Button>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => { setDefaultParentId(undefined); setAddVisible(true); }}>添加菜单</Button>
</Space>
)
}
......@@ -365,8 +373,9 @@ export default function MenusPage() {
<Spin spinning={menuLoading}>
{treeData.length > 0 && selectedRoleId ? (
<>
<div style={{ maxHeight: 'calc(100vh - 300px)', overflowY: 'auto' }}>
<div style={{ maxHeight: 'calc(100vh - 250px)', overflowY: 'auto' }}>
<Tree
className="menu-manage-tree"
checkable
checkStrictly
defaultExpandAll
......@@ -430,7 +439,7 @@ export default function MenusPage() {
}
}}
>
{menuFormFields}
{renderFormFields(parentTreeData)}
</DrawerForm>
{/* Edit Menu Drawer */}
......@@ -470,7 +479,7 @@ export default function MenusPage() {
}
}}
>
{menuFormFields}
{renderFormFields(editParentTreeData)}
</DrawerForm>
</div>
);
......
......@@ -146,30 +146,25 @@ const AdminPharmacyPage: React.FC = () => {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>药品库管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台药品信息与库存状态</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #0D9488' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #2B6DE8' }}>
<Statistic title="药品总数" value={stats.total_count} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #52c41a' }}>
<Statistic title="库存充足" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="库存预警" value={stats.low_stock_count} valueStyle={{ color: '#fa8c16' }} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #f5222d' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #f5222d' }}>
<Statistic title="缺货药品" value={stats.out_of_stock_count} valueStyle={{ color: '#f5222d' }} suffix="种" />
</Card>
</Col>
......@@ -185,7 +180,7 @@ const AdminPharmacyPage: React.FC = () => {
key: 'drugs',
label: <span><MedicineBoxOutlined /> 药品信息</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 12 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索药品" style={{ width: 200, borderRadius: 8 }} allowClear
......@@ -207,7 +202,7 @@ const AdminPharmacyPage: React.FC = () => {
key: 'stock',
label: <Badge count={stats.low_stock_count + stats.out_of_stock_count} offset={[10, 0]}>库存预警</Badge>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable search={false} columns={drugColumns}
dataSource={drugs.filter((d) => d.status !== 'normal')}
rowKey="id" pagination={false} loading={loading} size="small" />
......
......@@ -126,30 +126,25 @@ const AdminPrescriptionPage: React.FC = () => {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>处方监管</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>监督平台处方合规性,审查异常用药</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #0D9488' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #2B6DE8' }}>
<Statistic title="处方总数" value={stats.total_count} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #52c41a' }}>
<Statistic title="合规" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="预警" value={stats.warning_count} valueStyle={{ color: '#fa8c16' }} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', borderLeft: '4px solid #f5222d' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', borderLeft: '4px solid #f5222d' }}>
<Statistic title="异常" value={stats.rejected_count} valueStyle={{ color: '#f5222d' }} suffix="张" />
</Card>
</Col>
......@@ -159,7 +154,7 @@ const AdminPrescriptionPage: React.FC = () => {
{
key: 'all', label: '全部处方',
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索处方/患者/医生" style={{ width: 200, borderRadius: 8 }} allowClear
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setPage(1); fetchList(); }} />
......@@ -175,7 +170,7 @@ const AdminPrescriptionPage: React.FC = () => {
key: 'warning',
label: <Badge count={stats.warning_count + stats.rejected_count} offset={[10, 0]}>异常预警</Badge>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Alert message="以下处方存在合规风险,请及时审查处理" type="warning" showIcon style={{ marginBottom: 16 }} />
<List dataSource={prescriptions.filter((d) => d.warning_level !== 'normal')} renderItem={(item) => (
<Card style={{ marginBottom: 12, borderRadius: 8, borderLeft: `4px solid ${item.warning_level === 'rejected' ? '#f5222d' : '#fa8c16'}` }} hoverable>
......
......@@ -63,14 +63,8 @@ export default function RolesPage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<SafetyOutlined style={{ marginRight: 8, color: '#722ed1' }} />角色管理
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>系统角色一览(权限以 User.Role 为唯一依据)</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {roles.length} 个角色</span>
</div>
......
......@@ -8,7 +8,7 @@ import {
import { ProTable, DrawerForm, ProFormText, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
import {
PlusOutlined, DeleteOutlined, EditOutlined, ImportOutlined,
SafetyOutlined, FilterOutlined,
FilterOutlined,
} from '@ant-design/icons';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { ColumnsType } from 'antd/es/table';
......@@ -219,24 +219,16 @@ export default function SafetyPage() {
] as ColumnsType<SafetyLog>;
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<SafetyOutlined style={{ marginRight: 8, color: '#0D9488' }} />
内容安全管理
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>配置安全词过滤规则,保障AI输入/输出内容合规</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={[16, 16]}>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}><Statistic title="活跃规则" value={stats?.active_rules ?? 0} valueStyle={{ color: '#52c41a' }} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}><Statistic title="过滤总数" value={stats?.total_logs ?? 0} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}><Statistic title="拦截次数" value={stats?.blocked ?? 0} valueStyle={{ color: '#ff4d4f' }} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}><Statistic title="替换次数" value={stats?.replaced ?? 0} valueStyle={{ color: '#0D9488' }} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}><Statistic title="警告次数" value={stats?.warned ?? 0} valueStyle={{ color: '#fa8c16' }} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}><Statistic title="活跃规则" value={stats?.active_rules ?? 0} valueStyle={{ color: '#52c41a' }} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}><Statistic title="过滤总数" value={stats?.total_logs ?? 0} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}><Statistic title="拦截次数" value={stats?.blocked ?? 0} valueStyle={{ color: '#ff4d4f' }} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}><Statistic title="替换次数" value={stats?.replaced ?? 0} valueStyle={{ color: '#2B6DE8' }} /></Card></Col>
<Col span={4}><Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}><Statistic title="警告次数" value={stats?.warned ?? 0} valueStyle={{ color: '#fa8c16' }} /></Card></Col>
</Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
......
......@@ -6,7 +6,7 @@ import {
message, Row, Col, Statistic, Empty,
} from 'antd';
import {
CheckOutlined, CloseOutlined, EyeOutlined, ClockCircleOutlined,
CheckOutlined, CloseOutlined, EyeOutlined,
FilterOutlined,
} from '@ant-design/icons';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
......@@ -163,39 +163,31 @@ export default function AdminTasksPage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<ClockCircleOutlined style={{ marginRight: 8, color: '#0D9488' }} />
人工审核任务
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>工作流中需要人工处理的审核任务</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="待处理" value={pendingCount} valueStyle={{ color: '#fa8c16' }} />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="已通过" value={approvedCount} valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="已拒绝" value={rejectedCount} valueStyle={{ color: '#ff4d4f' }} />
</Card>
</Col>
<Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="总任务" value={tasks.length} />
</Card>
</Col>
</Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Space style={{ marginBottom: 16 }}>
<FilterOutlined />
<Select
......
......@@ -60,7 +60,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
const columns = [
{
title: '工具名称', dataIndex: 'name', key: 'name',
render: (v: string) => <Space><CodeOutlined style={{ color: '#0D9488' }} /><Text code style={{ fontSize: 13 }}>{v}</Text></Space>,
render: (v: string) => <Space><CodeOutlined style={{ color: '#2B6DE8' }} /><Text code style={{ fontSize: 13 }}>{v}</Text></Space>,
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, width: 280 },
{
......@@ -100,13 +100,13 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
return (
<>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable search={false} dataSource={filtered} columns={columns} rowKey="name" loading={isLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: t => `共 ${t} 个工具` }} />
</Card>
<Drawer
title={<Space><CodeOutlined style={{ color: '#0D9488' }} /><span>工具详情</span></Space>}
title={<Space><CodeOutlined style={{ color: '#2B6DE8' }} /><span>工具详情</span></Space>}
open={detailModal.open}
onClose={() => setDetailModal({ open: false, tool: null })}
placement="right"
......@@ -126,7 +126,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
{detailModal.tool && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="工具名称"><Text code style={{ color: '#0D9488' }}>{detailModal.tool.name}</Text></Descriptions.Item>
<Descriptions.Item label="工具名称"><Text code style={{ color: '#2B6DE8' }}>{detailModal.tool.name}</Text></Descriptions.Item>
<Descriptions.Item label="描述">{detailModal.tool.description}</Descriptions.Item>
<Descriptions.Item label="分类"><Tag color={(CATEGORY_CONFIG[detailModal.tool.category] || CATEGORY_CONFIG.other).color}>{(CATEGORY_CONFIG[detailModal.tool.category] || CATEGORY_CONFIG.other).label}</Tag></Descriptions.Item>
<Descriptions.Item label="状态"><Badge status={detailModal.tool.is_enabled ? 'success' : 'default'} text={detailModal.tool.is_enabled ? '已启用' : '已禁用'} /></Descriptions.Item>
......
......@@ -163,14 +163,14 @@ export default function HTTPToolsTab({ search }: Props) {
</Space>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable search={false} dataSource={tools} columns={columns} rowKey="id" loading={isLoading} size="small"
pagination={{ pageSize: 10, showTotal: t => `共 ${t} 个工具` }} />
</Card>
{/* Create/Edit DrawerForm */}
<DrawerForm
title={<Space><ApiOutlined style={{ color: '#0D9488' }} />{editingId ? '编辑 HTTP 工具' : '新建 HTTP 工具'}</Space>}
title={<Space><ApiOutlined style={{ color: '#2B6DE8' }} />{editingId ? '编辑 HTTP 工具' : '新建 HTTP 工具'}</Space>}
open={modalOpen}
onOpenChange={(open) => { setModalOpen(open); if (!open) form.resetFields(); }}
onFinish={async () => { handleSave(); return false; }}
......
......@@ -122,7 +122,7 @@ export default function SkillsTab({ search }: Props) {
<Col key={skill.skill_id} xs={24} sm={12} md={8} lg={6}>
<Card
size="small"
style={{ borderRadius: 12, border: '1px solid #E0F2F1', transition: 'all 0.2s' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF', transition: 'all 0.2s' }}
styles={{ body: { padding: 16 } }}
hoverable
>
......
......@@ -35,7 +35,7 @@ const CATEGORY_CONFIG: Record<string, { color: string; label: string; icon: Reac
export type CATEGORY_CONFIG_TYPE = typeof CATEGORY_CONFIG;
export const SOURCE_CONFIG = {
builtin: { label: '内置', color: '#0D9488' },
builtin: { label: '内置', color: '#2B6DE8' },
http: { label: 'HTTP', color: '#fa8c16' },
} as const;
......@@ -93,19 +93,10 @@ export default function UnifiedToolsPage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Header */}
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>工具中心</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>
管理所有 Agent 工具与技能包 — 共 <Text strong>{totalCount}</Text> 个工具,
已启用 <Text strong style={{ color: '#52c41a' }}>{enabledCount}</Text> 个
</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Filter bar */}
<div style={{
padding: '10px 16px', borderRadius: 12, border: '1px solid #E0F2F1', background: '#fff',
padding: '10px 16px', borderRadius: 12, border: '1px solid #E5E8EF', background: '#fff',
display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
}}>
<Input
......
......@@ -211,14 +211,9 @@ export default function WorkflowsPage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>工作流管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>设计和管理 AI 工作流,实现复杂业务流程自动化</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 操作栏 */}
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<DeploymentUnitOutlined style={{ color: '#8c8c8c' }} />
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {workflows.length} 个工作流</span>
......@@ -229,7 +224,7 @@ export default function WorkflowsPage() {
</Card>
{/* 工作流列表 */}
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable search={false} dataSource={workflows} columns={columns} rowKey="id" loading={tableLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: (t) => `共 ${t} 条` }}
/>
......
......@@ -155,17 +155,10 @@ const AIAssistPage: React.FC = () => {
};
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<RobotOutlined style={{ marginRight: 6 }} />AI 辅助诊断
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>AI分析仅供参考,最终诊断以医生临床判断为准</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Alert message="AI 辅助诊断仅供参考,最终诊断以医生临床判断为准" type="info" showIcon style={{ fontSize: 12 }} />
<Card size="small" style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card size="small" style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div className="text-xs font-semibold mb-1">症状描述</div>
<TextArea rows={3} placeholder="请输入患者的主诉和症状描述.." value={symptoms}
onChange={(e) => setSymptoms(e.target.value)} className="mb-2" size="small" />
......
......@@ -103,8 +103,8 @@ const DoctorCertificationPage: React.FC = () => {
if (currentStep === 1) {
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ maxWidth: 480, margin: '60px auto', textAlign: 'center', borderRadius: 12, border: '1px solid #E0F2F1', padding: '40px 20px' }}>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ maxWidth: 480, margin: '60px auto', textAlign: 'center', borderRadius: 12, border: '1px solid #E5E8EF', padding: '40px 20px' }}>
<div style={{ width: 64, height: 64, borderRadius: '50%', background: '#f6ffed', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px', fontSize: 32, color: '#52c41a' }}>
<CheckCircleOutlined />
</div>
......@@ -119,12 +119,7 @@ const DoctorCertificationPage: React.FC = () => {
}
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生资质认证</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>提交执业资质,通过审核后即可开始接诊</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Steps
current={currentStep}
items={[
......@@ -135,7 +130,7 @@ const DoctorCertificationPage: React.FC = () => {
style={{ maxWidth: 600 }}
/>
<Card style={{ maxWidth: 680, borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ maxWidth: 680, borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="license_no" label="执业证号" rules={[{ required: true, message: '请输入执业证书号' }]}>
<Input placeholder="请输入医师执业证书号" />
......
......@@ -140,13 +140,8 @@ const ChronicReviewPage: React.FC = () => {
return (
<Spin spinning={loading}>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>慢病续方审核</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>审核患者的慢病续方申请,AI 已预生成处方草稿</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs defaultActiveKey="pending" items={[
{
key: 'pending',
......
......@@ -274,10 +274,10 @@ const AIPanel: React.FC<AIPanelProps> = ({
key={i}
style={{
fontSize: 12, marginTop: 6, paddingLeft: 8,
borderLeft: msg.role === 'user' ? '2px solid #0D9488' : '2px solid #52c41a',
borderLeft: msg.role === 'user' ? '2px solid #2B9E6E' : '2px solid #52c41a',
}}
>
<Text strong style={{ fontSize: 12, color: msg.role === 'user' ? '#0D9488' : '#52c41a' }}>
<Text strong style={{ fontSize: 12, color: msg.role === 'user' ? '#2B9E6E' : '#52c41a' }}>
{msg.role === 'user' ? '患者' : 'AI助手'}
</Text>
<div style={{ marginTop: 2, color: '#333', lineHeight: 1.5 }}>{msg.content}</div>
......@@ -357,7 +357,7 @@ const AIPanel: React.FC<AIPanelProps> = ({
<div style={{
height: '100%',
borderRadius: 12,
border: '1px solid #E0F2F1',
border: '1px solid #E5E8EF',
background: '#fff',
display: 'flex',
flexDirection: 'column',
......
......@@ -242,20 +242,20 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
padding: '8px 12px', background: '#f5f5f5', borderRadius: 8,
display: 'flex', alignItems: 'center', gap: 8,
}}>
<FileTextOutlined style={{ fontSize: 20, color: '#0D9488' }} />
<FileTextOutlined style={{ fontSize: 20, color: '#2B9E6E' }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 500 }}>文件</div>
<div style={{ fontSize: 11, color: '#999' }}>点击下载</div>
</div>
<a href={mediaUrl} target="_blank" rel="noopener noreferrer">
<DownloadOutlined style={{ fontSize: 16, color: '#0D9488' }} />
<DownloadOutlined style={{ fontSize: 16, color: '#2B9E6E' }} />
</a>
</div>
);
case 'audio':
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<PlayCircleOutlined style={{ fontSize: 20, color: '#0D9488' }} />
<PlayCircleOutlined style={{ fontSize: 20, color: '#2B9E6E' }} />
<audio controls src={mediaUrl} style={{ height: 32 }} />
</div>
);
......@@ -357,7 +357,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
color: '#374151', background: '#f9fafb', border: '1px solid #f0f0f0',
lineHeight: 1.4, transition: 'background 0.15s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.background = '#F0FDFA'; }}
onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.background = '#F0FFF5'; }}
onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.background = '#f9fafb'; }}
>
{tpl.content}
......@@ -379,7 +379,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
display: 'flex',
flexDirection: 'column',
borderRadius: 12,
border: '1px solid #E0F2F1',
border: '1px solid #E5E8EF',
background: '#fff',
overflow: 'hidden',
}}>
......@@ -415,7 +415,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
}
</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
<span style={{ color: '#0D9488', fontFamily: 'monospace', marginRight: 4 }}>{activeConsult.serial_number}</span>
<span style={{ color: '#2B9E6E', fontFamily: 'monospace', marginRight: 4 }}>{activeConsult.serial_number}</span>
{activeConsult.patient_gender && `${activeConsult.patient_gender} · `}
{activeConsult.patient_age && `${activeConsult.patient_age}岁 · `}
{activeConsult.chief_complaint
......@@ -525,7 +525,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
<>
{/* 对方正在输入提示 */}
{typingUser && (
<div style={{ padding: '4px 12px', fontSize: 11, color: '#0D9488', background: '#F0FDFA' }}>
<div style={{ padding: '4px 12px', fontSize: 11, color: '#2B9E6E', background: '#F0FFF5' }}>
{typingUser}正在输入...
</div>
)}
......@@ -571,7 +571,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
beforeUpload={file => { handleUpload(file); return false; }}
>
<Tooltip title="发送图片">
<Button type="text" size="small" icon={<PictureOutlined />} style={{ color: '#0D9488', fontSize: 13 }} />
<Button type="text" size="small" icon={<PictureOutlined />} style={{ color: '#2B9E6E', fontSize: 13 }} />
</Tooltip>
</Upload>
<Upload
......
......@@ -56,8 +56,8 @@ const PatientCard: React.FC<{
onClick={() => !isWaiting && onSelect()}
style={{
padding: '10px 12px',
background: isSelected ? '#F0FDFA' : '#fff',
borderLeft: `3px solid ${isSelected ? '#0D9488' : riskColor}`,
background: isSelected ? '#F0FFF5' : '#fff',
borderLeft: `3px solid ${isSelected ? '#2B9E6E' : riskColor}`,
borderBottom: '1px solid #f5f5f5',
cursor: isWaiting ? 'default' : 'pointer',
transition: 'background 0.15s',
......@@ -93,7 +93,7 @@ const PatientCard: React.FC<{
</Text>
{patient.is_revisit && (
<Tooltip title={`复诊 · 第${patient.visit_count + 1}次就诊`}>
<ThunderboltOutlined style={{ fontSize: 11, color: '#0D9488' }} />
<ThunderboltOutlined style={{ fontSize: 11, color: '#2B9E6E' }} />
</Tooltip>
)}
</div>
......@@ -102,7 +102,7 @@ const PatientCard: React.FC<{
: <MessageOutlined style={{ fontSize: 12, color: '#52c41a' }} />
}
</div>
<div style={{ fontSize: 10, color: '#0D9488', fontFamily: 'monospace', marginBottom: 2 }}>
<div style={{ fontSize: 10, color: '#2B9E6E', fontFamily: 'monospace', marginBottom: 2 }}>
{patient.serial_number}
</div>
<div style={{ fontSize: 11, color: '#8c8c8c', marginBottom: 3 }}>
......@@ -253,7 +253,7 @@ const PatientList: React.FC<PatientListProps> = ({
<div style={{
height: '100%',
borderRadius: 12,
border: '1px solid #E0F2F1',
border: '1px solid #E5E8EF',
background: '#fff',
display: 'flex',
flexDirection: 'column',
......
......@@ -161,7 +161,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie
const statusInfo = controlStatusMap[cr.control_status] || { text: cr.control_status, color: 'default' };
return (
<div key={cr.id} style={{
padding: '10px 12px', border: '1px solid #E0F2F1', borderRadius: 8, background: '#fafafa',
padding: '10px 12px', border: '1px solid #E5E8EF', borderRadius: 8, background: '#fafafa',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Text strong style={{ fontSize: 13 }}>{cr.disease_name}</Text>
......@@ -244,7 +244,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie
<Space direction="vertical" style={{ width: '100%' }} size={6}>
{profile.family_history.map((fm, i) => (
<div key={i} style={{
padding: '8px 12px', border: '1px solid #E0F2F1', borderRadius: 8, background: '#fafafa',
padding: '8px 12px', border: '1px solid #E5E8EF', borderRadius: 8, background: '#fafafa',
}}>
<Text strong style={{ fontSize: 12 }}>{fm.name}</Text>
<Tag style={{ marginLeft: 6, fontSize: 10 }}>{fm.relation}</Tag>
......@@ -263,7 +263,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie
<Drawer
title={
<Space>
<UserOutlined style={{ color: '#0D9488' }} />
<UserOutlined style={{ color: '#2B9E6E' }} />
<span>患者画像</span>
{profile?.basic_info?.name && (
<Tag color="blue">{profile.basic_info.name}</Tag>
......
......@@ -91,7 +91,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
>
<div style={{ marginBottom: 8 }}>
<Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#0D9488' }} />
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#2B9E6E' }} />
<Text strong>{consult.patient_name || '患者'}</Text>
<Tag color={consult.type === 'video' ? 'blue' : 'green'} style={{ fontSize: 11 }}>
{consult.type === 'video' ? '视频' : '图文'}
......@@ -186,7 +186,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
return (
<Card
style={{ borderRadius: 12, border: '1px solid #E0F2F1', height: '100%', overflow: 'auto' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF', height: '100%', overflow: 'auto' }}
styles={{ body: { padding: 12 } }}
size="small"
>
......
......@@ -274,13 +274,13 @@ const ConsultPage: React.FC = () => {
const statItems = [
{ label: '候诊中', value: waitingCount, color: '#fa8c16', icon: <ClockCircleOutlined />, unit: '人' },
{ label: '接诊中', value: inProgressCount, color: '#52c41a', icon: <MessageOutlined />, unit: '人' },
{ label: '今日完成', value: completedCount, color: '#0D9488', icon: <CheckCircleOutlined />, unit: '人' },
{ label: '今日完成', value: completedCount, color: '#2B9E6E', icon: <CheckCircleOutlined />, unit: '人' },
{ label: '今日收入', value: workbenchStats ? (workbenchStats.income_today / 100).toFixed(0) : '—', color: '#0891B2', icon: <DashboardOutlined />, unit: '元' },
];
const efficiencyItems = workbenchStats ? [
{ label: '平均候诊', value: workbenchStats.avg_wait_minutes.toFixed(1), unit: '分钟', icon: <FieldTimeOutlined />, color: '#fa8c16' },
{ label: '平均问诊', value: workbenchStats.avg_consult_minutes.toFixed(1), unit: '分钟', icon: <ClockCircleOutlined />, color: '#0D9488' },
{ label: '平均问诊', value: workbenchStats.avg_consult_minutes.toFixed(1), unit: '分钟', icon: <ClockCircleOutlined />, color: '#2B9E6E' },
{ label: '处方率', value: workbenchStats.prescription_rate.toFixed(0), unit: '%', icon: <MedicineBoxOutlined />, color: '#52c41a' },
{ label: '满意度', value: workbenchStats.avg_satisfaction > 0 ? workbenchStats.avg_satisfaction.toFixed(1) : '—', unit: workbenchStats.avg_satisfaction > 0 ? '分' : '', icon: <StarOutlined />, color: '#faad14' },
{ label: '今日转诊', value: workbenchStats.transfer_count, unit: '次', icon: <SwapOutlined />, color: '#13c2c2' },
......@@ -288,17 +288,10 @@ const ConsultPage: React.FC = () => {
] : [];
return (
<div style={{ height: 'calc(100vh - 72px)', display: 'flex', flexDirection: 'column', gap: 16, padding: '20px 24px' }}>
<div style={{ height: 'calc(100vh - 72px)', display: 'flex', flexDirection: 'column', gap: 16, padding: '12px 16px' }}>
{/* 顶部标题 + 统计卡片 */}
<div style={{ flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div>
<div style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', display: 'flex', alignItems: 'center', gap: 8 }}>
<MessageOutlined style={{ color: '#0D9488' }} />
问诊大厅
</div>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>实时接诊管理,AI辅助诊断</div>
</div>
<div style={{ marginLeft: 'auto', display: 'flex', gap: 10, alignItems: 'center' }}>
{statItems.map(({ label, value, color, icon, unit }) => (
<div
......@@ -307,7 +300,7 @@ const ConsultPage: React.FC = () => {
background: '#fff',
borderRadius: 10,
padding: '8px 18px',
border: '1px solid #E0F2F1',
border: '1px solid #E5E8EF',
minWidth: 100,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
}}
......@@ -326,10 +319,10 @@ const ConsultPage: React.FC = () => {
onClick={() => setShowEfficiency(!showEfficiency)}
style={{
cursor: 'pointer', padding: '6px 10px', borderRadius: 8,
background: showEfficiency ? '#F0FDFA' : '#f5f5f5',
background: showEfficiency ? '#F0FFF5' : '#f5f5f5',
border: `1px solid ${showEfficiency ? '#91d5ff' : '#e8e8e8'}`,
display: 'flex', alignItems: 'center', gap: 4,
fontSize: 12, color: showEfficiency ? '#0D9488' : '#8c8c8c',
fontSize: 12, color: showEfficiency ? '#2B9E6E' : '#8c8c8c',
transition: 'all 0.2s',
}}
>
......
......@@ -107,17 +107,10 @@ const IncomePage: React.FC = () => {
return (
<Spin spinning={loading}>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>收入统计</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您的问诊收入与提现记录</div>
</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={[16, 16]}>
<Col span={8}>
<div style={{ borderRadius: 12, padding: '20px 24px', background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)', position: 'relative', overflow: 'hidden' }}>
<div style={{ borderRadius: 12, padding: '12px 16px', background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: -30, right: -30, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }} />
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.8)', marginBottom: 6 }}>可提现余额</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#fff', marginBottom: 12 }}>¥{(stats.total_balance / 100).toFixed(2)}</div>
......@@ -126,7 +119,7 @@ const IncomePage: React.FC = () => {
</div>
</Col>
<Col span={8}>
<div style={{ background: '#fff', borderRadius: 12, padding: '20px 24px', border: '1px solid #E0F2F1', borderLeft: '4px solid #52c41a' }}>
<div style={{ background: '#fff', borderRadius: 12, padding: '12px 16px', border: '1px solid #E5E8EF', borderLeft: '4px solid #52c41a' }}>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 6 }}>本月收入</div>
<div style={{ fontSize: 26, fontWeight: 700, color: '#52c41a', marginBottom: 6 }}>¥{(stats.month_income / 100).toFixed(2)}</div>
<div style={{ fontSize: 12, color: '#bfbfbf', display: 'flex', alignItems: 'center', gap: 4 }}>
......@@ -135,9 +128,9 @@ const IncomePage: React.FC = () => {
</div>
</Col>
<Col span={8}>
<div style={{ background: '#fff', borderRadius: 12, padding: '20px 24px', border: '1px solid #E0F2F1', borderLeft: '4px solid #0D9488' }}>
<div style={{ background: '#fff', borderRadius: 12, padding: '12px 16px', border: '1px solid #E5E8EF', borderLeft: '4px solid #2B9E6E' }}>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 6 }}>本月问诊量</div>
<div style={{ fontSize: 26, fontWeight: 700, color: '#0D9488', marginBottom: 6 }}>
<div style={{ fontSize: 26, fontWeight: 700, color: '#2B9E6E', marginBottom: 6 }}>
{stats.month_consults} <span style={{ fontSize: 14, color: '#bfbfbf', fontWeight: 400 }}>次</span>
</div>
<div style={{ fontSize: 12, color: '#bfbfbf' }}>日均 {(stats.month_consults / Math.max(1, new Date().getDate())).toFixed(1)} 次</div>
......@@ -152,7 +145,7 @@ const IncomePage: React.FC = () => {
key: 'records',
label: <span><FileTextOutlined /> 收入明细</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ marginBottom: 16 }}>
<Space>
<RangePicker value={dateRange} onChange={(dates) => setDateRange(dates as any)} />
......@@ -200,7 +193,7 @@ const IncomePage: React.FC = () => {
<Text type="secondary">图文问诊</Text>
<Text>¥{(bill.text_income / 100).toFixed(0)}</Text>
</Space>
<Progress percent={Math.round(bill.text_income / bill.total_income * 100)} size="small" strokeColor="#0D9488" />
<Progress percent={Math.round(bill.text_income / bill.total_income * 100)} size="small" strokeColor="#2B9E6E" />
</div>
<div style={{ marginBottom: 8 }}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
......@@ -227,7 +220,7 @@ const IncomePage: React.FC = () => {
key: 'withdraw',
label: <span><BankOutlined /> 提现记录</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Table
dataSource={withdrawals}
rowKey="id"
......
This diff is collapsed.
......@@ -140,18 +140,13 @@ const PatientRecordPage: React.FC = () => {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>患者档案</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您接诊过的患者健康记录与历史问诊</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={16} align="stretch">
<Col span={8}>
<Card
title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>}
extra={<Text type="secondary" style={{ fontSize: 13 }}>{total} 人</Text>}
style={{ borderRadius: 12, border: '1px solid #E0F2F1', height: '100%' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF', height: '100%' }}
>
<Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机"
value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}
......@@ -205,7 +200,7 @@ const PatientRecordPage: React.FC = () => {
<Spin spinning={detailLoading}>
{selectedPatient ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Descriptions title={<span style={{ fontSize: 14, fontWeight: 600 }}>基本信息</span>} bordered size="small" column={3}>
<Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item>
<Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item>
......@@ -222,7 +217,7 @@ const PatientRecordPage: React.FC = () => {
</Descriptions>
</Card>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs defaultActiveKey="consult" items={[
{
key: 'consult',
......@@ -254,7 +249,7 @@ const PatientRecordPage: React.FC = () => {
</Card>
</div>
) : (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', textAlign: 'center', padding: '60px 0' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', textAlign: 'center', padding: '60px 0' }}>
<Empty description="请从左侧选择患者查看档案" />
</Card>
)}
......
......@@ -140,18 +140,13 @@ const PatientRecordPage: React.FC = () => {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>患者档案</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您接诊过的患者健康记录与历史问诊</div>
</div>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Row gutter={16} align="stretch">
<Col span={8}>
<Card
title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>}
extra={<Text type="secondary" style={{ fontSize: 13 }}>{total} 人</Text>}
style={{ borderRadius: 12, border: '1px solid #E0F2F1', height: '100%' }}
style={{ borderRadius: 12, border: '1px solid #E5E8EF', height: '100%' }}
>
<Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机"
value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}
......@@ -205,7 +200,7 @@ const PatientRecordPage: React.FC = () => {
<Spin spinning={detailLoading}>
{selectedPatient ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Descriptions title={<span style={{ fontSize: 14, fontWeight: 600 }}>基本信息</span>} bordered size="small" column={3}>
<Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item>
<Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item>
......@@ -222,7 +217,7 @@ const PatientRecordPage: React.FC = () => {
</Descriptions>
</Card>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs defaultActiveKey="consult" items={[
{
key: 'consult',
......@@ -254,7 +249,7 @@ const PatientRecordPage: React.FC = () => {
</Card>
</div>
) : (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1', textAlign: 'center', padding: '60px 0' }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF', textAlign: 'center', padding: '60px 0' }}>
<Empty description="请从左侧选择患者查看档案" />
</Card>
)}
......
This diff is collapsed.
This diff is collapsed.
......@@ -165,16 +165,8 @@ export default function DoctorTasksPage() {
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>
<ClockCircleOutlined style={{ marginRight: 8, color: '#0D9488' }} />
待办任务
</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>工作流人工审核任务,需要您及时处理</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
......
This diff is collapsed.
This diff is collapsed.
......@@ -78,7 +78,7 @@ const PatientVideoConsultPage: React.FC = () => {
return (
<div style={{ height: 'calc(100vh - 120px)', display: 'flex', flexDirection: 'column' }}>
<Card size="small" style={{ flex: 1, display: 'flex', flexDirection: 'column', borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Card size="small" style={{ flex: 1, display: 'flex', flexDirection: 'column', borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div className="text-sm font-bold mb-2">视频问诊</div>
<div style={{ flex: 1, display: 'flex', gap: 8, background: '#000', borderRadius: 6, padding: 8, minHeight: 350, position: 'relative' }}>
<div style={{ flex: 1, position: 'relative' }}>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -45,7 +45,7 @@ const iconMap: Record<string, React.ReactNode> = {
};
const colorMap: Record<string, string> = {
navigate: '#0D9488',
navigate: '#2B6DE8',
chat: '#52c41a',
followup: '#0891B2',
};
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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