commit 435af55c4a955a100d6fd9467b778981f6b5e09e Author: MingNian <1281442923@qq.com> Date: Wed May 20 16:18:56 2026 +0800 Initial commit: HealthManager full-stack health management platform Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR Frontend patient: React 19 + TypeScript + Vite (mobile H5) Frontend doctor: React 19 + TypeScript + Vite (desktop web) diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..1a64841 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "健康管家 Web Demo", + "runtimeExecutable": "cmd.exe", + "runtimeArgs": ["/c", "D:\\nodejs\\npm.cmd", "run", "dev"], + "port": 5175 + } + ] +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6349d3e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(winget install *)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a35e4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +bin/ +obj/ + +# Data (large files) +data/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vs/ +.vscode/ +.idea/ +*.suo +*.user +*.userosscache +*.sln.docstates + +# OS +.DS_Store +Thumbs.db +*.swp +*.swo + +# Logs +*.log +pg.log + +# Temp +*.tmp +tmp/ diff --git a/backend/HealthManager.slnx b/backend/HealthManager.slnx new file mode 100644 index 0000000..d59aebc --- /dev/null +++ b/backend/HealthManager.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/backend/src/HealthManager.Application/DTOs/AuthDtos.cs b/backend/src/HealthManager.Application/DTOs/AuthDtos.cs new file mode 100644 index 0000000..a2f3db2 --- /dev/null +++ b/backend/src/HealthManager.Application/DTOs/AuthDtos.cs @@ -0,0 +1,19 @@ +namespace HealthManager.Application.DTOs.Auth; + +public record LoginRequest(string Phone, string SmsCode); + +public record RegisterRequest(string Phone, string SmsCode, string Name); + +public record SendSmsRequest(string Phone); + +public record AuthResponse(Guid UserId, string Name, string Role, string AccessToken, string RefreshToken); + +public record UserProfileResponse( + Guid Id, string Name, string Phone, string Role, + string? Gender, DateOnly? Birthday, decimal? HeightCm, decimal? WeightKg, + List? MedicalHistory, DateOnly? StentDate, string? StentType, + string? Department, string? Title, List? Specialty, string? Introduction); + +public record UpdateProfileRequest( + string? Name, string? Gender, DateOnly? Birthday, + decimal? HeightCm, decimal? WeightKg, List? MedicalHistory); diff --git a/backend/src/HealthManager.Application/DTOs/ConsultationDtos.cs b/backend/src/HealthManager.Application/DTOs/ConsultationDtos.cs new file mode 100644 index 0000000..598797a --- /dev/null +++ b/backend/src/HealthManager.Application/DTOs/ConsultationDtos.cs @@ -0,0 +1,16 @@ +namespace HealthManager.Application.DTOs.Consultation; + +public record ConsultationCreateRequest(Guid DoctorId, string Subject); + +public record ConsultationResponse( + Guid Id, Guid PatientId, Guid DoctorId, + string PatientName, string DoctorName, + string? Subject, string Status, DateTime StartedAt, + DateTime? ClosedAt, string? Summary); + +public record SendMessageRequest(string Content, string ContentType = "text", string? ImageUrl = null); + +public record MessageResponse( + Guid Id, Guid SenderId, string SenderRole, + string ContentType, string Content, + string? ImageUrl, bool IsRead, DateTime CreatedAt); diff --git a/backend/src/HealthManager.Application/DTOs/FollowUpDtos.cs b/backend/src/HealthManager.Application/DTOs/FollowUpDtos.cs new file mode 100644 index 0000000..0fe9652 --- /dev/null +++ b/backend/src/HealthManager.Application/DTOs/FollowUpDtos.cs @@ -0,0 +1,12 @@ +namespace HealthManager.Application.DTOs.FollowUp; + +public record FollowUpCreateRequest( + string Title, string? Description, DateTime ScheduledAt, bool ReminderEnabled = true); + +public record FollowUpUpdateRequest( + string? Title, string? Description, DateTime? ScheduledAt, string? Status, string? Notes); + +public record FollowUpResponse( + Guid Id, Guid PatientId, string PatientName, Guid? DoctorId, string? DoctorName, + string Title, string? Description, DateTime ScheduledAt, string Status, + string? Notes, bool ReminderEnabled, DateTime CreatedAt); diff --git a/backend/src/HealthManager.Application/DTOs/HealthDtos.cs b/backend/src/HealthManager.Application/DTOs/HealthDtos.cs new file mode 100644 index 0000000..930f7c4 --- /dev/null +++ b/backend/src/HealthManager.Application/DTOs/HealthDtos.cs @@ -0,0 +1,14 @@ +namespace HealthManager.Application.DTOs.Health; + +public record HealthRecordCreateRequest( + string Type, string ValueJson, string Unit, DateTime RecordedAt, string? Notes); + +public record HealthRecordResponse( + Guid Id, string Type, string ValueJson, string Unit, + DateTime RecordedAt, string Source, string? Notes, DateTime CreatedAt); + +public record HealthStatsResponse( + string Type, string Unit, + HealthRecordResponse? Latest, + decimal? WeekAvg, decimal? MonthAvg, + string Trend); // up, down, stable diff --git a/backend/src/HealthManager.Application/DTOs/MedicationDtos.cs b/backend/src/HealthManager.Application/DTOs/MedicationDtos.cs new file mode 100644 index 0000000..a8c7978 --- /dev/null +++ b/backend/src/HealthManager.Application/DTOs/MedicationDtos.cs @@ -0,0 +1,18 @@ +namespace HealthManager.Application.DTOs.Medication; + +public record MedicationCreateRequest( + string DrugName, string Dosage, string Frequency, + List TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes); + +public record MedicationResponse( + Guid Id, Guid UserId, string DrugName, string Dosage, + string Frequency, List TimeSlots, + DateOnly StartDate, DateOnly? EndDate, string? Notes, + string Status, DateTime CreatedAt); + +public record MedicationRecordResponse( + Guid Id, Guid MedicationId, string TimeSlot, DateTime? TakenAt, + bool IsTaken, string? SkippedReason, DateTime CreatedAt); + +public record AdherenceResponse( + Guid MedicationId, string DrugName, decimal Rate, int TakenDays, int TotalDays); diff --git a/backend/src/HealthManager.Application/DTOs/ReportDtos.cs b/backend/src/HealthManager.Application/DTOs/ReportDtos.cs new file mode 100644 index 0000000..6c52fdf --- /dev/null +++ b/backend/src/HealthManager.Application/DTOs/ReportDtos.cs @@ -0,0 +1,15 @@ +namespace HealthManager.Application.DTOs.Report; + +public record ReportUploadRequest(string Title, string Category, List ImageUrls); + +public record ReportInterpretRequest(string Summary, List Items, string RiskLevel, string? Suggestions); + +public record ReportResponse( + Guid Id, Guid PatientId, string PatientName, string? DoctorName, + string Title, string Category, List ImageUrls, + string Status, string? RiskLevel, string? Summary, string? Suggestions, + List Items, DateTime UploadedAt, DateTime? CompletedAt); + +public record ReportItemDto( + string ItemName, string ResultValue, string? Unit, + string? ReferenceRange, bool IsAbnormal); diff --git a/backend/src/HealthManager.Application/HealthManager.Application.csproj b/backend/src/HealthManager.Application/HealthManager.Application.csproj new file mode 100644 index 0000000..746a4f8 --- /dev/null +++ b/backend/src/HealthManager.Application/HealthManager.Application.csproj @@ -0,0 +1,14 @@ + + + + + + + + + net10.0 + enable + enable + + + diff --git a/backend/src/HealthManager.Application/Services/AuthService.cs b/backend/src/HealthManager.Application/Services/AuthService.cs new file mode 100644 index 0000000..2b07d9a --- /dev/null +++ b/backend/src/HealthManager.Application/Services/AuthService.cs @@ -0,0 +1,52 @@ +using System.Security.Cryptography; +using System.Text; +using HealthManager.Domain.Entities; +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class AuthService(AppDbContext db) +{ + public async Task GetUserByPhoneAsync(string phone) + { + return await db.Users.FirstOrDefaultAsync(u => u.Phone == phone && !u.IsDeleted); + } + + public async Task GetRefreshTokenAsync(string token) + { + return await db.RefreshTokens + .Include(rt => rt.User) + .FirstOrDefaultAsync(rt => rt.Token == token && rt.RevokedAt == null && rt.ExpiresAt > DateTime.UtcNow); + } + + public async Task RevokeRefreshTokenAsync(Guid userId) + { + var tokens = await db.RefreshTokens + .Where(rt => rt.UserId == userId && rt.RevokedAt == null) + .ToListAsync(); + foreach (var t in tokens) + t.RevokedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + public async Task SaveRefreshTokenAsync(Guid userId, string token, DateTime expiresAt) + { + db.RefreshTokens.Add(new RefreshToken + { + UserId = userId, + Token = token, + ExpiresAt = expiresAt, + }); + await db.SaveChangesAsync(); + } + + public static string HashPassword(string password) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(password)); + return Convert.ToHexStringLower(bytes); + } + + public static bool VerifyPassword(string password, string hash) + => HashPassword(password) == hash; +} diff --git a/backend/src/HealthManager.Application/Services/ConsultationService.cs b/backend/src/HealthManager.Application/Services/ConsultationService.cs new file mode 100644 index 0000000..d8fcc17 --- /dev/null +++ b/backend/src/HealthManager.Application/Services/ConsultationService.cs @@ -0,0 +1,73 @@ +using HealthManager.Domain.Entities; +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class ConsultationService(AppDbContext db) +{ + public async Task> GetPatientConsultationsAsync(Guid patientId) + => await db.Consultations + .Include(c => c.Doctor) + .Where(c => c.PatientId == patientId) + .OrderByDescending(c => c.StartedAt) + .ToListAsync(); + + public async Task> GetDoctorConsultationsAsync(Guid doctorId) + => await db.Consultations + .Include(c => c.Patient) + .Where(c => c.DoctorId == doctorId) + .OrderByDescending(c => c.StartedAt) + .ToListAsync(); + + public async Task GetByIdAsync(Guid id) + => await db.Consultations + .Include(c => c.Patient) + .Include(c => c.Doctor) + .FirstOrDefaultAsync(c => c.Id == id); + + public async Task StartAsync(Guid patientId, Guid doctorId, string subject) + { + var consultation = new Consultation + { + PatientId = patientId, + DoctorId = doctorId, + Subject = subject, + }; + db.Consultations.Add(consultation); + await db.SaveChangesAsync(); + return consultation; + } + + public async Task> GetMessagesAsync(Guid consultationId) + => await db.ConsultationMessages + .Include(m => m.Sender) + .Where(m => m.ConsultationId == consultationId) + .OrderBy(m => m.CreatedAt) + .ToListAsync(); + + public async Task SendMessageAsync(Guid consultationId, Guid senderId, string senderRole, + string content, string contentType = "text", string? imageUrl = null) + { + var message = new ConsultationMessage + { + ConsultationId = consultationId, + SenderId = senderId, + SenderRole = senderRole, + Content = content, + ContentType = contentType, + ImageUrl = imageUrl, + }; + db.ConsultationMessages.Add(message); + await db.SaveChangesAsync(); + return message; + } + + public async Task> GetDoctorsAsync(string? department = null) + { + var query = db.Users.Where(u => u.Role == "doctor" && u.IsAvailable && !u.IsDeleted); + if (!string.IsNullOrEmpty(department)) + query = query.Where(u => u.Department == department); + return await query.ToListAsync(); + } +} diff --git a/backend/src/HealthManager.Application/Services/FollowUpService.cs b/backend/src/HealthManager.Application/Services/FollowUpService.cs new file mode 100644 index 0000000..5376a95 --- /dev/null +++ b/backend/src/HealthManager.Application/Services/FollowUpService.cs @@ -0,0 +1,62 @@ +using HealthManager.Domain.Entities; +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class FollowUpService(AppDbContext db) +{ + public async Task> GetPatientFollowUpsAsync(Guid patientId) + => await db.FollowUps + .Include(f => f.Doctor) + .Where(f => f.PatientId == patientId) + .OrderBy(f => f.ScheduledAt) + .ToListAsync(); + + public async Task> GetDoctorFollowUpsAsync(Guid doctorId) + => await db.FollowUps + .Include(f => f.Patient) + .Where(f => f.DoctorId == doctorId) + .OrderBy(f => f.ScheduledAt) + .ToListAsync(); + + public async Task AddAsync(Guid patientId, string title, string? description, DateTime scheduledAt, bool reminderEnabled, Guid? doctorId = null) + { + var followUp = new FollowUp + { + PatientId = patientId, + DoctorId = doctorId, + Title = title, + Description = description, + ScheduledAt = DateTime.SpecifyKind(scheduledAt, DateTimeKind.Utc), + ReminderEnabled = reminderEnabled, + }; + db.FollowUps.Add(followUp); + await db.SaveChangesAsync(); + return followUp; + } + + public async Task GetByIdAsync(Guid id) + => await db.FollowUps + .Include(f => f.Patient) + .Include(f => f.Doctor) + .FirstOrDefaultAsync(f => f.Id == id); + + public async Task UpdateAsync(Guid id, Guid doctorId, string? title, string? description, + DateTime? scheduledAt, string? status, string? notes) + { + var followUp = await db.FollowUps.FindAsync(id); + if (followUp == null) return null; + + if (title != null) followUp.Title = title; + if (description != null) followUp.Description = description; + if (scheduledAt.HasValue) followUp.ScheduledAt = DateTime.SpecifyKind(scheduledAt.Value, DateTimeKind.Utc); + if (status != null) followUp.Status = status; + if (notes != null) followUp.Notes = notes; + followUp.DoctorId = doctorId; + followUp.UpdatedAt = DateTime.UtcNow; + + await db.SaveChangesAsync(); + return followUp; + } +} diff --git a/backend/src/HealthManager.Application/Services/HealthService.cs b/backend/src/HealthManager.Application/Services/HealthService.cs new file mode 100644 index 0000000..d1c1ad5 --- /dev/null +++ b/backend/src/HealthManager.Application/Services/HealthService.cs @@ -0,0 +1,114 @@ +using System.Text.Json; +using HealthManager.Domain.Entities; +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class HealthService(AppDbContext db) +{ + public async Task> GetRecordsAsync(Guid userId, string? type, int days = 30) + { + var cutoff = DateTime.UtcNow.AddDays(-days); + var query = db.HealthRecords.Where(hr => hr.UserId == userId && hr.RecordedAt >= cutoff); + + if (!string.IsNullOrEmpty(type)) + query = query.Where(hr => hr.Type == type); + + return await query.OrderByDescending(hr => hr.RecordedAt).ToListAsync(); + } + + public async Task GetLatestAsync(Guid userId, string type) + { + return await db.HealthRecords + .Where(hr => hr.UserId == userId && hr.Type == type) + .OrderByDescending(hr => hr.RecordedAt) + .FirstOrDefaultAsync(); + } + + public async Task AddRecordAsync(Guid userId, string type, string valueJson, string unit, DateTime recordedAt, string? notes) + { + var record = new HealthRecord + { + UserId = userId, + Type = type, + Value = JsonDocument.Parse(valueJson), + Unit = unit, + RecordedAt = DateTime.SpecifyKind(recordedAt, DateTimeKind.Utc), + Notes = notes, + Source = "manual", + }; + db.HealthRecords.Add(record); + await db.SaveChangesAsync(); + return record; + } + + public async Task> GetStatsAsync(Guid userId) + { + var types = new[] { "blood_pressure", "heart_rate", "blood_sugar", "spo2", "weight", "steps" }; + var result = new Dictionary(); + + foreach (var type in types) + { + var latest = await GetLatestAsync(userId, type); + var records = await db.HealthRecords + .Where(hr => hr.UserId == userId && hr.Type == type) + .OrderByDescending(hr => hr.RecordedAt) + .ToListAsync(); + + // Calculate trend from last 7 days vs previous 7 days + var now = DateTime.UtcNow; + var weekAgo = now.AddDays(-7); + var twoWeeksAgo = now.AddDays(-14); + + var recentValues = records.Where(r => r.RecordedAt >= weekAgo).ToList(); + var olderValues = records.Where(r => r.RecordedAt >= twoWeeksAgo && r.RecordedAt < weekAgo).ToList(); + + var recentAvg = CalculateAvg(recentValues); + var olderAvg = CalculateAvg(olderValues); + + string trend = "stable"; + if (olderAvg > 0 && recentAvg > 0) + { + if (recentAvg > olderAvg * 1.03m) trend = "up"; + else if (recentAvg < olderAvg * 0.97m) trend = "down"; + } + + result[type] = new + { + Latest = latest, + WeekAvg = recentAvg > 0 ? recentAvg : (decimal?)null, + MonthAvg = records.Count > 0 ? CalculateAvg(records) : (decimal?)null, + Trend = trend, + }; + } + + return result; + } + + private static decimal CalculateAvg(List records) + { + if (records.Count == 0) return 0; + + var sum = 0m; + var count = 0; + foreach (var r in records) + { + // Blood pressure: { systolic, diastolic, pulse } + if (r.Value.RootElement.TryGetProperty("systolic", out var sys) && + r.Value.RootElement.TryGetProperty("diastolic", out var dia)) + { + sum += sys.GetDecimal(); // Use systolic for trend + count++; + } + // All other types: { value } — heart_rate, blood_sugar, spo2, weight, steps + else if (r.Value.RootElement.TryGetProperty("value", out var val)) + { + sum += val.GetDecimal(); + count++; + } + } + + return count > 0 ? sum / count : 0; + } +} diff --git a/backend/src/HealthManager.Application/Services/MedicationService.cs b/backend/src/HealthManager.Application/Services/MedicationService.cs new file mode 100644 index 0000000..f062fb8 --- /dev/null +++ b/backend/src/HealthManager.Application/Services/MedicationService.cs @@ -0,0 +1,93 @@ +using HealthManager.Domain.Entities; +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class MedicationService(AppDbContext db) +{ + public async Task> GetUserMedicationsAsync(Guid userId) + => await db.Medications + .Where(m => m.UserId == userId) + .OrderByDescending(m => m.CreatedAt) + .ToListAsync(); + + public async Task GetByIdAsync(Guid id) + => await db.Medications.FindAsync(id); + + public async Task AddAsync(Guid userId, string drugName, string dosage, string frequency, + List timeSlots, DateOnly startDate, DateOnly? endDate, string? notes, Guid? doctorId = null) + { + var med = new Medication + { + UserId = userId, + DoctorId = doctorId, + DrugName = drugName, + Dosage = dosage, + Frequency = frequency, + TimeSlots = timeSlots, + StartDate = startDate, + EndDate = endDate, + Notes = notes, + }; + db.Medications.Add(med); + await db.SaveChangesAsync(); + return med; + } + + public async Task> GetRecordsAsync(Guid medicationId) + => await db.MedicationRecords + .Where(mr => mr.MedicationId == medicationId) + .OrderByDescending(mr => mr.CreatedAt) + .ToListAsync(); + + public async Task MarkTakenAsync(Guid medicationId, Guid userId, string timeSlot) + { + // Find existing record for today's slot, update it instead of creating duplicate + var today = DateTime.UtcNow.Date; + var existing = await db.MedicationRecords + .FirstOrDefaultAsync(mr => + mr.MedicationId == medicationId && + mr.UserId == userId && + mr.TimeSlot == timeSlot && + mr.TakenAt.HasValue && + mr.TakenAt.Value.Date == today); + + if (existing != null) + { + existing.TakenAt = DateTime.UtcNow; + existing.IsTaken = true; + existing.SkippedReason = null; + await db.SaveChangesAsync(); + return existing; + } + + var record = new MedicationRecord + { + MedicationId = medicationId, + UserId = userId, + TimeSlot = timeSlot, + IsTaken = true, + TakenAt = DateTime.UtcNow, + }; + db.MedicationRecords.Add(record); + await db.SaveChangesAsync(); + return record; + } + + public async Task GetAdherenceRateAsync(Guid medicationId, int days = 30) + { + var medication = await db.Medications.FindAsync(medicationId); + if (medication == null) return 0; + + var cutoff = DateTime.UtcNow.AddDays(-days); + var allRecords = await db.MedicationRecords + .Where(mr => mr.MedicationId == medicationId && mr.CreatedAt >= cutoff) + .ToListAsync(); + + var takenCount = allRecords.Count(mr => mr.IsTaken); + var totalCount = allRecords.Count; + + return totalCount > 0 ? Math.Round((decimal)takenCount / totalCount * 100, 1) : 0; + } +} diff --git a/backend/src/HealthManager.Application/Services/NotificationService.cs b/backend/src/HealthManager.Application/Services/NotificationService.cs new file mode 100644 index 0000000..74c1396 --- /dev/null +++ b/backend/src/HealthManager.Application/Services/NotificationService.cs @@ -0,0 +1,52 @@ +using HealthManager.Domain.Entities; +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class NotificationService(AppDbContext db) +{ + public async Task> GetUserNotificationsAsync(Guid userId) + => await db.Notifications + .Where(n => n.UserId == userId) + .OrderByDescending(n => n.CreatedAt) + .ToListAsync(); + + public async Task GetUnreadCountAsync(Guid userId) + => await db.Notifications.CountAsync(n => n.UserId == userId && !n.IsRead); + + public async Task MarkAsReadAsync(Guid notificationId) + { + var notification = await db.Notifications.FindAsync(notificationId); + if (notification != null && !notification.IsRead) + { + notification.IsRead = true; + notification.ReadAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + } + + public async Task MarkAllAsReadAsync(Guid userId) + { + var unread = await db.Notifications.Where(n => n.UserId == userId && !n.IsRead).ToListAsync(); + foreach (var n in unread) + { + n.IsRead = true; + n.ReadAt = DateTime.UtcNow; + } + await db.SaveChangesAsync(); + } + + public async Task CreateAsync(Guid userId, string type, string title, string content, Guid? relatedId = null) + { + db.Notifications.Add(new Notification + { + UserId = userId, + Type = type, + Title = title, + Content = content, + RelatedId = relatedId, + }); + await db.SaveChangesAsync(); + } +} diff --git a/backend/src/HealthManager.Application/Services/PatientService.cs b/backend/src/HealthManager.Application/Services/PatientService.cs new file mode 100644 index 0000000..42e2ad9 --- /dev/null +++ b/backend/src/HealthManager.Application/Services/PatientService.cs @@ -0,0 +1,43 @@ +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class PatientService(AppDbContext db) +{ + public async Task> GetPatientsAsync(string? search, string? department, int page = 1, int pageSize = 20) + { + var query = db.Users.Where(u => u.Role == "patient" && !u.IsDeleted); + + if (!string.IsNullOrEmpty(search)) + query = query.Where(u => u.Name.Contains(search) || u.Phone.Contains(search)); + + var patients = await query + .OrderByDescending(u => u.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(u => new + { + u.Id, u.Name, u.Phone, u.Gender, u.Birthday, + u.StentDate, u.StentType, + u.CreatedAt, + }) + .ToListAsync(); + + return patients.Cast().ToList(); + } + + public async Task GetPatientDetailAsync(Guid patientId) + { + return await db.Users + .Where(u => u.Id == patientId && u.Role == "patient" && !u.IsDeleted) + .Select(u => new + { + u.Id, u.Name, u.Phone, u.Gender, u.Birthday, + u.HeightCm, u.WeightKg, u.MedicalHistory, + u.StentDate, u.StentType, + u.CreatedAt, + }) + .FirstOrDefaultAsync(); + } +} diff --git a/backend/src/HealthManager.Application/Services/ReportService.cs b/backend/src/HealthManager.Application/Services/ReportService.cs new file mode 100644 index 0000000..cbff934 --- /dev/null +++ b/backend/src/HealthManager.Application/Services/ReportService.cs @@ -0,0 +1,76 @@ +using HealthManager.Domain.Entities; +using HealthManager.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace HealthManager.Application.Services; + +public class ReportService(AppDbContext db) +{ + public async Task> GetPatientReportsAsync(Guid patientId) + => await db.Reports + .Include(r => r.Doctor) + .Include(r => r.Items) + .Where(r => r.PatientId == patientId) + .OrderByDescending(r => r.UploadedAt) + .ToListAsync(); + + public async Task GetByIdAsync(Guid id) + => await db.Reports + .Include(r => r.Patient) + .Include(r => r.Doctor) + .Include(r => r.Items) + .FirstOrDefaultAsync(r => r.Id == id); + + public async Task UploadAsync(Guid patientId, string title, string category, List imageUrls) + { + var report = new Report + { + PatientId = patientId, + Title = title, + Category = category, + ImageUrls = imageUrls, + UploadedBy = patientId, + }; + db.Reports.Add(report); + await db.SaveChangesAsync(); + return report; + } + + public async Task> GetPendingAsync() + => await db.Reports + .Include(r => r.Patient) + .Where(r => r.Status == "pending") + .OrderByDescending(r => r.UploadedAt) + .ToListAsync(); + + public async Task InterpretAsync(Guid reportId, Guid doctorId, string summary, + List<(string name, string value, string? unit, string? range, bool abnormal)> items, + string riskLevel, string? suggestions) + { + var report = await db.Reports.FindAsync(reportId) ?? throw new InvalidOperationException("Report not found"); + report.DoctorId = doctorId; + report.Status = "completed"; + report.Summary = summary; + report.RiskLevel = riskLevel; + report.Suggestions = suggestions; + report.CompletedAt = DateTime.UtcNow; + + var order = 0; + foreach (var (name, value, unit, range, abnormal) in items) + { + db.ReportItems.Add(new ReportItem + { + ReportId = reportId, + ItemName = name, + ResultValue = value, + Unit = unit, + ReferenceRange = range, + IsAbnormal = abnormal, + SortOrder = order++, + }); + } + + await db.SaveChangesAsync(); + return report; + } +} diff --git a/backend/src/HealthManager.Domain/Entities/Consultation.cs b/backend/src/HealthManager.Domain/Entities/Consultation.cs new file mode 100644 index 0000000..0d4622b --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/Consultation.cs @@ -0,0 +1,19 @@ +namespace HealthManager.Domain.Entities; + +public class Consultation +{ + public Guid Id { get; set; } + public Guid PatientId { get; set; } + public Guid DoctorId { get; set; } + public string? Subject { get; set; } + public string Status { get; set; } = "active"; // active, closed + public DateTime StartedAt { get; set; } = DateTime.UtcNow; + public DateTime? ClosedAt { get; set; } + public Guid? ClosedBy { get; set; } + public string? Summary { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public User Patient { get; set; } = null!; + public User Doctor { get; set; } = null!; + public ICollection Messages { get; set; } = []; +} diff --git a/backend/src/HealthManager.Domain/Entities/ConsultationMessage.cs b/backend/src/HealthManager.Domain/Entities/ConsultationMessage.cs new file mode 100644 index 0000000..0b6729a --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/ConsultationMessage.cs @@ -0,0 +1,18 @@ +namespace HealthManager.Domain.Entities; + +public class ConsultationMessage +{ + public Guid Id { get; set; } + public Guid ConsultationId { get; set; } + public Guid SenderId { get; set; } + public string SenderRole { get; set; } = string.Empty; // patient, doctor, system + public string ContentType { get; set; } = "text"; // text, image, file, template + public string Content { get; set; } = string.Empty; + public string? ImageUrl { get; set; } + public string? FileUrl { get; set; } + public bool IsRead { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public Consultation Consultation { get; set; } = null!; + public User Sender { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/Device.cs b/backend/src/HealthManager.Domain/Entities/Device.cs new file mode 100644 index 0000000..e6ce793 --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/Device.cs @@ -0,0 +1,18 @@ +namespace HealthManager.Domain.Entities; + +public class Device +{ + public Guid Id { get; set; } + public Guid? UserId { get; set; } + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string? MacAddress { get; set; } + public string? SerialNumber { get; set; } + public bool IsBound { get; set; } + public DateTime? BoundAt { get; set; } + public DateTime? LastSyncAt { get; set; } + public int? BatteryLevel { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public User? User { get; set; } +} diff --git a/backend/src/HealthManager.Domain/Entities/DietRecord.cs b/backend/src/HealthManager.Domain/Entities/DietRecord.cs new file mode 100644 index 0000000..5c621df --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/DietRecord.cs @@ -0,0 +1,16 @@ +namespace HealthManager.Domain.Entities; + +public class DietRecord +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string MealType { get; set; } = string.Empty; // breakfast, lunch, dinner, snack + public string FoodName { get; set; } = string.Empty; + public string? Portion { get; set; } + public int? Calories { get; set; } + public string? Notes { get; set; } + public DateOnly RecordedAt { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public User User { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/ExerciseRecord.cs b/backend/src/HealthManager.Domain/Entities/ExerciseRecord.cs new file mode 100644 index 0000000..c1ff7c7 --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/ExerciseRecord.cs @@ -0,0 +1,16 @@ +namespace HealthManager.Domain.Entities; + +public class ExerciseRecord +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string ExerciseType { get; set; } = string.Empty; + public int DurationMin { get; set; } + public string? Intensity { get; set; } // low, moderate, high + public int? CaloriesBurned { get; set; } + public string? Notes { get; set; } + public DateOnly RecordedAt { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public User User { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/FollowUp.cs b/backend/src/HealthManager.Domain/Entities/FollowUp.cs new file mode 100644 index 0000000..2df2174 --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/FollowUp.cs @@ -0,0 +1,19 @@ +namespace HealthManager.Domain.Entities; + +public class FollowUp +{ + public Guid Id { get; set; } + public Guid PatientId { get; set; } + public Guid? DoctorId { get; set; } + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public DateTime ScheduledAt { get; set; } + public string Status { get; set; } = "upcoming"; // upcoming, completed, cancelled, rescheduled + public string? Notes { get; set; } + public bool ReminderEnabled { get; set; } = true; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public User Patient { get; set; } = null!; + public User? Doctor { get; set; } +} diff --git a/backend/src/HealthManager.Domain/Entities/HealthRecord.cs b/backend/src/HealthManager.Domain/Entities/HealthRecord.cs new file mode 100644 index 0000000..9cba0c8 --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/HealthRecord.cs @@ -0,0 +1,18 @@ +using System.Text.Json; + +namespace HealthManager.Domain.Entities; + +public class HealthRecord +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Type { get; set; } = string.Empty; // blood_pressure, heart_rate, blood_sugar, spo2, weight, steps + public JsonDocument Value { get; set; } = null!; + public string Unit { get; set; } = string.Empty; + public DateTime RecordedAt { get; set; } + public string Source { get; set; } = "manual"; // manual, device, doctor + public string? Notes { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public User User { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/Medication.cs b/backend/src/HealthManager.Domain/Entities/Medication.cs new file mode 100644 index 0000000..72a0801 --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/Medication.cs @@ -0,0 +1,22 @@ +namespace HealthManager.Domain.Entities; + +public class Medication +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public Guid? DoctorId { get; set; } + public string DrugName { get; set; } = string.Empty; + public string Dosage { get; set; } = string.Empty; + public string Frequency { get; set; } = string.Empty; + public List TimeSlots { get; set; } = []; + public DateOnly StartDate { get; set; } + public DateOnly? EndDate { get; set; } + public string? Notes { get; set; } + public string Status { get; set; } = "active"; // active, completed, stopped + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public User User { get; set; } = null!; + public User? Doctor { get; set; } + public ICollection Records { get; set; } = []; +} diff --git a/backend/src/HealthManager.Domain/Entities/MedicationRecord.cs b/backend/src/HealthManager.Domain/Entities/MedicationRecord.cs new file mode 100644 index 0000000..648214f --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/MedicationRecord.cs @@ -0,0 +1,16 @@ +namespace HealthManager.Domain.Entities; + +public class MedicationRecord +{ + public Guid Id { get; set; } + public Guid MedicationId { get; set; } + public Guid UserId { get; set; } + public string TimeSlot { get; set; } = string.Empty; + public DateTime? TakenAt { get; set; } + public bool IsTaken { get; set; } + public string? SkippedReason { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public Medication Medication { get; set; } = null!; + public User User { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/Notification.cs b/backend/src/HealthManager.Domain/Entities/Notification.cs new file mode 100644 index 0000000..6706a66 --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/Notification.cs @@ -0,0 +1,16 @@ +namespace HealthManager.Domain.Entities; + +public class Notification +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Type { get; set; } = string.Empty; // medication_reminder, followup_reminder, consultation_new, report_completed, health_alert, system + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public Guid? RelatedId { get; set; } + public bool IsRead { get; set; } + public DateTime? ReadAt { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public User User { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/QuickReplyTemplate.cs b/backend/src/HealthManager.Domain/Entities/QuickReplyTemplate.cs new file mode 100644 index 0000000..7f1b90d --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/QuickReplyTemplate.cs @@ -0,0 +1,14 @@ +namespace HealthManager.Domain.Entities; + +public class QuickReplyTemplate +{ + public Guid Id { get; set; } + public Guid DoctorId { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public string? Category { get; set; } + public int SortOrder { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public User Doctor { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/RefreshToken.cs b/backend/src/HealthManager.Domain/Entities/RefreshToken.cs new file mode 100644 index 0000000..bd6287c --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/RefreshToken.cs @@ -0,0 +1,13 @@ +namespace HealthManager.Domain.Entities; + +public class RefreshToken +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Token { get; set; } = string.Empty; + public DateTime ExpiresAt { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? RevokedAt { get; set; } + + public User User { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/Report.cs b/backend/src/HealthManager.Domain/Entities/Report.cs new file mode 100644 index 0000000..c13bcca --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/Report.cs @@ -0,0 +1,22 @@ +namespace HealthManager.Domain.Entities; + +public class Report +{ + public Guid Id { get; set; } + public Guid PatientId { get; set; } + public Guid? DoctorId { get; set; } + public string Title { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public List ImageUrls { get; set; } = []; + public string Status { get; set; } = "pending"; // pending, interpreting, completed + public string? RiskLevel { get; set; } // normal, attention, abnormal + public string? Summary { get; set; } + public string? Suggestions { get; set; } + public Guid UploadedBy { get; set; } + public DateTime UploadedAt { get; set; } = DateTime.UtcNow; + public DateTime? CompletedAt { get; set; } + + public User Patient { get; set; } = null!; + public User? Doctor { get; set; } + public ICollection Items { get; set; } = []; +} diff --git a/backend/src/HealthManager.Domain/Entities/ReportItem.cs b/backend/src/HealthManager.Domain/Entities/ReportItem.cs new file mode 100644 index 0000000..ea840cd --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/ReportItem.cs @@ -0,0 +1,15 @@ +namespace HealthManager.Domain.Entities; + +public class ReportItem +{ + public Guid Id { get; set; } + public Guid ReportId { get; set; } + public string ItemName { get; set; } = string.Empty; + public string ResultValue { get; set; } = string.Empty; + public string? Unit { get; set; } + public string? ReferenceRange { get; set; } + public bool IsAbnormal { get; set; } + public int SortOrder { get; set; } + + public Report Report { get; set; } = null!; +} diff --git a/backend/src/HealthManager.Domain/Entities/User.cs b/backend/src/HealthManager.Domain/Entities/User.cs new file mode 100644 index 0000000..b47ed95 --- /dev/null +++ b/backend/src/HealthManager.Domain/Entities/User.cs @@ -0,0 +1,41 @@ +namespace HealthManager.Domain.Entities; + +public class User +{ + public Guid Id { get; set; } + public string Role { get; set; } = string.Empty; // patient, doctor, admin + public string Phone { get; set; } = string.Empty; + public string PasswordHash { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? AvatarUrl { get; set; } + + // Patient fields + public string? Gender { get; set; } + public DateOnly? Birthday { get; set; } + public decimal? HeightCm { get; set; } + public decimal? WeightKg { get; set; } + public List? MedicalHistory { get; set; } + public DateOnly? StentDate { get; set; } + public string? StentType { get; set; } + + // Doctor fields + public string? Department { get; set; } + public string? Title { get; set; } + public List? Specialty { get; set; } + public string? Introduction { get; set; } + public bool IsAvailable { get; set; } = true; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public bool IsDeleted { get; set; } + public DateTime? DeletedAt { get; set; } + + // Navigation + public ICollection RefreshTokens { get; set; } = []; + public ICollection HealthRecords { get; set; } = []; + public ICollection Medications { get; set; } = []; + public ICollection MedicationRecords { get; set; } = []; + public ICollection ExerciseRecords { get; set; } = []; + public ICollection DietRecords { get; set; } = []; + public ICollection Notifications { get; set; } = []; +} diff --git a/backend/src/HealthManager.Domain/HealthManager.Domain.csproj b/backend/src/HealthManager.Domain/HealthManager.Domain.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/backend/src/HealthManager.Domain/HealthManager.Domain.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/backend/src/HealthManager.Domain/Interfaces/IJwtProvider.cs b/backend/src/HealthManager.Domain/Interfaces/IJwtProvider.cs new file mode 100644 index 0000000..6806106 --- /dev/null +++ b/backend/src/HealthManager.Domain/Interfaces/IJwtProvider.cs @@ -0,0 +1,7 @@ +namespace HealthManager.Domain.Interfaces; + +public interface IJwtProvider +{ + string GenerateAccessToken(Guid userId, string name, string role); + string GenerateRefreshToken(); +} diff --git a/backend/src/HealthManager.Infrastructure/HealthManager.Infrastructure.csproj b/backend/src/HealthManager.Infrastructure/HealthManager.Infrastructure.csproj new file mode 100644 index 0000000..60a1f5d --- /dev/null +++ b/backend/src/HealthManager.Infrastructure/HealthManager.Infrastructure.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/backend/src/HealthManager.Infrastructure/Services/JwtProvider.cs b/backend/src/HealthManager.Infrastructure/Services/JwtProvider.cs new file mode 100644 index 0000000..55f12ea --- /dev/null +++ b/backend/src/HealthManager.Infrastructure/Services/JwtProvider.cs @@ -0,0 +1,39 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using HealthManager.Domain.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace HealthManager.Infrastructure.Services; + +public class JwtProvider(IConfiguration configuration) : IJwtProvider +{ + private static readonly JwtSecurityTokenHandler TokenHandler = new(); + public string GenerateAccessToken(Guid userId, string name, string role) + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]!)); + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, userId.ToString()), + new Claim(ClaimTypes.Name, name), + new Claim(ClaimTypes.Role, role), + }; + + var token = new JwtSecurityToken( + issuer: configuration["Jwt:Issuer"], + audience: configuration["Jwt:Audience"], + claims: claims, + expires: DateTime.UtcNow.AddMinutes(30), + signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)); + + return TokenHandler.WriteToken(token); + } + + public string GenerateRefreshToken() + { + var bytes = RandomNumberGenerator.GetBytes(64); + return Convert.ToBase64String(bytes); + } +} diff --git a/backend/src/HealthManager.WebApi/Controllers/AuthController.cs b/backend/src/HealthManager.WebApi/Controllers/AuthController.cs new file mode 100644 index 0000000..3e72153 --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/AuthController.cs @@ -0,0 +1,120 @@ +using System.Security.Claims; +using HealthManager.Application.DTOs.Auth; +using HealthManager.Domain.Interfaces; +using HealthManager.Application.Services; +using HealthManager.Domain.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/auth")] +public class AuthController( + AuthService authService, + IJwtProvider jwtProvider) : ControllerBase +{ + [HttpPost("send-sms")] + public IActionResult SendSms([FromBody] SendSmsRequest request) + { + // Demo: always succeed + return Ok(new { message = "验证码已发送" }); + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginRequest request) + { + var user = await authService.GetUserByPhoneAsync(request.Phone); + if (user == null) + return Unauthorized(new { message = "用户不存在" }); + + // Demo: accept any SMS code + var accessToken = jwtProvider.GenerateAccessToken(user.Id, user.Name, user.Role); + var refreshToken = jwtProvider.GenerateRefreshToken(); + await authService.SaveRefreshTokenAsync(user.Id, refreshToken, DateTime.UtcNow.AddDays(7)); + + return Ok(new AuthResponse(user.Id, user.Name, user.Role, accessToken, refreshToken)); + } + + [HttpPost("register")] + public async Task Register([FromBody] RegisterRequest request) + { + var existing = await authService.GetUserByPhoneAsync(request.Phone); + if (existing != null) + return Conflict(new { message = "该手机号已注册" }); + + var user = new User + { + Phone = request.Phone, + Name = request.Name, + Role = "patient", + PasswordHash = AuthService.HashPassword("demo123"), + }; + + // Access DbContext via DI + var db = HttpContext.RequestServices.GetRequiredService(); + db.Users.Add(user); + await db.SaveChangesAsync(); + + var accessToken = jwtProvider.GenerateAccessToken(user.Id, user.Name, user.Role); + var refreshToken = jwtProvider.GenerateRefreshToken(); + await authService.SaveRefreshTokenAsync(user.Id, refreshToken, DateTime.UtcNow.AddDays(7)); + + return Ok(new AuthResponse(user.Id, user.Name, user.Role, accessToken, refreshToken)); + } + + [HttpPost("refresh")] + public async Task RefreshToken([FromBody] RefreshTokenRequest request) + { + var saved = await authService.GetRefreshTokenAsync(request.RefreshToken); + if (saved == null) + return Unauthorized(new { message = "无效的刷新令牌" }); + + await authService.RevokeRefreshTokenAsync(saved.UserId); + + var accessToken = jwtProvider.GenerateAccessToken(saved.User.Id, saved.User.Name, saved.User.Role); + var refreshToken = jwtProvider.GenerateRefreshToken(); + await authService.SaveRefreshTokenAsync(saved.UserId, refreshToken, DateTime.UtcNow.AddDays(7)); + + return Ok(new AuthResponse(saved.User.Id, saved.User.Name, saved.User.Role, accessToken, refreshToken)); + } + + [HttpGet("me")] + [Authorize] + public async Task GetProfile() + { + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + var db = HttpContext.RequestServices.GetRequiredService(); + var user = await db.Users.FindAsync(userId); + if (user == null) return NotFound(); + + return Ok(new UserProfileResponse( + user.Id, user.Name, user.Phone, user.Role, + user.Gender, user.Birthday, user.HeightCm, user.WeightKg, + user.MedicalHistory, user.StentDate, user.StentType, + user.Department, user.Title, user.Specialty, user.Introduction)); + } + + [HttpPut("me")] + [Authorize] + public async Task UpdateProfile([FromBody] UpdateProfileRequest request) + { + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + var db = HttpContext.RequestServices.GetRequiredService(); + var user = await db.Users.FindAsync(userId); + if (user == null) return NotFound(); + + if (request.Name != null) user.Name = request.Name; + if (request.Gender != null) user.Gender = request.Gender; + if (request.Birthday.HasValue) user.Birthday = request.Birthday; + if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm; + if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg; + if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory; + user.UpdatedAt = DateTime.UtcNow; + + await db.SaveChangesAsync(); + return Ok(new { message = "更新成功" }); + } +} + +public record RefreshTokenRequest(string RefreshToken); diff --git a/backend/src/HealthManager.WebApi/Controllers/ConsultationController.cs b/backend/src/HealthManager.WebApi/Controllers/ConsultationController.cs new file mode 100644 index 0000000..ac06cdd --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/ConsultationController.cs @@ -0,0 +1,71 @@ +using System.Security.Claims; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/consultations")] +[Authorize] +public class ConsultationController(ConsultationService consultationService) : ControllerBase +{ + private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + private string Role => User.FindFirstValue(ClaimTypes.Role)!; + + [HttpGet("doctors")] + public async Task GetDoctors([FromQuery] string? department) + { + var doctors = await consultationService.GetDoctorsAsync(department); + return Ok(doctors.Select(d => new + { + d.Id, d.Name, d.Department, d.Title, d.Specialty, d.Introduction, d.AvatarUrl, + })); + } + + [HttpGet] + public async Task GetConsultations() + { + var consultations = Role == "doctor" + ? await consultationService.GetDoctorConsultationsAsync(UserId) + : await consultationService.GetPatientConsultationsAsync(UserId); + + return Ok(consultations.Select(c => new + { + c.Id, c.PatientId, c.DoctorId, + PatientName = c.Patient?.Name, + DoctorName = c.Doctor?.Name, + c.Subject, c.Status, c.StartedAt, c.ClosedAt, c.Summary, + })); + } + + [HttpPost] + public async Task StartConsultation([FromBody] StartConsultationRequest request) + { + var consultation = await consultationService.StartAsync(UserId, request.DoctorId, request.Subject); + return Ok(new { consultation.Id, consultation.PatientId, consultation.DoctorId, consultation.Status }); + } + + [HttpGet("{id:guid}/messages")] + public async Task GetMessages(Guid id) + { + var messages = await consultationService.GetMessagesAsync(id); + return Ok(messages.Select(m => new + { + m.Id, m.SenderId, m.SenderRole, m.ContentType, m.Content, + m.ImageUrl, m.IsRead, m.CreatedAt, + SenderName = m.Sender?.Name, + })); + } + + [HttpPost("{id:guid}/messages")] + public async Task SendMessage(Guid id, [FromBody] SendMessageRequest request) + { + var message = await consultationService.SendMessageAsync(id, UserId, Role, request.Content, + request.ContentType, request.ImageUrl); + return Ok(new { message.Id, message.SenderId, message.SenderRole, message.Content, message.CreatedAt }); + } +} + +public record StartConsultationRequest(Guid DoctorId, string Subject); +public record SendMessageRequest(string Content, string ContentType = "text", string? ImageUrl = null); diff --git a/backend/src/HealthManager.WebApi/Controllers/FollowUpController.cs b/backend/src/HealthManager.WebApi/Controllers/FollowUpController.cs new file mode 100644 index 0000000..f7065db --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/FollowUpController.cs @@ -0,0 +1,67 @@ +using System.Security.Claims; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/follow-ups")] +[Authorize] +public class FollowUpController(FollowUpService followUpService) : ControllerBase +{ + private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + private string Role => User.FindFirstValue(ClaimTypes.Role)!; + + [HttpGet] + public async Task GetFollowUps() + { + var followUps = Role == "doctor" + ? await followUpService.GetDoctorFollowUpsAsync(UserId) + : await followUpService.GetPatientFollowUpsAsync(UserId); + + return Ok(followUps.Select(f => new + { + f.Id, f.PatientId, f.DoctorId, f.Title, f.Description, + f.ScheduledAt, f.Status, f.Notes, f.ReminderEnabled, f.CreatedAt, + PatientName = f.Patient?.Name, + DoctorName = f.Doctor?.Name, + })); + } + + [HttpGet("{id:guid}")] + public async Task GetFollowUp(Guid id) + { + var followUp = await followUpService.GetByIdAsync(id); + if (followUp == null) return NotFound(new { message = "复查不存在" }); + return Ok(new + { + followUp.Id, followUp.PatientId, followUp.DoctorId, followUp.Title, + followUp.Description, followUp.ScheduledAt, followUp.Status, + followUp.Notes, followUp.ReminderEnabled, followUp.CreatedAt, + }); + } + + [HttpPost] + public async Task AddFollowUp([FromBody] FollowUpCreateRequest request) + { + var followUp = await followUpService.AddAsync(UserId, request.Title, request.Description, + request.ScheduledAt, request.ReminderEnabled); + return Ok(new { followUp.Id, followUp.Title, followUp.Status }); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "doctor")] + public async Task UpdateFollowUp(Guid id, [FromBody] FollowUpUpdateRequest request) + { + var followUp = await followUpService.UpdateAsync(id, UserId, request.Title, request.Description, + request.ScheduledAt, request.Status, request.Notes); + if (followUp == null) return NotFound(new { message = "复查不存在" }); + return Ok(new { followUp.Id, followUp.Title, followUp.Status }); + } +} + +public record FollowUpCreateRequest(string Title, string? Description, DateTime ScheduledAt, bool ReminderEnabled = true); + +public record FollowUpUpdateRequest( + string? Title, string? Description, DateTime? ScheduledAt, string? Status, string? Notes); diff --git a/backend/src/HealthManager.WebApi/Controllers/HealthController.cs b/backend/src/HealthManager.WebApi/Controllers/HealthController.cs new file mode 100644 index 0000000..b899074 --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/HealthController.cs @@ -0,0 +1,61 @@ +using System.Security.Claims; +using System.Text.Json; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/health-records")] +[Authorize] +public class HealthController(HealthService healthService) : ControllerBase +{ + private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + private string Role => User.FindFirstValue(ClaimTypes.Role)!; + + [HttpGet] + public async Task GetRecords([FromQuery] string? type, [FromQuery] int days = 30) + { + var targetUserId = UserId; + + // Doctors can query any patient + if (Role == "doctor" && Request.Query.ContainsKey("patientId")) + targetUserId = Guid.Parse(Request.Query["patientId"]!); + + var records = await healthService.GetRecordsAsync(targetUserId, type, days); + return Ok(records.Select(r => new + { + r.Id, r.Type, Value = r.Value.RootElement.GetRawText(), r.Unit, + r.RecordedAt, r.Source, r.Notes, r.CreatedAt, + })); + } + + [HttpGet("stats")] + public async Task GetStats() + { + var stats = await healthService.GetStatsAsync(UserId); + return Ok(stats); + } + + [HttpGet("latest/{type}")] + public async Task GetLatest(string type) + { + var record = await healthService.GetLatestAsync(UserId, type); + if (record == null) return Ok((object?)null); + return Ok(new { record.Id, record.Type, Value = record.Value.RootElement.GetRawText(), record.Unit, record.RecordedAt, record.Source }); + } + + [HttpPost] + public async Task AddRecord([FromBody] HealthRecordCreateRequest request) + { + // Validate JSON + try { JsonDocument.Parse(request.ValueJson); } + catch (JsonException) { return BadRequest(new { message = "无效的数据格式" }); } + + var record = await healthService.AddRecordAsync(UserId, request.Type, request.ValueJson, request.Unit, request.RecordedAt, request.Notes); + return Ok(new { record.Id, record.Type, Value = record.Value.RootElement.GetRawText(), record.Unit, record.RecordedAt, record.Source }); + } +} + +public record HealthRecordCreateRequest(string Type, string ValueJson, string Unit, DateTime RecordedAt, string? Notes); diff --git a/backend/src/HealthManager.WebApi/Controllers/MedicationController.cs b/backend/src/HealthManager.WebApi/Controllers/MedicationController.cs new file mode 100644 index 0000000..d451f86 --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/MedicationController.cs @@ -0,0 +1,80 @@ +using System.Security.Claims; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/medications")] +[Authorize] +public class MedicationController(MedicationService medicationService) : ControllerBase +{ + private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + private string Role => User.FindFirstValue(ClaimTypes.Role)!; + + [HttpGet] + public async Task GetMedications() + { + var targetUserId = UserId; + if (Role == "doctor" && Request.Query.ContainsKey("patientId")) + targetUserId = Guid.Parse(Request.Query["patientId"]!); + + var medications = await medicationService.GetUserMedicationsAsync(targetUserId); + return Ok(medications.Select(m => new + { + m.Id, m.UserId, m.DrugName, m.Dosage, m.Frequency, m.TimeSlots, + m.StartDate, m.EndDate, m.Notes, m.Status, m.CreatedAt, + })); + } + + [HttpPost] + public async Task AddMedication([FromBody] MedicationCreateRequest request) + { + var med = await medicationService.AddAsync(UserId, request.DrugName, request.Dosage, + request.Frequency, request.TimeSlots, request.StartDate, request.EndDate, request.Notes); + return Ok(new { med.Id, med.DrugName, med.Dosage, med.Frequency, med.Status }); + } + + [HttpGet("{id:guid}")] + public async Task GetMedication(Guid id) + { + var med = await medicationService.GetByIdAsync(id); + if (med == null) return NotFound(new { message = "药品不存在" }); + return Ok(new + { + med.Id, med.UserId, med.DrugName, med.Dosage, med.Frequency, med.TimeSlots, + med.StartDate, med.EndDate, med.Notes, med.Status, med.CreatedAt, + }); + } + + [HttpGet("{id:guid}/records")] + public async Task GetRecords(Guid id) + { + var records = await medicationService.GetRecordsAsync(id); + return Ok(records.Select(r => new + { + r.Id, r.MedicationId, r.TimeSlot, r.TakenAt, r.IsTaken, r.SkippedReason, r.CreatedAt, + })); + } + + [HttpPost("{id:guid}/take")] + public async Task MarkTaken(Guid id, [FromBody] MarkTakenRequest request) + { + var record = await medicationService.MarkTakenAsync(id, UserId, request.TimeSlot); + return Ok(new { record.Id, record.TimeSlot, record.TakenAt, record.IsTaken }); + } + + [HttpGet("{id:guid}/adherence")] + public async Task GetAdherence(Guid id) + { + var rate = await medicationService.GetAdherenceRateAsync(id); + return Ok(new { medicationId = id, rate }); + } +} + +public record MedicationCreateRequest( + string DrugName, string Dosage, string Frequency, + List TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes); + +public record MarkTakenRequest(string TimeSlot); diff --git a/backend/src/HealthManager.WebApi/Controllers/NotificationController.cs b/backend/src/HealthManager.WebApi/Controllers/NotificationController.cs new file mode 100644 index 0000000..0562167 --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/NotificationController.cs @@ -0,0 +1,46 @@ +using System.Security.Claims; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/notifications")] +[Authorize] +public class NotificationController(NotificationService notificationService) : ControllerBase +{ + private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + [HttpGet] + public async Task GetNotifications() + { + var notifications = await notificationService.GetUserNotificationsAsync(UserId); + return Ok(notifications.Select(n => new + { + n.Id, n.Type, n.Title, n.Content, n.RelatedId, + n.IsRead, n.ReadAt, n.CreatedAt, + })); + } + + [HttpGet("unread-count")] + public async Task GetUnreadCount() + { + var count = await notificationService.GetUnreadCountAsync(UserId); + return Ok(new { count }); + } + + [HttpPut("{id:guid}/read")] + public async Task MarkAsRead(Guid id) + { + await notificationService.MarkAsReadAsync(id); + return Ok(new { message = "已标记为已读" }); + } + + [HttpPut("read-all")] + public async Task MarkAllAsRead() + { + await notificationService.MarkAllAsReadAsync(UserId); + return Ok(new { message = "全部已读" }); + } +} diff --git a/backend/src/HealthManager.WebApi/Controllers/PatientController.cs b/backend/src/HealthManager.WebApi/Controllers/PatientController.cs new file mode 100644 index 0000000..7caed6b --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/PatientController.cs @@ -0,0 +1,27 @@ +using System.Security.Claims; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/patients")] +[Authorize(Roles = "doctor")] +public class PatientController(PatientService patientService) : ControllerBase +{ + [HttpGet] + public async Task GetPatients([FromQuery] string? search, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) + { + var patients = await patientService.GetPatientsAsync(search, null, page, pageSize); + return Ok(patients); + } + + [HttpGet("{id:guid}")] + public async Task GetPatientDetail(Guid id) + { + var patient = await patientService.GetPatientDetailAsync(id); + if (patient == null) return NotFound(new { message = "患者不存在" }); + return Ok(patient); + } +} diff --git a/backend/src/HealthManager.WebApi/Controllers/ReportController.cs b/backend/src/HealthManager.WebApi/Controllers/ReportController.cs new file mode 100644 index 0000000..fd0cd7c --- /dev/null +++ b/backend/src/HealthManager.WebApi/Controllers/ReportController.cs @@ -0,0 +1,87 @@ +using System.Security.Claims; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HealthManager.WebApi.Controllers; + +[ApiController] +[Route("api/reports")] +[Authorize] +public class ReportController(ReportService reportService) : ControllerBase +{ + private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + private string Role => User.FindFirstValue(ClaimTypes.Role)!; + + [HttpGet] + public async Task GetReports() + { + var targetUserId = UserId; + if (Role == "doctor" && Request.Query.ContainsKey("patientId")) + targetUserId = Guid.Parse(Request.Query["patientId"]!); + + var reports = await reportService.GetPatientReportsAsync(targetUserId); + return Ok(reports.Select(r => new + { + r.Id, r.PatientId, r.Title, r.Category, r.ImageUrls, r.Status, + r.RiskLevel, r.UploadedAt, r.CompletedAt, + DoctorName = r.Doctor?.Name, + })); + } + + [HttpGet("pending")] + [Authorize(Roles = "doctor")] + public async Task GetPending() + { + var reports = await reportService.GetPendingAsync(); + return Ok(reports.Select(r => new + { + r.Id, r.PatientId, r.Title, r.Category, r.Status, r.UploadedAt, + PatientName = r.Patient?.Name, + })); + } + + [HttpGet("{id:guid}")] + public async Task GetReport(Guid id) + { + var report = await reportService.GetByIdAsync(id); + if (report == null) return NotFound(new { message = "报告不存在" }); + + return Ok(new + { + report.Id, report.PatientId, report.Title, report.Category, report.ImageUrls, + report.Status, report.RiskLevel, report.Summary, report.Suggestions, + report.UploadedAt, report.CompletedAt, + PatientName = report.Patient?.Name, + DoctorName = report.Doctor?.Name, + Items = report.Items.Select(i => new + { + i.Id, i.ItemName, i.ResultValue, i.Unit, i.ReferenceRange, i.IsAbnormal, + }), + }); + } + + [HttpPost] + public async Task UploadReport([FromBody] ReportUploadRequest request) + { + var report = await reportService.UploadAsync(UserId, request.Title, request.Category, request.ImageUrls); + return Ok(new { report.Id, report.Title, report.Status }); + } + + [HttpPost("{id:guid}/interpret")] + [Authorize(Roles = "doctor")] + public async Task InterpretReport(Guid id, [FromBody] ReportInterpretRequest request) + { + var items = request.Items.Select(i => (i.ItemName, i.ResultValue, i.Unit, i.ReferenceRange, i.IsAbnormal)).ToList(); + var report = await reportService.InterpretAsync(id, UserId, request.Summary, items, request.RiskLevel, request.Suggestions); + return Ok(new { report.Id, report.Status, report.RiskLevel }); + } +} + +public record ReportUploadRequest(string Title, string Category, List ImageUrls); + +public record ReportInterpretRequest( + string Summary, List Items, string RiskLevel, string? Suggestions); + +public record ReportItemRequest( + string ItemName, string ResultValue, string? Unit, string? ReferenceRange, bool IsAbnormal); diff --git a/backend/src/HealthManager.WebApi/HealthManager.WebApi.csproj b/backend/src/HealthManager.WebApi/HealthManager.WebApi.csproj new file mode 100644 index 0000000..20bedad --- /dev/null +++ b/backend/src/HealthManager.WebApi/HealthManager.WebApi.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/backend/src/HealthManager.WebApi/HealthManager.WebApi.http b/backend/src/HealthManager.WebApi/HealthManager.WebApi.http new file mode 100644 index 0000000..db4123b --- /dev/null +++ b/backend/src/HealthManager.WebApi/HealthManager.WebApi.http @@ -0,0 +1,6 @@ +@HealthManager.WebApi_HostAddress = http://localhost:5133 + +GET {{HealthManager.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/src/HealthManager.WebApi/Hubs/ChatHub.cs b/backend/src/HealthManager.WebApi/Hubs/ChatHub.cs new file mode 100644 index 0000000..10faab2 --- /dev/null +++ b/backend/src/HealthManager.WebApi/Hubs/ChatHub.cs @@ -0,0 +1,40 @@ +using System.Security.Claims; +using HealthManager.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace HealthManager.WebApi.Hubs; + +[Authorize] +public class ChatHub(ConsultationService consultationService) : Hub +{ + public async Task JoinConsultation(Guid consultationId) + { + await Groups.AddToGroupAsync(Context.ConnectionId, $"consultation_{consultationId}"); + } + + public async Task LeaveConsultation(Guid consultationId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"consultation_{consultationId}"); + } + + public async Task SendMessage(Guid consultationId, string content) + { + var userId = Guid.Parse(Context.User!.FindFirstValue(ClaimTypes.NameIdentifier)!); + var role = Context.User!.FindFirstValue(ClaimTypes.Role)!; + + var message = await consultationService.SendMessageAsync(consultationId, userId, role, content); + + await Clients.Group($"consultation_{consultationId}").SendAsync("ReceiveMessage", new + { + message.Id, + message.SenderId, + message.SenderRole, + message.ContentType, + message.Content, + message.ImageUrl, + message.IsRead, + message.CreatedAt, + }); + } +} diff --git a/backend/src/HealthManager.WebApi/Program.cs b/backend/src/HealthManager.WebApi/Program.cs new file mode 100644 index 0000000..29bba0e --- /dev/null +++ b/backend/src/HealthManager.WebApi/Program.cs @@ -0,0 +1,101 @@ +using System.Text; +using HealthManager.Domain.Interfaces; +using HealthManager.Application.Services; +using HealthManager.Infrastructure.Data; +using HealthManager.Infrastructure.Services; +using HealthManager.WebApi.Hubs; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Swashbuckle.AspNetCore.SwaggerGen; + +var builder = WebApplication.CreateBuilder(args); + +// Database +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); + +// JWT +var jwtSecret = builder.Configuration["Jwt:Secret"]!; +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)), + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) + context.Token = accessToken; + return Task.CompletedTask; + } + }; + }); +builder.Services.AddAuthorization(); + +// Services +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// SignalR +builder.Services.AddSignalR(); + +// Swagger +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("Dev", policy => + { + policy.WithOrigins("http://localhost:5173", "http://localhost:5174", "http://localhost:5175") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors("Dev"); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.MapHub("/hubs/chat"); + +// Auto-migrate and seed on startup +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.EnsureCreatedAsync(); + await DataSeeder.SeedAsync(db); +} + +app.Run(); diff --git a/backend/src/HealthManager.WebApi/Properties/launchSettings.json b/backend/src/HealthManager.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..1e05984 --- /dev/null +++ b/backend/src/HealthManager.WebApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7241;http://localhost:5133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/src/HealthManager.WebApi/appsettings.Development.json b/backend/src/HealthManager.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/src/HealthManager.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/src/HealthManager.WebApi/appsettings.json b/backend/src/HealthManager.WebApi/appsettings.json new file mode 100644 index 0000000..8118304 --- /dev/null +++ b/backend/src/HealthManager.WebApi/appsettings.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "Default": "Host=localhost;Port=5432;Database=health_manager;Username=postgres;Password=postgres123" + }, + "Jwt": { + "Secret": "health-manager-jwt-secret-key-2026-super-secure-long-enough!", + "Issuer": "HealthManager", + "Audience": "HealthManagerApp" + }, + "Redis": { + "Connection": "localhost:6379" + }, + "MinIO": { + "Endpoint": "localhost:9000", + "AccessKey": "minioadmin", + "SecretKey": "minioadmin" + } +} diff --git a/frontend-doctor/.gitignore b/frontend-doctor/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend-doctor/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend-doctor/README.md b/frontend-doctor/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend-doctor/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend-doctor/eslint.config.js b/frontend-doctor/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/frontend-doctor/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/frontend-doctor/index.html b/frontend-doctor/index.html new file mode 100644 index 0000000..786cce0 --- /dev/null +++ b/frontend-doctor/index.html @@ -0,0 +1,12 @@ + + + + + + 健康管理平台 - 医生工作台 + + +
+ + + diff --git a/frontend-doctor/package-lock.json b/frontend-doctor/package-lock.json new file mode 100644 index 0000000..8c22209 --- /dev/null +++ b/frontend-doctor/package-lock.json @@ -0,0 +1,2979 @@ +{ + "name": "frontend-doctor", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend-doctor", + "version": "0.0.0", + "dependencies": { + "dayjs": "^1.11.20", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "framer-motion": "^12.39.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router-dom": "^7.15.1", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tybys/wasm-util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/echarts": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.1.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz", + "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.360", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", + "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/framer-motion": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.39.0.tgz", + "integrity": "sha512-+vnLfzrv0MzjLzNl+nvNvR7jdg3q4cxxjz/YvzfifHl0TREtL00cs1RoMTxs+1PzLiEqZGV6gYsBY0oEAYZ24w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.39.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/framer-motion/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.39.0.tgz", + "integrity": "sha512-Xn7aAcGDhco/JZTXOub64UmaYn73C6J1Po7Fk+8EvkJsNGTqfhon6UJY53vJKXW5v5Zl8HrYsVxv6oPXeGoGLQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-router": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", + "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz", + "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==", + "license": "MIT", + "dependencies": { + "react-router": "7.15.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/size-sensor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", + "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==", + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zrender": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zustand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend-doctor/package.json b/frontend-doctor/package.json new file mode 100644 index 0000000..f5af741 --- /dev/null +++ b/frontend-doctor/package.json @@ -0,0 +1,36 @@ +{ + "name": "frontend-doctor", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "dayjs": "^1.11.20", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "framer-motion": "^12.39.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router-dom": "^7.15.1", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} diff --git a/frontend-doctor/public/favicon.svg b/frontend-doctor/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend-doctor/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-doctor/public/icons.svg b/frontend-doctor/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend-doctor/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend-doctor/src/App.css b/frontend-doctor/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/frontend-doctor/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend-doctor/src/App.tsx b/frontend-doctor/src/App.tsx new file mode 100644 index 0000000..55c71af --- /dev/null +++ b/frontend-doctor/src/App.tsx @@ -0,0 +1,6 @@ +import { RouterProvider } from 'react-router-dom'; +import { router } from './router'; + +export default function App() { + return ; +} diff --git a/frontend-doctor/src/assets/hero.png b/frontend-doctor/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/frontend-doctor/src/assets/hero.png differ diff --git a/frontend-doctor/src/assets/react.svg b/frontend-doctor/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend-doctor/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-doctor/src/assets/styles/global.css b/frontend-doctor/src/assets/styles/global.css new file mode 100644 index 0000000..a220352 --- /dev/null +++ b/frontend-doctor/src/assets/styles/global.css @@ -0,0 +1,4 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #333; } +a { color: inherit; } +button { cursor: pointer; } diff --git a/frontend-doctor/src/assets/vite.svg b/frontend-doctor/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend-doctor/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend-doctor/src/components/layout/DoctorLayout.tsx b/frontend-doctor/src/components/layout/DoctorLayout.tsx new file mode 100644 index 0000000..575a6bd --- /dev/null +++ b/frontend-doctor/src/components/layout/DoctorLayout.tsx @@ -0,0 +1,83 @@ +import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { useAuthStore } from '../../stores/auth.store'; + +const navItems = [ + { to: '/dashboard', label: '工作台', icon: '📊' }, + { to: '/patients', label: '患者管理', icon: '👥' }, + { to: '/consultations', label: '在线问诊', icon: '💬' }, + { to: '/reports', label: '报告审核', icon: '📋' }, + { to: '/follow-ups', label: '随访管理', icon: '📅' }, +]; + +const sidebarBg = '#0F1D3D'; +const accentColor = '#4D8FFF'; +const textMuted = '#8E9DB5'; + +export function DoctorLayout() { + const { user, logout } = useAuthStore(); + const navigate = useNavigate(); + + const handleLogout = () => { + logout(); + navigate('/login'); + }; + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ +
+
+ ); +} diff --git a/frontend-doctor/src/index.css b/frontend-doctor/src/index.css new file mode 100644 index 0000000..5fb3313 --- /dev/null +++ b/frontend-doctor/src/index.css @@ -0,0 +1,111 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +#root { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +body { + margin: 0; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/frontend-doctor/src/main.tsx b/frontend-doctor/src/main.tsx new file mode 100644 index 0000000..6a9d725 --- /dev/null +++ b/frontend-doctor/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { RouterProvider } from 'react-router-dom'; +import { router } from './router'; +import './assets/styles/global.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/frontend-doctor/src/pages/auth/LoginPage.tsx b/frontend-doctor/src/pages/auth/LoginPage.tsx new file mode 100644 index 0000000..8ba01aa --- /dev/null +++ b/frontend-doctor/src/pages/auth/LoginPage.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuthStore } from '../../stores/auth.store'; + +export function LoginPage() { + const [phone, setPhone] = useState('13700137000'); + const [code, setCode] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const login = useAuthStore((s) => s.login); + const navigate = useNavigate(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + try { + await login(phone, code || '0000'); + navigate('/dashboard'); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : '登录失败'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

