Commit caca4568 authored by yuguo's avatar yuguo

fix

parent fbf70318
...@@ -59,6 +59,11 @@ func main() { ...@@ -59,6 +59,11 @@ func main() {
log.Println("开始执行数据库表迁移...") log.Println("开始执行数据库表迁移...")
db := database.GetDB() db := database.GetDB()
// 修复:workflow_executions.trigger_by 从 uuid 改为 varchar(支持 "system" 等非 UUID 值)
if db.Migrator().HasTable("workflow_executions") {
db.Exec("ALTER TABLE workflow_executions ALTER COLUMN trigger_by TYPE VARCHAR(100)")
}
// 修复:给已有 consultations 记录补充 serial_number(迁移 NOT NULL 前必须) // 修复:给已有 consultations 记录补充 serial_number(迁移 NOT NULL 前必须)
if db.Migrator().HasTable("consultations") { if db.Migrator().HasTable("consultations") {
if !db.Migrator().HasColumn(&model.Consultation{}, "serial_number") { if !db.Migrator().HasColumn(&model.Consultation{}, "serial_number") {
......
...@@ -9,7 +9,7 @@ import ( ...@@ -9,7 +9,7 @@ import (
// currentPromptVersion 当前代码中提示词模板的版本号 // currentPromptVersion 当前代码中提示词模板的版本号
// 每次修改提示词内容时递增此值,ensurePromptTemplates 会自动同步到数据库 // 每次修改提示词内容时递增此值,ensurePromptTemplates 会自动同步到数据库
const currentPromptVersion = 3 const currentPromptVersion = 5
// ensurePromptTemplates 确保所有内置提示词模板存在于数据库中(种子数据) // ensurePromptTemplates 确保所有内置提示词模板存在于数据库中(种子数据)
// 逻辑:不存在则创建;已存在但版本低于代码版本则更新内容 // 逻辑:不存在则创建;已存在但版本低于代码版本则更新内容
...@@ -264,19 +264,16 @@ func ensurePromptTemplates() { ...@@ -264,19 +264,16 @@ func ensurePromptTemplates() {
Name: "鉴别诊断分析", Name: "鉴别诊断分析",
Scene: "consult_diagnosis", Scene: "consult_diagnosis",
TemplateType: "system", TemplateType: "system",
Content: `你是一位经验丰富的临床医生AI助手,请根据以下患者信息进行鉴别诊断分析。 Content: `你是一位经验丰富的临床医生AI助手,请根据本次就诊信息进行鉴别诊断分析。
## 患者信息 ## 本次就诊信息
- 主诉:{{chief_complaint}} {{consult_context}}
{{#pre_consult_analysis}}- 预问诊分析:{{pre_consult_analysis}}{{/pre_consult_analysis}}
{{#allergy_history}}- 过敏史:{{allergy_history}}{{/allergy_history}}
{{#chronic_diseases}}- 慢性病史:{{chronic_diseases}}{{/chronic_diseases}}
## 对话记录 ## 医患对话记录
{{chat_history}} {{chat_history}}
## 要求 ## 要求
请按以下格式给出鉴别诊断建议: 结合上述就诊信息和对话记录,按以下格式给出鉴别诊断建议:
### 初步诊断 ### 初步诊断
列出最可能的诊断(按可能性从高到低排列) 列出最可能的诊断(按可能性从高到低排列)
...@@ -302,19 +299,16 @@ func ensurePromptTemplates() { ...@@ -302,19 +299,16 @@ func ensurePromptTemplates() {
Name: "用药建议分析", Name: "用药建议分析",
Scene: "consult_medication", Scene: "consult_medication",
TemplateType: "system", TemplateType: "system",
Content: `你是一位经验丰富的临床药师AI助手,请根据以下患者信息给出用药建议。 Content: `你是一位经验丰富的临床药师AI助手,请根据本次就诊信息给出用药建议。
## 患者信息 ## 本次就诊信息
- 主诉:{{chief_complaint}} {{consult_context}}
{{#pre_consult_analysis}}- 预问诊分析:{{pre_consult_analysis}}{{/pre_consult_analysis}}
{{#allergy_history}}- 过敏史:{{allergy_history}}{{/allergy_history}}
{{#chronic_diseases}}- 慢性病史:{{chronic_diseases}}{{/chronic_diseases}}
## 对话记录 ## 医患对话记录
{{chat_history}} {{chat_history}}
## 要求 ## 要求
请按以下格式给出用药建议: 结合上述就诊信息和对话记录,按以下格式给出用药建议:
### 推荐用药方案 ### 推荐用药方案
......
...@@ -24,7 +24,7 @@ type WorkflowExecution struct { ...@@ -24,7 +24,7 @@ type WorkflowExecution struct {
WorkflowID string `gorm:"type:varchar(100);index" json:"workflow_id"` WorkflowID string `gorm:"type:varchar(100);index" json:"workflow_id"`
Version int `json:"version"` Version int `json:"version"`
TriggerType string `gorm:"type:varchar(50)" json:"trigger_type"` TriggerType string `gorm:"type:varchar(50)" json:"trigger_type"`
TriggerBy string `gorm:"type:uuid" json:"trigger_by"` TriggerBy string `gorm:"type:varchar(100)" json:"trigger_by"`
Input string `gorm:"type:jsonb" json:"input"` Input string `gorm:"type:jsonb" json:"input"`
Output string `gorm:"type:jsonb" json:"output"` Output string `gorm:"type:jsonb" json:"output"`
Status string `gorm:"type:varchar(20)" json:"status"` // pending, running, completed, failed Status string `gorm:"type:varchar(20)" json:"status"` // pending, running, completed, failed
......
...@@ -74,7 +74,7 @@ func (s *Service) ApproveDoctorReview(ctx context.Context, reviewID string) erro ...@@ -74,7 +74,7 @@ func (s *Service) ApproveDoctorReview(ctx context.Context, reviewID string) erro
DepartmentID: dept.ID, DepartmentID: dept.ID,
Hospital: review.Hospital, Hospital: review.Hospital,
Rating: 5.0, Rating: 5.0,
Price: 5000, // 默认50元 Price: 50, // 默认50元
Status: "approved", Status: "approved",
} }
if err := tx.Create(doctor).Error; err != nil { if err := tx.Create(doctor).Error; err != nil {
......
...@@ -33,6 +33,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { ...@@ -33,6 +33,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm.DELETE("/users/:id", h.DeleteUser) adm.DELETE("/users/:id", h.DeleteUser)
adm.PUT("/users/:id/status", h.UpdateUserStatus) adm.PUT("/users/:id/status", h.UpdateUserStatus)
adm.POST("/users/:id/reset-password", h.ResetUserPassword) adm.POST("/users/:id/reset-password", h.ResetUserPassword)
adm.POST("/users/:id/change-password", h.ChangeUserPassword)
// 患者管理 // 患者管理
adm.GET("/patients", h.GetPatientList) adm.GET("/patients", h.GetPatientList)
...@@ -41,6 +42,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { ...@@ -41,6 +42,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm.DELETE("/patients/:id", h.DeletePatient) adm.DELETE("/patients/:id", h.DeletePatient)
adm.PUT("/patients/:id/status", h.UpdatePatientStatus) adm.PUT("/patients/:id/status", h.UpdatePatientStatus)
adm.POST("/patients/:id/reset-password", h.ResetPatientPassword) adm.POST("/patients/:id/reset-password", h.ResetPatientPassword)
adm.POST("/patients/:id/change-password", h.ChangePatientPassword)
// 医生管理 // 医生管理
adm.GET("/doctors", h.GetDoctorManageList) adm.GET("/doctors", h.GetDoctorManageList)
...@@ -49,6 +51,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { ...@@ -49,6 +51,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm.DELETE("/doctors/:id", h.DeleteDoctor) adm.DELETE("/doctors/:id", h.DeleteDoctor)
adm.PUT("/doctors/:id/status", h.UpdateDoctorStatus) adm.PUT("/doctors/:id/status", h.UpdateDoctorStatus)
adm.POST("/doctors/:id/reset-password", h.ResetDoctorPassword) adm.POST("/doctors/:id/reset-password", h.ResetDoctorPassword)
adm.POST("/doctors/:id/change-password", h.ChangeDoctorPassword)
// 医生审核 // 医生审核
adm.GET("/doctor-reviews", h.GetDoctorReviewList) adm.GET("/doctor-reviews", h.GetDoctorReviewList)
...@@ -68,6 +71,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { ...@@ -68,6 +71,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm.DELETE("/admins/:id", h.DeleteAdmin) adm.DELETE("/admins/:id", h.DeleteAdmin)
adm.PUT("/admins/:id/status", h.UpdateAdminStatus) adm.PUT("/admins/:id/status", h.UpdateAdminStatus)
adm.POST("/admins/:id/reset-password", h.ResetAdminPassword) adm.POST("/admins/:id/reset-password", h.ResetAdminPassword)
adm.POST("/admins/:id/change-password", h.ChangeAdminPassword)
// 系统日志 // 系统日志
adm.GET("/logs", h.GetSystemLogs) adm.GET("/logs", h.GetSystemLogs)
...@@ -473,6 +477,74 @@ func (h *Handler) ResetAdminPassword(c *gin.Context) { ...@@ -473,6 +477,74 @@ func (h *Handler) ResetAdminPassword(c *gin.Context) {
response.Success(c, nil) response.Success(c, nil)
} }
// ChangeUserPassword 修改用户密码
func (h *Handler) ChangeUserPassword(c *gin.Context) {
id := c.Param("id")
var req struct {
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请输入新密码")
return
}
if err := h.service.ChangeUserPassword(c.Request.Context(), id, req.Password); err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, nil)
}
// ChangePatientPassword 修改患者密码
func (h *Handler) ChangePatientPassword(c *gin.Context) {
id := c.Param("id")
var req struct {
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请输入新密码")
return
}
if err := h.service.ChangeUserPassword(c.Request.Context(), id, req.Password); err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, nil)
}
// ChangeDoctorPassword 修改医生密码
func (h *Handler) ChangeDoctorPassword(c *gin.Context) {
id := c.Param("id")
var req struct {
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请输入新密码")
return
}
if err := h.service.ChangeUserPassword(c.Request.Context(), id, req.Password); err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, nil)
}
// ChangeAdminPassword 修改管理员密码
func (h *Handler) ChangeAdminPassword(c *gin.Context) {
id := c.Param("id")
var req struct {
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请输入新密码")
return
}
if err := h.service.ChangeUserPassword(c.Request.Context(), id, req.Password); err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, nil)
}
// SeedRBACHandler 手动导入RBAC种子数据(角色、权限、菜单) // SeedRBACHandler 手动导入RBAC种子数据(角色、权限、菜单)
func (h *Handler) SeedRBACHandler(c *gin.Context) { func (h *Handler) SeedRBACHandler(c *gin.Context) {
SeedRBAC() SeedRBAC()
......
...@@ -368,6 +368,27 @@ func (s *Service) ResetUserPassword(ctx context.Context, userID string) error { ...@@ -368,6 +368,27 @@ func (s *Service) ResetUserPassword(ctx context.Context, userID string) error {
return s.db.Model(&model.User{}).Where("id = ?", userID).Update("password", hashedPassword).Error return s.db.Model(&model.User{}).Where("id = ?", userID).Update("password", hashedPassword).Error
} }
// ChangeUserPassword 修改用户密码(管理员设置指定密码)
func (s *Service) ChangeUserPassword(ctx context.Context, userID, newPassword string) error {
if len(newPassword) < 6 {
return fmt.Errorf("密码长度不能少于6位")
}
hashedPassword, err := utils.HashPassword(newPassword)
if err != nil {
return err
}
return s.db.Model(&model.User{}).Where("id = ?", userID).Update("password", hashedPassword).Error
}
// ChangeDoctorPassword 修改医生密码
func (s *Service) ChangeDoctorPassword(ctx context.Context, doctorID, newPassword string) error {
var doctor model.Doctor
if err := s.db.Where("id = ?", doctorID).First(&doctor).Error; err != nil {
return err
}
return s.ChangeUserPassword(ctx, doctor.UserID, newPassword)
}
// UpdateDoctorStatus 更新医生状态(通过 user_id 更新用户状态) // UpdateDoctorStatus 更新医生状态(通过 user_id 更新用户状态)
func (s *Service) UpdateDoctorStatus(ctx context.Context, doctorID, status string) error { func (s *Service) UpdateDoctorStatus(ctx context.Context, doctorID, status string) error {
var doctor model.Doctor var doctor model.Doctor
......
...@@ -343,7 +343,7 @@ func escapeJSON(s string) string { ...@@ -343,7 +343,7 @@ func escapeJSON(s string) string {
case '\t': case '\t':
result = append(result, '\\', 't') result = append(result, '\\', 't')
default: default:
result = append(result, byte(ch)) result = append(result, []byte(string(ch))...)
} }
} }
return string(result) return string(result)
......
...@@ -422,6 +422,7 @@ func (s *Service) AIAssist(ctx context.Context, consultID string, scene string) ...@@ -422,6 +422,7 @@ func (s *Service) AIAssist(ctx context.Context, consultID string, scene string)
} }
// buildTemplatePrompt 构建模板提示词(共享逻辑) // buildTemplatePrompt 构建模板提示词(共享逻辑)
// 通过就诊流水号(consultID)查询本次问诊的完整上下文,所有数据通过模板变量统一替换
func (s *Service) buildTemplatePrompt( func (s *Service) buildTemplatePrompt(
scene string, scene string,
consult model.Consultation, consult model.Consultation,
...@@ -434,47 +435,58 @@ func (s *Service) buildTemplatePrompt( ...@@ -434,47 +435,58 @@ func (s *Service) buildTemplatePrompt(
if err := s.db.Where("template_key = ? AND status = 'active'", scene).First(&tmpl).Error; err != nil { if err := s.db.Where("template_key = ? AND status = 'active'", scene).First(&tmpl).Error; err != nil {
log.Printf("[AIAssist] 未找到模板 %s,使用默认提示词", scene) log.Printf("[AIAssist] 未找到模板 %s,使用默认提示词", scene)
if scene == "consult_diagnosis" { if scene == "consult_diagnosis" {
tmpl.Content = "你是一位经验丰富的临床医生AI助手,请根据患者信息进行鉴别诊断分析,列出可能的诊断及依据。" tmpl.Content = "你是一位经验丰富的临床医生AI助手,请根据患者信息进行鉴别诊断分析,列出可能的诊断及依据。\n\n## 本次就诊信息\n{{consult_context}}\n\n## 对话记录\n{{chat_history}}\n\n请给出鉴别诊断建议。"
} else { } else {
tmpl.Content = "你是一位经验丰富的临床药师AI助手,请根据患者信息给出用药建议,包括药品、用法用量和注意事项。" tmpl.Content = "你是一位经验丰富的临床药师AI助手,请根据患者信息给出用药建议\n\n## 本次就诊信息\n{{consult_context}}\n\n## 对话记录\n{{chat_history}}\n\n请给出用药建议。"
} }
} }
systemPrompt := tmpl.Content // === 通过 consultID 查询本次问诊关联的患者信息 ===
systemPrompt = strings.ReplaceAll(systemPrompt, "{{chief_complaint}}", consult.ChiefComplaint) var patient model.User
s.db.Where("id = ?", consult.PatientID).First(&patient)
// 条件块替换辅助函数 var patientProfile model.PatientProfile
replaceBlock := func(prompt, tag, value string) string { s.db.Where("user_id = ?", consult.PatientID).First(&patientProfile)
openTag := "{{#" + tag + "}}"
closeTag := "{{/" + tag + "}}" // 构建本次就诊完整上下文
if value != "" { var ctx strings.Builder
prompt = strings.ReplaceAll(prompt, openTag, "") ctx.WriteString(fmt.Sprintf("- 就诊流水号:%s\n", consult.SerialNumber))
prompt = strings.ReplaceAll(prompt, closeTag, "") ctx.WriteString(fmt.Sprintf("- 问诊类型:%s\n", consult.Type))
prompt = strings.ReplaceAll(prompt, "{{"+tag+"}}", value) if patient.RealName != "" {
} else { ctx.WriteString(fmt.Sprintf("- 患者姓名:%s\n", patient.RealName))
for {
start := strings.Index(prompt, openTag)
if start == -1 {
break
} }
end := strings.Index(prompt, closeTag) if patient.Gender != "" {
if end == -1 { gender := patient.Gender
break if gender == "male" {
gender = "男"
} else if gender == "female" {
gender = "女"
} }
prompt = prompt[:start] + prompt[end+len(closeTag):] ctx.WriteString(fmt.Sprintf("- 性别:%s\n", gender))
} }
if patient.Age > 0 {
ctx.WriteString(fmt.Sprintf("- 年龄:%d岁\n", patient.Age))
} }
return prompt if consult.ChiefComplaint != "" {
ctx.WriteString(fmt.Sprintf("- 主诉:%s\n", consult.ChiefComplaint))
}
if consult.MedicalHistory != "" {
ctx.WriteString(fmt.Sprintf("- 本次病史:%s\n", consult.MedicalHistory))
}
if patientProfile.MedicalHistory != "" {
ctx.WriteString(fmt.Sprintf("- 既往病史:%s\n", patientProfile.MedicalHistory))
}
if allergyHistory != "" {
ctx.WriteString(fmt.Sprintf("- 过敏史:%s\n", allergyHistory))
} }
systemPrompt = replaceBlock(systemPrompt, "pre_consult_analysis", preConsult.AIAnalysis)
systemPrompt = replaceBlock(systemPrompt, "allergy_history", allergyHistory)
if len(chronicDiseases) > 0 { if len(chronicDiseases) > 0 {
systemPrompt = replaceBlock(systemPrompt, "chronic_diseases", strings.Join(chronicDiseases, "、")) ctx.WriteString(fmt.Sprintf("- 慢性病史:%s\n", strings.Join(chronicDiseases, "、")))
} else { }
systemPrompt = replaceBlock(systemPrompt, "chronic_diseases", "") if preConsult.AIAnalysis != "" {
ctx.WriteString(fmt.Sprintf("- 预问诊分析:%s\n", preConsult.AIAnalysis))
} }
// 构建对话记录
var chatText strings.Builder var chatText strings.Builder
for _, msg := range chatHistory { for _, msg := range chatHistory {
role := msg["role"] role := msg["role"]
...@@ -489,8 +501,32 @@ func (s *Service) buildTemplatePrompt( ...@@ -489,8 +501,32 @@ func (s *Service) buildTemplatePrompt(
chatText.WriteString(msg["content"]) chatText.WriteString(msg["content"])
chatText.WriteString("\n") chatText.WriteString("\n")
} }
// 统一替换所有模板变量
systemPrompt := tmpl.Content
systemPrompt = strings.ReplaceAll(systemPrompt, "{{consult_context}}", ctx.String())
systemPrompt = strings.ReplaceAll(systemPrompt, "{{serial_number}}", consult.SerialNumber)
systemPrompt = strings.ReplaceAll(systemPrompt, "{{consult_type}}", consult.Type)
systemPrompt = strings.ReplaceAll(systemPrompt, "{{patient_name}}", patient.RealName)
if patient.Gender == "male" {
systemPrompt = strings.ReplaceAll(systemPrompt, "{{patient_gender}}", "男")
} else if patient.Gender == "female" {
systemPrompt = strings.ReplaceAll(systemPrompt, "{{patient_gender}}", "女")
} else {
systemPrompt = strings.ReplaceAll(systemPrompt, "{{patient_gender}}", patient.Gender)
}
systemPrompt = strings.ReplaceAll(systemPrompt, "{{patient_age}}", fmt.Sprintf("%d", patient.Age))
systemPrompt = strings.ReplaceAll(systemPrompt, "{{chief_complaint}}", consult.ChiefComplaint)
systemPrompt = strings.ReplaceAll(systemPrompt, "{{medical_history}}", consult.MedicalHistory)
systemPrompt = strings.ReplaceAll(systemPrompt, "{{past_medical_history}}", patientProfile.MedicalHistory)
systemPrompt = strings.ReplaceAll(systemPrompt, "{{allergy_history}}", allergyHistory)
systemPrompt = strings.ReplaceAll(systemPrompt, "{{chronic_diseases}}", strings.Join(chronicDiseases, "、"))
systemPrompt = strings.ReplaceAll(systemPrompt, "{{pre_consult_analysis}}", preConsult.AIAnalysis)
systemPrompt = strings.ReplaceAll(systemPrompt, "{{chat_history}}", chatText.String()) systemPrompt = strings.ReplaceAll(systemPrompt, "{{chat_history}}", chatText.String())
log.Printf("[AIAssist] 场景=%s 流水号=%s 患者=%s 主诉=%s 聊天记录=%d条",
scene, consult.SerialNumber, patient.RealName, consult.ChiefComplaint, len(chatHistory))
return systemPrompt return systemPrompt
} }
......
...@@ -2,43 +2,20 @@ package middleware ...@@ -2,43 +2,20 @@ package middleware
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strings"
) )
// 允许的源列表,生产环境应从配置读取
var allowedOrigins = []string{
"http://localhost:3000",
"http://localhost:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
}
func CORS() gin.HandlerFunc { func CORS() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin") origin := c.Request.Header.Get("Origin")
// 验证Origin是否在允许列表中 if origin != "" {
allowed := false
for _, allowedOrigin := range allowedOrigins {
if origin == allowedOrigin || strings.HasPrefix(origin, allowedOrigin) {
allowed = true
break
}
}
if allowed {
c.Header("Access-Control-Allow-Origin", origin) c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true") c.Header("Access-Control-Allow-Credentials", "true")
} else if origin == "" {
// 无Origin头的请求(如Postman),开发环境允许
c.Header("Access-Control-Allow-Origin", "*")
} else { } else {
// 拒绝未授权的源 c.Header("Access-Control-Allow-Origin", "*")
c.AbortWithStatus(403)
return
} }
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, X-Requested-With") c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, X-Requested-With")
c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type") c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type")
......
...@@ -17,27 +17,11 @@ import ( ...@@ -17,27 +17,11 @@ import (
"internet-hospital/pkg/utils" "internet-hospital/pkg/utils"
) )
var allowedOrigins = []string{
"http://localhost:3000",
"http://localhost:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
}
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true // 无Origin头(如原生应用)
}
for _, allowed := range allowedOrigins {
if origin == allowed || strings.HasPrefix(origin, allowed) {
return true return true
}
}
return false
}, },
} }
......
...@@ -149,7 +149,7 @@ func (e *Engine) Execute(ctx context.Context, workflowID string, input map[strin ...@@ -149,7 +149,7 @@ func (e *Engine) Execute(ctx context.Context, workflowID string, input map[strin
outputJSON, _ := json.Marshal(output) outputJSON, _ := json.Marshal(output)
completedAt := time.Now() completedAt := time.Now()
db.Model(&exec).Updates(map[string]interface{}{ db.Model(&model.WorkflowExecution{}).Where("execution_id = ?", executionID).Updates(map[string]interface{}{
"status": status, "status": status,
"output": string(outputJSON), "output": string(outputJSON),
"error_message": errMsg, "error_message": errMsg,
...@@ -230,6 +230,20 @@ func (e *Engine) executeTool(ctx context.Context, node *Node, execCtx *Execution ...@@ -230,6 +230,20 @@ func (e *Engine) executeTool(ctx context.Context, node *Node, execCtx *Execution
params, _ := node.Config["params"].(map[string]interface{}) params, _ := node.Config["params"].(map[string]interface{})
if params == nil { if params == nil {
params = execCtx.Variables params = execCtx.Variables
} else {
// 替换 params 中的 {{var}} 模板变量
resolved := make(map[string]interface{}, len(params))
for k, v := range params {
if s, ok := v.(string); ok {
for varK, varV := range execCtx.Variables {
s = replaceAll(s, "{{"+varK+"}}", fmt.Sprintf("%v", varV))
}
resolved[k] = s
} else {
resolved[k] = v
}
}
params = resolved
} }
return tool.Execute(ctx, params) return tool.Execute(ctx, params)
} }
......
...@@ -273,6 +273,9 @@ export const adminApi = { ...@@ -273,6 +273,9 @@ export const adminApi = {
resetPatientPassword: (patientId: string) => resetPatientPassword: (patientId: string) =>
post<null>(`/admin/patients/${patientId}/reset-password`), post<null>(`/admin/patients/${patientId}/reset-password`),
changePatientPassword: (patientId: string, password: string) =>
post<null>(`/admin/patients/${patientId}/change-password`, { password }),
// === 医生管理 === // === 医生管理 ===
getDoctorList: (params: UserListParams) => getDoctorList: (params: UserListParams) =>
get<PaginatedResponse<DoctorItem>>('/admin/doctors', { params }), get<PaginatedResponse<DoctorItem>>('/admin/doctors', { params }),
...@@ -307,6 +310,9 @@ export const adminApi = { ...@@ -307,6 +310,9 @@ export const adminApi = {
resetDoctorPassword: (doctorId: string) => resetDoctorPassword: (doctorId: string) =>
post<null>(`/admin/doctors/${doctorId}/reset-password`), post<null>(`/admin/doctors/${doctorId}/reset-password`),
changeDoctorPassword: (doctorId: string, password: string) =>
post<null>(`/admin/doctors/${doctorId}/change-password`, { password }),
// === 管理员管理 === // === 管理员管理 ===
getAdminList: (params: UserListParams) => getAdminList: (params: UserListParams) =>
get<PaginatedResponse<UserInfo>>('/admin/admins', { params }), get<PaginatedResponse<UserInfo>>('/admin/admins', { params }),
...@@ -326,6 +332,9 @@ export const adminApi = { ...@@ -326,6 +332,9 @@ export const adminApi = {
resetAdminPassword: (adminId: string) => resetAdminPassword: (adminId: string) =>
post<null>(`/admin/admins/${adminId}/reset-password`), post<null>(`/admin/admins/${adminId}/reset-password`),
changeAdminPassword: (adminId: string, password: string) =>
post<null>(`/admin/admins/${adminId}/change-password`, { password }),
// === 医生审核 === // === 医生审核 ===
getDoctorReviewList: (status?: string) => getDoctorReviewList: (status?: string) =>
get<PaginatedResponse<DoctorReviewItem>>('/admin/doctor-reviews', { params: { status } }), get<PaginatedResponse<DoctorReviewItem>>('/admin/doctor-reviews', { params: { status } }),
......
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Input, Button, Space, Tag, Avatar, Modal, message, Typography, Form } from 'antd'; import { Card, Input, Button, Space, Tag, Avatar, Modal, message, Typography, Form, Dropdown } from 'antd';
import { DrawerForm, ProFormText, ProTable } from '@ant-design/pro-components'; import { DrawerForm, ProFormText, ProTable } from '@ant-design/pro-components';
import { SearchOutlined, UserOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { SearchOutlined, UserOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined, MoreOutlined, KeyOutlined, LockOutlined } from '@ant-design/icons';
import { adminApi } from '@/api/admin'; import { adminApi } from '@/api/admin';
import type { UserInfo } from '@/api/user'; import type { UserInfo } from '@/api/user';
...@@ -22,6 +22,9 @@ const AdminAdminsPage: React.FC = () => { ...@@ -22,6 +22,9 @@ const AdminAdminsPage: React.FC = () => {
const [editDrawerVisible, setEditDrawerVisible] = useState(false); const [editDrawerVisible, setEditDrawerVisible] = useState(false);
const [editLoading, setEditLoading] = useState(false); const [editLoading, setEditLoading] = useState(false);
const [editingAdmin, setEditingAdmin] = useState<UserInfo | null>(null); const [editingAdmin, setEditingAdmin] = useState<UserInfo | null>(null);
const [pwdModalVisible, setPwdModalVisible] = useState(false);
const [pwdRecord, setPwdRecord] = useState<UserInfo | null>(null);
const [newPassword, setNewPassword] = useState('');
const fetchAdmins = async () => { const fetchAdmins = async () => {
setLoading(true); setLoading(true);
...@@ -102,6 +105,26 @@ const AdminAdminsPage: React.FC = () => { ...@@ -102,6 +105,26 @@ const AdminAdminsPage: React.FC = () => {
} }
}; };
const handleChangePassword = async () => {
if (!pwdRecord || !newPassword) {
message.error('请输入新密码');
return;
}
if (newPassword.length < 6) {
message.error('密码长度不能少于6位');
return;
}
try {
await adminApi.changeAdminPassword(pwdRecord.id, newPassword);
message.success('密码修改成功');
setPwdModalVisible(false);
setNewPassword('');
setPwdRecord(null);
} catch {
message.error('修改密码失败');
}
};
const handleDeleteAdmin = (record: UserInfo) => { const handleDeleteAdmin = (record: UserInfo) => {
Modal.confirm({ Modal.confirm({
title: '确认删除该管理员?', title: '确认删除该管理员?',
...@@ -192,12 +215,10 @@ const AdminAdminsPage: React.FC = () => { ...@@ -192,12 +215,10 @@ const AdminAdminsPage: React.FC = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 280, width: 200,
render: (_, record) => ( render: (_, record) => (
<Space> <Space size={4}>
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEditAdmin(record)}> <Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEditAdmin(record)}>编辑</Button>
编辑
</Button>
<Button <Button
type="link" type="link"
size="small" size="small"
...@@ -206,12 +227,34 @@ const AdminAdminsPage: React.FC = () => { ...@@ -206,12 +227,34 @@ const AdminAdminsPage: React.FC = () => {
> >
{record.status === 'active' ? '禁用' : '启用'} {record.status === 'active' ? '禁用' : '启用'}
</Button> </Button>
<Button type="link" size="small" onClick={() => handleResetPassword(record)}> <Dropdown
重置密码 menu={{
</Button> items: [
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDeleteAdmin(record)}> {
删除 key: 'changePwd',
</Button> icon: <KeyOutlined />,
label: '修改密码',
onClick: () => { setPwdRecord(record); setPwdModalVisible(true); },
},
{
key: 'resetPwd',
icon: <LockOutlined />,
label: '重置密码',
onClick: () => handleResetPassword(record),
},
{ type: 'divider' },
{
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
danger: true,
onClick: () => handleDeleteAdmin(record),
},
],
}}
>
<Button type="link" size="small" icon={<MoreOutlined />} />
</Dropdown>
</Space> </Space>
), ),
}, },
...@@ -258,6 +301,24 @@ const AdminAdminsPage: React.FC = () => { ...@@ -258,6 +301,24 @@ const AdminAdminsPage: React.FC = () => {
/> />
</Card> </Card>
<Modal
title="修改密码"
open={pwdModalVisible}
onOk={handleChangePassword}
onCancel={() => { setPwdModalVisible(false); setNewPassword(''); setPwdRecord(null); }}
okText="确认修改"
>
<div style={{ marginBottom: 8, color: '#8c8c8c' }}>
管理员:{pwdRecord?.real_name || pwdRecord?.phone}
</div>
<Input.Password
placeholder="请输入新密码(至少6位)"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
minLength={6}
/>
</Modal>
<DrawerForm <DrawerForm
title="添加管理员" title="添加管理员"
open={addDrawerVisible} open={addDrawerVisible}
......
...@@ -139,7 +139,7 @@ const AdminDashboardPage: React.FC = () => { ...@@ -139,7 +139,7 @@ const AdminDashboardPage: React.FC = () => {
<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: '#0D9488' }} />, color: '#0D9488', bg: '#F0FDFA', extra: '' },
{ title: '注册医生', value: stats.total_doctors, prefix: '', icon: <MedicineBoxOutlined style={{ fontSize: 18, color: '#52c41a' }} />, color: '#52c41a', bg: '#f6ffed', extra: `在线 ${stats.online_doctors}` }, { 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: '' },
].map((item, i) => ( ].map((item, i) => (
...@@ -150,7 +150,7 @@ const AdminDashboardPage: React.FC = () => { ...@@ -150,7 +150,7 @@ const AdminDashboardPage: React.FC = () => {
title: item.title, title: item.title,
value: item.value, value: item.value,
prefix: item.prefix || undefined, prefix: item.prefix || undefined,
description: item.extra ? <Text style={{ fontSize: 12, color: '#bfbfbf' }}>{item.extra}</Text> : undefined, suffix: (item as any).suffix || undefined,
icon: ( icon: (
<div style={{ width: 36, height: 36, borderRadius: 10, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ width: 36, height: 36, borderRadius: 10, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{item.icon} {item.icon}
......
...@@ -4,11 +4,12 @@ import React, { useRef, useState, useEffect } from 'react'; ...@@ -4,11 +4,12 @@ import React, { useRef, useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { import {
Tag, Typography, Space, Button, Avatar, Modal, Drawer, App, Tag, Typography, Space, Button, Avatar, Modal, Drawer, App,
Input, Descriptions, Input, Descriptions, Dropdown,
} from 'antd'; } from 'antd';
import { import {
UserOutlined, CheckCircleOutlined, UserOutlined, CheckCircleOutlined,
EyeOutlined, EditOutlined, PlusOutlined, DeleteOutlined, EyeOutlined, EditOutlined, PlusOutlined, DeleteOutlined,
MoreOutlined, KeyOutlined, LockOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
ProTable, DrawerForm, ProFormText, ProFormSelect, ProFormTextArea, ProTable, DrawerForm, ProFormText, ProFormSelect, ProFormTextArea,
...@@ -49,6 +50,11 @@ const AdminDoctorsPage: React.FC = () => { ...@@ -49,6 +50,11 @@ const AdminDoctorsPage: React.FC = () => {
const [reviewModalVisible, setReviewModalVisible] = useState(false); const [reviewModalVisible, setReviewModalVisible] = useState(false);
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
// Change password state
const [pwdModalVisible, setPwdModalVisible] = useState(false);
const [pwdRecord, setPwdRecord] = useState<DoctorItem | null>(null);
const [newPassword, setNewPassword] = useState('');
// Department options from backend // Department options from backend
const [departmentOptions, setDepartmentOptions] = useState<DeptOption[]>([]); const [departmentOptions, setDepartmentOptions] = useState<DeptOption[]>([]);
...@@ -160,6 +166,26 @@ const AdminDoctorsPage: React.FC = () => { ...@@ -160,6 +166,26 @@ const AdminDoctorsPage: React.FC = () => {
}); });
}; };
const handleChangePassword = async () => {
if (!pwdRecord || !newPassword) {
message.error('请输入新密码');
return;
}
if (newPassword.length < 6) {
message.error('密码长度不能少于6位');
return;
}
try {
await adminApi.changeDoctorPassword(pwdRecord.user_id, newPassword);
message.success('密码修改成功');
setPwdModalVisible(false);
setNewPassword('');
setPwdRecord(null);
} catch {
message.error('修改密码失败');
}
};
const handleEditDoctor = (record: DoctorItem) => { const handleEditDoctor = (record: DoctorItem) => {
setEditingDoctor(record); setEditingDoctor(record);
setEditModalVisible(true); setEditModalVisible(true);
...@@ -257,34 +283,48 @@ const AdminDoctorsPage: React.FC = () => { ...@@ -257,34 +283,48 @@ const AdminDoctorsPage: React.FC = () => {
{ {
title: '操作', title: '操作',
valueType: 'option', valueType: 'option',
width: 320, width: 240,
render: (_, record) => ( render: (_, record) => (
<Space size={0}> <Space size={4}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}> <Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>详情</Button>
详情
</Button>
{record.review_status === 'pending' && ( {record.review_status === 'pending' && (
<Button type="link" size="small" icon={<CheckCircleOutlined />} onClick={() => handleReview(record)}> <Button type="link" size="small" icon={<CheckCircleOutlined />} onClick={() => handleReview(record)}>审核</Button>
审核
</Button>
)} )}
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEditDoctor(record)}> <Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEditDoctor(record)}>编辑</Button>
编辑 <Dropdown
</Button> menu={{
<Button items: [
type="link" {
size="small" key: 'toggle',
danger={record.user_status === 'active'} label: record.user_status === 'active' ? '禁用' : '启用',
onClick={() => handleToggleStatus(record)} danger: record.user_status === 'active',
onClick: () => handleToggleStatus(record),
},
{
key: 'changePwd',
icon: <KeyOutlined />,
label: '修改密码',
onClick: () => { setPwdRecord(record); setPwdModalVisible(true); },
},
{
key: 'resetPwd',
icon: <LockOutlined />,
label: '重置密码',
onClick: () => handleResetPassword(record),
},
{ type: 'divider' },
{
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
danger: true,
onClick: () => handleDelete(record),
},
],
}}
> >
{record.user_status === 'active' ? '禁用' : '启用'} <Button type="link" size="small" icon={<MoreOutlined />} />
</Button> </Dropdown>
<Button type="link" size="small" onClick={() => handleResetPassword(record)}>
重置密码
</Button>
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
删除
</Button>
</Space> </Space>
), ),
}, },
...@@ -514,6 +554,25 @@ const AdminDoctorsPage: React.FC = () => { ...@@ -514,6 +554,25 @@ const AdminDoctorsPage: React.FC = () => {
/> />
</DrawerForm> </DrawerForm>
{/* ========== Change Password Modal ========== */}
<Modal
title="修改密码"
open={pwdModalVisible}
onOk={handleChangePassword}
onCancel={() => { setPwdModalVisible(false); setNewPassword(''); setPwdRecord(null); }}
okText="确认修改"
>
<div style={{ marginBottom: 8, color: '#8c8c8c' }}>
医生:{pwdRecord?.name}
</div>
<Input.Password
placeholder="请输入新密码(至少6位)"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
minLength={6}
/>
</Modal>
{/* ========== Detail Drawer ========== */} {/* ========== Detail Drawer ========== */}
<Drawer <Drawer
title="医生详情" title="医生详情"
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { Button, Space, Tag, Avatar, Modal, Typography, App } from 'antd'; import { Button, Space, Tag, Avatar, Modal, Typography, App, Dropdown, Input } from 'antd';
import { import {
UserOutlined, PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
MoreOutlined, KeyOutlined, LockOutlined, StopOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
ProTable, DrawerForm, ProFormText, ProFormSelect, ProFormDigit, ProTable, DrawerForm, ProFormText, ProFormSelect, ProFormDigit,
...@@ -27,6 +28,11 @@ const AdminPatientsPage: React.FC = () => { ...@@ -27,6 +28,11 @@ const AdminPatientsPage: React.FC = () => {
const [editModalVisible, setEditModalVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false);
const [editingRecord, setEditingRecord] = useState<UserInfo | null>(null); const [editingRecord, setEditingRecord] = useState<UserInfo | null>(null);
// Change password state
const [pwdModalVisible, setPwdModalVisible] = useState(false);
const [pwdRecord, setPwdRecord] = useState<UserInfo | null>(null);
const [newPassword, setNewPassword] = useState('');
// ?action=add auto-open add modal // ?action=add auto-open add modal
useEffect(() => { useEffect(() => {
if (searchParams.get('action') === 'add') { if (searchParams.get('action') === 'add') {
...@@ -71,6 +77,26 @@ const AdminPatientsPage: React.FC = () => { ...@@ -71,6 +77,26 @@ const AdminPatientsPage: React.FC = () => {
setEditModalVisible(true); setEditModalVisible(true);
}; };
const handleChangePassword = async () => {
if (!pwdRecord || !newPassword) {
message.error('请输入新密码');
return;
}
if (newPassword.length < 6) {
message.error('密码长度不能少于6位');
return;
}
try {
await adminApi.changePatientPassword(pwdRecord.id, newPassword);
message.success('密码修改成功');
setPwdModalVisible(false);
setNewPassword('');
setPwdRecord(null);
} catch {
message.error('修改密码失败');
}
};
const handleDelete = (record: UserInfo) => { const handleDelete = (record: UserInfo) => {
Modal.confirm({ Modal.confirm({
title: '确认删除该患者?', title: '确认删除该患者?',
...@@ -145,18 +171,46 @@ const AdminPatientsPage: React.FC = () => { ...@@ -145,18 +171,46 @@ const AdminPatientsPage: React.FC = () => {
{ {
title: '操作', title: '操作',
valueType: 'option', valueType: 'option',
width: 260, width: 200,
render: (_, record) => ( render: (_, record) => (
<Space size={0}> <Space size={4}>
<a onClick={() => handleEdit(record)}><EditOutlined /> 编辑</a> <Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<a <Button
style={record.status === 'active' ? { color: '#ff4d4f' } : undefined} type="link"
size="small"
danger={record.status === 'active'}
onClick={() => handleToggleStatus(record)} onClick={() => handleToggleStatus(record)}
> >
{record.status === 'active' ? '禁用' : '启用'} {record.status === 'active' ? '禁用' : '启用'}
</a> </Button>
<a onClick={() => handleResetPassword(record)}>重置密码</a> <Dropdown
<a style={{ color: '#ff4d4f' }} onClick={() => handleDelete(record)}><DeleteOutlined /> 删除</a> menu={{
items: [
{
key: 'changePwd',
icon: <KeyOutlined />,
label: '修改密码',
onClick: () => { setPwdRecord(record); setPwdModalVisible(true); },
},
{
key: 'resetPwd',
icon: <LockOutlined />,
label: '重置密码',
onClick: () => handleResetPassword(record),
},
{ type: 'divider' },
{
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
danger: true,
onClick: () => handleDelete(record),
},
],
}}
>
<Button type="link" size="small" icon={<MoreOutlined />} />
</Dropdown>
</Space> </Space>
), ),
}, },
...@@ -254,6 +308,25 @@ const AdminPatientsPage: React.FC = () => { ...@@ -254,6 +308,25 @@ const AdminPatientsPage: React.FC = () => {
/> />
</DrawerForm> </DrawerForm>
{/* Change Password Modal */}
<Modal
title="修改密码"
open={pwdModalVisible}
onOk={handleChangePassword}
onCancel={() => { setPwdModalVisible(false); setNewPassword(''); setPwdRecord(null); }}
okText="确认修改"
>
<div style={{ marginBottom: 8, color: '#8c8c8c' }}>
患者:{pwdRecord?.real_name || pwdRecord?.phone}
</div>
<Input.Password
placeholder="请输入新密码(至少6位)"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
minLength={6}
/>
</Modal>
{/* Edit Patient DrawerForm */} {/* Edit Patient DrawerForm */}
<DrawerForm <DrawerForm
title="编辑患者" title="编辑患者"
......
...@@ -103,7 +103,7 @@ const DoctorProfilePage: React.FC = () => { ...@@ -103,7 +103,7 @@ const DoctorProfilePage: React.FC = () => {
<Descriptions.Item label="医院">{profile.hospital}</Descriptions.Item> <Descriptions.Item label="医院">{profile.hospital}</Descriptions.Item>
<Descriptions.Item label="科室">{profile.department_name}</Descriptions.Item> <Descriptions.Item label="科室">{profile.department_name}</Descriptions.Item>
<Descriptions.Item label="问诊价格"> <Descriptions.Item label="问诊价格">
<span style={{ fontSize: 16, fontWeight: 700, color: '#52c41a' }}>¥{(profile.price / 100).toFixed(0)}</span> <span style={{ fontSize: 16, fontWeight: 700, color: '#52c41a' }}>¥{profile.price}</span>
<span style={{ fontSize: 12, color: '#8c8c8c' }}>/次</span> <span style={{ fontSize: 12, color: '#8c8c8c' }}>/次</span>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
......
...@@ -60,7 +60,7 @@ const DoctorDetailPage: React.FC = () => { ...@@ -60,7 +60,7 @@ const DoctorDetailPage: React.FC = () => {
<Col> <Col>
<div className="text-right"> <div className="text-right">
<div className="mb-2"> <div className="mb-2">
<Text strong className="text-xl! text-red-500!">¥{((doctor.price || 0) / 100).toFixed(0)}</Text> <Text strong className="text-xl! text-red-500!">¥{doctor.price || 0}</Text>
<Text type="secondary" className="text-xs!">/次</Text> <Text type="secondary" className="text-xs!">/次</Text>
</div> </div>
<Space> <Space>
......
...@@ -111,7 +111,7 @@ const DoctorRecommendations: React.FC<DoctorRecommendationsProps> = ({ ...@@ -111,7 +111,7 @@ const DoctorRecommendations: React.FC<DoctorRecommendationsProps> = ({
<Space split={<Divider type="vertical" />}> <Space split={<Divider type="vertical" />}>
<span>评分 {doctor.rating}</span> <span>评分 {doctor.rating}</span>
<span>问诊 {doctor.consult_count}</span> <span>问诊 {doctor.consult_count}</span>
<span style={{ color: '#fa8c16' }}>¥{(doctor.price / 100).toFixed(0)}/次</span> <span style={{ color: '#fa8c16' }}>¥{doctor.price}/次</span>
</Space> </Space>
} }
/> />
......
...@@ -158,7 +158,7 @@ const ConsultCreateDrawer: React.FC<ConsultCreateDrawerProps> = ({ ...@@ -158,7 +158,7 @@ const ConsultCreateDrawer: React.FC<ConsultCreateDrawerProps> = ({
<Text type="secondary">{doctor.hospital}</Text> <Text type="secondary">{doctor.hospital}</Text>
<br /> <br />
<Space style={{ marginTop: 4 }}> <Space style={{ marginTop: 4 }}>
<Tag color="orange">¥{((doctor.price || 0) / 100).toFixed(0)}/次</Tag> <Tag color="orange">¥{doctor.price || 0}/次</Tag>
<Text type="secondary">评分 {doctor.rating} · 问诊 {doctor.consult_count}</Text> <Text type="secondary">评分 {doctor.rating} · 问诊 {doctor.consult_count}</Text>
</Space> </Space>
</div> </div>
......
...@@ -48,7 +48,7 @@ interface AIAssistState { ...@@ -48,7 +48,7 @@ interface AIAssistState {
closeDrawer: () => void; closeDrawer: () => void;
} }
const DEFAULT_BOUNDS: Bounds = { x: -1, y: 80, width: 420, height: 600 }; const DEFAULT_BOUNDS: Bounds = { x: -1, y: 80, width: 840, height: 800 };
function loadBounds(role?: string): Bounds { function loadBounds(role?: string): Bounds {
if (typeof window === 'undefined') return DEFAULT_BOUNDS; if (typeof window === 'undefined') return DEFAULT_BOUNDS;
......
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