Commit 45960530 authored by yuguo's avatar yuguo

fix

parent caca4568
...@@ -57,7 +57,8 @@ ...@@ -57,7 +57,8 @@
"Bash(docker-compose ps:*)", "Bash(docker-compose ps:*)",
"Bash(cat web/src/app/\\\\\\(main\\\\\\)/admin/agents/page.tsx)", "Bash(cat web/src/app/\\\\\\(main\\\\\\)/admin/agents/page.tsx)",
"Bash(awk NR==68,NR==376:*)", "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() { ...@@ -308,11 +308,9 @@ func main() {
authApi.POST("/user/verify-identity", userHandler.VerifyIdentity) authApi.POST("/user/verify-identity", userHandler.VerifyIdentity)
authApi.POST("/doctor/appointment", doctorHandler.MakeAppointment) authApi.POST("/doctor/appointment", doctorHandler.MakeAppointment)
// 医生端路由(需要认证 + 医生角色) // 医生端路由(需要认证,handler 内部通过 user_id 查 doctors 表验证医生身份)
doctorApi := authApi.Group("")
doctorApi.Use(middleware.RequireRole("doctor"))
doctorPortalHandler := doctorportal.NewHandler() doctorPortalHandler := doctorportal.NewHandler()
doctorPortalHandler.RegisterRoutes(doctorApi) doctorPortalHandler.RegisterRoutes(authApi)
// 健康档案路由(需要认证) // 健康档案路由(需要认证)
healthHandler := health.NewHandler() healthHandler := health.NewHandler()
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
...@@ -14,6 +15,21 @@ import ( ...@@ -14,6 +15,21 @@ import (
"internet-hospital/pkg/response" "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 事件 // sendSSEEvent 发送 SSE 事件
func sendSSEEvent(c *gin.Context, flusher interface{ Flush() }, event string, data string) { func sendSSEEvent(c *gin.Context, flusher interface{ Flush() }, event string, data string) {
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, data) fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, data)
...@@ -86,7 +102,8 @@ func (h *Handler) Chat(c *gin.Context) { ...@@ -86,7 +102,8 @@ func (h *Handler) Chat(c *gin.Context) {
userID, _ := c.Get("user_id") userID, _ := c.Get("user_id")
uid, _ := userID.(string) uid, _ := userID.(string)
role, _ := c.Get("role") 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) output, err := h.svc.Chat(c.Request.Context(), agentID, uid, userRole, req.SessionID, req.Message, req.Context)
if err != nil { if err != nil {
...@@ -128,7 +145,8 @@ func (h *Handler) ChatStream(c *gin.Context) { ...@@ -128,7 +145,8 @@ func (h *Handler) ChatStream(c *gin.Context) {
userID, _ := c.Get("user_id") userID, _ := c.Get("user_id")
uid, _ := userID.(string) uid, _ := userID.(string)
role, _ := c.Get("role") role, _ := c.Get("role")
userRole, _ := role.(string) jwtRole, _ := role.(string)
userRole := inferRoleFromAgent(agentID, jwtRole)
emit := func(event, data string) { emit := func(event, data string) {
sendSSEEvent(c, flusher, event, data) sendSSEEvent(c, flusher, event, data)
...@@ -153,7 +171,8 @@ func (h *Handler) CreateSession(c *gin.Context) { ...@@ -153,7 +171,8 @@ func (h *Handler) CreateSession(c *gin.Context) {
userID, _ := c.Get("user_id") userID, _ := c.Get("user_id")
uid, _ := userID.(string) uid, _ := userID.(string)
role, _ := c.Get("role") role, _ := c.Get("role")
userRole, _ := role.(string) jwtRole, _ := role.(string)
userRole := inferRoleFromAgent(req.AgentID, jwtRole)
sessionID := uuid.New().String() sessionID := uuid.New().String()
session := model.AgentSession{ session := model.AgentSession{
......
...@@ -8,6 +8,7 @@ type Role struct { ...@@ -8,6 +8,7 @@ type Role struct {
Code string `gorm:"uniqueIndex;size:50;not null" json:"code"` Code string `gorm:"uniqueIndex;size:50;not null" json:"code"`
Name string `gorm:"size:100;not null" json:"name"` Name string `gorm:"size:100;not null" json:"name"`
Description string `gorm:"size:500" json:"description"` Description string `gorm:"size:500" json:"description"`
HomePath string `gorm:"size:200" json:"home_path"` // 登录后默认首页路径
IsSystem bool `gorm:"default:false" json:"is_system"` IsSystem bool `gorm:"default:false" json:"is_system"`
Status string `gorm:"size:20;default:active" json:"status"` // active | disabled Status string `gorm:"size:20;default:active" json:"status"` // active | disabled
Sort int `gorm:"default:0" json:"sort"` Sort int `gorm:"default:0" json:"sort"`
......
...@@ -94,7 +94,35 @@ func (h *Handler) UpdateMenu(c *gin.Context) { ...@@ -94,7 +94,35 @@ func (h *Handler) UpdateMenu(c *gin.Context) {
} }
updates := map[string]interface{}{} updates := map[string]interface{}{}
if req.ParentID != nil { 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 != "" { if req.Name != "" {
updates["name"] = req.Name updates["name"] = req.Name
......
...@@ -26,13 +26,21 @@ func (h *Handler) ListRoles(c *gin.Context) { ...@@ -26,13 +26,21 @@ func (h *Handler) ListRoles(c *gin.Context) {
func (h *Handler) SetUserRole(c *gin.Context) { func (h *Handler) SetUserRole(c *gin.Context) {
userID := c.Param("id") userID := c.Param("id")
var req struct { 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 { if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "role 必须为 patient、doctor 或 admin") response.BadRequest(c, "role 不能为空")
return return
} }
db := database.GetDB() 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 var user model.User
if err := db.Where("id = ?", userID).First(&user).Error; err != nil { if err := db.Where("id = ?", userID).First(&user).Error; err != nil {
response.Error(c, 404, "用户不存在") response.Error(c, 404, "用户不存在")
...@@ -102,23 +110,76 @@ type MenuTreeNode struct { ...@@ -102,23 +110,76 @@ type MenuTreeNode struct {
} }
// GetMyMenus 基于 JWT role 返回对应端菜单树 // GetMyMenus 基于 JWT role 返回对应端菜单树
// admin → /admin/* 菜单, patient → /patient/* 菜单, doctor → /doctor/* 菜单 // 优先从 role_menus 关联表获取该角色的菜单,若未配置则降级为路径前缀过滤
func (h *Handler) GetMyMenus(c *gin.Context) { func (h *Handler) GetMyMenus(c *gin.Context) {
role, _ := c.Get("role") role, _ := c.Get("role")
roleStr, _ := role.(string) roleStr, _ := role.(string)
if roleStr == "" {
prefix := rolePathPrefix(roleStr)
if prefix == "" {
response.Success(c, []MenuTreeNode{}) response.Success(c, []MenuTreeNode{})
return return
} }
db := database.GetDB() 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 var menus []model.Menu
db.Where("visible = ? AND status = ?", true, "active"). db.Where("id IN ? AND visible = ? AND status = ?", menuIDs, true, "active").
Order("sort asc, id asc").Find(&menus) Order("sort asc, id asc").Find(&menus)
filtered := filterMenusByPrefix(menus, prefix) // 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
}
var allMenus []model.Menu
db.Where("visible = ? AND status = ?", true, "active").
Order("sort asc, id asc").Find(&allMenus)
filtered := filterMenusByPrefix(allMenus, prefix)
response.Success(c, buildMenuTree(filtered, 0)) response.Success(c, buildMenuTree(filtered, 0))
} }
......
This diff is collapsed.
...@@ -3,6 +3,7 @@ package doctor ...@@ -3,6 +3,7 @@ package doctor
import ( import (
"context" "context"
"errors" "errors"
"time"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -44,6 +45,9 @@ func (s *Service) GetDepartments(ctx context.Context) ([]model.Department, error ...@@ -44,6 +45,9 @@ func (s *Service) GetDepartments(ctx context.Context) ([]model.Department, error
type DoctorWithDepartment struct { type DoctorWithDepartment struct {
model.Doctor 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) { func (s *Service) GetDoctorList(ctx context.Context, params *DoctorListParams) (*PaginatedResponse, error) {
...@@ -54,46 +58,59 @@ func (s *Service) GetDoctorList(ctx context.Context, params *DoctorListParams) ( ...@@ -54,46 +58,59 @@ func (s *Service) GetDoctorList(ctx context.Context, params *DoctorListParams) (
params.PageSize = 20 params.PageSize = 20
} }
// 先查询医生列表 today := time.Now().Format("2006-01-02")
query := s.db.Model(&model.Doctor{}).Where("status = ?", "approved") 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 != "" { if params.DepartmentID != "" {
query = query.Where("department_id = ?", params.DepartmentID) query = query.Where("doctors.department_id = ?", params.DepartmentID)
} }
if params.Keyword != "" { if params.Keyword != "" {
query = query.Where("name LIKE ?", "%"+params.Keyword+"%") query = query.Where("doctors.name LIKE ?", "%"+params.Keyword+"%")
} }
var total int64 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 { switch params.SortBy {
case "rating": case "rating":
query = query.Order("rating DESC") query = query.Order(baseOrder + ", doctors.rating DESC")
case "consult_count": case "consult_count":
query = query.Order("consult_count DESC") query = query.Order(baseOrder + ", doctors.consult_count DESC")
case "price": case "price":
query = query.Order("price ASC") query = query.Order(baseOrder + ", doctors.price ASC")
default: 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 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 return nil, err
} }
// 构建返回结果
var result []DoctorWithDepartment
for _, doctor := range doctors {
result = append(result, DoctorWithDepartment{
Doctor: doctor,
DepartmentName: doctor.Department.Name,
})
}
return &PaginatedResponse{ return &PaginatedResponse{
List: result, List: result,
Total: total, Total: total,
......
...@@ -172,10 +172,15 @@ func (s *Service) CreatePrescription(ctx context.Context, doctorID string, req * ...@@ -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") 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 != "" { if req.ConsultID != "" {
tx.Model(&model.Consultation{}).Where("id = ?", req.ConsultID). updates := map[string]interface{}{
Update("prescription_id", prescription.ID) "prescription_id": prescription.ID,
}
if req.Diagnosis != "" {
updates["diagnosis"] = req.Diagnosis
}
tx.Model(&model.Consultation{}).Where("id = ?", req.ConsultID).Updates(updates)
} }
tx.Commit() tx.Commit()
......
...@@ -5,7 +5,6 @@ import ( ...@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
...@@ -51,6 +50,7 @@ type LoginResponse struct { ...@@ -51,6 +50,7 @@ type LoginResponse struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"` ExpiresIn int64 `json:"expires_in"`
User *model.User `json:"user"` User *model.User `json:"user"`
HomePath string `json:"home_path"`
Menus []model.Menu `json:"menus"` Menus []model.Menu `json:"menus"`
} }
...@@ -97,11 +97,14 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes ...@@ -97,11 +97,14 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes
return nil, err return nil, err
} }
// 确定用户角色,默认为 patient // 确定用户角色:验证请求的角色是否存在于 DB,默认为 patient
role := "patient" role := "patient"
if req.Role == "doctor" || req.Role == "admin" { 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 role = req.Role
} }
}
fmt.Printf("最终角色: %s\n", role) fmt.Printf("最终角色: %s\n", role)
user := &model.User{ user := &model.User{
...@@ -135,11 +138,16 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes ...@@ -135,11 +138,16 @@ func (s *Service) Register(ctx context.Context, req *RegisterRequest) (*LoginRes
return nil, err return nil, err
} }
menus := s.getUserMenus(user.ID)
homePath := s.getRoleHomePath(user.Role)
return &LoginResponse{ return &LoginResponse{
AccessToken: tokenPair.AccessToken, AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken, RefreshToken: tokenPair.RefreshToken,
ExpiresIn: tokenPair.ExpiresIn, ExpiresIn: tokenPair.ExpiresIn,
User: user, User: user,
HomePath: homePath,
Menus: menus,
}, nil }, nil
} }
...@@ -172,12 +180,14 @@ func (s *Service) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, ...@@ -172,12 +180,14 @@ func (s *Service) Login(ctx context.Context, req *LoginRequest) (*LoginResponse,
} }
menus := s.getUserMenus(user.ID) menus := s.getUserMenus(user.ID)
homePath := s.getRoleHomePath(user.Role)
return &LoginResponse{ return &LoginResponse{
AccessToken: tokenPair.AccessToken, AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken, RefreshToken: tokenPair.RefreshToken,
ExpiresIn: tokenPair.ExpiresIn, ExpiresIn: tokenPair.ExpiresIn,
User: &user, User: &user,
HomePath: homePath,
Menus: menus, Menus: menus,
}, nil }, nil
} }
...@@ -193,6 +203,7 @@ func (s *Service) GetUserByID(ctx context.Context, userID string) (*model.User, ...@@ -193,6 +203,7 @@ func (s *Service) GetUserByID(ctx context.Context, userID string) (*model.User,
// UserProfileResponse 用户完整信息响应(包含菜单) // UserProfileResponse 用户完整信息响应(包含菜单)
type UserProfileResponse struct { type UserProfileResponse struct {
User *model.User `json:"user"` User *model.User `json:"user"`
HomePath string `json:"home_path"`
Menus []model.Menu `json:"menus"` Menus []model.Menu `json:"menus"`
} }
...@@ -204,86 +215,80 @@ func (s *Service) GetUserProfile(ctx context.Context, userID string) (*UserProfi ...@@ -204,86 +215,80 @@ func (s *Service) GetUserProfile(ctx context.Context, userID string) (*UserProfi
} }
menus := s.getUserMenus(userID) menus := s.getUserMenus(userID)
homePath := s.getRoleHomePath(user.Role)
return &UserProfileResponse{ return &UserProfileResponse{
User: &user, User: &user,
HomePath: homePath,
Menus: menus, Menus: menus,
}, nil }, nil
} }
// getUserMenus 基于 User.Role 返回对应端菜单树 // getRoleHomePath 查询角色的 home_path
// admin → /admin/* , patient → /patient/* , doctor → /doctor/* 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 { func (s *Service) getUserMenus(userID string) []model.Menu {
var user model.User var user model.User
if err := s.db.Select("role").Where("id = ?", userID).First(&user).Error; err != nil { if err := s.db.Select("role").Where("id = ?", userID).First(&user).Error; err != nil {
fmt.Printf("[getUserMenus] 查询用户失败: %v\n", err)
return nil return nil
} }
prefix := rolePathPrefix(user.Role) // 查找角色记录
fmt.Printf("[getUserMenus] userID=%s role=%s prefix=%s\n", userID, user.Role, prefix) var roleRecord model.Role
if prefix == "" { if err := s.db.Where("code = ?", user.Role).First(&roleRecord).Error; err != nil {
return []model.Menu{} return nil
} }
var flatMenus []model.Menu // 从 role_menus 获取关联的菜单ID
s.db.Where("visible = ? AND status = ?", true, "active"). var menuIDs []uint
Order("sort ASC, id ASC").Find(&flatMenus) s.db.Model(&model.RoleMenu{}).Where("role_id = ?", roleRecord.ID).Pluck("menu_id", &menuIDs)
fmt.Printf("[getUserMenus] 查询到 %d 条可见菜单\n", len(flatMenus)) if len(menuIDs) == 0 {
return nil
filtered := filterMenusByPrefix(flatMenus, prefix)
fmt.Printf("[getUserMenus] 过滤后 %d 条菜单 (prefix=%s)\n", len(filtered), prefix)
tree := buildMenuTree(filtered, 0)
fmt.Printf("[getUserMenus] 构建菜单树 %d 个顶级节点\n", len(tree))
return tree
}
// rolePathPrefix 返回角色对应的路由前缀
func rolePathPrefix(role string) string {
switch role {
case "admin":
return "/admin/"
case "patient":
return "/patient/"
case "doctor":
return "/doctor/"
default:
return ""
} }
}
// filterMenusByPrefix 筛选属于指定路径前缀的菜单(包含父级分组节点) // 查询这些菜单(仅 visible + active)
func filterMenusByPrefix(menus []model.Menu, prefix string) []model.Menu { var menus []model.Menu
matchIDs := map[uint]bool{} s.db.Where("id IN ? AND visible = ? AND status = ?", menuIDs, true, "active").
parentIDs := map[uint]bool{} Order("sort asc, id asc").Find(&menus)
// 确保父节点也被包含
existIDs := map[uint]bool{}
for _, m := range menus { for _, m := range menus {
if m.Path != "" && strings.HasPrefix(m.Path, prefix) { existIDs[m.ID] = true
matchIDs[m.ID] = true
if m.ParentID != 0 {
parentIDs[m.ParentID] = true
} }
} missingParentIDs := map[uint]bool{}
}
for changed := true; changed; {
changed = false
for _, m := range menus { for _, m := range menus {
if parentIDs[m.ID] && !matchIDs[m.ID] { if m.ParentID != 0 && !existIDs[m.ParentID] {
matchIDs[m.ID] = true missingParentIDs[m.ParentID] = true
if m.ParentID != 0 {
parentIDs[m.ParentID] = true
} }
changed = 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
} }
var result []model.Menu
for _, m := range menus {
if matchIDs[m.ID] {
result = append(result, m)
} }
} }
return result }
return buildMenuTree(menus, 0)
} }
// buildMenuTree 将扁平菜单列表构建为树形结构 // buildMenuTree 将扁平菜单列表构建为树形结构
......
...@@ -161,9 +161,9 @@ func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, e ...@@ -161,9 +161,9 @@ func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, e
messages = append(messages, input.History...) messages = append(messages, input.History...)
messages = append(messages, ai.ChatMessage{Role: "user", Content: input.Message}) messages = append(messages, ai.ChatMessage{Role: "user", Content: input.Message})
// v15: 根据用户消息智能筛选工具 // v15: 根据用户消息智能筛选工具(按角色过滤参数,如 navigate_page 只展示对应角色的页面)
selectedTools := a.getFilteredTools(input.Message) selectedTools := a.getFilteredTools(input.Message)
tools := a.getToolSchemas(selectedTools) tools := a.getToolSchemasForRole(selectedTools, input.UserRole)
var toolCallResults []ToolCallResult var toolCallResults []ToolCallResult
totalTokens := 0 totalTokens := 0
...@@ -276,9 +276,9 @@ func (a *ReActAgent) RunStream(ctx context.Context, input AgentInput, onEvent fu ...@@ -276,9 +276,9 @@ func (a *ReActAgent) RunStream(ctx context.Context, input AgentInput, onEvent fu
messages = append(messages, input.History...) messages = append(messages, input.History...)
messages = append(messages, ai.ChatMessage{Role: "user", Content: input.Message}) messages = append(messages, ai.ChatMessage{Role: "user", Content: input.Message})
// v15: 根据用户消息智能筛选工具 // v15: 根据用户消息智能筛选工具(按角色过滤参数,如 navigate_page 只展示对应角色的页面)
selectedTools := a.getFilteredTools(input.Message) selectedTools := a.getFilteredTools(input.Message)
tools := a.getToolSchemas(selectedTools) tools := a.getToolSchemasForRole(selectedTools, input.UserRole)
var toolCallResults []ToolCallResult var toolCallResults []ToolCallResult
totalTokens := 0 totalTokens := 0
...@@ -459,7 +459,11 @@ func (a *ReActAgent) getFilteredTools(userMessage string) []string { ...@@ -459,7 +459,11 @@ func (a *ReActAgent) getFilteredTools(userMessage string) []string {
} }
func (a *ReActAgent) getToolSchemas(toolNames []string) []interface{} { 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)) result := make([]interface{}, len(schemas))
for i, s := range schemas { for i, s := range schemas {
result[i] = s result[i] = s
......
...@@ -26,12 +26,17 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) { ...@@ -26,12 +26,17 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) {
} }
func (r *ToolRegistry) GetSchemas(names []string) []ToolSchema { 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() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
var schemas []ToolSchema var schemas []ToolSchema
for _, name := range names { for _, name := range names {
if t, ok := r.tools[name]; ok { if t, ok := r.tools[name]; ok {
schemas = append(schemas, ToSchema(t)) schemas = append(schemas, ToSchemaForRole(t, role))
} }
} }
return schemas return schemas
......
...@@ -10,6 +10,13 @@ type Tool interface { ...@@ -10,6 +10,13 @@ type Tool interface {
Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error)
} }
// RoleAwareTool 角色感知工具接口 — 工具参数可根据用户角色动态调整
// 例如 navigate_page 工具只向患者展示患者端页面,避免 LLM 生成跨角色导航
type RoleAwareTool interface {
Tool
ParametersForRole(role string) []ToolParameter
}
// ToolParameter 工具参数定义 // ToolParameter 工具参数定义
type ToolParameter struct { type ToolParameter struct {
Name string `json:"name"` Name string `json:"name"`
...@@ -52,9 +59,25 @@ type PropertySchema struct { ...@@ -52,9 +59,25 @@ type PropertySchema struct {
// ToSchema 将 Tool 转换为 OpenAI Function Calling 格式 // ToSchema 将 Tool 转换为 OpenAI Function Calling 格式
func ToSchema(t Tool) ToolSchema { 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) props := make(map[string]PropertySchema)
required := make([]string, 0) // 必须初始化为空数组,否则 JSON 序列化为 null 会导致 OpenAI 报错 required := make([]string, 0) // 必须初始化为空数组,否则 JSON 序列化为 null 会导致 OpenAI 报错
for _, p := range t.Parameters() { for _, p := range params {
props[p.Name] = PropertySchema{ props[p.Name] = PropertySchema{
Type: p.Type, Type: p.Type,
Description: p.Description, Description: p.Description,
......
...@@ -87,56 +87,33 @@ func (t *NavigatePageTool) Description() string { ...@@ -87,56 +87,33 @@ func (t *NavigatePageTool) Description() string {
return "为用户准备页面导航(不会自动打开页面)。调用此工具后,系统会在对话中显示一个'打开页面'按钮,用户点击后才会跳转。【重要】必须根据当前用户角色选择对应端的页面:admin用户选admin_*页面,doctor用户选doctor_*页面,patient用户选patient_*页面,否则会被拒绝访问。" return "为用户准备页面导航(不会自动打开页面)。调用此工具后,系统会在对话中显示一个'打开页面'按钮,用户点击后才会跳转。【重要】必须根据当前用户角色选择对应端的页面:admin用户选admin_*页面,doctor用户选doctor_*页面,patient用户选patient_*页面,否则会被拒绝访问。"
} }
func (t *NavigatePageTool) Parameters() []agent.ToolParameter { func (t *NavigatePageTool) Parameters() []agent.ToolParameter {
return t.ParametersForRole("")
}
// ParametersForRole 实现 RoleAwareTool 接口,按用户角色过滤可导航的页面
// 患者只能看到患者端页面,医生只能看到医生端页面,admin 可看到所有页面
func (t *NavigatePageTool) ParametersForRole(role string) []agent.ToolParameter {
cache := loadMenuCache() cache := loadMenuCache()
// 构建 page_code 枚举和说明(按角色分组) // 构建 page_code 枚举和说明(按角色过滤)
codes := make([]string, 0, len(cache)) codes := make([]string, 0, len(cache))
adminPages := make([]string, 0) var pageDescs []string
doctorPages := make([]string, 0)
patientPages := make([]string, 0)
publicPages := make([]string, 0)
for code, entry := range cache { for code, entry := range cache {
codes = append(codes, code) // 按角色过滤:非 admin 用户只能看到自己角色对应的页面和公共页面
pageInfo := fmt.Sprintf("%s (%s)", code, entry.Name) if role != "" && role != "admin" {
switch entry.RoleScope { if entry.RoleScope != role && entry.RoleScope != "public" {
case "admin": continue
adminPages = append(adminPages, pageInfo) }
case "doctor":
doctorPages = append(doctorPages, pageInfo)
case "patient":
patientPages = append(patientPages, pageInfo)
default:
publicPages = append(publicPages, pageInfo)
} }
codes = append(codes, code)
pageDescs = append(pageDescs, fmt.Sprintf("- %s (%s)", code, entry.Name))
} }
// 构建按角色分组的说明
var desc strings.Builder var desc strings.Builder
desc.WriteString("目标页面代码。【权限说明】只能导航到当前用户角色对应的页面,否则会被拒绝。") desc.WriteString("目标页面代码。可导航的页面列表:")
if len(adminPages) > 0 { for _, p := range pageDescs {
desc.WriteString("\n\n【管理端页面 - 仅admin可访问】") desc.WriteString("\n" + p)
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)
}
} }
return []agent.ToolParameter{ 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() { ...@@ -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/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: "内容安全", 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-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 { for i := range aiMenus {
db.Create(&aiMenus[i]) db.Create(&aiMenus[i])
...@@ -102,11 +103,43 @@ func main() { ...@@ -102,11 +103,43 @@ func main() {
db.Create(&sysMenus[i]) 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("📋 菜单结构:") fmt.Println("📋 菜单结构:")
fmt.Println(" 1. 运营大盘") fmt.Println(" 管理端: 5个顶级菜单 + 19个子菜单")
fmt.Println(" 2. 用户管理 (患者/医生/管理员)") fmt.Println(" 医生端: 10个菜单")
fmt.Println(" 3. 医疗业务 (科室/问诊/处方/药品/合规)") fmt.Println(" 患者端: 9个菜单")
fmt.Println(" 4. 智能体平台 (AI配置/Agent/工具/工作流/审核/知识库/安全/运营)") fmt.Println(" 总计: 43个菜单")
fmt.Println(" 5. 系统管理 (角色/菜单)")
} }
...@@ -16,6 +16,9 @@ export interface Doctor { ...@@ -16,6 +16,9 @@ export interface Doctor {
price: number; // 问诊价格(分) price: number; // 问诊价格(分)
is_online: boolean; is_online: boolean;
schedule: DoctorSchedule[]; schedule: DoctorSchedule[];
next_available_date: string | null;
next_available_time: string | null;
available_slots: number;
} }
export interface DoctorSchedule { export interface DoctorSchedule {
......
...@@ -2,7 +2,7 @@ import { post, get, put } from './request'; ...@@ -2,7 +2,7 @@ import { post, get, put } from './request';
import { MOCK_ENABLED, mockRegister, mockLogin } from './mock'; import { MOCK_ENABLED, mockRegister, mockLogin } from './mock';
// 用户相关类型定义 // 用户相关类型定义
export type UserRole = 'patient' | 'doctor' | 'admin'; export type UserRole = string;
export interface LoginParams { export interface LoginParams {
phone?: string; phone?: string;
...@@ -16,7 +16,7 @@ export interface RegisterParams { ...@@ -16,7 +16,7 @@ export interface RegisterParams {
password: string; password: string;
code: string; code: string;
real_name?: string; real_name?: string;
role?: 'patient' | 'doctor'; role?: string;
gender?: string; gender?: string;
age?: number; age?: number;
} }
...@@ -26,7 +26,7 @@ export interface UserInfo { ...@@ -26,7 +26,7 @@ export interface UserInfo {
phone: string; phone: string;
real_name: string; real_name: string;
avatar: string; avatar: string;
role: 'patient' | 'doctor' | 'admin'; role: string;
is_verified: boolean; is_verified: boolean;
status: 'active' | 'disabled'; status: 'active' | 'disabled';
gender?: string; gender?: string;
...@@ -39,6 +39,7 @@ export interface LoginResponse { ...@@ -39,6 +39,7 @@ export interface LoginResponse {
refresh_token: string; refresh_token: string;
expires_in: number; expires_in: number;
user: UserInfo; user: UserInfo;
home_path: string;
menus: Array<{ menus: Array<{
id: number; id: number;
name: string; name: string;
...@@ -91,6 +92,7 @@ export const userApi = { ...@@ -91,6 +92,7 @@ export const userApi = {
// 获取用户完整信息(包含菜单) // 获取用户完整信息(包含菜单)
getUserProfile: () => get<{ getUserProfile: () => get<{
user: UserInfo; user: UserInfo;
home_path: string;
menus: Array<{ menus: Array<{
id: number; id: number;
parent_id: number; parent_id: number;
......
This diff is collapsed.
...@@ -16,7 +16,7 @@ const { Title, Text, Paragraph } = Typography; ...@@ -16,7 +16,7 @@ const { Title, Text, Paragraph } = Typography;
const RegisterPage: React.FC = () => { const RegisterPage: React.FC = () => {
const router = useRouter(); const router = useRouter();
const { message } = App.useApp(); const { message } = App.useApp();
const { setUser, setTokens } = useUserStore(); const { setUser, setTokens, setHomePath } = useUserStore();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
const [countdown, setCountdown] = useState(0); const [countdown, setCountdown] = useState(0);
...@@ -47,14 +47,13 @@ const RegisterPage: React.FC = () => { ...@@ -47,14 +47,13 @@ const RegisterPage: React.FC = () => {
try { try {
const response = await userApi.register({ ...values, code: values.code, role: registerType }); const response = await userApi.register({ ...values, code: values.code, role: registerType });
setTokens(response.data.access_token, response.data.refresh_token); 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('注册成功'); message.success('注册成功');
setCurrentStep(2); setCurrentStep(2);
const userRole = response.data.user.role;
setTimeout(() => { setTimeout(() => {
if (userRole === 'doctor') router.push('/doctor/workbench'); router.push(homePath);
else if (userRole === 'admin') router.push('/admin/dashboard');
else router.push('/patient/home');
}, 2000); }, 2000);
} catch (error: any) { } catch (error: any) {
message.error(error.message || '注册失败'); message.error(error.message || '注册失败');
...@@ -70,7 +69,7 @@ const RegisterPage: React.FC = () => { ...@@ -70,7 +69,7 @@ const RegisterPage: React.FC = () => {
justifyContent: 'center', justifyContent: 'center',
overflow: 'auto', overflow: 'auto',
padding: '32px 0', 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' }}> <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}>
...@@ -93,7 +92,7 @@ const RegisterPage: React.FC = () => { ...@@ -93,7 +92,7 @@ const RegisterPage: React.FC = () => {
<div style={{ textAlign: 'center', marginBottom: 20 }}> <div style={{ textAlign: 'center', marginBottom: 20 }}>
<div style={{ <div style={{
width: 48, height: 48, borderRadius: 12, width: 48, height: 48, borderRadius: 12,
background: 'linear-gradient(135deg, #0D9488, #0F766E)', background: 'linear-gradient(135deg, #2B6DE8, #1A56C4)',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
margin: '0 auto 12px', margin: '0 auto 12px',
}}> }}>
...@@ -118,7 +117,7 @@ const RegisterPage: React.FC = () => { ...@@ -118,7 +117,7 @@ const RegisterPage: React.FC = () => {
style={{ style={{
padding: 16, borderRadius: 12, cursor: 'pointer', padding: 16, borderRadius: 12, cursor: 'pointer',
transition: 'all 0.2s', transition: 'all 0.2s',
border: `2px solid ${registerType === 'patient' ? '#0D9488' : '#e5e7eb'}`, border: `2px solid ${registerType === 'patient' ? '#2B6DE8' : '#e5e7eb'}`,
backgroundColor: registerType === 'patient' ? '#f0f5ff' : 'transparent', backgroundColor: registerType === 'patient' ? '#f0f5ff' : 'transparent',
}} }}
onClick={() => setRegisterType('patient')}> onClick={() => setRegisterType('patient')}>
...@@ -129,7 +128,7 @@ const RegisterPage: React.FC = () => { ...@@ -129,7 +128,7 @@ const RegisterPage: React.FC = () => {
backgroundColor: '#eff6ff', backgroundColor: '#eff6ff',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
<UserOutlined style={{ color: '#0D9488' }} /> <UserOutlined style={{ color: '#2B6DE8' }} />
</div> </div>
<div> <div>
<Text strong style={{ fontSize: 14 }}>我是患者</Text><br /> <Text strong style={{ fontSize: 14 }}>我是患者</Text><br />
...@@ -142,7 +141,7 @@ const RegisterPage: React.FC = () => { ...@@ -142,7 +141,7 @@ const RegisterPage: React.FC = () => {
style={{ style={{
padding: 16, borderRadius: 12, cursor: 'pointer', padding: 16, borderRadius: 12, cursor: 'pointer',
transition: 'all 0.2s', transition: 'all 0.2s',
border: `2px solid ${registerType === 'doctor' ? '#0D9488' : '#e5e7eb'}`, border: `2px solid ${registerType === 'doctor' ? '#2B6DE8' : '#e5e7eb'}`,
backgroundColor: registerType === 'doctor' ? '#f0f5ff' : 'transparent', backgroundColor: registerType === 'doctor' ? '#f0f5ff' : 'transparent',
}} }}
onClick={() => setRegisterType('doctor')}> onClick={() => setRegisterType('doctor')}>
...@@ -153,7 +152,7 @@ const RegisterPage: React.FC = () => { ...@@ -153,7 +152,7 @@ const RegisterPage: React.FC = () => {
backgroundColor: '#eff6ff', backgroundColor: '#eff6ff',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
<MedicineBoxOutlined style={{ color: '#0D9488' }} /> <MedicineBoxOutlined style={{ color: '#2B6DE8' }} />
</div> </div>
<div> <div>
<Text strong style={{ fontSize: 14 }}>我是医生</Text><br /> <Text strong style={{ fontSize: 14 }}>我是医生</Text><br />
...@@ -168,8 +167,8 @@ const RegisterPage: React.FC = () => { ...@@ -168,8 +167,8 @@ const RegisterPage: React.FC = () => {
style={{ style={{
marginTop: 20, height: 40, borderRadius: 8, marginTop: 20, height: 40, borderRadius: 8,
fontWeight: 600, border: 'none', fontWeight: 600, border: 'none',
background: 'linear-gradient(90deg, #0D9488, #0F766E)', background: 'linear-gradient(90deg, #2B6DE8, #1A56C4)',
boxShadow: '0 4px 12px rgba(13,148,136,0.3)', boxShadow: '0 4px 12px rgba(43,109,232,0.3)',
}} }}
onClick={() => setCurrentStep(1)}>下一步</Button> onClick={() => setCurrentStep(1)}>下一步</Button>
</div> </div>
...@@ -179,7 +178,7 @@ const RegisterPage: React.FC = () => { ...@@ -179,7 +178,7 @@ const RegisterPage: React.FC = () => {
<Form form={form} onFinish={handleRegister} size="middle"> <Form form={form} onFinish={handleRegister} size="middle">
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}> <Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Space.Compact style={{ width: '100%' }}> <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 <Button
disabled={countdown > 0} disabled={countdown > 0}
onClick={handleSendCode} onClick={handleSendCode}
...@@ -190,13 +189,13 @@ const RegisterPage: React.FC = () => { ...@@ -190,13 +189,13 @@ const RegisterPage: React.FC = () => {
</Space.Compact> </Space.Compact>
</Form.Item> </Form.Item>
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}> <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>
<Form.Item name="real_name" rules={[{ required: true, message: '请输入真实姓名' }]}> <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>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }, { min: 6, message: '密码至少6位' }]}> <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>
<Form.Item name="confirm_password" dependencies={['password']} <Form.Item name="confirm_password" dependencies={['password']}
rules={[{ required: true, message: '请确认密码' }, ({ getFieldValue }) => ({ rules={[{ required: true, message: '请确认密码' }, ({ getFieldValue }) => ({
...@@ -205,7 +204,7 @@ const RegisterPage: React.FC = () => { ...@@ -205,7 +204,7 @@ const RegisterPage: React.FC = () => {
return Promise.reject(new Error('两次输入的密码不一致')); 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> </Form.Item>
<Row gutter={12}> <Row gutter={12}>
<Col span={12}> <Col span={12}>
...@@ -227,8 +226,8 @@ const RegisterPage: React.FC = () => { ...@@ -227,8 +226,8 @@ const RegisterPage: React.FC = () => {
<Button type="primary" htmlType="submit" loading={loading} block <Button type="primary" htmlType="submit" loading={loading} block
style={{ style={{
height: 40, borderRadius: 8, fontWeight: 600, border: 'none', height: 40, borderRadius: 8, fontWeight: 600, border: 'none',
background: 'linear-gradient(90deg, #0D9488, #0F766E)', background: 'linear-gradient(90deg, #2B6DE8, #1A56C4)',
boxShadow: '0 4px 12px rgba(13,148,136,0.3)', boxShadow: '0 4px 12px rgba(43,109,232,0.3)',
}}>注册</Button> }}>注册</Button>
<Button block style={{ borderRadius: 8 }} onClick={() => setCurrentStep(0)}>上一步</Button> <Button block style={{ borderRadius: 8 }} onClick={() => setCurrentStep(0)}>上一步</Button>
</Space> </Space>
...@@ -254,7 +253,7 @@ const RegisterPage: React.FC = () => { ...@@ -254,7 +253,7 @@ const RegisterPage: React.FC = () => {
{currentStep !== 2 && ( {currentStep !== 2 && (
<div style={{ textAlign: 'center', marginTop: 16, fontSize: 14 }}> <div style={{ textAlign: 'center', marginTop: 16, fontSize: 14 }}>
<span style={{ color: '#8c8c8c' }}>已有账号?</span> <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>
)} )}
</div> </div>
......
...@@ -168,7 +168,7 @@ const AdminAdminsPage: React.FC = () => { ...@@ -168,7 +168,7 @@ const AdminAdminsPage: React.FC = () => {
key: 'admin', key: 'admin',
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
<Avatar icon={<UserOutlined />} src={record.avatar} style={{ backgroundColor: '#0D9488' }} /> <Avatar icon={<UserOutlined />} src={record.avatar} style={{ backgroundColor: '#2B6DE8' }} />
<div> <div>
<Text strong>{record.real_name || '未填写'}</Text> <Text strong>{record.real_name || '未填写'}</Text>
<br /> <br />
...@@ -261,16 +261,8 @@ const AdminAdminsPage: React.FC = () => { ...@@ -261,16 +261,8 @@ const AdminAdminsPage: React.FC = () => {
]; ];
return ( 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' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<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={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}> <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/用户名" prefix={<SearchOutlined />} value={keyword} <Input placeholder="搜索姓名/用户名" prefix={<SearchOutlined />} value={keyword}
...@@ -282,7 +274,7 @@ const AdminAdminsPage: React.FC = () => { ...@@ -282,7 +274,7 @@ const AdminAdminsPage: React.FC = () => {
</div> </div>
</Card> </Card>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable <ProTable
search={false} search={false}
columns={columns} columns={columns}
......
...@@ -60,7 +60,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI ...@@ -60,7 +60,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
const columns = [ const columns = [
{ {
title: '工具名称', dataIndex: 'name', key: 'name', 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 }, { title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, width: 280 },
{ {
...@@ -100,13 +100,13 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI ...@@ -100,13 +100,13 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
return ( 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" <ProTable search={false} dataSource={filtered} columns={columns} rowKey="name" loading={isLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: t => `共 ${t} 个工具` }} /> pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: t => `共 ${t} 个工具` }} />
</Card> </Card>
<Modal <Modal
title={<Space><CodeOutlined style={{ color: '#0D9488' }} /><span>工具详情</span></Space>} title={<Space><CodeOutlined style={{ color: '#2B6DE8' }} /><span>工具详情</span></Space>}
open={detailModal.open} open={detailModal.open}
onCancel={() => setDetailModal({ open: false, tool: null })} onCancel={() => setDetailModal({ open: false, tool: null })}
footer={detailModal.tool && ( footer={detailModal.tool && (
...@@ -124,7 +124,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI ...@@ -124,7 +124,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
{detailModal.tool && ( {detailModal.tool && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Descriptions column={1} bordered size="small"> <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="描述">{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="分类"><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> <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) { ...@@ -163,13 +163,13 @@ export default function HTTPToolsTab({ search }: Props) {
</Space> </Space>
</div> </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" <ProTable search={false} dataSource={tools} columns={columns} rowKey="id" loading={isLoading} size="small"
pagination={{ pageSize: 10, showTotal: t => `共 ${t} 个工具` }} /> pagination={{ pageSize: 10, showTotal: t => `共 ${t} 个工具` }} />
</Card> </Card>
{/* Create/Edit Modal */} {/* 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(); }} open={modalOpen} onCancel={() => { setModalOpen(false); form.resetFields(); }}
onOk={handleSave} confirmLoading={saveMut.isPending} width={680} destroyOnHidden> onOk={handleSave} confirmLoading={saveMut.isPending} width={680} destroyOnHidden>
<Form form={form} layout="vertical" style={{ marginTop: 12 }}> <Form form={form} layout="vertical" style={{ marginTop: 12 }}>
......
...@@ -121,7 +121,7 @@ export default function SkillsTab({ search }: Props) { ...@@ -121,7 +121,7 @@ export default function SkillsTab({ search }: Props) {
<Col key={skill.skill_id} xs={24} sm={12} md={8} lg={6}> <Col key={skill.skill_id} xs={24} sm={12} md={8} lg={6}>
<Card <Card
size="small" 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 } }} styles={{ body: { padding: 16 } }}
hoverable hoverable
> >
......
...@@ -240,7 +240,7 @@ function AgentsTab() { ...@@ -240,7 +240,7 @@ function AgentsTab() {
title: '智能体', key: 'name', title: '智能体', key: 'name',
render: (_: unknown, r: AgentDefinition) => ( render: (_: unknown, r: AgentDefinition) => (
<Space> <Space>
<RobotOutlined style={{ color: '#0D9488' }} /> <RobotOutlined style={{ color: '#2B6DE8' }} />
<div> <div>
<Text strong>{r.name}</Text> <Text strong>{r.name}</Text>
<br /> <br />
...@@ -380,7 +380,7 @@ function AgentsTab() { ...@@ -380,7 +380,7 @@ function AgentsTab() {
<div key={i} style={{ marginBottom: 12, textAlign: m.role === 'user' ? 'right' : 'left' }}> <div key={i} style={{ marginBottom: 12, textAlign: m.role === 'user' ? 'right' : 'left' }}>
<div style={{ <div style={{
display: 'inline-block', padding: '8px 14px', borderRadius: 8, maxWidth: '85%', 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', color: m.role === 'user' ? '#fff' : '#333',
textAlign: 'left', textAlign: 'left',
}}> }}>
...@@ -493,16 +493,8 @@ export default function AgentManagementPage() { ...@@ -493,16 +493,8 @@ export default function AgentManagementPage() {
const [activeTab, setActiveTab] = useState('agents'); const [activeTab, setActiveTab] = useState('agents');
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<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' }}>
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
......
...@@ -23,7 +23,7 @@ export const CATEGORY_CONFIG: Record<string, { color: string; label: string; ico ...@@ -23,7 +23,7 @@ export const CATEGORY_CONFIG: Record<string, { color: string; label: string; ico
export type CATEGORY_CONFIG_TYPE = typeof CATEGORY_CONFIG; export type CATEGORY_CONFIG_TYPE = typeof CATEGORY_CONFIG;
export const SOURCE_CONFIG = { export const SOURCE_CONFIG = {
builtin: { label: '内置', color: '#0D9488' }, builtin: { label: '内置', color: '#2B6DE8' },
http: { label: 'HTTP', color: '#fa8c16' }, http: { label: 'HTTP', color: '#fa8c16' },
} as const; } as const;
......
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
Statistic, Tabs, Input, Select, Timeline, Spin, Empty, Statistic, Tabs, Input, Select, Timeline, Spin, Empty,
} from 'antd'; } from 'antd';
import { import {
FundOutlined, SearchOutlined, ToolOutlined, RobotOutlined, SearchOutlined, ToolOutlined, RobotOutlined,
ThunderboltOutlined, ThunderboltOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
...@@ -176,49 +176,41 @@ export default function AICenterPage() { ...@@ -176,49 +176,41 @@ export default function AICenterPage() {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={4}> <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 />} /> <Statistic title="总调用次数" value={stats?.total_calls ?? 0} prefix={<ThunderboltOutlined />} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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' }} /> <Statistic title="成功率" value={stats?.total_calls ? Math.round((stats.success_calls / stats.total_calls) * 100) : 0} suffix="%" valueStyle={{ color: '#52c41a' }} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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} /> <Statistic title="总 Tokens" value={stats?.total_tokens ?? 0} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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 />} /> <Statistic title="Agent 执行" value={stats?.agent_execs ?? 0} prefix={<RobotOutlined />} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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 />} /> <Statistic title="工具调用" value={stats?.tool_calls ?? 0} prefix={<ToolOutlined />} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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' }} /> <Statistic title="模拟调用" value={stats?.mock_calls ?? 0} valueStyle={{ color: '#fa8c16' }} />
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
......
...@@ -294,12 +294,7 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -294,12 +294,7 @@ const AdminAIConfigPage: React.FC = () => {
}; };
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Tabs defaultActiveKey="model" items={[ <Tabs defaultActiveKey="model" items={[
{ {
key: 'model', key: 'model',
...@@ -308,7 +303,7 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -308,7 +303,7 @@ const AdminAIConfigPage: React.FC = () => {
<Spin spinning={loading}> <Spin spinning={loading}>
<Row gutter={24}> <Row gutter={24}>
<Col span={16}> <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 }} /> <Alert message="此配置将全局生效,影响所有 AI 功能" type="info" showIcon style={{ marginBottom: 20 }} />
<Form form={modelForm} layout="vertical"> <Form form={modelForm} layout="vertical">
<Row gutter={16}> <Row gutter={16}>
...@@ -360,7 +355,7 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -360,7 +355,7 @@ const AdminAIConfigPage: React.FC = () => {
</Card> </Card>
</Col> </Col>
<Col span={8}> <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%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
<div> <div>
<Text type="secondary">今日调用次数</Text> <Text type="secondary">今日调用次数</Text>
...@@ -387,7 +382,7 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -387,7 +382,7 @@ const AdminAIConfigPage: React.FC = () => {
key: 'prompts', key: 'prompts',
label: <span><FileTextOutlined /> Prompt 模板管理</span>, label: <span><FileTextOutlined /> Prompt 模板管理</span>,
children: ( 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' }}> <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Text type="secondary">管理各场景的 AI Prompt 模板</Text> <Text type="secondary">管理各场景的 AI Prompt 模板</Text>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddTemplate}>新建模板</Button> <Button type="primary" icon={<PlusOutlined />} onClick={handleAddTemplate}>新建模板</Button>
...@@ -400,7 +395,7 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -400,7 +395,7 @@ const AdminAIConfigPage: React.FC = () => {
key: 'safety', key: 'safety',
label: <span><SafetyOutlined /> 安全词过滤</span>, label: <span><SafetyOutlined /> 安全词过滤</span>,
children: ( 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 }} /> <Alert message="安全词过滤规则用于防止 AI 输出不安全或不合规的内容" type="info" showIcon style={{ marginBottom: 16 }} />
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}> <div style={{ textAlign: 'center', padding: 40, color: '#999' }}>
<ExclamationCircleOutlined style={{ fontSize: 48, marginBottom: 16 }} /> <ExclamationCircleOutlined style={{ fontSize: 48, marginBottom: 16 }} />
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
Badge, Collapse, Badge, Collapse,
} from 'antd'; } from 'antd';
import { import {
FileTextOutlined, SearchOutlined, ToolOutlined, RobotOutlined, SearchOutlined, ToolOutlined, RobotOutlined,
ThunderboltOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, CheckCircleOutlined, CloseCircleOutlined,
HistoryOutlined, FundOutlined, HistoryOutlined, FundOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
...@@ -246,50 +246,42 @@ export default function AILogsPage() { ...@@ -246,50 +246,42 @@ export default function AILogsPage() {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
{/* 统计卡片 */} {/* 统计卡片 */}
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={4}> <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 />} /> <Statistic title="总调用" value={aiStats?.total_calls ?? 0} prefix={<ThunderboltOutlined />} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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' }} /> <Statistic title="成功率" value={aiStats?.total_calls ? Math.round((aiStats.success_calls / aiStats.total_calls) * 100) : 0} suffix="%" valueStyle={{ color: '#52c41a' }} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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} /> <Statistic title="总 Tokens" value={aiStats?.total_tokens ?? 0} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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 />} /> <Statistic title="Agent 执行" value={aiStats?.agent_execs ?? 0} prefix={<RobotOutlined />} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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 />} /> <Statistic title="工具调用" value={aiStats?.tool_calls ?? 0} prefix={<ToolOutlined />} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <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' }} /> <Statistic title="模拟调用" value={aiStats?.mock_calls ?? 0} valueStyle={{ color: '#fa8c16' }} />
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
......
...@@ -210,30 +210,25 @@ const AdminCompliancePage: React.FC = () => { ...@@ -210,30 +210,25 @@ const AdminCompliancePage: React.FC = () => {
const generatedCount = reports.filter((r) => r.status === 'generated').length; const generatedCount = reports.filter((r) => r.status === 'generated').length;
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={6}> <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="条" /> <Statistic title="审计日志" value={logsTotal} prefix={<FileSearchOutlined />} suffix="条" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="项" /> <Statistic title="待处理" value={pendingCount} valueStyle={{ color: '#fa8c16' }} suffix="项" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="项" /> <Statistic title="已上报" value={submittedCount} suffix="项" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="项" /> <Statistic title="已生成" value={generatedCount} suffix="项" />
</Card> </Card>
</Col> </Col>
...@@ -244,7 +239,7 @@ const AdminCompliancePage: React.FC = () => { ...@@ -244,7 +239,7 @@ const AdminCompliancePage: React.FC = () => {
key: 'logs', key: 'logs',
label: <span><AuditOutlined /> 审计日志</span>, label: <span><AuditOutlined /> 审计日志</span>,
children: ( 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 }}> <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input placeholder="搜索操作内容" style={{ width: 180, borderRadius: 8 }} allowClear <Input placeholder="搜索操作内容" style={{ width: 180, borderRadius: 8 }} allowClear
value={searchKeyword} onChange={e => setSearchKeyword(e.target.value)} value={searchKeyword} onChange={e => setSearchKeyword(e.target.value)}
...@@ -262,16 +257,16 @@ const AdminCompliancePage: React.FC = () => { ...@@ -262,16 +257,16 @@ const AdminCompliancePage: React.FC = () => {
key: 'export', key: 'export',
label: <span><DownloadOutlined /> 数据导出</span>, label: <span><DownloadOutlined /> 数据导出</span>,
children: ( children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Row gutter={[16, 16]}> <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: 'users', title: '用户数据', desc: '患者和医生基本信息', icon: <UserOutlined style={{ fontSize: 24, color: '#52c41a' }} /> },
{ type: 'prescriptions', title: '处方数据', desc: '处方记录、用药统计', icon: <AuditOutlined style={{ fontSize: 24, color: '#0891B2' }} /> }, { type: 'prescriptions', title: '处方数据', desc: '处方记录、用药统计', icon: <AuditOutlined style={{ fontSize: 24, color: '#0891B2' }} /> },
{ type: 'logs', title: '系统日志', desc: '操作日志和审计记录', icon: <SettingOutlined style={{ fontSize: 24, color: '#fa8c16' }} /> }, { type: 'logs', title: '系统日志', desc: '操作日志和审计记录', icon: <SettingOutlined style={{ fontSize: 24, color: '#fa8c16' }} /> },
] as const).map((item) => ( ] as const).map((item) => (
<Col span={6} key={item.type}> <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); }}> onClick={() => { setExportType(item.type); setExportModalVisible(true); }}>
<div style={{ marginBottom: 10 }}>{item.icon}</div> <div style={{ marginBottom: 10 }}>{item.icon}</div>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{item.title}</div> <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{item.title}</div>
...@@ -287,7 +282,7 @@ const AdminCompliancePage: React.FC = () => { ...@@ -287,7 +282,7 @@ const AdminCompliancePage: React.FC = () => {
key: 'reports', key: 'reports',
label: <span><CloudUploadOutlined /> 监管上报</span>, label: <span><CloudUploadOutlined /> 监管上报</span>,
children: ( 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 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>管理合规报告,支持下载和上报</span> <span style={{ fontSize: 13, color: '#8c8c8c' }}>管理合规报告,支持下载和上报</span>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setGenerateModalVisible(true)}>生成新报告</Button> <Button type="primary" icon={<PlusOutlined />} onClick={() => setGenerateModalVisible(true)}>生成新报告</Button>
......
...@@ -111,7 +111,7 @@ const AdminConsultationsPage: React.FC = () => { ...@@ -111,7 +111,7 @@ const AdminConsultationsPage: React.FC = () => {
]; ];
return ( return (
<div style={{ padding: '20px 24px' }}> <div style={{ padding: '12px 16px' }}>
<ProTable<AdminConsultItem> <ProTable<AdminConsultItem>
headerTitle="问诊管理" headerTitle="问诊管理"
tooltip="查看和管理平台所有问诊记录" tooltip="查看和管理平台所有问诊记录"
......
...@@ -26,6 +26,22 @@ interface TrendItem { ...@@ -26,6 +26,22 @@ interface TrendItem {
revenue: number; 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 AdminDashboardPage: React.FC = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [stats, setStats] = useState<DashboardStats>({ const [stats, setStats] = useState<DashboardStats>({
...@@ -104,41 +120,36 @@ const AdminDashboardPage: React.FC = () => { ...@@ -104,41 +120,36 @@ const AdminDashboardPage: React.FC = () => {
}; };
// AI场景调用 TOP5 (水平柱状图) // AI场景调用 TOP5 (水平柱状图)
const sceneCounts = (aiStats as Record<string, unknown>).scene_counts as Record<string, number> | undefined; // 后端返回 [{scene: "xxx", count: 5}, ...] 数组
const sceneEntries = sceneCounts ? Object.entries(sceneCounts).sort((a, b) => b[1] - a[1]).slice(0, 5) : []; const sceneCountsRaw = (aiStats as Record<string, unknown>).scene_counts as { scene: string; count: number }[] | undefined;
const sceneTop5 = (sceneCountsRaw || []).slice(0, 5);
const aiSceneOption = { const aiSceneOption = {
tooltip: { trigger: 'axis' as const }, tooltip: { trigger: 'axis' as const },
xAxis: { type: 'value' as const, minInterval: 1 }, 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 }, 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 分布柱状图 // 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 = { const ai24hOption = {
tooltip: { trigger: 'axis' as const }, tooltip: { trigger: 'axis' as const },
xAxis: { type: 'category' as const, data: Array.from({ length: 24 }, (_, i) => `${i}:00`), axisLabel: { fontSize: 10, interval: 3 } }, 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 }, yAxis: { type: 'value' as const, minInterval: 1 },
grid: { left: 40, right: 12, top: 10, bottom: 24 }, 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 ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
{/* Row1: 核心指标 */} {/* Row1: 核心指标 */}
<Row gutter={[16, 16]}> <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.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.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: '' }, { 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 = () => { ...@@ -158,7 +169,7 @@ const AdminDashboardPage: React.FC = () => {
), ),
valueStyle: { fontSize: 24, fontWeight: 700, color: item.color }, valueStyle: { fontSize: 24, fontWeight: 700, color: item.color },
}} }}
style={{ borderRadius: 12, border: '1px solid #E0F2F1' }} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}
/> />
</Col> </Col>
))} ))}
...@@ -168,18 +179,18 @@ const AdminDashboardPage: React.FC = () => { ...@@ -168,18 +179,18 @@ const AdminDashboardPage: React.FC = () => {
<EChartsCard title="问诊趋势(近7天)" option={trendOption} loading={loading} height={260} /> <EChartsCard title="问诊趋势(近7天)" option={trendOption} loading={loading} height={260} />
{/* Row3: 三栏图表 */} {/* Row3: 三栏图表 */}
<Row gutter={[16, 16]}> <Row gutter={[16, 16]} align="stretch">
<Col xs={24} md={8}> <Col xs={24} md={8} style={{ display: 'flex' }}>
<EChartsCard title="问诊状态分布" option={doughnutOption} loading={loading} height={220} /> <EChartsCard title="问诊状态分布" option={doughnutOption} loading={loading} height={220} style={{ flex: 1 }} />
</Col> </Col>
<Col xs={24} md={8}> <Col xs={24} md={8} style={{ display: 'flex' }}>
<EChartsCard title="AI 场景调用 TOP5" option={aiSceneOption} loading={loading} height={220} /> <EChartsCard title="AI 场景调用 TOP5" option={aiSceneOption} loading={loading} height={220} style={{ flex: 1 }} />
</Col> </Col>
<Col xs={24} md={8}> <Col xs={24} md={8} style={{ display: 'flex' }}>
<ProCard <ProCard
title={<span style={{ fontSize: 14, fontWeight: 600 }}>平台概况</span>} title={<span style={{ fontSize: 14, fontWeight: 600 }}>平台概况</span>}
headerBordered headerBordered
style={{ borderRadius: 12, border: '1px solid #E0F2F1' }} style={{ borderRadius: 12, border: '1px solid #E5E8EF', flex: 1 }}
bodyStyle={{ padding: '12px 16px' }} bodyStyle={{ padding: '12px 16px' }}
> >
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
...@@ -195,7 +206,7 @@ const AdminDashboardPage: React.FC = () => { ...@@ -195,7 +206,7 @@ const AdminDashboardPage: React.FC = () => {
<Text style={{ fontSize: 12 }}>累计问诊</Text> <Text style={{ fontSize: 12 }}>累计问诊</Text>
<Text strong style={{ fontSize: 12 }}>{stats.total_consultations.toLocaleString()}</Text> <Text strong style={{ fontSize: 12 }}>{stats.total_consultations.toLocaleString()}</Text>
</div> </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> <div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
...@@ -233,7 +244,7 @@ const AdminDashboardPage: React.FC = () => { ...@@ -233,7 +244,7 @@ const AdminDashboardPage: React.FC = () => {
<ProCard <ProCard
title={<span style={{ fontSize: 14, fontWeight: 600 }}>待审核医生 <Tag color="orange" style={{ marginLeft: 6 }}>{stats.pending_doctor_reviews}</Tag></span>} title={<span style={{ fontSize: 14, fontWeight: 600 }}>待审核医生 <Tag color="orange" style={{ marginLeft: 6 }}>{stats.pending_doctor_reviews}</Tag></span>}
headerBordered headerBordered
style={{ borderRadius: 12, border: '1px solid #E0F2F1', height: '100%' }} style={{ borderRadius: 12, border: '1px solid #E5E8EF', height: '100%' }}
> >
<List <List
dataSource={recentReviews} dataSource={recentReviews}
...@@ -261,11 +272,11 @@ const AdminDashboardPage: React.FC = () => { ...@@ -261,11 +272,11 @@ const AdminDashboardPage: React.FC = () => {
{[ {[
{ icon: <CheckCircleOutlined />, color: '#52c41a', bg: '#f6ffed', label: '累计问诊', value: stats.total_consultations.toLocaleString() }, { 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: <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 }, { icon: <AuditOutlined />, color: '#0891B2', bg: '#ECFEFF', label: '待审核', value: stats.pending_doctor_reviews },
].map((item, i) => ( ].map((item, i) => (
<Col xs={12} sm={6} key={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={{ 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: 12, color: '#8c8c8c', marginBottom: 4 }}>{item.label}</div>
<div style={{ fontSize: 20, fontWeight: 700, color: item.color }}>{item.value}</div> <div style={{ fontSize: 20, fontWeight: 700, color: item.color }}>{item.value}</div>
......
...@@ -113,12 +113,7 @@ const AdminDepartmentsPage: React.FC = () => { ...@@ -113,12 +113,7 @@ const AdminDepartmentsPage: React.FC = () => {
); );
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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<Department> <ProTable<Department>
headerTitle="科室列表" headerTitle="科室列表"
rowKey="id" rowKey="id"
......
...@@ -334,11 +334,6 @@ const AdminDoctorsPage: React.FC = () => { ...@@ -334,11 +334,6 @@ const AdminDoctorsPage: React.FC = () => {
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <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> <ProTable<DoctorItem>
headerTitle="医生列表" headerTitle="医生列表"
tooltip="管理平台注册医生,审核资质认证" tooltip="管理平台注册医生,审核资质认证"
......
...@@ -84,7 +84,7 @@ export default function KnowledgePage() { ...@@ -84,7 +84,7 @@ export default function KnowledgePage() {
const colColumns = [ const colColumns = [
{ {
title: '集合名称', dataIndex: 'name', key: 'name', 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, title: '类别', dataIndex: 'category', key: 'category', width: 120,
...@@ -135,14 +135,9 @@ export default function KnowledgePage() { ...@@ -135,14 +135,9 @@ export default function KnowledgePage() {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
{/* 操作栏 */} {/* 操作栏 */}
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Button icon={<ReloadOutlined />} onClick={() => { fetchCollections(); fetchDocuments(); }}>刷新</Button> <Button icon={<ReloadOutlined />} onClick={() => { fetchCollections(); fetchDocuments(); }}>刷新</Button>
<Button icon={<SearchOutlined />} onClick={() => { setSearchResults([]); setSearchModal(true); }}>检索测试</Button> <Button icon={<SearchOutlined />} onClick={() => { setSearchResults([]); setSearchModal(true); }}>检索测试</Button>
...@@ -152,7 +147,7 @@ export default function KnowledgePage() { ...@@ -152,7 +147,7 @@ export default function KnowledgePage() {
</Card> </Card>
{/* 内容区 */} {/* 内容区 */}
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs <Tabs
items={[ items={[
{ {
......
This diff is collapsed.
...@@ -3,13 +3,11 @@ ...@@ -3,13 +3,11 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react'; import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { import {
Card, Button, Modal, Tree, Tag, Space, Spin, App, Card, Button, Modal, Tree, Tag, Space, Spin, App,
Dropdown,
} from 'antd'; } from 'antd';
import { import {
AppstoreOutlined, ReloadOutlined,
SaveOutlined, CheckSquareOutlined, MinusSquareOutlined, SaveOutlined, CheckSquareOutlined, MinusSquareOutlined,
PlusOutlined, EditOutlined, DeleteOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
EyeOutlined, EyeInvisibleOutlined, MoreOutlined, EyeOutlined, EyeInvisibleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
DrawerForm, ProFormText, ProFormDigit, ProFormSelect, ProFormSwitch, DrawerForm, ProFormText, ProFormDigit, ProFormSelect, ProFormSwitch,
...@@ -17,6 +15,7 @@ import { ...@@ -17,6 +15,7 @@ import {
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { menuApi, roleApi } from '@/api/rbac'; import { menuApi, roleApi } from '@/api/rbac';
import type { Menu, Role } from '@/api/rbac'; import type { Menu, Role } from '@/api/rbac';
import { AVAILABLE_ICONS, getIconComponent } from '@/utils/iconMapper';
type TreeDataNode = { type TreeDataNode = {
key: number; key: number;
...@@ -42,31 +41,17 @@ function menusToTreeData( ...@@ -42,31 +41,17 @@ function menusToTreeData(
key: m.id, key: m.id,
title: ( title: (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', paddingRight: 8 }}> <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.name}
{m.path && <span style={{ color: '#999', fontSize: 12, marginLeft: 8 }}>{m.path}</span>} {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={{ marginLeft: 6, fontSize: 11 }}>{m.type === 'directory' ? '目录' : m.type === 'button' ? '按钮' : '菜单'}</Tag>} {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={{ marginLeft: 6, fontSize: 11 }}><EyeInvisibleOutlined /> 隐藏</Tag>} {!m.visible && <Tag color="default" style={{ fontSize: 11 }}><EyeInvisibleOutlined /> 隐藏</Tag>}
</span> </span>
<Space size={4} style={{ flexShrink: 0 }} onClick={(e) => e.stopPropagation()}> <Space size={4} style={{ flexShrink: 0 }} onClick={(e) => e.stopPropagation()}>
<Dropdown <Button type="text" size="small" icon={<PlusOutlined />} onClick={() => onAddChild(m)} style={{ color: '#2B6DE8' }}>子菜单</Button>
menu={{ <Button type="text" size="small" icon={<EditOutlined />} onClick={() => onEdit(m)}>编辑</Button>
items: [ <Button type="text" size="small" icon={<DeleteOutlined />} onClick={() => onDelete(m)} danger>删除</Button>
{ 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>
</Space> </Space>
</div> </div>
), ),
...@@ -74,12 +59,37 @@ function menusToTreeData( ...@@ -74,12 +59,37 @@ function menusToTreeData(
})); }));
} }
// 将菜单树转为 TreeSelect 的 treeData // 收集某个菜单及其所有后代的 ID
function menusToSelectData(menus: Menu[]): any[] { function collectDescendantIds(menus: Menu[], id: number): Set<number> {
return menus.map((m) => ({ 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, value: m.id,
title: m.name, title: m.name,
children: m.children?.length ? menusToSelectData(m.children) : undefined, children: m.children?.length ? menusToSelectData(m.children, excludeIds) : undefined,
})); }));
} }
...@@ -124,7 +134,13 @@ export default function MenusPage() { ...@@ -124,7 +134,13 @@ export default function MenusPage() {
const [defaultParentId, setDefaultParentId] = useState<number | undefined>(undefined); const [defaultParentId, setDefaultParentId] = useState<number | undefined>(undefined);
const allMenuKeys = useMemo(() => collectAllKeys(menus), [menus]); 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) => { const handleEdit = useCallback((m: Menu) => {
setEditingMenu(m); setEditingMenu(m);
...@@ -233,8 +249,8 @@ export default function MenusPage() { ...@@ -233,8 +249,8 @@ export default function MenusPage() {
const selectedRole = roles.find((r) => r.id === selectedRoleId); const selectedRole = roles.find((r) => r.id === selectedRoleId);
// 表单字段 // 表单字段(接受 treeData 参数区分新增/编辑场景)
const menuFormFields = ( const renderFormFields = (treeData: any[]) => (
<> <>
<ProFormText <ProFormText
name="name" name="name"
...@@ -255,7 +271,7 @@ export default function MenusPage() { ...@@ -255,7 +271,7 @@ export default function MenusPage() {
placeholder="留空表示顶级菜单" placeholder="留空表示顶级菜单"
allowClear allowClear
fieldProps={{ fieldProps={{
treeData: parentTreeData, treeData,
treeDefaultExpandAll: true, treeDefaultExpandAll: true,
}} }}
/> />
...@@ -264,10 +280,23 @@ export default function MenusPage() { ...@@ -264,10 +280,23 @@ export default function MenusPage() {
label="路由路径" label="路由路径"
placeholder="如 /admin/menus" placeholder="如 /admin/menus"
/> />
<ProFormText <ProFormSelect
name="icon" name="icon"
label="图标" 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 <ProFormText
name="component" name="component"
...@@ -295,32 +324,10 @@ export default function MenusPage() { ...@@ -295,32 +324,10 @@ export default function MenusPage() {
); );
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 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>
{/* Main content */} {/* Main content */}
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ display: 'flex', gap: 16, minHeight: 500 }}> <div style={{ display: 'flex', gap: 16 }}>
{/* Left: Role list */} {/* Left: Role list */}
<Card <Card
title="角色列表" title="角色列表"
...@@ -358,6 +365,7 @@ export default function MenusPage() { ...@@ -358,6 +365,7 @@ export default function MenusPage() {
<Space> <Space>
<Button size="small" icon={<CheckSquareOutlined />} onClick={handleSelectAll}>全选</Button> <Button size="small" icon={<CheckSquareOutlined />} onClick={handleSelectAll}>全选</Button>
<Button size="small" icon={<MinusSquareOutlined />} onClick={handleDeselectAll}>全不选</Button> <Button size="small" icon={<MinusSquareOutlined />} onClick={handleDeselectAll}>全不选</Button>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => { setDefaultParentId(undefined); setAddVisible(true); }}>添加菜单</Button>
</Space> </Space>
) )
} }
...@@ -365,8 +373,9 @@ export default function MenusPage() { ...@@ -365,8 +373,9 @@ export default function MenusPage() {
<Spin spinning={menuLoading}> <Spin spinning={menuLoading}>
{treeData.length > 0 && selectedRoleId ? ( {treeData.length > 0 && selectedRoleId ? (
<> <>
<div style={{ maxHeight: 'calc(100vh - 300px)', overflowY: 'auto' }}> <div style={{ maxHeight: 'calc(100vh - 250px)', overflowY: 'auto' }}>
<Tree <Tree
className="menu-manage-tree"
checkable checkable
checkStrictly checkStrictly
defaultExpandAll defaultExpandAll
...@@ -430,7 +439,7 @@ export default function MenusPage() { ...@@ -430,7 +439,7 @@ export default function MenusPage() {
} }
}} }}
> >
{menuFormFields} {renderFormFields(parentTreeData)}
</DrawerForm> </DrawerForm>
{/* Edit Menu Drawer */} {/* Edit Menu Drawer */}
...@@ -470,7 +479,7 @@ export default function MenusPage() { ...@@ -470,7 +479,7 @@ export default function MenusPage() {
} }
}} }}
> >
{menuFormFields} {renderFormFields(editParentTreeData)}
</DrawerForm> </DrawerForm>
</div> </div>
); );
......
...@@ -146,30 +146,25 @@ const AdminPharmacyPage: React.FC = () => { ...@@ -146,30 +146,25 @@ const AdminPharmacyPage: React.FC = () => {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={6}> <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="种" /> <Statistic title="药品总数" value={stats.total_count} suffix="种" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="种" /> <Statistic title="库存充足" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="种" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="种" /> <Statistic title="库存预警" value={stats.low_stock_count} valueStyle={{ color: '#fa8c16' }} suffix="种" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="种" /> <Statistic title="缺货药品" value={stats.out_of_stock_count} valueStyle={{ color: '#f5222d' }} suffix="种" />
</Card> </Card>
</Col> </Col>
...@@ -185,7 +180,7 @@ const AdminPharmacyPage: React.FC = () => { ...@@ -185,7 +180,7 @@ const AdminPharmacyPage: React.FC = () => {
key: 'drugs', key: 'drugs',
label: <span><MedicineBoxOutlined /> 药品信息</span>, label: <span><MedicineBoxOutlined /> 药品信息</span>,
children: ( 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', justifyContent: 'space-between', gap: 8, marginBottom: 12 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索药品" style={{ width: 200, borderRadius: 8 }} allowClear <Input prefix={<SearchOutlined />} placeholder="搜索药品" style={{ width: 200, borderRadius: 8 }} allowClear
...@@ -207,7 +202,7 @@ const AdminPharmacyPage: React.FC = () => { ...@@ -207,7 +202,7 @@ const AdminPharmacyPage: React.FC = () => {
key: 'stock', key: 'stock',
label: <Badge count={stats.low_stock_count + stats.out_of_stock_count} offset={[10, 0]}>库存预警</Badge>, label: <Badge count={stats.low_stock_count + stats.out_of_stock_count} offset={[10, 0]}>库存预警</Badge>,
children: ( children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<ProTable search={false} columns={drugColumns} <ProTable search={false} columns={drugColumns}
dataSource={drugs.filter((d) => d.status !== 'normal')} dataSource={drugs.filter((d) => d.status !== 'normal')}
rowKey="id" pagination={false} loading={loading} size="small" /> rowKey="id" pagination={false} loading={loading} size="small" />
......
...@@ -126,30 +126,25 @@ const AdminPrescriptionPage: React.FC = () => { ...@@ -126,30 +126,25 @@ const AdminPrescriptionPage: React.FC = () => {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={6}> <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="张" /> <Statistic title="处方总数" value={stats.total_count} suffix="张" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="张" /> <Statistic title="合规" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="张" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="张" /> <Statistic title="预警" value={stats.warning_count} valueStyle={{ color: '#fa8c16' }} suffix="张" />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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="张" /> <Statistic title="异常" value={stats.rejected_count} valueStyle={{ color: '#f5222d' }} suffix="张" />
</Card> </Card>
</Col> </Col>
...@@ -159,7 +154,7 @@ const AdminPrescriptionPage: React.FC = () => { ...@@ -159,7 +154,7 @@ const AdminPrescriptionPage: React.FC = () => {
{ {
key: 'all', label: '全部处方', key: 'all', label: '全部处方',
children: ( 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 }}> <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索处方/患者/医生" style={{ width: 200, borderRadius: 8 }} allowClear <Input prefix={<SearchOutlined />} placeholder="搜索处方/患者/医生" style={{ width: 200, borderRadius: 8 }} allowClear
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setPage(1); fetchList(); }} /> value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setPage(1); fetchList(); }} />
...@@ -175,7 +170,7 @@ const AdminPrescriptionPage: React.FC = () => { ...@@ -175,7 +170,7 @@ const AdminPrescriptionPage: React.FC = () => {
key: 'warning', key: 'warning',
label: <Badge count={stats.warning_count + stats.rejected_count} offset={[10, 0]}>异常预警</Badge>, label: <Badge count={stats.warning_count + stats.rejected_count} offset={[10, 0]}>异常预警</Badge>,
children: ( 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 }} /> <Alert message="以下处方存在合规风险,请及时审查处理" type="warning" showIcon style={{ marginBottom: 16 }} />
<List dataSource={prescriptions.filter((d) => d.warning_level !== 'normal')} renderItem={(item) => ( <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> <Card style={{ marginBottom: 12, borderRadius: 8, borderLeft: `4px solid ${item.warning_level === 'rejected' ? '#f5222d' : '#fa8c16'}` }} hoverable>
......
...@@ -63,14 +63,8 @@ export default function RolesPage() { ...@@ -63,14 +63,8 @@ export default function RolesPage() {
]; ];
return ( 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 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> <span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {roles.length} 个角色</span>
</div> </div>
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
import { ProTable, DrawerForm, ProFormText, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components'; import { ProTable, DrawerForm, ProFormText, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
import { import {
PlusOutlined, DeleteOutlined, EditOutlined, ImportOutlined, PlusOutlined, DeleteOutlined, EditOutlined, ImportOutlined,
SafetyOutlined, FilterOutlined, FilterOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
...@@ -219,24 +219,16 @@ export default function SafetyPage() { ...@@ -219,24 +219,16 @@ export default function SafetyPage() {
] as ColumnsType<SafetyLog>; ] as ColumnsType<SafetyLog>;
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={[16, 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 #E5E8EF' }}><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 #E5E8EF' }}><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 #E5E8EF' }}><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 #E5E8EF' }}><Statistic title="替换次数" value={stats?.replaced ?? 0} valueStyle={{ color: '#2B6DE8' }} /></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?.warned ?? 0} valueStyle={{ color: '#fa8c16' }} /></Card></Col>
</Row> </Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
......
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
message, Row, Col, Statistic, Empty, message, Row, Col, Statistic, Empty,
} from 'antd'; } from 'antd';
import { import {
CheckOutlined, CloseOutlined, EyeOutlined, ClockCircleOutlined, CheckOutlined, CloseOutlined, EyeOutlined,
FilterOutlined, FilterOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
...@@ -163,39 +163,31 @@ export default function AdminTasksPage() { ...@@ -163,39 +163,31 @@ export default function AdminTasksPage() {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={6}> <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' }} /> <Statistic title="待处理" value={pendingCount} valueStyle={{ color: '#fa8c16' }} />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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' }} /> <Statistic title="已通过" value={approvedCount} valueStyle={{ color: '#52c41a' }} />
</Card> </Card>
</Col> </Col>
<Col span={6}> <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' }} /> <Statistic title="已拒绝" value={rejectedCount} valueStyle={{ color: '#ff4d4f' }} />
</Card> </Card>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Statistic title="总任务" value={tasks.length} /> <Statistic title="总任务" value={tasks.length} />
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Space style={{ marginBottom: 16 }}> <Space style={{ marginBottom: 16 }}>
<FilterOutlined /> <FilterOutlined />
<Select <Select
......
...@@ -60,7 +60,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI ...@@ -60,7 +60,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
const columns = [ const columns = [
{ {
title: '工具名称', dataIndex: 'name', key: 'name', 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 }, { title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, width: 280 },
{ {
...@@ -100,13 +100,13 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI ...@@ -100,13 +100,13 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
return ( 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" <ProTable search={false} dataSource={filtered} columns={columns} rowKey="name" loading={isLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: t => `共 ${t} 个工具` }} /> pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: t => `共 ${t} 个工具` }} />
</Card> </Card>
<Drawer <Drawer
title={<Space><CodeOutlined style={{ color: '#0D9488' }} /><span>工具详情</span></Space>} title={<Space><CodeOutlined style={{ color: '#2B6DE8' }} /><span>工具详情</span></Space>}
open={detailModal.open} open={detailModal.open}
onClose={() => setDetailModal({ open: false, tool: null })} onClose={() => setDetailModal({ open: false, tool: null })}
placement="right" placement="right"
...@@ -126,7 +126,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI ...@@ -126,7 +126,7 @@ export default function BuiltinToolsTab({ search, categoryFilter, CATEGORY_CONFI
{detailModal.tool && ( {detailModal.tool && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Descriptions column={1} bordered size="small"> <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="描述">{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="分类"><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> <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) { ...@@ -163,14 +163,14 @@ export default function HTTPToolsTab({ search }: Props) {
</Space> </Space>
</div> </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" <ProTable search={false} dataSource={tools} columns={columns} rowKey="id" loading={isLoading} size="small"
pagination={{ pageSize: 10, showTotal: t => `共 ${t} 个工具` }} /> pagination={{ pageSize: 10, showTotal: t => `共 ${t} 个工具` }} />
</Card> </Card>
{/* Create/Edit DrawerForm */} {/* Create/Edit DrawerForm */}
<DrawerForm <DrawerForm
title={<Space><ApiOutlined style={{ color: '#0D9488' }} />{editingId ? '编辑 HTTP 工具' : '新建 HTTP 工具'}</Space>} title={<Space><ApiOutlined style={{ color: '#2B6DE8' }} />{editingId ? '编辑 HTTP 工具' : '新建 HTTP 工具'}</Space>}
open={modalOpen} open={modalOpen}
onOpenChange={(open) => { setModalOpen(open); if (!open) form.resetFields(); }} onOpenChange={(open) => { setModalOpen(open); if (!open) form.resetFields(); }}
onFinish={async () => { handleSave(); return false; }} onFinish={async () => { handleSave(); return false; }}
......
...@@ -122,7 +122,7 @@ export default function SkillsTab({ search }: Props) { ...@@ -122,7 +122,7 @@ export default function SkillsTab({ search }: Props) {
<Col key={skill.skill_id} xs={24} sm={12} md={8} lg={6}> <Col key={skill.skill_id} xs={24} sm={12} md={8} lg={6}>
<Card <Card
size="small" 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 } }} styles={{ body: { padding: 16 } }}
hoverable hoverable
> >
......
...@@ -35,7 +35,7 @@ const CATEGORY_CONFIG: Record<string, { color: string; label: string; icon: Reac ...@@ -35,7 +35,7 @@ const CATEGORY_CONFIG: Record<string, { color: string; label: string; icon: Reac
export type CATEGORY_CONFIG_TYPE = typeof CATEGORY_CONFIG; export type CATEGORY_CONFIG_TYPE = typeof CATEGORY_CONFIG;
export const SOURCE_CONFIG = { export const SOURCE_CONFIG = {
builtin: { label: '内置', color: '#0D9488' }, builtin: { label: '内置', color: '#2B6DE8' },
http: { label: 'HTTP', color: '#fa8c16' }, http: { label: 'HTTP', color: '#fa8c16' },
} as const; } as const;
...@@ -93,19 +93,10 @@ export default function UnifiedToolsPage() { ...@@ -93,19 +93,10 @@ export default function UnifiedToolsPage() {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
{/* Filter bar */} {/* Filter bar */}
<div style={{ <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', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
}}> }}>
<Input <Input
......
...@@ -211,14 +211,9 @@ export default function WorkflowsPage() { ...@@ -211,14 +211,9 @@ export default function WorkflowsPage() {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<DeploymentUnitOutlined style={{ color: '#8c8c8c' }} /> <DeploymentUnitOutlined style={{ color: '#8c8c8c' }} />
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {workflows.length} 个工作流</span> <span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {workflows.length} 个工作流</span>
...@@ -229,7 +224,7 @@ export default function WorkflowsPage() { ...@@ -229,7 +224,7 @@ export default function WorkflowsPage() {
</Card> </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" <ProTable search={false} dataSource={workflows} columns={columns} rowKey="id" loading={tableLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: (t) => `共 ${t} 条` }} pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: (t) => `共 ${t} 条` }}
/> />
......
...@@ -155,17 +155,10 @@ const AIAssistPage: React.FC = () => { ...@@ -155,17 +155,10 @@ const AIAssistPage: React.FC = () => {
}; };
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Alert message="AI 辅助诊断仅供参考,最终诊断以医生临床判断为准" type="info" showIcon style={{ fontSize: 12 }} /> <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> <div className="text-xs font-semibold mb-1">症状描述</div>
<TextArea rows={3} placeholder="请输入患者的主诉和症状描述.." value={symptoms} <TextArea rows={3} placeholder="请输入患者的主诉和症状描述.." value={symptoms}
onChange={(e) => setSymptoms(e.target.value)} className="mb-2" size="small" /> onChange={(e) => setSymptoms(e.target.value)} className="mb-2" size="small" />
......
...@@ -103,8 +103,8 @@ const DoctorCertificationPage: React.FC = () => { ...@@ -103,8 +103,8 @@ const DoctorCertificationPage: React.FC = () => {
if (currentStep === 1) { if (currentStep === 1) {
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <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 #E0F2F1', padding: '40px 20px' }}> <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' }}> <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 /> <CheckCircleOutlined />
</div> </div>
...@@ -119,12 +119,7 @@ const DoctorCertificationPage: React.FC = () => { ...@@ -119,12 +119,7 @@ const DoctorCertificationPage: React.FC = () => {
} }
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Steps <Steps
current={currentStep} current={currentStep}
items={[ items={[
...@@ -135,7 +130,7 @@ const DoctorCertificationPage: React.FC = () => { ...@@ -135,7 +130,7 @@ const DoctorCertificationPage: React.FC = () => {
style={{ maxWidth: 600 }} 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 form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="license_no" label="执业证号" rules={[{ required: true, message: '请输入执业证书号' }]}> <Form.Item name="license_no" label="执业证号" rules={[{ required: true, message: '请输入执业证书号' }]}>
<Input placeholder="请输入医师执业证书号" /> <Input placeholder="请输入医师执业证书号" />
......
...@@ -140,13 +140,8 @@ const ChronicReviewPage: React.FC = () => { ...@@ -140,13 +140,8 @@ const ChronicReviewPage: React.FC = () => {
return ( return (
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<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' }}>
<Tabs defaultActiveKey="pending" items={[ <Tabs defaultActiveKey="pending" items={[
{ {
key: 'pending', key: 'pending',
......
...@@ -274,10 +274,10 @@ const AIPanel: React.FC<AIPanelProps> = ({ ...@@ -274,10 +274,10 @@ const AIPanel: React.FC<AIPanelProps> = ({
key={i} key={i}
style={{ style={{
fontSize: 12, marginTop: 6, paddingLeft: 8, 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助手'} {msg.role === 'user' ? '患者' : 'AI助手'}
</Text> </Text>
<div style={{ marginTop: 2, color: '#333', lineHeight: 1.5 }}>{msg.content}</div> <div style={{ marginTop: 2, color: '#333', lineHeight: 1.5 }}>{msg.content}</div>
...@@ -357,7 +357,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ ...@@ -357,7 +357,7 @@ const AIPanel: React.FC<AIPanelProps> = ({
<div style={{ <div style={{
height: '100%', height: '100%',
borderRadius: 12, borderRadius: 12,
border: '1px solid #E0F2F1', border: '1px solid #E5E8EF',
background: '#fff', background: '#fff',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
......
...@@ -242,20 +242,20 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ ...@@ -242,20 +242,20 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
padding: '8px 12px', background: '#f5f5f5', borderRadius: 8, padding: '8px 12px', background: '#f5f5f5', borderRadius: 8,
display: 'flex', alignItems: 'center', gap: 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={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 500 }}>文件</div> <div style={{ fontSize: 12, fontWeight: 500 }}>文件</div>
<div style={{ fontSize: 11, color: '#999' }}>点击下载</div> <div style={{ fontSize: 11, color: '#999' }}>点击下载</div>
</div> </div>
<a href={mediaUrl} target="_blank" rel="noopener noreferrer"> <a href={mediaUrl} target="_blank" rel="noopener noreferrer">
<DownloadOutlined style={{ fontSize: 16, color: '#0D9488' }} /> <DownloadOutlined style={{ fontSize: 16, color: '#2B9E6E' }} />
</a> </a>
</div> </div>
); );
case 'audio': case 'audio':
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <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 }} /> <audio controls src={mediaUrl} style={{ height: 32 }} />
</div> </div>
); );
...@@ -357,7 +357,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ ...@@ -357,7 +357,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
color: '#374151', background: '#f9fafb', border: '1px solid #f0f0f0', color: '#374151', background: '#f9fafb', border: '1px solid #f0f0f0',
lineHeight: 1.4, transition: 'background 0.15s', 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'; }} onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.background = '#f9fafb'; }}
> >
{tpl.content} {tpl.content}
...@@ -379,7 +379,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ ...@@ -379,7 +379,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
borderRadius: 12, borderRadius: 12,
border: '1px solid #E0F2F1', border: '1px solid #E5E8EF',
background: '#fff', background: '#fff',
overflow: 'hidden', overflow: 'hidden',
}}> }}>
...@@ -415,7 +415,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ ...@@ -415,7 +415,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
} }
</div> </div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}> <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_gender && `${activeConsult.patient_gender} · `}
{activeConsult.patient_age && `${activeConsult.patient_age}岁 · `} {activeConsult.patient_age && `${activeConsult.patient_age}岁 · `}
{activeConsult.chief_complaint {activeConsult.chief_complaint
...@@ -525,7 +525,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ ...@@ -525,7 +525,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
<> <>
{/* 对方正在输入提示 */} {/* 对方正在输入提示 */}
{typingUser && ( {typingUser && (
<div style={{ padding: '4px 12px', fontSize: 11, color: '#0D9488', background: '#F0FDFA' }}> <div style={{ padding: '4px 12px', fontSize: 11, color: '#2B9E6E', background: '#F0FFF5' }}>
{typingUser}正在输入... {typingUser}正在输入...
</div> </div>
)} )}
...@@ -571,7 +571,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ ...@@ -571,7 +571,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
beforeUpload={file => { handleUpload(file); return false; }} beforeUpload={file => { handleUpload(file); return false; }}
> >
<Tooltip title="发送图片"> <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> </Tooltip>
</Upload> </Upload>
<Upload <Upload
......
...@@ -56,8 +56,8 @@ const PatientCard: React.FC<{ ...@@ -56,8 +56,8 @@ const PatientCard: React.FC<{
onClick={() => !isWaiting && onSelect()} onClick={() => !isWaiting && onSelect()}
style={{ style={{
padding: '10px 12px', padding: '10px 12px',
background: isSelected ? '#F0FDFA' : '#fff', background: isSelected ? '#F0FFF5' : '#fff',
borderLeft: `3px solid ${isSelected ? '#0D9488' : riskColor}`, borderLeft: `3px solid ${isSelected ? '#2B9E6E' : riskColor}`,
borderBottom: '1px solid #f5f5f5', borderBottom: '1px solid #f5f5f5',
cursor: isWaiting ? 'default' : 'pointer', cursor: isWaiting ? 'default' : 'pointer',
transition: 'background 0.15s', transition: 'background 0.15s',
...@@ -93,7 +93,7 @@ const PatientCard: React.FC<{ ...@@ -93,7 +93,7 @@ const PatientCard: React.FC<{
</Text> </Text>
{patient.is_revisit && ( {patient.is_revisit && (
<Tooltip title={`复诊 · 第${patient.visit_count + 1}次就诊`}> <Tooltip title={`复诊 · 第${patient.visit_count + 1}次就诊`}>
<ThunderboltOutlined style={{ fontSize: 11, color: '#0D9488' }} /> <ThunderboltOutlined style={{ fontSize: 11, color: '#2B9E6E' }} />
</Tooltip> </Tooltip>
)} )}
</div> </div>
...@@ -102,7 +102,7 @@ const PatientCard: React.FC<{ ...@@ -102,7 +102,7 @@ const PatientCard: React.FC<{
: <MessageOutlined style={{ fontSize: 12, color: '#52c41a' }} /> : <MessageOutlined style={{ fontSize: 12, color: '#52c41a' }} />
} }
</div> </div>
<div style={{ fontSize: 10, color: '#0D9488', fontFamily: 'monospace', marginBottom: 2 }}> <div style={{ fontSize: 10, color: '#2B9E6E', fontFamily: 'monospace', marginBottom: 2 }}>
{patient.serial_number} {patient.serial_number}
</div> </div>
<div style={{ fontSize: 11, color: '#8c8c8c', marginBottom: 3 }}> <div style={{ fontSize: 11, color: '#8c8c8c', marginBottom: 3 }}>
...@@ -253,7 +253,7 @@ const PatientList: React.FC<PatientListProps> = ({ ...@@ -253,7 +253,7 @@ const PatientList: React.FC<PatientListProps> = ({
<div style={{ <div style={{
height: '100%', height: '100%',
borderRadius: 12, borderRadius: 12,
border: '1px solid #E0F2F1', border: '1px solid #E5E8EF',
background: '#fff', background: '#fff',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
......
...@@ -161,7 +161,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie ...@@ -161,7 +161,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie
const statusInfo = controlStatusMap[cr.control_status] || { text: cr.control_status, color: 'default' }; const statusInfo = controlStatusMap[cr.control_status] || { text: cr.control_status, color: 'default' };
return ( return (
<div key={cr.id} style={{ <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 }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Text strong style={{ fontSize: 13 }}>{cr.disease_name}</Text> <Text strong style={{ fontSize: 13 }}>{cr.disease_name}</Text>
...@@ -244,7 +244,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie ...@@ -244,7 +244,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie
<Space direction="vertical" style={{ width: '100%' }} size={6}> <Space direction="vertical" style={{ width: '100%' }} size={6}>
{profile.family_history.map((fm, i) => ( {profile.family_history.map((fm, i) => (
<div key={i} style={{ <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> <Text strong style={{ fontSize: 12 }}>{fm.name}</Text>
<Tag style={{ marginLeft: 6, fontSize: 10 }}>{fm.relation}</Tag> <Tag style={{ marginLeft: 6, fontSize: 10 }}>{fm.relation}</Tag>
...@@ -263,7 +263,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie ...@@ -263,7 +263,7 @@ const PatientProfileDrawer: React.FC<PatientProfileDrawerProps> = ({ open, patie
<Drawer <Drawer
title={ title={
<Space> <Space>
<UserOutlined style={{ color: '#0D9488' }} /> <UserOutlined style={{ color: '#2B9E6E' }} />
<span>患者画像</span> <span>患者画像</span>
{profile?.basic_info?.name && ( {profile?.basic_info?.name && (
<Tag color="blue">{profile.basic_info.name}</Tag> <Tag color="blue">{profile.basic_info.name}</Tag>
......
...@@ -91,7 +91,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -91,7 +91,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
> >
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Space> <Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#0D9488' }} /> <Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#2B9E6E' }} />
<Text strong>{consult.patient_name || '患者'}</Text> <Text strong>{consult.patient_name || '患者'}</Text>
<Tag color={consult.type === 'video' ? 'blue' : 'green'} style={{ fontSize: 11 }}> <Tag color={consult.type === 'video' ? 'blue' : 'green'} style={{ fontSize: 11 }}>
{consult.type === 'video' ? '视频' : '图文'} {consult.type === 'video' ? '视频' : '图文'}
...@@ -186,7 +186,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({ ...@@ -186,7 +186,7 @@ const WaitingQueue: React.FC<WaitingQueueProps> = ({
return ( return (
<Card <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 } }} styles={{ body: { padding: 12 } }}
size="small" size="small"
> >
......
...@@ -274,13 +274,13 @@ const ConsultPage: React.FC = () => { ...@@ -274,13 +274,13 @@ const ConsultPage: React.FC = () => {
const statItems = [ const statItems = [
{ label: '候诊中', value: waitingCount, color: '#fa8c16', icon: <ClockCircleOutlined />, unit: '人' }, { label: '候诊中', value: waitingCount, color: '#fa8c16', icon: <ClockCircleOutlined />, unit: '人' },
{ label: '接诊中', value: inProgressCount, color: '#52c41a', icon: <MessageOutlined />, 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: '元' }, { label: '今日收入', value: workbenchStats ? (workbenchStats.income_today / 100).toFixed(0) : '—', color: '#0891B2', icon: <DashboardOutlined />, unit: '元' },
]; ];
const efficiencyItems = workbenchStats ? [ const efficiencyItems = workbenchStats ? [
{ label: '平均候诊', value: workbenchStats.avg_wait_minutes.toFixed(1), unit: '分钟', icon: <FieldTimeOutlined />, color: '#fa8c16' }, { 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.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.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' }, { label: '今日转诊', value: workbenchStats.transfer_count, unit: '次', icon: <SwapOutlined />, color: '#13c2c2' },
...@@ -288,17 +288,10 @@ const ConsultPage: React.FC = () => { ...@@ -288,17 +288,10 @@ const ConsultPage: React.FC = () => {
] : []; ] : [];
return ( 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={{ flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}> <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' }}> <div style={{ marginLeft: 'auto', display: 'flex', gap: 10, alignItems: 'center' }}>
{statItems.map(({ label, value, color, icon, unit }) => ( {statItems.map(({ label, value, color, icon, unit }) => (
<div <div
...@@ -307,7 +300,7 @@ const ConsultPage: React.FC = () => { ...@@ -307,7 +300,7 @@ const ConsultPage: React.FC = () => {
background: '#fff', background: '#fff',
borderRadius: 10, borderRadius: 10,
padding: '8px 18px', padding: '8px 18px',
border: '1px solid #E0F2F1', border: '1px solid #E5E8EF',
minWidth: 100, minWidth: 100,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)', boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
}} }}
...@@ -326,10 +319,10 @@ const ConsultPage: React.FC = () => { ...@@ -326,10 +319,10 @@ const ConsultPage: React.FC = () => {
onClick={() => setShowEfficiency(!showEfficiency)} onClick={() => setShowEfficiency(!showEfficiency)}
style={{ style={{
cursor: 'pointer', padding: '6px 10px', borderRadius: 8, cursor: 'pointer', padding: '6px 10px', borderRadius: 8,
background: showEfficiency ? '#F0FDFA' : '#f5f5f5', background: showEfficiency ? '#F0FFF5' : '#f5f5f5',
border: `1px solid ${showEfficiency ? '#91d5ff' : '#e8e8e8'}`, border: `1px solid ${showEfficiency ? '#91d5ff' : '#e8e8e8'}`,
display: 'flex', alignItems: 'center', gap: 4, display: 'flex', alignItems: 'center', gap: 4,
fontSize: 12, color: showEfficiency ? '#0D9488' : '#8c8c8c', fontSize: 12, color: showEfficiency ? '#2B9E6E' : '#8c8c8c',
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
> >
......
...@@ -107,17 +107,10 @@ const IncomePage: React.FC = () => { ...@@ -107,17 +107,10 @@ const IncomePage: React.FC = () => {
return ( return (
<Spin spinning={loading}> <Spin spinning={loading}>
<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 }}>收入统计</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您的问诊收入与提现记录</div>
</div>
</div>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={8}> <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={{ 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: 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> <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 = () => { ...@@ -126,7 +119,7 @@ const IncomePage: React.FC = () => {
</div> </div>
</Col> </Col>
<Col span={8}> <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: 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: 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 }}> <div style={{ fontSize: 12, color: '#bfbfbf', display: 'flex', alignItems: 'center', gap: 4 }}>
...@@ -135,9 +128,9 @@ const IncomePage: React.FC = () => { ...@@ -135,9 +128,9 @@ const IncomePage: React.FC = () => {
</div> </div>
</Col> </Col>
<Col span={8}> <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: 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> {stats.month_consults} <span style={{ fontSize: 14, color: '#bfbfbf', fontWeight: 400 }}>次</span>
</div> </div>
<div style={{ fontSize: 12, color: '#bfbfbf' }}>日均 {(stats.month_consults / Math.max(1, new Date().getDate())).toFixed(1)} 次</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 = () => { ...@@ -152,7 +145,7 @@ const IncomePage: React.FC = () => {
key: 'records', key: 'records',
label: <span><FileTextOutlined /> 收入明细</span>, label: <span><FileTextOutlined /> 收入明细</span>,
children: ( children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Space> <Space>
<RangePicker value={dateRange} onChange={(dates) => setDateRange(dates as any)} /> <RangePicker value={dateRange} onChange={(dates) => setDateRange(dates as any)} />
...@@ -200,7 +193,7 @@ const IncomePage: React.FC = () => { ...@@ -200,7 +193,7 @@ const IncomePage: React.FC = () => {
<Text type="secondary">图文问诊</Text> <Text type="secondary">图文问诊</Text>
<Text>¥{(bill.text_income / 100).toFixed(0)}</Text> <Text>¥{(bill.text_income / 100).toFixed(0)}</Text>
</Space> </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>
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}> <Space style={{ width: '100%', justifyContent: 'space-between' }}>
...@@ -227,7 +220,7 @@ const IncomePage: React.FC = () => { ...@@ -227,7 +220,7 @@ const IncomePage: React.FC = () => {
key: 'withdraw', key: 'withdraw',
label: <span><BankOutlined /> 提现记录</span>, label: <span><BankOutlined /> 提现记录</span>,
children: ( children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Table <Table
dataSource={withdrawals} dataSource={withdrawals}
rowKey="id" rowKey="id"
......
This diff is collapsed.
...@@ -140,18 +140,13 @@ const PatientRecordPage: React.FC = () => { ...@@ -140,18 +140,13 @@ const PatientRecordPage: React.FC = () => {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={16} align="stretch"> <Row gutter={16} align="stretch">
<Col span={8}> <Col span={8}>
<Card <Card
title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>} title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>}
extra={<Text type="secondary" style={{ fontSize: 13 }}>{total} 人</Text>} 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="搜索姓名/手机" <Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机"
value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)} value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}
...@@ -205,7 +200,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -205,7 +200,7 @@ const PatientRecordPage: React.FC = () => {
<Spin spinning={detailLoading}> <Spin spinning={detailLoading}>
{selectedPatient ? ( {selectedPatient ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <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 title={<span style={{ fontSize: 14, fontWeight: 600 }}>基本信息</span>} bordered size="small" column={3}>
<Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item> <Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item>
<Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item> <Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item>
...@@ -222,7 +217,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -222,7 +217,7 @@ const PatientRecordPage: React.FC = () => {
</Descriptions> </Descriptions>
</Card> </Card>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs defaultActiveKey="consult" items={[ <Tabs defaultActiveKey="consult" items={[
{ {
key: 'consult', key: 'consult',
...@@ -254,7 +249,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -254,7 +249,7 @@ const PatientRecordPage: React.FC = () => {
</Card> </Card>
</div> </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="请从左侧选择患者查看档案" /> <Empty description="请从左侧选择患者查看档案" />
</Card> </Card>
)} )}
......
...@@ -140,18 +140,13 @@ const PatientRecordPage: React.FC = () => { ...@@ -140,18 +140,13 @@ const PatientRecordPage: React.FC = () => {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', 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>
<Row gutter={16} align="stretch"> <Row gutter={16} align="stretch">
<Col span={8}> <Col span={8}>
<Card <Card
title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>} title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>}
extra={<Text type="secondary" style={{ fontSize: 13 }}>{total} 人</Text>} 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="搜索姓名/手机" <Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机"
value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)} value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}
...@@ -205,7 +200,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -205,7 +200,7 @@ const PatientRecordPage: React.FC = () => {
<Spin spinning={detailLoading}> <Spin spinning={detailLoading}>
{selectedPatient ? ( {selectedPatient ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <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 title={<span style={{ fontSize: 14, fontWeight: 600 }}>基本信息</span>} bordered size="small" column={3}>
<Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item> <Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item>
<Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item> <Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item>
...@@ -222,7 +217,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -222,7 +217,7 @@ const PatientRecordPage: React.FC = () => {
</Descriptions> </Descriptions>
</Card> </Card>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Tabs defaultActiveKey="consult" items={[ <Tabs defaultActiveKey="consult" items={[
{ {
key: 'consult', key: 'consult',
...@@ -254,7 +249,7 @@ const PatientRecordPage: React.FC = () => { ...@@ -254,7 +249,7 @@ const PatientRecordPage: React.FC = () => {
</Card> </Card>
</div> </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="请从左侧选择患者查看档案" /> <Empty description="请从左侧选择患者查看档案" />
</Card> </Card>
)} )}
......
...@@ -64,7 +64,7 @@ const DoctorProfilePage: React.FC = () => { ...@@ -64,7 +64,7 @@ const DoctorProfilePage: React.FC = () => {
if (!profile) return null; if (!profile) return null;
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Hero 卡片 */} {/* Hero 卡片 */}
<div style={{ <div style={{
borderRadius: 12, overflow: 'hidden', borderRadius: 12, overflow: 'hidden',
...@@ -96,7 +96,7 @@ const DoctorProfilePage: React.FC = () => { ...@@ -96,7 +96,7 @@ const DoctorProfilePage: React.FC = () => {
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col xs={24} md={12}> <Col xs={24} md={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>详细信息</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>详细信息</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Descriptions column={2} size="small"> <Descriptions column={2} size="small">
<Descriptions.Item label="姓名">{profile.name}</Descriptions.Item> <Descriptions.Item label="姓名">{profile.name}</Descriptions.Item>
<Descriptions.Item label="职称">{profile.title}</Descriptions.Item> <Descriptions.Item label="职称">{profile.title}</Descriptions.Item>
...@@ -111,16 +111,16 @@ const DoctorProfilePage: React.FC = () => { ...@@ -111,16 +111,16 @@ const DoctorProfilePage: React.FC = () => {
</Col> </Col>
<Col xs={24} md={12}> <Col xs={24} md={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>接诊设置</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>接诊设置</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #E5E8EF' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#f8faff', borderRadius: 10, border: '1px solid #E0F2F1' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#f8faff', borderRadius: 10, border: '1px solid #E5E8EF' }}>
<div> <div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>AI 辅助接诊</div> <div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>AI 辅助接诊</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后 AI 将协助分析患者症状</div> <div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后 AI 将协助分析患者症状</div>
</div> </div>
<Switch checked={profile.ai_assist_enabled} onChange={handleToggleAI} /> <Switch checked={profile.ai_assist_enabled} onChange={handleToggleAI} />
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#f8faff', borderRadius: 10, border: '1px solid #E0F2F1' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#f8faff', borderRadius: 10, border: '1px solid #E5E8EF' }}>
<div> <div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>自动接诊</div> <div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>自动接诊</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后图文问诊将自动接诊</div> <div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后图文问诊将自动接诊</div>
...@@ -138,7 +138,7 @@ const DoctorProfilePage: React.FC = () => { ...@@ -138,7 +138,7 @@ const DoctorProfilePage: React.FC = () => {
</Col> </Col>
</Row> </Row>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>擅长领域</span>} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>擅长领域</span>} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Space wrap size={8}> <Space wrap size={8}>
{(profile.specialties || []).map((s, i) => ( {(profile.specialties || []).map((s, i) => (
<Tag key={i} color="processing" style={{ borderRadius: 20, padding: '4px 12px', fontSize: 13 }}>{s}</Tag> <Tag key={i} color="processing" style={{ borderRadius: 20, padding: '4px 12px', fontSize: 13 }}>{s}</Tag>
...@@ -146,7 +146,7 @@ const DoctorProfilePage: React.FC = () => { ...@@ -146,7 +146,7 @@ const DoctorProfilePage: React.FC = () => {
</Space> </Space>
</Card> </Card>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>个人简介</span>} style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>个人简介</span>} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Text style={{ fontSize: 14, color: '#595959', lineHeight: 1.7 }}>{profile.introduction}</Text> <Text style={{ fontSize: 14, color: '#595959', lineHeight: 1.7 }}>{profile.introduction}</Text>
</Card> </Card>
......
...@@ -92,19 +92,15 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -92,19 +92,15 @@ const DoctorSchedulePage: React.FC = () => {
}; };
return ( 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 style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>排班管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理您的接诊时间段安排</div>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule} style={{ borderRadius: 8 }}>添加排班</Button> <Button type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule} style={{ borderRadius: 8 }}>添加排班</Button>
</div> </div>
<Spin spinning={loading}> <Spin spinning={loading}>
<Row gutter={16}> <Row gutter={16}>
<Col span={16}> <Col span={16}>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<Calendar <Calendar
fullscreen={false} fullscreen={false}
value={selectedDate} value={selectedDate}
...@@ -118,12 +114,12 @@ const DoctorSchedulePage: React.FC = () => { ...@@ -118,12 +114,12 @@ const DoctorSchedulePage: React.FC = () => {
<Card <Card
title={ title={
<span style={{ fontSize: 14, fontWeight: 600 }}> <span style={{ fontSize: 14, fontWeight: 600 }}>
<CalendarOutlined style={{ marginRight: 6, color: '#0D9488' }} /> <CalendarOutlined style={{ marginRight: 6, color: '#2B9E6E' }} />
{selectedDate.format('MM月DD日')} 排班 {selectedDate.format('MM月DD日')} 排班
</span> </span>
} }
extra={<Button type="link" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加</Button>} extra={<Button type="link" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加</Button>}
style={{ borderRadius: 12, border: '1px solid #E0F2F1' }} style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}
> >
{todaySchedule.length === 0 ? ( {todaySchedule.length === 0 ? (
<div style={{ textAlign: 'center', padding: '32px 0' }}> <div style={{ textAlign: 'center', padding: '32px 0' }}>
......
...@@ -165,16 +165,8 @@ export default function DoctorTasksPage() { ...@@ -165,16 +165,8 @@ export default function DoctorTasksPage() {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<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' }}>
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
......
...@@ -125,10 +125,7 @@ const DoctorWorkbenchPage: React.FC = () => { ...@@ -125,10 +125,7 @@ const DoctorWorkbenchPage: React.FC = () => {
if (!isVerified) { if (!isVerified) {
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生工作台</h2>
</div>
<Alert <Alert
message="请先完成资质认证" message="请先完成资质认证"
description={ description={
...@@ -149,14 +146,10 @@ const DoctorWorkbenchPage: React.FC = () => { ...@@ -149,14 +146,10 @@ const DoctorWorkbenchPage: React.FC = () => {
} }
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Header */} {/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div> <Button type="primary" style={{ borderRadius: 8, marginLeft: 'auto' }} onClick={() => router.push('/doctor/consult')}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生工作台</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>每10秒自动刷新数据</div>
</div>
<Button type="primary" style={{ borderRadius: 8 }} onClick={() => router.push('/doctor/consult')}>
进入问诊大厅 进入问诊大厅
</Button> </Button>
</div> </div>
...@@ -164,7 +157,7 @@ const DoctorWorkbenchPage: React.FC = () => { ...@@ -164,7 +157,7 @@ const DoctorWorkbenchPage: React.FC = () => {
{/* Row1: 核心指标 */} {/* Row1: 核心指标 */}
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{[ {[
{ title: '今日问诊', value: stats.today_consult_count, icon: <MessageOutlined />, color: '#0D9488', bg: '#F0FDFA', suffix: '人次' }, { title: '今日问诊', value: stats.today_consult_count, icon: <MessageOutlined />, color: '#2B9E6E', bg: '#F0FFF5', suffix: '人次' },
{ title: '等待接诊', value: stats.waiting_count, icon: <ClockCircleOutlined />, color: '#fa8c16', bg: '#fff7e6', suffix: '人' }, { title: '等待接诊', value: stats.waiting_count, icon: <ClockCircleOutlined />, color: '#fa8c16', bg: '#fff7e6', suffix: '人' },
{ title: '今日完成', value: stats.completed_today, icon: <CheckCircleOutlined />, color: '#52c41a', bg: '#f6ffed', suffix: '例' }, { title: '今日完成', value: stats.completed_today, icon: <CheckCircleOutlined />, color: '#52c41a', bg: '#f6ffed', suffix: '例' },
{ title: '今日收入', value: stats.income_today, icon: <DollarOutlined />, color: '#0891B2', bg: '#ECFEFF', suffix: '元' }, { title: '今日收入', value: stats.income_today, icon: <DollarOutlined />, color: '#0891B2', bg: '#ECFEFF', suffix: '元' },
...@@ -172,7 +165,7 @@ const DoctorWorkbenchPage: React.FC = () => { ...@@ -172,7 +165,7 @@ const DoctorWorkbenchPage: React.FC = () => {
<Col xs={12} sm={6} key={i}> <Col xs={12} sm={6} key={i}>
<div style={{ <div style={{
background: '#fff', borderRadius: 12, padding: '16px 20px', background: '#fff', borderRadius: 12, padding: '16px 20px',
border: '1px solid #E0F2F1', transition: 'box-shadow 0.2s', border: '1px solid #E5E8EF', transition: 'box-shadow 0.2s',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, background: item.bg, color: item.color }}>{item.icon}</div> <div style={{ width: 36, height: 36, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, background: item.bg, color: item.color }}>{item.icon}</div>
...@@ -203,7 +196,7 @@ const DoctorWorkbenchPage: React.FC = () => { ...@@ -203,7 +196,7 @@ const DoctorWorkbenchPage: React.FC = () => {
title={<span style={{ fontSize: 14, fontWeight: 600 }}>等待接诊 <Badge count={stats.waiting_count} style={{ backgroundColor: '#fa8c16', marginLeft: 4 }} /></span>} title={<span style={{ fontSize: 14, fontWeight: 600 }}>等待接诊 <Badge count={stats.waiting_count} style={{ backgroundColor: '#fa8c16', marginLeft: 4 }} /></span>}
extra={<Button type="link" onClick={() => router.push('/doctor/consult')}>查看全部</Button>} extra={<Button type="link" onClick={() => router.push('/doctor/consult')}>查看全部</Button>}
loading={loading} loading={loading}
style={{ borderRadius: 12, border: '1px solid #E0F2F1', height: '100%' }} style={{ borderRadius: 12, border: '1px solid #E5E8EF', height: '100%' }}
> >
<List <List
dataSource={waitingPatients} dataSource={waitingPatients}
...@@ -240,7 +233,7 @@ const DoctorWorkbenchPage: React.FC = () => { ...@@ -240,7 +233,7 @@ const DoctorWorkbenchPage: React.FC = () => {
<Col xs={24} md={10}> <Col xs={24} md={10}>
<Card <Card
title={<span style={{ fontSize: 14, fontWeight: 600 }}>综合数据</span>} title={<span style={{ fontSize: 14, fontWeight: 600 }}>综合数据</span>}
style={{ borderRadius: 12, border: '1px solid #E0F2F1', height: '100%' }} style={{ borderRadius: 12, border: '1px solid #E5E8EF', height: '100%' }}
styles={{ body: { padding: '12px 16px' } }} styles={{ body: { padding: '12px 16px' } }}
> >
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
......
...@@ -458,7 +458,7 @@ const MetricsTab: React.FC = () => { ...@@ -458,7 +458,7 @@ const MetricsTab: React.FC = () => {
{ {
title: '数值', key: 'value', title: '数值', key: 'value',
render: (_: any, r: HealthMetric) => ( render: (_: any, r: HealthMetric) => (
<Text strong style={{ color: '#0D9488' }}> <Text strong style={{ color: '#2B6DE8' }}>
{meta.hasValue2 ? `${r.value1}/${r.value2}` : r.value1} {r.unit} {meta.hasValue2 ? `${r.value1}/${r.value2}` : r.value1} {r.unit}
</Text> </Text>
), ),
...@@ -511,7 +511,7 @@ const MetricsTab: React.FC = () => { ...@@ -511,7 +511,7 @@ const MetricsTab: React.FC = () => {
<Col span={8}> <Col span={8}>
<Card size="small" style={{ borderRadius: 8, background: '#f0f5ff' }}> <Card size="small" style={{ borderRadius: 8, background: '#f0f5ff' }}>
<Statistic title="最新值" value={meta.hasValue2 ? `${latest.value1}/${latest.value2}` : latest.value1} <Statistic title="最新值" value={meta.hasValue2 ? `${latest.value1}/${latest.value2}` : latest.value1}
suffix={meta.unit} valueStyle={{ color: '#0D9488' }} /> suffix={meta.unit} valueStyle={{ color: '#2B6DE8' }} />
</Card> </Card>
</Col> </Col>
<Col span={8}> <Col span={8}>
...@@ -574,12 +574,8 @@ const PatientChronicPage: React.FC = () => { ...@@ -574,12 +574,8 @@ const PatientChronicPage: React.FC = () => {
]; ];
return ( return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div> <Card style={{ borderRadius: 12, border: '1px solid #E5E8EF' }}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>慢病管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理您的慢性病档案、续方申请、用药提醒和健康指标</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<Tabs items={tabItems} /> <Tabs items={tabItems} />
</Card> </Card>
</div> </div>
......
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.
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.
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