医生登录

+ + {error &&
{error}
} + +
+ + setPhone(e.target.value)} + style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 4, fontSize: 14 }} /> +
+ +
+ + setCode(e.target.value)} + placeholder="输入任意验证码" + style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 4, fontSize: 14 }} /> +
+ + + +

+ 演示账号:13700137000 (王建国 主任医师) +

+
+
+ ); +} diff --git a/frontend-doctor/src/pages/consultations/ChatPage.tsx b/frontend-doctor/src/pages/consultations/ChatPage.tsx new file mode 100644 index 0000000..efd15b5 --- /dev/null +++ b/frontend-doctor/src/pages/consultations/ChatPage.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState, useRef } from 'react'; +import { useParams } from 'react-router-dom'; +import { api } from '../../services/api-client'; + +interface Message { + id: string; senderId: string; senderRole: string; + content: string; contentType: string; createdAt: string; +} + +export function ChatPage() { + const { id } = useParams<{ id: string }>(); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const bottomRef = useRef(null); + + useEffect(() => { + if (!id) return; + api.get(`/api/consultations/${id}/messages`) + .then((r) => setMessages(r.data)) + .catch(() => {}); + }, [id]); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const handleSend = async () => { + if (!input.trim() || !id) return; + try { + const res = await api.post(`/api/consultations/${id}/messages`, { content: input }); + setMessages((prev) => [...prev, res.data]); + setInput(''); + } catch { /* ignore */ } + }; + + return ( +
+
+ 在线问诊 +
+ +
+ {messages.map((msg) => ( +
+
+
{msg.content}
+
+ {msg.createdAt?.split('T')[1]?.slice(0, 5)} +
+
+
+ ))} +
+
+ +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSend()} + placeholder="输入回复..." + style={{ flex: 1, padding: '10px 14px', border: '1px solid #ddd', borderRadius: 20, fontSize: 14 }} /> + +
+
+ ); +} diff --git a/frontend-doctor/src/pages/consultations/ConsultationListPage.tsx b/frontend-doctor/src/pages/consultations/ConsultationListPage.tsx new file mode 100644 index 0000000..7a2b063 --- /dev/null +++ b/frontend-doctor/src/pages/consultations/ConsultationListPage.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../../services/api-client'; + +interface ConsultationItem { + id: string; patientId: string; patientName: string; subject: string; + status: string; startedAt: string; +} + +interface RawConsultation { + id: string; patientId: string; patientName?: string; subject?: string; + status: string; startedAt: string; +} + +export function ConsultationListPage() { + const [consultations, setConsultations] = useState([]); + + useEffect(() => { + api.get('/api/consultations').then((r) => { + const mapped = r.data.map((c) => ({ + id: c.id, + patientId: c.patientId, + patientName: c.patientName || 'unknown', + subject: c.subject || 'online consult', + status: c.status, + startedAt: c.startedAt, + })); + setConsultations(mapped); + }).catch(() => {}); + }, []); + + return ( +
+

