Compare commits
2 Commits
d6a432aec4
...
db443b258e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db443b258e | ||
|
|
ede4a8d29e |
@@ -1,15 +0,0 @@
|
|||||||
# PostgreSQL
|
|
||||||
ConnectionStrings__Default=Host=localhost;Port=5432;Database=health_manager;Username=postgres;Password=your_password
|
|
||||||
|
|
||||||
# JWT
|
|
||||||
Jwt__Secret=your-jwt-secret-change-me
|
|
||||||
Jwt__Issuer=HealthManager
|
|
||||||
Jwt__Audience=HealthManagerApp
|
|
||||||
|
|
||||||
# Redis (reserved)
|
|
||||||
Redis__Connection=localhost:6379
|
|
||||||
|
|
||||||
# MinIO (reserved)
|
|
||||||
MinIO__Endpoint=localhost:9000
|
|
||||||
MinIO__AccessKey=minioadmin
|
|
||||||
MinIO__SecretKey=minioadmin
|
|
||||||
@@ -16,4 +16,5 @@ public record UserProfileResponse(
|
|||||||
|
|
||||||
public record UpdateProfileRequest(
|
public record UpdateProfileRequest(
|
||||||
string? Name, string? Gender, DateOnly? Birthday,
|
string? Name, string? Gender, DateOnly? Birthday,
|
||||||
decimal? HeightCm, decimal? WeightKg, List<string>? MedicalHistory);
|
decimal? HeightCm, decimal? WeightKg, List<string>? MedicalHistory,
|
||||||
|
string? Department, string? Title, string? Introduction, List<string>? Specialty);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class FollowUpService(AppDbContext db)
|
|||||||
.OrderBy(f => f.ScheduledAt)
|
.OrderBy(f => f.ScheduledAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
public async Task<FollowUp> AddAsync(Guid patientId, string title, string? description, DateTime scheduledAt, bool reminderEnabled, Guid? doctorId = null)
|
public async Task<FollowUp> AddAsync(Guid patientId, string title, string? description, DateTime scheduledAt, bool reminderEnabled, Guid? doctorId = null, string? notes = null)
|
||||||
{
|
{
|
||||||
var followUp = new FollowUp
|
var followUp = new FollowUp
|
||||||
{
|
{
|
||||||
@@ -30,6 +30,7 @@ public class FollowUpService(AppDbContext db)
|
|||||||
Description = description,
|
Description = description,
|
||||||
ScheduledAt = DateTime.SpecifyKind(scheduledAt, DateTimeKind.Utc),
|
ScheduledAt = DateTime.SpecifyKind(scheduledAt, DateTimeKind.Utc),
|
||||||
ReminderEnabled = reminderEnabled,
|
ReminderEnabled = reminderEnabled,
|
||||||
|
Notes = notes,
|
||||||
};
|
};
|
||||||
db.FollowUps.Add(followUp);
|
db.FollowUps.Add(followUp);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@@ -59,4 +60,13 @@ public class FollowUpService(AppDbContext db)
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return followUp;
|
return followUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
var followUp = await db.FollowUps.FindAsync(id);
|
||||||
|
if (followUp == null) return false;
|
||||||
|
db.FollowUps.Remove(followUp);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,4 +90,34 @@ public class MedicationService(AppDbContext db)
|
|||||||
|
|
||||||
return totalCount > 0 ? Math.Round((decimal)takenCount / totalCount * 100, 1) : 0;
|
return totalCount > 0 ? Math.Round((decimal)takenCount / totalCount * 100, 1) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Medication?> UpdateAsync(Guid medicationId, Guid userId, string? drugName,
|
||||||
|
string? dosage, string? frequency, List<string>? timeSlots,
|
||||||
|
DateOnly? startDate, DateOnly? endDate, string? notes, string? status)
|
||||||
|
{
|
||||||
|
var med = await db.Medications.FindAsync(medicationId);
|
||||||
|
if (med == null || med.UserId != userId) return null;
|
||||||
|
|
||||||
|
if (drugName != null) med.DrugName = drugName;
|
||||||
|
if (dosage != null) med.Dosage = dosage;
|
||||||
|
if (frequency != null) med.Frequency = frequency;
|
||||||
|
if (timeSlots != null) med.TimeSlots = timeSlots;
|
||||||
|
if (startDate.HasValue) med.StartDate = startDate.Value;
|
||||||
|
if (endDate.HasValue) med.EndDate = endDate;
|
||||||
|
if (notes != null) med.Notes = notes;
|
||||||
|
if (status != null) med.Status = status;
|
||||||
|
med.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return med;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(Guid medicationId, Guid userId)
|
||||||
|
{
|
||||||
|
var med = await db.Medications.FindAsync(medicationId);
|
||||||
|
if (med == null || med.UserId != userId) return false;
|
||||||
|
db.Medications.Remove(med);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using HealthManager.Application.Services;
|
|||||||
using HealthManager.Domain.Entities;
|
using HealthManager.Domain.Entities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace HealthManager.WebApi.Controllers;
|
namespace HealthManager.WebApi.Controllers;
|
||||||
|
|
||||||
@@ -29,14 +30,28 @@ public class AuthController(
|
|||||||
{
|
{
|
||||||
// Demo: auto-register new users
|
// Demo: auto-register new users
|
||||||
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
|
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
|
||||||
user = new User
|
|
||||||
|
// Check if this phone was soft-deleted — restore instead of creating duplicate
|
||||||
|
var deleted = await db.Users.IgnoreQueryFilters()
|
||||||
|
.FirstOrDefaultAsync(u => u.Phone == request.Phone && u.IsDeleted);
|
||||||
|
if (deleted != null)
|
||||||
{
|
{
|
||||||
Phone = request.Phone,
|
deleted.IsDeleted = false;
|
||||||
Name = "用户" + request.Phone[^4..],
|
deleted.DeletedAt = null;
|
||||||
Role = "patient",
|
deleted.UpdatedAt = DateTime.UtcNow;
|
||||||
PasswordHash = AuthService.HashPassword("demo123"),
|
user = deleted;
|
||||||
};
|
}
|
||||||
db.Users.Add(user);
|
else
|
||||||
|
{
|
||||||
|
user = new User
|
||||||
|
{
|
||||||
|
Phone = request.Phone,
|
||||||
|
Name = "用户" + request.Phone[^4..],
|
||||||
|
Role = "patient",
|
||||||
|
PasswordHash = AuthService.HashPassword("demo123"),
|
||||||
|
};
|
||||||
|
db.Users.Add(user);
|
||||||
|
}
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +136,10 @@ public class AuthController(
|
|||||||
if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm;
|
if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm;
|
||||||
if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg;
|
if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg;
|
||||||
if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory;
|
if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory;
|
||||||
|
if (request.Department != null) user.Department = request.Department;
|
||||||
|
if (request.Title != null) user.Title = request.Title;
|
||||||
|
if (request.Introduction != null) user.Introduction = request.Introduction;
|
||||||
|
if (request.Specialty != null) user.Specialty = request.Specialty;
|
||||||
user.UpdatedAt = DateTime.UtcNow;
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|||||||
@@ -55,10 +55,19 @@ public class FollowUpController(FollowUpService followUpService) : ControllerBas
|
|||||||
}
|
}
|
||||||
|
|
||||||
var followUp = await followUpService.AddAsync(patientId, request.Title, request.Description,
|
var followUp = await followUpService.AddAsync(patientId, request.Title, request.Description,
|
||||||
request.ScheduledAt, request.ReminderEnabled, doctorId);
|
request.ScheduledAt, request.ReminderEnabled, doctorId, request.Notes);
|
||||||
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
|
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}")]
|
||||||
|
[Authorize(Roles = "doctor")]
|
||||||
|
public async Task<IActionResult> DeleteFollowUp(Guid id)
|
||||||
|
{
|
||||||
|
var ok = await followUpService.DeleteAsync(id);
|
||||||
|
if (!ok) return NotFound(new { message = "复查不存在" });
|
||||||
|
return Ok(new { message = "删除成功" });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "doctor")]
|
[Authorize(Roles = "doctor")]
|
||||||
public async Task<IActionResult> UpdateFollowUp(Guid id, [FromBody] FollowUpUpdateRequest request)
|
public async Task<IActionResult> UpdateFollowUp(Guid id, [FromBody] FollowUpUpdateRequest request)
|
||||||
@@ -77,6 +86,7 @@ public class FollowUpCreateRequest
|
|||||||
public DateTime ScheduledAt { get; set; }
|
public DateTime ScheduledAt { get; set; }
|
||||||
public bool ReminderEnabled { get; set; } = true;
|
public bool ReminderEnabled { get; set; } = true;
|
||||||
public Guid? PatientId { get; set; }
|
public Guid? PatientId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FollowUpUpdateRequest
|
public class FollowUpUpdateRequest
|
||||||
|
|||||||
@@ -71,10 +71,32 @@ public class MedicationController(MedicationService medicationService) : Control
|
|||||||
var rate = await medicationService.GetAdherenceRateAsync(id);
|
var rate = await medicationService.GetAdherenceRateAsync(id);
|
||||||
return Ok(new { medicationId = id, rate });
|
return Ok(new { medicationId = id, rate });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}")]
|
||||||
|
public async Task<IActionResult> UpdateMedication(Guid id, [FromBody] MedicationUpdateRequest request)
|
||||||
|
{
|
||||||
|
var med = await medicationService.UpdateAsync(id, UserId, request.DrugName, request.Dosage,
|
||||||
|
request.Frequency, request.TimeSlots, request.StartDate, request.EndDate, request.Notes, request.Status);
|
||||||
|
if (med == null) return NotFound(new { message = "药品不存在" });
|
||||||
|
return Ok(new { med.Id, med.DrugName, med.Dosage, med.Status });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}")]
|
||||||
|
public async Task<IActionResult> DeleteMedication(Guid id)
|
||||||
|
{
|
||||||
|
var ok = await medicationService.DeleteAsync(id, UserId);
|
||||||
|
if (!ok) return NotFound(new { message = "药品不存在" });
|
||||||
|
return Ok(new { message = "删除成功" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record MedicationCreateRequest(
|
public record MedicationCreateRequest(
|
||||||
string DrugName, string Dosage, string Frequency,
|
string DrugName, string Dosage, string Frequency,
|
||||||
List<string> TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes);
|
List<string> TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes);
|
||||||
|
|
||||||
|
public record MedicationUpdateRequest(
|
||||||
|
string? DrugName, string? Dosage, string? Frequency,
|
||||||
|
List<string>? TimeSlots, DateOnly? StartDate, DateOnly? EndDate,
|
||||||
|
string? Notes, string? Status);
|
||||||
|
|
||||||
public record MarkTakenRequest(string TimeSlot);
|
public record MarkTakenRequest(string TimeSlot);
|
||||||
|
|||||||
@@ -9,24 +9,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
// Load .env file into environment variables
|
|
||||||
var envPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", ".env");
|
|
||||||
if (File.Exists(envPath))
|
|
||||||
{
|
|
||||||
foreach (var line in File.ReadAllLines(envPath))
|
|
||||||
{
|
|
||||||
var trimmed = line.Trim();
|
|
||||||
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) continue;
|
|
||||||
var eq = trimmed.IndexOf('=');
|
|
||||||
if (eq > 0)
|
|
||||||
{
|
|
||||||
var key = trimmed[..eq].Trim();
|
|
||||||
var value = trimmed[(eq + 1)..].Trim();
|
|
||||||
Environment.SetEnvironmentVariable(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
|
|||||||
@@ -7,19 +7,19 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Default": ""
|
"Default": "Host=localhost;Port=5432;Database=health_manager;Username=postgres;Password=postgres123"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Secret": "",
|
"Secret": "health-manager-jwt-secret-key-2026-super-secure-long-enough!",
|
||||||
"Issuer": "HealthManager",
|
"Issuer": "HealthManager",
|
||||||
"Audience": "HealthManagerApp"
|
"Audience": "HealthManagerApp"
|
||||||
},
|
},
|
||||||
"Redis": {
|
"Redis": {
|
||||||
"Connection": ""
|
"Connection": "localhost:6379"
|
||||||
},
|
},
|
||||||
"MinIO": {
|
"MinIO": {
|
||||||
"Endpoint": "",
|
"Endpoint": "localhost:9000",
|
||||||
"AccessKey": "",
|
"AccessKey": "minioadmin",
|
||||||
"SecretKey": ""
|
"SecretKey": "minioadmin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function ChatPage() {
|
|||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const connRef = useRef<HubConnection | null>(null);
|
const connRef = useRef<HubConnection | null>(null);
|
||||||
|
|
||||||
|
// Load initial messages via HTTP
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
api.get<Message[]>(`/api/consultations/${id}/messages`)
|
api.get<Message[]>(`/api/consultations/${id}/messages`)
|
||||||
@@ -32,11 +33,12 @@ export function ChatPage() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
// Set up SignalR connection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
const conn = new HubConnectionBuilder()
|
const conn = new HubConnectionBuilder()
|
||||||
.withUrl(`${import.meta.env.VITE_API_URL}/hubs/chat`, {
|
.withUrl('http://localhost:5000/hubs/chat', {
|
||||||
accessTokenFactory: () => getToken(),
|
accessTokenFactory: () => getToken(),
|
||||||
})
|
})
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
@@ -44,6 +46,7 @@ export function ChatPage() {
|
|||||||
|
|
||||||
conn.on('ReceiveMessage', (msg: Message) => {
|
conn.on('ReceiveMessage', (msg: Message) => {
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
|
// Dedup — guard against reconnection replay
|
||||||
if (prev.some((m) => m.id === msg.id)) return prev;
|
if (prev.some((m) => m.id === msg.id)) return prev;
|
||||||
return [...prev, msg];
|
return [...prev, msg];
|
||||||
});
|
});
|
||||||
@@ -70,6 +73,7 @@ export function ChatPage() {
|
|||||||
};
|
};
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
// Auto-scroll on new messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
@@ -85,37 +89,31 @@ export function ChatPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 0px)' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 0px)' }}>
|
||||||
<div style={{
|
<div style={{ padding: '14px 20px', background: '#fff', borderBottom: '1px solid #eee', fontSize: 15, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
padding: '15px 24px', background: '#fff', borderBottom: '1px solid #F0F2F5',
|
|
||||||
fontSize: 15, fontWeight: 600, color: '#1A1D28', display: 'flex', alignItems: 'center', gap: 8,
|
|
||||||
boxShadow: '0 1px 4px rgba(0,0,0,0.03)',
|
|
||||||
}}>
|
|
||||||
在线问诊
|
在线问诊
|
||||||
<span style={{
|
<span style={{
|
||||||
width: 8, height: 8, borderRadius: '50%',
|
width: 8, height: 8, borderRadius: '50%',
|
||||||
background: connected ? '#20C997' : '#C0C5D2',
|
background: connected ? '#4caf50' : '#ccc',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1, overflow: 'auto', padding: 24, background: '#F5F7FB' }}>
|
<div style={{ flex: 1, overflow: 'auto', padding: 20, background: '#fafafa' }}>
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<div key={msg.id} style={{
|
<div key={msg.id} style={{
|
||||||
display: 'flex', justifyContent: msg.senderRole === 'doctor' ? 'flex-end' : 'flex-start',
|
display: 'flex', justifyContent: msg.senderRole === 'doctor' ? 'flex-end' : 'flex-start',
|
||||||
marginBottom: 14,
|
marginBottom: 12,
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: '70%', padding: '12px 16px', borderRadius: 14, fontSize: 14,
|
maxWidth: '70%', padding: '10px 14px', borderRadius: 12, fontSize: 14,
|
||||||
background: msg.senderRole === 'doctor'
|
background: msg.senderRole === 'doctor' ? '#1976d2' : '#fff',
|
||||||
? 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)'
|
color: msg.senderRole === 'doctor' ? '#fff' : '#333',
|
||||||
: '#fff',
|
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||||
color: msg.senderRole === 'doctor' ? '#fff' : '#1A1D28',
|
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
|
||||||
}}>
|
}}>
|
||||||
<div>{msg.content}</div>
|
<div>{msg.content}</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 10, marginTop: 6, textAlign: 'right',
|
fontSize: 10, marginTop: 4, textAlign: 'right',
|
||||||
opacity: 0.65,
|
opacity: 0.7,
|
||||||
}}>
|
}}>
|
||||||
{msg.createdAt?.split('T')[1]?.slice(0, 5)}
|
{msg.createdAt?.split('T')[1]?.slice(0, 5)}
|
||||||
</div>
|
</div>
|
||||||
@@ -125,23 +123,14 @@ export function ChatPage() {
|
|||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={{ padding: '12px 20px', background: '#fff', borderTop: '1px solid #eee', display: 'flex', gap: 12 }}>
|
||||||
padding: '14px 24px', background: '#fff', borderTop: '1px solid #F0F2F5',
|
|
||||||
display: 'flex', gap: 12, boxShadow: '0 -1px 4px rgba(0,0,0,0.03)',
|
|
||||||
}}>
|
|
||||||
<input value={input} onChange={(e) => setInput(e.target.value)}
|
<input value={input} onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||||
placeholder="输入回复..."
|
placeholder="输入回复..."
|
||||||
style={{
|
style={{ flex: 1, padding: '10px 14px', border: '1px solid #ddd', borderRadius: 20, fontSize: 14 }} />
|
||||||
flex: 1, padding: '11px 16px', border: '1.5px solid #E1E5ED', borderRadius: 24,
|
|
||||||
fontSize: 14, outline: 'none',
|
|
||||||
}}
|
|
||||||
onFocus={(e) => e.currentTarget.style.borderColor = '#4F6EF7'}
|
|
||||||
onBlur={(e) => e.currentTarget.style.borderColor = '#E1E5ED'} />
|
|
||||||
<button onClick={handleSend} style={{
|
<button onClick={handleSend} style={{
|
||||||
padding: '11px 24px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
|
padding: '10px 24px', background: '#1976d2', color: '#fff',
|
||||||
border: 'none', borderRadius: 24, fontSize: 14, fontWeight: 600,
|
border: 'none', borderRadius: 20, fontSize: 14,
|
||||||
boxShadow: '0 4px 14px rgba(79,110,247,0.3)',
|
|
||||||
}}>
|
}}>
|
||||||
发送
|
发送
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function DashboardPage() {
|
|||||||
const [patients, consultations, reports, followUps] = await Promise.all([
|
const [patients, consultations, reports, followUps] = await Promise.all([
|
||||||
api.get<RawPatient[]>('/api/patients'),
|
api.get<RawPatient[]>('/api/patients'),
|
||||||
api.get<RawConsultation[]>('/api/consultations'),
|
api.get<RawConsultation[]>('/api/consultations'),
|
||||||
api.get<RawReport[]>('/api/reports?status=pending'),
|
api.get<RawReport[]>('/api/reports/pending'),
|
||||||
api.get<RawFollowUp[]>('/api/follow-ups'),
|
api.get<RawFollowUp[]>('/api/follow-ups'),
|
||||||
]);
|
]);
|
||||||
setStats({
|
setStats({
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export function FollowUpListPage() {
|
|||||||
|
|
||||||
const statusLabel = (s: string) => {
|
const statusLabel = (s: string) => {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case 'pending': return { text: '待随访', color: '#F59E0B', bg: '#FFF8E6' };
|
case 'upcoming': return { text: '待随访', color: '#F59E0B', bg: '#FFF8E6' };
|
||||||
case 'completed': return { text: '已完成', color: '#20C997', bg: '#E6F9F2' };
|
case 'completed': return { text: '已完成', color: '#20C997', bg: '#E6F9F2' };
|
||||||
case 'missed': return { text: '已错过', color: '#EF4444', bg: '#FEE9E9' };
|
case 'cancelled': return { text: '已取消', color: '#EF4444', bg: '#FEE9E9' };
|
||||||
default: return { text: s, color: '#9BA0B4', bg: '#F5F6F9' };
|
default: return { text: s, color: '#9BA0B4', bg: '#F5F6F9' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,54 +56,50 @@ export function ReportDetailPage() {
|
|||||||
finally { setSubmitting(false); }
|
finally { setSubmitting(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!report) return <div style={{ padding: 28, color: '#9BA0B4' }}>加载中...</div>;
|
if (!report) return <div style={{ padding: 24 }}>加载中...</div>;
|
||||||
|
|
||||||
const isCompleted = report.status === 'completed';
|
const isCompleted = report.status === 'completed';
|
||||||
const riskMap: Record<string, { text: string; color: string }> = {
|
const riskMap: Record<string, { text: string; color: string }> = {
|
||||||
normal: { text: '正常', color: '#20C997' },
|
normal: { text: '正常', color: '#2e7d32' },
|
||||||
attention: { text: '关注', color: '#F59E0B' },
|
attention: { text: '关注', color: '#f57c00' },
|
||||||
abnormal: { text: '异常', color: '#EF4444' },
|
abnormal: { text: '异常', color: '#c62828' },
|
||||||
};
|
|
||||||
|
|
||||||
const inputStyle: React.CSSProperties = {
|
|
||||||
width: '100%', padding: '10px 14px', border: '1.5px solid #E1E5ED',
|
|
||||||
borderRadius: 10, fontSize: 13, outline: 'none', boxSizing: 'border-box', fontFamily: 'inherit',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 28 }}>
|
<div style={{ padding: 24 }}>
|
||||||
<Link to="/reports" style={{ fontSize: 13, color: '#4F6EF7', fontWeight: 500 }}>← 返回报告列表</Link>
|
<Link to="/reports" style={{ fontSize: 13, color: '#1976d2' }}>← 返回报告列表</Link>
|
||||||
|
|
||||||
<div style={{ background: '#fff', marginTop: 16, padding: 28, borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
|
<div style={{ background: '#fff', marginTop: 16, padding: 24, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>{report.title}</h2>
|
<h2 style={{ margin: 0 }}>{report.title}</h2>
|
||||||
<div style={{ marginTop: 8, fontSize: 13, color: '#9BA0B4' }}>
|
<div style={{ marginTop: 8, fontSize: 13, color: '#888' }}>
|
||||||
患者:{report.patientName || '未知'} |
|
患者:{report.patientName || '未知'} |
|
||||||
分类:{categoryMap[report.category] || report.category} |
|
分类:{categoryMap[report.category] || report.category} |
|
||||||
日期:{report.createdAt?.split('T')[0]}
|
日期:{report.createdAt?.split('T')[0]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span style={{
|
||||||
padding: '6px 14px', borderRadius: 12, fontSize: 12, fontWeight: 600,
|
padding: '4px 12px', borderRadius: 12, fontSize: 12, fontWeight: 500,
|
||||||
background: isCompleted ? '#E6F9F2' : '#FFF8E6',
|
background: isCompleted ? '#e8f5e9' : '#fff3e0',
|
||||||
color: isCompleted ? '#20C997' : '#F59E0B',
|
color: isCompleted ? '#2e7d32' : '#f57c00',
|
||||||
}}>
|
}}>
|
||||||
{isCompleted ? '已完成' : '待审核'}
|
{isCompleted ? '已完成' : '待审核'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 图片 */}
|
||||||
{report.imageUrls && report.imageUrls.length > 0 && (
|
{report.imageUrls && report.imageUrls.length > 0 && (
|
||||||
<div style={{ marginTop: 24 }}>
|
<div style={{ marginTop: 20 }}>
|
||||||
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 10, color: '#5A6072' }}>上传图片({report.imageUrls.length}张)</h4>
|
<h4 style={{ fontSize: 14, marginBottom: 8 }}>上传图片({report.imageUrls.length}张)</h4>
|
||||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||||
{report.imageUrls.map((url, i) => (
|
{report.imageUrls.map((url, i) => (
|
||||||
<div key={i} onClick={() => setLightbox(url)} style={{
|
<div key={i} onClick={() => setLightbox(url)} style={{
|
||||||
width: 120, height: 120, borderRadius: 12, overflow: 'hidden',
|
width: 120, height: 120, borderRadius: 8, overflow: 'hidden',
|
||||||
cursor: 'pointer', border: '2px solid #F0F2F5', background: '#F9FAFC',
|
cursor: 'pointer', border: '2px solid #eee', background: '#f5f5f5',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
}}>
|
}}>
|
||||||
<img src={`${import.meta.env.VITE_API_URL}${url}`} alt={`图片${i}`}
|
<img src={`http://localhost:5000${url}`} alt={`图片${i}`}
|
||||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }}
|
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }}
|
||||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -112,35 +108,37 @@ export function ReportDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 灯箱 */}
|
||||||
{lightbox && (
|
{lightbox && (
|
||||||
<div onClick={() => setLightbox(null)} style={{
|
<div onClick={() => setLightbox(null)} style={{
|
||||||
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)',
|
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 999, cursor: 'pointer',
|
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 999, cursor: 'pointer',
|
||||||
}}>
|
}}>
|
||||||
<img src={`${import.meta.env.VITE_API_URL}${lightbox}`} alt="预览" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 12 }} />
|
<img src={`http://localhost:5000${lightbox}`} alt="预览" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8 }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 已完成解读 */}
|
||||||
{isCompleted && (
|
{isCompleted && (
|
||||||
<div style={{ marginTop: 24, padding: 20, background: '#E6F9F2', borderRadius: 12 }}>
|
<div style={{ marginTop: 20, padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
|
||||||
<h4 style={{ fontSize: 15, fontWeight: 600, marginBottom: 12, color: '#20C997' }}>解读结果</h4>
|
<h4 style={{ fontSize: 14, marginBottom: 8 }}>解读结果</h4>
|
||||||
<div style={{ fontSize: 13, color: '#5A6072' }}>
|
<div style={{ fontSize: 13 }}>
|
||||||
<p style={{ margin: '6px 0' }}><strong style={{ color: '#1A1D28' }}>风险等级:</strong>
|
<p><strong>风险等级:</strong>
|
||||||
<span style={{ color: riskMap[report.riskLevel || '']?.color, fontWeight: 600 }}>
|
<span style={{ color: riskMap[report.riskLevel || '']?.color, fontWeight: 600 }}>
|
||||||
{riskMap[report.riskLevel || '']?.text || report.riskLevel || '-'}
|
{riskMap[report.riskLevel || '']?.text || report.riskLevel || '-'}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p style={{ margin: '6px 0' }}><strong style={{ color: '#1A1D28' }}>总结:</strong>{report.summary || '-'}</p>
|
<p><strong>总结:</strong>{report.summary || '-'}</p>
|
||||||
{report.suggestions && <p style={{ margin: '6px 0' }}><strong style={{ color: '#1A1D28' }}>建议:</strong>{report.suggestions}</p>}
|
{report.suggestions && <p><strong>建议:</strong>{report.suggestions}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{report.items && report.items.length > 0 && (
|
{report.items && report.items.length > 0 && (
|
||||||
<table style={{ width: '100%', marginTop: 14, borderCollapse: 'collapse', fontSize: 12 }}>
|
<table style={{ width: '100%', marginTop: 12, borderCollapse: 'collapse', fontSize: 12 }}>
|
||||||
<thead><tr style={{ textAlign: 'left', borderBottom: '2px solid #c8e6c9' }}>
|
<thead><tr style={{ textAlign: 'left', borderBottom: '2px solid #c8e6c9' }}>
|
||||||
<th style={{ padding: '6px 8px', color: '#5A6072' }}>检查项目</th>
|
<th style={{ padding: '6px 8px' }}>检查项目</th>
|
||||||
<th style={{ padding: '6px 8px', color: '#5A6072' }}>结果</th>
|
<th style={{ padding: '6px 8px' }}>结果</th>
|
||||||
<th style={{ padding: '6px 8px', color: '#5A6072' }}>参考范围</th>
|
<th style={{ padding: '6px 8px' }}>参考范围</th>
|
||||||
<th style={{ padding: '6px 8px', color: '#5A6072' }}>是否异常</th>
|
<th style={{ padding: '6px 8px' }}>是否异常</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{report.items.map((item) => (
|
{report.items.map((item) => (
|
||||||
@@ -148,7 +146,7 @@ export function ReportDetailPage() {
|
|||||||
<td style={{ padding: '6px 8px' }}>{item.itemName}</td>
|
<td style={{ padding: '6px 8px' }}>{item.itemName}</td>
|
||||||
<td style={{ padding: '6px 8px' }}>{item.resultValue} {item.unit || ''}</td>
|
<td style={{ padding: '6px 8px' }}>{item.resultValue} {item.unit || ''}</td>
|
||||||
<td style={{ padding: '6px 8px' }}>{item.referenceRange || '-'}</td>
|
<td style={{ padding: '6px 8px' }}>{item.referenceRange || '-'}</td>
|
||||||
<td style={{ padding: '6px 8px', color: item.isAbnormal ? '#EF4444' : '#20C997', fontWeight: 600 }}>
|
<td style={{ padding: '6px 8px', color: item.isAbnormal ? '#c62828' : '#2e7d32', fontWeight: 500 }}>
|
||||||
{item.isAbnormal ? '是' : '否'}
|
{item.isAbnormal ? '是' : '否'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -159,66 +157,68 @@ export function ReportDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 解读表单 */}
|
||||||
{!isCompleted && (
|
{!isCompleted && (
|
||||||
<div style={{ marginTop: 28, borderTop: '1px solid #F0F2F5', paddingTop: 24 }}>
|
<div style={{ marginTop: 24, borderTop: '1px solid #eee', paddingTop: 20 }}>
|
||||||
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 18, color: '#1A1D28' }}>医生解读</h3>
|
<h3 style={{ fontSize: 15, marginBottom: 16 }}>医生解读</h3>
|
||||||
|
|
||||||
<div style={{ marginBottom: 14 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5 }}>解读总结</label>
|
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>解读总结</label>
|
||||||
<textarea value={summary} onChange={(e) => setSummary(e.target.value)}
|
<textarea value={summary} onChange={(e) => setSummary(e.target.value)}
|
||||||
placeholder="请输入您的专业解读总结..."
|
placeholder="请输入您的专业解读总结..."
|
||||||
rows={4}
|
rows={4}
|
||||||
style={{ ...inputStyle, resize: 'vertical' }} />
|
style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, resize: 'vertical', fontFamily: 'inherit', boxSizing: 'border-box' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 16, marginBottom: 14 }}>
|
<div style={{ display: 'flex', gap: 16, marginBottom: 12 }}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5 }}>风险等级</label>
|
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>风险等级</label>
|
||||||
<select value={riskLevel} onChange={(e) => setRiskLevel(e.target.value)} style={inputStyle}>
|
<select value={riskLevel} onChange={(e) => setRiskLevel(e.target.value)}
|
||||||
|
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit' }}>
|
||||||
<option value="normal">正常</option>
|
<option value="normal">正常</option>
|
||||||
<option value="attention">需关注</option>
|
<option value="attention">需关注</option>
|
||||||
<option value="abnormal">异常</option>
|
<option value="abnormal">异常</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 5 }}>医生建议</label>
|
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>医生建议</label>
|
||||||
<input value={suggestions} onChange={(e) => setSuggestions(e.target.value)}
|
<input value={suggestions} onChange={(e) => setSuggestions(e.target.value)}
|
||||||
placeholder="如:继续当前用药方案"
|
placeholder="如:继续当前用药方案"
|
||||||
style={inputStyle} />
|
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit', boxSizing: 'border-box' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 14 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: '#5A6072', marginBottom: 8 }}>检查项目</label>
|
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>检查项目</label>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
|
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 6, alignItems: 'center' }}>
|
||||||
<input placeholder="项目名称" value={item.itemName} onChange={(e) => updateItem(i, 'itemName', e.target.value)}
|
<input placeholder="项目名称" value={item.itemName} onChange={(e) => updateItem(i, 'itemName', e.target.value)}
|
||||||
style={{ flex: 2, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
|
style={{ flex: 2, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||||
<input placeholder="结果" value={item.resultValue} onChange={(e) => updateItem(i, 'resultValue', e.target.value)}
|
<input placeholder="结果" value={item.resultValue} onChange={(e) => updateItem(i, 'resultValue', e.target.value)}
|
||||||
style={{ flex: 1, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
|
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||||
<input placeholder="单位" value={item.unit} onChange={(e) => updateItem(i, 'unit', e.target.value)}
|
<input placeholder="单位" value={item.unit} onChange={(e) => updateItem(i, 'unit', e.target.value)}
|
||||||
style={{ width: 70, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
|
style={{ width: 70, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||||
<input placeholder="参考范围" value={item.referenceRange} onChange={(e) => updateItem(i, 'referenceRange', e.target.value)}
|
<input placeholder="参考范围" value={item.referenceRange} onChange={(e) => updateItem(i, 'referenceRange', e.target.value)}
|
||||||
style={{ flex: 1, padding: '8px 12px', border: '1.5px solid #E1E5ED', borderRadius: 8, fontSize: 12, outline: 'none' }} />
|
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||||
<label style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4, color: '#5A6072' }}>
|
<label style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 3 }}>
|
||||||
<input type="checkbox" checked={item.isAbnormal} onChange={(e) => updateItem(i, 'isAbnormal', e.target.checked)} />
|
<input type="checkbox" checked={item.isAbnormal} onChange={(e) => updateItem(i, 'isAbnormal', e.target.checked)} />
|
||||||
异常
|
异常
|
||||||
</label>
|
</label>
|
||||||
<button onClick={() => removeItem(i)}
|
<button onClick={() => removeItem(i)}
|
||||||
style={{ background: 'none', border: 'none', color: '#EF4444', cursor: 'pointer', fontSize: 18, fontWeight: 700 }}
|
style={{ background: 'none', border: 'none', color: '#c62828', cursor: 'pointer', fontSize: 16 }}
|
||||||
disabled={items.length <= 1}>×</button>
|
disabled={items.length <= 1}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button onClick={addItem} style={{
|
<button onClick={addItem} style={{
|
||||||
padding: '6px 14px', border: '1.5px dashed #4F6EF7', borderRadius: 8,
|
padding: '4px 12px', border: '1px dashed #1976d2', borderRadius: 4,
|
||||||
background: 'none', color: '#4F6EF7', cursor: 'pointer', fontSize: 12, fontWeight: 500,
|
background: 'none', color: '#1976d2', cursor: 'pointer', fontSize: 12,
|
||||||
}}>+ 添加项目</button>
|
}}>+ 添加项目</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={handleInterpret} disabled={submitting} style={{
|
<button onClick={handleInterpret} disabled={submitting} style={{
|
||||||
padding: '11px 32px', background: 'linear-gradient(135deg, #4F6EF7 0%, #6C8AFF 100%)', color: '#fff',
|
padding: '10px 28px', background: '#1976d2', color: '#fff',
|
||||||
border: 'none', borderRadius: 10, fontSize: 14, cursor: 'pointer', fontWeight: 600,
|
border: 'none', borderRadius: 6, fontSize: 14, cursor: 'pointer',
|
||||||
opacity: submitting ? 0.7 : 1, marginTop: 8, boxShadow: '0 4px 16px rgba(79,110,247,0.25)',
|
opacity: submitting ? 0.7 : 1, marginTop: 8,
|
||||||
}}>
|
}}>
|
||||||
{submitting ? '提交中...' : '提交解读'}
|
{submitting ? '提交中...' : '提交解读'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { api } from '../../services/api-client';
|
|||||||
|
|
||||||
interface RawReportItem {
|
interface RawReportItem {
|
||||||
id: string; patientId: string; patientName?: string;
|
id: string; patientId: string; patientName?: string;
|
||||||
title: string; category: string; status: string; createdAt: string;
|
title: string; category: string; status: string; uploadedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportListPage() {
|
export function ReportListPage() {
|
||||||
@@ -52,7 +52,7 @@ export function ReportListPage() {
|
|||||||
{s.text}
|
{s.text}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 20px', color: '#9BA0B4' }}>{r.createdAt?.split('T')[0]}</td>
|
<td style={{ padding: '12px 20px', color: '#9BA0B4' }}>{r.uploadedAt?.split('T')[0]}</td>
|
||||||
<td style={{ padding: '12px 20px' }}>
|
<td style={{ padding: '12px 20px' }}>
|
||||||
<Link to={`/reports/${r.id}`} style={{
|
<Link to={`/reports/${r.id}`} style={{
|
||||||
color: '#4F6EF7', fontSize: 12, fontWeight: 600,
|
color: '#4F6EF7', fontSize: 12, fontWeight: 600,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ interface ApiResponse<T> {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
const BASE_URL = 'http://localhost:5000';
|
||||||
|
|
||||||
// Endpoints that should NEVER include auth token
|
// Endpoints that should NEVER include auth token
|
||||||
const PUBLIC_ENDPOINTS = ['/api/auth/login', '/api/auth/register', '/api/auth/send-sms', '/api/auth/refresh'];
|
const PUBLIC_ENDPOINTS = ['/api/auth/login', '/api/auth/register', '/api/auth/send-sms', '/api/auth/refresh'];
|
||||||
|
|||||||
9
frontend-doctor/src/vite-env.d.ts
vendored
9
frontend-doctor/src/vite-env.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly VITE_API_URL: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv;
|
|
||||||
}
|
|
||||||
@@ -62,7 +62,7 @@ export function ChatPage() {
|
|||||||
|
|
||||||
// Set up SignalR connection
|
// Set up SignalR connection
|
||||||
const conn = new HubConnectionBuilder()
|
const conn = new HubConnectionBuilder()
|
||||||
.withUrl(`${import.meta.env.VITE_API_URL}/hubs/chat`, {
|
.withUrl('http://localhost:5000/hubs/chat', {
|
||||||
accessTokenFactory: () => getToken(),
|
accessTokenFactory: () => getToken(),
|
||||||
})
|
})
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function ReportDetailPage() {
|
|||||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8 }}>报告图片</div>
|
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8 }}>报告图片</div>
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
{report.imageUrls.map((url, i) => (
|
{report.imageUrls.map((url, i) => (
|
||||||
<img key={i} src={`${import.meta.env.VITE_API_URL}${url}`} alt="report"
|
<img key={i} src={`http://localhost:5000${url}`} alt="report"
|
||||||
style={{ width: 80, height: 80, borderRadius: 8, objectFit: 'cover', border: '1px solid #eee' }} />
|
style={{ width: 80, height: 80, borderRadius: 8, objectFit: 'cover', border: '1px solid #eee' }} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function ReportUploadPage() {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const token = JSON.parse(localStorage.getItem('hrt_auth') || '{}')?.state?.token;
|
const token = JSON.parse(localStorage.getItem('hrt_auth') || '{}')?.state?.token;
|
||||||
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/files/upload`, {
|
const res = await fetch('http://localhost:5000/api/files/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ApiResponse<T> {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
const BASE_URL = 'http://localhost:5000';
|
||||||
|
|
||||||
// Endpoints that should NEVER include auth token
|
// Endpoints that should NEVER include auth token
|
||||||
const PUBLIC_ENDPOINTS = ['/api/auth/login', '/api/auth/register', '/api/auth/send-sms', '/api/auth/refresh'];
|
const PUBLIC_ENDPOINTS = ['/api/auth/login', '/api/auth/register', '/api/auth/send-sms', '/api/auth/refresh'];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface RawReport {
|
|||||||
imageUrls: string[];
|
imageUrls: string[];
|
||||||
status: string;
|
status: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
createdAt: string;
|
uploadedAt: string;
|
||||||
interpretedAt?: string;
|
interpretedAt?: string;
|
||||||
interpretedBy?: string;
|
interpretedBy?: string;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ function mapReport(r: RawReport): Report {
|
|||||||
userId: r.patientId,
|
userId: r.patientId,
|
||||||
title: r.title,
|
title: r.title,
|
||||||
imageUrls: r.imageUrls,
|
imageUrls: r.imageUrls,
|
||||||
uploadAt: r.createdAt,
|
uploadAt: r.uploadedAt,
|
||||||
status: r.status as Report['status'],
|
status: r.status as Report['status'],
|
||||||
category: r.category,
|
category: r.category,
|
||||||
result,
|
result,
|
||||||
|
|||||||
9
frontend-patient/src/vite-env.d.ts
vendored
9
frontend-patient/src/vite-env.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly VITE_API_URL: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user