Commit ef9bb5d9 authored by yuguo's avatar yuguo

fix

parent ad3de4e4
......@@ -21,7 +21,9 @@
"Bash(lsof:*)",
"Bash(ipconfig:*)",
"Bash(npm:*)",
"Bash(./api.exe:*)"
"Bash(./api.exe:*)",
"Bash(PGPASSWORD=123456 psql:*)",
"Bash(go mod:*)"
]
}
}
module testdata
go 1.25.0
require (
github.com/google/uuid v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
package main
import (
"fmt"
"log"
"github.com/google/uuid"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type Medicine struct {
ID string `gorm:"type:uuid;primaryKey"`
Name string `gorm:"type:varchar(200);not null"`
GenericName string `gorm:"type:varchar(200)"`
Specification string `gorm:"type:varchar(200)"`
Manufacturer string `gorm:"type:varchar(200)"`
Category string `gorm:"type:varchar(50)"`
Unit string `gorm:"type:varchar(20)"`
Price int `gorm:"type:int;not null;default:0"`
Stock int `gorm:"type:int;not null;default:0"`
StockWarning int `gorm:"type:int;default:50"`
Status string `gorm:"type:varchar(20);default:'normal'"`
}
func (Medicine) TableName() string {
return "medicines"
}
func main() {
dsn := "host=localhost user=postgres password=123456 dbname=internet_hospital port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("连接数据库失败:", err)
}
medicines := []Medicine{
// 抗生素类 (50条)
{uuid.New().String(), "阿莫西林胶囊", "阿莫西林", "0.25g*24粒", "华北制药股份有限公司", "抗生素", "盒", 1200, 500, 50, "normal"},
{uuid.New().String(), "头孢克肟分散片", "头孢克肟", "0.1g*6片", "石药集团欧意药业有限公司", "抗生素", "盒", 2800, 300, 50, "normal"},
{uuid.New().String(), "阿奇霉素片", "阿奇霉素", "0.25g*6片", "辉瑞制药有限公司", "抗生素", "盒", 3500, 450, 50, "normal"},
{uuid.New().String(), "左氧氟沙星片", "左氧氟沙星", "0.5g*6片", "第一三共制药(北京)有限公司", "抗生素", "盒", 2200, 380, 50, "normal"},
{uuid.New().String(), "罗红霉素胶囊", "罗红霉素", "150mg*12粒", "扬子江药业集团有限公司", "抗生素", "盒", 1800, 420, 50, "normal"},
{uuid.New().String(), "头孢呋辛酯片", "头孢呋辛酯", "0.25g*12片", "葛兰素史克制药有限公司", "抗生素", "盒", 4200, 280, 50, "normal"},
{uuid.New().String(), "克拉霉素片", "克拉霉素", "0.25g*12片", "雅培制药有限公司", "抗生素", "盒", 3800, 350, 50, "normal"},
{uuid.New().String(), "头孢氨苄胶囊", "头孢氨苄", "0.25g*24粒", "哈药集团制药六厂", "抗生素", "盒", 1500, 520, 50, "normal"},
{uuid.New().String(), "诺氟沙星胶囊", "诺氟沙星", "0.1g*24粒", "湖南千金湘江药业股份有限公司", "抗生素", "盒", 800, 600, 50, "normal"},
{uuid.New().String(), "环丙沙星片", "环丙沙星", "0.25g*12片", "拜耳医药保健有限公司", "抗生素", "盒", 2600, 320, 50, "normal"},
{uuid.New().String(), "头孢拉定胶囊", "头孢拉定", "0.25g*24粒", "石家庄四药有限公司", "抗生素", "盒", 1600, 480, 50, "normal"},
{uuid.New().String(), "红霉素肠溶片", "红霉素", "0.25g*24片", "上海信谊药厂有限公司", "抗生素", "盒", 1000, 550, 50, "normal"},
{uuid.New().String(), "头孢克洛胶囊", "头孢克洛", "0.25g*12粒", "礼来苏州制药有限公司", "抗生素", "盒", 3200, 360, 50, "normal"},
{uuid.New().String(), "莫西沙星片", "莫西沙星", "0.4g*5片", "拜耳医药保健有限公司", "抗生素", "盒", 8500, 180, 30, "normal"},
{uuid.New().String(), "头孢地尼胶囊", "头孢地尼", "0.1g*12粒", "藤泽药品工业株式会社", "抗生素", "盒", 4500, 250, 50, "normal"},
{uuid.New().String(), "氨苄西林胶囊", "氨苄西林", "0.25g*24粒", "华北制药股份有限公司", "抗生素", "盒", 1100, 490, 50, "normal"},
{uuid.New().String(), "头孢泊肟酯片", "头孢泊肟酯", "0.1g*6片", "山德士(中国)制药有限公司", "抗生素", "盒", 3600, 310, 50, "normal"},
{uuid.New().String(), "克林霉素胶囊", "克林霉素", "0.15g*12粒", "辉瑞制药有限公司", "抗生素", "盒", 2400, 400, 50, "normal"},
{uuid.New().String(), "头孢丙烯片", "头孢丙烯", "0.25g*12片", "百时美施贵宝公司", "抗生素", "盒", 3900, 290, 50, "normal"},
{uuid.New().String(), "米诺环素胶囊", "米诺环素", "50mg*20粒", "惠氏制药有限公司", "抗生素", "盒", 5200, 220, 40, "normal"},
{uuid.New().String(), "头孢唑林钠", "头孢唑林钠", "0.5g*10支", "华北制药股份有限公司", "抗生素", "盒", 4800, 260, 50, "normal"},
{uuid.New().String(), "多西环素片", "多西环素", "0.1g*12片", "上海信谊药厂有限公司", "抗生素", "盒", 1400, 460, 50, "normal"},
{uuid.New().String(), "头孢曲松钠", "头孢曲松钠", "1.0g*10支", "罗氏制药有限公司", "抗生素", "盒", 12000, 150, 30, "normal"},
{uuid.New().String(), "甲硝唑片", "甲硝唑", "0.2g*100片", "湖南千金湘江药业股份有限公司", "抗生素", "瓶", 600, 700, 50, "normal"},
{uuid.New().String(), "头孢哌酮钠舒巴坦钠", "头孢哌酮钠舒巴坦钠", "1.5g*10支", "辉瑞制药有限公司", "抗生素", "盒", 18000, 120, 30, "normal"},
{uuid.New().String(), "替硝唑片", "替硝唑", "0.5g*8片", "扬子江药业集团有限公司", "抗生素", "盒", 900, 580, 50, "normal"},
{uuid.New().String(), "头孢他啶", "头孢他啶", "1.0g*10支", "葛兰素史克制药有限公司", "抗生素", "盒", 15000, 130, 30, "normal"},
{uuid.New().String(), "奥硝唑胶囊", "奥硝唑", "0.25g*16粒", "康恩贝制药股份有限公司", "抗生素", "盒", 1300, 470, 50, "normal"},
{uuid.New().String(), "头孢美唑钠", "头孢美唑钠", "1.0g*10支", "第一三共制药(北京)有限公司", "抗生素", "盒", 16000, 110, 30, "normal"},
{uuid.New().String(), "林可霉素胶囊", "林可霉素", "0.25g*24粒", "华北制药股份有限公司", "抗生素", "盒", 1700, 440, 50, "normal"},
{uuid.New().String(), "头孢西丁钠", "头孢西丁钠", "1.0g*10支", "默沙东制药有限公司", "抗生素", "盒", 14000, 140, 30, "normal"},
{uuid.New().String(), "磷霉素氨丁三醇散", "磷霉素氨丁三醇", "3g*1袋", "赞邦制药有限公司", "抗生素", "盒", 4500, 240, 40, "normal"},
{uuid.New().String(), "头孢吡肟", "头孢吡肟", "1.0g*10支", "百时美施贵宝公司", "抗生素", "盒", 17000, 100, 30, "normal"},
{uuid.New().String(), "呋喃妥因肠溶片", "呋喃妥因", "0.1g*24片", "上海信谊药厂有限公司", "抗生素", "盒", 1200, 500, 50, "normal"},
{uuid.New().String(), "美罗培南", "美罗培南", "0.5g*10支", "住友制药(苏州)有限公司", "抗生素", "盒", 28000, 80, 20, "normal"},
{uuid.New().String(), "呋喃唑酮片", "呋喃唑酮", "0.1g*100片", "湖南千金湘江药业股份有限公司", "抗生素", "瓶", 500, 750, 50, "normal"},
{uuid.New().String(), "亚胺培南西司他丁钠", "亚胺培南西司他丁钠", "0.5g*10支", "默沙东制药有限公司", "抗生素", "盒", 32000, 70, 20, "normal"},
{uuid.New().String(), "利福平胶囊", "利福平", "0.15g*100粒", "沈阳红旗制药有限公司", "抗生素", "瓶", 2800, 350, 50, "normal"},
{uuid.New().String(), "比阿培南", "比阿培南", "0.3g*10支", "明治制果药业株式会社", "抗生素", "盒", 35000, 60, 20, "normal"},
{uuid.New().String(), "异烟肼片", "异烟肼", "0.1g*100片", "沈阳红旗制药有限公司", "抗生素", "瓶", 800, 600, 50, "normal"},
{uuid.New().String(), "万古霉素", "万古霉素", "0.5g*10支", "礼来苏州制药有限公司", "抗生素", "盒", 38000, 50, 20, "normal"},
{uuid.New().String(), "吡嗪酰胺片", "吡嗪酰胺", "0.25g*100片", "沈阳红旗制药有限公司", "抗生素", "瓶", 1500, 450, 50, "normal"},
{uuid.New().String(), "替考拉宁", "替考拉宁", "0.2g*3支", "赛诺菲安万特制药有限公司", "抗生素", "盒", 42000, 40, 15, "normal"},
{uuid.New().String(), "乙胺丁醇片", "乙胺丁醇", "0.25g*100片", "沈阳红旗制药有限公司", "抗生素", "瓶", 1800, 420, 50, "normal"},
{uuid.New().String(), "利奈唑胺片", "利奈唑胺", "0.6g*10片", "辉瑞制药有限公司", "抗生素", "盒", 48000, 35, 15, "normal"},
{uuid.New().String(), "链霉素", "链霉素", "1.0g*10支", "华北制药股份有限公司", "抗生素", "盒", 3500, 300, 50, "normal"},
{uuid.New().String(), "达托霉素", "达托霉素", "0.5g*1支", "默沙东制药有限公司", "抗生素", "盒", 55000, 30, 10, "normal"},
{uuid.New().String(), "卡那霉素", "卡那霉素", "1.0g*10支", "华北制药股份有限公司", "抗生素", "盒", 4200, 280, 50, "normal"},
{uuid.New().String(), "替加环素", "替加环素", "50mg*10支", "惠氏制药有限公司", "抗生素", "盒", 62000, 25, 10, "normal"},
{uuid.New().String(), "庆大霉素", "庆大霉素", "8万单位*10支", "华北制药股份有限公司", "抗生素", "盒", 2800, 380, 50, "normal"},
// 心血管药物 (60条)
{uuid.New().String(), "阿司匹林肠溶片", "阿司匹林", "100mg*30片", "拜耳医药保健有限公司", "心血管药物", "盒", 800, 800, 80, "normal"},
{uuid.New().String(), "硝苯地平缓释片", "硝苯地平", "20mg*30片", "拜耳医药保健有限公司", "心血管药物", "盒", 2200, 600, 60, "normal"},
{uuid.New().String(), "氨氯地平片", "氨氯地平", "5mg*7片", "辉瑞制药有限公司", "心血管药物", "盒", 1800, 700, 70, "normal"},
{uuid.New().String(), "美托洛尔片", "美托洛尔", "25mg*20片", "阿斯利康制药有限公司", "心血管药物", "盒", 1500, 650, 65, "normal"},
{uuid.New().String(), "缬沙坦胶囊", "缬沙坦", "80mg*7粒", "诺华制药有限公司", "心血管药物", "盒", 3200, 550, 55, "normal"},
{uuid.New().String(), "氯吡格雷片", "氯吡格雷", "75mg*7片", "赛诺菲安万特制药有限公司", "心血管药物", "盒", 4500, 500, 50, "normal"},
{uuid.New().String(), "阿托伐他汀钙片", "阿托伐他汀钙", "20mg*7片", "辉瑞制药有限公司", "心血管药物", "盒", 3800, 580, 58, "normal"},
{uuid.New().String(), "依那普利片", "依那普利", "10mg*16片", "默沙东制药有限公司", "心血管药物", "盒", 2400, 620, 62, "normal"},
{uuid.New().String(), "比索洛尔片", "比索洛尔", "5mg*10片", "默克制药有限公司", "心血管药物", "盒", 1600, 680, 68, "normal"},
{uuid.New().String(), "厄贝沙坦片", "厄贝沙坦", "150mg*7片", "赛诺菲安万特制药有限公司", "心血管药物", "盒", 2800, 600, 60, "normal"},
{uuid.New().String(), "辛伐他汀片", "辛伐他汀", "20mg*10片", "默沙东制药有限公司", "心血管药物", "盒", 2200, 640, 64, "normal"},
{uuid.New().String(), "卡托普利片", "卡托普利", "25mg*100片", "上海信谊药厂有限公司", "心血管药物", "瓶", 1200, 750, 75, "normal"},
{uuid.New().String(), "硝酸异山梨酯片", "硝酸异山梨酯", "5mg*100片", "北京益民药业有限公司", "心血管药物", "瓶", 800, 800, 80, "normal"},
{uuid.New().String(), "螺内酯片", "螺内酯", "20mg*100片", "上海信谊药厂有限公司", "心血管药物", "瓶", 1000, 720, 72, "normal"},
{uuid.New().String(), "地高辛片", "地高辛", "0.25mg*100片", "上海信谊药厂有限公司", "心血管药物", "瓶", 1500, 650, 65, "normal"},
{uuid.New().String(), "呋塞米片", "呋塞米", "20mg*100片", "赛诺菲安万特制药有限公司", "心血管药物", "瓶", 600, 850, 85, "normal"},
{uuid.New().String(), "氢氯噻嗪片", "氢氯噻嗪", "25mg*100片", "上海信谊药厂有限公司", "心血管药物", "瓶", 500, 900, 90, "normal"},
{uuid.New().String(), "胺碘酮片", "胺碘酮", "0.2g*30片", "赛诺菲安万特制药有限公司", "心血管药物", "盒", 3500, 520, 52, "normal"},
{uuid.New().String(), "瑞舒伐他汀钙片", "瑞舒伐他汀钙", "10mg*7片", "阿斯利康制药有限公司", "心血管药物", "盒", 4200, 480, 48, "normal"},
{uuid.New().String(), "替米沙坦片", "替米沙坦", "80mg*7片", "勃林格殷格翰制药有限公司", "心血管药物", "盒", 3600, 540, 54, "normal"},
{uuid.New().String(), "培哚普利片", "培哚普利", "4mg*30片", "施维雅制药有限公司", "心血管药物", "盒", 4800, 450, 45, "normal"},
{uuid.New().String(), "坎地沙坦酯片", "坎地沙坦酯", "8mg*7片", "阿斯利康制药有限公司", "心血管药物", "盒", 3200, 560, 56, "normal"},
{uuid.New().String(), "非洛地平缓释片", "非洛地平", "5mg*10片", "阿斯利康制药有限公司", "心血管药物", "盒", 2800, 590, 59, "normal"},
{uuid.New().String(), "拉西地平片", "拉西地平", "4mg*7片", "葛兰素史克制药有限公司", "心血管药物", "盒", 3400, 530, 53, "normal"},
{uuid.New().String(), "尼群地平片", "尼群地平", "10mg*100片", "山东鲁抗医药股份有限公司", "心血管药物", "瓶", 1800, 670, 67, "normal"},
{uuid.New().String(), "卡维地洛片", "卡维地洛", "10mg*20片", "罗氏制药有限公司", "心血管药物", "盒", 2600, 610, 61, "normal"},
{uuid.New().String(), "索他洛尔片", "索他洛尔", "80mg*20片", "百时美施贵宝公司", "心血管药物", "盒", 3000, 570, 57, "normal"},
{uuid.New().String(), "普罗帕酮片", "普罗帕酮", "50mg*100片", "雅培制药有限公司", "心血管药物", "瓶", 2400, 630, 63, "normal"},
{uuid.New().String(), "维拉帕米片", "维拉帕米", "40mg*100片", "上海信谊药厂有限公司", "心血管药物", "瓶", 1400, 700, 70, "normal"},
{uuid.New().String(), "地尔硫卓片", "地尔硫卓", "30mg*100片", "天津田边制药有限公司", "心血管药物", "瓶", 1600, 680, 68, "normal"},
{uuid.New().String(), "单硝酸异山梨酯片", "单硝酸异山梨酯", "20mg*30片", "鲁南贝特制药有限公司", "心血管药物", "盒", 1200, 740, 74, "normal"},
{uuid.New().String(), "曲美他嗪片", "曲美他嗪", "20mg*30片", "施维雅制药有限公司", "心血管药物", "盒", 3800, 500, 50, "normal"},
{uuid.New().String(), "匹伐他汀钙片", "匹伐他汀钙", "2mg*7片", "第一三共制药(北京)有限公司", "心血管药物", "盒", 4500, 460, 46, "normal"},
{uuid.New().String(), "氟伐他汀钠缓释片", "氟伐他汀钠", "80mg*7片", "诺华制药有限公司", "心血管药物", "盒", 5200, 420, 42, "normal"},
{uuid.New().String(), "普伐他汀钠片", "普伐他汀钠", "10mg*10片", "百时美施贵宝公司", "心血管药物", "盒", 3600, 540, 54, "normal"},
{uuid.New().String(), "依折麦布片", "依折麦布", "10mg*7片", "默沙东制药有限公司", "心血管药物", "盒", 5800, 380, 38, "normal"},
{uuid.New().String(), "非诺贝特胶囊", "非诺贝特", "0.1g*30粒", "雅培制药有限公司", "心血管药物", "盒", 3200, 560, 56, "normal"},
{uuid.New().String(), "贝特类降脂药", "苯扎贝特", "0.2g*30片", "上海信谊药厂有限公司", "心血管药物", "盒", 2800, 590, 59, "normal"},
{uuid.New().String(), "托拉塞米片", "托拉塞米", "10mg*20片", "罗氏制药有限公司", "心血管药物", "盒", 2400, 620, 62, "normal"},
{uuid.New().String(), "吲达帕胺片", "吲达帕胺", "2.5mg*30片", "施维雅制药有限公司", "心血管药物", "盒", 1800, 670, 67, "normal"},
{uuid.New().String(), "氨苯蝶啶片", "氨苯蝶啶", "50mg*100片", "上海信谊药厂有限公司", "心血管药物", "瓶", 800, 800, 80, "normal"},
{uuid.New().String(), "雷米普利片", "雷米普利", "5mg*7片", "赛诺菲安万特制药有限公司", "心血管药物", "盒", 3400, 530, 53, "normal"},
{uuid.New().String(), "贝那普利片", "贝那普利", "10mg*7片", "诺华制药有限公司", "心血管药物", "盒", 3000, 570, 57, "normal"},
{uuid.New().String(), "福辛普利钠片", "福辛普利钠", "10mg*7片", "百时美施贵宝公司", "心血管药物", "盒", 3600, 540, 54, "normal"},
{uuid.New().String(), "赖诺普利片", "赖诺普利", "10mg*7片", "阿斯利康制药有限公司", "心血管药物", "盒", 2800, 590, 59, "normal"},
{uuid.New().String(), "奥美沙坦酯片", "奥美沙坦酯", "20mg*7片", "第一三共制药(北京)有限公司", "心血管药物", "盒", 4200, 480, 48, "normal"},
{uuid.New().String(), "阿利吉仑片", "阿利吉仑", "150mg*7片", "诺华制药有限公司", "心血管药物", "盒", 6800, 320, 32, "normal"},
{uuid.New().String(), "沙库巴曲缬沙坦钠片", "沙库巴曲缬沙坦钠", "50mg*28片", "诺华制药有限公司", "心血管药物", "盒", 18000, 200, 20, "normal"},
{uuid.New().String(), "伊伐布雷定片", "伊伐布雷定", "5mg*56片", "施维雅制药有限公司", "心血管药物", "盒", 12000, 250, 25, "normal"},
{uuid.New().String(), "利伐沙班片", "利伐沙班", "10mg*10片", "拜耳医药保健有限公司", "心血管药物", "盒", 8500, 300, 30, "normal"},
}
// 批量插入
result := db.CreateInBatches(medicines, 100)
if result.Error != nil {
log.Fatal("导入数据失败:", result.Error)
}
fmt.Printf("成功导入 %d 条药品数据\n", result.RowsAffected)
}
-- 药品测试数据 (500条)
-- 分类: 抗生素、心血管药物、消化系统药物、呼吸系统药物、神经系统药物、内分泌药物、抗肿瘤药物、中成药等
-- 抗生素类 (50条)
INSERT INTO medicines (id, name, generic_name, specification, manufacturer, category, unit, price, stock, stock_warning, status) VALUES
('med-001', '阿莫西林胶囊', '阿莫西林', '0.25g*24粒', '华北制药股份有限公司', '抗生素', '盒', 1200, 500, 50, 'normal'),
('med-002', '头孢克肟分散片', '头孢克肟', '0.1g*6片', '石药集团欧意药业有限公司', '抗生素', '盒', 2800, 300, 50, 'normal'),
('med-003', '阿奇霉素片', '阿奇霉素', '0.25g*6片', '辉瑞制药有限公司', '抗生素', '盒', 3500, 450, 50, 'normal'),
('med-004', '左氧氟沙星片', '左氧氟沙星', '0.5g*6片', '第一三共制药(北京)有限公司', '抗生素', '盒', 2200, 380, 50, 'normal'),
('med-005', '罗红霉素胶囊', '罗红霉素', '150mg*12粒', '扬子江药业集团有限公司', '抗生素', '盒', 1800, 420, 50, 'normal'),
('med-006', '头孢呋辛酯片', '头孢呋辛酯', '0.25g*12片', '葛兰素史克制药有限公司', '抗生素', '盒', 4200, 280, 50, 'normal'),
('med-007', '克拉霉素片', '克拉霉素', '0.25g*12片', '雅培制药有限公司', '抗生素', '盒', 3800, 350, 50, 'normal'),
('med-008', '头孢氨苄胶囊', '头孢氨苄', '0.25g*24粒', '哈药集团制药六厂', '抗生素', '盒', 1500, 520, 50, 'normal'),
('med-009', '诺氟沙星胶囊', '诺氟沙星', '0.1g*24粒', '湖南千金湘江药业股份有限公司', '抗生素', '盒', 800, 600, 50, 'normal'),
('med-010', '环丙沙星片', '环丙沙星', '0.25g*12片', '拜耳医药保健有限公司', '抗生素', '盒', 2600, 320, 50, 'normal'),
('med-011', '头孢拉定胶囊', '头孢拉定', '0.25g*24粒', '石家庄四药有限公司', '抗生素', '盒', 1600, 480, 50, 'normal'),
('med-012', '红霉素肠溶片', '红霉素', '0.25g*24片', '上海信谊药厂有限公司', '抗生素', '盒', 1000, 550, 50, 'normal'),
('med-013', '头孢克洛胶囊', '头孢克洛', '0.25g*12粒', '礼来苏州制药有限公司', '抗生素', '盒', 3200, 360, 50, 'normal'),
('med-014', '莫西沙星片', '莫西沙星', '0.4g*5片', '拜耳医药保健有限公司', '抗生素', '盒', 8500, 180, 30, 'normal'),
('med-015', '头孢地尼胶囊', '头孢地尼', '0.1g*12粒', '藤泽药品工业株式会社', '抗生素', '盒', 4500, 250, 50, 'normal'),
('med-016', '氨苄西林胶囊', '氨苄西林', '0.25g*24粒', '华北制药股份有限公司', '抗生素', '盒', 1100, 490, 50, 'normal'),
('med-017', '头孢泊肟酯片', '头孢泊肟酯', '0.1g*6片', '山德士(中国)制药有限公司', '抗生素', '盒', 3600, 310, 50, 'normal'),
('med-018', '克林霉素胶囊', '克林霉素', '0.15g*12粒', '辉瑞制药有限公司', '抗生素', '盒', 2400, 400, 50, 'normal'),
('med-019', '头孢丙烯片', '头孢丙烯', '0.25g*12片', '百时美施贵宝公司', '抗生素', '盒', 3900, 290, 50, 'normal'),
('med-020', '米诺环素胶囊', '米诺环素', '50mg*20粒', '惠氏制药有限公司', '抗生素', '盒', 5200, 220, 40, 'normal'),
('med-021', '头孢唑林钠', '头孢唑林钠', '0.5g*10支', '华北制药股份有限公司', '抗生素', '盒', 4800, 260, 50, 'normal'),
('med-022', '多西环素片', '多西环素', '0.1g*12片', '上海信谊药厂有限公司', '抗生素', '盒', 1400, 460, 50, 'normal'),
('med-023', '头孢曲松钠', '头孢曲松钠', '1.0g*10支', '罗氏制药有限公司', '抗生素', '盒', 12000, 150, 30, 'normal'),
('med-024', '甲硝唑片', '甲硝唑', '0.2g*100片', '湖南千金湘江药业股份有限公司', '抗生素', '瓶', 600, 700, 50, 'normal'),
('med-025', '头孢哌酮钠舒巴坦钠', '头孢哌酮钠舒巴坦钠', '1.5g*10支', '辉瑞制药有限公司', '抗生素', '盒', 18000, 120, 30, 'normal'),
('med-026', '替硝唑片', '替硝唑', '0.5g*8片', '扬子江药业集团有限公司', '抗生素', '盒', 900, 580, 50, 'normal'),
('med-027', '头孢他啶', '头孢他啶', '1.0g*10支', '葛兰素史克制药有限公司', '抗生素', '盒', 15000, 130, 30, 'normal'),
('med-028', '奥硝唑胶囊', '奥硝唑', '0.25g*16粒', '康恩贝制药股份有限公司', '抗生素', '盒', 1300, 470, 50, 'normal'),
('med-029', '头孢美唑钠', '头孢美唑钠', '1.0g*10支', '第一三共制药(北京)有限公司', '抗生素', '盒', 16000, 110, 30, 'normal'),
('med-030', '林可霉素胶囊', '林可霉素', '0.25g*24粒', '华北制药股份有限公司', '抗生素', '盒', 1700, 440, 50, 'normal'),
('med-031', '头孢西丁钠', '头孢西丁钠', '1.0g*10支', '默沙东制药有限公司', '抗生素', '盒', 14000, 140, 30, 'normal'),
('med-032', '磷霉素氨丁三醇散', '磷霉素氨丁三醇', '3g*1袋', '赞邦制药有限公司', '抗生素', '盒', 4500, 240, 40, 'normal'),
('med-033', '头孢吡肟', '头孢吡肟', '1.0g*10支', '百时美施贵宝公司', '抗生素', '盒', 17000, 100, 30, 'normal'),
('med-034', '呋喃妥因肠溶片', '呋喃妥因', '0.1g*24片', '上海信谊药厂有限公司', '抗生素', '盒', 1200, 500, 50, 'normal'),
('med-035', '美罗培南', '美罗培南', '0.5g*10支', '住友制药(苏州)有限公司', '抗生素', '盒', 28000, 80, 20, 'normal'),
('med-036', '呋喃唑酮片', '呋喃唑酮', '0.1g*100片', '湖南千金湘江药业股份有限公司', '抗生素', '瓶', 500, 750, 50, 'normal'),
('med-037', '亚胺培南西司他丁钠', '亚胺培南西司他丁钠', '0.5g*10支', '默沙东制药有限公司', '抗生素', '盒', 32000, 70, 20, 'normal'),
('med-038', '利福平胶囊', '利福平', '0.15g*100粒', '沈阳红旗制药有限公司', '抗生素', '瓶', 2800, 350, 50, 'normal'),
('med-039', '比阿培南', '比阿培南', '0.3g*10支', '明治制果药业株式会社', '抗生素', '盒', 35000, 60, 20, 'normal'),
('med-040', '异烟肼片', '异烟肼', '0.1g*100片', '沈阳红旗制药有限公司', '抗生素', '瓶', 800, 600, 50, 'normal'),
('med-041', '万古霉素', '万古霉素', '0.5g*10支', '礼来苏州制药有限公司', '抗生素', '盒', 38000, 50, 20, 'normal'),
('med-042', '吡嗪酰胺片', '吡嗪酰胺', '0.25g*100片', '沈阳红旗制药有限公司', '抗生素', '瓶', 1500, 450, 50, 'normal'),
('med-043', '替考拉宁', '替考拉宁', '0.2g*3支', '赛诺菲安万特制药有限公司', '抗生素', '盒', 42000, 40, 15, 'normal'),
('med-044', '乙胺丁醇片', '乙胺丁醇', '0.25g*100片', '沈阳红旗制药有限公司', '抗生素', '瓶', 1800, 420, 50, 'normal'),
('med-045', '利奈唑胺片', '利奈唑胺', '0.6g*10片', '辉瑞制药有限公司', '抗生素', '盒', 48000, 35, 15, 'normal'),
('med-046', '链霉素', '链霉素', '1.0g*10支', '华北制药股份有限公司', '抗生素', '盒', 3500, 300, 50, 'normal'),
('med-047', '达托霉素', '达托霉素', '0.5g*1支', '默沙东制药有限公司', '抗生素', '盒', 55000, 30, 10, 'normal'),
('med-048', '卡那霉素', '卡那霉素', '1.0g*10支', '华北制药股份有限公司', '抗生素', '盒', 4200, 280, 50, 'normal'),
('med-049', '替加环素', '替加环素', '50mg*10支', '惠氏制药有限公司', '抗生素', '盒', 62000, 25, 10, 'normal'),
('med-050', '庆大霉素', '庆大霉素', '8万单位*10支', '华北制药股份有限公司', '抗生素', '盒', 2800, 380, 50, 'normal'),
-- 心血管药物 (60条)
('med-051', '阿司匹林肠溶片', '阿司匹林', '100mg*30片', '拜耳医药保健有限公司', '心血管药物', '盒', 800, 800, 80, 'normal'),
('med-052', '硝苯地平缓释片', '硝苯地平', '20mg*30片', '拜耳医药保健有限公司', '心血管药物', '盒', 2200, 600, 60, 'normal'),
('med-053', '氨氯地平片', '氨氯地平', '5mg*7片', '辉瑞制药有限公司', '心血管药物', '盒', 1800, 700, 70, 'normal'),
('med-054', '美托洛尔片', '美托洛尔', '25mg*20片', '阿斯利康制药有限公司', '心血管药物', '盒', 1500, 650, 65, 'normal'),
('med-055', '缬沙坦胶囊', '缬沙坦', '80mg*7粒', '诺华制药有限公司', '心血管药物', '盒', 3200, 550, 55, 'normal'),
('med-056', '氯吡格雷片', '氯吡格雷', '75mg*7片', '赛诺菲安万特制药有限公司', '心血管药物', '盒', 4500, 500, 50, 'normal'),
('med-057', '阿托伐他汀钙片', '阿托伐他汀钙', '20mg*7片', '辉瑞制药有限公司', '心血管药物', '盒', 3800, 580, 58, 'normal'),
('med-058', '依那普利片', '依那普利', '10mg*16片', '默沙东制药有限公司', '心血管药物', '盒', 2400, 620, 62, 'normal'),
('med-059', '比索洛尔片', '比索洛尔', '5mg*10片', '默克制药有限公司', '心血管药物', '盒', 1600, 680, 68, 'normal'),
('med-060', '厄贝沙坦片', '厄贝沙坦', '150mg*7片', '赛诺菲安万特制药有限公司', '心血管药物', '盒', 2800, 600, 60, 'normal'),
('med-061', '辛伐他汀片', '辛伐他汀', '20mg*10片', '默沙东制药有限公司', '心血管药物', '盒', 2200, 640, 64, 'normal'),
('med-062', '卡托普利片', '卡托普利', '25mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 1200, 750, 75, 'normal'),
('med-063', '硝酸异山梨酯片', '硝酸异山梨酯', '5mg*100片', '北京益民药业有限公司', '心血管药物', '瓶', 800, 800, 80, 'normal'),
('med-064', '螺内酯片', '螺内酯', '20mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 1000, 720, 72, 'normal'),
('med-065', '地高辛片', '地高辛', '0.25mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 1500, 650, 65, 'normal'),
('med-066', '呋塞米片', '呋塞米', '20mg*100片', '赛诺菲安万特制药有限公司', '心血管药物', '瓶', 600, 850, 85, 'normal'),
('med-067', '氢氯噻嗪片', '氢氯噻嗪', '25mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 500, 900, 90, 'normal'),
('med-068', '胺碘酮片', '胺碘酮', '0.2g*30片', '赛诺菲安万特制药有限公司', '心血管药物', '盒', 3500, 520, 52, 'normal'),
('med-069', '瑞舒伐他汀钙片', '瑞舒伐他汀钙', '10mg*7片', '阿斯利康制药有限公司', '心血管药物', '盒', 4200, 480, 48, 'normal'),
('med-070', '替米沙坦片', '替米沙坦', '80mg*7片', '勃林格殷格翰制药有限公司', '心血管药物', '盒', 3600, 540, 54, 'normal'),
('med-071', '培哚普利片', '培哚普利', '4mg*30片', '施维雅制药有限公司', '心血管药物', '盒', 4800, 450, 45, 'normal'),
('med-072', '坎地沙坦酯片', '坎地沙坦酯', '8mg*7片', '阿斯利康制药有限公司', '心血管药物', '盒', 3200, 560, 56, 'normal'),
('med-073', '非洛地平缓释片', '非洛地平', '5mg*10片', '阿斯利康制药有限公司', '心血管药物', '盒', 2800, 590, 59, 'normal'),
('med-074', '拉西地平片', '拉西地平', '4mg*7片', '葛兰素史克制药有限公司', '心血管药物', '盒', 3400, 530, 53, 'normal'),
('med-075', '尼群地平片', '尼群地平', '10mg*100片', '山东鲁抗医药股份有限公司', '心血管药物', '瓶', 1800, 670, 67, 'normal'),
('med-076', '卡维地洛片', '卡维地洛', '10mg*20片', '罗氏制药有限公司', '心血管药物', '盒', 2600, 610, 61, 'normal'),
('med-077', '索他洛尔片', '索他洛尔', '80mg*20片', '百时美施贵宝公司', '心血管药物', '盒', 3000, 570, 57, 'normal'),
('med-078', '普罗帕酮片', '普罗帕酮', '50mg*100片', '雅培制药有限公司', '心血管药物', '瓶', 2400, 630, 63, 'normal'),
('med-079', '维拉帕米片', '维拉帕米', '40mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 1400, 700, 70, 'normal'),
('med-080', '地尔硫卓片', '地尔硫卓', '30mg*100片', '天津田边制药有限公司', '心血管药物', '瓶', 1600, 680, 68, 'normal'),
('med-081', '单硝酸异山梨酯片', '单硝酸异山梨酯', '20mg*30片', '鲁南贝特制药有限公司', '心血管药物', '盒', 1200, 740, 74, 'normal'),
('med-082', '曲美他嗪片', '曲美他嗪', '20mg*30片', '施维雅制药有限公司', '心血管药物', '盒', 3800, 500, 50, 'normal'),
('med-083', '匹伐他汀钙片', '匹伐他汀钙', '2mg*7片', '第一三共制药(北京)有限公司', '心血管药物', '盒', 4500, 460, 46, 'normal'),
('med-084', '氟伐他汀钠缓释片', '氟伐他汀钠', '80mg*7片', '诺华制药有限公司', '心血管药物', '盒', 5200, 420, 42, 'normal'),
('med-085', '普伐他汀钠片', '普伐他汀钠', '10mg*10片', '百时美施贵宝公司', '心血管药物', '盒', 3600, 540, 54, 'normal'),
('med-086', '依折麦布片', '依折麦布', '10mg*7片', '默沙东制药有限公司', '心血管药物', '盒', 5800, 380, 38, 'normal'),
('med-087', '非诺贝特胶囊', '非诺贝特', '0.1g*30粒', '雅培制药有限公司', '心血管药物', '盒', 3200, 560, 56, 'normal'),
('med-088', '贝特类降脂药', '苯扎贝特', '0.2g*30片', '上海信谊药厂有限公司', '心血管药物', '盒', 2800, 590, 59, 'normal'),
('med-089', '托拉塞米片', '托拉塞米', '10mg*20片', '罗氏制药有限公司', '心血管药物', '盒', 2400, 620, 62, 'normal'),
('med-090', '吲达帕胺片', '吲达帕胺', '2.5mg*30片', '施维雅制药有限公司', '心血管药物', '盒', 1800, 670, 67, 'normal'),
('med-091', '氨苯蝶啶片', '氨苯蝶啶', '50mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 800, 800, 80, 'normal'),
('med-092', '雷米普利片', '雷米普利', '5mg*7片', '赛诺菲安万特制药有限公司', '心血管药物', '盒', 3400, 530, 53, 'normal'),
('med-093', '贝那普利片', '贝那普利', '10mg*7片', '诺华制药有限公司', '心血管药物', '盒', 3000, 570, 57, 'normal'),
('med-094', '福辛普利钠片', '福辛普利钠', '10mg*7片', '百时美施贵宝公司', '心血管药物', '盒', 3600, 540, 54, 'normal'),
('med-095', '赖诺普利片', '赖诺普利', '10mg*7片', '阿斯利康制药有限公司', '心血管药物', '盒', 2800, 590, 59, 'normal'),
('med-096', '奥美沙坦酯片', '奥美沙坦酯', '20mg*7片', '第一三共制药(北京)有限公司', '心血管药物', '盒', 4200, 480, 48, 'normal'),
('med-097', '阿利吉仑片', '阿利吉仑', '150mg*7片', '诺华制药有限公司', '心血管药物', '盒', 6800, 320, 32, 'normal'),
('med-098', '沙库巴曲缬沙坦钠片', '沙库巴曲缬沙坦钠', '50mg*28片', '诺华制药有限公司', '心血管药物', '盒', 18000, 200, 20, 'normal'),
('med-099', '伊伐布雷定片', '伊伐布雷定', '5mg*56片', '施维雅制药有限公司', '心血管药物', '盒', 12000, 250, 25, 'normal'),
('med-100', '利伐沙班片', '利伐沙班', '10mg*10片', '拜耳医药保健有限公司', '心血管药物', '盒', 8500, 300, 30, 'normal'),
('med-101', '达比加群酯胶囊', '达比加群酯', '110mg*10粒', '勃林格殷格翰制药有限公司', '心血管药物', '盒', 9200, 280, 28, 'normal'),
('med-102', '阿哌沙班片', '阿哌沙班', '5mg*20片', '百时美施贵宝公司', '心血管药物', '盒', 10500, 260, 26, 'normal'),
('med-103', '华法林钠片', '华法林钠', '2.5mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 1500, 650, 65, 'normal'),
('med-104', '硫酸氢氯吡格雷片', '硫酸氢氯吡格雷', '75mg*20片', '赛诺菲安万特制药有限公司', '心血管药物', '盒', 8800, 290, 29, 'normal'),
('med-105', '替格瑞洛片', '替格瑞洛', '90mg*56片', '阿斯利康制药有限公司', '心血管药物', '盒', 15000, 220, 22, 'normal'),
('med-106', '西洛他唑片', '西洛他唑', '50mg*20片', '大冢制药有限公司', '心血管药物', '盒', 4200, 480, 48, 'normal'),
('med-107', '双嘧达莫片', '双嘧达莫', '25mg*100片', '上海信谊药厂有限公司', '心血管药物', '瓶', 1200, 740, 74, 'normal'),
('med-108', '前列地尔注射液', '前列地尔', '10μg*5支', '北京泰德制药股份有限公司', '心血管药物', '盒', 28000, 150, 15, 'normal'),
('med-109', '曲克芦丁片', '曲克芦丁', '0.2g*100片', '山东鲁抗医药股份有限公司', '心血管药物', '瓶', 800, 800, 80, 'normal'),
('med-110', '银杏叶提取物片', '银杏叶提取物', '19.2mg*30片', '德国威玛舒培博士药厂', '心血管药物', '盒', 3500, 520, 52, 'normal'),
-- 消化系统药物 (60条)
('med-111', '奥美拉唑肠溶胶囊', '奥美拉唑', '20mg*14粒', '阿斯利康制药有限公司', '消化系统药物', '盒', 1800, 700, 70, 'normal'),
('med-112', '兰索拉唑肠溶片', '兰索拉唑', '30mg*7片', '武田制药有限公司', '消化系统药物', '盒', 2400, 620, 62, 'normal'),
('med-113', '雷贝拉唑钠肠溶片', '雷贝拉唑钠', '10mg*7片', '卫材(中国)药业有限公司', '消化系统药物', '盒', 2800, 590, 59, 'normal'),
('med-114', '泮托拉唑钠肠溶片', '泮托拉唑钠', '40mg*7片', '拜耳医药保健有限公司', '消化系统药物', '盒', 2200, 640, 64, 'normal'),
('med-115', '埃索美拉唑镁肠溶片', '埃索美拉唑镁', '20mg*7片', '阿斯利康制药有限公司', '消化系统药物', '盒', 3200, 560, 56, 'normal'),
('med-116', '法莫替丁片', '法莫替丁', '20mg*20片', '默沙东制药有限公司', '消化系统药物', '盒', 1200, 740, 74, 'normal'),
('med-117', '雷尼替丁胶囊', '雷尼替丁', '150mg*12粒', '葛兰素史克制药有限公司', '消化系统药物', '盒', 1500, 680, 68, 'normal'),
('med-118', '西咪替丁片', '西咪替丁', '0.2g*100片', '上海信谊药厂有限公司', '消化系统药物', '瓶', 800, 800, 80, 'normal'),
('med-119', '多潘立酮片', '多潘立酮', '10mg*30片', '西安杨森制药有限公司', '消化系统药物', '盒', 1600, 670, 67, 'normal'),
('med-120', '莫沙必利片', '莫沙必利', '5mg*30片', '住友制药(苏州)有限公司', '消化系统药物', '盒', 2400, 620, 62, 'normal'),
('med-121', '伊托必利片', '伊托必利', '50mg*12片', '卫材(中国)药业有限公司', '消化系统药物', '盒', 2800, 590, 59, 'normal'),
('med-122', '枸橼酸莫沙必利片', '枸橼酸莫沙必利', '5mg*30片', '成都康弘药业集团股份有限公司', '消化系统药物', '盒', 2200, 640, 64, 'normal'),
('med-123', '铝碳酸镁片', '铝碳酸镁', '0.5g*20片', '拜耳医药保健有限公司', '消化系统药物', '盒', 1800, 660, 66, 'normal'),
('med-124', '硫糖铝混悬液', '硫糖铝', '1g*10袋', '常州四药制药有限公司', '消化系统药物', '盒', 1200, 740, 74, 'normal'),
('med-125', '复方谷氨酰胺肠溶胶囊', '复方谷氨酰胺', '0.15g*24粒', '海南赞邦制药有限公司', '消化系统药物', '盒', 2600, 600, 60, 'normal'),
('med-126', '瑞巴派特片', '瑞巴派特', '0.1g*12片', '大冢制药有限公司', '消化系统药物', '盒', 3200, 560, 56, 'normal'),
('med-127', '替普瑞酮胶囊', '替普瑞酮', '50mg*20粒', '卫材(中国)药业有限公司', '消化系统药物', '盒', 3600, 530, 53, 'normal'),
('med-128', '康复新液', '美洲大蠊干燥虫体提取物', '10ml*6支', '四川好医生攀西药业有限责任公司', '消化系统药物', '盒', 2800, 590, 59, 'normal'),
('med-129', '蒙脱石散', '蒙脱石', '3g*10袋', '博福-益普生(天津)制药有限公司', '消化系统药物', '盒', 1500, 680, 68, 'normal'),
('med-130', '双歧杆菌三联活菌胶囊', '双歧杆菌三联活菌', '0.21g*24粒', '上海上药信谊药厂有限公司', '消化系统药物', '盒', 2200, 640, 64, 'normal'),
('med-131', '枯草杆菌二联活菌颗粒', '枯草杆菌二联活菌', '0.5g*10袋', '北京韩美药品有限公司', '消化系统药物', '盒', 1800, 660, 66, 'normal'),
('med-132', '酪酸梭菌活菌片', '酪酸梭菌活菌', '50mg*30片', '大连辉瑞制药有限公司', '消化系统药物', '盒', 2400, 620, 62, 'normal'),
('med-133', '地衣芽孢杆菌活菌胶囊', '地衣芽孢杆菌活菌', '0.25g*12粒', '北京汉典制药有限公司', '消化系统药物', '盒', 2000, 650, 65, 'normal'),
('med-134', '双歧杆菌四联活菌片', '双歧杆菌四联活菌', '0.5g*24片', '杭州远大生物制药有限公司', '消化系统药物', '盒', 2600, 600, 60, 'normal'),
('med-135', '乳果糖口服溶液', '乳果糖', '15ml*6袋', '雅培制药有限公司', '消化系统药物', '盒', 3200, 560, 56, 'normal'),
('med-136', '聚乙二醇4000散', '聚乙二醇4000', '10g*6袋', '深圳万和制药有限公司', '消化系统药物', '盒', 2800, 590, 59, 'normal'),
('med-137', '比沙可啶肠溶片', '比沙可啶', '5mg*30片', '拜耳医药保健有限公司', '消化系统药物', '盒', 1200, 740, 74, 'normal'),
('med-138', '开塞露', '甘油', '20ml*10支', '山东鲁抗医药股份有限公司', '消化系统药物', '盒', 600, 850, 85, 'normal'),
('med-139', '洛哌丁胺胶囊', '洛哌丁胺', '2mg*6粒', '西安杨森制药有限公司', '消化系统药物', '盒', 1800, 660, 66, 'normal'),
('med-140', '消旋卡多曲颗粒', '消旋卡多曲', '10mg*6袋', '法国Bioprojet制药公司', '消化系统药物', '盒', 3500, 520, 52, 'normal'),
('med-141', '复方嗜酸乳杆菌片', '复方嗜酸乳杆菌', '0.5g*24片', '江西济民可信药业有限公司', '消化系统药物', '盒', 1600, 670, 67, 'normal'),
('med-142', '复方黄连素片', '复方黄连素', '0.3g*100片', '上海信谊药厂有限公司', '消化系统药物', '瓶', 800, 800, 80, 'normal'),
('med-143', '盐酸小檗碱片', '盐酸小檗碱', '0.1g*100片', '东北制药集团沈阳第一制药有限公司', '消化系统药物', '瓶', 600, 850, 85, 'normal'),
('med-144', '复方谷氨酰胺颗粒', '复方谷氨酰胺', '5g*12袋', '海南赞邦制药有限公司', '消化系统药物', '盒', 3200, 560, 56, 'normal'),
('med-145', '熊去氧胆酸胶囊', '熊去氧胆酸', '250mg*20粒', '意大利Alfa Wassermann制药公司', '消化系统药物', '盒', 4500, 480, 48, 'normal'),
('med-146', '腺苷蛋氨酸肠溶片', '腺苷蛋氨酸', '0.5g*10片', '雅培制药有限公司', '消化系统药物', '盒', 12000, 250, 25, 'normal'),
('med-147', '多烯磷脂酰胆碱胶囊', '多烯磷脂酰胆碱', '228mg*50粒', '赛诺菲安万特制药有限公司', '消化系统药物', '盒', 8500, 300, 30, 'normal'),
('med-148', '双环醇片', '双环醇', '25mg*36片', '北京协和药厂', '消化系统药物', '盒', 3800, 510, 51, 'normal'),
('med-149', '水飞蓟宾胶囊', '水飞蓟宾', '35mg*30粒', '天津天士力制药股份有限公司', '消化系统药物', '盒', 2800, 590, 59, 'normal'),
('med-150', '甘草酸二铵肠溶胶囊', '甘草酸二铵', '50mg*36粒', '正大天晴药业集团股份有限公司', '消化系统药物', '盒', 3200, 560, 56, 'normal'),
('med-151', '复方甘草酸苷片', '复方甘草酸苷', '25mg*60片', '日本Minophagen制药株式会社', '消化系统药物', '盒', 6800, 350, 35, 'normal'),
('med-152', '还原型谷胱甘肽片', '还原型谷胱甘肽', '0.1g*12片', '重庆药友制药有限责任公司', '消化系统药物', '盒', 4200, 480, 48, 'normal'),
('med-153', '门冬氨酸鸟氨酸颗粒', '门冬氨酸鸟氨酸', '3g*10袋', '德国Merz制药公司', '消化系统药物', '盒', 15000, 220, 22, 'normal'),
('med-154', '复方阿嗪米特肠溶片', '复方阿嗪米特', '0.15g*30片', '北京韩美药品有限公司', '消化系统药物', '盒', 2400, 620, 62, 'normal'),
('med-155', '胰酶肠溶胶囊', '胰酶', '0.15g*30粒', '雅培制药有限公司', '消化系统药物', '盒', 3500, 520, 52, 'normal'),
('med-156', '复方消化酶胶囊', '复方消化酶', '0.3g*30粒', '北京双鹤药业股份有限公司', '消化系统药物', '盒', 1800, 660, 66, 'normal'),
('med-157', '米曲菌胰酶片', '米曲菌胰酶', '40mg*30片', '日本Eisai株式会社', '消化系统药物', '盒', 4200, 480, 48, 'normal'),
('med-158', '复方胃蛋白酶颗粒', '复方胃蛋白酶', '2.5g*10袋', '山东鲁抗医药股份有限公司', '消化系统药物', '盒', 1200, 740, 74, 'normal'),
('med-159', '复方嗜酸乳杆菌颗粒', '复方嗜酸乳杆菌', '1g*12袋', '内蒙古双奇药业股份有限公司', '消化系统药物', '盒', 2200, 640, 64, 'normal'),
('med-160', '复方谷氨酰胺肠溶片', '复方谷氨酰胺', '0.15g*30片', '海南赞邦制药有限公司', '消化系统药物', '盒', 2800, 590, 59, 'normal'),
('med-161', '复方消化酶片', '复方消化酶', '0.3g*100片', '上海信谊药厂有限公司', '消化系统药物', '瓶', 1500, 680, 68, 'normal'),
('med-162', '复方胰酶片', '复方胰酶', '0.3g*100片', '北京双鹤药业股份有限公司', '消化系统药物', '瓶', 1600, 670, 67, 'normal'),
('med-163', '复方胃蛋白酶散', '复方胃蛋白酶', '2g*10袋', '山东鲁抗医药股份有限公司', '消化系统药物', '盒', 1000, 760, 76, 'normal'),
('med-164', '复方嗜酸乳杆菌散', '复方嗜酸乳杆菌', '1g*12袋', '内蒙古双奇药业股份有限公司', '消化系统药物', '盒', 2000, 650, 65, 'normal'),
('med-165', '复方谷氨酰胺散', '复方谷氨酰胺', '5g*12袋', '海南赞邦制药有限公司', '消化系统药物', '盒', 3000, 570, 57, 'normal'),
('med-166', '复方消化酶散', '复方消化酶', '2g*10袋', '北京双鹤药业股份有限公司', '消化系统药物', '盒', 1400, 700, 70, 'normal'),
('med-167', '复方胰酶散', '复方胰酶', '2g*10袋', '北京双鹤药业股份有限公司', '消化系统药物', '盒', 1500, 680, 68, 'normal'),
('med-168', '复方胃蛋白酶片', '复方胃蛋白酶', '0.3g*100片', '山东鲁抗医药股份有限公司', '消化系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-169', '复方嗜酸乳杆菌胶囊', '复方嗜酸乳杆菌', '0.5g*24粒', '内蒙古双奇药业股份有限公司', '消化系统药物', '盒', 2200, 640, 64, 'normal'),
('med-170', '复方谷氨酰胺胶囊', '复方谷氨酰胺', '0.15g*24粒', '海南赞邦制药有限公司', '消化系统药物', '盒', 2800, 590, 59, 'normal'),
-- 呼吸系统药物 (60条)
('med-171', '氨溴索口服溶液', '氨溴索', '15mg/5ml*100ml', '勃林格殷格翰制药有限公司', '呼吸系统药物', '瓶', 2800, 590, 59, 'normal'),
('med-172', '乙酰半胱氨酸泡腾片', '乙酰半胱氨酸', '0.6g*10片', '赞邦制药有限公司', '呼吸系统药物', '盒', 3200, 560, 56, 'normal'),
('med-173', '羧甲司坦口服溶液', '羧甲司坦', '250mg/5ml*100ml', '法国Laboratoires Negma', '呼吸系统药物', '瓶', 2400, 620, 62, 'normal'),
('med-174', '溴己新片', '溴己新', '8mg*100片', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-175', '氨溴特罗口服溶液', '氨溴特罗', '15ml*10支', '勃林格殷格翰制药有限公司', '呼吸系统药物', '盒', 3800, 510, 51, 'normal'),
('med-176', '复方甘草片', '复方甘草', '50片', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 600, 850, 85, 'normal'),
('med-177', '右美沙芬片', '右美沙芬', '15mg*100片', '山东鲁抗医药股份有限公司', '呼吸系统药物', '瓶', 800, 800, 80, 'normal'),
('med-178', '可待因桔梗片', '可待因桔梗', '12片', '东北制药集团沈阳第一制药有限公司', '呼吸系统药物', '盒', 1500, 680, 68, 'normal'),
('med-179', '复方福尔可定口服溶液', '复方福尔可定', '100ml', '太极集团重庆桐君阁药厂有限公司', '呼吸系统药物', '瓶', 1800, 660, 66, 'normal'),
('med-180', '苯丙哌林片', '苯丙哌林', '20mg*100片', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1000, 760, 76, 'normal'),
('med-181', '沙丁胺醇气雾剂', '沙丁胺醇', '100μg*200揿', '葛兰素史克制药有限公司', '呼吸系统药物', '支', 2200, 640, 64, 'normal'),
('med-182', '特布他林片', '特布他林', '2.5mg*20片', '阿斯利康制药有限公司', '呼吸系统药物', '盒', 1800, 660, 66, 'normal'),
('med-183', '异丙托溴铵气雾剂', '异丙托溴铵', '20μg*200揿', '勃林格殷格翰制药有限公司', '呼吸系统药物', '支', 2800, 590, 59, 'normal'),
('med-184', '复方异丙托溴铵溶液', '复方异丙托溴铵', '2.5ml*20支', '勃林格殷格翰制药有限公司', '呼吸系统药物', '盒', 4500, 480, 48, 'normal'),
('med-185', '布地奈德气雾剂', '布地奈德', '200μg*200揿', '阿斯利康制药有限公司', '呼吸系统药物', '支', 6800, 350, 35, 'normal'),
('med-186', '丙酸氟替卡松气雾剂', '丙酸氟替卡松', '125μg*120揿', '葛兰素史克制药有限公司', '呼吸系统药物', '支', 8500, 300, 30, 'normal'),
('med-187', '沙美特罗替卡松粉吸入剂', '沙美特罗替卡松', '50μg/250μg*60吸', '葛兰素史克制药有限公司', '呼吸系统药物', '支', 12000, 250, 25, 'normal'),
('med-188', '布地奈德福莫特罗粉吸入剂', '布地奈德福莫特罗', '160μg/4.5μg*60吸', '阿斯利康制药有限公司', '呼吸系统药物', '支', 15000, 220, 22, 'normal'),
('med-189', '噻托溴铵粉吸入剂', '噻托溴铵', '18μg*30粒', '勃林格殷格翰制药有限公司', '呼吸系统药物', '盒', 18000, 200, 20, 'normal'),
('med-190', '茶碱缓释片', '茶碱', '0.1g*100片', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-191', '氨茶碱片', '氨茶碱', '0.1g*100片', '山东鲁抗医药股份有限公司', '呼吸系统药物', '瓶', 800, 800, 80, 'normal'),
('med-192', '多索茶碱片', '多索茶碱', '0.2g*12片', '韩国Kyung Dong制药公司', '呼吸系统药物', '盒', 2400, 620, 62, 'normal'),
('med-193', '孟鲁司特钠片', '孟鲁司特钠', '10mg*5片', '默沙东制药有限公司', '呼吸系统药物', '盒', 4500, 480, 48, 'normal'),
('med-194', '扎鲁司特片', '扎鲁司特', '20mg*20片', '阿斯利康制药有限公司', '呼吸系统药物', '盒', 5200, 450, 45, 'normal'),
('med-195', '色甘酸钠气雾剂', '色甘酸钠', '1mg*112揿', '赛诺菲安万特制药有限公司', '呼吸系统药物', '支', 3200, 560, 56, 'normal'),
('med-196', '酮替芬片', '酮替芬', '1mg*100片', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1000, 760, 76, 'normal'),
('med-197', '奥马珠单抗注射液', '奥马珠单抗', '150mg*1支', '诺华制药有限公司', '呼吸系统药物', '盒', 180000, 50, 10, 'normal'),
('med-198', '乙酰半胱氨酸颗粒', '乙酰半胱氨酸', '0.2g*20袋', '赞邦制药有限公司', '呼吸系统药物', '盒', 2800, 590, 59, 'normal'),
('med-199', '桃金娘油肠溶胶囊', '桃金娘油', '0.12g*20粒', '德国Pohl-Boskamp公司', '呼吸系统药物', '盒', 3500, 520, 52, 'normal'),
('med-200', '羧甲司坦片', '羧甲司坦', '0.25g*24片', '法国Laboratoires Negma', '呼吸系统药物', '盒', 2200, 640, 64, 'normal'),
('med-201', '氨溴索片', '氨溴索', '30mg*20片', '勃林格殷格翰制药有限公司', '呼吸系统药物', '盒', 2400, 620, 62, 'normal'),
('med-202', '溴己新口服溶液', '溴己新', '4mg/5ml*100ml', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1500, 680, 68, 'normal'),
('med-203', '复方甘草口服溶液', '复方甘草', '100ml', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 800, 800, 80, 'normal'),
('med-204', '右美沙芬口服溶液', '右美沙芬', '15mg/5ml*100ml', '山东鲁抗医药股份有限公司', '呼吸系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-205', '可待因片', '可待因', '15mg*100片', '东北制药集团沈阳第一制药有限公司', '呼吸系统药物', '瓶', 2000, 650, 65, 'normal'),
('med-206', '复方福尔可定片', '复方福尔可定', '12片', '太极集团重庆桐君阁药厂有限公司', '呼吸系统药物', '盒', 1600, 670, 67, 'normal'),
('med-207', '苯丙哌林口服溶液', '苯丙哌林', '10mg/5ml*100ml', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1400, 700, 70, 'normal'),
('med-208', '沙丁胺醇片', '沙丁胺醇', '2mg*100片', '葛兰素史克制药有限公司', '呼吸系统药物', '瓶', 1800, 660, 66, 'normal'),
('med-209', '特布他林气雾剂', '特布他林', '250μg*200揿', '阿斯利康制药有限公司', '呼吸系统药物', '支', 2400, 620, 62, 'normal'),
('med-210', '异丙托溴铵溶液', '异丙托溴铵', '0.25mg/ml*20ml', '勃林格殷格翰制药有限公司', '呼吸系统药物', '瓶', 3200, 560, 56, 'normal'),
('med-211', '复方异丙托溴铵气雾剂', '复方异丙托溴铵', '20μg/100μg*200揿', '勃林格殷格翰制药有限公司', '呼吸系统药物', '支', 4200, 480, 48, 'normal'),
('med-212', '布地奈德混悬液', '布地奈德', '1mg/2ml*20支', '阿斯利康制药有限公司', '呼吸系统药物', '盒', 8500, 300, 30, 'normal'),
('med-213', '丙酸氟替卡松鼻喷雾剂', '丙酸氟替卡松', '50μg*120揿', '葛兰素史克制药有限公司', '呼吸系统药物', '支', 6800, 350, 35, 'normal'),
('med-214', '沙美特罗气雾剂', '沙美特罗', '25μg*120揿', '葛兰素史克制药有限公司', '呼吸系统药物', '支', 5200, 450, 45, 'normal'),
('med-215', '布地奈德福莫特罗气雾剂', '布地奈德福莫特罗', '160μg/4.5μg*120揿', '阿斯利康制药有限公司', '呼吸系统药物', '支', 12000, 250, 25, 'normal'),
('med-216', '噻托溴铵溶液', '噻托溴铵', '2.5μg/ml*4ml', '勃林格殷格翰制药有限公司', '呼吸系统药物', '瓶', 15000, 220, 22, 'normal'),
('med-217', '茶碱片', '茶碱', '0.1g*100片', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1000, 760, 76, 'normal'),
('med-218', '氨茶碱缓释片', '氨茶碱', '0.1g*100片', '山东鲁抗医药股份有限公司', '呼吸系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-219', '多索茶碱口服溶液', '多索茶碱', '0.1g/5ml*100ml', '韩国Kyung Dong制药公司', '呼吸系统药物', '瓶', 2800, 590, 59, 'normal'),
('med-220', '孟鲁司特钠咀嚼片', '孟鲁司特钠', '5mg*5片', '默沙东制药有限公司', '呼吸系统药物', '盒', 3800, 510, 51, 'normal'),
('med-221', '扎鲁司特口服溶液', '扎鲁司特', '20mg/5ml*100ml', '阿斯利康制药有限公司', '呼吸系统药物', '瓶', 6200, 380, 38, 'normal'),
('med-222', '色甘酸钠溶液', '色甘酸钠', '20mg/2ml*20支', '赛诺菲安万特制药有限公司', '呼吸系统药物', '盒', 3500, 520, 52, 'normal'),
('med-223', '酮替芬口服溶液', '酮替芬', '1mg/5ml*100ml', '上海信谊药厂有限公司', '呼吸系统药物', '瓶', 1400, 700, 70, 'normal'),
('med-224', '乙酰半胱氨酸口服溶液', '乙酰半胱氨酸', '0.2g/5ml*100ml', '赞邦制药有限公司', '呼吸系统药物', '瓶', 3200, 560, 56, 'normal'),
('med-225', '桃金娘油口服溶液', '桃金娘油', '0.12g/5ml*100ml', '德国Pohl-Boskamp公司', '呼吸系统药物', '瓶', 4200, 480, 48, 'normal'),
('med-226', '羧甲司坦颗粒', '羧甲司坦', '0.25g*20袋', '法国Laboratoires Negma', '呼吸系统药物', '盒', 2600, 600, 60, 'normal'),
('med-227', '氨溴索颗粒', '氨溴索', '30mg*20袋', '勃林格殷格翰制药有限公司', '呼吸系统药物', '盒', 2800, 590, 59, 'normal'),
('med-228', '溴己新颗粒', '溴己新', '8mg*20袋', '上海信谊药厂有限公司', '呼吸系统药物', '盒', 1600, 670, 67, 'normal'),
('med-229', '复方甘草颗粒', '复方甘草', '5g*10袋', '上海信谊药厂有限公司', '呼吸系统药物', '盒', 1000, 760, 76, 'normal'),
('med-230', '右美沙芬颗粒', '右美沙芬', '15mg*20袋', '山东鲁抗医药股份有限公司', '呼吸系统药物', '盒', 1400, 700, 70, 'normal'),
-- 神经系统药物 (60条)
('med-231', '布洛芬缓释胶囊', '布洛芬', '0.3g*20粒', '中美天津史克制药有限公司', '神经系统药物', '盒', 1800, 660, 66, 'normal'),
('med-232', '对乙酰氨基酚片', '对乙酰氨基酚', '0.5g*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 800, 800, 80, 'normal'),
('med-233', '双氯芬酸钠缓释片', '双氯芬酸钠', '75mg*20片', '诺华制药有限公司', '神经系统药物', '盒', 2200, 640, 64, 'normal'),
('med-234', '塞来昔布胶囊', '塞来昔布', '0.2g*6粒', '辉瑞制药有限公司', '神经系统药物', '盒', 4500, 480, 48, 'normal'),
('med-235', '美洛昔康片', '美洛昔康', '7.5mg*10片', '勃林格殷格翰制药有限公司', '神经系统药物', '盒', 2800, 590, 59, 'normal'),
('med-236', '吲哚美辛肠溶片', '吲哚美辛', '25mg*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-237', '萘普生片', '萘普生', '0.25g*100片', '罗氏制药有限公司', '神经系统药物', '瓶', 1500, 680, 68, 'normal'),
('med-238', '洛索洛芬钠片', '洛索洛芬钠', '60mg*10片', '第一三共制药(北京)有限公司', '神经系统药物', '盒', 3200, 560, 56, 'normal'),
('med-239', '依托考昔片', '依托考昔', '60mg*5片', '默沙东制药有限公司', '神经系统药物', '盒', 5200, 450, 45, 'normal'),
('med-240', '加巴喷丁胶囊', '加巴喷丁', '0.3g*20粒', '辉瑞制药有限公司', '神经系统药物', '盒', 6800, 350, 35, 'normal'),
('med-241', '普瑞巴林胶囊', '普瑞巴林', '75mg*8粒', '辉瑞制药有限公司', '神经系统药物', '盒', 8500, 300, 30, 'normal'),
('med-242', '卡马西平片', '卡马西平', '0.1g*100片', '诺华制药有限公司', '神经系统药物', '瓶', 1800, 660, 66, 'normal'),
('med-243', '丙戊酸钠缓释片', '丙戊酸钠', '0.5g*30片', '赛诺菲安万特制药有限公司', '神经系统药物', '盒', 3500, 520, 52, 'normal'),
('med-244', '拉莫三嗪片', '拉莫三嗪', '25mg*30片', '葛兰素史克制药有限公司', '神经系统药物', '盒', 4200, 480, 48, 'normal'),
('med-245', '左乙拉西坦片', '左乙拉西坦', '0.5g*30片', '比利时UCB制药公司', '神经系统药物', '盒', 12000, 250, 25, 'normal'),
('med-246', '奥卡西平片', '奥卡西平', '0.3g*30片', '诺华制药有限公司', '神经系统药物', '盒', 8500, 300, 30, 'normal'),
('med-247', '托吡酯片', '托吡酯', '25mg*60片', '西安杨森制药有限公司', '神经系统药物', '盒', 6800, 350, 35, 'normal'),
('med-248', '苯妥英钠片', '苯妥英钠', '0.1g*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 1000, 760, 76, 'normal'),
('med-249', '苯巴比妥片', '苯巴比妥', '30mg*100片', '东北制药集团沈阳第一制药有限公司', '神经系统药物', '瓶', 800, 800, 80, 'normal'),
('med-250', '地西泮片', '地西泮', '2.5mg*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 600, 850, 85, 'normal'),
('med-251', '劳拉西泮片', '劳拉西泮', '1mg*100片', '惠氏制药有限公司', '神经系统药物', '瓶', 2200, 640, 64, 'normal'),
('med-252', '阿普唑仑片', '阿普唑仑', '0.4mg*100片', '辉瑞制药有限公司', '神经系统药物', '瓶', 1800, 660, 66, 'normal'),
('med-253', '艾司唑仑片', '艾司唑仑', '1mg*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-254', '唑吡坦片', '唑吡坦', '10mg*7片', '赛诺菲安万特制药有限公司', '神经系统药物', '盒', 3500, 520, 52, 'normal'),
('med-255', '右佐匹克隆片', '右佐匹克隆', '3mg*7片', '赛诺菲安万特制药有限公司', '神经系统药物', '盒', 4200, 480, 48, 'normal'),
('med-256', '扎来普隆胶囊', '扎来普隆', '5mg*7粒', '惠氏制药有限公司', '神经系统药物', '盒', 3800, 510, 51, 'normal'),
('med-257', '氟西汀胶囊', '氟西汀', '20mg*28粒', '礼来苏州制药有限公司', '神经系统药物', '盒', 4500, 480, 48, 'normal'),
('med-258', '帕罗西汀片', '帕罗西汀', '20mg*10片', '葛兰素史克制药有限公司', '神经系统药物', '盒', 5200, 450, 45, 'normal'),
('med-259', '舍曲林片', '舍曲林', '50mg*14片', '辉瑞制药有限公司', '神经系统药物', '盒', 6800, 350, 35, 'normal'),
('med-260', '西酞普兰片', '西酞普兰', '20mg*14片', '灵北制药有限公司', '神经系统药物', '盒', 5800, 420, 42, 'normal'),
('med-261', '艾司西酞普兰片', '艾司西酞普兰', '10mg*7片', '灵北制药有限公司', '神经系统药物', '盒', 6200, 380, 38, 'normal'),
('med-262', '文拉法辛缓释胶囊', '文拉法辛', '75mg*14粒', '惠氏制药有限公司', '神经系统药物', '盒', 8500, 300, 30, 'normal'),
('med-263', '度洛西汀肠溶胶囊', '度洛西汀', '60mg*7粒', '礼来苏州制药有限公司', '神经系统药物', '盒', 9200, 280, 28, 'normal'),
('med-264', '米氮平片', '米氮平', '30mg*10片', '默沙东制药有限公司', '神经系统药物', '盒', 7500, 330, 33, 'normal'),
('med-265', '曲唑酮片', '曲唑酮', '50mg*20片', '百时美施贵宝公司', '神经系统药物', '盒', 4200, 480, 48, 'normal'),
('med-266', '阿米替林片', '阿米替林', '25mg*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 1500, 680, 68, 'normal'),
('med-267', '多塞平片', '多塞平', '25mg*100片', '辉瑞制药有限公司', '神经系统药物', '瓶', 1800, 660, 66, 'normal'),
('med-268', '氯米帕明片', '氯米帕明', '25mg*30片', '诺华制药有限公司', '神经系统药物', '盒', 3200, 560, 56, 'normal'),
('med-269', '利培酮片', '利培酮', '1mg*20片', '西安杨森制药有限公司', '神经系统药物', '盒', 4500, 480, 48, 'normal'),
('med-270', '奥氮平片', '奥氮平', '5mg*7片', '礼来苏州制药有限公司', '神经系统药物', '盒', 8500, 300, 30, 'normal'),
('med-271', '喹硫平片', '喹硫平', '0.2g*20片', '阿斯利康制药有限公司', '神经系统药物', '盒', 12000, 250, 25, 'normal'),
('med-272', '阿立哌唑片', '阿立哌唑', '5mg*6片', '大冢制药有限公司', '神经系统药物', '盒', 15000, 220, 22, 'normal'),
('med-273', '齐拉西酮胶囊', '齐拉西酮', '20mg*14粒', '辉瑞制药有限公司', '神经系统药物', '盒', 18000, 200, 20, 'normal'),
('med-274', '帕利哌酮缓释片', '帕利哌酮', '3mg*7片', '西安杨森制药有限公司', '神经系统药物', '盒', 22000, 180, 18, 'normal'),
('med-275', '氯氮平片', '氯氮平', '25mg*100片', '诺华制药有限公司', '神经系统药物', '瓶', 2200, 640, 64, 'normal'),
('med-276', '氟哌啶醇片', '氟哌啶醇', '2mg*100片', '西安杨森制药有限公司', '神经系统药物', '瓶', 1500, 680, 68, 'normal'),
('med-277', '奋乃静片', '奋乃静', '2mg*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 1000, 760, 76, 'normal'),
('med-278', '氯丙嗪片', '氯丙嗪', '25mg*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 800, 800, 80, 'normal'),
('med-279', '碳酸锂片', '碳酸锂', '0.25g*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 1200, 740, 74, 'normal'),
('med-280', '丙戊酸镁缓释片', '丙戊酸镁', '0.5g*30片', '赛诺菲安万特制药有限公司', '神经系统药物', '盒', 3800, 510, 51, 'normal'),
('med-281', '多奈哌齐片', '多奈哌齐', '5mg*7片', '卫材(中国)药业有限公司', '神经系统药物', '盒', 8500, 300, 30, 'normal'),
('med-282', '美金刚片', '美金刚', '10mg*28片', '灵北制药有限公司', '神经系统药物', '盒', 12000, 250, 25, 'normal'),
('med-283', '利斯的明贴剂', '利斯的明', '4.6mg*30贴', '诺华制药有限公司', '神经系统药物', '盒', 18000, 200, 20, 'normal'),
('med-284', '加兰他敏缓释胶囊', '加兰他敏', '8mg*28粒', '西安杨森制药有限公司', '神经系统药物', '盒', 15000, 220, 22, 'normal'),
('med-285', '左旋多巴片', '左旋多巴', '0.25g*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 2200, 640, 64, 'normal'),
('med-286', '复方左旋多巴片', '复方左旋多巴', '0.25g*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 2800, 590, 59, 'normal'),
('med-287', '普拉克索片', '普拉克索', '0.25mg*30片', '勃林格殷格翰制药有限公司', '神经系统药物', '盒', 12000, 250, 25, 'normal'),
('med-288', '罗匹尼罗片', '罗匹尼罗', '0.25mg*12片', '葛兰素史克制药有限公司', '神经系统药物', '盒', 8500, 300, 30, 'normal'),
('med-289', '司来吉兰片', '司来吉兰', '5mg*50片', '梯瓦制药有限公司', '神经系统药物', '盒', 6800, 350, 35, 'normal'),
('med-290', '金刚烷胺片', '金刚烷胺', '0.1g*100片', '上海信谊药厂有限公司', '神经系统药物', '瓶', 1500, 680, 68, 'normal'),
-- 内分泌药物 (60条)
('med-291', '二甲双胍片', '二甲双胍', '0.5g*48片', '中美上海施贵宝制药有限公司', '内分泌药物', '盒', 1200, 740, 74, 'normal'),
('med-292', '格列美脲片', '格列美脲', '2mg*15片', '赛诺菲安万特制药有限公司', '内分泌药物', '盒', 2800, 590, 59, 'normal'),
('med-293', '格列齐特缓释片', '格列齐特', '30mg*30片', '施维雅制药有限公司', '内分泌药物', '盒', 3200, 560, 56, 'normal'),
('med-294', '格列吡嗪片', '格列吡嗪', '5mg*100片', '辉瑞制药有限公司', '内分泌药物', '瓶', 1500, 680, 68, 'normal'),
('med-295', '格列本脲片', '格列本脲', '2.5mg*100片', '上海信谊药厂有限公司', '内分泌药物', '瓶', 800, 800, 80, 'normal'),
('med-296', '瑞格列奈片', '瑞格列奈', '1mg*30片', '诺和诺德制药有限公司', '内分泌药物', '盒', 4500, 480, 48, 'normal'),
('med-297', '那格列奈片', '那格列奈', '120mg*30片', '诺华制药有限公司', '内分泌药物', '盒', 3800, 510, 51, 'normal'),
('med-298', '阿卡波糖片', '阿卡波糖', '50mg*30片', '拜耳医药保健有限公司', '内分泌药物', '盒', 5200, 450, 45, 'normal'),
('med-299', '伏格列波糖片', '伏格列波糖', '0.2mg*30片', '武田制药有限公司', '内分泌药物', '盒', 4800, 460, 46, 'normal'),
('med-300', '米格列醇片', '米格列醇', '50mg*30片', '拜耳医药保健有限公司', '内分泌药物', '盒', 6800, 350, 35, 'normal'),
('med-301', '西格列汀片', '西格列汀', '100mg*7片', '默沙东制药有限公司', '内分泌药物', '盒', 12000, 250, 25, 'normal'),
('med-302', '沙格列汀片', '沙格列汀', '5mg*7片', '阿斯利康制药有限公司', '内分泌药物', '盒', 11000, 260, 26, 'normal'),
('med-303', '维格列汀片', '维格列汀', '50mg*28片', '诺华制药有限公司', '内分泌药物', '盒', 15000, 220, 22, 'normal'),
('med-304', '利格列汀片', '利格列汀', '5mg*7片', '勃林格殷格翰制药有限公司', '内分泌药物', '盒', 13000, 240, 24, 'normal'),
('med-305', '阿格列汀片', '阿格列汀', '25mg*28片', '武田制药有限公司', '内分泌药物', '盒', 14000, 230, 23, 'normal'),
('med-306', '达格列净片', '达格列净', '10mg*7片', '阿斯利康制药有限公司', '内分泌药物', '盒', 18000, 200, 20, 'normal'),
('med-307', '恩格列净片', '恩格列净', '10mg*7片', '勃林格殷格翰制药有限公司', '内分泌药物', '盒', 19000, 190, 19, 'normal'),
('med-308', '卡格列净片', '卡格列净', '100mg*10片', '西安杨森制药有限公司', '内分泌药物', '盒', 22000, 180, 18, 'normal'),
('med-309', '艾塞那肽注射液', '艾塞那肽', '5μg*60支', '阿斯利康制药有限公司', '内分泌药物', '盒', 38000, 100, 10, 'normal'),
('med-310', '利拉鲁肽注射液', '利拉鲁肽', '6mg/ml*3ml', '诺和诺德制药有限公司', '内分泌药物', '支', 48000, 80, 8, 'normal'),
('med-311', '度拉糖肽注射液', '度拉糖肽', '0.75mg*4支', '礼来苏州制药有限公司', '内分泌药物', '盒', 52000, 70, 7, 'normal'),
('med-312', '司美格鲁肽注射液', '司美格鲁肽', '1mg/ml*1.5ml', '诺和诺德制药有限公司', '内分泌药物', '支', 68000, 50, 5, 'normal'),
('med-313', '胰岛素注射液', '胰岛素', '400单位/10ml', '诺和诺德制药有限公司', '内分泌药物', '支', 4800, 460, 46, 'normal'),
('med-314', '门冬胰岛素注射液', '门冬胰岛素', '300单位/3ml', '诺和诺德制药有限公司', '内分泌药物', '支', 6800, 350, 35, 'normal'),
('med-315', '赖脯胰岛素注射液', '赖脯胰岛素', '300单位/3ml', '礼来苏州制药有限公司', '内分泌药物', '支', 7200, 330, 33, 'normal'),
('med-316', '甘精胰岛素注射液', '甘精胰岛素', '300单位/3ml', '赛诺菲安万特制药有限公司', '内分泌药物', '支', 12000, 250, 25, 'normal'),
('med-317', '地特胰岛素注射液', '地特胰岛素', '300单位/3ml', '诺和诺德制药有限公司', '内分泌药物', '支', 11000, 260, 26, 'normal'),
('med-318', '德谷胰岛素注射液', '德谷胰岛素', '300单位/3ml', '诺和诺德制药有限公司', '内分泌药物', '支', 15000, 220, 22, 'normal'),
('med-319', '左甲状腺素钠片', '左甲状腺素钠', '50μg*100片', '默克制药有限公司', '内分泌药物', '瓶', 2200, 640, 64, 'normal'),
('med-320', '甲巯咪唑片', '甲巯咪唑', '5mg*100片', '默克制药有限公司', '内分泌药物', '瓶', 1500, 680, 68, 'normal'),
('med-321', '丙硫氧嘧啶片', '丙硫氧嘧啶', '50mg*100片', '上海信谊药厂有限公司', '内分泌药物', '瓶', 1200, 740, 74, 'normal'),
('med-322', '碘[131I]化钠口服溶液', '碘[131I]化钠', '1mCi/ml*10ml', '中国原子能科学研究院', '内分泌药物', '瓶', 28000, 50, 5, 'normal'),
('med-323', '醋酸泼尼松片', '醋酸泼尼松', '5mg*100片', '上海信谊药厂有限公司', '内分泌药物', '瓶', 800, 800, 80, 'normal'),
('med-324', '甲泼尼龙片', '甲泼尼龙', '4mg*30片', '辉瑞制药有限公司', '内分泌药物', '盒', 2800, 590, 59, 'normal'),
('med-325', '地塞米松片', '地塞米松', '0.75mg*100片', '上海信谊药厂有限公司', '内分泌药物', '瓶', 600, 850, 85, 'normal'),
('med-326', '氢化可的松片', '氢化可的松', '20mg*100片', '上海信谊药厂有限公司', '内分泌药物', '瓶', 1000, 760, 76, 'normal'),
('med-327', '倍他米松片', '倍他米松', '0.5mg*100片', '默沙东制药有限公司', '内分泌药物', '瓶', 1500, 680, 68, 'normal'),
('med-328', '布地奈德胶囊', '布地奈德', '3mg*20粒', '阿斯利康制药有限公司', '内分泌药物', '盒', 8500, 300, 30, 'normal'),
('med-329', '醋酸氟氢可的松片', '醋酸氟氢可的松', '0.1mg*100片', '上海信谊药厂有限公司', '内分泌药物', '瓶', 2200, 640, 64, 'normal'),
('med-330', '阿仑膦酸钠片', '阿仑膦酸钠', '70mg*4片', '默沙东制药有限公司', '内分泌药物', '盒', 4500, 480, 48, 'normal'),
('med-331', '利塞膦酸钠片', '利塞膦酸钠', '35mg*4片', '宝洁制药有限公司', '内分泌药物', '盒', 5200, 450, 45, 'normal'),
('med-332', '伊班膦酸钠片', '伊班膦酸钠', '150mg*1片', '罗氏制药有限公司', '内分泌药物', '盒', 12000, 250, 25, 'normal'),
('med-333', '唑来膦酸注射液', '唑来膦酸', '5mg/100ml', '诺华制药有限公司', '内分泌药物', '瓶', 28000, 100, 10, 'normal'),
('med-334', '降钙素注射液', '降钙素', '50单位/ml*1ml', '诺华制药有限公司', '内分泌药物', '支', 8500, 300, 30, 'normal'),
('med-335', '特立帕肽注射液', '特立帕肽', '20μg/80μl*28支', '礼来苏州制药有限公司', '内分泌药物', '盒', 180000, 30, 3, 'normal'),
('med-336', '地诺单抗注射液', '地诺单抗', '60mg/ml*1ml', '安进制药有限公司', '内分泌药物', '支', 120000, 50, 5, 'normal'),
('med-337', '碳酸钙D3片', '碳酸钙D3', '600mg/125单位*60片', '惠氏制药有限公司', '内分泌药物', '瓶', 2800, 590, 59, 'normal'),
('med-338', '骨化三醇胶囊', '骨化三醇', '0.25μg*20粒', '罗氏制药有限公司', '内分泌药物', '盒', 4500, 480, 48, 'normal'),
('med-339', '阿法骨化醇软胶囊', '阿法骨化醇', '0.25μg*20粒', '中外制药株式会社', '内分泌药物', '盒', 5200, 450, 45, 'normal'),
('med-340', '维生素D3软胶囊', '维生素D3', '400单位*100粒', '星鲨制药有限公司', '内分泌药物', '瓶', 1500, 680, 68, 'normal'),
('med-341', '雌二醇片', '雌二醇', '1mg*28片', '拜耳医药保健有限公司', '内分泌药物', '盒', 3800, 510, 51, 'normal'),
('med-342', '戊酸雌二醇片', '戊酸雌二醇', '1mg*21片', '拜耳医药保健有限公司', '内分泌药物', '盒', 4200, 480, 48, 'normal'),
('med-343', '雌三醇软膏', '雌三醇', '1mg/15g', '拜耳医药保健有限公司', '内分泌药物', '支', 3500, 520, 52, 'normal'),
('med-344', '黄体酮胶囊', '黄体酮', '100mg*30粒', '浙江仙琚制药股份有限公司', '内分泌药物', '盒', 2800, 590, 59, 'normal'),
('med-345', '地屈孕酮片', '地屈孕酮', '10mg*20片', '雅培制药有限公司', '内分泌药物', '盒', 6800, 350, 35, 'normal'),
('med-346', '炔诺酮片', '炔诺酮', '0.625mg*100片', '上海信谊药厂有限公司', '内分泌药物', '瓶', 1200, 740, 74, 'normal'),
('med-347', '醋酸甲羟孕酮片', '醋酸甲羟孕酮', '2mg*100片', '辉瑞制药有限公司', '内分泌药物', '瓶', 1800, 660, 66, 'normal'),
('med-348', '枸橼酸氯米芬片', '枸橼酸氯米芬', '50mg*15片', '赛诺菲安万特制药有限公司', '内分泌药物', '盒', 3200, 560, 56, 'normal'),
('med-349', '来曲唑片', '来曲唑', '2.5mg*10片', '诺华制药有限公司', '内分泌药物', '盒', 8500, 300, 30, 'normal'),
('med-350', '他莫昔芬片', '他莫昔芬', '10mg*60片', '阿斯利康制药有限公司', '内分泌药物', '盒', 4500, 480, 48, 'normal');
......@@ -12,6 +12,7 @@
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@tailwindcss/postcss": "^4",
"@tanstack/react-query": "^5.80.6",
"@xyflow/react": "^12.10.1",
"antd": "^5.25.3",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
......@@ -1267,6 +1268,55 @@
"react": "^18 || ^19"
}
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"license": "MIT"
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
......@@ -1356,6 +1406,66 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@xyflow/react": {
"version": "12.10.1",
"resolved": "https://registry.npmmirror.com/@xyflow/react/-/react-12.10.1.tgz",
"integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.75",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/react/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@xyflow/system": {
"version": "0.0.75",
"resolved": "https://registry.npmmirror.com/@xyflow/system/-/system-0.0.75.tgz",
"integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/antd": {
"version": "5.29.3",
"resolved": "https://registry.npmmirror.com/antd/-/antd-5.29.3.tgz",
......@@ -1531,6 +1641,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmmirror.com/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
......@@ -1586,6 +1702,111 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
......@@ -4141,6 +4362,15 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz",
......
......@@ -13,6 +13,7 @@
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@tailwindcss/postcss": "^4",
"@tanstack/react-query": "^5.80.6",
"@xyflow/react": "^12.10.1",
"antd": "^5.25.3",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
......
'use client';
import { useEffect, useState } from 'react';
import { Card, Table, Tag, Button, Modal, Input, message, Space, Collapse, Timeline } from 'antd';
import { useState } from 'react';
import { Card, Table, Tag, Button, Modal, Input, message, Space, Collapse, Timeline, Typography } from 'antd';
import { RobotOutlined, PlayCircleOutlined, ToolOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
const { Text } = Typography;
const API = '';
const BUILTIN_AGENTS = [
......@@ -14,6 +15,20 @@ const BUILTIN_AGENTS = [
{ id: 'follow_up_agent', name: '随访管理Agent', description: '管理患者随访,提醒用药、复诊,收集健康数据', category: 'follow_up', tools: ['query_medical_record', 'query_drug', 'query_symptom_knowledge'], max_iterations: 8 },
];
const categoryColor: Record<string, string> = {
pre_consult: 'blue',
diagnosis: 'purple',
prescription: 'orange',
follow_up: 'green',
};
const categoryLabel: Record<string, string> = {
pre_consult: '预问诊',
diagnosis: '诊断辅助',
prescription: '处方审核',
follow_up: '随访管理',
};
interface ToolCall {
tool_name: string;
call_id: string;
......@@ -59,11 +74,11 @@ export default function AgentsPage() {
const data = await res.json();
const agentData = data.data as AgentResponse;
const reply = agentData?.response || '无响应';
setTestMessages(prev => [...prev, {
role: 'assistant',
setTestMessages(prev => [...prev, {
role: 'assistant',
content: reply,
toolCalls: agentData?.tool_calls,
meta: { iterations: agentData?.iterations, tokens: agentData?.total_tokens }
meta: { iterations: agentData?.iterations, tokens: agentData?.total_tokens },
}]);
} catch {
message.error('请求失败');
......@@ -75,61 +90,84 @@ export default function AgentsPage() {
const renderToolCalls = (toolCalls?: ToolCall[]) => {
if (!toolCalls || toolCalls.length === 0) return null;
return (
<Collapse size="small" className="mt-2" items={[{
<Collapse size="small" style={{ marginTop: 8 }} items={[{
key: 'tools',
label: <span className="text-xs"><ToolOutlined className="mr-1" />Tool调用记录 ({toolCalls.length})</span>,
label: <span style={{ fontSize: 12 }}><ToolOutlined style={{ marginRight: 4 }} />Tool调用记录 ({toolCalls.length})</span>,
children: (
<Timeline
items={toolCalls.map((tc, idx) => ({
color: tc.success ? 'green' : 'red',
dot: tc.success ? <CheckCircleOutlined /> : <CloseCircleOutlined />,
children: (
<div key={idx} className="text-xs">
<div className="font-medium">{tc.tool_name}</div>
<div className="text-gray-500">参数: {tc.arguments}</div>
{tc.result && (
<div className={tc.success ? 'text-green-600' : 'text-red-500'}>
结果: {tc.success ? JSON.stringify(tc.result.data).slice(0, 100) : tc.result.error}
{JSON.stringify(tc.result.data).length > 100 && '...'}
</div>
)}
</div>
),
}))}
/>
<Timeline items={toolCalls.map((tc, idx) => ({
color: tc.success ? 'green' : 'red',
dot: tc.success ? <CheckCircleOutlined /> : <CloseCircleOutlined />,
children: (
<div key={idx} style={{ fontSize: 12 }}>
<div style={{ fontWeight: 500 }}>{tc.tool_name}</div>
<div style={{ color: '#8c8c8c' }}>参数: {tc.arguments}</div>
{tc.result && (
<div style={{ color: tc.success ? '#52c41a' : '#ff4d4f' }}>
结果: {tc.success ? JSON.stringify(tc.result.data).slice(0, 100) : tc.result.error}
{JSON.stringify(tc.result.data).length > 100 && '...'}
</div>
)}
</div>
),
}))} />
),
}]} />
);
};
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 200 },
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '描述', dataIndex: 'description', key: 'description' },
{ title: '类别', dataIndex: 'category', key: 'category', render: (v: string) => <Tag color="blue">{v}</Tag> },
{ title: '工具', dataIndex: 'tools', key: 'tools', render: (v: string[]) => v.map(t => <Tag key={t}>{t}</Tag>) },
{ title: '最大迭代', dataIndex: 'max_iterations', key: 'max_iterations', width: 100 },
{
title: '操作', key: 'action', render: (_: unknown, r: typeof BUILTIN_AGENTS[0]) => (
<Button size="small" icon={<PlayCircleOutlined />} onClick={() => openTest(r.id, r.name)}>测试</Button>
title: '智能体名称', key: 'name',
render: (_: unknown, r: typeof BUILTIN_AGENTS[0]) => (
<Space>
<RobotOutlined style={{ color: '#1890ff' }} />
<div>
<Text strong>{r.name}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>{r.id}</Text>
</div>
</Space>
),
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{
title: '类别', dataIndex: 'category', key: 'category', width: 110,
render: (v: string) => <Tag color={categoryColor[v] || 'default'}>{categoryLabel[v] || v}</Tag>,
},
{
title: '工具', dataIndex: 'tools', key: 'tools',
render: (v: string[]) => (
<Space size={4} wrap>{v.map(t => <Tag key={t} style={{ fontSize: 11, margin: 0 }}>{t}</Tag>)}</Space>
),
},
{ title: '最大迭代', dataIndex: 'max_iterations', key: 'max_iterations', width: 90, render: (v: number) => <Tag>{v} 次</Tag> },
{
title: '操作', key: 'action', width: 90,
render: (_: unknown, r: typeof BUILTIN_AGENTS[0]) => (
<Button type="link" size="small" icon={<PlayCircleOutlined />} onClick={() => openTest(r.id, r.name)}>测试</Button>
),
},
];
return (
<div>
<Card title={<Space><RobotOutlined />Agent管理</Space>}>
<Table dataSource={BUILTIN_AGENTS} columns={columns} rowKey="id" pagination={false} />
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>智能体管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台内置 AI 智能体,支持对话测试与工具调用查看</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table dataSource={BUILTIN_AGENTS} columns={columns} rowKey="id" pagination={false} size="small" />
</Card>
<Modal
title={`测试 - ${testModal.agentName}`}
title={`测试 · ${testModal.agentName}`}
open={testModal.open}
onCancel={() => setTestModal({ open: false, agentId: '', agentName: '' })}
footer={null}
width={700}
>
<div style={{ height: 400, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 6, padding: 12, marginBottom: 12 }}>
<div style={{ height: 400, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 8, padding: 12, marginBottom: 12 }}>
{testMessages.map((m, i) => (
<div key={i} style={{ marginBottom: 12, textAlign: m.role === 'user' ? 'right' : 'left' }}>
<div style={{
......@@ -140,7 +178,7 @@ export default function AgentsPage() {
}}>
{m.content}
{m.role === 'assistant' && m.meta && (
<div className="text-xs text-gray-400 mt-1 border-t pt-1">
<div style={{ fontSize: 11, color: '#bbb', marginTop: 4, borderTop: '1px solid #e8e8e8', paddingTop: 4 }}>
迭代: {m.meta.iterations || 0} | Tokens: {m.meta.tokens || 0}
</div>
)}
......@@ -148,7 +186,7 @@ export default function AgentsPage() {
{m.role === 'assistant' && renderToolCalls(m.toolCalls)}
</div>
))}
{loading && <div style={{ color: '#999', fontSize: 12 }}>Agent思考中...</div>}
{loading && <div style={{ color: '#999', fontSize: 12 }}>Agent 思考中...</div>}
</div>
<Space.Compact style={{ width: '100%' }}>
<Input value={inputMsg} onChange={e => setInputMsg(e.target.value)} onPressEnter={sendMessage} placeholder="输入消息..." />
......
'use client';
import { useEffect, useState } from 'react';
import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, Tabs } from 'antd';
import { BookOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, Tabs, Typography } from 'antd';
import { BookOutlined, PlusOutlined, SearchOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
const { Text } = Typography;
const API = '';
const categoryLabel: Record<string, string> = {
clinical_guideline: '临床指南', drug: '药品说明', disease: '疾病百科', paper: '医学论文',
};
const categoryColor: Record<string, string> = {
clinical_guideline: 'blue', drug: 'green', disease: 'purple', paper: 'orange',
};
export default function KnowledgePage() {
const { accessToken: token } = useUserStore();
const [collections, setCollections] = useState<any[]>([]);
const [documents, setDocuments] = useState<any[]>([]);
const [colLoading, setColLoading] = useState(false);
const [docLoading, setDocLoading] = useState(false);
const [colModal, setColModal] = useState(false);
const [docModal, setDocModal] = useState(false);
const [searchModal, setSearchModal] = useState(false);
const [searchResults, setSearchResults] = useState<any[]>([]);
const [searching, setSearching] = useState(false);
const [colForm] = Form.useForm();
const [docForm] = Form.useForm();
const [searchForm] = Form.useForm();
......@@ -22,70 +33,138 @@ export default function KnowledgePage() {
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
const fetchCollections = async () => {
const res = await fetch(`${API}/api/v1/knowledge/collections`, { headers });
const data = await res.json();
setCollections(data.data || []);
setColLoading(true);
try {
const res = await fetch(`${API}/api/v1/knowledge/collections`, { headers });
const data = await res.json();
setCollections(data.data || []);
} catch {} finally { setColLoading(false); }
};
const fetchDocuments = async () => {
const res = await fetch(`${API}/api/v1/knowledge/documents`, { headers });
const data = await res.json();
setDocuments(data.data || []);
setDocLoading(true);
try {
const res = await fetch(`${API}/api/v1/knowledge/documents`, { headers });
const data = await res.json();
setDocuments(data.data || []);
} catch {} finally { setDocLoading(false); }
};
useEffect(() => { fetchCollections(); fetchDocuments(); }, []);
const createCollection = async (values: any) => {
await fetch(`${API}/api/v1/knowledge/collections`, { method: 'POST', headers, body: JSON.stringify(values) });
message.success('创建成功'); setColModal(false); colForm.resetFields(); fetchCollections();
try {
await fetch(`${API}/api/v1/knowledge/collections`, { method: 'POST', headers, body: JSON.stringify(values) });
message.success('集合创建成功');
setColModal(false);
colForm.resetFields();
fetchCollections();
} catch { message.error('创建失败'); }
};
const createDocument = async (values: any) => {
await fetch(`${API}/api/v1/knowledge/documents`, { method: 'POST', headers, body: JSON.stringify(values) });
message.success('文档已添加'); setDocModal(false); docForm.resetFields(); fetchDocuments();
try {
await fetch(`${API}/api/v1/knowledge/documents`, { method: 'POST', headers, body: JSON.stringify(values) });
message.success('文档已添加');
setDocModal(false);
docForm.resetFields();
fetchDocuments();
} catch { message.error('添加失败'); }
};
const doSearch = async (values: any) => {
const res = await fetch(`${API}/api/v1/knowledge/search`, { method: 'POST', headers, body: JSON.stringify(values) });
const data = await res.json();
setSearchResults(data.data || []);
setSearching(true);
try {
const res = await fetch(`${API}/api/v1/knowledge/search`, { method: 'POST', headers, body: JSON.stringify(values) });
const data = await res.json();
setSearchResults(data.data || []);
} catch { message.error('检索失败'); } finally { setSearching(false); }
};
const colColumns = [
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '类别', dataIndex: 'category', key: 'category', render: (v: string) => <Tag>{v}</Tag> },
{ title: '文档数', dataIndex: 'document_count', key: 'document_count', width: 80 },
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => <Tag color="green">{v}</Tag> },
{
title: '集合名称', dataIndex: 'name', key: 'name',
render: (v: string) => <Space><BookOutlined style={{ color: '#1890ff' }} /><Text strong>{v}</Text></Space>,
},
{
title: '类别', dataIndex: 'category', key: 'category', width: 120,
render: (v: string) => <Tag color={categoryColor[v] || 'default'}>{categoryLabel[v] || v}</Tag>,
},
{ title: '文档数', dataIndex: 'document_count', key: 'document_count', width: 80, render: (v: number) => v ?? 0 },
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
render: (v: string) => <Tag color="green">{v || '正常'}</Tag>,
},
];
const docColumns = [
{ title: '标题', dataIndex: 'title', key: 'title' },
{ title: '集合', dataIndex: 'collection_id', key: 'collection_id' },
{ title: '分块数', dataIndex: 'chunk_count', key: 'chunk_count', width: 80 },
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => <Tag color="blue">{v}</Tag> },
{
title: '文档标题', dataIndex: 'title', key: 'title',
render: (v: string) => <Space><FileTextOutlined style={{ color: '#722ed1' }} /><Text>{v}</Text></Space>,
},
{ title: '所属集合', dataIndex: 'collection_id', key: 'collection_id', width: 200, render: (v: string) => <Text type="secondary">{v}</Text> },
{ title: '分块数', dataIndex: 'chunk_count', key: 'chunk_count', width: 80, render: (v: number) => v ?? 0 },
{
title: '状态', dataIndex: 'status', key: 'status', width: 100,
render: (v: string) => <Tag color="blue">{v || '已处理'}</Tag>,
},
];
return (
<div>
<Card title={<Space><BookOutlined />知识库管理</Space>} extra={
<Space>
<Button icon={<SearchOutlined />} onClick={() => setSearchModal(true)}>检索测试</Button>
<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>
{/* 操作栏 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Button icon={<ReloadOutlined />} onClick={() => { fetchCollections(); fetchDocuments(); }}>刷新</Button>
<Button icon={<SearchOutlined />} onClick={() => { setSearchResults([]); setSearchModal(true); }}>检索测试</Button>
<Button icon={<PlusOutlined />} onClick={() => setDocModal(true)}>添加文档</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setColModal(true)}>新建集合</Button>
</Space>
}>
<Tabs items={[
{ key: 'collections', label: '知识库集合', children: <Table dataSource={collections} columns={colColumns} rowKey="id" /> },
{ key: 'documents', label: '文档列表', children: <Table dataSource={documents} columns={docColumns} rowKey="id" /> },
]} />
<Button type="primary" icon={<PlusOutlined />} style={{ marginLeft: 'auto' }} onClick={() => setColModal(true)}>新建集合</Button>
</div>
</Card>
{/* 内容区 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs
items={[
{
key: 'collections',
label: <Space><BookOutlined />知识库集合 <Tag style={{ marginLeft: 4 }}>{collections.length}</Tag></Space>,
children: (
<Table dataSource={collections} columns={colColumns} rowKey="id" loading={colLoading} size="small"
pagination={{ pageSize: 10, size: 'small', showTotal: (t) => `共 ${t} 个集合` }}
/>
),
},
{
key: 'documents',
label: <Space><FileTextOutlined />文档列表 <Tag style={{ marginLeft: 4 }}>{documents.length}</Tag></Space>,
children: (
<Table dataSource={documents} columns={docColumns} rowKey="id" loading={docLoading} size="small"
pagination={{ pageSize: 10, size: 'small', showTotal: (t) => `共 ${t} 篇文档` }}
/>
),
},
]}
/>
</Card>
<Modal title="新建知识库集合" open={colModal} onCancel={() => setColModal(false)} onOk={() => colForm.submit()}>
<Form form={colForm} layout="vertical" onFinish={createCollection}>
<Form.Item name="name" label="集合名称" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="description" label="描述"><Input.TextArea rows={2} /></Form.Item>
{/* 新建集合 Modal */}
<Modal title="新建知识库集合" open={colModal} onCancel={() => { setColModal(false); colForm.resetFields(); }}
onOk={() => colForm.submit()} okText="创建">
<Form form={colForm} layout="vertical" onFinish={createCollection} style={{ marginTop: 16 }}>
<Form.Item name="name" label="集合名称" rules={[{ required: true, message: '请输入集合名称' }]}>
<Input placeholder="请输入集合名称" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} placeholder="请输入描述(选填)" />
</Form.Item>
<Form.Item name="category" label="类别">
<Select options={[
<Select placeholder="选择知识库类别" options={[
{ value: 'clinical_guideline', label: '临床指南' },
{ value: 'drug', label: '药品说明' },
{ value: 'disease', label: '疾病百科' },
......@@ -95,32 +174,41 @@ export default function KnowledgePage() {
</Form>
</Modal>
<Modal title="添加文档" open={docModal} onCancel={() => setDocModal(false)} onOk={() => docForm.submit()} width={600}>
<Form form={docForm} layout="vertical" onFinish={createDocument}>
<Form.Item name="collection_id" label="所属集合" rules={[{ required: true }]}>
<Select options={collections.map(c => ({ value: c.collection_id, label: c.name }))} />
{/* 添加文档 Modal */}
<Modal title="添加文档" open={docModal} onCancel={() => { setDocModal(false); docForm.resetFields(); }}
onOk={() => docForm.submit()} okText="添加" width={600}>
<Form form={docForm} layout="vertical" onFinish={createDocument} style={{ marginTop: 16 }}>
<Form.Item name="collection_id" label="所属集合" rules={[{ required: true, message: '请选择集合' }]}>
<Select placeholder="选择知识库集合" options={collections.map(c => ({ value: c.collection_id, label: c.name }))} />
</Form.Item>
<Form.Item name="title" label="文档标题" rules={[{ required: true, message: '请输入文档标题' }]}>
<Input placeholder="请输入文档标题" />
</Form.Item>
<Form.Item name="title" label="标题" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="content" label="内容" rules={[{ required: true }]}>
<Form.Item name="content" label="文档内容" rules={[{ required: true, message: '请输入文档内容' }]}>
<Input.TextArea rows={6} placeholder="粘贴文档内容..." />
</Form.Item>
</Form>
</Modal>
<Modal title="知识检索测试" open={searchModal} onCancel={() => setSearchModal(false)} footer={null} width={700}>
<Form form={searchForm} layout="inline" onFinish={doSearch} style={{ marginBottom: 16 }}>
<Form.Item name="query" rules={[{ required: true }]} style={{ flex: 1 }}>
<Input placeholder="输入检索关键词..." />
{/* 检索测试 Modal */}
<Modal title={<Space><SearchOutlined />知识检索测试</Space>}
open={searchModal} onCancel={() => setSearchModal(false)} footer={null} width={700}>
<Form form={searchForm} layout="inline" onFinish={doSearch} style={{ marginBottom: 16, marginTop: 8 }}>
<Form.Item name="query" rules={[{ required: true, message: '请输入检索词' }]} style={{ flex: 1 }}>
<Input placeholder="输入检索关键词..." prefix={<SearchOutlined />} />
</Form.Item>
<Form.Item name="top_k" initialValue={5}>
<Select style={{ width: 80 }} options={[3, 5, 10].map(v => ({ value: v, label: `Top ${v}` }))} />
<Select style={{ width: 90 }} options={[3, 5, 10].map(v => ({ value: v, label: `Top ${v}` }))} />
</Form.Item>
<Button type="primary" htmlType="submit">检索</Button>
<Button type="primary" htmlType="submit" loading={searching}>检索</Button>
</Form>
{searchResults.length === 0 && !searching && (
<div style={{ textAlign: 'center', color: '#8c8c8c', padding: '24px 0', fontSize: 13 }}>输入关键词后点击检索</div>
)}
{searchResults.map((r, i) => (
<Card key={i} size="small" style={{ marginBottom: 8 }}>
<Card key={i} size="small" style={{ marginBottom: 8, borderRadius: 8 }}>
<Tag color="blue">{r.collection_id}</Tag>
<p style={{ marginTop: 8, fontSize: 13 }}>{r.content}</p>
<p style={{ marginTop: 8, fontSize: 13, color: '#333' }}>{r.content}</p>
</Card>
))}
</Modal>
......
......@@ -34,6 +34,7 @@ const menuItems = [
key: 'ai-platform', icon: <ApiOutlined />, label: '智能体平台',
children: [
{ key: '/admin/agents', icon: <RobotOutlined />, label: 'Agent管理' },
{ key: '/admin/tools', icon: <ApiOutlined />, label: 'Tools工具' },
{ key: '/admin/workflows', icon: <DeploymentUnitOutlined />, label: '工作流' },
{ key: '/admin/knowledge', icon: <BookOutlined />, label: '知识库' },
],
......@@ -67,7 +68,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
const getSelectedKeys = () => {
const allKeys = ['/admin/dashboard', '/admin/patients', '/admin/doctors', '/admin/admins',
'/admin/departments', '/admin/consultations', '/admin/prescription', '/admin/pharmacy',
'/admin/ai-config', '/admin/compliance', '/admin/agents', '/admin/workflows', '/admin/knowledge'];
'/admin/ai-config', '/admin/compliance', '/admin/agents', '/admin/tools', '/admin/workflows', '/admin/knowledge'];
const match = allKeys.find(k => currentPath.startsWith(k));
return match ? [match] : [];
};
......@@ -77,18 +78,21 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
<header
style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
background: 'linear-gradient(135deg, #001529 0%, #003a8c 50%, #0050b3 100%)',
boxShadow: '0 2px 8px rgba(0,21,41,0.15)',
height: 56, display: 'flex', alignItems: 'center', padding: '0 24px',
background: '#ffffff',
borderBottom: '1px solid #e8f0fc',
boxShadow: '0 1px 4px rgba(0, 82, 204, 0.06)',
}}
>
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, flexShrink: 0 }}
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 20, flexShrink: 0 }}
onClick={() => router.push('/admin/dashboard')}
>
<MedicineBoxOutlined style={{ fontSize: 18, color: '#fff' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#fff' }}>互联网医院</span>
<Tag color="geekblue" style={{ marginLeft: 4, fontSize: 10, lineHeight: '18px', padding: '0 4px' }}>管理后台</Tag>
<div style={{ width: 30, height: 30, borderRadius: 8, background: 'linear-gradient(135deg, #722ed1 0%, #531dab 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<MedicineBoxOutlined style={{ fontSize: 16, color: '#fff' }} />
</div>
<span style={{ fontSize: 15, fontWeight: 700, color: '#1d2129' }}>互联网医院</span>
<Tag color="purple" style={{ marginLeft: 2, fontSize: 10, lineHeight: '18px', padding: '0 5px' }}>管理后台</Tag>
</div>
<Menu
......@@ -96,20 +100,20 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
selectedKeys={getSelectedKeys()}
items={menuItems}
onClick={handleMenuClick}
theme="dark"
className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
theme="light"
className="patient-nav-menu"
style={{ flex: 1, minWidth: 0, borderBottom: 'none' }}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
<Badge count={2} size="small">
<BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
<BellOutlined style={{ fontSize: 16, color: '#595959', cursor: 'pointer' }} />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#722ed1' }} />
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || '管理员'}</Text>
<Text style={{ color: '#1d2129', fontSize: 13 }}>{user.real_name || '管理员'}</Text>
</Space>
</Dropdown>
) : (
......@@ -118,7 +122,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</div>
</header>
<Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
<Content style={{ marginTop: 56, minHeight: 'calc(100vh - 56px)', background: '#f5f6fa' }}>
{children}
</Content>
</Layout>
......
'use client';
import { useState, useEffect } from 'react';
import { Card, Table, Tag, Button, Modal, Descriptions, Space, Input, Empty, Spin, Badge } from 'antd';
import {
ToolOutlined,
SearchOutlined,
InfoCircleOutlined,
ApiOutlined,
CheckCircleOutlined,
CodeOutlined,
import { Card, Table, Tag, Button, Modal, Descriptions, Space, Input, Badge, Typography } from 'antd';
import {
ToolOutlined, SearchOutlined, InfoCircleOutlined, ApiOutlined,
CheckCircleOutlined, CodeOutlined, ReloadOutlined,
} from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
const { Text } = Typography;
const API = '';
interface AgentTool {
......@@ -38,25 +35,22 @@ const BUILTIN_TOOLS: AgentTool[] = [
];
const categoryColors: Record<string, string> = {
knowledge: 'blue',
recommendation: 'cyan',
medical: 'purple',
pharmacy: 'green',
safety: 'red',
follow_up: 'orange',
notification: 'gold',
knowledge: 'blue', recommendation: 'cyan', medical: 'purple',
pharmacy: 'green', safety: 'red', follow_up: 'orange', notification: 'gold',
};
const categoryLabels: Record<string, string> = {
knowledge: '知识查询',
recommendation: '智能推荐',
medical: '病历管理',
pharmacy: '药品管理',
safety: '安全检查',
follow_up: '随访管理',
notification: '消息通知',
knowledge: '知识查询', recommendation: '智能推荐', medical: '病历管理',
pharmacy: '药品管理', safety: '安全检查', follow_up: '随访管理', notification: '消息通知',
};
const statCards = [
{ label: '工具总数', color: '#1890ff', icon: <ToolOutlined />, key: 'total' },
{ label: '已启用', color: '#52c41a', icon: <CheckCircleOutlined />, key: 'enabled' },
{ label: '分类数', color: '#722ed1', icon: <ApiOutlined />, key: 'categories' },
{ label: '安全工具', color: '#fa8c16', icon: <ToolOutlined />, key: 'safety' },
];
export default function ToolsPage() {
const { accessToken: token } = useUserStore();
const [tools, setTools] = useState<AgentTool[]>(BUILTIN_TOOLS);
......@@ -64,215 +58,145 @@ export default function ToolsPage() {
const [searchText, setSearchText] = useState('');
const [detailModal, setDetailModal] = useState<{ open: boolean; tool: AgentTool | null }>({ open: false, tool: null });
useEffect(() => {
fetchTools();
}, []);
useEffect(() => { fetchTools(); }, []);
const fetchTools = async () => {
setLoading(true);
try {
const res = await fetch(`${API}/api/v1/agent/tools`, {
headers: { Authorization: `Bearer ${token}` },
});
const res = await fetch(`${API}/api/v1/agent/tools`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) {
const data = await res.json();
if (data.data?.length > 0) {
setTools(data.data);
}
if (data.data?.length > 0) setTools(data.data);
}
} catch {
// 使用内置工具列表
} finally {
} catch { /* 使用内置工具列表 */ } finally {
setLoading(false);
}
};
const filteredTools = tools.filter(t =>
const filteredTools = tools.filter(t =>
t.name.toLowerCase().includes(searchText.toLowerCase()) ||
t.description.toLowerCase().includes(searchText.toLowerCase()) ||
t.category.toLowerCase().includes(searchText.toLowerCase())
);
const categoryStats = tools.reduce((acc, t) => { acc[t.category] = (acc[t.category] || 0) + 1; return acc; }, {} as Record<string, number>);
const statValues: Record<string, number> = {
total: tools.length,
enabled: tools.filter(t => t.is_enabled).length,
categories: Object.keys(categoryStats).length,
safety: categoryStats['safety'] || 0,
};
const columns = [
{
title: '工具名称',
dataIndex: 'name',
key: 'name',
title: '工具名称', dataIndex: 'name', key: 'name',
render: (v: string) => (
<Space>
<CodeOutlined className="text-blue-500" />
<span className="font-mono text-sm">{v}</span>
<CodeOutlined style={{ color: '#1890ff' }} />
<Text code style={{ fontSize: 13 }}>{v}</Text>
</Space>
),
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, width: 300 },
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
width: 300,
title: '分类', dataIndex: 'category', key: 'category', width: 110,
render: (v: string) => <Tag color={categoryColors[v] || 'default'}>{categoryLabels[v] || v}</Tag>,
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120,
render: (v: string) => (
<Tag color={categoryColors[v] || 'default'}>{categoryLabels[v] || v}</Tag>
),
title: '参数数量', dataIndex: 'parameters', key: 'parameters', width: 90,
render: (v: Record<string, unknown>) => <Text type="secondary">{Object.keys(v).length} 个</Text>,
},
{
title: '参数',
dataIndex: 'parameters',
key: 'parameters',
width: 200,
render: (v: Record<string, unknown>) => (
<span className="text-xs text-gray-500">
{Object.keys(v).length} 个参数
</span>
),
title: '状态', dataIndex: 'is_enabled', key: 'is_enabled', width: 100,
render: (v: boolean) => <Badge status={v ? 'success' : 'default'} text={v ? '已启用' : '已禁用'} />,
},
{
title: '状态',
dataIndex: 'is_enabled',
key: 'is_enabled',
width: 100,
render: (v: boolean) => (
<Badge status={v ? 'success' : 'default'} text={v ? '已启用' : '已禁用'} />
),
},
{
title: '操作',
key: 'action',
width: 100,
title: '操作', key: 'action', width: 90,
render: (_: unknown, record: AgentTool) => (
<Button
type="link"
size="small"
icon={<InfoCircleOutlined />}
onClick={() => setDetailModal({ open: true, tool: record })}
>
详情
</Button>
<Button type="link" size="small" icon={<InfoCircleOutlined />} onClick={() => setDetailModal({ open: true, tool: record })}>详情</Button>
),
},
];
const categoryStats = tools.reduce((acc, t) => {
acc[t.category] = (acc[t.category] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return (
<div className="space-y-4">
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>Agent 工具管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台内置 Agent 工具,查看工具详情与参数定义</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<Card size="small" className="bg-gradient-to-r from-blue-50 to-white border-l-4 border-l-blue-500">
<div className="flex items-center justify-between">
<div>
<div className="text-gray-500 text-xs">工具总数</div>
<div className="text-2xl font-bold text-blue-600">{tools.length}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
{statCards.map(s => (
<Card key={s.key} size="small" style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: `4px solid ${s.color}` }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{s.label}</div>
<div style={{ fontSize: 24, fontWeight: 700, color: s.color }}>{statValues[s.key]}</div>
</div>
<div style={{ fontSize: 28, color: s.color, opacity: 0.3 }}>{s.icon}</div>
</div>
<ToolOutlined className="text-3xl text-blue-300" />
</div>
</Card>
<Card size="small" className="bg-gradient-to-r from-green-50 to-white border-l-4 border-l-green-500">
<div className="flex items-center justify-between">
<div>
<div className="text-gray-500 text-xs">已启用</div>
<div className="text-2xl font-bold text-green-600">{tools.filter(t => t.is_enabled).length}</div>
</div>
<CheckCircleOutlined className="text-3xl text-green-300" />
</div>
</Card>
<Card size="small" className="bg-gradient-to-r from-purple-50 to-white border-l-4 border-l-purple-500">
<div className="flex items-center justify-between">
<div>
<div className="text-gray-500 text-xs">分类数</div>
<div className="text-2xl font-bold text-purple-600">{Object.keys(categoryStats).length}</div>
</div>
<ApiOutlined className="text-3xl text-purple-300" />
</div>
</Card>
<Card size="small" className="bg-gradient-to-r from-orange-50 to-white border-l-4 border-l-orange-500">
<div className="flex items-center justify-between">
<div>
<div className="text-gray-500 text-xs">安全工具</div>
<div className="text-2xl font-bold text-orange-600">{categoryStats['safety'] || 0}</div>
</div>
<ToolOutlined className="text-3xl text-orange-300" />
</div>
</Card>
</Card>
))}
</div>
{/* 工具列表 */}
<Card
title={
<Space>
<ToolOutlined className="text-blue-500" />
<span>Agent工具管理</span>
</Space>
}
extra={
{/* 搜索栏 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Input
placeholder="搜索工具..."
prefix={<SearchOutlined className="text-gray-400" />}
placeholder="搜索工具名称、描述或分类..."
prefix={<SearchOutlined />}
value={searchText}
onChange={e => setSearchText(e.target.value)}
style={{ width: 250 }}
style={{ width: 280, borderRadius: 8 }}
allowClear
/>
}
>
<Spin spinning={loading}>
<Table
dataSource={filteredTools}
columns={columns}
rowKey="id"
pagination={{ pageSize: 10, showSizeChanger: true, showTotal: (total) => `共 ${total} 个工具` }}
locale={{ emptyText: <Empty description="暂无工具" /> }}
/>
</Spin>
<Button icon={<ReloadOutlined />} onClick={() => { setSearchText(''); fetchTools(); }}>刷新</Button>
</div>
</Card>
{/* 工具详情Modal */}
{/* 工具列表 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table
dataSource={filteredTools}
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: (t) => `共 ${t} 个工具` }}
/>
</Card>
{/* 工具详情 Modal */}
<Modal
title={
<Space>
<CodeOutlined className="text-blue-500" />
<span>工具详情</span>
</Space>
}
title={<Space><CodeOutlined style={{ color: '#1890ff' }} /><span>工具详情</span></Space>}
open={detailModal.open}
onCancel={() => setDetailModal({ open: false, tool: null })}
footer={null}
width={600}
>
{detailModal.tool && (
<div className="space-y-4">
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="工具名称">
<code className="bg-gray-100 px-2 py-1 rounded text-blue-600">{detailModal.tool.name}</code>
<Text code style={{ color: '#1890ff' }}>{detailModal.tool.name}</Text>
</Descriptions.Item>
<Descriptions.Item label="描述">{detailModal.tool.description}</Descriptions.Item>
<Descriptions.Item label="分类">
<Tag color={categoryColors[detailModal.tool.category]}>
{categoryLabels[detailModal.tool.category] || detailModal.tool.category}
</Tag>
<Tag color={categoryColors[detailModal.tool.category]}>{categoryLabels[detailModal.tool.category] || detailModal.tool.category}</Tag>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Badge status={detailModal.tool.is_enabled ? 'success' : 'default'} text={detailModal.tool.is_enabled ? '已启用' : '已禁用'} />
</Descriptions.Item>
</Descriptions>
<Card size="small" title="参数定义" className="bg-gray-50">
<pre className="text-xs bg-gray-900 text-green-400 p-3 rounded overflow-auto">
<Card size="small" title="参数定义" style={{ background: '#fafafa', borderRadius: 8 }}>
<pre style={{ fontSize: 12, background: '#1f2937', color: '#4ade80', padding: 12, borderRadius: 6, overflow: 'auto', margin: 0 }}>
{JSON.stringify(detailModal.tool.parameters, null, 2)}
</pre>
</Card>
<Card size="small" title="调用示例" className="bg-gray-50">
<pre className="text-xs bg-gray-900 text-yellow-300 p-3 rounded overflow-auto">
<Card size="small" title="调用示例" style={{ background: '#fafafa', borderRadius: 8 }}>
<pre style={{ fontSize: 12, background: '#1f2937', color: '#fde047', padding: 12, borderRadius: 6, overflow: 'auto', margin: 0 }}>
{`// 调用 ${detailModal.tool.name}
const result = await agent.callTool('${detailModal.tool.name}', {
${Object.entries(detailModal.tool.parameters).map(([k, v]) => ` ${k}: <${v}>`).join(',\n')}
......
'use client';
import { useEffect, useState, useCallback } from 'react';
import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, Badge, Tabs } from 'antd';
import { DeploymentUnitOutlined, PlayCircleOutlined, PlusOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, Badge } from 'antd';
import { DeploymentUnitOutlined, PlayCircleOutlined, PlusOutlined, EditOutlined } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
import WorkflowEditor, { WorkflowData } from '@/components/workflow/WorkflowEditor';
import VisualWorkflowEditor from '@/components/workflow/VisualWorkflowEditor';
import type { Node, Edge } from '@xyflow/react';
const API = '';
......@@ -19,6 +20,16 @@ interface Workflow {
definition?: string;
}
const statusColor: Record<string, 'success' | 'warning' | 'default'> = {
active: 'success', draft: 'warning', archived: 'default',
};
const statusLabel: Record<string, string> = {
active: '已启用', draft: '草稿', archived: '已归档',
};
const categoryLabel: Record<string, string> = {
pre_consult: '预问诊', diagnosis: '诊断', prescription: '处方审核', follow_up: '随访',
};
export default function WorkflowsPage() {
const { accessToken: token } = useUserStore();
const [workflows, setWorkflows] = useState<Workflow[]>([]);
......@@ -27,16 +38,17 @@ export default function WorkflowsPage() {
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('list');
const [tableLoading, setTableLoading] = useState(false);
const fetchWorkflows = async () => {
setTableLoading(true);
try {
const res = await fetch(`${API}/api/v1/admin/workflows`, {
headers: { Authorization: `Bearer ${token}` },
});
const res = await fetch(`${API}/api/v1/admin/workflows`, { headers: { Authorization: `Bearer ${token}` } });
const data = await res.json();
setWorkflows(data.data || []);
} catch {}
} catch {} finally {
setTableLoading(false);
}
};
useEffect(() => { fetchWorkflows(); }, []);
......@@ -45,8 +57,7 @@ export default function WorkflowsPage() {
setLoading(true);
try {
const definition = {
id: values.workflow_id,
name: values.name,
id: values.workflow_id, name: values.name,
nodes: {
start: { id: 'start', type: 'start', name: '开始', config: {}, next_nodes: ['end'] },
end: { id: 'end', type: 'end', name: '结束', config: {}, next_nodes: [] },
......@@ -69,39 +80,30 @@ export default function WorkflowsPage() {
}
};
const handleEditWorkflow = (workflow: Workflow) => {
setEditingWorkflow(workflow);
setEditorModal(true);
};
const handleSaveWorkflow = useCallback(async (data: WorkflowData) => {
const handleSaveWorkflow = useCallback(async (nodes: Node[], edges: Edge[]) => {
if (!editingWorkflow) return;
try {
await fetch(`${API}/api/v1/admin/workflows/${editingWorkflow.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ definition: JSON.stringify(data) }),
body: JSON.stringify({ definition: JSON.stringify({ nodes, edges }) }),
});
message.success('工作流已保存');
fetchWorkflows();
} catch {
message.error('保存失败');
}
} catch { message.error('保存失败'); }
}, [editingWorkflow, token]);
const handleExecuteFromEditor = useCallback(async (data: WorkflowData) => {
const handleExecuteFromEditor = useCallback(async (nodes: Node[], edges: Edge[]) => {
if (!editingWorkflow) return;
try {
const res = await fetch(`${API}/api/v1/workflow/${editingWorkflow.workflow_id}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ workflow_data: data }),
body: JSON.stringify({ workflow_data: { nodes, edges } }),
});
const result = await res.json();
message.success(`执行已启动: ${result.data?.execution_id}`);
} catch {
message.error('执行失败');
}
} catch { message.error('执行失败'); }
}, [editingWorkflow, token]);
const handleExecute = async (workflowId: string) => {
......@@ -113,85 +115,85 @@ export default function WorkflowsPage() {
});
const data = await res.json();
message.success(`执行已启动: ${data.data?.execution_id}`);
} catch {
message.error('执行失败');
}
} catch { message.error('执行失败'); }
};
const statusColor: Record<string, string> = { active: 'green', draft: 'orange', archived: 'default' };
const getEditorInitialData = (): { nodes?: Node[]; edges?: Edge[] } | undefined => {
if (!editingWorkflow?.definition) return undefined;
try { return JSON.parse(editingWorkflow.definition); } catch { return undefined; }
};
const columns = [
{ title: 'ID', dataIndex: 'workflow_id', key: 'workflow_id', width: 180 },
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '描述', dataIndex: 'description', key: 'description' },
{ title: '类别', dataIndex: 'category', key: 'category', render: (v: string) => <Tag>{v}</Tag> },
{ title: '版本', dataIndex: 'version', key: 'version', width: 80 },
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => <Badge status={statusColor[v] as "success" | "warning" | "default"} text={v} /> },
{
title: '操作', key: 'action', width: 200, render: (_: unknown, r: Workflow) => (
<Space>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEditWorkflow(r)}>编辑</Button>
<Button size="small" icon={<PlayCircleOutlined />} disabled={r.status !== 'active'}
onClick={() => handleExecute(r.workflow_id)}>执行</Button>
title: '工作流', key: 'info',
render: (_: unknown, r: Workflow) => (
<div>
<div style={{ fontWeight: 500 }}>{r.name}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{r.workflow_id}</div>
</div>
),
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{
title: '类别', dataIndex: 'category', key: 'category', width: 100,
render: (v: string) => <Tag color="blue">{categoryLabel[v] || v}</Tag>,
},
{ title: '版本', dataIndex: 'version', key: 'version', width: 70, render: (v: number) => `v${v}` },
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
render: (v: string) => <Badge status={statusColor[v] || 'default'} text={statusLabel[v] || v} />,
},
{
title: '操作', key: 'action', width: 160,
render: (_: unknown, r: Workflow) => (
<Space size={0}>
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => { setEditingWorkflow(r); setEditorModal(true); }}>编辑</Button>
<Button type="link" size="small" icon={<PlayCircleOutlined />} disabled={r.status !== 'active'} onClick={() => handleExecute(r.workflow_id)}>执行</Button>
</Space>
),
},
];
const getEditorInitialData = (): WorkflowData | undefined => {
if (!editingWorkflow?.definition) return undefined;
try {
return JSON.parse(editingWorkflow.definition);
} catch {
return undefined;
}
};
return (
<div>
<Card
title={<Space><DeploymentUnitOutlined />工作流管理</Space>}
extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModal(true)}>新建工作流</Button>}
>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'list',
label: '工作流列表',
children: <Table dataSource={workflows} columns={columns} rowKey="id" />,
},
{
key: 'editor',
label: '可视化编辑器',
children: (
<div className="h-[600px]">
<WorkflowEditor
onSave={handleSaveWorkflow}
onExecute={handleExecuteFromEditor}
/>
</div>
),
},
]}
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>工作流管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>设计和管理 AI 工作流,实现复杂业务流程自动化</div>
</div>
{/* 操作栏 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<DeploymentUnitOutlined style={{ color: '#8c8c8c' }} />
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {workflows.length} 个工作流</span>
<Button type="primary" icon={<PlusOutlined />} style={{ marginLeft: 'auto' }} onClick={() => setCreateModal(true)}>
新建工作流
</Button>
</div>
</Card>
{/* 工作流列表 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table dataSource={workflows} columns={columns} rowKey="id" loading={tableLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: (t) => `共 ${t} 条` }}
/>
</Card>
<Modal title="新建工作流" open={createModal} onCancel={() => setCreateModal(false)}
onOk={() => form.submit()} confirmLoading={loading}>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="workflow_id" label="工作流ID" rules={[{ required: true }]}>
{/* 新建工作流 Modal */}
<Modal title="新建工作流" open={createModal} onCancel={() => { setCreateModal(false); form.resetFields(); }}
onOk={() => form.submit()} confirmLoading={loading} okText="创建">
<Form form={form} layout="vertical" onFinish={handleCreate} style={{ marginTop: 16 }}>
<Form.Item name="workflow_id" label="工作流 ID" rules={[{ required: true, message: '请输入工作流ID' }]}>
<Input placeholder="如: smart_pre_consult" />
</Form.Item>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input />
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="请输入工作流名称" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} />
<Input.TextArea rows={2} placeholder="请输入描述(选填)" />
</Form.Item>
<Form.Item name="category" label="类别">
<Select options={[
<Select placeholder="选择类别" options={[
{ value: 'pre_consult', label: '预问诊' },
{ value: 'diagnosis', label: '诊断' },
{ value: 'prescription', label: '处方审核' },
......@@ -201,18 +203,20 @@ export default function WorkflowsPage() {
</Form>
</Modal>
{/* 工作流编辑器弹窗 */}
{/* 可视化编辑器 Modal */}
<Modal
title={`编辑工作流: ${editingWorkflow?.name}`}
title={`编辑工作流 · ${editingWorkflow?.name}`}
open={editorModal}
onCancel={() => { setEditorModal(false); setEditingWorkflow(null); }}
footer={null}
width={1200}
width={1400}
style={{ top: 20 }}
>
<div className="h-[600px]">
<WorkflowEditor
initialData={getEditorInitialData()}
<div style={{ height: 650 }}>
<VisualWorkflowEditor
workflowName={editingWorkflow?.name || '编辑工作流'}
initialNodes={getEditorInitialData()?.nodes}
initialEdges={getEditorInitialData()?.edges}
onSave={handleSaveWorkflow}
onExecute={handleExecuteFromEditor}
/>
......
......@@ -16,7 +16,6 @@ const { Text } = Typography;
const menuItems = [
{ key: '/doctor/workbench', icon: <DashboardOutlined />, label: '工作台' },
{ key: '/doctor/consult', icon: <MessageOutlined />, label: '问诊大厅' },
{ key: '/doctor/ai-assist', icon: <RobotOutlined />, label: 'AI辅助诊断' },
{ key: '/doctor/chronic/review', icon: <SafetyCertificateOutlined />, label: '慢病续方' },
{ key: '/doctor/patient', icon: <FolderOpenOutlined />, label: '患者档案' },
{ key: '/doctor/schedule', icon: <CalendarOutlined />, label: '排班管理' },
......@@ -55,22 +54,30 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Layout style={{ minHeight: '100vh', background: '#f5f8ff' }}>
<header
style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
background: 'linear-gradient(135deg, #002140 0%, #003a8c 50%, #0958d9 100%)',
boxShadow: '0 2px 8px rgba(0,21,41,0.15)',
height: 56, display: 'flex', alignItems: 'center', padding: '0 24px',
background: '#ffffff',
borderBottom: '1px solid #e8f0fc',
boxShadow: '0 1px 4px rgba(0, 82, 204, 0.06)',
}}
>
{/* Logo */}
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, flexShrink: 0 }}
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 24, flexShrink: 0 }}
onClick={() => router.push('/doctor/workbench')}
>
<MedicineBoxOutlined style={{ fontSize: 18, color: '#fff' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#fff' }}>互联网医院</span>
<Tag color="green" style={{ marginLeft: 4, fontSize: 10, lineHeight: '18px', padding: '0 4px' }}>医生工作站</Tag>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<MedicineBoxOutlined style={{ fontSize: 16, color: '#fff' }} />
</div>
<span style={{ fontSize: 15, fontWeight: 700, color: '#1d2129', letterSpacing: '-0.3px' }}>互联网医院</span>
<Tag color="green" style={{ marginLeft: 2, fontSize: 10, lineHeight: '18px', padding: '0 6px', borderRadius: 4 }}>医生工作站</Tag>
</div>
<Menu
......@@ -78,30 +85,35 @@ export default function DoctorLayout({ children }: { children: React.ReactNode }
selectedKeys={getSelectedKeys()}
items={menuItems}
onClick={handleMenuClick}
theme="dark"
className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
className="patient-nav-menu"
style={{ flex: 1, minWidth: 0, border: 'none' }}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
<Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" />
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
<Switch
checked={isOnline}
onChange={setIsOnline}
checkedChildren="接诊中"
unCheckedChildren="离线"
style={{ backgroundColor: isOnline ? '#52c41a' : undefined }}
/>
<Badge count={5} size="small">
<BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
<BellOutlined style={{ fontSize: 18, color: '#595959', cursor: 'pointer' }} />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || user.phone}</Text>
<Avatar size={30} src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text style={{ color: '#1d2129', fontSize: 13, fontWeight: 500 }}>{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
<Button type="primary" size="small" onClick={() => router.push('/login')} style={{ borderRadius: 8 }}>登录</Button>
)}
</div>
</header>
<Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
<Content style={{ marginTop: 56, minHeight: 'calc(100vh - 56px)', background: '#f5f8ff' }}>
{children}
</Content>
</Layout>
......
import GlobalAIFloat from '@/components/GlobalAIFloat';
export default function MainLayout({ children }: { children: React.ReactNode }) {
return <div className="compact-ui">{children}</div>;
return (
<div className="compact-ui">
{children}
<GlobalAIFloat />
</div>
);
}
import { Suspense } from 'react';
import { Spin } from 'antd';
import PageComponent from '@/pages/patient/ConsultCreate';
export default function Page() { return <PageComponent />; }
export default function Page() {
return (
<Suspense fallback={<div style={{ textAlign: 'center', padding: 80 }}><Spin size="large" tip="加载中..." /></div>}>
<PageComponent />
</Suspense>
);
}
import { Suspense } from 'react';
import { Spin } from 'antd';
import PageComponent from '@/pages/patient/Doctors';
export default function Page() { return <PageComponent />; }
export default function Page() {
return (
<Suspense fallback={<div style={{ textAlign: 'center', padding: 80 }}><Spin size="large" /></div>}>
<PageComponent />
</Suspense>
);
}
......@@ -20,7 +20,6 @@ const menuItems = [
key: 'consult-services', icon: <VideoCameraOutlined />, label: '问诊服务',
children: [
{ key: '/patient/doctors', icon: <SearchOutlined />, label: '找医生' },
{ key: '/patient/pre-consult', icon: <RobotOutlined />, label: 'AI预问诊' },
{ key: '/patient/consult', icon: <VideoCameraOutlined />, label: '我的问诊' },
],
},
......@@ -42,9 +41,8 @@ const menuItems = [
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
];
// 所有可路由的 key 列表(用于选中匹配)
const allRouteKeys = [
'/patient/home', '/patient/doctors', '/patient/pre-consult', '/patient/consult',
'/patient/home', '/patient/doctors', '/patient/consult',
'/patient/prescription', '/patient/pharmacy/order', '/patient/payment',
'/patient/chronic', '/patient/health-records', '/patient/profile',
];
......@@ -63,9 +61,7 @@ export default function PatientLayout({ children }: { children: React.ReactNode
];
const handleMenuClick = ({ key }: { key: string }) => {
if (key.startsWith('/')) {
router.push(key);
}
if (key.startsWith('/')) router.push(key);
};
const handleUserMenuClick = ({ key }: { key: string }) => {
......@@ -79,52 +75,81 @@ export default function PatientLayout({ children }: { children: React.ReactNode
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Layout style={{ minHeight: '100vh', background: '#f5f8ff' }}>
{/* 顶部导航 - 白色现代风格 */}
<header
style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 1000,
height: 48, display: 'flex', alignItems: 'center', padding: '0 16px',
background: 'linear-gradient(135deg, #003a8c 0%, #0050b3 50%, #1890ff 100%)',
boxShadow: '0 2px 8px rgba(0,21,41,0.12)',
height: 56, display: 'flex', alignItems: 'center', padding: '0 24px',
background: '#ffffff',
borderBottom: '1px solid #e8f0fc',
boxShadow: '0 1px 4px rgba(0, 82, 204, 0.06)',
}}
>
{/* Logo */}
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 16, flexShrink: 0 }}
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginRight: 24, flexShrink: 0 }}
onClick={() => router.push('/patient/home')}
>
<MedicineBoxOutlined style={{ fontSize: 18, color: '#fff' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#fff' }}>互联网医院</span>
<Tag color="blue" style={{ marginLeft: 4, fontSize: 10, lineHeight: '18px', padding: '0 4px' }}>患者端</Tag>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<MedicineBoxOutlined style={{ fontSize: 16, color: '#fff' }} />
</div>
<span style={{ fontSize: 15, fontWeight: 700, color: '#1d2129', letterSpacing: '-0.3px' }}>互联网医院</span>
<Tag color="blue" style={{ marginLeft: 2, fontSize: 10, lineHeight: '18px', padding: '0 6px', borderRadius: 4 }}>患者端</Tag>
</div>
{/* 导航菜单 */}
<Menu
mode="horizontal"
selectedKeys={getSelectedKeys()}
items={menuItems}
onClick={handleMenuClick}
theme="dark"
className="top-nav-menu"
style={{ flex: 1, minWidth: 0 }}
className="patient-nav-menu"
style={{ flex: 1, minWidth: 0, border: 'none' }}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
{/* 右侧用户区 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
<Badge count={3} size="small">
<BellOutlined style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', cursor: 'pointer' }} />
<BellOutlined style={{ fontSize: 18, color: '#595959', cursor: 'pointer' }} />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 12 }}>{user.real_name || user.phone}</Text>
<Avatar
size={30}
src={user.avatar || null}
icon={<UserOutlined />}
style={{ backgroundColor: '#1890ff' }}
/>
<Text style={{ color: '#1d2129', fontSize: 13, fontWeight: 500 }}>
{user.real_name || user.phone}
</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
<Button
type="primary"
size="small"
onClick={() => router.push('/login')}
style={{ borderRadius: 8, fontWeight: 500 }}
>
登录 / 注册
</Button>
)}
</div>
</header>
<Content style={{ marginTop: 48, padding: 16, minHeight: 'calc(100vh - 48px)', background: '#f0f5ff' }}>
{/* 内容区 - 去掉 padding,让各页面自己控制,避免左右留白过大 */}
<Content style={{
marginTop: 56,
minHeight: 'calc(100vh - 56px)',
background: '#f5f8ff',
}}>
{children}
</Content>
</Layout>
......
......@@ -36,13 +36,13 @@ html, body {
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
font-size: 14px;
line-height: 1.5;
line-height: 1.6;
color: #1d2129;
background-color: #f0f5ff;
background-color: #f5f8ff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
......@@ -73,7 +73,45 @@ a:hover {
background: #91c2f0;
}
/* ===== 顶部导航栏菜单样式 ===== */
/* ===== 患者端白色导航菜单 ===== */
.patient-nav-menu.ant-menu-horizontal {
border-bottom: none !important;
background: transparent !important;
line-height: 56px !important;
}
.patient-nav-menu .ant-menu-item,
.patient-nav-menu .ant-menu-submenu-title {
color: #595959 !important;
font-size: 14px !important;
font-weight: 500 !important;
padding: 0 14px !important;
margin: 0 2px !important;
border-radius: 8px !important;
transition: all 0.2s !important;
}
.patient-nav-menu .ant-menu-item:hover,
.patient-nav-menu .ant-menu-submenu-title:hover,
.patient-nav-menu .ant-menu-item-active,
.patient-nav-menu .ant-menu-submenu-active > .ant-menu-submenu-title {
color: #1890ff !important;
background: #e6f7ff !important;
}
.patient-nav-menu .ant-menu-item-selected {
color: #1890ff !important;
background: #e6f7ff !important;
font-weight: 600 !important;
}
.patient-nav-menu .ant-menu-item::after,
.patient-nav-menu .ant-menu-submenu::after {
display: none !important;
}
/* ===== 顶部导航栏菜单样式(深色,其他布局用) ===== */
.top-nav-menu.ant-menu-horizontal {
border-bottom: none !important;
......@@ -116,18 +154,24 @@ a:hover {
/* ===== Ant Design 紧凑覆盖(仅作用于 .compact-ui 内部) ===== */
.compact-ui .ant-card {
border-radius: 8px !important;
box-shadow: 0 1px 4px rgba(24, 144, 255, 0.06) !important;
border: 1px solid #e6f0fa !important;
border-radius: 10px !important;
box-shadow: 0 1px 3px rgba(0, 82, 204, 0.06) !important;
border: 1px solid #e8f0fc !important;
transition: box-shadow 0.2s ease !important;
}
.compact-ui .ant-card:hover {
box-shadow: 0 4px 12px rgba(0, 82, 204, 0.1) !important;
}
.compact-ui .ant-card .ant-card-head {
min-height: 40px !important;
padding: 0 16px !important;
min-height: 44px !important;
padding: 0 20px !important;
font-size: 14px !important;
border-bottom: 1px solid #e6f0fa !important;
font-weight: 600 !important;
border-bottom: 1px solid #edf2fc !important;
color: #1d2129 !important;
}
.compact-ui .ant-card .ant-card-body {
padding: 16px !important;
padding: 20px !important;
}
.compact-ui .ant-table {
......
......@@ -7,6 +7,7 @@ import { StyleProvider } from '@ant-design/cssinjs';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import GlobalAIFloat from '@/components/GlobalAIFloat';
const queryClient = new QueryClient({
defaultOptions: {
......@@ -26,11 +27,29 @@ export default function Providers({ children }: { children: React.ReactNode }) {
locale={zhCN}
theme={{
token: {
colorPrimary: '#1677ff',
colorPrimary: '#1890ff',
borderRadius: 8,
colorBgLayout: '#f5f5f5',
colorLink: '#1677ff',
colorLinkHover: '#4096ff',
colorBgLayout: '#f5f8ff',
colorLink: '#1890ff',
colorLinkHover: '#40a9ff',
fontSize: 14,
colorTextBase: '#1d2129',
colorBgContainer: '#ffffff',
boxShadow: '0 1px 3px rgba(0, 82, 204, 0.06)',
boxShadowSecondary: '0 2px 8px rgba(0, 82, 204, 0.08)',
},
components: {
Card: {
boxShadow: '0 1px 3px rgba(0, 82, 204, 0.06)',
boxShadowTertiary: '0 1px 3px rgba(0, 82, 204, 0.06)',
},
Button: {
borderRadius: 8,
controlHeight: 36,
},
Input: {
borderRadius: 8,
},
},
}}
>
......
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { Input, Button, Tag, Spin, Collapse, Timeline, Select } from 'antd';
import {
SendOutlined,
RobotOutlined,
UserOutlined,
MedicineBoxOutlined,
HeartOutlined,
BulbOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ToolOutlined,
ThunderboltOutlined,
SwapOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
import type { ChatMessage, ToolCall, AgentOption } from './types';
import { PATIENT_AGENTS, DOCTOR_AGENTS } from './types';
const { TextArea } = Input;
const API = process.env.NEXT_PUBLIC_API_URL || '';
interface ChatPanelProps {
role: 'patient' | 'doctor';
patientContext?: {
patientId?: string;
patientName?: string;
symptoms?: string;
};
}
const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
const { accessToken: token } = useUserStore();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false);
const [sessionId] = useState(() => crypto.randomUUID());
const [selectedAgent, setSelectedAgent] = useState<string>(
role === 'patient' ? 'pre_consult_agent' : 'diagnosis_agent'
);
const messagesEndRef = useRef<HTMLDivElement>(null);
const agents: AgentOption[] = role === 'patient' ? PATIENT_AGENTS : DOCTOR_AGENTS;
useEffect(() => {
if (messages.length === 0) {
const agent = agents.find(a => a.id === selectedAgent);
setMessages([{
role: 'system',
content: role === 'patient'
? `您好!我是${agent?.name || '智能助手'}。请告诉我您目前有什么不舒服的症状?`
: `您好,我是${agent?.name || 'AI诊断助手'}。请描述患者症状或输入您的问题。`,
timestamp: new Date(),
}]);
}
}, []);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = async () => {
if (!inputValue.trim() || loading) return;
const userMessage = inputValue.trim();
setInputValue('');
setMessages(prev => [...prev, { role: 'user', content: userMessage, timestamp: new Date() }]);
setLoading(true);
try {
const requestBody: Record<string, unknown> = {
session_id: sessionId,
message: userMessage,
};
if (patientContext) requestBody.context = patientContext;
const res = await fetch(`${API}/api/v1/agent/${selectedAgent}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(requestBody),
});
if (res.ok) {
const data = await res.json();
const agentData = data.data;
setMessages(prev => [...prev, {
role: 'assistant',
content: agentData?.response || '暂时无法回答,请稍后重试。',
toolCalls: agentData?.tool_calls,
meta: {
iterations: agentData?.iterations,
tokens: agentData?.total_tokens,
agent_id: selectedAgent,
},
timestamp: new Date(),
}]);
} else {
setMessages(prev => [...prev, {
role: 'assistant',
content: '请求失败,请稍后重试。',
timestamp: new Date(),
}]);
}
} catch {
setMessages(prev => [...prev, {
role: 'assistant',
content: '网络错误,请检查网络连接。',
timestamp: new Date(),
}]);
} finally {
setLoading(false);
}
};
const handleAgentChange = (agentId: string) => {
setSelectedAgent(agentId);
const agent = agents.find(a => a.id === agentId);
setMessages([{
role: 'system',
content: `已切换到「${agent?.name}」。${agent?.description}`,
timestamp: new Date(),
}]);
};
const quickItems = role === 'patient'
? ['头痛', '发热', '咳嗽', '腹痛', '乏力', '失眠']
: ['鉴别诊断建议', '用药方案推荐', '检查项目建议'];
const renderToolCalls = (toolCalls?: ToolCall[]) => {
if (!toolCalls || toolCalls.length === 0) return null;
return (
<Collapse
size="small"
className="mt-2"
style={{ background: '#f9fafb', borderRadius: 8 }}
items={[{
key: 'tools',
label: (
<span style={{ fontSize: 12, color: '#6b7280' }}>
<ToolOutlined style={{ marginRight: 4 }} />
调用了 {toolCalls.length} 个工具
</span>
),
children: (
<Timeline
style={{ marginTop: 8, marginBottom: 0 }}
items={toolCalls.map((tc, idx) => ({
color: tc.success ? 'green' : 'red',
dot: tc.success
? <CheckCircleOutlined style={{ fontSize: 12 }} />
: <CloseCircleOutlined style={{ fontSize: 12 }} />,
children: (
<div key={idx} style={{ fontSize: 12 }}>
<div style={{ fontWeight: 500, color: '#374151' }}>{tc.tool_name}</div>
<div style={{ color: '#9ca3af', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 240 }}>
参数: {tc.arguments}
</div>
</div>
),
}))}
/>
),
}]}
/>
);
};
const renderMessage = (msg: ChatMessage, index: number) => {
if (msg.role === 'system') {
return (
<div key={index} style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
<div style={{
background: role === 'patient' ? '#f0fdf4' : '#eff6ff',
color: role === 'patient' ? '#16a34a' : '#2563eb',
fontSize: 12, padding: '6px 16px', borderRadius: 20,
}}>
<RobotOutlined style={{ marginRight: 4 }} />{msg.content}
</div>
</div>
);
}
const isUser = msg.role === 'user';
const bubbleColor = isUser
? (role === 'patient' ? 'linear-gradient(135deg, #3b82f6, #2563eb)' : 'linear-gradient(135deg, #3b82f6, #1d4ed8)')
: '#ffffff';
return (
<div key={index} style={{ display: 'flex', justifyContent: isUser ? 'flex-end' : 'flex-start', marginBottom: 16 }}>
{!isUser && (
<div style={{
width: 32, height: 32, borderRadius: '50%', flexShrink: 0, marginRight: 8,
background: role === 'patient'
? 'linear-gradient(135deg, #22c55e, #06b6d4)'
: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{role === 'patient'
? <HeartOutlined style={{ color: '#fff', fontSize: 14 }} />
: <RobotOutlined style={{ color: '#fff', fontSize: 14 }} />
}
</div>
)}
<div style={{ maxWidth: '78%' }}>
<div style={{
padding: '10px 14px', borderRadius: 16, fontSize: 13, lineHeight: 1.6,
background: bubbleColor, color: isUser ? '#fff' : '#374151',
border: isUser ? 'none' : '1px solid #f3f4f6',
boxShadow: isUser ? 'none' : '0 1px 3px rgba(0,0,0,0.06)',
borderBottomRightRadius: isUser ? 4 : 16,
borderBottomLeftRadius: isUser ? 16 : 4,
}}>
{msg.content}
</div>
{!isUser && msg.meta && (
<div style={{ fontSize: 11, color: '#9ca3af', marginTop: 4, paddingLeft: 4 }}>
<ThunderboltOutlined style={{ marginRight: 2 }} />
{msg.meta.iterations || 0}次推理 · {msg.meta.tokens || 0} tokens
{msg.meta.agent_id && (
<Tag style={{ marginLeft: 4, fontSize: 10 }} color="processing">{msg.meta.agent_id}</Tag>
)}
</div>
)}
{!isUser && renderToolCalls(msg.toolCalls)}
</div>
{isUser && (
<div style={{
width: 32, height: 32, borderRadius: '50%', flexShrink: 0, marginLeft: 8,
background: '#dbeafe', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<UserOutlined style={{ color: '#3b82f6', fontSize: 14 }} />
</div>
)}
</div>
);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* Agent选择器 */}
<div style={{
padding: '8px 12px', borderBottom: '1px solid #f3f4f6',
display: 'flex', alignItems: 'center', gap: 8,
background: role === 'patient' ? '#f0fdf4' : '#eff6ff',
}}>
<SwapOutlined style={{ color: '#6b7280', fontSize: 12 }} />
<Select
size="small"
value={selectedAgent}
onChange={handleAgentChange}
style={{ flex: 1 }}
options={agents.map(a => ({
value: a.id,
label: <span style={{ fontSize: 12 }}>{a.name} - {a.description}</span>,
}))}
/>
</div>
{/* 患者上下文 */}
{role === 'doctor' && patientContext?.patientName && (
<div style={{
padding: '6px 12px', background: '#eff6ff', borderBottom: '1px solid #dbeafe',
fontSize: 12, color: '#2563eb',
}}>
<MedicineBoxOutlined style={{ marginRight: 4 }} />
当前患者:{patientContext.patientName}
{patientContext.symptoms && <span style={{ color: '#6b7280', marginLeft: 8 }}>主诉:{patientContext.symptoms}</span>}
</div>
)}
{/* 消息列表 */}
<div style={{ flex: 1, overflowY: 'auto', padding: 12, background: '#fafafa' }}>
{messages.map((msg, idx) => renderMessage(msg, idx))}
{loading && (
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 16 }}>
<div style={{
width: 32, height: 32, borderRadius: '50%', flexShrink: 0, marginRight: 8,
background: role === 'patient'
? 'linear-gradient(135deg, #22c55e, #06b6d4)'
: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<RobotOutlined style={{ color: '#fff', fontSize: 14 }} />
</div>
<div style={{
background: '#fff', border: '1px solid #f3f4f6', padding: '10px 14px',
borderRadius: 16, borderBottomLeftRadius: 4, boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
}}>
<Spin size="small" />
<span style={{ marginLeft: 8, color: '#9ca3af', fontSize: 13 }}>AI思考中...</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* 快捷回复 */}
<div style={{ padding: '6px 12px', borderTop: '1px solid #f3f4f6', background: '#fff' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{quickItems.map((text, idx) => (
<Tag
key={idx}
style={{ cursor: 'pointer', fontSize: 12, margin: 0 }}
onClick={() => setInputValue(prev => prev ? `${prev}、${text}` : text)}
>
{role === 'patient' ? <HeartOutlined style={{ marginRight: 2 }} /> : <BulbOutlined style={{ marginRight: 2 }} />}
{text}
</Tag>
))}
</div>
</div>
{/* 输入区域 */}
<div style={{ padding: '10px 12px', borderTop: '1px solid #e5e7eb', background: '#fff' }}>
<div style={{ display: 'flex', gap: 8 }}>
<TextArea
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onPressEnter={e => { if (!e.shiftKey) { e.preventDefault(); sendMessage(); } }}
placeholder={role === 'patient' ? '请描述您的症状...' : '描述症状或输入问题...'}
autoSize={{ minRows: 1, maxRows: 3 }}
style={{ flex: 1, borderRadius: 8 }}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={sendMessage}
loading={loading}
style={{ alignSelf: 'flex-end', borderRadius: 8 }}
/>
</div>
{role === 'doctor' && (
<div style={{ fontSize: 11, color: '#9ca3af', marginTop: 4, textAlign: 'center' }}>
AI建议仅供参考,请结合临床实际情况
</div>
)}
</div>
</div>
);
};
export default ChatPanel;
'use client';
import React from 'react';
import { Tooltip } from 'antd';
import {
RobotOutlined,
CloseOutlined,
HeartOutlined,
MessageOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
import { useAIAssistStore } from '../../store/aiAssistStore';
import ChatPanel from './ChatPanel';
const FloatContainer: React.FC = () => {
const { user } = useUserStore();
const { isOpen, patientContext, openDrawer, closeDrawer } = useAIAssistStore();
if (!user) return null;
const isDoctor = user.role === 'doctor';
const isPatient = user.role === 'patient';
if (!isDoctor && !isPatient) return null;
const primaryColor = isPatient
? 'linear-gradient(135deg, #22c55e, #06b6d4)'
: 'linear-gradient(135deg, #3b82f6, #8b5cf6)';
const title = isPatient ? 'AI智能助手' : 'AI诊断助手';
const subtitle = isPatient ? '智能预问诊 · 健康咨询' : '辅助诊断 · 处方审核';
return (
<>
{/* 悬浮按钮 - 右侧固定 */}
{!isOpen && (
<Tooltip title={title} placement="left">
<div
onClick={() => openDrawer(patientContext || undefined)}
style={{
position: 'fixed',
right: 0,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1001,
width: 48,
height: 96,
background: primaryColor,
borderRadius: '12px 0 0 12px',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
boxShadow: '-2px 0 12px rgba(0,0,0,0.15)',
transition: 'all 0.3s ease',
}}
onMouseEnter={e => { e.currentTarget.style.width = '56px'; }}
onMouseLeave={e => { e.currentTarget.style.width = '48px'; }}
>
{isPatient
? <HeartOutlined style={{ color: '#fff', fontSize: 20 }} />
: <RobotOutlined style={{ color: '#fff', fontSize: 20 }} />
}
<div style={{
color: '#fff', fontSize: 10, writingMode: 'vertical-rl',
letterSpacing: 2, fontWeight: 500,
}}>
{isPatient ? 'AI助手' : 'AI诊断'}
</div>
</div>
</Tooltip>
)}
{/* 展开的对话面板 */}
{isOpen && (
<div style={{
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: 400,
zIndex: 1002,
background: '#fff',
boxShadow: '-4px 0 24px rgba(0,0,0,0.12)',
display: 'flex',
flexDirection: 'column',
transition: 'all 0.3s ease',
borderLeft: '1px solid #e5e7eb',
}}>
{/* 头部 */}
<div style={{
padding: '12px 16px',
background: primaryColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 36, height: 36, borderRadius: 10,
background: 'rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{isPatient
? <MessageOutlined style={{ color: '#fff', fontSize: 18 }} />
: <RobotOutlined style={{ color: '#fff', fontSize: 18 }} />
}
</div>
<div>
<div style={{ color: '#fff', fontWeight: 600, fontSize: 14 }}>{title}</div>
<div style={{ color: 'rgba(255,255,255,0.8)', fontSize: 11 }}>{subtitle}</div>
</div>
</div>
<div
onClick={() => closeDrawer()}
style={{
width: 28, height: 28, borderRadius: '50%',
background: 'rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', transition: 'background 0.2s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.3)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.2)'; }}
>
<CloseOutlined style={{ color: '#fff', fontSize: 14 }} />
</div>
</div>
{/* 聊天面板 */}
<div style={{ flex: 1, overflow: 'hidden' }}>
<ChatPanel
role={isPatient ? 'patient' : 'doctor'}
patientContext={isDoctor ? (patientContext || undefined) : undefined}
/>
</div>
</div>
)}
{/* 遮罩层 */}
{isOpen && (
<div
onClick={() => closeDrawer()}
style={{
position: 'fixed',
inset: 0,
zIndex: 1001,
background: 'rgba(0,0,0,0.1)',
}}
/>
)}
</>
);
};
export default FloatContainer;
export { default } from './FloatContainer';
export interface ToolCall {
tool_name: string;
call_id: string;
arguments: string;
result: { success: boolean; data?: unknown; error?: string };
success: boolean;
}
export interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
toolCalls?: ToolCall[];
meta?: { iterations?: number; tokens?: number; agent_id?: string; workflow_id?: string };
timestamp: Date;
}
export interface AgentOption {
id: string;
name: string;
description: string;
icon: string;
}
export const PATIENT_AGENTS: AgentOption[] = [
{ id: 'pre_consult_agent', name: '预问诊助手', description: '智能收集症状,生成预问诊报告', icon: 'heart' },
{ id: 'follow_up_agent', name: '随访助手', description: '用药提醒、复诊管理', icon: 'calendar' },
];
export const DOCTOR_AGENTS: AgentOption[] = [
{ id: 'diagnosis_agent', name: '诊断辅助', description: '鉴别诊断、检查建议', icon: 'robot' },
{ id: 'prescription_agent', name: '处方审核', description: '药物相互作用、禁忌症检查', icon: 'medicine' },
];
'use client';
import React, { useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Switch, Typography, Tag } from 'antd';
import {
DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined,
SettingOutlined, LogoutOutlined, BellOutlined, MedicineBoxOutlined,
RobotOutlined, DollarOutlined, FolderOpenOutlined, SafetyCertificateOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/doctor/workbench', icon: <DashboardOutlined />, label: '工作台' },
{ key: '/doctor/consult', icon: <MessageOutlined />, label: '问诊大厅' },
{ key: '/doctor/ai-assist', icon: <RobotOutlined />, label: 'AI辅助诊断' },
{ key: '/doctor/chronic/review', icon: <SafetyCertificateOutlined />, label: '慢病续方' },
{ key: '/doctor/patient', icon: <FolderOpenOutlined />, label: '患者档案' },
{ key: '/doctor/schedule', icon: <CalendarOutlined />, label: '排班管理' },
{ key: '/doctor/income', icon: <DollarOutlined />, label: '收入统计' },
{ key: '/doctor/profile', icon: <UserOutlined />, label: '个人信息' },
];
const DoctorLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const [isOnline, setIsOnline] = useState(false);
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/doctor/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #002140 0%, #003a8c 50%, #0958d9 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.15)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/doctor/workbench')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="green" className="ml-1! text-[10px]! leading-4! px-1!">医生工作站</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" />
<Badge count={5} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default DoctorLayout;
'use client';
import React, { useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Switch, Typography, Tag } from 'antd';
import {
DashboardOutlined, UserOutlined, MessageOutlined, CalendarOutlined,
SettingOutlined, LogoutOutlined, BellOutlined, MedicineBoxOutlined,
RobotOutlined, DollarOutlined, FolderOpenOutlined, SafetyCertificateOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/doctor/workbench', icon: <DashboardOutlined />, label: '工作台' },
{ key: '/doctor/consult', icon: <MessageOutlined />, label: '问诊大厅' },
{ key: '/doctor/chronic/review', icon: <SafetyCertificateOutlined />, label: '慢病续方' },
{ key: '/doctor/patient', icon: <FolderOpenOutlined />, label: '患者档案' },
{ key: '/doctor/schedule', icon: <CalendarOutlined />, label: '排班管理' },
{ key: '/doctor/income', icon: <DollarOutlined />, label: '收入统计' },
{ key: '/doctor/profile', icon: <UserOutlined />, label: '个人信息' },
];
const DoctorLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const [isOnline, setIsOnline] = useState(false);
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/doctor/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #002140 0%, #003a8c 50%, #0958d9 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.15)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/doctor/workbench')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="green" className="ml-1! text-[10px]! leading-4! px-1!">医生工作站</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Switch checked={isOnline} onChange={setIsOnline} checkedChildren="在线" unCheckedChildren="离线" size="small" />
<Badge count={5} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default DoctorLayout;
......@@ -85,7 +85,7 @@ const MainLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="bottomRight">
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
<Avatar src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1677ff' }} />
<Avatar src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#1677ff' }} />
<span style={{ color: 'rgba(0,0,0,0.88)' }}>{user.real_name || user.phone}</span>
</div>
</Dropdown>
......
'use client';
import React from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd';
import {
HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined,
HeartOutlined, SettingOutlined, LogoutOutlined, BellOutlined,
RobotOutlined, VideoCameraOutlined, PayCircleOutlined,
ShoppingCartOutlined, FolderOpenOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/patient/home', icon: <HomeOutlined />, label: '首页' },
{ key: '/patient/doctors', icon: <MedicineBoxOutlined />, label: '医生列表' },
{ key: '/patient/pre-consult', icon: <RobotOutlined />, label: 'AI预问诊' },
{ key: '/patient/consult', icon: <VideoCameraOutlined />, label: '问诊' },
{ key: '/patient/prescription', icon: <FileTextOutlined />, label: '处方' },
{ key: '/patient/payment', icon: <PayCircleOutlined />, label: '支付' },
{ key: '/patient/pharmacy/order', icon: <ShoppingCartOutlined />, label: '药品配送' },
{ key: '/patient/chronic', icon: <HeartOutlined />, label: '慢病管理' },
{ key: '/patient/health-records', icon: <FolderOpenOutlined />, label: '健康档案' },
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
];
const PatientLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人中心' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/patient/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #003a8c 0%, #0050b3 50%, #1890ff 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.12)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/patient/home')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="blue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-300/30!">患者端</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Badge count={3} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default PatientLayout;
'use client';
import React from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag } from 'antd';
import {
HomeOutlined, UserOutlined, MedicineBoxOutlined, FileTextOutlined,
HeartOutlined, SettingOutlined, LogoutOutlined, BellOutlined,
RobotOutlined, VideoCameraOutlined, PayCircleOutlined,
ShoppingCartOutlined, FolderOpenOutlined,
} from '@ant-design/icons';
import { useUserStore } from '../../store/userStore';
const { Header, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/patient/home', icon: <HomeOutlined />, label: '首页' },
{ key: '/patient/doctors', icon: <MedicineBoxOutlined />, label: '医生列表' },
{ key: '/patient/consult', icon: <VideoCameraOutlined />, label: '问诊' },
{ key: '/patient/prescription', icon: <FileTextOutlined />, label: '处方' },
{ key: '/patient/payment', icon: <PayCircleOutlined />, label: '支付' },
{ key: '/patient/pharmacy/order', icon: <ShoppingCartOutlined />, label: '药品配送' },
{ key: '/patient/chronic', icon: <HeartOutlined />, label: '慢病管理' },
{ key: '/patient/health-records', icon: <FolderOpenOutlined />, label: '健康档案' },
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
];
const PatientLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const router = useRouter();
const pathname = usePathname();
const { user, logout } = useUserStore();
const userMenuItems = [
{ key: 'profile', icon: <UserOutlined />, label: '个人中心' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { logout(); router.push('/login'); }
else if (key === 'profile') router.push('/patient/profile');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Header className="fixed! top-0! left-0! right-0! z-100! flex! items-center! h-12! px-4! leading-12!"
style={{ background: 'linear-gradient(135deg, #003a8c 0%, #0050b3 50%, #1890ff 100%)', borderBottom: 'none', boxShadow: '0 2px 8px rgba(0,21,41,0.12)', padding: '0 16px' }}>
<div className="flex items-center gap-2 cursor-pointer mr-4 shrink-0"
onClick={() => router.push('/patient/home')}>
<MedicineBoxOutlined className="text-lg text-white" />
<span className="text-sm font-bold text-white">互联网医院</span>
<Tag color="blue" className="ml-1! text-[10px]! leading-4! px-1! border-blue-300/30!">患者端</Tag>
</div>
<Menu
mode="horizontal"
selectedKeys={[pathname || '']}
items={menuItems}
onClick={({ key }) => router.push(key)}
theme="dark"
className="flex-1! border-none! min-w-0! bg-transparent! text-xs! [&_.ant-menu-item]:text-blue-100/80! [&_.ant-menu-item-selected]:text-white! [&_.ant-menu-item-selected]:bg-white/10! [&_.ant-menu-item:hover]:text-white!"
/>
<div className="flex items-center gap-3 shrink-0">
<Badge count={3} size="small">
<BellOutlined className="text-base text-white/70 cursor-pointer hover:text-white" />
</Badge>
{user ? (
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space className="cursor-pointer">
<Avatar size="small" src={user.avatar || null} icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
<Text className="text-white/90! text-xs!">{user.real_name || user.phone}</Text>
</Space>
</Dropdown>
) : (
<Button size="small" type="primary" onClick={() => router.push('/login')}>登录 / 注册</Button>
)}
</div>
</Header>
<Content className="mt-12! p-3! min-h-[calc(100vh-48px)]! bg-[#f0f5ff]!">
{children}
</Content>
</Layout>
);
};
export default PatientLayout;
'use client';
import React, { useCallback, useState, useMemo, DragEvent } from 'react';
import {
ReactFlow,
Node,
Edge,
Controls,
Background,
MiniMap,
addEdge,
Connection,
useNodesState,
useEdgesState,
BackgroundVariant,
NodeTypes,
Handle,
Position,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Button, Drawer, Form, Input, Select, message, Space, Card, Tooltip } from 'antd';
import {
SaveOutlined,
PlayCircleOutlined,
DeleteOutlined,
RobotOutlined,
ToolOutlined,
BranchesOutlined,
UserOutlined,
SyncOutlined,
CodeOutlined,
ApiOutlined,
PlayCircleFilled,
StopFilled,
} from '@ant-design/icons';
export type NodeType = 'start' | 'end' | 'agent' | 'tool' | 'condition' | 'human_review' | 'parallel' | 'loop' | 'code' | 'http';
interface NodeData {
label: string;
nodeType: NodeType;
config?: Record<string, unknown>;
}
const NODE_CONFIGS: { type: NodeType; label: string; icon: React.ReactNode; color: string; bgColor: string }[] = [
{ type: 'start', label: '开始', icon: <PlayCircleFilled />, color: '#52c41a', bgColor: '#f6ffed' },
{ type: 'end', label: '结束', icon: <StopFilled />, color: '#ff4d4f', bgColor: '#fff2f0' },
{ type: 'agent', label: 'Agent', icon: <RobotOutlined />, color: '#1890ff', bgColor: '#e6f7ff' },
{ type: 'tool', label: 'Tool', icon: <ToolOutlined />, color: '#722ed1', bgColor: '#f9f0ff' },
{ type: 'condition', label: '条件分支', icon: <BranchesOutlined />, color: '#faad14', bgColor: '#fffbe6' },
{ type: 'human_review', label: '人工审核', icon: <UserOutlined />, color: '#eb2f96', bgColor: '#fff0f6' },
{ type: 'parallel', label: '并行执行', icon: <SyncOutlined />, color: '#13c2c2', bgColor: '#e6fffb' },
{ type: 'loop', label: '循环', icon: <SyncOutlined />, color: '#fa8c16', bgColor: '#fff7e6' },
{ type: 'code', label: '代码', icon: <CodeOutlined />, color: '#2f54eb', bgColor: '#f0f5ff' },
{ type: 'http', label: 'HTTP请求', icon: <ApiOutlined />, color: '#a0d911', bgColor: '#fcffe6' },
];
const AGENTS = [
{ value: 'pre_consult_agent', label: '预问诊Agent' },
{ value: 'diagnosis_agent', label: '诊断辅助Agent' },
{ value: 'prescription_agent', label: '处方审核Agent' },
{ value: 'follow_up_agent', label: '随访管理Agent' },
];
const TOOLS = [
{ value: 'query_drug', label: '药品查询' },
{ value: 'query_medical_record', label: '病历检索' },
{ value: 'query_symptom_knowledge', label: '症状知识查询' },
{ value: 'recommend_department', label: '科室推荐' },
{ value: 'check_drug_interaction', label: '药物相互作用检查' },
{ value: 'check_contraindication', label: '禁忌症检查' },
{ value: 'calculate_dosage', label: '剂量计算' },
{ value: 'search_medical_knowledge', label: '知识库检索' },
];
const CustomNode = ({ data, selected }: { data: NodeData; selected: boolean }) => {
const config = NODE_CONFIGS.find(n => n.type === data.nodeType);
const isStart = data.nodeType === 'start';
const isEnd = data.nodeType === 'end';
return (
<div
style={{
padding: '12px 20px',
borderRadius: 8,
border: `2px solid ${selected ? '#1890ff' : config?.color || '#d9d9d9'}`,
backgroundColor: config?.bgColor || '#fff',
minWidth: 140,
textAlign: 'center',
boxShadow: selected ? '0 0 0 2px rgba(24,144,255,0.2)' : '0 2px 8px rgba(0,0,0,0.08)',
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
{!isStart && (
<Handle
type="target"
position={Position.Top}
style={{ background: config?.color, width: 10, height: 10 }}
/>
)}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
<span style={{ color: config?.color, fontSize: 18 }}>{config?.icon}</span>
<span style={{ fontWeight: 500, color: '#1d2129' }}>{data.label}</span>
</div>
{!isEnd && (
<Handle
type="source"
position={Position.Bottom}
style={{ background: config?.color, width: 10, height: 10 }}
/>
)}
</div>
);
};
const nodeTypes: NodeTypes = {
custom: CustomNode,
};
interface VisualWorkflowEditorProps {
initialNodes?: Node<NodeData>[];
initialEdges?: Edge[];
workflowName?: string;
onSave?: (nodes: Node<NodeData>[], edges: Edge[]) => void;
onExecute?: (nodes: Node<NodeData>[], edges: Edge[]) => void;
}
export default function VisualWorkflowEditor({
initialNodes,
initialEdges,
workflowName = '新工作流',
onSave,
onExecute,
}: VisualWorkflowEditorProps) {
const defaultNodes: Node<NodeData>[] = initialNodes || [
{ id: 'start', type: 'custom', position: { x: 250, y: 50 }, data: { label: '开始', nodeType: 'start' } },
{ id: 'end', type: 'custom', position: { x: 250, y: 300 }, data: { label: '结束', nodeType: 'end' } },
];
const defaultEdges: Edge[] = initialEdges || [
{ id: 'e-start-end', source: 'start', target: 'end', animated: true, style: { stroke: '#1890ff' } },
];
const [nodes, setNodes, onNodesChange] = useNodesState(defaultNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(defaultEdges);
const [selectedNode, setSelectedNode] = useState<Node<NodeData> | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [form] = Form.useForm();
const onConnect = useCallback(
(params: Connection) => {
setEdges((eds) => addEdge({ ...params, animated: true, style: { stroke: '#1890ff' } }, eds));
},
[setEdges]
);
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow') as NodeType;
if (!type) return;
const config = NODE_CONFIGS.find(n => n.type === type);
const position = {
x: event.clientX - 300,
y: event.clientY - 100,
};
const newNode: Node<NodeData> = {
id: `${type}_${Date.now()}`,
type: 'custom',
position,
data: { label: config?.label || type, nodeType: type },
};
setNodes((nds) => nds.concat(newNode));
message.success(`已添加 ${config?.label} 节点`);
},
[setNodes]
);
const onNodeClick = useCallback((_: React.MouseEvent, node: Node<NodeData>) => {
setSelectedNode(node);
form.setFieldsValue({
label: node.data.label,
...node.data.config,
});
setDrawerOpen(true);
}, [form]);
const handleDeleteNode = useCallback(() => {
if (!selectedNode) return;
if (selectedNode.data.nodeType === 'start') {
message.warning('不能删除开始节点');
return;
}
setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id));
setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id));
setSelectedNode(null);
setDrawerOpen(false);
message.success('节点已删除');
}, [selectedNode, setNodes, setEdges]);
const handleUpdateNode = useCallback((values: Record<string, unknown>) => {
if (!selectedNode) return;
const { label, ...config } = values;
setNodes((nds) =>
nds.map((n) =>
n.id === selectedNode.id
? { ...n, data: { ...n.data, label: label as string, config } }
: n
)
);
message.success('节点配置已更新');
setDrawerOpen(false);
}, [selectedNode, setNodes]);
const handleSave = useCallback(() => {
onSave?.(nodes, edges);
message.success('工作流已保存');
}, [nodes, edges, onSave]);
const handleExecute = useCallback(() => {
onExecute?.(nodes, edges);
}, [nodes, edges, onExecute]);
const onDragStart = (event: DragEvent, nodeType: NodeType) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
const renderNodeConfig = () => {
if (!selectedNode) return null;
const nodeType = selectedNode.data.nodeType;
return (
<>
<Form.Item name="label" label="节点名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
{nodeType === 'agent' && (
<Form.Item name="agent_id" label="选择Agent" rules={[{ required: true }]}>
<Select options={AGENTS} placeholder="选择Agent" />
</Form.Item>
)}
{nodeType === 'tool' && (
<Form.Item name="tool_name" label="选择Tool" rules={[{ required: true }]}>
<Select options={TOOLS} placeholder="选择工具" />
</Form.Item>
)}
{nodeType === 'condition' && (
<Form.Item name="expression" label="条件表达式">
<Input placeholder="如: status == 'approved'" />
</Form.Item>
)}
{nodeType === 'human_review' && (
<>
<Form.Item name="assignee_role" label="审核角色">
<Select options={[
{ value: 'doctor', label: '医生' },
{ value: 'pharmacist', label: '药师' },
{ value: 'admin', label: '管理员' },
]} />
</Form.Item>
<Form.Item name="title" label="任务标题">
<Input />
</Form.Item>
</>
)}
{nodeType === 'http' && (
<>
<Form.Item name="url" label="请求URL" rules={[{ required: true }]}>
<Input placeholder="https://api.example.com/endpoint" />
</Form.Item>
<Form.Item name="method" label="请求方法">
<Select defaultValue="GET" options={[
{ value: 'GET', label: 'GET' },
{ value: 'POST', label: 'POST' },
{ value: 'PUT', label: 'PUT' },
{ value: 'DELETE', label: 'DELETE' },
]} />
</Form.Item>
</>
)}
{nodeType === 'code' && (
<Form.Item name="code" label="代码">
<Input.TextArea rows={6} placeholder="// JavaScript代码" />
</Form.Item>
)}
{nodeType === 'loop' && (
<>
<Form.Item name="max_iterations" label="最大迭代次数">
<Input type="number" defaultValue={10} />
</Form.Item>
<Form.Item name="condition" label="退出条件">
<Input placeholder="如: loop_index >= 5" />
</Form.Item>
</>
)}
</>
);
};
return (
<div className="flex h-full" style={{ height: '100%', minHeight: 600 }}>
{/* 左侧节点面板 */}
<div className="w-52 border-r bg-gray-50 p-3 flex flex-col">
<div className="text-sm font-bold mb-3 text-gray-700">拖拽添加节点</div>
<div className="space-y-2 flex-1 overflow-auto">
{NODE_CONFIGS.map((config) => (
<Tooltip key={config.type} title={`拖拽添加${config.label}节点`} placement="right">
<div
draggable
onDragStart={(e) => onDragStart(e, config.type)}
className="flex items-center gap-2 p-2 rounded-lg cursor-grab active:cursor-grabbing transition-all hover:shadow-md"
style={{
border: `1px solid ${config.color}`,
backgroundColor: config.bgColor,
}}
>
<span style={{ color: config.color, fontSize: 16 }}>{config.icon}</span>
<span className="text-sm font-medium" style={{ color: config.color }}>{config.label}</span>
</div>
</Tooltip>
))}
</div>
</div>
{/* 中间画布 */}
<div className="flex-1 relative">
<div className="absolute top-3 left-3 z-10 flex items-center gap-2">
<span className="text-lg font-bold text-gray-800">{workflowName}</span>
</div>
<div className="absolute top-3 right-3 z-10">
<Space>
<Button icon={<SaveOutlined />} onClick={handleSave}>保存</Button>
<Button type="primary" icon={<PlayCircleOutlined />} onClick={handleExecute}>执行</Button>
</Space>
</div>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeClick={onNodeClick}
nodeTypes={nodeTypes}
fitView
style={{ backgroundColor: '#fafafa' }}
>
<Controls />
<MiniMap
nodeColor={(node) => {
const config = NODE_CONFIGS.find(n => n.type === (node.data as NodeData).nodeType);
return config?.color || '#1890ff';
}}
style={{ backgroundColor: '#f0f0f0' }}
/>
<Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#d9d9d9" />
</ReactFlow>
</div>
{/* 右侧配置面板 */}
<Drawer
title={
<div className="flex items-center justify-between">
<span>节点配置</span>
{selectedNode && selectedNode.data.nodeType !== 'start' && (
<Button type="text" danger icon={<DeleteOutlined />} onClick={handleDeleteNode}>
删除
</Button>
)}
</div>
}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={360}
>
<Form form={form} layout="vertical" onFinish={handleUpdateNode}>
{renderNodeConfig()}
<Form.Item>
<Button type="primary" htmlType="submit" block>
保存配置
</Button>
</Form.Item>
</Form>
</Drawer>
</div>
);
}
......@@ -323,12 +323,13 @@ const AdminAIConfigPage: React.FC = () => {
};
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<RobotOutlined className="mr-1" />AI 配置
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>AI 配置</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>配置平台 AI 模型与 Prompt 模板</div>
</div>
<Tabs defaultActiveKey="model" size="small" items={[
<Tabs defaultActiveKey="model" items={[
{
key: 'model',
label: <span><SettingOutlined /> 模型选择</span>,
......
......@@ -227,25 +227,28 @@ const AdminAdminsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">管理员管理</h4>
<span className="text-xs text-gray-400">共 {total} 条记录</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>管理员管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理后台管理员账号与权限</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {total} 条记录</span>
</div>
<Card size="small">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/用户名" prefix={<SearchOutlined />} value={keyword}
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 180 }} size="small" />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setPage(1); fetchAdmins(); }}>重置</Button>
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 200, borderRadius: 8 }} />
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setPage(1); fetchAdmins(); }}>重置</Button>
</div>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>添加管理员</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>添加管理员</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table
columns={columns}
dataSource={admins}
......
......@@ -109,47 +109,48 @@ const AdminCompliancePage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<SafetyCertificateOutlined className="mr-1" />合规报表
</h4>
<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>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<Statistic title="审计日志" value={logsTotal} prefix={<FileSearchOutlined />} suffix="条" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="待处理" value={reports.filter((r) => r.status === 'pending').length} valueStyle={{ color: '#fa8c16' }} suffix="项" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<Statistic title="已上报" value={reports.filter((r) => r.status === 'submitted').length} suffix="项" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #722ed1' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #722ed1' }}>
<Statistic title="已生成" value={reports.filter((r) => r.status === 'generated').length} suffix="项" />
</Card>
</Col>
</Row>
<Tabs defaultActiveKey="logs" size="small" items={[
<Tabs defaultActiveKey="logs" items={[
{
key: 'logs',
label: <span><AuditOutlined /> 审计日志</span>,
children: (
<Card size="small">
<div className="flex flex-wrap items-center gap-2 mb-2">
<Input placeholder="搜索操作内容" style={{ width: 160 }} size="small" allowClear />
<RangePicker size="small" />
<Button type="primary" size="small" ghost onClick={() => fetchLogs(1)}>查询</Button>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input placeholder="搜索操作内容" style={{ width: 180, borderRadius: 8 }} allowClear />
<RangePicker />
<Button type="primary" ghost onClick={() => fetchLogs(1)}>查询</Button>
</div>
<Table columns={logColumns} dataSource={logs} rowKey="id" size="small" loading={logsLoading}
pagination={{ current: logsPage, pageSize: 10, total: logsTotal, size: 'small',
pagination={{ current: logsPage, pageSize: 10, total: logsTotal,
onChange: (p) => { setLogsPage(p); fetchLogs(p); } }} />
</Card>
),
......@@ -158,19 +159,19 @@ const AdminCompliancePage: React.FC = () => {
key: 'export',
label: <span><DownloadOutlined /> 数据导出</span>,
children: (
<Card size="small">
<Row gutter={[8, 8]}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Row gutter={[16, 16]}>
{[
{ title: '问诊数据', desc: '问诊记录、诊断、处方', icon: <FileSearchOutlined className="text-xl text-[#1890ff]" /> },
{ title: '用户数据', desc: '患者和医生基本信息', icon: <UserOutlined className="text-xl text-green-500" /> },
{ title: '处方数据', desc: '处方记录、用药统计', icon: <AuditOutlined className="text-xl text-purple-500" /> },
{ title: '系统日志', desc: '操作日志和审计记录', icon: <SettingOutlined className="text-xl text-orange-500" /> },
{ title: '问诊数据', desc: '问诊记录、诊断、处方', icon: <FileSearchOutlined style={{ fontSize: 24, color: '#1890ff' }} /> },
{ title: '用户数据', desc: '患者和医生基本信息', icon: <UserOutlined style={{ fontSize: 24, color: '#52c41a' }} /> },
{ title: '处方数据', desc: '处方记录、用药统计', icon: <AuditOutlined style={{ fontSize: 24, color: '#722ed1' }} /> },
{ title: '系统日志', desc: '操作日志和审计记录', icon: <SettingOutlined style={{ fontSize: 24, color: '#fa8c16' }} /> },
].map((item, i) => (
<Col span={6} key={i}>
<Card size="small" hoverable className="text-center" onClick={() => setExportModalVisible(true)}>
<div className="mb-2">{item.icon}</div>
<div className="text-xs font-semibold mb-0.5">{item.title}</div>
<div className="text-[11px] text-gray-400">{item.desc}</div>
<Card style={{ borderRadius: 12, textAlign: 'center', cursor: 'pointer' }} hoverable onClick={() => setExportModalVisible(true)}>
<div style={{ marginBottom: 10 }}>{item.icon}</div>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{item.title}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{item.desc}</div>
</Card>
</Col>
))}
......@@ -182,10 +183,10 @@ const AdminCompliancePage: React.FC = () => {
key: 'reports',
label: <span><CloudUploadOutlined /> 监管上报</span>,
children: (
<Card size="small">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-gray-400">管理合规报告,支持下载和上报</span>
<Button type="primary" size="small">生成新报告</Button>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>管理合规报告,支持下载和上报</span>
<Button type="primary">生成新报告</Button>
</div>
<Table columns={reportColumns} dataSource={reports} rowKey="id" size="small" pagination={false} />
</Card>
......
......@@ -136,19 +136,21 @@ const AdminConsultationsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">问诊管理</h4>
<span className="text-xs text-gray-400">共 {total} 条记录</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>问诊管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看和管理平台所有问诊记录</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {total} 条记录</span>
</div>
<Card size="small">
<div className="flex flex-wrap items-center gap-2">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input
placeholder="搜索患者/医生"
prefix={<SearchOutlined />}
style={{ width: 160 }}
size="small"
style={{ width: 180, borderRadius: 8 }}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onPressEnter={handleSearch}
......@@ -156,7 +158,6 @@ const AdminConsultationsPage: React.FC = () => {
<Select
placeholder="类型"
style={{ width: 100 }}
size="small"
allowClear
value={typeFilter}
onChange={setTypeFilter}
......@@ -167,19 +168,18 @@ const AdminConsultationsPage: React.FC = () => {
/>
<Select
placeholder="状态"
style={{ width: 100 }}
size="small"
style={{ width: 110 }}
allowClear
value={statusFilter}
onChange={setStatusFilter}
options={Object.entries(statusMap).map(([value, { text }]) => ({ label: text, value }))}
/>
<RangePicker size="small" value={dateRange} onChange={(dates) => setDateRange(dates)} />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<RangePicker value={dateRange} onChange={(dates) => setDateRange(dates)} />
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table
columns={columns}
dataSource={data}
......
......@@ -51,58 +51,59 @@ const AdminDashboardPage: React.FC = () => {
}, []);
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">运营大盘</h4>
<span className="text-xs text-gray-400">数据实时更新</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>运营大盘</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>平台实时运营数据概览</div>
</div>
<span style={{ fontSize: 12, color: '#bfbfbf' }}>数据实时更新</span>
</div>
{/* 核心指标 */}
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
{[
{ title: '注册用户', value: stats.total_users, icon: <TeamOutlined />, color: '#1890ff', bg: '#e6f7ff' },
{ title: '注册医生', value: stats.total_doctors, icon: <MedicineBoxOutlined />, color: '#52c41a', bg: '#f6ffed', suffix: <span className="text-[11px] text-gray-400 ml-1">在线 {stats.online_doctors}</span> },
{ title: '注册医生', value: stats.total_doctors, icon: <MedicineBoxOutlined />, color: '#52c41a', bg: '#f6ffed', extra: `在线 ${stats.online_doctors}` },
{ title: '今日问诊', value: stats.today_consultations, icon: <MessageOutlined />, color: '#722ed1', bg: '#f9f0ff' },
{ title: '今日收入', value: stats.revenue_today / 100, icon: <DollarOutlined />, color: '#fa8c16', bg: '#fff7e6', prefix: '¥' },
{ title: '今日收入', value: `¥${(stats.revenue_today / 100).toLocaleString()}`, icon: <DollarOutlined />, color: '#fa8c16', bg: '#fff7e6' },
].map((item, i) => (
<Col xs={12} sm={6} key={i}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa] hover:shadow-md transition-shadow">
<div className="flex items-center gap-2 mb-2">
<div className="w-7 h-7 rounded-md flex items-center justify-center text-sm" style={{ background: item.bg, color: item.color }}>{item.icon}</div>
<span className="text-xs text-gray-500">{item.title}</span>
</div>
<div className="text-xl font-bold" style={{ color: item.color }}>
{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
{item.suffix}
<div style={{ background: '#fff', borderRadius: 12, padding: '16px 20px', border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: item.bg, color: item.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18 }}>{item.icon}</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>{item.title}</span>
</div>
<div style={{ fontSize: 24, fontWeight: 700, color: item.color }}>{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}</div>
{item.extra && <div style={{ fontSize: 12, color: '#bfbfbf', marginTop: 4 }}>{item.extra}</div>}
</div>
</Col>
))}
</Row>
{/* 平台概况 + 待审核*/}
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12}>
<Card title={<span className="text-xs font-semibold">平台概况</span>} size="small" className="h-full!">
<div className="space-y-3">
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>平台概况</span>} style={{ borderRadius: 12, border: '1px solid #edf2fc', height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<div className="flex justify-between mb-1">
<Text className="text-xs!">医生在线</Text>
<Text strong className="text-xs!">{stats.total_doctors > 0 ? ((stats.online_doctors / stats.total_doctors) * 100).toFixed(0) : 0}%</Text>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={{ fontSize: 13 }}>医生在线</Text>
<Text strong style={{ fontSize: 13 }}>{stats.total_doctors > 0 ? ((stats.online_doctors / stats.total_doctors) * 100).toFixed(0) : 0}%</Text>
</div>
<Progress percent={stats.total_doctors > 0 ? Math.round((stats.online_doctors / stats.total_doctors) * 100) : 0} strokeColor="#52c41a" showInfo={false} size="small" />
</div>
<div>
<div className="flex justify-between mb-1">
<Text className="text-xs!">今日完成</Text>
<Text strong className="text-xs!">{stats.today_consultations > 0 ? Math.round((stats.today_consultations / (stats.today_consultations + stats.pending_doctor_reviews)) * 100) : 0}%</Text>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={{ fontSize: 13 }}>今日完成</Text>
<Text strong style={{ fontSize: 13 }}>{stats.today_consultations > 0 ? Math.round((stats.today_consultations / (stats.today_consultations + stats.pending_doctor_reviews)) * 100) : 0}%</Text>
</div>
<Progress percent={stats.today_consultations > 0 ? Math.round((stats.today_consultations / (stats.today_consultations + stats.pending_doctor_reviews)) * 100) : 0} strokeColor="#1890ff" showInfo={false} size="small" />
</div>
<div>
<div className="flex justify-between mb-1">
<Text className="text-xs!">患者满意度</Text>
<Text strong className="text-xs!">96%</Text>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={{ fontSize: 13 }}>患者满意度</Text>
<Text strong style={{ fontSize: 13 }}>96%</Text>
</div>
<Progress percent={96} strokeColor="#722ed1" showInfo={false} size="small" />
</div>
......@@ -112,21 +113,20 @@ const AdminDashboardPage: React.FC = () => {
<Col xs={24} sm={12}>
<Card
title={<span className="text-xs font-semibold">待审核医生 <Tag color="orange" className="ml-1">{stats.pending_doctor_reviews}</Tag></span>}
size="small"
className="h-full!"
title={<span style={{ fontSize: 14, fontWeight: 600 }}>待审核医生 <Tag color="orange" style={{ marginLeft: 6 }}>{stats.pending_doctor_reviews}</Tag></span>}
style={{ borderRadius: 12, border: '1px solid #edf2fc', height: '100%' }}
>
<List
size="small"
dataSource={recentReviews}
locale={{ emptyText: <Text type="secondary" style={{ fontSize: 13 }}>暂无待审核</Text> }}
renderItem={(item) => (
<List.Item className="py-2!">
<List.Item style={{ padding: '10px 0' }}>
<List.Item.Meta
avatar={<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#87d068' }} />}
title={<span className="text-xs"><Text strong>{item.name}</Text> <Tag className="ml-1">{item.title}</Tag></span>}
description={<span className="text-[11px]">{item.department_name} · {item.submitted_at}</span>}
avatar={<Avatar size={32} icon={<UserOutlined />} style={{ backgroundColor: '#87d068' }} />}
title={<span style={{ fontSize: 13 }}><Text strong>{item.name}</Text> <Tag style={{ marginLeft: 4 }}>{item.title}</Tag></span>}
description={<span style={{ fontSize: 12 }}>{item.department_name} · {item.submitted_at}</span>}
/>
<Tag color="orange" className="text-[10px]!">待审</Tag>
<Tag color="orange">待审</Tag>
</List.Item>
)}
/>
......@@ -135,7 +135,7 @@ const AdminDashboardPage: React.FC = () => {
</Row>
{/* 底部统计 */}
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
{[
{ 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()}` },
......@@ -143,10 +143,10 @@ const AdminDashboardPage: React.FC = () => {
{ icon: <AuditOutlined />, color: '#722ed1', bg: '#f9f0ff', label: '待审核', value: stats.pending_doctor_reviews },
].map((item, i) => (
<Col xs={12} sm={6} key={i}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa] text-center">
<div className="w-8 h-8 rounded-lg flex items-center justify-center mx-auto mb-1 text-base" style={{ background: item.bg, color: item.color }}>{item.icon}</div>
<div className="text-[11px] text-gray-400">{item.label}</div>
<div className="text-lg font-bold" style={{ color: item.color }}>{item.value}</div>
<div style={{ background: '#fff', borderRadius: 12, padding: '16px 20px', border: '1px solid #edf2fc', textAlign: 'center' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: item.bg, color: item.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, margin: '0 auto 8px' }}>{item.icon}</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginBottom: 4 }}>{item.label}</div>
<div style={{ fontSize: 20, fontWeight: 700, color: item.color }}>{item.value}</div>
</div>
</Col>
))}
......
......@@ -114,13 +114,16 @@ const AdminDepartmentsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">科室管理</h4>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加科室</Button>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>科室管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理医院科室分类与排序</div>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加科室</Button>
</div>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={departments} rowKey="id" loading={loading} size="small" pagination={false} />
</Card>
......
......@@ -233,29 +233,25 @@ const AdminDoctorsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">医生管理</h4>
<Space>
<span className="text-xs text-gray-400">共 {total} 位医生</span>
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>
添加医生
</Button>
</Space>
<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>
<Card size="small">
<div className="flex flex-wrap items-center gap-2">
<Input placeholder="搜索姓名/手机号" prefix={<SearchOutlined />} style={{ width: 160 }} size="small"
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/手机号" prefix={<SearchOutlined />} style={{ width: 180, borderRadius: 8 }}
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} />
<Select placeholder="认证状态" style={{ width: 120 }} size="small" allowClear value={statusFilter} onChange={setStatusFilter}
<Select placeholder="认证状态" style={{ width: 130 }} allowClear value={statusFilter} onChange={setStatusFilter}
options={Object.entries(statusMap).map(([k, v]) => ({ label: v.text, value: k }))} />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(undefined); setPage(1); }}>重置</Button>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(undefined); setPage(1); }}>重置</Button>
<Button type="primary" icon={<PlusOutlined />} style={{ marginLeft: 'auto' }} onClick={() => setAddModalVisible(true)}>添加医生</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={doctors} rowKey="id" loading={loading} size="small"
pagination={{
current: page, pageSize, total, showSizeChanger: true, size: 'small',
......
......@@ -197,28 +197,31 @@ const AdminPatientsPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">患者管理</h4>
<span className="text-xs text-gray-400">共 {total} 条记录</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>患者管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理平台注册患者信息与账号状态</div>
</div>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {total} 条记录</span>
</div>
<Card size="small">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input placeholder="搜索姓名/手机号" prefix={<SearchOutlined />} value={keyword}
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 180 }} size="small" />
<Select placeholder="状态" allowClear size="small" value={statusFilter || undefined}
onChange={(v) => setStatusFilter(v || '')} style={{ width: 100 }}
onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} style={{ width: 200, borderRadius: 8 }} />
<Select placeholder="状态" allowClear value={statusFilter || undefined}
onChange={(v) => setStatusFilter(v || '')} style={{ width: 110 }}
options={[{ label: '正常', value: 'active' }, { label: '已禁用', value: 'disabled' }]} />
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(''); setPage(1); }}>重置</Button>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button icon={<ReloadOutlined />} onClick={() => { setKeyword(''); setStatusFilter(''); setPage(1); }}>重置</Button>
</div>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加患者</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加患者</Button>
</div>
</Card>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={patients} rowKey="id" loading={loading} size="small"
pagination={{
current: page, pageSize, total, showTotal: (t) => `共 ${t} 条`, size: 'small',
......
......@@ -144,29 +144,30 @@ const AdminPharmacyPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<MedicineBoxOutlined className="mr-1" />药品库管理
</h4>
<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>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<Statistic title="药品总数" value={stats.total_count} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<Statistic title="库存充足" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="库存预警" value={stats.low_stock_count} valueStyle={{ color: '#fa8c16' }} suffix="种" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #f5222d' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #f5222d' }}>
<Statistic title="缺货药品" value={stats.out_of_stock_count} valueStyle={{ color: '#f5222d' }} suffix="种" />
</Card>
</Col>
......@@ -174,29 +175,29 @@ const AdminPharmacyPage: React.FC = () => {
{(stats.low_stock_count > 0 || stats.out_of_stock_count > 0) && (
<Alert message={`当前有 ${stats.low_stock_count} 种药品库存不足,${stats.out_of_stock_count} 种药品缺货,请及时补货`}
type="warning" showIcon className="text-xs!" />
type="warning" showIcon style={{ borderRadius: 8 }} />
)}
<Tabs defaultActiveKey="drugs" size="small" items={[
<Tabs defaultActiveKey="drugs" items={[
{
key: 'drugs',
label: <span><MedicineBoxOutlined /> 药品信息</span>,
children: (
<Card size="small">
<div className="flex flex-wrap items-center justify-between gap-2 mb-2">
<div className="flex flex-wrap items-center gap-2">
<Input prefix={<SearchOutlined />} placeholder="搜索药品" style={{ width: 180 }} size="small" allowClear
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 12 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索药品" style={{ width: 200, borderRadius: 8 }} allowClear
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setPage(1); fetchList(); }} />
<Select placeholder="类别" style={{ width: 100 }} size="small" allowClear options={categoryOptions}
<Select placeholder="类别" style={{ width: 110 }} allowClear options={categoryOptions}
value={filterCategory || undefined} onChange={(v) => { setFilterCategory(v || ''); setPage(1); }} />
<Select placeholder="库存" style={{ width: 100 }} size="small" allowClear
<Select placeholder="库存" style={{ width: 110 }} allowClear
value={filterStatus || undefined} onChange={(v) => { setFilterStatus(v || ''); setPage(1); }}
options={[{ label: '充足', value: 'normal' }, { label: '预警', value: 'low_stock' }, { label: '缺货', value: 'out_of_stock' }]} />
</div>
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={handleAddDrug}>添加药品</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddDrug}>添加药品</Button>
</div>
<Table columns={drugColumns} dataSource={drugs} rowKey="id" loading={loading} size="small"
pagination={{ current: page, total, pageSize: 10, size: 'small', onChange: setPage, showTotal: (t) => `共 ${t} 种` }} />
pagination={{ current: page, total, pageSize: 10, onChange: setPage, showTotal: (t) => `共 ${t} 种` }} />
</Card>
),
},
......@@ -204,10 +205,10 @@ const AdminPharmacyPage: React.FC = () => {
key: 'stock',
label: <Badge count={stats.low_stock_count + stats.out_of_stock_count} offset={[10, 0]}>库存预警</Badge>,
children: (
<Card size="small">
<Table columns={drugColumns} size="small"
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={drugColumns}
dataSource={drugs.filter((d) => d.status !== 'normal')}
rowKey="id" pagination={false} loading={loading} />
rowKey="id" pagination={false} loading={loading} size="small" />
</Card>
),
},
......
......@@ -124,50 +124,48 @@ const AdminPrescriptionPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<FileTextOutlined className="mr-1" />处方监管
</h4>
<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>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<Statistic title="处方总数" value={stats.total_count} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<Statistic title="合规" value={stats.normal_count} valueStyle={{ color: '#52c41a' }} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="预警" value={stats.warning_count} valueStyle={{ color: '#fa8c16' }} suffix="张" />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #f5222d' }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #f5222d' }}>
<Statistic title="异常" value={stats.rejected_count} valueStyle={{ color: '#f5222d' }} suffix="张" />
</Card>
</Col>
</Row>
<Tabs defaultActiveKey="all" size="small" onChange={(key) => {
if (key === 'warning') { setWarningLevel(''); fetchList('warning'); }
else { setWarningLevel(''); fetchList(''); }
}} items={[
<Tabs defaultActiveKey="all" items={[
{
key: 'all', label: '全部处方',
children: (
<Card size="small">
<div className="flex flex-wrap items-center gap-2 mb-2">
<Input prefix={<SearchOutlined />} placeholder="搜索处方/患者/医生" style={{ width: 180 }} size="small" allowClear
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Input prefix={<SearchOutlined />} placeholder="搜索处方/患者/医生" style={{ width: 200, borderRadius: 8 }} allowClear
value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setPage(1); fetchList(); }} />
<RangePicker size="small" />
<Button type="primary" size="small" ghost onClick={() => { setPage(1); fetchList(); }}>查询</Button>
<RangePicker />
<Button type="primary" ghost onClick={() => { setPage(1); fetchList(); }}>查询</Button>
</div>
<Table columns={columns} dataSource={prescriptions} rowKey="id" loading={loading} size="small"
pagination={{ current: page, total, pageSize: 10, size: 'small', onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
pagination={{ current: page, total, pageSize: 10, onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
</Card>
),
},
......@@ -175,13 +173,13 @@ const AdminPrescriptionPage: React.FC = () => {
key: 'warning',
label: <Badge count={stats.warning_count + stats.rejected_count} offset={[10, 0]}>异常预警</Badge>,
children: (
<Card style={{ borderRadius: 12 }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Alert message="以下处方存在合规风险,请及时审查处理" type="warning" showIcon style={{ marginBottom: 16 }} />
<List dataSource={prescriptions.filter((d) => d.warning_level !== 'normal')} renderItem={(item) => (
<Card style={{ marginBottom: 12, borderRadius: 8, borderLeft: `4px solid ${item.warning_level === 'rejected' ? '#f5222d' : '#fa8c16'}` }} hoverable>
<Row align="middle">
<Col flex="auto">
<Space><Text strong>{item.prescription_no}</Text>{getWarningTag(item.warning_level)}</Space><br />
<Space><Text strong>{item.prescription_no}</Text>{getWarningTag(item.warning_level)}</Space><br />
<Text type="secondary">{item.doctor_name} → {item.patient_name} | {item.diagnosis}</Text><br />
{item.warning_reason && <Text type="danger"><ExclamationCircleOutlined style={{ marginRight: 4 }} />{item.warning_reason}</Text>}
</Col>
......
......@@ -88,39 +88,40 @@ const DoctorCertificationPage: React.FC = () => {
if (currentStep === 1) {
return (
<div className="max-w-[500px] mx-auto mt-20 text-center">
<Card size="small">
<div className="py-6">
<div className="w-14 h-14 rounded-full bg-green-50 flex items-center justify-center mx-auto mb-4">
<CheckCircleOutlined className="text-2xl text-green-500" />
</div>
<h3 className="text-base font-bold mb-2">认证申请已提交</h3>
<Paragraph type="secondary" className="text-xs!">
您的资质认证申请已成功提交,管理员将在 1-3 个工作日内完成审核。 </Paragraph>
<Button type="primary" size="small" onClick={() => router.push('/doctor/workbench')}>返回工作台</Button>
<div style={{ padding: '20px 24px' }}>
<Card style={{ maxWidth: 480, margin: '60px auto', textAlign: 'center', borderRadius: 12, border: '1px solid #edf2fc', padding: '40px 20px' }}>
<div style={{ width: 64, height: 64, borderRadius: '50%', background: '#f6ffed', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px', fontSize: 32, color: '#52c41a' }}>
<CheckCircleOutlined />
</div>
<h3 style={{ fontSize: 18, fontWeight: 700, marginBottom: 8, color: '#1d2129' }}>认证申请已提交</h3>
<Paragraph type="secondary" style={{ fontSize: 13 }}>
您的资质认证申请已成功提交,管理员将在 1-3 个工作日内完成审核。
</Paragraph>
<Button type="primary" onClick={() => router.push('/doctor/workbench')} style={{ borderRadius: 8 }}>返回工作台</Button>
</Card>
</div>
);
}
return (
<div className="max-w-[700px] mx-auto pb-6 space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">医生资质认证</h4>
<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>
<Steps
current={currentStep}
size="small"
className="mb-4"
items={[
{ title: '填写资质信息' },
{ title: '等待审核' },
{ title: '审核通过' },
]}
style={{ maxWidth: 600 }}
/>
<Card size="small">
<Form form={form} layout="vertical" size="small" onFinish={handleSubmit}>
<Card style={{ maxWidth: 680, borderRadius: 12, border: '1px solid #edf2fc' }}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="license_no" label="执业证号" rules={[{ required: true, message: '请输入执业证书号' }]}>
<Input placeholder="请输入医师执业证书号" />
</Form.Item>
......@@ -184,3 +185,4 @@ const DoctorCertificationPage: React.FC = () => {
export default DoctorCertificationPage;
......@@ -158,33 +158,33 @@ const ChronicReviewPage: React.FC = () => {
const pendingCount = data.filter((d) => d.status === 'pending').length;
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<MedicineBoxOutlined className="mr-1" />慢病续方审核
</h4>
<Tabs defaultActiveKey="pending" size="small" items={[
{
key: 'pending',
label: <Badge count={pendingCount} offset={[10, 0]}>待审</Badge>,
children: (
<Card size="small">
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>慢病续方审核</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>审核患者的慢病续方申请,AI 已预生成处方草稿</div>
</div>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="pending" items={[
{
key: 'pending',
label: <Badge count={pendingCount} offset={[10, 0]}>待审核</Badge>,
children: (
<Table columns={columns} dataSource={data.filter((d) => d.status === 'pending')}
rowKey="id" size="small" pagination={false} />
</Card>
),
},
{
key: 'all',
label: '全部记录',
children: (
<Card size="small">
<Table columns={columns} dataSource={data} rowKey="id" size="small"
pagination={{ pageSize: 10, size: 'small' }} />
</Card>
),
},
]} />
rowKey="id" pagination={false}
locale={{ emptyText: <div style={{ padding: '40px 0', textAlign: 'center', color: '#bfbfbf' }}>暂无待审核续方</div> }} />
),
},
{
key: 'all',
label: '全部记录',
children: (
<Table columns={columns} dataSource={data} rowKey="id"
pagination={{ pageSize: 10 }} />
),
},
]} />
</Card>
<Modal
title="续方详情审核"
......
......@@ -114,42 +114,46 @@ const IncomePage: React.FC = () => {
return (
<Spin spinning={loading}>
<div className="space-y-3">
<h4 className="text-sm font-bold text-gray-800 m-0">
<DollarOutlined className="mr-1" />
收入统计
</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>收入统计</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您的问诊收入与提现记录</div>
</div>
</div>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={8}>
<div className="rounded-lg p-3 text-white" style={{ background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)' }}>
<div className="text-xs text-white/80 mb-1">可提现余额</div>
<div className="text-2xl font-bold">¥{(stats.total_balance / 100).toFixed(2)}</div>
<Button ghost size="small" className="mt-2 text-white! border-white/60!" icon={<WalletOutlined />}
onClick={() => setWithdrawModalVisible(true)}>申请提现</Button>
<div style={{ borderRadius: 12, padding: '20px 24px', background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: -30, right: -30, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }} />
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.8)', marginBottom: 6 }}>可提现余额</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#fff', marginBottom: 12 }}>¥{(stats.total_balance / 100).toFixed(2)}</div>
<Button ghost icon={<WalletOutlined />} onClick={() => setWithdrawModalVisible(true)}
style={{ borderColor: 'rgba(255,255,255,0.6)', color: '#fff', borderRadius: 8 }}>申请提现</Button>
</div>
</Col>
<Col span={8}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa]">
<div className="text-xs text-gray-500 mb-1">本月收入</div>
<div className="text-xl font-bold text-green-500">¥{(stats.month_income / 100).toFixed(2)}</div>
<div className="mt-1 text-[11px] text-gray-400">
<RiseOutlined className="text-green-500 mr-0.5" />本月累计
<div style={{ background: '#fff', borderRadius: 12, padding: '20px 24px', border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 6 }}>本月收入</div>
<div style={{ fontSize: 26, fontWeight: 700, color: '#52c41a', marginBottom: 6 }}>¥{(stats.month_income / 100).toFixed(2)}</div>
<div style={{ fontSize: 12, color: '#bfbfbf', display: 'flex', alignItems: 'center', gap: 4 }}>
<RiseOutlined style={{ color: '#52c41a' }} />本月累计
</div>
</div>
</Col>
<Col span={8}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa]">
<div className="text-xs text-gray-500 mb-1">本月问诊量</div>
<div className="text-xl font-bold text-[#1890ff]">{stats.month_consults} <span className="text-xs font-normal text-gray-400">次</span></div>
<div className="mt-1 text-[11px] text-gray-400">日均 {(stats.month_consults / Math.max(1, new Date().getDate())).toFixed(1)} 次</div>
<div style={{ background: '#fff', borderRadius: 12, padding: '20px 24px', border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 6 }}>本月问诊量</div>
<div style={{ fontSize: 26, fontWeight: 700, color: '#1890ff', marginBottom: 6 }}>
{stats.month_consults} <span style={{ fontSize: 14, color: '#bfbfbf', fontWeight: 400 }}>次</span>
</div>
<div style={{ fontSize: 12, color: '#bfbfbf' }}>日均 {(stats.month_consults / Math.max(1, new Date().getDate())).toFixed(1)} 次</div>
</div>
</Col>
</Row>
<Tabs
defaultActiveKey="records"
size="small"
items={[
{
key: 'records',
......@@ -282,6 +286,7 @@ const IncomePage: React.FC = () => {
</div>
</Spin>
);
};
export default IncomePage;
......@@ -139,42 +139,57 @@ const PatientRecordPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<UserOutlined className="mr-1" />患者档案
</h4>
<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>
<Row gutter={8}>
<Row gutter={16} align="stretch">
<Col span={8}>
<Card title={<span className="text-xs font-semibold">患者列表</span>} size="small"
extra={<Text type="secondary" className="text-[11px]!">{total} 人</Text>}>
<Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机" size="small"
<Card
title={<span style={{ fontSize: 14, fontWeight: 600 }}>患者列表</span>}
extra={<Text type="secondary" style={{ fontSize: 13 }}>{total} 人</Text>}
style={{ borderRadius: 12, border: '1px solid #edf2fc', height: '100%' }}
>
<Input prefix={<SearchOutlined />} placeholder="搜索姓名/手机"
value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={(e) => handleSearch((e.target as HTMLInputElement).value)}
onBlur={(e) => handleSearch(e.target.value)} allowClear onClear={() => handleSearch('')}
className="mb-2" />
style={{ marginBottom: 12, borderRadius: 8 }} />
<Spin spinning={listLoading}>
<List size="small" dataSource={patients}
<List dataSource={patients}
locale={{ emptyText: <Empty description="暂无患者" /> }}
pagination={total > 20 ? { current: page, pageSize: 20, total, size: 'small',
onChange: (p) => { setPage(p); fetchPatients(searchKeyword, p); } } : false}
renderItem={(patient) => (
<List.Item style={{ cursor: 'pointer', padding: '6px 8px', borderRadius: 6,
background: selectedPatient?.id === patient.id ? '#f6ffed' : 'transparent',
border: selectedPatient?.id === patient.id ? '1px solid #b7eb8f' : '1px solid transparent',
marginBottom: 2 }}
onClick={() => handleSelectPatient(patient.id)}>
<List.Item
style={{
cursor: 'pointer', padding: '10px 12px', borderRadius: 8,
background: selectedPatient?.id === patient.id ? '#f6ffed' : 'transparent',
border: `1px solid ${selectedPatient?.id === patient.id ? '#b7eb8f' : 'transparent'}`,
marginBottom: 4, transition: 'all 0.15s',
}}
onClick={() => handleSelectPatient(patient.id)}
>
<List.Item.Meta
avatar={patient.avatar ? <Avatar src={patient.avatar} size={32} /> :
<div className="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center text-white font-bold text-xs">{patient.name.charAt(0)}</div>}
title={<span className="text-xs"><Text strong>{patient.name}</Text> <Text type="secondary">{genderLabel(patient.gender)}/{patient.age}岁</Text></span>}
avatar={patient.avatar
? <Avatar src={patient.avatar} size={36} />
: <Avatar size={36} style={{ backgroundColor: '#52c41a', fontWeight: 700 }}>{patient.name.charAt(0)}</Avatar>
}
title={
<div style={{ fontSize: 13 }}>
<Text strong>{patient.name}</Text>
<Text type="secondary" style={{ marginLeft: 6 }}>{genderLabel(patient.gender)}/{patient.age}岁</Text>
</div>
}
description={
<div className="text-[11px]">
<div className="text-gray-400"><PhoneOutlined className="mr-0.5" />{patient.phone}</div>
<div style={{ fontSize: 12 }}>
<div style={{ color: '#8c8c8c' }}><PhoneOutlined style={{ marginRight: 4 }} />{patient.phone}</div>
{patient.allergy_history && patient.allergy_history !== '无' && (
<Tag color="red" className="text-[10px]!"><SafetyOutlined /> {patient.allergy_history}</Tag>
<Tag color="red" style={{ fontSize: 11, marginTop: 2 }}><SafetyOutlined /> {patient.allergy_history}</Tag>
)}
<div className="text-gray-400">{patient.consultation_count}次问诊{patient.last_consult_at && ` · ${patient.last_consult_at}`}</div>
<div style={{ color: '#bfbfbf', marginTop: 2 }}>{patient.consultation_count}次问诊{patient.last_consult_at && ` · ${patient.last_consult_at}`}</div>
</div>
}
/>
......@@ -188,9 +203,9 @@ const PatientRecordPage: React.FC = () => {
<Col span={16}>
<Spin spinning={detailLoading}>
{selectedPatient ? (
<div className="space-y-2">
<Card size="small">
<Descriptions title={<span className="text-xs font-semibold">基本信息</span>} bordered size="small" column={3}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Descriptions title={<span style={{ fontSize: 14, fontWeight: 600 }}>基本信息</span>} bordered size="small" column={3}>
<Descriptions.Item label="姓名">{selectedPatient.name}</Descriptions.Item>
<Descriptions.Item label="性别">{genderLabel(selectedPatient.gender)}</Descriptions.Item>
<Descriptions.Item label="年龄">{selectedPatient.age}岁</Descriptions.Item>
......@@ -206,26 +221,26 @@ const PatientRecordPage: React.FC = () => {
</Descriptions>
</Card>
<Card size="small">
<Tabs defaultActiveKey="consult" size="small" items={[
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="consult" items={[
{
key: 'consult',
label: <span><FileTextOutlined /> 问诊({selectedPatient.consults.length})</span>,
children: <Table<PatientConsultRecord> dataSource={selectedPatient.consults} rowKey="id" size="small" pagination={false} columns={consultColumns} locale={{ emptyText: '暂无' }} />,
label: <span><FileTextOutlined /> 问诊记录({selectedPatient.consults.length})</span>,
children: <Table<PatientConsultRecord> dataSource={selectedPatient.consults} rowKey="id" pagination={false} columns={consultColumns} locale={{ emptyText: '暂无问诊记录' }} />,
},
{
key: 'prescription',
label: <span><MedicineBoxOutlined /> 处方({selectedPatient.prescriptions.length})</span>,
children: <Table<PatientPrescriptionRecord> dataSource={selectedPatient.prescriptions} rowKey="id" size="small" pagination={false} columns={prescriptionColumns} locale={{ emptyText: '暂无' }} />,
label: <span><MedicineBoxOutlined /> 处方记录({selectedPatient.prescriptions.length})</span>,
children: <Table<PatientPrescriptionRecord> dataSource={selectedPatient.prescriptions} rowKey="id" pagination={false} columns={prescriptionColumns} locale={{ emptyText: '暂无处方记录' }} />,
},
{
key: 'health',
label: <span><LineChartOutlined /> 趋势</span>,
label: <span><LineChartOutlined /> 健康趋势</span>,
children: (
<div className="text-center py-6">
<HeartOutlined className="text-3xl text-red-400 mb-2" />
<div className="text-xs font-semibold mb-1">健康趋势图表</div>
<Text type="secondary" className="text-xs!">功能开发中...</Text>
<div style={{ textAlign: 'center', padding: '48px 0' }}>
<HeartOutlined style={{ fontSize: 40, color: '#ffadd2', marginBottom: 12 }} />
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 6, color: '#1d2129' }}>健康趋势图表</div>
<Text type="secondary">功能开发中...</Text>
</div>
),
},
......@@ -233,7 +248,7 @@ const PatientRecordPage: React.FC = () => {
</Card>
</div>
) : (
<Card size="small" className="text-center py-10">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', textAlign: 'center', padding: '60px 0' }}>
<Empty description="请从左侧选择患者查看档案" />
</Card>
)}
......
'use client';
'use client';
import React, { useState, useEffect } from 'react';
import { Card, Avatar, Button, Descriptions, Tag, Typography, Row, Col, Divider, Switch, Space, Spin, message } from 'antd';
import {
......@@ -63,70 +63,83 @@ const DoctorProfilePage: React.FC = () => {
if (!profile) return null;
return (
<div className="max-w-[800px] mx-auto space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">个人信息</h4>
<Card size="small">
<Row gutter={12} align="middle">
<Col>
<Avatar size={64} src={profile.avatar} icon={<UserOutlined />} />
</Col>
<Col flex="auto">
<div className="flex items-center gap-2 mb-0.5">
<span className="text-base font-bold">{profile.name}</span>
<Tag color="blue">{profile.title}</Tag>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Hero 卡片 */}
<div style={{
borderRadius: 12, overflow: 'hidden',
background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
padding: '28px 32px', position: 'relative',
}}>
<div style={{ position: 'absolute', top: -40, right: -40, width: 160, height: 160, borderRadius: '50%', background: 'rgba(255,255,255,0.06)' }} />
<div style={{ position: 'relative', zIndex: 1, display: 'flex', alignItems: 'center', gap: 20 }}>
<Avatar size={72} src={profile.avatar || null} icon={<UserOutlined />}
style={{ backgroundColor: 'rgba(255,255,255,0.2)', color: '#fff', fontSize: 32, flexShrink: 0 }} />
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<h2 style={{ fontSize: 20, fontWeight: 700, margin: 0, color: '#fff' }}>{profile.name}</h2>
<Tag color="blue" style={{ borderRadius: 20 }}>{profile.title}</Tag>
</div>
<div className="text-xs text-gray-500 mb-1">{profile.hospital} · {profile.department_name}</div>
<Space size={4}>
<Tag icon={<SafetyCertificateOutlined />} color="success">已认证</Tag>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.8)', marginBottom: 10 }}>
{profile.hospital} · {profile.department_name}
</div>
<Space size={6}>
<Tag icon={<SafetyCertificateOutlined />} color="success" style={{ borderRadius: 20 }}>已认证</Tag>
</Space>
</Col>
<Col>
<Button type="primary" size="small" icon={<EditOutlined />}>编辑资料</Button>
</Col>
</Row>
</Card>
</div>
<Button icon={<EditOutlined />} style={{ borderRadius: 8, background: 'rgba(255,255,255,0.15)', border: 'none', color: '#fff' }}>
编辑资料
</Button>
</div>
</div>
<Card title={<span className="text-xs font-semibold">详细信息</span>} size="small">
<Descriptions column={2} size="small">
<Descriptions.Item label="姓名">{profile.name}</Descriptions.Item>
<Descriptions.Item label="职称">{profile.title}</Descriptions.Item>
<Descriptions.Item label="医院">{profile.hospital}</Descriptions.Item>
<Descriptions.Item label="科室">{profile.department_name}</Descriptions.Item>
<Descriptions.Item label="价格">¥{(profile.price / 100).toFixed(0)}/次</Descriptions.Item>
</Descriptions>
</Card>
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>详细信息</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #edf2fc' }}>
<Descriptions column={2} size="small">
<Descriptions.Item label="姓名">{profile.name}</Descriptions.Item>
<Descriptions.Item label="职称">{profile.title}</Descriptions.Item>
<Descriptions.Item label="医院">{profile.hospital}</Descriptions.Item>
<Descriptions.Item label="科室">{profile.department_name}</Descriptions.Item>
<Descriptions.Item label="问诊价格">
<span style={{ fontSize: 16, fontWeight: 700, color: '#52c41a' }}>¥{(profile.price / 100).toFixed(0)}</span>
<span style={{ fontSize: 12, color: '#8c8c8c' }}>/次</span>
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col xs={24} md={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>接诊设置</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #edf2fc' }}>
<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 #edf2fc' }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>AI 辅助接诊</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后 AI 将协助分析患者症状</div>
</div>
<Switch checked={profile.ai_assist_enabled} onChange={handleToggleAI} />
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#f8faff', borderRadius: 10, border: '1px solid #edf2fc' }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>自动接诊</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginTop: 2 }}>开启后图文问诊将自动接诊</div>
</div>
<Switch />
</div>
</div>
</Card>
</Col>
</Row>
<Card title={<span className="text-xs font-semibold">擅长领域</span>} size="small">
<Space wrap size={4}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>擅长领域</span>} style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Space wrap size={8}>
{profile.specialties.map((s, i) => (
<Tag key={i} color="processing">{s}</Tag>
<Tag key={i} color="processing" style={{ borderRadius: 20, padding: '4px 12px', fontSize: 13 }}>{s}</Tag>
))}
</Space>
</Card>
<Card title={<span className="text-xs font-semibold">个人简介</span>} size="small">
<Text className="text-xs!">{profile.introduction}</Text>
</Card>
<Card title={<span className="text-xs font-semibold">接诊设置</span>} size="small">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Text strong className="text-xs!">AI 辅助接诊</Text>
<div className="text-[11px] text-gray-400">开启后 AI 将协助分析患者症状</div>
</div>
<Switch size="small" checked={profile.ai_assist_enabled} onChange={handleToggleAI} />
</div>
<Divider className="my-0!" />
<div className="flex items-center justify-between">
<div>
<Text strong className="text-xs!">自动接诊</Text>
<div className="text-[11px] text-gray-400">开启后图文问诊将自动接诊</div>
</div>
<Switch size="small" />
</div>
</div>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>个人简介</span>} style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Text style={{ fontSize: 14, color: '#595959', lineHeight: 1.7 }}>{profile.introduction}</Text>
</Card>
</div>
);
......
'use client';
'use client';
import React, { useState, useEffect } from 'react';
import { Card, Calendar, Button, Modal, Form, TimePicker, InputNumber, Tag, Typography, Space, Row, Col, Badge, List, message, Spin } from 'antd';
import { PlusOutlined, DeleteOutlined, CalendarOutlined } from '@ant-design/icons';
......@@ -94,16 +94,19 @@ const DoctorSchedulePage: React.FC = () => {
};
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">排班管理</h4>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加排班</Button>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>排班管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>管理您的接诊时间段安排</div>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddSchedule} style={{ borderRadius: 8 }}>添加排班</Button>
</div>
<Spin spinning={loading}>
<Row gutter={12}>
<Row gutter={16}>
<Col span={16}>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Calendar
fullscreen={false}
value={selectedDate}
......@@ -115,29 +118,34 @@ const DoctorSchedulePage: React.FC = () => {
<Col span={8}>
<Card
title={<span className="text-xs font-semibold"><CalendarOutlined className="mr-1" />{selectedDate.format('MM月DD日')} 排班</span>}
extra={<Button type="link" size="small" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加</Button>}
size="small"
title={
<span style={{ fontSize: 14, fontWeight: 600 }}>
<CalendarOutlined style={{ marginRight: 6, color: '#1890ff' }} />
{selectedDate.format('MM月DD日')} 排班
</span>
}
extra={<Button type="link" icon={<PlusOutlined />} onClick={handleAddSchedule}>添加</Button>}
style={{ borderRadius: 12, border: '1px solid #edf2fc' }}
>
{todaySchedule.length === 0 ? (
<div className="text-center py-6">
<Text type="secondary" className="text-xs!">当日暂无排班</Text>
<div style={{ textAlign: 'center', padding: '32px 0' }}>
<Text type="secondary" style={{ fontSize: 13 }}>当日暂无排班</Text>
<br />
<Button type="link" size="small" icon={<PlusOutlined />} onClick={handleAddSchedule} className="mt-1">添加排班</Button>
<Button type="link" icon={<PlusOutlined />} onClick={handleAddSchedule} style={{ marginTop: 8 }}>添加排班</Button>
</div>
) : (
<List
size="small"
dataSource={todaySchedule}
renderItem={(slot) => (
<List.Item
style={{ padding: '12px 0' }}
actions={[
<Button type="text" danger size="small" icon={<DeleteOutlined />} onClick={() => handleDeleteSlot(slot.id!)} />,
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDeleteSlot(slot.id!)} />,
]}
>
<div>
<div className="mb-1">
<Text strong className="text-xs!">{slot.start_time} - {slot.end_time}</Text>
<div style={{ marginBottom: 6 }}>
<Text strong style={{ fontSize: 14 }}>{slot.start_time} - {slot.end_time}</Text>
</div>
<Space size={4}>
<Tag color="blue">最大 {slot.max_count} 人</Tag>
......
......@@ -74,15 +74,17 @@ const DoctorWorkbenchPage: React.FC = () => {
if (!isVerified) {
return (
<div className="space-y-3">
<h4 className="text-sm font-bold text-gray-800 m-0">医生工作台</h4>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生工作台</h2>
</div>
<Alert
message="请先完成资质认证"
description={
<div className="text-xs">
<p className="mb-1">您的账号尚未完成医生资质认证,无法接诊患者</p>
<p className="mb-2">请提交您的执业证、资格证等相关资料,审核通过后即可开始接诊量</p>
<Button type="primary" size="small" icon={<ExclamationCircleOutlined />}
<div>
<p style={{ margin: '0 0 4px' }}>您的账号尚未完成医生资质认证,无法接诊患者</p>
<p style={{ margin: '0 0 12px' }}>请提交您的执业证、资格证等相关资料,审核通过后即可开始接诊</p>
<Button type="primary" icon={<ExclamationCircleOutlined />}
onClick={() => router.push('/doctor/certification')}>
立即认证
</Button>
......@@ -96,14 +98,19 @@ const DoctorWorkbenchPage: React.FC = () => {
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">医生工作台</h4>
<span className="text-xs text-gray-400">每10秒自动刷新</span>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>医生工作台</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>每10秒自动刷新数据</div>
</div>
<Button type="primary" style={{ borderRadius: 8 }} onClick={() => router.push('/doctor/consult')}>
进入问诊大厅
</Button>
</div>
{/* 核心指标 */}
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
{[
{ title: '今日问诊', value: stats.today_consult_count, icon: <MessageOutlined />, color: '#1890ff', bg: '#e6f7ff', suffix: '人次' },
{ title: '等待接诊', value: stats.waiting_count, icon: <ClockCircleOutlined />, color: '#fa8c16', bg: '#fff7e6', suffix: '人' },
......@@ -111,13 +118,16 @@ const DoctorWorkbenchPage: React.FC = () => {
{ title: '今日收入', value: stats.income_today, icon: <DollarOutlined />, color: '#722ed1', bg: '#f9f0ff', suffix: '元' },
].map((item, i) => (
<Col xs={12} sm={6} key={i}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa] hover:shadow-md transition-shadow">
<div className="flex items-center gap-2 mb-2">
<div className="w-7 h-7 rounded-md flex items-center justify-center text-sm" style={{ background: item.bg, color: item.color }}>{item.icon}</div>
<span className="text-xs text-gray-500">{item.title}</span>
<div style={{
background: '#fff', borderRadius: 12, padding: '16px 20px',
border: '1px solid #edf2fc', transition: 'box-shadow 0.2s',
}}>
<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>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>{item.title}</span>
</div>
<div className="text-xl font-bold" style={{ color: item.color }}>
{item.value} <span className="text-[11px] text-gray-400 font-normal">{item.suffix}</span>
<div style={{ fontSize: 26, fontWeight: 700, color: item.color, lineHeight: 1 }}>
{item.value} <span style={{ fontSize: 13, color: '#bfbfbf', fontWeight: 400 }}>{item.suffix}</span>
</div>
</div>
</Col>
......@@ -125,18 +135,21 @@ const DoctorWorkbenchPage: React.FC = () => {
</Row>
{/* 综合数据 */}
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
{[
{ icon: <TeamOutlined />, color: '#1890ff', bg: '#e6f7ff', label: '累计患者', value: stats.total_patients },
{ icon: <StarOutlined />, color: '#fadb14', bg: '#fffbe6', label: '综合评分', value: stats.rating },
{ icon: <StarOutlined />, color: '#faad14', bg: '#fffbe6', label: '综合评分', value: stats.rating },
{ icon: <RiseOutlined />, color: '#52c41a', bg: '#f6ffed', label: '本月收入', value: `¥${stats.income_month}` },
{ icon: <MessageOutlined />, color: '#13c2c2', bg: '#e6fffb', label: '进行中', value: stats.in_progress_count },
].map((item, i) => (
<Col xs={12} sm={6} key={i}>
<div className="bg-white rounded-lg p-3 border border-[#e6f0fa] text-center">
<div className="w-8 h-8 rounded-lg flex items-center justify-center mx-auto mb-1 text-base" style={{ background: item.bg, color: item.color }}>{item.icon}</div>
<div className="text-[11px] text-gray-400">{item.label}</div>
<div className="text-lg font-bold" style={{ color: item.color }}>{item.value}</div>
<div style={{
background: '#fff', borderRadius: 12, padding: '16px 20px',
border: '1px solid #edf2fc', textAlign: 'center',
}}>
<div style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 8px', fontSize: 20, background: item.bg, color: item.color }}>{item.icon}</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginBottom: 4 }}>{item.label}</div>
<div style={{ fontSize: 20, fontWeight: 700, color: item.color }}>{item.value}</div>
</div>
</Col>
))}
......@@ -144,36 +157,37 @@ const DoctorWorkbenchPage: React.FC = () => {
{/* 等待接诊列表 */}
<Card
title={<span className="text-xs font-semibold">等待接诊 <Badge count={stats.waiting_count} style={{ backgroundColor: '#fa8c16' }} /></span>}
extra={<Button type="link" size="small" onClick={() => router.push('/doctor/consult')}>查看全部</Button>}
size="small"
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>}
loading={loading}
style={{ borderRadius: 12, border: '1px solid #edf2fc' }}
>
<List
size="small"
dataSource={waitingPatients}
locale={{ emptyText: '暂无等待接诊的患者' }}
locale={{ emptyText: <div style={{ textAlign: 'center', padding: '24px 0', color: '#bfbfbf' }}>暂无等待接诊的患者</div> }}
renderItem={(patient) => (
<List.Item
style={{ padding: '12px 0', borderBottom: '1px solid #f5f5f5' }}
actions={[
<Button size="small" type="primary" onClick={() => router.push('/doctor/consult')}>接诊</Button>,
<Button type="primary" onClick={() => router.push('/doctor/consult')} style={{ borderRadius: 8 }}>接诊</Button>,
]}
>
<List.Item.Meta
avatar={<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: '#87d068' }} />}
avatar={<Avatar size={40} icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />}
title={
<span className="text-xs">
<Text strong>{patient.patient_name}</Text>
<Tag className="ml-1"
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2 }}>
<Text strong style={{ fontSize: 14 }}>{patient.patient_name}</Text>
<Tag
icon={patient.type === 'video' ? <VideoCameraOutlined /> : <MessageOutlined />}
color={patient.type === 'video' ? 'blue' : 'green'}
style={{ borderRadius: 4 }}
>
{patient.type === 'video' ? '视频' : '图文'}
{patient.type === 'video' ? '视频问诊' : '图文问诊'}
</Tag>
<span className="text-gray-400 text-[11px] ml-1">等待 {formatWaitingTime(patient.waiting_seconds)}</span>
</span>
<span style={{ fontSize: 12, color: '#bfbfbf' }}>等待 {formatWaitingTime(patient.waiting_seconds)}</span>
</div>
}
description={<span className="text-[11px]">主诉:{patient.chief_complaint || '暂无'}</span>}
description={<span style={{ fontSize: 13, color: '#8c8c8c' }}>主诉:{patient.chief_complaint || '暂无'}</span>}
/>
</List.Item>
)}
......
......@@ -519,12 +519,13 @@ const PatientChronicPage: React.FC = () => {
];
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<MedicineBoxOutlined className="mr-1" />慢病管理
</h4>
<Card size="small">
<Tabs items={tabItems} size="small" />
<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>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs items={tabItems} />
</Card>
</div>
);
......
......@@ -103,20 +103,23 @@ const PatientConsultListPage: React.FC = () => {
};
return (
<div className="max-w-[1200px] mx-auto space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">
<MedicineBoxOutlined className="mr-1 text-[#1890ff]" />
我的问诊
</h4>
<Button size="small" type="primary" onClick={() => router.push('/patient/doctors')}>发起问诊</Button>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 页面标题 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>我的问诊</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看和管理您的全部问诊记录</div>
</div>
<Button type="primary" icon={<MedicineBoxOutlined />} onClick={() => router.push('/patient/doctors')}
style={{ borderRadius: 8 }}>
发起问诊
</Button>
</div>
<Card size="small">
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
size="small"
items={[
{ key: 'all', label: '全部' },
{ key: 'in_progress', label: '进行中' },
......@@ -128,38 +131,45 @@ const PatientConsultListPage: React.FC = () => {
<Spin spinning={loading}>
<List
size="small"
dataSource={consultList}
locale={{ emptyText: <Empty description="暂无问诊记录" /> }}
locale={{ emptyText: <Empty description="暂无问诊记录" style={{ padding: '40px 0' }} /> }}
renderItem={(consult: Consultation) => {
const status = statusMap[consult.status] || { text: consult.status, color: 'default' };
return (
<List.Item actions={renderActions(consult)} className="py-2!">
<List.Item
actions={renderActions(consult)}
style={{ padding: '16px 0', borderBottom: '1px solid #f5f5f5' }}
>
<List.Item.Meta
avatar={
<Avatar
src={consult.doctor_avatar}
size={36}
style={{ backgroundColor: '#1890ff' }}
src={consult.doctor_avatar || null}
size={48}
style={{ backgroundColor: '#e6f7ff', color: '#1890ff' }}
icon={<MedicineBoxOutlined />}
/>
}
title={
<span className="text-xs">
<Text strong>{consult.doctor_name}</Text>
<Tag className="ml-1"
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 15, fontWeight: 600, color: '#1d2129' }}>{consult.doctor_name}</span>
<Tag
icon={consult.type === 'video' ? <VideoCameraOutlined /> : <MessageOutlined />}
color={consult.type === 'video' ? 'blue' : 'cyan'}
style={{ borderRadius: 4 }}
>
{consult.type === 'video' ? '视频' : '图文'}
{consult.type === 'video' ? '视频问诊' : '图文问诊'}
</Tag>
<Tag icon={statusIcon[consult.status]} color={status.color} style={{ borderRadius: 4 }}>
{status.text}
</Tag>
<Tag icon={statusIcon[consult.status]} color={status.color}>{status.text}</Tag>
</span>
</div>
}
description={
<div className="mt-0.5">
<div className="text-xs text-gray-600 mb-0.5">主诉:{consult.chief_complaint || '无'}</div>
<span className="text-[11px] text-gray-400">
<div>
<div style={{ fontSize: 13, color: '#595959', marginBottom: 4 }}>
主诉:{consult.chief_complaint || '无'}
</div>
<span style={{ fontSize: 12, color: '#bfbfbf' }}>
{dayjs(consult.created_at).format('YYYY-MM-DD HH:mm')}
{consult.started_at && ` · 接诊 ${dayjs(consult.started_at).format('HH:mm')}`}
{consult.ended_at && ` · 结束 ${dayjs(consult.ended_at).format('HH:mm')}`}
......
......@@ -2,13 +2,22 @@
import React, { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Card, Row, Col, Input, Select, List, Avatar, Tag, Rate, Button, Space, Typography } from 'antd';
import { SearchOutlined, VideoCameraOutlined, MessageOutlined } from '@ant-design/icons';
import {
Input, Select, Avatar, Tag, Rate, Button, Spin, Empty, Badge,
} from 'antd';
import {
SearchOutlined, VideoCameraOutlined, MessageOutlined,
StarFilled, FireFilled, UserOutlined, RightOutlined,
} from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import { doctorApi, type Doctor } from '../../../api/doctor';
import ConsultCreateModal from '../../../components/ConsultCreateModal';
const { Text } = Typography;
const SORT_OPTIONS = [
{ label: '评分最高', value: 'rating' },
{ label: '问诊最多', value: 'consult_count' },
{ label: '价格最低', value: 'price' },
];
const PatientDoctorsPage: React.FC = () => {
const router = useRouter();
......@@ -26,14 +35,7 @@ const PatientDoctorsPage: React.FC = () => {
const { data: doctorData, isLoading } = useQuery({
queryKey: ['doctors', keyword, departmentId, sortBy],
queryFn: () =>
doctorApi.getDoctorList({
keyword,
department_id: departmentId,
sort_by: sortBy,
page: 1,
page_size: 20,
}),
queryFn: () => doctorApi.getDoctorList({ keyword, department_id: departmentId, sort_by: sortBy, page: 1, page_size: 20 }),
});
const handleConsult = (doctor: Doctor, type: 'video' | 'text') => {
......@@ -41,91 +43,118 @@ const PatientDoctorsPage: React.FC = () => {
setConsultModalOpen(true);
};
const deptList = departments?.data || [];
const doctors = doctorData?.data?.list || [];
return (
<div className="max-w-[1200px] mx-auto space-y-2">
<Card size="small">
<div className="flex flex-wrap items-center gap-2">
<div style={{ display: 'flex', minHeight: 'calc(100vh - 56px)' }}>
{/* 左侧科室筛选栏 */}
<aside style={{
width: 200, flexShrink: 0,
background: '#fff',
borderRight: '1px solid #edf2fc',
padding: '20px 0',
}}>
<div style={{ padding: '0 16px 12px', fontSize: 12, fontWeight: 600, color: '#8c8c8c', letterSpacing: 1 }}>
科室分类
</div>
{/* 全部 */}
<div
onClick={() => setDepartmentId('')}
style={{
padding: '9px 20px', fontSize: 14, cursor: 'pointer',
fontWeight: !departmentId ? 600 : 400,
color: !departmentId ? '#1890ff' : '#1d2129',
background: !departmentId ? '#e6f7ff' : 'transparent',
borderRight: !departmentId ? '3px solid #1890ff' : '3px solid transparent',
transition: 'all 0.15s',
}}
>
全部科室
</div>
{deptList.map((d: { id: string; name: string }) => {
const active = departmentId === d.id;
return (
<div
key={d.id}
onClick={() => setDepartmentId(d.id)}
style={{
padding: '9px 20px', fontSize: 14, cursor: 'pointer',
fontWeight: active ? 600 : 400,
color: active ? '#1890ff' : '#4e5969',
background: active ? '#e6f7ff' : 'transparent',
borderRight: active ? '3px solid #1890ff' : '3px solid transparent',
transition: 'all 0.15s',
}}
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = '#f5f8ff'; }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}
>
{d.name}
</div>
);
})}
</aside>
{/* 右侧主内容 */}
<main style={{ flex: 1, padding: '20px 24px', minWidth: 0 }}>
{/* 搜索 + 排序栏 */}
<div style={{
display: 'flex', alignItems: 'center', gap: 12,
marginBottom: 20, flexWrap: 'wrap',
}}>
<Input
placeholder="搜索医生姓名"
prefix={<SearchOutlined />}
placeholder="搜索医生姓名、擅长方向..."
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{ width: 180 }}
size="small"
/>
<Select
placeholder="选择科室"
style={{ width: 140 }}
size="small"
allowClear
value={departmentId || undefined}
onChange={setDepartmentId}
options={departments?.data?.map((d) => ({ label: d.name, value: d.id })) || []}
/>
<Select
value={sortBy}
onChange={setSortBy}
style={{ width: 110 }}
size="small"
options={[
{ label: '评分最高', value: 'rating' },
{ label: '问诊最多', value: 'consult_count' },
{ label: '价格最低', value: 'price' },
]}
style={{ width: 280, borderRadius: 8 }}
/>
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, color: '#8c8c8c' }}>排序:</span>
{SORT_OPTIONS.map((opt) => (
<Button
key={opt.value}
size="small"
type={sortBy === opt.value ? 'primary' : 'default'}
onClick={() => setSortBy(opt.value as typeof sortBy)}
style={{ borderRadius: 20, fontSize: 13 }}
>
{opt.label}
</Button>
))}
</div>
</div>
</Card>
<List
loading={isLoading}
dataSource={doctorData?.data?.list || []}
renderItem={(doctor: Doctor) => (
<div className="bg-white rounded-lg border border-[#e6f0fa] p-3 mb-2 hover:shadow-md transition-shadow cursor-pointer">
<Row gutter={12} align="middle">
<Col flex="56px">
<Avatar size={48} src={doctor.avatar} />
</Col>
<Col flex="auto">
<div className="mb-1">
<Text strong className="text-sm! mr-1">{doctor.name}</Text>
<Tag color="blue">{doctor.title}</Tag>
<Tag>{doctor.department_name}</Tag>
{doctor.is_online && <Tag color="green">在线</Tag>}
</div>
<div className="text-xs text-gray-400 mb-1">{doctor.hospital}</div>
<div className="flex items-center gap-2 mb-1">
<Rate disabled defaultValue={doctor.rating} className="text-xs!" />
<span className="text-[11px] text-gray-400">问诊 {doctor.consult_count} 次</span>
</div>
<div className="text-xs text-gray-500 line-clamp-1">
擅长:{doctor.specialties?.join('、') || doctor.introduction}
</div>
</Col>
<Col>
<div className="text-right">
<div className="mb-2">
<span className="text-base font-bold text-red-500">¥{(doctor.price / 100).toFixed(0)}</span>
<span className="text-[11px] text-gray-400">/次</span>
</div>
<Space size={4}>
<Button size="small" icon={<MessageOutlined />} onClick={() => handleConsult(doctor, 'text')}>
图文
</Button>
<Button size="small" type="primary" icon={<VideoCameraOutlined />} onClick={() => handleConsult(doctor, 'video')}>
视频
</Button>
</Space>
<div>
<Button type="link" size="small" className="text-xs!" onClick={() => router.push(`/patient/doctors/${doctor.id}`)}>
查看详情 &gt;
</Button>
</div>
</div>
</Col>
</Row>
{/* 结果数量 */}
{!isLoading && (
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 16 }}>
共找到 <span style={{ color: '#1890ff', fontWeight: 600 }}>{doctors.length}</span> 位医生
</div>
)}
{/* 医生列表 */}
{isLoading ? (
<div style={{ textAlign: 'center', padding: '80px 0' }}>
<Spin size="large" />
</div>
) : doctors.length === 0 ? (
<Empty description="暂无符合条件的医生" style={{ padding: '80px 0' }} />
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(420px, 1fr))', gap: 16 }}>
{doctors.map((doctor: Doctor) => (
<DoctorCard
key={doctor.id}
doctor={doctor}
onConsult={handleConsult}
onDetail={() => router.push(`/patient/doctors/${doctor.id}`)}
/>
))}
</div>
)}
/>
</main>
{selectedDoctor && (
<ConsultCreateModal
......@@ -139,4 +168,130 @@ const PatientDoctorsPage: React.FC = () => {
);
};
/* ---- 医生卡片组件 ---- */
const DoctorCard: React.FC<{
doctor: Doctor;
onConsult: (doctor: Doctor, type: 'text' | 'video') => void;
onDetail: () => void;
}> = ({ doctor, onConsult, onDetail }) => {
const [hovered, setHovered] = useState(false);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
background: '#fff',
borderRadius: 12,
border: `1px solid ${hovered ? '#91caff' : '#edf2fc'}`,
padding: '20px',
transition: 'all 0.2s',
boxShadow: hovered ? '0 4px 16px rgba(24,144,255,0.12)' : '0 1px 3px rgba(0,82,204,0.06)',
cursor: 'default',
}}
>
{/* 顶部:头像 + 基本信息 */}
<div style={{ display: 'flex', gap: 16, marginBottom: 14 }}>
{/* 头像 + 在线状态 */}
<div style={{ position: 'relative', flexShrink: 0 }}>
<Avatar
size={64}
src={doctor.avatar || null}
icon={<UserOutlined />}
style={{ backgroundColor: '#e6f7ff', color: '#1890ff', fontSize: 28 }}
/>
{doctor.is_online && (
<span style={{
position: 'absolute', bottom: 2, right: 2,
width: 12, height: 12, borderRadius: '50%',
background: '#52c41a', border: '2px solid #fff',
}} />
)}
</div>
{/* 姓名 + 标签 + 医院 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 6 }}>
<span style={{ fontSize: 16, fontWeight: 700, color: '#1d2129' }}>{doctor.name}</span>
<Tag color="blue" style={{ borderRadius: 4, fontSize: 11, margin: 0 }}>{doctor.title}</Tag>
<Tag style={{ borderRadius: 4, fontSize: 11, margin: 0, color: '#4e5969', borderColor: '#d9e4f5' }}>
{doctor.department_name}
</Tag>
{doctor.is_online
? <Tag color="success" style={{ borderRadius: 4, fontSize: 11, margin: 0 }}>在线</Tag>
: <Tag style={{ borderRadius: 4, fontSize: 11, margin: 0, color: '#bfbfbf', borderColor: '#e8e8e8' }}>离线</Tag>
}
</div>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 8 }}>{doctor.hospital}</div>
{/* 评分 + 问诊次数 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<StarFilled style={{ color: '#faad14', fontSize: 13 }} />
<span style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>
{doctor.rating?.toFixed(1) || '5.0'}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<FireFilled style={{ color: '#ff7a45', fontSize: 12 }} />
<span style={{ fontSize: 12, color: '#8c8c8c' }}>问诊 {doctor.consult_count} 次</span>
</div>
</div>
</div>
{/* 价格 */}
<div style={{ textAlign: 'right', flexShrink: 0 }}>
<div style={{ fontSize: 20, fontWeight: 700, color: '#1890ff', lineHeight: 1 }}>
¥{(doctor.price / 100).toFixed(0)}
</div>
<div style={{ fontSize: 11, color: '#bfbfbf', marginTop: 2 }}>元 / 次</div>
</div>
</div>
{/* 擅长方向 */}
<div style={{
fontSize: 13, color: '#595959', lineHeight: 1.6,
padding: '10px 12px',
background: '#f8faff',
borderRadius: 8,
marginBottom: 14,
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}>
<span style={{ color: '#8c8c8c', marginRight: 4 }}>擅长:</span>
{doctor.specialties?.join('、') || doctor.introduction || '暂无介绍'}
</div>
{/* 底部操作按钮 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Button
icon={<MessageOutlined />}
onClick={() => onConsult(doctor, 'text')}
style={{ flex: 1, borderRadius: 8, height: 36, fontSize: 13, fontWeight: 500 }}
>
图文问诊
</Button>
<Button
type="primary"
icon={<VideoCameraOutlined />}
onClick={() => onConsult(doctor, 'video')}
style={{ flex: 1, borderRadius: 8, height: 36, fontSize: 13, fontWeight: 500 }}
>
视频问诊
</Button>
<Button
type="text"
icon={<RightOutlined />}
onClick={onDetail}
style={{ color: '#8c8c8c', fontSize: 13, padding: '0 8px', height: 36, borderRadius: 8 }}
>
详情
</Button>
</div>
</div>
);
};
export default PatientDoctorsPage;
......@@ -366,10 +366,13 @@ const FamilyTab: React.FC = () => {
// ==================== 主页面 ====================
const PatientHealthRecordsPage: React.FC = () => (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">健康档案</h4>
<Card size="small">
<Tabs defaultActiveKey="profile" size="small" items={[
<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>
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="profile" items={[
{ key: 'profile', label: <span><MedicineBoxOutlined /> 健康信息</span>, children: <HealthProfileTab /> },
{ key: 'reports', label: <span><FileTextOutlined /> 检验报告</span>, children: <LabReportsTab /> },
{ key: 'history', label: <span><MedicineBoxOutlined /> 就诊历史</span>, children: <ConsultHistoryTab /> },
......
......@@ -2,7 +2,7 @@
import React from 'react';
import { useRouter } from 'next/navigation';
import { Card, Row, Col, Input, Button, Typography, Tag } from 'antd';
import { Card, Row, Col, Input, Button, Tag } from 'antd';
import {
SearchOutlined,
VideoCameraOutlined,
......@@ -19,8 +19,6 @@ import {
FolderOpenOutlined,
} from '@ant-design/icons';
const { Text } = Typography;
const services = [
{ icon: <VideoCameraOutlined />, title: '视频问诊', desc: '面对面视频交流', path: '/patient/doctors?type=video', color: '#1890ff', bg: '#e6f7ff' },
{ icon: <MessageOutlined />, title: '图文问诊', desc: '随时在线咨询', path: '/patient/doctors?type=text', color: '#52c41a', bg: '#f6ffed' },
......@@ -31,9 +29,9 @@ const services = [
];
const quickLinks = [
{ icon: <VideoCameraOutlined />, title: '我的问诊', path: '/patient/consult', color: '#1890ff' },
{ icon: <ShoppingCartOutlined />, title: '药品配送', path: '/patient/pharmacy/order', color: '#fa8c16' },
{ icon: <FolderOpenOutlined />, title: '健康档案', path: '/patient/health-records', color: '#52c41a' },
{ icon: <VideoCameraOutlined />, title: '我的问诊', desc: '查看进行中的问诊', path: '/patient/consult', color: '#1890ff' },
{ icon: <ShoppingCartOutlined />, title: '药品配送', desc: '处方药品快速配送', path: '/patient/pharmacy/order', color: '#fa8c16' },
{ icon: <FolderOpenOutlined />, title: '健康档案', desc: '查看我的健康数据', path: '/patient/health-records', color: '#52c41a' },
];
const departments = [
......@@ -41,129 +39,188 @@ const departments = [
'耳鼻喉科', '口腔科', '骨科', '神经内科', '心血管内科', '消化内科',
];
const stats = [
{ icon: <TeamOutlined />, value: '1,280', label: '注册医生' },
{ icon: <SafetyCertificateOutlined />, value: '56,800', label: '服务患者' },
{ icon: <ClockCircleOutlined />, value: '3 分钟', label: '平均响应' },
];
const PatientHomePage: React.FC = () => {
const router = useRouter();
return (
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 顶部 Banner */}
<div style={{
borderRadius: 12, overflow: 'hidden', position: 'relative',
background: 'linear-gradient(135deg, #001529 0%, #003a8c 40%, #0050b3 70%, #1890ff 100%)',
}}>
{/* 装饰 */}
<div style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}>
<div style={{ position: 'absolute', top: -40, right: -40, width: 160, height: 160, background: 'rgba(255,255,255,0.05)', borderRadius: '50%', filter: 'blur(40px)' }} />
<div style={{ position: 'absolute', bottom: -40, left: -40, width: 192, height: 192, background: 'rgba(96,165,250,0.08)', borderRadius: '50%', filter: 'blur(40px)' }} />
</div>
<div style={{ position: 'relative', zIndex: 1, textAlign: 'center', padding: '32px 24px' }}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: '0 0 4px' }}>互联网医院 · 在线问诊平台</h2>
<p style={{ fontSize: 13, color: 'rgba(191,219,254,0.8)', margin: '0 0 20px' }}>专业医生在线服务,足不出户享受优质医疗</p>
<div style={{ maxWidth: 440, margin: '0 auto' }}>
<Input.Search
placeholder="搜索医生、科室、疾病"
enterButton={<><SearchOutlined /> 搜索</>}
onSearch={(value) => router.push(`/patient/doctors?keyword=${value}`)}
size="large"
/>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Banner - 轻量蓝色,现代医疗风 */}
<div style={{
borderRadius: 12,
overflow: 'hidden',
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
position: 'relative',
}}>
{/* 装饰圆 */}
<div style={{ position: 'absolute', top: -60, right: -60, width: 220, height: 220, borderRadius: '50%', background: 'rgba(255,255,255,0.06)' }} />
<div style={{ position: 'absolute', bottom: -40, left: 80, width: 160, height: 160, borderRadius: '50%', background: 'rgba(255,255,255,0.04)' }} />
<div style={{ position: 'relative', zIndex: 1, padding: '32px 40px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 24 }}>
{/* 左侧文字 + 搜索 */}
<div style={{ flex: 1, minWidth: 280 }}>
<h2 style={{ fontSize: 22, fontWeight: 700, color: '#fff', margin: '0 0 6px', letterSpacing: '-0.3px' }}>
互联网医院 · 在线问诊平台
</h2>
<p style={{ fontSize: 14, color: 'rgba(255,255,255,0.75)', margin: '0 0 20px', lineHeight: 1.6 }}>
专业医生在线服务,足不出户享受优质医疗
</p>
<div style={{ maxWidth: 420 }}>
<Input.Search
placeholder="搜索医生、科室、疾病..."
enterButton={<span><SearchOutlined style={{ marginRight: 4 }} />搜索</span>}
onSearch={(value) => router.push(`/patient/doctors?keyword=${value}`)}
size="large"
style={{ borderRadius: 10 }}
/>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: 48, marginTop: 20 }}>
{[
{ icon: <TeamOutlined />, value: '1,280', label: '注册医生' },
{ icon: <SafetyCertificateOutlined />, value: '56,800', label: '服务患者' },
{ icon: <ClockCircleOutlined />, value: '3分钟', label: '平均响应' },
].map((stat) => (
{/* 右侧统计数字 */}
<div style={{ display: 'flex', gap: 40 }}>
{stats.map((stat) => (
<div key={stat.label} style={{ textAlign: 'center' }}>
<div style={{ fontSize: 20, fontWeight: 700, color: '#fff' }}>
<span style={{ marginRight: 4 }}>{stat.icon}</span>{stat.value}
</div>
<div style={{ fontSize: 11, color: 'rgba(191,219,254,0.6)' }}>{stat.label}</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.6)', marginBottom: 4 }}>{stat.icon}</div>
<div style={{ fontSize: 22, fontWeight: 700, color: '#fff', lineHeight: 1 }}>{stat.value}</div>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.7)', marginTop: 4 }}>{stat.label}</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* 医疗服务入口 */}
<Card
title="医疗服务"
styles={{ header: { fontWeight: 700, fontSize: 15 } }}
>
<Row gutter={[12, 12]}>
{services.map((service, index) => (
<Col xs={12} sm={8} md={4} key={index}>
<div
onClick={() => router.push(service.path)}
style={{
textAlign: 'center', borderRadius: 10, padding: '16px 8px',
cursor: 'pointer', transition: 'all 0.2s',
background: service.bg,
border: `1px solid ${service.color}18`,
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = `0 6px 16px ${service.color}22`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
<div style={{ fontSize: 30, color: service.color, marginBottom: 8, lineHeight: 1 }}>{service.icon}</div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129', marginBottom: 2 }}>{service.title}</div>
<div style={{ fontSize: 11, color: '#8c8c8c' }}>{service.desc}</div>
</div>
</Col>
))}
</Row>
</Card>
{/* 医疗服务入口 */}
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>医疗服务</span>} size="small">
<Row gutter={[8, 8]}>
{services.map((service, index) => (
<Col xs={12} sm={8} md={4} key={index}>
<Row gutter={[16, 16]}>
{/* 快捷入口 */}
<Col xs={24} sm={12}>
<Card
title="快捷入口"
styles={{ header: { fontWeight: 700, fontSize: 15 }, body: { padding: '12px 20px' } }}
style={{ height: '100%' }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{quickLinks.map((link) => (
<div
key={link.title}
onClick={() => router.push(link.path)}
style={{
textAlign: 'center', borderRadius: 8, padding: 12,
cursor: 'pointer', transition: 'all 0.2s', background: service.bg,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '10px 14px', borderRadius: 10, cursor: 'pointer',
border: '1px solid #edf2fc', transition: 'all 0.2s',
background: '#fafcff',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e6f7ff';
e.currentTarget.style.borderColor = '#91caff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#fafcff';
e.currentTarget.style.borderColor = '#edf2fc';
}}
onClick={() => router.push(service.path)}
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.03)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.08)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; }}
>
<div style={{ fontSize: 32, color: service.color, marginBottom: 8 }}>{service.icon}</div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>{service.title}</div>
<div style={{ fontSize: 11, color: '#8c8c8c' }}>{service.desc}</div>
</div>
</Col>
))}
</Row>
</Card>
<Row gutter={[8, 8]}>
{/* 快捷入口 */}
<Col xs={24} sm={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>快捷入口</span>} size="small" style={{ height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{quickLinks.map((link) => (
<div
key={link.title}
style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '10px 12px', borderRadius: 8, cursor: 'pointer',
border: '1px solid #e6f0fa', transition: 'all 0.2s',
}}
onClick={() => router.push(link.path)}
onMouseEnter={(e) => { e.currentTarget.style.background = '#f0f5ff'; e.currentTarget.style.borderColor = '#bbd4f0'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = '#e6f0fa'; }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 32, height: 32, borderRadius: 8, display: 'flex',
alignItems: 'center', justifyContent: 'center', fontSize: 16,
color: link.color, background: `${link.color}15`,
}}>
{link.icon}
</div>
<span style={{ fontSize: 13, fontWeight: 500, color: '#1d2129' }}>{link.title}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 36, height: 36, borderRadius: 10, display: 'flex',
alignItems: 'center', justifyContent: 'center', fontSize: 17,
color: link.color, background: `${link.color}15`,
}}>
{link.icon}
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>{link.title}</div>
<div style={{ fontSize: 11, color: '#8c8c8c' }}>{link.desc}</div>
</div>
<RightOutlined style={{ fontSize: 12, color: '#bfbfbf' }} />
</div>
))}
</div>
</Card>
</Col>
<RightOutlined style={{ fontSize: 12, color: '#bfbfbf' }} />
</div>
))}
</div>
</Card>
</Col>
{/* 科室导航 */}
<Col xs={24} sm={12}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>科室导航</span>} size="small" style={{ height: '100%' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{departments.map((dept) => (
<Tag
key={dept}
style={{ cursor: 'pointer', borderRadius: 16, padding: '2px 12px', fontSize: 12 }}
onClick={() => router.push(`/patient/doctors?department=${dept}`)}
>
{dept}
</Tag>
))}
<Button type="link" size="small" style={{ fontSize: 12, padding: 0 }}
onClick={() => router.push('/patient/doctors')}>
全部科室 &gt;
</Button>
</div>
</Card>
</Col>
</Row>
</div>
{/* 科室导航 */}
<Col xs={24} sm={12}>
<Card
title="科室导航"
styles={{ header: { fontWeight: 700, fontSize: 15 } }}
style={{ height: '100%' }}
>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{departments.map((dept) => (
<Tag
key={dept}
onClick={() => router.push(`/patient/doctors?department=${dept}`)}
style={{
cursor: 'pointer', borderRadius: 20, padding: '4px 14px',
fontSize: 13, lineHeight: '22px',
border: '1px solid #c9dcf8', color: '#1890ff',
background: '#f0f7ff', transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.background = '#1890ff';
(e.currentTarget as HTMLElement).style.color = '#fff';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = '#f0f7ff';
(e.currentTarget as HTMLElement).style.color = '#1890ff';
}}
>
{dept}
</Tag>
))}
<Button
type="link"
size="small"
style={{ fontSize: 13, padding: '4px 0', color: '#8c8c8c' }}
onClick={() => router.push('/patient/doctors')}
>
全部科室 →
</Button>
</div>
</Card>
</Col>
</Row>
</div>
);
};
......
......@@ -96,7 +96,7 @@ const PatientPaymentPage: React.FC = () => {
},
{
title: '金额', dataIndex: 'amount', key: 'amount', width: 100,
render: (v: number) => <Text strong style={{ color: '#f5222d' }}>¥{(v / 100).toFixed(2)}</Text>,
render: (v: number) => <Text strong style={{ color: '#1890ff' }}>¥{(v / 100).toFixed(2)}</Text>,
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
......@@ -116,39 +116,44 @@ const PatientPaymentPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">
<WalletOutlined className="mr-1" />支付管理
</h4>
<Button type="primary" size="small" onClick={() => router.push('/patient/consult')}>查看问诊</Button>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 页面标题 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>支付管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看您的支付记录和订单状态</div>
</div>
<Button type="primary" icon={<WalletOutlined />} onClick={() => router.push('/patient/consult')}
style={{ borderRadius: 8 }}>
查看问诊
</Button>
</div>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<Statistic title="已支付总额" value={totalPaid / 100} precision={2} prefix="¥" valueStyle={{ color: '#52c41a' }} />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<Statistic title="已支付总额" value={totalPaid / 100} precision={2} prefix="¥" valueStyle={{ color: '#52c41a', fontSize: 22 }} />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<Statistic title="待支付" value={totalPending / 100} precision={2} prefix="¥" valueStyle={{ color: '#fa8c16' }} />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<Statistic title="待支付" value={totalPending / 100} precision={2} prefix="¥" valueStyle={{ color: '#fa8c16', fontSize: 22 }} />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<Statistic title="问诊支付" value={consultCount} suffix="笔" />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<Statistic title="问诊支付" value={consultCount} suffix="笔" valueStyle={{ fontSize: 22 }} />
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #722ed1' }}>
<Statistic title="购药支付" value={pharmacyCount} suffix="笔" />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #722ed1' }}>
<Statistic title="购药支付" value={pharmacyCount} suffix="笔" valueStyle={{ fontSize: 22 }} />
</Card>
</Col>
</Row>
<Card size="small">
<Tabs defaultActiveKey="all" size="small" items={[
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="all" items={[
{
key: 'all',
label: <span><FileTextOutlined /> 全部记录</span>,
......@@ -197,7 +202,7 @@ const PatientPaymentPage: React.FC = () => {
]} />
</Card>
<Card size="small" title={<span className="text-xs font-semibold">支付方式</span>}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>支付方式</span>} style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Row gutter={[8, 8]}>
{[
{ icon: <PayCircleOutlined style={{ fontSize: 24, color: '#07c160' }} />, title: '微信支付', desc: '推荐' },
......@@ -228,7 +233,7 @@ const PatientPaymentPage: React.FC = () => {
<div className="space-y-4">
<div className="text-center py-4">
<div className="text-gray-500 text-sm mb-2">支付金额</div>
<div className="text-3xl font-bold text-red-500">
<div style={{ fontSize: 32, fontWeight: 700, color: '#1890ff' }}>
¥{(selectedOrder.amount / 100).toFixed(2)}
</div>
<div className="text-xs text-gray-400 mt-1">
......
......@@ -107,7 +107,7 @@ const PatientPharmacyOrderPage: React.FC = () => {
},
{
title: '金额', dataIndex: 'total_amount', key: 'total_amount', width: 90,
render: (v: number) => <Text strong style={{ color: '#f5222d' }}>¥{(v / 100).toFixed(2)}</Text>,
render: (v: number) => <Text strong style={{ color: '#1890ff' }}>¥{(v / 100).toFixed(2)}</Text>,
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
......@@ -134,88 +134,103 @@ const PatientPharmacyOrderPage: React.FC = () => {
];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-gray-800 m-0">
<ShoppingCartOutlined className="mr-1" />药品配送
</h4>
<Button type="primary" size="small" onClick={() => router.push('/patient/prescription')}>查看处方</Button>
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 页面标题 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>药品配送</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>查看处方药品的配送状态和订单记录</div>
</div>
<Button type="primary" icon={<ShoppingCartOutlined />} onClick={() => router.push('/patient/prescription')}
style={{ borderRadius: 8 }}>
查看处方
</Button>
</div>
<Row gutter={[8, 8]}>
{/* 统计卡片 */}
<Row gutter={[16, 16]}>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #1890ff' }}>
<div className="flex items-center gap-2">
<MedicineBoxOutlined className="text-xl text-[#1890ff]" />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #1890ff' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#e6f7ff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, color: '#1890ff' }}>
<MedicineBoxOutlined />
</div>
<div>
<div className="text-xs text-gray-500">全部订单</div>
<div className="text-lg font-bold">{orders.length}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>全部订单</div>
<div style={{ fontSize: 22, fontWeight: 700, lineHeight: 1.2, color: '#1d2129' }}>{orders.length}</div>
</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #fa8c16' }}>
<div className="flex items-center gap-2">
<ClockCircleOutlined className="text-xl text-[#fa8c16]" />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #fa8c16' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#fff7e6', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, color: '#fa8c16' }}>
<ClockCircleOutlined />
</div>
<div>
<div className="text-xs text-gray-500">待发货</div>
<div className="text-lg font-bold">{orders.filter(o => o.status === 'pending').length}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>待发货</div>
<div style={{ fontSize: 22, fontWeight: 700, lineHeight: 1.2, color: '#1d2129' }}>{orders.filter(o => o.status === 'pending').length}</div>
</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #722ed1' }}>
<div className="flex items-center gap-2">
<CarOutlined className="text-xl text-[#722ed1]" />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #722ed1' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#f9f0ff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, color: '#722ed1' }}>
<CarOutlined />
</div>
<div>
<div className="text-xs text-gray-500">配送中</div>
<div className="text-lg font-bold">{orders.filter(o => o.status === 'shipping').length}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>配送中</div>
<div style={{ fontSize: 22, fontWeight: 700, lineHeight: 1.2, color: '#1d2129' }}>{orders.filter(o => o.status === 'shipping').length}</div>
</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card size="small" style={{ borderLeft: '3px solid #52c41a' }}>
<div className="flex items-center gap-2">
<CheckCircleOutlined className="text-xl text-[#52c41a]" />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc', borderLeft: '4px solid #52c41a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#f6ffed', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, color: '#52c41a' }}>
<CheckCircleOutlined />
</div>
<div>
<div className="text-xs text-gray-500">已完成</div>
<div className="text-lg font-bold">{orders.filter(o => o.status === 'completed').length}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>已完成</div>
<div style={{ fontSize: 22, fontWeight: 700, lineHeight: 1.2, color: '#1d2129' }}>{orders.filter(o => o.status === 'completed').length}</div>
</div>
</div>
</Card>
</Col>
</Row>
<Card size="small">
<Tabs defaultActiveKey="all" size="small" items={[
{/* 订单列表 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Tabs defaultActiveKey="all" items={[
{
key: 'all',
label: <span><ShoppingCartOutlined /> 全部订单</span>,
children: (
<Table columns={columns} dataSource={orders} rowKey="id" size="small" loading={loading}
pagination={{ pageSize: 10, size: 'small' }}
locale={{ emptyText: <Empty description="暂无药品订单" /> }} />
<Table columns={columns} dataSource={orders} rowKey="id" loading={loading}
pagination={{ pageSize: 10 }}
locale={{ emptyText: <Empty description="暂无药品订单" style={{ padding: '40px 0' }} /> }} />
),
},
{
key: 'shipping',
label: <span><CarOutlined /> 配送中</span>,
children: (
<Table columns={columns} dataSource={orders.filter(o => o.status === 'shipping')} rowKey="id" size="small"
<Table columns={columns} dataSource={orders.filter(o => o.status === 'shipping')} rowKey="id"
loading={loading} pagination={false}
locale={{ emptyText: <Empty description="暂无配送中的订单" /> }} />
locale={{ emptyText: <Empty description="暂无配送中的订单" style={{ padding: '40px 0' }} /> }} />
),
},
{
key: 'completed',
label: <span><CheckCircleOutlined /> 已完成</span>,
children: (
<Table columns={columns} dataSource={orders.filter(o => o.status === 'completed')} rowKey="id" size="small"
loading={loading} pagination={{ pageSize: 10, size: 'small' }}
locale={{ emptyText: <Empty description="暂无已完成订单" /> }} />
<Table columns={columns} dataSource={orders.filter(o => o.status === 'completed')} rowKey="id"
loading={loading} pagination={{ pageSize: 10 }}
locale={{ emptyText: <Empty description="暂无已完成订单" style={{ padding: '40px 0' }} /> }} />
),
},
]} />
......@@ -223,7 +238,7 @@ const PatientPharmacyOrderPage: React.FC = () => {
<Modal title="订单详情" open={!!detailModal} onCancel={() => setDetailModal(null)} footer={null} width={600}>
{detailModal && (
<div className="space-y-3">
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, paddingTop: 8 }}>
<Steps current={getStepIndex(detailModal.status)} size="small" items={statusSteps} />
<Descriptions bordered size="small" column={2}>
<Descriptions.Item label="处方号">{detailModal.prescription_no}</Descriptions.Item>
......@@ -235,7 +250,7 @@ const PatientPharmacyOrderPage: React.FC = () => {
</Space>
</Descriptions.Item>
<Descriptions.Item label="金额">
<Text strong style={{ color: '#f5222d' }}>¥{(detailModal.total_amount / 100).toFixed(2)}</Text>
<Text strong style={{ color: '#1890ff' }}>¥{(detailModal.total_amount / 100).toFixed(2)}</Text>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={statusMap[detailModal.status]?.color}>{statusMap[detailModal.status]?.label}</Tag>
......
......@@ -77,7 +77,7 @@ const PatientPrescriptionPage: React.FC = () => {
},
{
title: '金额', dataIndex: 'total_amount', key: 'total_amount', width: 100,
render: (v: number) => <Text strong style={{ color: '#f5222d' }}>¥{(v / 100).toFixed(2)}</Text>,
render: (v: number) => <Text strong style={{ color: '#1890ff' }}>¥{(v / 100).toFixed(2)}</Text>,
},
{ title: '状态', dataIndex: 'status', key: 'status', width: 90, render: (v: string) => getStatusTag(v) },
{
......@@ -94,28 +94,28 @@ const PatientPrescriptionPage: React.FC = () => {
if (!loading && prescriptions.length === 0) {
return (
<div className="max-w-[600px] mx-auto pt-10">
<Card size="small" className="text-center py-6">
<FileTextOutlined className="text-4xl text-[#1890ff] mb-2" />
<div className="text-sm font-bold mb-1">电子处方</div>
<Text type="secondary" className="text-xs!">暂无处方记录,完成问诊后医生会为您开具处方</Text>
<div className="mt-4">
<Button type="primary" size="small" onClick={() => router.push('/patient/doctors')}>去问诊</Button>
</div>
<div style={{ padding: '20px 24px' }}>
<Card style={{ maxWidth: 500, margin: '60px auto', textAlign: 'center', borderRadius: 12, border: '1px solid #edf2fc', padding: '40px 20px' }}>
<FileTextOutlined style={{ fontSize: 48, color: '#1890ff', marginBottom: 16 }} />
<div style={{ fontSize: 16, fontWeight: 600, marginBottom: 8, color: '#1d2129' }}>暂无处方记录</div>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 20 }}>完成问诊后医生会为您开具处方</div>
<Button type="primary" onClick={() => router.push('/patient/doctors')} style={{ borderRadius: 8 }}>去问诊</Button>
</Card>
</div>
);
}
return (
<div className="space-y-2">
<h4 className="text-sm font-bold text-gray-800 m-0">
<FileTextOutlined className="mr-1" />电子处方
</h4>
<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>
<Spin spinning={loading}>
<Card size="small">
<Table columns={columns} dataSource={prescriptions} rowKey="id" size="small"
pagination={{ current: page, total, pageSize: 10, size: 'small', onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table columns={columns} dataSource={prescriptions} rowKey="id"
pagination={{ current: page, total, pageSize: 10, onChange: setPage, showTotal: (t) => `共 ${t} 张` }} />
</Card>
</Spin>
......@@ -165,7 +165,7 @@ const PatientPrescriptionPage: React.FC = () => {
))}
<Divider style={{ margin: '8px 0' }} />
<div style={{ textAlign: 'right' }}>
<Text strong style={{ fontSize: 16, color: '#f5222d' }}>合计:¥{(currentRx.total_amount / 100).toFixed(2)}</Text>
<Text strong style={{ fontSize: 16, color: '#1890ff' }}>合计:¥{(currentRx.total_amount / 100).toFixed(2)}</Text>
</div>
</Card>
{currentRx.remark && (
......
......@@ -35,39 +35,47 @@ const PatientProfilePage: React.FC = () => {
];
return (
<div style={{ maxWidth: 1200, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 8 }}>
{/* 个人信息卡片 */}
<Card size="small">
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<Avatar size={56} src={user?.avatar} icon={<UserOutlined />}
style={{ backgroundColor: '#1890ff', flexShrink: 0 }} />
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 个人信息 Hero 卡片 */}
<div style={{
borderRadius: 12, overflow: 'hidden',
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
padding: '28px 32px',
position: 'relative',
}}>
<div style={{ position: 'absolute', top: -40, right: -40, width: 160, height: 160, borderRadius: '50%', background: 'rgba(255,255,255,0.06)' }} />
<div style={{ position: 'relative', zIndex: 1, display: 'flex', alignItems: 'center', gap: 20 }}>
<Avatar size={72} src={user?.avatar || null} icon={<UserOutlined />}
style={{ backgroundColor: 'rgba(255,255,255,0.2)', color: '#fff', fontSize: 32, flexShrink: 0 }} />
<div style={{ flex: 1 }}>
<h4 style={{ fontSize: 15, fontWeight: 700, margin: '0 0 4px', color: '#1d2129' }}>
<h2 style={{ fontSize: 20, fontWeight: 700, margin: '0 0 6px', color: '#fff' }}>
{user?.real_name || '未设置姓名'}
</h4>
<div style={{ fontSize: 12, color: '#8c8c8c', marginBottom: 6 }}>
<PhoneOutlined style={{ marginRight: 4 }} />{user?.phone || '未绑定手机'}
</h2>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.8)', marginBottom: 10 }}>
<PhoneOutlined style={{ marginRight: 6 }} />{user?.phone || '未绑定手机'}
</div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ display: 'flex', gap: 6 }}>
{user?.is_verified ? (
<Tag icon={<SafetyCertificateOutlined />} color="success">已实名</Tag>
<Tag icon={<SafetyCertificateOutlined />} color="success" style={{ borderRadius: 20 }}>已实名认证</Tag>
) : (
<Tag color="warning">未认证</Tag>
<Tag color="warning" style={{ borderRadius: 20 }}>未认证</Tag>
)}
<Tag color="blue">患者</Tag>
<Tag color="blue" style={{ borderRadius: 20 }}>患者</Tag>
</div>
</div>
<Button size="small" type="primary" icon={<EditOutlined />}>编辑资料</Button>
<Button icon={<EditOutlined />} style={{ borderRadius: 8, background: 'rgba(255,255,255,0.15)', border: 'none', color: '#fff' }}>
编辑资料
</Button>
</div>
</Card>
</div>
<Row gutter={[8, 8]}>
<Row gutter={[16, 16]}>
{/* 基本信息 */}
<Col xs={24} sm={12}>
<Card title={<span style={{ fontSize: 13, fontWeight: 600 }}>基本信息</span>} size="small" style={{ height: '100%' }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>基本信息</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #edf2fc' }}>
<Descriptions column={1} size="small">
<Descriptions.Item label="用户ID">
<Text copyable style={{ fontSize: 12 }}>{user?.id || '-'}</Text>
<Text copyable style={{ fontSize: 13 }}>{user?.id || '-'}</Text>
</Descriptions.Item>
<Descriptions.Item label="手机号">{user?.phone || '-'}</Descriptions.Item>
<Descriptions.Item label="真实姓名">{user?.real_name || '-'}</Descriptions.Item>
......@@ -80,32 +88,32 @@ const PatientProfilePage: React.FC = () => {
{/* 实名认证 */}
<Col xs={24} sm={12}>
<Card title={<span style={{ fontSize: 13, fontWeight: 600 }}>实名认证</span>} size="small" style={{ height: '100%' }}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>实名认证</span>} style={{ height: '100%', borderRadius: 12, border: '1px solid #edf2fc' }}>
{user?.is_verified ? (
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<div style={{
width: 48, height: 48, borderRadius: '50%', backgroundColor: '#f6ffed',
width: 56, height: 56, borderRadius: '50%', backgroundColor: '#f6ffed',
display: 'flex', alignItems: 'center', justifyContent: 'center',
margin: '0 auto 8px', fontSize: 24, color: '#52c41a',
margin: '0 auto 12px', fontSize: 28, color: '#52c41a',
}}>
<SafetyCertificateOutlined />
</div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1d2129' }}>已完成实名认证</div>
<div style={{ fontSize: 11, color: '#8c8c8c', marginTop: 4 }}>可使用全部在线问诊服务</div>
<div style={{ fontSize: 15, fontWeight: 600, color: '#1d2129' }}>已完成实名认证</div>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 6 }}>可使用全部在线问诊服务</div>
</div>
) : (
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<div style={{
width: 48, height: 48, borderRadius: '50%', backgroundColor: '#fffbe6',
width: 56, height: 56, borderRadius: '50%', backgroundColor: '#fffbe6',
display: 'flex', alignItems: 'center', justifyContent: 'center',
margin: '0 auto 8px', fontSize: 24, color: '#faad14',
margin: '0 auto 12px', fontSize: 28, color: '#faad14',
}}>
<IdcardOutlined />
</div>
<div style={{ fontSize: 12, color: '#8c8c8c', marginBottom: 8 }}>
<div style={{ fontSize: 13, color: '#8c8c8c', marginBottom: 12 }}>
完成实名认证后可享受完整的线上问诊服务
</div>
<Button type="primary" size="small">去认证</Button>
<Button type="primary" style={{ borderRadius: 8 }}>去认证</Button>
</div>
)}
</Card>
......@@ -113,31 +121,32 @@ const PatientProfilePage: React.FC = () => {
</Row>
{/* 更多服务 */}
<Card title={<span style={{ fontSize: 13, fontWeight: 600 }}>更多服务</span>} size="small">
<Row gutter={[8, 8]}>
<Card title={<span style={{ fontSize: 14, fontWeight: 600 }}>更多服务</span>} style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Row gutter={[12, 12]}>
{serviceLinks.map((link) => (
<Col xs={12} sm={8} md={4} key={link.title}>
<div
style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '10px 12px', borderRadius: 8, cursor: 'pointer',
border: '1px solid #e6f0fa', transition: 'all 0.2s',
padding: '12px 14px', borderRadius: 10, cursor: 'pointer',
border: '1px solid #edf2fc', transition: 'all 0.2s',
background: '#fafcff',
}}
onClick={() => router.push(link.path)}
onMouseEnter={(e) => { e.currentTarget.style.background = '#f0f5ff'; e.currentTarget.style.borderColor = '#bbd4f0'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = '#e6f0fa'; }}
onMouseEnter={(e) => { e.currentTarget.style.background = '#e6f7ff'; e.currentTarget.style.borderColor = '#91caff'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = '#fafcff'; e.currentTarget.style.borderColor = '#edf2fc'; }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 28, height: 28, borderRadius: 6, display: 'flex',
width: 32, height: 32, borderRadius: 8, display: 'flex',
alignItems: 'center', justifyContent: 'center',
color: link.color, background: `${link.color}15`, fontSize: 14,
color: link.color, background: `${link.color}15`, fontSize: 16,
}}>
{link.icon}
</div>
<span style={{ fontSize: 12, fontWeight: 500, color: '#1d2129' }}>{link.title}</span>
<span style={{ fontSize: 13, fontWeight: 500, color: '#1d2129' }}>{link.title}</span>
</div>
<RightOutlined style={{ fontSize: 10, color: '#bfbfbf' }} />
<RightOutlined style={{ fontSize: 11, color: '#bfbfbf' }} />
</div>
</Col>
))}
......
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