在线问诊

+ +
+ {consultations.map((c) => ( + +
+
{c.patientName}
+
{c.subject}
+
+
+ + {c.status === 'active' ? '进行中' : '已结束'} + +
+ {c.startedAt?.split('T')[0]} +
+
+ + ))} + {consultations.length === 0 && ( +
暂无问诊记录
+ )} +
+
+ ); +} diff --git a/frontend-doctor/src/pages/dashboard/DashboardPage.tsx b/frontend-doctor/src/pages/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..94fb7f6 --- /dev/null +++ b/frontend-doctor/src/pages/dashboard/DashboardPage.tsx @@ -0,0 +1,102 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../../services/api-client'; +import { useAuthStore } from '../../stores/auth.store'; +import type { DashboardStats } from '../../types'; + +interface RawPatient { id: string; name: string; phone: string; } +interface RawConsultation { id: string; status: string; patientName: string; subject: string; } +interface RawFollowUp { id: string; scheduledAt: string; title: string; status: string; } +interface RawReport { id: string; title: string; status: string; } + +export function DashboardPage() { + const user = useAuthStore((s) => s.user); + const [stats, setStats] = useState({ + totalPatients: 0, activeConsultations: 0, pendingReports: 0, + todayFollowUps: 0, weeklyNewPatients: 0, weeklyConsultations: 0, + }); + + useEffect(() => { + async function loadStats() { + try { + const [patients, consultations, reports, followUps] = await Promise.all([ + api.get('/api/patients'), + api.get('/api/consultations'), + api.get('/api/reports?status=pending'), + api.get('/api/follow-ups'), + ]); + setStats({ + totalPatients: patients.data.length, + activeConsultations: consultations.data.filter((c) => c.status === 'active').length, + pendingReports: reports.data.length, + todayFollowUps: followUps.data.filter((f) => { + const d = (f.scheduledAt as string)?.split('T')[0]; + return d === new Date().toISOString().split('T')[0]; + }).length, + weeklyNewPatients: 0, + weeklyConsultations: consultations.data.length, + }); + } catch { /* ignore */ } + } + loadStats(); + }, []); + + return ( +
+

欢迎回来,{user?.name}

+ +
+ {[ + { label: '患者总数', value: stats.totalPatients, color: '#1976d2' }, + { label: '进行中问诊', value: stats.activeConsultations, color: '#388e3c' }, + { label: '待审核报告', value: stats.pendingReports, color: '#f57c00' }, + { label: '今日随访', value: stats.todayFollowUps, color: '#7b1fa2' }, + ].map((item) => ( +
+
{item.value}
+
{item.label}
+
+ ))} +
+ +
+
+

快捷操作

+
+ {[ + { label: '患者列表', href: '/patients' }, + { label: '在线问诊', href: '/consultations' }, + { label: '报告审核', href: '/reports' }, + { label: '随访管理', href: '/follow-ups' }, + ].map((action) => ( + + {action.label} → + + ))} +
+
+ +
+

今日待办

+
    +
  • + 📋 待审核报告: {stats.pendingReports} 份 +
  • +
  • + 💬 进行中问诊: {stats.activeConsultations} 个 +
  • +
  • + 📅 今日随访: {stats.todayFollowUps} 项 +
  • +
+
+
+
+ ); +} diff --git a/frontend-doctor/src/pages/followups/FollowUpEditPage.tsx b/frontend-doctor/src/pages/followups/FollowUpEditPage.tsx new file mode 100644 index 0000000..f0942e9 --- /dev/null +++ b/frontend-doctor/src/pages/followups/FollowUpEditPage.tsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { api } from '../../services/api-client'; + +export function FollowUpEditPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const isNew = !id || id === 'new'; + + const [title, setTitle] = useState(''); + const [patientId, setPatientId] = useState(''); + const [scheduledAt, setScheduledAt] = useState(''); + const [notes, setNotes] = useState(''); + const [patients, setPatients] = useState<{ id: string; name: string }[]>([]); + + useEffect(() => { + api.get<{ id: string; name: string }[]>('/api/patients').then((r) => setPatients(r.data)); + if (!isNew) { + api.get>(`/api/follow-ups/${id}`).then((r) => { + setTitle(r.data.title as string); + setPatientId(r.data.patientId as string); + setScheduledAt((r.data.scheduledAt as string)?.slice(0, 16) || ''); + setNotes((r.data.notes as string) || ''); + }).catch(() => {}); + } + }, [id, isNew]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const body = { title, patientId, scheduledAt, notes }; + try { + if (isNew) { + await api.post('/api/follow-ups', body); + } else { + await api.put(`/api/follow-ups/${id}`, body); + } + navigate('/follow-ups'); + } catch { alert('操作失败'); } + }; + + return ( +
+

{isNew ? '新建随访' : '编辑随访'}

+ +
+
+ + setTitle(e.target.value)} required + style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }} /> +
+ +
+ + +
+ +
+ + setScheduledAt(e.target.value)} required + style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 4 }} /> +
+ +
+ +