From 693c66c25892bcd1d40fd4b8a6cd9a772f13b9d2 Mon Sep 17 00:00:00 2001 From: danaisuiyuan Date: Mon, 11 May 2026 10:04:07 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(integral):=20=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E5=A5=96=E9=87=91=E9=87=8D=E5=A4=8D=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将个人奖金转积分流程改为先写唯一流水再加积分,并用 wa_selfbonus_logid 唯一索引兜底多入口并发场景;同时补充历史重复数据修复与索引落地 SQL 脚本。 Co-authored-by: Cursor --- .../UserIntegralConcurrencyServiceImpl.java | 251 +----------------- .../service/impl/WaSelfbonusServiceImpl.java | 236 ++++++++-------- ...unique_index_uk_integral_selfbonus_log.sql | 23 ++ ...x_duplicate_selfbonus_integral_records.sql | 130 +++++++++ 4 files changed, 262 insertions(+), 378 deletions(-) create mode 100644 backend/sql/add_unique_index_uk_integral_selfbonus_log.sql create mode 100644 backend/sql/fix_duplicate_selfbonus_integral_records.sql diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserIntegralConcurrencyServiceImpl.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserIntegralConcurrencyServiceImpl.java index a0054b3..eca6b9e 100644 --- a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserIntegralConcurrencyServiceImpl.java +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserIntegralConcurrencyServiceImpl.java @@ -1,265 +1,22 @@ package com.zbkj.service.service.impl; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.zbkj.common.model.consignment.WaSelfbonusLog; -import com.zbkj.common.model.user.User; -import com.zbkj.common.model.user.UserIntegralRecord; -import com.zbkj.common.utils.CrmebUtil; -import com.zbkj.service.dao.UserDao; -import com.zbkj.service.dao.UserIntegralRecordDao; -import com.zbkj.service.dao.consignment.WaSelfbonusLogDao; -import com.zbkj.service.service.UserService; import com.zbkj.service.service.WaSelfbonusSyncService; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.Date; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** - * 用户积分并发安全服务实现类 - * 专门处理高并发场景下的积分更新,避免数据库锁等待超时 + * 保留并发安全服务 bean 名称兼容历史调用。 + * 实际逻辑统一委托到 waSelfbonusSyncService,避免双实现规则漂移。 */ -@Slf4j @Service("userIntegralConcurrencyService") public class UserIntegralConcurrencyServiceImpl { @Autowired - private WaSelfbonusLogDao waSelfbonusLogDao; + private WaSelfbonusSyncService waSelfbonusSyncService; - @Autowired - private UserIntegralRecordDao userIntegralRecordDao; - - @Autowired - private UserDao userDao; - - @Autowired - private UserService userService; - - // 使用ConcurrentHashMap来缓存正在处理的用户ID,防止重复处理 - private final ConcurrentHashMap processingUsers = new ConcurrentHashMap<>(); - - /** - * 同步个人奖金变动到用户积分 - 并发安全版本 - * 根据个人奖金变动记录,为对应的用户增加积分(奖金金额的50%) - */ - @Transactional(rollbackFor = Exception.class) public Map syncSelfbonusToIntegral() { - log.info("开始同步个人奖金变动到用户积分(并发安全版)"); - - int successCount = 0; - int skipCount = 0; - int failCount = 0; - - try { - // 查询最新的个人奖金变动记录 - LambdaQueryWrapper bonusLogWrapper = new LambdaQueryWrapper<>(); - bonusLogWrapper.orderByDesc(WaSelfbonusLog::getCreatedAt); // 按创建时间倒序 - bonusLogWrapper.last("LIMIT 500"); // 限制查询500条,避免一次性处理过多 - List bonusLogList = waSelfbonusLogDao.selectList(bonusLogWrapper); - - for (WaSelfbonusLog bonusLog : bonusLogList) { - try { - // 检查该奖金记录是否已经处理过 - LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); - checkWrapper.eq(UserIntegralRecord::getWaSelfbonusLogid, bonusLog.getId()); - Integer existCount = userIntegralRecordDao.selectCount(checkWrapper); - - if (existCount != null && existCount > 0) { - log.debug("奖金记录已处理,跳过: bonusLogId={}, userId={}", bonusLog.getId(), bonusLog.getUserId()); - skipCount++; - continue; - } - - // 获取用户ID - Integer ebUserId = bonusLog.getUserId(); - - // 使用同步块确保同一用户不会被重复处理 - Object lock = processingUsers.computeIfAbsent(ebUserId, k -> new Object()); - synchronized (lock) { - try { - // 再次检查积分记录是否已存在(双重检查) - existCount = userIntegralRecordDao.selectCount(checkWrapper); - if (existCount != null && existCount > 0) { - log.debug("奖金记录已在其他线程处理,跳过: bonusLogId={}", bonusLog.getId()); - skipCount++; - continue; - } - - // 查询用户信息 - User user = userDao.selectById(ebUserId); - if (user == null) { - log.warn("未找到对应的系统用户,跳过: waUserId={}", bonusLog.getUserId()); - skipCount++; - continue; - } - - // 验证奖金类型和金额 - if (!isValidBonusLog(bonusLog)) { - log.debug("奖金记录不符合处理条件,跳过: bonusLogId={}, type={}, amount={}", - bonusLog.getId(), bonusLog.getType(), bonusLog.getMoney()); - skipCount++; - continue; - } - - // 计算积分值 - BigDecimal integralValue = calculateIntegralValue(bonusLog.getMoney()); - if (integralValue.compareTo(BigDecimal.ZERO) <= 0) { - log.debug("计算出的积分为0或负数,跳过: bonusLogId={}, integralValue={}", - bonusLog.getId(), integralValue); - skipCount++; - continue; - } - - // 使用CAS方式更新积分,避免锁竞争 - Boolean updateResult = updateIntegralWithRetry(user.getUid(), integralValue, "add", 3); - - if (!updateResult) { - log.error("更新用户积分失败(重试后): userId={}, integralValue={}", user.getUid(), integralValue); - failCount++; - continue; - } - - // 插入积分记录 - UserIntegralRecord integralRecord = createUserIntegralRecord(user.getUid(), bonusLog, integralValue); - int insertResult = userIntegralRecordDao.insert(integralRecord); - if (insertResult <= 0) { - log.error("插入积分记录失败: userId={}, bonusLogId={}", user.getUid(), bonusLog.getId()); - failCount++; - continue; - } - - successCount++; - log.info("成功同步奖金到积分: bonusLogId={}, userId={}, bonusAmount={}, integralValue={}", - bonusLog.getId(), user.getUid(), bonusLog.getMoney(), integralValue); - - } finally { - // 清理锁对象 - processingUsers.remove(ebUserId); - } - } - - } catch (Exception e) { - failCount++; - log.error("处理奖金记录失败: bonusLogId={}, error={}", bonusLog.getId(), e.getMessage(), e); - } - } - - Map result = new HashMap<>(); - result.put("total", bonusLogList.size()); - result.put("successCount", successCount); - result.put("skipCount", skipCount); - result.put("failCount", failCount); - - log.info("同步个人奖金变动到用户积分完成(并发安全版): 总数={}, 成功={}, 跳过={}, 失败={}", - bonusLogList.size(), successCount, skipCount, failCount); - - result.put("message", "同步完成"); - return result; - - } catch (Exception e) { - log.error("同步个人奖金变动到用户积分异常", e); - Map result = new HashMap<>(); - result.put("message", "同步失败: " + e.getMessage()); - result.put("error", e.getMessage()); - return result; - } - } - - /** - * 验证奖金记录是否符合处理条件 - */ - private boolean isValidBonusLog(WaSelfbonusLog bonusLog) { - // 只处理收入类型(type=1) - if (bonusLog.getType() == null || bonusLog.getType() != 1) { - return false; - } - - // 验证奖金金额有效 - if (bonusLog.getMoney() == null || bonusLog.getMoney().compareTo(BigDecimal.ZERO) <= 0) { - return false; - } - - return true; - } - - /** - * 计算积分值 - */ - private BigDecimal calculateIntegralValue(BigDecimal bonusAmount) { - // 计算积分:奖金金额 * 50%,向下取整 - BigDecimal integralDecimal = bonusAmount.multiply(new BigDecimal("0.5")); - return integralDecimal.setScale(3, RoundingMode.DOWN); - } - - /** - * 创建积分记录对象 - */ - private UserIntegralRecord createUserIntegralRecord(Integer userId, WaSelfbonusLog bonusLog, BigDecimal integralValue) { - User user = userDao.selectById(userId); // 重新查询用户获取最新积分 - Integer newIntegral = user != null && user.getIntegral() != null ? user.getIntegral().intValue() : 0; - - UserIntegralRecord integralRecord = new UserIntegralRecord(); - integralRecord.setUid(userId); - integralRecord.setLinkId(String.valueOf(bonusLog.getId())); // 关联奖金记录ID - integralRecord.setLinkType("selfbonus"); // 关联类型:个人奖金 - integralRecord.setType(1); // 类型:1-增加 - integralRecord.setTitle("个人奖金奖励"); - integralRecord.setIntegral(integralValue); - integralRecord.setBalance(newIntegral); // 实际上应该是更新后的积分,这里可能需要调整 - integralRecord.setMark(String.format("个人奖金变动奖励,奖金金额:%.3f,积分:%d", - bonusLog.getMoney(), integralValue.intValue())); - integralRecord.setStatus(3); // 状态:3-完成 - integralRecord.setWaSelfbonusLogid(bonusLog.getId()); // 关联个人奖金记录ID - integralRecord.setCreateTime(new Date()); - integralRecord.setUpdateTime(new Date()); - - return integralRecord; - } - - /** - * 带重试机制的积分更新 - */ - private Boolean updateIntegralWithRetry(Integer uid, BigDecimal integral, String type, int maxRetries) { - int attempts = 0; - Exception lastException = null; - - while (attempts < maxRetries) { - try { - attempts++; - - // 直接更新积分,不再依赖乐观锁 - Boolean result = userService.operationIntegral(uid, integral, BigDecimal.ZERO, type); - - if (result) { - return true; - } else { - log.warn("积分更新失败,准备重试 (attempt {}/{})", attempts, maxRetries); - } - } catch (Exception e) { - lastException = e; - log.warn("积分更新异常,准备重试 (attempt {}/{}), error: {}", attempts, maxRetries, e.getMessage()); - - // 如果是数据库锁等待超时,等待一段时间再重试 - if (e.getMessage() != null && e.getMessage().contains("Lock wait timeout")) { - try { - Thread.sleep(100 * attempts); // 指数退避 - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - break; - } - } - } - } - - log.error("积分更新达到最大重试次数仍然失败,最后一次异常: ", lastException); - return false; + return waSelfbonusSyncService.syncSelfbonusToIntegral(); } } \ No newline at end of file diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaSelfbonusServiceImpl.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaSelfbonusServiceImpl.java index 300f43e..57f287e 100644 --- a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaSelfbonusServiceImpl.java +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/WaSelfbonusServiceImpl.java @@ -11,8 +11,8 @@ import com.zbkj.service.service.UserService; import com.zbkj.service.service.WaSelfbonusSyncService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import java.math.BigDecimal; @@ -44,12 +44,14 @@ public class WaSelfbonusServiceImpl implements WaSelfbonusSyncService { @Autowired private UserService userService; + @Autowired + private TransactionTemplate transactionTemplate; + /** * 同步个人奖金变动到用户积分 * 根据个人奖金变动记录,为对应的用户增加积分(奖金金额的50%) */ @Override - @Transactional(rollbackFor = Exception.class) public Map syncSelfbonusToIntegral() { log.info("开始同步个人奖金变动到用户积分"); @@ -66,101 +68,16 @@ public class WaSelfbonusServiceImpl implements WaSelfbonusSyncService { for (WaSelfbonusLog bonusLog : bonusLogList) { try { - // 检查该奖金记录是否已经处理过(通过 waSelfbonusLogid 字段查询积分记录) - LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); - checkWrapper.eq(UserIntegralRecord::getWaSelfbonusLogid, bonusLog.getId()); - Integer existCount = userIntegralRecordDao.selectCount(checkWrapper); - - if (existCount != null && existCount > 0) { - // 已处理过,跳过 - log.debug("奖金记录已处理,跳过: bonusLogId={}, userId={}", bonusLog.getId(), bonusLog.getUserId()); + ProcessStatus processStatus = transactionTemplate.execute(status -> processBonusLogAtomically(bonusLog)); + if (processStatus == ProcessStatus.SKIP) { skipCount++; continue; } - - // 根据 wa_users 的 user_id 查找对应的 eb_user 表的 uid - // 注意:wa_users.id 对应 eb_user.uid(在同步时已建立关联) - Integer ebUserId = bonusLog.getUserId(); // wa_users.id 就是 eb_user.uid - - // 查询 eb_user 表中的用户 - User user = userDao.selectById(ebUserId); - if (user == null) { - log.warn("未找到对应的系统用户,跳过: waUserId={}", bonusLog.getUserId()); - skipCount++; + if (processStatus == ProcessStatus.SUCCESS) { + successCount++; continue; } - - // 计算积分值:个人奖金变更金额的50%(只处理收入类型的奖金变动) - if (bonusLog.getType() == null || bonusLog.getType() != 1) { - // 只处理收入类型(type=1),支出类型不处理 - log.debug("跳过非收入类型的奖金变动: bonusLogId={}, type={}", bonusLog.getId(), bonusLog.getType()); - skipCount++; - continue; - } - - // 奖金金额(收入为正数) - BigDecimal bonusAmount = bonusLog.getMoney(); - if (bonusAmount == null || bonusAmount.compareTo(BigDecimal.ZERO) <= 0) { - log.debug("奖金金额无效,跳过: bonusLogId={}, money={}", bonusLog.getId(), bonusAmount); - skipCount++; - continue; - } - - // 计算积分:奖金金额 * 50%,向下取整 - BigDecimal integralDecimal = bonusAmount.multiply(new BigDecimal("0.5")); - - BigDecimal integralValue = integralDecimal.setScale(3, RoundingMode.DOWN); - - if (integralValue.compareTo(BigDecimal.ZERO) <= 0) { - log.debug("计算出的积分为0,跳过: bonusLogId={}, integralValue={}", bonusLog.getId(), integralValue); - skipCount++; - continue; - } - - // 更新用户积分 - 不再需要当前积分值作为乐观锁条件 - Boolean updateResult = userService.operationIntegral( - user.getUid(), - integralValue, - BigDecimal.valueOf(0), // 不再使用当前积分作为乐观锁条件 - "add" - ); - - if (!updateResult) { - log.error("更新用户积分失败: userId={}, integralValue={}", user.getUid(), integralValue); - failCount++; - continue; - } - - // 重新查询用户获取最新积分 - user = userDao.selectById(user.getUid()); - Integer newIntegral = user.getIntegral() != null ? user.getIntegral().intValue() : 0; - - // 新增积分记录 - UserIntegralRecord integralRecord = new UserIntegralRecord(); - integralRecord.setUid(user.getUid()); - integralRecord.setLinkId(String.valueOf(bonusLog.getId())); // 关联奖金记录ID - integralRecord.setLinkType("selfbonus"); // 关联类型:个人奖金 - integralRecord.setType(1); // 类型:1-增加 - integralRecord.setTitle("个人奖金奖励"); - integralRecord.setIntegral(integralValue); - integralRecord.setBalance(newIntegral); - integralRecord.setMark(String.format("个人奖金变动奖励,奖金金额:%.3f,积分:%d", - bonusAmount, integralValue.intValue())); - integralRecord.setStatus(3); // 状态:3-完成 - integralRecord.setWaSelfbonusLogid(bonusLog.getId()); // 关联个人奖金记录ID - integralRecord.setCreateTime(new Date()); - integralRecord.setUpdateTime(new Date()); - - int insertResult = userIntegralRecordDao.insert(integralRecord); - if (insertResult <= 0) { - log.error("插入积分记录失败: userId={}, bonusLogId={}", user.getUid(), bonusLog.getId()); - failCount++; - continue; - } - - successCount++; - log.info("成功同步奖金到积分: bonusLogId={}, userId={}, bonusAmount={}, integralValue={}", - bonusLog.getId(), user.getUid(), bonusAmount, integralValue); + failCount++; } catch (Exception e) { failCount++; @@ -188,47 +105,104 @@ public class WaSelfbonusServiceImpl implements WaSelfbonusSyncService { return result; } } - + /** - * 带重试机制的用户积分更新 + * 原子处理单条奖金日志: + * 1) 先插入积分流水(受唯一索引保护) + * 2) 插入成功后再更新用户总积分 + * 3) 回填本条流水的 balance */ - private Boolean updateUserIntegralWithRetry(Integer uid, BigDecimal integralValue, int maxRetries) { - int attempts = 0; - Exception lastException = null; - - while (attempts < maxRetries) { - try { - attempts++; - - Boolean result = userService.operationIntegral( - uid, - integralValue, - BigDecimal.ZERO, // 不再使用当前积分作为乐观锁条件 - "add" - ); - - if (result) { - return true; - } else { - log.warn("积分更新失败,准备重试 (attempt {}/{})", attempts, maxRetries); - } - } catch (Exception e) { - lastException = e; - log.warn("积分更新异常,准备重试 (attempt {}/{}), error: {}", attempts, maxRetries, e.getMessage()); - - // 如果是数据库锁等待超时,等待一段时间再重试 - if (e.getMessage() != null && e.getMessage().contains("Lock wait timeout")) { - try { - Thread.sleep(100 * attempts); // 指数退避 - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - break; - } - } - } + private ProcessStatus processBonusLogAtomically(WaSelfbonusLog bonusLog) { + if (isAlreadyProcessed(bonusLog.getId())) { + log.debug("奖金记录已处理,跳过: bonusLogId={}, userId={}", bonusLog.getId(), bonusLog.getUserId()); + return ProcessStatus.SKIP; } - - log.error("积分更新达到最大重试次数仍然失败,最后一次异常: ", lastException); - return false; + + Integer ebUserId = bonusLog.getUserId(); + User user = userDao.selectById(ebUserId); + if (user == null) { + log.warn("未找到对应的系统用户,跳过: waUserId={}", bonusLog.getUserId()); + return ProcessStatus.SKIP; + } + + if (bonusLog.getType() == null || bonusLog.getType() != 1) { + log.debug("跳过非收入类型的奖金变动: bonusLogId={}, type={}", bonusLog.getId(), bonusLog.getType()); + return ProcessStatus.SKIP; + } + + BigDecimal bonusAmount = bonusLog.getMoney(); + if (bonusAmount == null || bonusAmount.compareTo(BigDecimal.ZERO) <= 0) { + log.debug("奖金金额无效,跳过: bonusLogId={}, money={}", bonusLog.getId(), bonusAmount); + return ProcessStatus.SKIP; + } + + BigDecimal integralValue = bonusAmount.multiply(new BigDecimal("0.5")).setScale(3, RoundingMode.DOWN); + if (integralValue.compareTo(BigDecimal.ZERO) <= 0) { + log.debug("计算出的积分为0,跳过: bonusLogId={}, integralValue={}", bonusLog.getId(), integralValue); + return ProcessStatus.SKIP; + } + + UserIntegralRecord integralRecord = buildIntegralRecord(user.getUid(), bonusLog, bonusAmount, integralValue); + try { + int insertResult = userIntegralRecordDao.insert(integralRecord); + if (insertResult <= 0) { + throw new IllegalStateException("插入积分记录失败"); + } + } catch (DuplicateKeyException duplicateKeyException) { + // 数据库唯一索引兜底,保证多入口并发只处理一次 + log.info("奖金记录并发重复处理,已跳过: bonusLogId={}, userId={}", bonusLog.getId(), user.getUid()); + return ProcessStatus.SKIP; + } + + Boolean updateResult = userService.operationIntegral( + user.getUid(), + integralValue, + BigDecimal.ZERO, + "add" + ); + + if (!updateResult) { + throw new IllegalStateException(String.format("更新用户积分失败: userId=%s, integralValue=%s", user.getUid(), integralValue)); + } + + User latestUser = userDao.selectById(user.getUid()); + Integer latestIntegral = latestUser != null && latestUser.getIntegral() != null ? latestUser.getIntegral().intValue() : 0; + integralRecord.setBalance(latestIntegral); + integralRecord.setUpdateTime(new Date()); + userIntegralRecordDao.updateById(integralRecord); + + log.info("成功同步奖金到积分: bonusLogId={}, userId={}, bonusAmount={}, integralValue={}", + bonusLog.getId(), user.getUid(), bonusAmount, integralValue); + return ProcessStatus.SUCCESS; + } + + private boolean isAlreadyProcessed(Integer waSelfbonusLogId) { + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(UserIntegralRecord::getWaSelfbonusLogid, waSelfbonusLogId); + Integer existCount = userIntegralRecordDao.selectCount(checkWrapper); + return existCount != null && existCount > 0; + } + + private UserIntegralRecord buildIntegralRecord(Integer uid, WaSelfbonusLog bonusLog, BigDecimal bonusAmount, BigDecimal integralValue) { + UserIntegralRecord integralRecord = new UserIntegralRecord(); + integralRecord.setUid(uid); + integralRecord.setLinkId(String.valueOf(bonusLog.getId())); + integralRecord.setLinkType("selfbonus"); + integralRecord.setType(1); + integralRecord.setTitle("个人奖金奖励"); + integralRecord.setIntegral(integralValue); + integralRecord.setBalance(0); + integralRecord.setMark(String.format("个人奖金变动奖励,奖金金额:%.3f,积分:%.3f", + bonusAmount, integralValue)); + integralRecord.setStatus(3); + integralRecord.setWaSelfbonusLogid(bonusLog.getId()); + integralRecord.setCreateTime(new Date()); + integralRecord.setUpdateTime(new Date()); + return integralRecord; + } + + private enum ProcessStatus { + SUCCESS, + SKIP } } diff --git a/backend/sql/add_unique_index_uk_integral_selfbonus_log.sql b/backend/sql/add_unique_index_uk_integral_selfbonus_log.sql new file mode 100644 index 0000000..cfe5f52 --- /dev/null +++ b/backend/sql/add_unique_index_uk_integral_selfbonus_log.sql @@ -0,0 +1,23 @@ +-- Add strong idempotency guard for selfbonus -> integral conversion. +-- This index guarantees one wa_selfbonus_log can map to at most one integral record. +-- Prerequisite: clear duplicate wa_selfbonus_logid rows first. + +-- Pre-checks +SELECT wa_selfbonus_logid, COUNT(*) AS cnt +FROM eb_user_integral_record +WHERE wa_selfbonus_logid IS NOT NULL +GROUP BY wa_selfbonus_logid +HAVING COUNT(*) > 1 +LIMIT 20; + +SELECT COUNT(*) AS zero_cnt +FROM eb_user_integral_record +WHERE wa_selfbonus_logid = 0; + +-- Apply unique index +ALTER TABLE eb_user_integral_record +ADD UNIQUE KEY uk_integral_selfbonus_log (wa_selfbonus_logid); + +-- Verify index exists +SHOW INDEX FROM eb_user_integral_record +WHERE Key_name = 'uk_integral_selfbonus_log'; diff --git a/backend/sql/fix_duplicate_selfbonus_integral_records.sql b/backend/sql/fix_duplicate_selfbonus_integral_records.sql new file mode 100644 index 0000000..8879c7d --- /dev/null +++ b/backend/sql/fix_duplicate_selfbonus_integral_records.sql @@ -0,0 +1,130 @@ +-- Purpose: +-- 1) Find duplicate selfbonus integral rows generated from same wa_selfbonus_log +-- 2) Backup affected data +-- 3) Keep min(id), delete duplicate rows +-- 4) Resync eb_user.integral from remaining ledger rows +-- 5) Rebuild integer balance snapshot for affected users +-- +-- Notes: +-- - Designed for MySQL 5.7 +-- - Run during low traffic window +-- - Review backup table names before execution + +-- 0) Preview duplicate groups +SELECT uid, + wa_selfbonus_logid, + link_id, + COUNT(*) AS cnt, + SUM(integral) AS total_integral, + MIN(id) AS keep_id, + GROUP_CONCAT(id ORDER BY id) AS record_ids +FROM eb_user_integral_record +WHERE link_type = 'selfbonus' + AND type = 1 + AND wa_selfbonus_logid IS NOT NULL +GROUP BY uid, wa_selfbonus_logid, link_id +HAVING COUNT(*) > 1; + +-- 1) Backup duplicate rows and affected users +DROP TABLE IF EXISTS backup_euir_selfbonus_dups_20260511_0959; +CREATE TABLE backup_euir_selfbonus_dups_20260511_0959 AS +SELECT e.* +FROM eb_user_integral_record e +JOIN ( + SELECT uid, wa_selfbonus_logid, link_id, MIN(id) AS keep_id, COUNT(*) AS cnt + FROM eb_user_integral_record + WHERE link_type = 'selfbonus' + AND type = 1 + AND wa_selfbonus_logid IS NOT NULL + GROUP BY uid, wa_selfbonus_logid, link_id + HAVING COUNT(*) > 1 +) d + ON d.uid = e.uid + AND d.wa_selfbonus_logid = e.wa_selfbonus_logid + AND d.link_id = e.link_id; + +DROP TABLE IF EXISTS backup_eb_user_integral_before_fix_20260511_0959; +CREATE TABLE backup_eb_user_integral_before_fix_20260511_0959 AS +SELECT u.* +FROM eb_user u +WHERE u.uid IN ( + SELECT DISTINCT uid FROM backup_euir_selfbonus_dups_20260511_0959 +); + +-- 2) Deduplicate + resync in one transaction +START TRANSACTION; + +DROP TEMPORARY TABLE IF EXISTS tmp_dup_groups; +CREATE TEMPORARY TABLE tmp_dup_groups AS +SELECT uid, wa_selfbonus_logid, link_id, MIN(id) AS keep_id +FROM eb_user_integral_record +WHERE link_type = 'selfbonus' + AND type = 1 + AND wa_selfbonus_logid IS NOT NULL +GROUP BY uid, wa_selfbonus_logid, link_id +HAVING COUNT(*) > 1; + +DROP TEMPORARY TABLE IF EXISTS tmp_affected_uids; +CREATE TEMPORARY TABLE tmp_affected_uids AS +SELECT DISTINCT uid FROM tmp_dup_groups; + +DELETE e +FROM eb_user_integral_record e +JOIN tmp_dup_groups d + ON d.uid = e.uid + AND d.wa_selfbonus_logid = e.wa_selfbonus_logid + AND d.link_id = e.link_id +WHERE e.id <> d.keep_id; + +UPDATE eb_user u +JOIN ( + SELECT r.uid, COALESCE(SUM(r.integral), 0) AS sum_integral + FROM eb_user_integral_record r + JOIN tmp_affected_uids t ON t.uid = r.uid + GROUP BY r.uid +) s ON s.uid = u.uid +SET u.integral = s.sum_integral; + +SET @run_uid := 0; +SET @run_bal := 0; +UPDATE eb_user_integral_record e +JOIN ( + SELECT t.id, FLOOR(t.running) AS new_balance + FROM ( + SELECT s.id, + s.uid, + (@run_bal := IF(@run_uid = s.uid, @run_bal + s.integral, s.integral)) AS running, + (@run_uid := s.uid) AS uid_guard + FROM ( + SELECT id, uid, integral + FROM eb_user_integral_record + WHERE uid IN (SELECT uid FROM tmp_affected_uids) + ORDER BY uid, id + ) s + ) t +) x ON x.id = e.id +SET e.balance = x.new_balance; + +COMMIT; + +-- 3) Post-checks +SELECT COUNT(*) AS remaining_dup_groups +FROM ( + SELECT 1 + FROM eb_user_integral_record + WHERE link_type = 'selfbonus' + AND type = 1 + AND wa_selfbonus_logid IS NOT NULL + GROUP BY uid, wa_selfbonus_logid, link_id + HAVING COUNT(*) > 1 +) a; + +SELECT u.uid, u.integral, s.sum_integral +FROM eb_user u +JOIN ( + SELECT uid, SUM(integral) AS sum_integral + FROM eb_user_integral_record + GROUP BY uid +) s ON s.uid = u.uid +WHERE u.uid IN (SELECT DISTINCT uid FROM backup_euir_selfbonus_dups_20260511_0959) +ORDER BY u.uid; From 403ffe0fdeca217aa479f76bc3e7a0b6a84cbc50 Mon Sep 17 00:00:00 2001 From: danaisuiyuan Date: Mon, 11 May 2026 13:07:40 +0800 Subject: [PATCH 2/3] feat(dashboard): add boss dashboard H5 and APIs Implement the mobile dashboard frontend, admin overview APIs, report archive export, and local dev proxy so the boss dashboard can run against real backend data while preserving MSW demos. Co-authored-by: Cursor --- .../zbkj/admin/config/WebSecurityConfig.java | 2 + .../controller/BossDashboardController.java | 50 + .../dashboard/BossDashboardResponse.java | 106 + .../service/service/BossDashboardService.java | 25 + .../impl/BossDashboardServiceImpl.java | 564 +++ dashboard-frontend/.env.development | 5 + dashboard-frontend/.gitignore | 24 + dashboard-frontend/README.md | 122 + dashboard-frontend/eslint.config.js | 22 + dashboard-frontend/index.html | 13 + dashboard-frontend/package.json | 50 + dashboard-frontend/pnpm-lock.yaml | 3461 +++++++++++++++++ dashboard-frontend/public/favicon.svg | 1 + dashboard-frontend/public/icons.svg | 24 + .../public/mockServiceWorker.js | 349 ++ dashboard-frontend/src/App.tsx | 27 + .../src/app/layouts/MobileLayout.tsx | 34 + .../src/app/providers/AppProviders.tsx | 26 + .../src/components/charts/MiniTrendChart.tsx | 76 + .../src/components/kpi/KpiCard.tsx | 32 + .../src/features/boss-dashboard/api.ts | 18 + .../boss-dashboard/components/RankList.tsx | 38 + .../components/RiskAlertSection.tsx | 42 + .../components/TodaySnapshotSection.tsx | 64 + .../src/features/boss-dashboard/mock.ts | 104 + .../pages/BossDashboardPage.tsx | 105 + .../boss-dashboard/pages/OperationsPages.tsx | 468 +++ .../src/features/boss-dashboard/types.ts | 72 + .../src/features/common/PlaceholderPage.tsx | 18 + dashboard-frontend/src/index.css | 589 +++ dashboard-frontend/src/main.tsx | 20 + .../src/services/http/client.ts | 26 + .../src/services/mock/browser.ts | 4 + .../src/services/mock/handlers.ts | 58 + dashboard-frontend/src/utils/format.test.ts | 22 + dashboard-frontend/src/utils/format.ts | 33 + dashboard-frontend/tsconfig.app.json | 25 + dashboard-frontend/tsconfig.json | 7 + dashboard-frontend/tsconfig.node.json | 24 + dashboard-frontend/vite.config.ts | 17 + 40 files changed, 6767 insertions(+) create mode 100644 backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/BossDashboardController.java create mode 100644 backend/crmeb-common/src/main/java/com/zbkj/common/response/dashboard/BossDashboardResponse.java create mode 100644 backend/crmeb-service/src/main/java/com/zbkj/service/service/BossDashboardService.java create mode 100644 backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/BossDashboardServiceImpl.java create mode 100644 dashboard-frontend/.env.development create mode 100644 dashboard-frontend/.gitignore create mode 100644 dashboard-frontend/README.md create mode 100644 dashboard-frontend/eslint.config.js create mode 100644 dashboard-frontend/index.html create mode 100644 dashboard-frontend/package.json create mode 100644 dashboard-frontend/pnpm-lock.yaml create mode 100644 dashboard-frontend/public/favicon.svg create mode 100644 dashboard-frontend/public/icons.svg create mode 100644 dashboard-frontend/public/mockServiceWorker.js create mode 100644 dashboard-frontend/src/App.tsx create mode 100644 dashboard-frontend/src/app/layouts/MobileLayout.tsx create mode 100644 dashboard-frontend/src/app/providers/AppProviders.tsx create mode 100644 dashboard-frontend/src/components/charts/MiniTrendChart.tsx create mode 100644 dashboard-frontend/src/components/kpi/KpiCard.tsx create mode 100644 dashboard-frontend/src/features/boss-dashboard/api.ts create mode 100644 dashboard-frontend/src/features/boss-dashboard/components/RankList.tsx create mode 100644 dashboard-frontend/src/features/boss-dashboard/components/RiskAlertSection.tsx create mode 100644 dashboard-frontend/src/features/boss-dashboard/components/TodaySnapshotSection.tsx create mode 100644 dashboard-frontend/src/features/boss-dashboard/mock.ts create mode 100644 dashboard-frontend/src/features/boss-dashboard/pages/BossDashboardPage.tsx create mode 100644 dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx create mode 100644 dashboard-frontend/src/features/boss-dashboard/types.ts create mode 100644 dashboard-frontend/src/features/common/PlaceholderPage.tsx create mode 100644 dashboard-frontend/src/index.css create mode 100644 dashboard-frontend/src/main.tsx create mode 100644 dashboard-frontend/src/services/http/client.ts create mode 100644 dashboard-frontend/src/services/mock/browser.ts create mode 100644 dashboard-frontend/src/services/mock/handlers.ts create mode 100644 dashboard-frontend/src/utils/format.test.ts create mode 100644 dashboard-frontend/src/utils/format.ts create mode 100644 dashboard-frontend/tsconfig.app.json create mode 100644 dashboard-frontend/tsconfig.json create mode 100644 dashboard-frontend/tsconfig.node.json create mode 100644 dashboard-frontend/vite.config.ts diff --git a/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java b/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java index 0ab04c6..8b139c7 100644 --- a/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java +++ b/backend/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java @@ -148,6 +148,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .antMatchers("/api/admin/store/product/copy/**").permitAll() .antMatchers("/api/admin/merchandise/select").permitAll() .antMatchers("/api/admin/merchandise/update").permitAll() + // 老板驾驶舱独立 H5 页面接口,本机演示和报表归档使用 + .antMatchers("/api/admin/dashboard/**").permitAll() // 积分模块外部免认证只读接口(供 /integral-external/* 页面调用) .antMatchers("/api/external/integral/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 diff --git a/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/BossDashboardController.java b/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/BossDashboardController.java new file mode 100644 index 0000000..1fb4748 --- /dev/null +++ b/backend/crmeb-admin/src/main/java/com/zbkj/admin/controller/BossDashboardController.java @@ -0,0 +1,50 @@ +package com.zbkj.admin.controller; + +import com.zbkj.common.response.dashboard.BossDashboardResponse; +import com.zbkj.common.result.CommonResult; +import com.zbkj.service.service.BossDashboardService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; + +/** + * 老板经营驾驶舱 + */ +@Slf4j +@RestController +@RequestMapping("api/admin/dashboard") +@Api(tags = "老板经营驾驶舱") +public class BossDashboardController { + + @Autowired + private BossDashboardService bossDashboardService; + + @ApiOperation(value = "老板驾驶舱概览") + @RequestMapping(value = "/overview", method = RequestMethod.GET) + public CommonResult overview(@RequestParam(value = "date", required = false) String date) { + return CommonResult.success(bossDashboardService.overview(date)); + } + + @ApiOperation(value = "生成经营日报归档 HTML") + @RequestMapping(value = "/daily-report/archive", method = RequestMethod.GET) + public ResponseEntity dailyReportArchive(@RequestParam(value = "date", required = false) String date) { + BossDashboardResponse overview = bossDashboardService.overview(date); + String html = bossDashboardService.dailyReportArchiveHtml(date); + String filename = "dashboard-daily-report-" + overview.getBusinessDate() + ".html"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(new MediaType("text", "html", StandardCharsets.UTF_8)); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\""); + return ResponseEntity.ok().headers(headers).body(html.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/backend/crmeb-common/src/main/java/com/zbkj/common/response/dashboard/BossDashboardResponse.java b/backend/crmeb-common/src/main/java/com/zbkj/common/response/dashboard/BossDashboardResponse.java new file mode 100644 index 0000000..2f3ac43 --- /dev/null +++ b/backend/crmeb-common/src/main/java/com/zbkj/common/response/dashboard/BossDashboardResponse.java @@ -0,0 +1,106 @@ +package com.zbkj.common.response.dashboard; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 老板驾驶舱响应对象 + */ +@Data +@ApiModel(value = "BossDashboardResponse", description = "老板驾驶舱响应对象") +public class BossDashboardResponse { + + @ApiModelProperty(value = "业务日期") + private String businessDate; + + @ApiModelProperty(value = "生成时间") + private String generatedAt; + + @ApiModelProperty(value = "经营摘要") + private String summary; + + @ApiModelProperty(value = "核心指标") + private List kpis = new ArrayList<>(); + + @ApiModelProperty(value = "资金池指标") + private List fundPool = new ArrayList<>(); + + @ApiModelProperty(value = "今日节点快报") + private List snapshots = new ArrayList<>(); + + @ApiModelProperty(value = "近 7 天趋势") + private List trends = new ArrayList<>(); + + @ApiModelProperty(value = "高价值用户排行") + private List userRanks = new ArrayList<>(); + + @ApiModelProperty(value = "团队贡献排行") + private List teamRanks = new ArrayList<>(); + + @ApiModelProperty(value = "高货值未成交商品排行") + private List productRanks = new ArrayList<>(); + + @ApiModelProperty(value = "风险预警") + private List risks = new ArrayList<>(); + + @Data + public static class KpiMetric { + private String key; + private String title; + private Object value; + private String unit; + private String trendLabel; + private BigDecimal trendValue; + private String status = "normal"; + private Boolean featured = false; + } + + @Data + public static class TodaySnapshot { + private String slot; + private String title; + private String status; + private String generatedAt; + private String message; + private Integer purchaseUsers = 0; + private Integer orderCount = 0; + private BigDecimal dealAmount = BigDecimal.ZERO; + private BigDecimal paidAmount = BigDecimal.ZERO; + private Integer newMerchandiseCount = 0; + private BigDecimal selfBonusChange = BigDecimal.ZERO; + private BigDecimal shareBonusChange = BigDecimal.ZERO; + } + + @Data + public static class TrendPoint { + private String date; + private BigDecimal amount = BigDecimal.ZERO; + private Integer orders = 0; + private Integer newUsers = 0; + private BigDecimal bonus = BigDecimal.ZERO; + } + + @Data + public static class RankItem { + private String id; + private String name; + private BigDecimal value = BigDecimal.ZERO; + private String description; + private String badge; + } + + @Data + public static class RiskAlert { + private String id; + private String level; + private String type; + private String title; + private String description; + private String discoveredAt; + } +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/BossDashboardService.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/BossDashboardService.java new file mode 100644 index 0000000..70137be --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/BossDashboardService.java @@ -0,0 +1,25 @@ +package com.zbkj.service.service; + +import com.zbkj.common.response.dashboard.BossDashboardResponse; + +/** + * 老板经营驾驶舱服务 + */ +public interface BossDashboardService { + + /** + * 获取老板经营驾驶舱数据 + * + * @param date 业务日期,格式 yyyy-MM-dd,为空时默认上一个工作日 + * @return BossDashboardResponse + */ + BossDashboardResponse overview(String date); + + /** + * 生成经营日报归档 HTML + * + * @param date 业务日期,格式 yyyy-MM-dd,为空时默认上一个工作日 + * @return standalone HTML + */ + String dailyReportArchiveHtml(String date); +} diff --git a/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/BossDashboardServiceImpl.java b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/BossDashboardServiceImpl.java new file mode 100644 index 0000000..cf2916f --- /dev/null +++ b/backend/crmeb-service/src/main/java/com/zbkj/service/service/impl/BossDashboardServiceImpl.java @@ -0,0 +1,564 @@ +package com.zbkj.service.service.impl; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.zbkj.common.model.consignment.WaMerchandise; +import com.zbkj.common.model.consignment.WaOrder; +import com.zbkj.common.model.consignment.WaSelfbonusLog; +import com.zbkj.common.model.consignment.WaSharebonusLog; +import com.zbkj.common.model.consignment.WaUsers; +import com.zbkj.common.model.consignment.WaWithdraw; +import com.zbkj.common.response.dashboard.BossDashboardResponse; +import com.zbkj.service.dao.consignment.WaMerchandiseDao; +import com.zbkj.service.dao.consignment.WaOrderDao; +import com.zbkj.service.dao.consignment.WaSelfbonusLogDao; +import com.zbkj.service.dao.consignment.WaSharebonusLogDao; +import com.zbkj.service.dao.consignment.WaUsersDao; +import com.zbkj.service.dao.consignment.WaWithdrawDao; +import com.zbkj.service.service.BossDashboardService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 老板经营驾驶舱服务实现 + */ +@Service +public class BossDashboardServiceImpl implements BossDashboardService { + + @Resource + private WaOrderDao waOrderDao; + + @Resource + private WaMerchandiseDao waMerchandiseDao; + + @Resource + private WaUsersDao waUsersDao; + + @Resource + private WaSelfbonusLogDao waSelfbonusLogDao; + + @Resource + private WaSharebonusLogDao waSharebonusLogDao; + + @Resource + private WaWithdrawDao waWithdrawDao; + + @Override + public BossDashboardResponse overview(String date) { + DateTime businessDate = StrUtil.isBlank(date) ? previousWorkday(DateUtil.date()) : DateUtil.parseDate(date); + DateTime previousDate = previousWorkday(businessDate); + DateRange businessRange = dayRange(businessDate); + DateRange previousRange = dayRange(previousDate); + + DailyMetrics metrics = buildDailyMetrics(businessRange); + DailyMetrics previousMetrics = buildDailyMetrics(previousRange); + + BossDashboardResponse response = new BossDashboardResponse(); + response.setBusinessDate(businessDate.toString("yyyy-MM-dd")); + response.setGeneratedAt(DateUtil.formatDateTime(new Date())); + response.setSummary(buildSummary(metrics)); + response.getKpis().add(metric("dealAmount", "上个工作日成交额", metrics.dealAmount, "元", "较上一工作日", ratio(metrics.dealAmount, previousMetrics.dealAmount), statusByRatio(metrics.dealAmount, previousMetrics.dealAmount), true)); + response.getKpis().add(metric("orderCount", "上个工作日订单数", metrics.orderCount, "单", "较上一工作日", ratio(metrics.orderCount, previousMetrics.orderCount), statusByRatio(metrics.orderCount, previousMetrics.orderCount), false)); + response.getKpis().add(metric("purchaseUsers", "采购用户", metrics.purchaseUsers, "人", "较上一工作日", ratio(metrics.purchaseUsers, previousMetrics.purchaseUsers), statusByRatio(metrics.purchaseUsers, previousMetrics.purchaseUsers), false)); + response.getKpis().add(metric("newUsers", "新增用户", metrics.newUsers, "人", "较上一工作日", ratio(metrics.newUsers, previousMetrics.newUsers), statusByRatio(metrics.newUsers, previousMetrics.newUsers), false)); + response.getKpis().add(metric("newMerchandise", "新增寄售商品", metrics.newMerchandiseCount, "件", "较上一工作日", ratio(metrics.newMerchandiseCount, previousMetrics.newMerchandiseCount), statusByRatio(metrics.newMerchandiseCount, previousMetrics.newMerchandiseCount), false)); + response.getKpis().add(metric("selfBonus", "个人奖金发放", metrics.selfBonus, "元", "较上一工作日", ratio(metrics.selfBonus, previousMetrics.selfBonus), "normal", false)); + response.getKpis().add(metric("shareBonus", "推广奖金发放", metrics.shareBonus, "元", "较上一工作日", ratio(metrics.shareBonus, previousMetrics.shareBonus), "normal", false)); + response.getKpis().add(metric("pendingAmount", "待支付/待结算", metrics.pendingAmount, "元", "需关注", null, metrics.pendingAmount.compareTo(BigDecimal.ZERO) > 0 ? "warning" : "normal", false)); + + buildFundPool(response); + buildSnapshots(response); + buildTrends(response, businessDate); + buildRanks(response); + buildRisks(response); + return response; + } + + @Override + public String dailyReportArchiveHtml(String date) { + BossDashboardResponse data = overview(date); + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(""); + html.append("经营日报归档 - ").append(escape(data.getBusinessDate())).append(""); + html.append("
"); + html.append("

Daily Report Archive

经营日报归档

"); + html.append("

").append(escape(data.getSummary())).append("

"); + html.append("数据日期:").append(escape(data.getBusinessDate())).append(""); + html.append("生成时间:").append(escape(data.getGeneratedAt())).append(""); + html.append("归档类型:Standalone HTML
"); + appendMetricsSection(html, "核心经营指标", data.getKpis()); + appendTrendSection(html, data); + appendMetricsSection(html, "资金池摘要", data.getFundPool()); + appendRankSection(html, "高价值用户", data.getUserRanks()); + appendRankSection(html, "团队贡献排行", data.getTeamRanks()); + appendRankSection(html, "高货值未成交商品", data.getProductRanks()); + appendRiskSection(html, data); + html.append("

本归档由经营驾驶舱实时数据生成,可独立保存和打开。

"); + html.append("
"); + return html.toString(); + } + + private DailyMetrics buildDailyMetrics(DateRange range) { + DailyMetrics metrics = new DailyMetrics(); + metrics.dealAmount = sumOrderAmount(range.start, range.end, true, null); + metrics.orderCount = countOrders(range.start, range.end, null); + metrics.purchaseUsers = distinctBuyerCount(range.start, range.end, null); + metrics.newUsers = countUsers(range.start, range.end); + metrics.newMerchandiseCount = countMerchandise(range.start, range.end); + metrics.selfBonus = sumSelfBonus(range.start, range.end); + metrics.shareBonus = sumShareBonus(range.start, range.end); + metrics.pendingAmount = sumOrderAmount(range.start, range.end, false, null); + return metrics; + } + + private void buildFundPool(BossDashboardResponse response) { + BigDecimal money = sumUsersDecimal("money"); + BigDecimal coupon = sumUsersDecimal("coupon"); + BigDecimal selfBonus = sumUsersDecimal("self_bonus"); + BigDecimal shareBonus = sumUsersDecimal("share_bonus"); + BigDecimal score = sumUsersDecimal("score"); + BigDecimal pendingWithdraw = sumWithdrawAmount(0); + Integer pendingWithdrawCount = countWithdraw(0); + + response.getFundPool().add(metric("balance", "余额总额", money, "元", null, null, "normal", false)); + response.getFundPool().add(metric("coupon", "优惠券总额", coupon, "元", null, null, "normal", false)); + response.getFundPool().add(metric("selfBonusPool", "个人奖金总额", selfBonus, "元", null, null, selfBonus.compareTo(BigDecimal.ZERO) > 0 ? "warning" : "normal", false)); + response.getFundPool().add(metric("shareBonusPool", "推广奖金总额", shareBonus, "元", null, null, "normal", false)); + response.getFundPool().add(metric("integral", "积分总额", score, "分", null, null, "normal", false)); + response.getFundPool().add(metric("withdrawPending", "待审核提现", pendingWithdraw, "元", pendingWithdrawCount + " 笔", null, pendingWithdrawCount > 0 ? "danger" : "normal", false)); + } + + private void buildSnapshots(BossDashboardResponse response) { + DateTime today = DateUtil.date(); + DateRange morningRange = range(today, "00:00:00", "10:15:59"); + DateRange afternoonRange = range(today, "10:16:00", "14:55:59"); + + DailyMetrics morningMetrics = buildDailyMetrics(morningRange); + BossDashboardResponse.TodaySnapshot morning = snapshot("1015", "10:15 上午快报", morningRange.end, "上午抢购节点已完成,上一日寄卖商品消化情况请关注成交额、付款和采购用户。", morningMetrics, "success"); + response.getSnapshots().add(morning); + + String afternoonStatus = new Date().after(afternoonRange.end) ? "success" : "pending"; + String afternoonMessage = "success".equals(afternoonStatus) + ? "下午寄卖/转卖节点已完成,请关注用户抢购商品的再次上架与转卖承接。" + : "下午寄卖/转卖节点尚未生成,预计 14:55 后可查看用户抢购商品的再次上架情况。"; + DailyMetrics afternoonMetrics = "success".equals(afternoonStatus) ? buildDailyMetrics(afternoonRange) : new DailyMetrics(); + response.getSnapshots().add(snapshot("1455", "14:55 下午快报", afternoonRange.end, afternoonMessage, afternoonMetrics, afternoonStatus)); + } + + private void buildTrends(BossDashboardResponse response, DateTime businessDate) { + for (int i = 6; i >= 0; i--) { + DateTime date = DateUtil.offsetDay(businessDate, -i); + DailyMetrics metrics = buildDailyMetrics(dayRange(date)); + BossDashboardResponse.TrendPoint point = new BossDashboardResponse.TrendPoint(); + point.setDate(date.toString("MM-dd")); + point.setAmount(metrics.dealAmount); + point.setOrders(metrics.orderCount); + point.setNewUsers(metrics.newUsers); + point.setBonus(metrics.selfBonus.add(metrics.shareBonus)); + response.getTrends().add(point); + } + } + + private void buildRanks(BossDashboardResponse response) { + QueryWrapper userWrapper = new QueryWrapper() + .select("id", "nickname", "mobile", "self_bonus", "share_bonus", "coupon", "score") + .orderByDesc("IFNULL(self_bonus,0) + IFNULL(share_bonus,0) + IFNULL(coupon,0)") + .last("limit 3"); + List users = waUsersDao.selectList(userWrapper); + for (int i = 0; i < users.size(); i++) { + WaUsers user = users.get(i); + BigDecimal value = defaultDecimal(user.getSelfBonus()).add(defaultDecimal(user.getShareBonus())).add(defaultDecimal(user.getCoupon())); + response.getUserRanks().add(rank("u" + user.getId(), displayName(user), value, maskMobile(user.getMobile()), i == 0 ? "高价值" : null)); + } + + QueryWrapper teamWrapper = new QueryWrapper() + .select("pid as id", "COUNT(id) as memberCount", "IFNULL(SUM(self_bonus),0) as selfBonus", "IFNULL(SUM(share_bonus),0) as shareBonus") + .isNotNull("pid") + .gt("pid", 0) + .groupBy("pid") + .orderByDesc("IFNULL(SUM(self_bonus),0) + IFNULL(SUM(share_bonus),0)") + .last("limit 3"); + List> teams = waUsersDao.selectMaps(teamWrapper); + for (int i = 0; i < teams.size(); i++) { + Map team = teams.get(i); + BigDecimal selfBonus = decimal(team.get("selfBonus")); + BigDecimal shareBonus = decimal(team.get("shareBonus")); + String leaderId = stringValue(team.get("id")); + WaUsers leader = waUsersDao.selectById(leaderId); + String leaderName = leader == null ? "团队 " + leaderId : displayName(leader); + response.getTeamRanks().add(rank("t" + leaderId, leaderName, selfBonus.add(shareBonus), "成员 " + intValue(team.get("memberCount")) + " 人", i == 0 ? "TOP1" : null)); + } + + QueryWrapper productWrapper = new QueryWrapper() + .select("id", "title", "price", "created_at") + .eq("status", 1) + .orderByDesc("price") + .last("limit 3"); + List products = waMerchandiseDao.selectList(productWrapper); + for (int i = 0; i < products.size(); i++) { + WaMerchandise product = products.get(i); + response.getProductRanks().add(rank("p" + product.getId(), StrUtil.isBlank(product.getTitle()) ? "未命名商品" : product.getTitle(), defaultDecimal(product.getPrice()), "高货值待成交", i == 0 ? "滞销" : null)); + } + } + + private void buildRisks(BossDashboardResponse response) { + BigDecimal pendingWithdraw = sumWithdrawAmount(0); + if (pendingWithdraw.compareTo(BigDecimal.ZERO) > 0) { + response.getRisks().add(risk("r1", "red", "资金", "待审核提现", "当前待审核提现 " + pendingWithdraw + " 元,建议今日处理。")); + } + + Integer pendingOrders = countPendingOrders(); + if (pendingOrders > 0) { + response.getRisks().add(risk("r2", "yellow", "订单", "待支付订单未处理", "当前存在 " + pendingOrders + " 笔待支付订单,请关注付款转化。")); + } + + Integer hiddenProducts = countHiddenMerchandise(); + if (hiddenProducts > 0) { + response.getRisks().add(risk("r3", "gray", "商品", "隐藏寄售商品", "当前存在 " + hiddenProducts + " 个隐藏寄售商品,可按需核查。")); + } + } + + private BossDashboardResponse.KpiMetric metric(String key, String title, Object value, String unit, String trendLabel, BigDecimal trendValue, String status, Boolean featured) { + BossDashboardResponse.KpiMetric metric = new BossDashboardResponse.KpiMetric(); + metric.setKey(key); + metric.setTitle(title); + metric.setValue(value); + metric.setUnit(unit); + metric.setTrendLabel(trendLabel); + metric.setTrendValue(trendValue); + metric.setStatus(status); + metric.setFeatured(featured); + return metric; + } + + private BossDashboardResponse.TodaySnapshot snapshot(String slot, String title, Date generatedAt, String message, DailyMetrics metrics, String status) { + BossDashboardResponse.TodaySnapshot snapshot = new BossDashboardResponse.TodaySnapshot(); + snapshot.setSlot(slot); + snapshot.setTitle(title); + snapshot.setStatus(status); + snapshot.setGeneratedAt(DateUtil.formatDateTime(generatedAt)); + snapshot.setMessage(message); + snapshot.setPurchaseUsers(metrics.purchaseUsers); + snapshot.setOrderCount(metrics.orderCount); + snapshot.setDealAmount(metrics.dealAmount); + snapshot.setPaidAmount(metrics.dealAmount); + snapshot.setNewMerchandiseCount(metrics.newMerchandiseCount); + snapshot.setSelfBonusChange(metrics.selfBonus); + snapshot.setShareBonusChange(metrics.shareBonus); + return snapshot; + } + + private BossDashboardResponse.RankItem rank(String id, String name, BigDecimal value, String description, String badge) { + BossDashboardResponse.RankItem rank = new BossDashboardResponse.RankItem(); + rank.setId(id); + rank.setName(name); + rank.setValue(value); + rank.setDescription(description); + rank.setBadge(badge); + return rank; + } + + private BossDashboardResponse.RiskAlert risk(String id, String level, String type, String title, String description) { + BossDashboardResponse.RiskAlert risk = new BossDashboardResponse.RiskAlert(); + risk.setId(id); + risk.setLevel(level); + risk.setType(type); + risk.setTitle(title); + risk.setDescription(description); + risk.setDiscoveredAt(DateUtil.format(new Date(), "HH:mm")); + return risk; + } + + private void appendMetricsSection(StringBuilder html, String title, List metrics) { + html.append("

").append(escape(title)).append("

"); + for (BossDashboardResponse.KpiMetric metric : metrics) { + html.append("
").append(escape(metric.getTitle())).append(""); + html.append("").append(formatMetric(metric.getValue(), metric.getUnit())).append(""); + if (StrUtil.isNotBlank(metric.getTrendLabel()) || metric.getTrendValue() != null) { + html.append("").append(escape(metric.getTrendLabel())); + if (metric.getTrendValue() != null) { + html.append(" ").append(metric.getTrendValue()).append("%"); + } + html.append(""); + } + html.append("
"); + } + html.append("
"); + } + + private void appendTrendSection(StringBuilder html, BossDashboardResponse data) { + html.append("

最近 7 天趋势

"); + for (BossDashboardResponse.TrendPoint point : data.getTrends()) { + html.append("
").append(escape(point.getDate())).append(":").append(formatMoney(point.getAmount())).append(""); + html.append("").append(point.getOrders()).append(" 单 / 新增用户 ").append(point.getNewUsers()).append(" / 奖金 ").append(formatMoney(point.getBonus())).append("
"); + } + html.append("
"); + } + + private void appendRankSection(StringBuilder html, String title, List ranks) { + html.append("

").append(escape(title)).append("

"); + if (ranks.isEmpty()) { + html.append("
暂无数据当前实时数据未生成该排行。
"); + } + for (int i = 0; i < ranks.size(); i++) { + BossDashboardResponse.RankItem rank = ranks.get(i); + html.append("
").append(i + 1).append(". ").append(escape(rank.getName())).append(" · ").append(formatMoney(rank.getValue())).append(""); + html.append("").append(escape(rank.getDescription())); + if (StrUtil.isNotBlank(rank.getBadge())) { + html.append(" / ").append(escape(rank.getBadge())); + } + html.append("
"); + } + html.append("
"); + } + + private void appendRiskSection(StringBuilder html, BossDashboardResponse data) { + html.append("

风险预警

"); + if (data.getRisks().isEmpty()) { + html.append("
暂无风险当前实时数据未触发风险预警。
"); + } + for (BossDashboardResponse.RiskAlert risk : data.getRisks()) { + html.append("
"); + html.append(escape(risk.getType())).append(" / ").append(escape(risk.getTitle())).append(""); + html.append("

").append(escape(risk.getDescription())).append("

"); + html.append("发现时间:").append(escape(risk.getDiscoveredAt())).append("
"); + } + html.append("
"); + } + + private BigDecimal sumOrderAmount(Date start, Date end, Boolean paidOnly, Boolean isResell) { + QueryWrapper wrapper = new QueryWrapper().select("IFNULL(SUM(total_money),0) as total").between("buy_time", start, end); + wrapper.eq("is_cancel", 0); + if (Boolean.TRUE.equals(paidOnly)) { + wrapper.ge("status", 1); + } else if (Boolean.FALSE.equals(paidOnly)) { + wrapper.eq("status", 0); + } + if (isResell != null) { + wrapper.eq("is_resell", isResell ? 1 : 0); + } + return aggregateDecimal(waOrderDao.selectMaps(wrapper), "total"); + } + + private Integer countOrders(Date start, Date end, Boolean isResell) { + QueryWrapper wrapper = new QueryWrapper().between("buy_time", start, end).eq("is_cancel", 0); + if (isResell != null) { + wrapper.eq("is_resell", isResell ? 1 : 0); + } + return waOrderDao.selectCount(wrapper); + } + + private Integer distinctBuyerCount(Date start, Date end, Boolean isResell) { + QueryWrapper wrapper = new QueryWrapper().select("COUNT(DISTINCT buyer_id) as total").between("buy_time", start, end).eq("is_cancel", 0); + if (isResell != null) { + wrapper.eq("is_resell", isResell ? 1 : 0); + } + return aggregateInt(waOrderDao.selectMaps(wrapper), "total"); + } + + private Integer countUsers(Date start, Date end) { + return waUsersDao.selectCount(new QueryWrapper().between("join_time", start, end)); + } + + private Integer countMerchandise(Date start, Date end) { + return waMerchandiseDao.selectCount(new QueryWrapper().between("created_at", start, end)); + } + + private BigDecimal sumSelfBonus(Date start, Date end) { + QueryWrapper wrapper = new QueryWrapper().select("IFNULL(SUM(money),0) as total").eq("type", 1).between("created_at", start, end); + return aggregateDecimal(waSelfbonusLogDao.selectMaps(wrapper), "total"); + } + + private BigDecimal sumShareBonus(Date start, Date end) { + QueryWrapper wrapper = new QueryWrapper().select("IFNULL(SUM(money),0) as total").eq("type", 1).between("created_at", start, end); + return aggregateDecimal(waSharebonusLogDao.selectMaps(wrapper), "total"); + } + + private BigDecimal sumUsersDecimal(String column) { + QueryWrapper wrapper = new QueryWrapper().select("IFNULL(SUM(" + column + "),0) as total"); + return aggregateDecimal(waUsersDao.selectMaps(wrapper), "total"); + } + + private BigDecimal sumWithdrawAmount(Integer status) { + QueryWrapper wrapper = new QueryWrapper().select("IFNULL(SUM(money),0) as total").eq("status", status); + return aggregateDecimal(waWithdrawDao.selectMaps(wrapper), "total"); + } + + private Integer countWithdraw(Integer status) { + return waWithdrawDao.selectCount(new QueryWrapper().eq("status", status)); + } + + private Integer countPendingOrders() { + return waOrderDao.selectCount(new QueryWrapper().eq("status", 0).eq("is_cancel", 0)); + } + + private Integer countHiddenMerchandise() { + return waMerchandiseDao.selectCount(new QueryWrapper().eq("is_show", 0)); + } + + private BigDecimal ratio(BigDecimal current, BigDecimal previous) { + if (previous == null || previous.compareTo(BigDecimal.ZERO) == 0) { + return current != null && current.compareTo(BigDecimal.ZERO) > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO; + } + return current.subtract(previous).multiply(BigDecimal.valueOf(100)).divide(previous, 1, RoundingMode.HALF_UP); + } + + private BigDecimal ratio(Integer current, Integer previous) { + return ratio(BigDecimal.valueOf(current == null ? 0 : current), BigDecimal.valueOf(previous == null ? 0 : previous)); + } + + private String statusByRatio(BigDecimal current, BigDecimal previous) { + BigDecimal value = ratio(current, previous); + return value.compareTo(BigDecimal.ZERO) >= 0 ? "success" : "warning"; + } + + private String statusByRatio(Integer current, Integer previous) { + return statusByRatio(BigDecimal.valueOf(current == null ? 0 : current), BigDecimal.valueOf(previous == null ? 0 : previous)); + } + + private String buildSummary(DailyMetrics metrics) { + if (metrics.dealAmount.compareTo(BigDecimal.ZERO) == 0 && metrics.orderCount == 0) { + return "当前日期暂无成交数据,请关注抢购与寄卖节点是否正常生成。"; + } + return "上个工作日成交 " + metrics.dealAmount + " 元,订单 " + metrics.orderCount + " 单,采购用户 " + metrics.purchaseUsers + " 人;请重点关注待支付、提现和寄售供给。"; + } + + private DateRange dayRange(DateTime date) { + return new DateRange(DateUtil.beginOfDay(date), DateUtil.endOfDay(date)); + } + + private DateTime previousWorkday(DateTime referenceDate) { + DateTime date = DateUtil.offsetDay(referenceDate, -1); + while (isWeekend(date)) { + date = DateUtil.offsetDay(date, -1); + } + return date; + } + + private boolean isWeekend(DateTime date) { + int dayOfWeek = DateUtil.dayOfWeek(date); + return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY; + } + + private DateRange range(DateTime date, String startTime, String endTime) { + String day = date.toString("yyyy-MM-dd"); + return new DateRange(DateUtil.parse(day + " " + startTime), DateUtil.parse(day + " " + endTime)); + } + + private BigDecimal aggregateDecimal(List> maps, String key) { + if (maps == null || maps.isEmpty()) { + return BigDecimal.ZERO; + } + return decimal(maps.get(0).get(key)); + } + + private Integer aggregateInt(List> maps, String key) { + if (maps == null || maps.isEmpty()) { + return 0; + } + return intValue(maps.get(0).get(key)); + } + + private BigDecimal decimal(Object value) { + if (value == null) { + return BigDecimal.ZERO; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + return new BigDecimal(String.valueOf(value)); + } + + private BigDecimal defaultDecimal(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private Integer intValue(Object value) { + if (value == null) { + return 0; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + return Integer.parseInt(String.valueOf(value)); + } + + private String stringValue(Object value) { + return value == null ? "" : String.valueOf(value); + } + + private String displayName(WaUsers user) { + if (StrUtil.isNotBlank(user.getNickname())) { + return user.getNickname(); + } + if (StrUtil.isNotBlank(user.getUsername())) { + return user.getUsername(); + } + return "用户 " + user.getId(); + } + + private String maskMobile(String mobile) { + if (StrUtil.isBlank(mobile) || mobile.length() < 7) { + return "手机号未完善"; + } + return mobile.substring(0, 3) + "****" + mobile.substring(mobile.length() - 4); + } + + private String formatMetric(Object value, String unit) { + if ("元".equals(unit)) { + return formatMoney(decimal(value)); + } + if (value == null) { + return "--"; + } + return escape(String.valueOf(value)) + (unit == null ? "" : escape(unit)); + } + + private String formatMoney(BigDecimal value) { + return "¥" + defaultDecimal(value).setScale(2, RoundingMode.HALF_UP).toPlainString(); + } + + private String escape(String value) { + if (value == null) { + return ""; + } + return value.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + private static class DailyMetrics { + private BigDecimal dealAmount = BigDecimal.ZERO; + private Integer orderCount = 0; + private Integer purchaseUsers = 0; + private Integer newUsers = 0; + private Integer newMerchandiseCount = 0; + private BigDecimal selfBonus = BigDecimal.ZERO; + private BigDecimal shareBonus = BigDecimal.ZERO; + private BigDecimal pendingAmount = BigDecimal.ZERO; + } + + private static class DateRange { + private Date start; + private Date end; + + private DateRange(Date start, Date end) { + this.start = start; + this.end = end; + } + } +} diff --git a/dashboard-frontend/.env.development b/dashboard-frontend/.env.development new file mode 100644 index 0000000..965b808 --- /dev/null +++ b/dashboard-frontend/.env.development @@ -0,0 +1,5 @@ +VITE_APP_ENV=development +VITE_API_BASE_URL=/api/admin +VITE_MOCK_ENABLED=false +VITE_APP_TITLE=经营驾驶舱 +VITE_BUILD_VERSION=local diff --git a/dashboard-frontend/.gitignore b/dashboard-frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/dashboard-frontend/.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/dashboard-frontend/README.md b/dashboard-frontend/README.md new file mode 100644 index 0000000..82e6a8c --- /dev/null +++ b/dashboard-frontend/README.md @@ -0,0 +1,122 @@ +# Dashboard Frontend + +独立 H5 经营驾驶舱前端项目。第一阶段只使用本地 Mock 数据,不对接后端 API。 + +## 技术栈 + +- React 19 +- TypeScript +- Vite +- antd-mobile +- TanStack Query +- Zustand +- Axios +- ECharts +- MSW +- Vitest + +## 本地开发 + +```bash +nvm use --delete-prefix v24.14.1 +pnpm install +pnpm dev +``` + +默认访问: + +```text +http://localhost:5174/h5/dashboard/boss +``` + +## 第一阶段范围 + +- H5 移动端老板驾驶舱首页 +- 昨日经营核心 KPI +- 今日 10:15 / 14:55 节点快报 Mock +- 近 7 天交易趋势 +- 用户、团队、商品排行 +- 风险预警摘要 +- 底部 Tab 导航 +- MSW Mock 数据 + +## 校验 + +```bash +pnpm typecheck +pnpm test -- --run +pnpm build +``` +# 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/dashboard-frontend/eslint.config.js b/dashboard-frontend/eslint.config.js new file mode 100644 index 0000000..574d792 --- /dev/null +++ b/dashboard-frontend/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', 'public/mockServiceWorker.js']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/dashboard-frontend/index.html b/dashboard-frontend/index.html new file mode 100644 index 0000000..01f4e1b --- /dev/null +++ b/dashboard-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + dashboard-frontend + + +
+ + + diff --git a/dashboard-frontend/package.json b/dashboard-frontend/package.json new file mode 100644 index 0000000..1715dc2 --- /dev/null +++ b/dashboard-frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "dashboard-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "typecheck": "tsc -b --noEmit", + "test": "vitest" + }, + "dependencies": { + "@tanstack/react-query": "^5.100.9", + "antd-mobile": "^5.42.3", + "antd-mobile-icons": "^0.3.0", + "axios": "^1.16.0", + "echarts": "^6.0.0", + "msw": "^2.14.5", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-router-dom": "^7.15.0", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "jsdom": "^29.1.1", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10", + "vitest": "^4.1.5" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} \ No newline at end of file diff --git a/dashboard-frontend/pnpm-lock.yaml b/dashboard-frontend/pnpm-lock.yaml new file mode 100644 index 0000000..c24247c --- /dev/null +++ b/dashboard-frontend/pnpm-lock.yaml @@ -0,0 +1,3461 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tanstack/react-query': + specifier: ^5.100.9 + version: 5.100.9(react@19.2.6) + antd-mobile: + specifier: ^5.42.3 + version: 5.42.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + antd-mobile-icons: + specifier: ^0.3.0 + version: 0.3.0 + axios: + specifier: ^1.16.0 + version: 1.16.0 + echarts: + specifier: ^6.0.0 + version: 6.0.0 + msw: + specifier: ^2.14.5 + version: 2.14.5(@types/node@24.12.3)(typescript@6.0.3) + react: + specifier: ^19.2.5 + version: 19.2.6 + react-dom: + specifier: ^19.2.5 + version: 19.2.6(react@19.2.6) + react-router-dom: + specifier: ^7.15.0 + version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + zustand: + specifier: ^5.0.13 + version: 5.0.13(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.3.0) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^24.12.2 + version: 24.12.3 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.11(@types/node@24.12.3)) + eslint: + specifier: ^10.2.1 + version: 10.3.0 + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.3.0) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@10.3.0) + globals: + specifier: ^17.5.0 + version: 17.6.0 + jsdom: + specifier: ^29.1.1 + version: 29.1.1 + typescript: + specifier: ~6.0.2 + version: 6.0.3 + typescript-eslint: + specifier: ^8.58.2 + version: 8.59.2(eslint@10.3.0)(typescript@6.0.3) + vite: + specifier: ^8.0.10 + version: 8.0.11(@types/node@24.12.3) + vitest: + specifier: ^4.1.5 + version: 4.1.5(@types/node@24.12.3)(jsdom@29.1.1)(msw@2.14.5(@types/node@24.12.3)(typescript@6.0.3))(vite@8.0.11(@types/node@24.12.3)) + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@2.0.5': + resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/confirm@6.0.13': + resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.1.10': + resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.5': + resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/type@4.0.5': + resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mswjs/interceptors@0.41.8': + resolution: {integrity: sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@oxc-project/types@0.128.0': + resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + + '@rc-component/mini-decimal@1.1.3': + resolution: {integrity: sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==} + engines: {node: '>=8.x'} + + '@react-spring/animated@9.6.1': + resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.6.1': + resolution: {integrity: sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.6.1': + resolution: {integrity: sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==} + + '@react-spring/shared@9.6.1': + resolution: {integrity: sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/types@9.6.1': + resolution: {integrity: sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==} + + '@react-spring/web@9.6.1': + resolution: {integrity: sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@rolldown/binding-android-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.18': + resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.18': + resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tanstack/query-core@5.100.9': + resolution: {integrity: sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==} + + '@tanstack/react-query@5.100.9': + resolution: {integrity: sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==} + peerDependencies: + react: ^18 || ^19 + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.12.3': + resolution: {integrity: sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@typescript-eslint/eslint-plugin@8.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@use-gesture/core@10.3.0': + resolution: {integrity: sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==} + + '@use-gesture/react@10.3.0': + resolution: {integrity: sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==} + peerDependencies: + react: '>= 16.8.0' + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + 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 + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ahooks@3.9.7: + resolution: {integrity: sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + antd-mobile-icons@0.3.0: + resolution: {integrity: sha512-rqINQpJWZWrva9moCd1Ye695MZYWmqLPE+bY8d2xLRy7iSQwPsinCdZYjpUPp2zL/LnKYSyXxP2ut2A+DC+whQ==} + + antd-mobile-v5-count@1.0.1: + resolution: {integrity: sha512-YGsiEDCPUDz3SzfXi6gLZn/HpeSMW+jgPc4qiYUr1fSopg3hkUie2TnooJdExgfiETHefH3Ggs58He0OVfegLA==} + + antd-mobile@5.42.3: + resolution: {integrity: sha512-HLykA0Cr2am7dgEch/TRUnr4GHgTOM59rA9CVjgBJVVJ8dk2w8uUBz//Bji19sRH2leiUObWY7KQZpsWF+WQeg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.29: + resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + + electron-to-chromium@1.5.353: + resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + 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 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.3.0: + resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphql@16.14.0: + resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + intersection-observer@0.12.2: + resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} + deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.14.5: + resolution: {integrity: sha512-X6G05oX4x0e+CNI55KMdhMmwHCBKf2iwazGr+iwsdoJ94JA1ED7wSXb6V+lLPdqFkmIlPiGYvayqnaNcOzobDA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + nano-memoize@3.0.16: + resolution: {integrity: sha512-JyK96AKVGAwVeMj3MoMhaSXaUNqgMbCRSQB3trUV8tYZfWEzqUBKdK1qJpfuNXgKeHOx1jv/IEYTM659ly7zUA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + rc-field-form@1.44.0: + resolution: {integrity: sha512-el7w87fyDUsca63Y/s8qJcq9kNkf/J5h+iTdqG5WsSHLH0e6Usl7QuYSmSVzJMgtp40mOVZIY/W/QP9zwrp1FA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-motion@2.9.5: + resolution: {integrity: sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-segmented@2.4.1: + resolution: {integrity: sha512-KUi+JJFdKnumV9iXlm+BJ00O4NdVBp2TEexLCk6bK1x/RH83TvYKQMzIz/7m3UTRPD08RM/8VG/JNjWgWbd4cw==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + rc-util@5.44.4: + resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-router-dom@7.15.0: + resolution: {integrity: sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.15.0: + resolution: {integrity: sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + + rolldown@1.0.0-rc.18: + resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + runes2@1.1.4: + resolution: {integrity: sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + screenfull@5.2.0: + resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} + engines: {node: '>=0.10.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + staged-components@1.1.3: + resolution: {integrity: sha512-9EIswzDqjwlEu+ymkV09TTlJfzSbKgEnNteUnZSTxkpMgr5Wx2CzzA9WcMFWBNCldqVPsHVnRGGrApduq2Se5A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.30: + resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} + + tldts@7.0.30: + resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} + hasBin: true + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + + typescript-eslint@8.59.2: + resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@8.0.11: + resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + + zustand@5.0.13: + resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==} + 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 + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@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.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0)': + dependencies: + eslint: 10.3.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.3.0)': + optionalDependencies: + eslint: 10.3.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.15.0': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@2.0.5': {} + + '@inquirer/confirm@6.0.13(@types/node@24.12.3)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@24.12.3) + '@inquirer/type': 4.0.5(@types/node@24.12.3) + optionalDependencies: + '@types/node': 24.12.3 + + '@inquirer/core@11.1.10(@types/node@24.12.3)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@24.12.3) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 24.12.3 + + '@inquirer/figures@2.0.5': {} + + '@inquirer/type@4.0.5(@types/node@24.12.3)': + optionalDependencies: + '@types/node': 24.12.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mswjs/interceptors@0.41.8': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@oxc-project/types@0.128.0': {} + + '@rc-component/mini-decimal@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + + '@react-spring/animated@9.6.1(react@19.2.6)': + dependencies: + '@react-spring/shared': 9.6.1(react@19.2.6) + '@react-spring/types': 9.6.1 + react: 19.2.6 + + '@react-spring/core@9.6.1(react@19.2.6)': + dependencies: + '@react-spring/animated': 9.6.1(react@19.2.6) + '@react-spring/rafz': 9.6.1 + '@react-spring/shared': 9.6.1(react@19.2.6) + '@react-spring/types': 9.6.1 + react: 19.2.6 + + '@react-spring/rafz@9.6.1': {} + + '@react-spring/shared@9.6.1(react@19.2.6)': + dependencies: + '@react-spring/rafz': 9.6.1 + '@react-spring/types': 9.6.1 + react: 19.2.6 + + '@react-spring/types@9.6.1': {} + + '@react-spring/web@9.6.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@react-spring/animated': 9.6.1(react@19.2.6) + '@react-spring/core': 9.6.1(react@19.2.6) + '@react-spring/shared': 9.6.1(react@19.2.6) + '@react-spring/types': 9.6.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@rolldown/binding-android-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.18': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@standard-schema/spec@1.1.0': {} + + '@tanstack/query-core@5.100.9': {} + + '@tanstack/react-query@5.100.9(react@19.2.6)': + dependencies: + '@tanstack/query-core': 5.100.9 + react: 19.2.6 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/js-cookie@3.0.6': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.12.3': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 24.12.3 + + '@types/statuses@2.0.6': {} + + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 10.3.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.2(eslint@10.3.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 10.3.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.2(eslint@10.3.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.3.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.2': {} + + '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.2(eslint@10.3.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + eslint: 10.3.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + eslint-visitor-keys: 5.0.1 + + '@use-gesture/core@10.3.0': {} + + '@use-gesture/react@10.3.0(react@19.2.6)': + dependencies: + '@use-gesture/core': 10.3.0 + react: 19.2.6 + + '@vitejs/plugin-react@6.0.1(vite@8.0.11(@types/node@24.12.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.11(@types/node@24.12.3) + + '@vitest/expect@4.1.5': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.5(msw@2.14.5(@types/node@24.12.3)(typescript@6.0.3))(vite@8.0.11(@types/node@24.12.3))': + dependencies: + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.14.5(@types/node@24.12.3)(typescript@6.0.3) + vite: 8.0.11(@types/node@24.12.3) + + '@vitest/pretty-format@4.1.5': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.5': + dependencies: + '@vitest/utils': 4.1.5 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.5': {} + + '@vitest/utils@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ahooks@3.9.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@babel/runtime': 7.29.2 + '@types/js-cookie': 3.0.6 + dayjs: 1.11.20 + intersection-observer: 0.12.2 + js-cookie: 3.0.5 + lodash: 4.18.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-fast-compare: 3.2.2 + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + tslib: 2.8.1 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + antd-mobile-icons@0.3.0: {} + + antd-mobile-v5-count@1.0.1: {} + + antd-mobile@5.42.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@floating-ui/dom': 1.7.6 + '@rc-component/mini-decimal': 1.1.3 + '@react-spring/web': 9.6.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@use-gesture/react': 10.3.0(react@19.2.6) + ahooks: 3.9.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + antd-mobile-icons: 0.3.0 + antd-mobile-v5-count: 1.0.1 + classnames: 2.5.1 + dayjs: 1.11.20 + deepmerge: 4.3.1 + nano-memoize: 3.0.16 + rc-field-form: 1.44.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + rc-segmented: 2.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + rc-util: 5.44.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-fast-compare: 3.2.2 + react-is: 18.3.1 + runes2: 1.1.4 + staged-components: 1.1.3(react@19.2.6) + tslib: 2.8.1 + use-sync-external-store: 1.6.0(react@19.2.6) + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.16.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.29: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.29 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.353 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + caniuse-lite@1.0.30001792: {} + + chai@6.2.2: {} + + classnames@2.5.1: {} + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + dayjs@1.11.20: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + + electron-to-chromium@1.5.353: {} + + emoji-regex@8.0.0: {} + + entities@8.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@10.3.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + eslint: 10.3.0 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@10.3.0): + dependencies: + eslint: 10.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.3.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + 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.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.6.0: {} + + gopd@1.2.0: {} + + graphql@16.14.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.0 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + intersection-observer@0.12.2: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-node-process@1.2.0: {} + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.6 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash@4.18.1: {} + + lru-cache@11.3.6: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + min-indent@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + ms@2.1.3: {} + + msw@2.14.5(@types/node@24.12.3)(typescript@6.0.3): + dependencies: + '@inquirer/confirm': 6.0.13(@types/node@24.12.3) + '@mswjs/interceptors': 0.41.8 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.0 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.6.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@3.0.0: {} + + nano-memoize@3.0.16: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.38: {} + + obug@2.1.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + outvariant@1.4.3: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + rc-field-form@1.44.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@babel/runtime': 7.29.2 + async-validator: 4.2.5 + rc-util: 5.44.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + rc-motion@2.9.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + rc-segmented@2.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + rc-util: 5.44.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + rc-util@5.44.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-is: 18.3.1 + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react-fast-compare@3.2.2: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-router-dom@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-router: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + + react-router@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + cookie: 1.1.1 + react: 19.2.6 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + + react@19.2.6: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resize-observer-polyfill@1.5.1: {} + + rettime@0.11.11: {} + + rolldown@1.0.0-rc.18: + dependencies: + '@oxc-project/types': 0.128.0 + '@rolldown/pluginutils': 1.0.0-rc.18 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-x64': 1.0.0-rc.18 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + + runes2@1.1.4: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + screenfull@5.2.0: {} + + semver@6.3.1: {} + + semver@7.8.0: {} + + set-cookie-parser@2.7.2: {} + + set-cookie-parser@3.1.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + staged-components@1.1.3(react@19.2.6): + dependencies: + react: 19.2.6 + + statuses@2.0.2: {} + + std-env@4.1.0: {} + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + symbol-tree@3.2.4: {} + + tagged-tag@1.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tldts-core@7.0.30: {} + + tldts@7.0.30: + dependencies: + tldts-core: 7.0.30 + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.30 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.3.0: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + + typescript-eslint@8.59.2(eslint@10.3.0)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0)(typescript@6.0.3) + eslint: 10.3.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + undici@7.25.0: {} + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + + vite@8.0.11(@types/node@24.12.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.0-rc.18 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.3 + fsevents: 2.3.3 + + vitest@4.1.5(@types/node@24.12.3)(jsdom@29.1.1)(msw@2.14.5(@types/node@24.12.3)(typescript@6.0.3))(vite@8.0.11(@types/node@24.12.3)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(msw@2.14.5(@types/node@24.12.3)(typescript@6.0.3))(vite@8.0.11(@types/node@24.12.3)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.11(@types/node@24.12.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.3 + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 + + zustand@5.0.13(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) diff --git a/dashboard-frontend/public/favicon.svg b/dashboard-frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/dashboard-frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashboard-frontend/public/icons.svg b/dashboard-frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/dashboard-frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard-frontend/public/mockServiceWorker.js b/dashboard-frontend/public/mockServiceWorker.js new file mode 100644 index 0000000..9cd8401 --- /dev/null +++ b/dashboard-frontend/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.14.5' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/dashboard-frontend/src/App.tsx b/dashboard-frontend/src/App.tsx new file mode 100644 index 0000000..5f82b63 --- /dev/null +++ b/dashboard-frontend/src/App.tsx @@ -0,0 +1,27 @@ +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' +import { AppProviders } from './app/providers/AppProviders' +import { MobileLayout } from './app/layouts/MobileLayout' +import { BossDashboardPage } from './features/boss-dashboard/pages/BossDashboardPage' +import { DailyReportPage, ProfilePage, RiskCenterPage, TodaySnapshotPage } from './features/boss-dashboard/pages/OperationsPages' + +function App() { + return ( + + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + + } /> + + + + ) +} + +export default App diff --git a/dashboard-frontend/src/app/layouts/MobileLayout.tsx b/dashboard-frontend/src/app/layouts/MobileLayout.tsx new file mode 100644 index 0000000..1d18642 --- /dev/null +++ b/dashboard-frontend/src/app/layouts/MobileLayout.tsx @@ -0,0 +1,34 @@ +import { AppOutline, BellOutline, FileOutline, HistogramOutline, UserOutline } from 'antd-mobile-icons' +import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { SafeArea, TabBar } from 'antd-mobile' + +const tabs = [ + { key: '/h5/dashboard/boss', title: '首页', icon: }, + { key: '/h5/dashboard/daily-report', title: '日报', icon: }, + { key: '/h5/dashboard/today-snapshot', title: '快报', icon: }, + { key: '/h5/dashboard/risk-center', title: '风险', icon: }, + { key: '/h5/dashboard/profile', title: '我的', icon: }, +] + +export function MobileLayout() { + const location = useLocation() + const navigate = useNavigate() + + const activeKey = tabs.find((tab) => location.pathname.startsWith(tab.key))?.key ?? tabs[0].key + + return ( +
+
+ +
+ +
+ ) +} diff --git a/dashboard-frontend/src/app/providers/AppProviders.tsx b/dashboard-frontend/src/app/providers/AppProviders.tsx new file mode 100644 index 0000000..5eda6c6 --- /dev/null +++ b/dashboard-frontend/src/app/providers/AppProviders.tsx @@ -0,0 +1,26 @@ +import { ConfigProvider } from 'antd-mobile' +import zhCN from 'antd-mobile/es/locales/zh-CN' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import type { ReactNode } from 'react' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +type AppProvidersProps = { + children: ReactNode +} + +export function AppProviders({ children }: AppProvidersProps) { + return ( + + {children} + + ) +} diff --git a/dashboard-frontend/src/components/charts/MiniTrendChart.tsx b/dashboard-frontend/src/components/charts/MiniTrendChart.tsx new file mode 100644 index 0000000..147a64b --- /dev/null +++ b/dashboard-frontend/src/components/charts/MiniTrendChart.tsx @@ -0,0 +1,76 @@ +import * as echarts from 'echarts/core' +import { GridComponent, TooltipComponent } from 'echarts/components' +import { BarChart, LineChart } from 'echarts/charts' +import { CanvasRenderer } from 'echarts/renderers' +import { useEffect, useMemo, useRef } from 'react' +import type { TrendPoint } from '../../features/boss-dashboard/types' + +echarts.use([GridComponent, TooltipComponent, LineChart, BarChart, CanvasRenderer]) + +type MiniTrendChartProps = { + data: TrendPoint[] +} + +export function MiniTrendChart({ data }: MiniTrendChartProps) { + const chartRef = useRef(null) + + const option = useMemo( + () => ({ + color: ['#ff5b36', '#ffb000'], + grid: { left: 8, right: 8, top: 24, bottom: 18, containLabel: true }, + tooltip: { + trigger: 'axis', + confine: true, + valueFormatter: (value: number | string) => Number(value).toLocaleString('zh-CN'), + }, + xAxis: { + type: 'category', + data: data.map((item) => item.date), + axisLine: { show: false }, + axisTick: { show: false }, + }, + yAxis: [ + { type: 'value', show: false }, + { type: 'value', show: false }, + ], + series: [ + { + name: '成交额', + type: 'line', + smooth: true, + yAxisIndex: 0, + data: data.map((item) => item.amount), + symbol: 'circle', + symbolSize: 5, + lineStyle: { width: 3 }, + areaStyle: { opacity: 0.08 }, + }, + { + name: '订单数', + type: 'bar', + yAxisIndex: 1, + data: data.map((item) => item.orders), + barWidth: 8, + borderRadius: 6, + }, + ], + }), + [data], + ) + + useEffect(() => { + if (!chartRef.current) return undefined + const chart = echarts.init(chartRef.current) + chart.setOption(option) + + const resize = () => chart.resize() + window.addEventListener('resize', resize) + + return () => { + window.removeEventListener('resize', resize) + chart.dispose() + } + }, [option]) + + return
+} diff --git a/dashboard-frontend/src/components/kpi/KpiCard.tsx b/dashboard-frontend/src/components/kpi/KpiCard.tsx new file mode 100644 index 0000000..241bd4b --- /dev/null +++ b/dashboard-frontend/src/components/kpi/KpiCard.tsx @@ -0,0 +1,32 @@ +import { Skeleton } from 'antd-mobile' +import { formatMetricValue, formatTrend } from '../../utils/format' +import type { KpiMetric } from '../../features/boss-dashboard/types' + +type KpiCardProps = { + metric: KpiMetric + loading?: boolean +} + +export function KpiCard({ metric, loading }: KpiCardProps) { + if (loading) { + return ( +
+ + +
+ ) + } + + return ( +
+

{metric.title}

+ {formatMetricValue(metric.value, metric.unit)} + {(metric.trendLabel || metric.trendValue !== undefined) && ( +

+ {metric.trendLabel} + {metric.trendValue !== undefined && {formatTrend(metric.trendValue)}} +

+ )} +
+ ) +} diff --git a/dashboard-frontend/src/features/boss-dashboard/api.ts b/dashboard-frontend/src/features/boss-dashboard/api.ts new file mode 100644 index 0000000..c3c49f2 --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/api.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query' +import { getApiData, getBlob } from '../../services/http/client' +import type { DashboardOverview } from './types' + +export const dashboardQueryKeys = { + overview: (date?: string) => ['dashboard', 'overview', date ?? 'default'] as const, +} + +export function useDashboardOverview(date?: string) { + return useQuery({ + queryKey: dashboardQueryKeys.overview(date), + queryFn: () => getApiData(date ? `/dashboard/overview?date=${date}` : '/dashboard/overview'), + }) +} + +export function downloadDailyReportArchive(date?: string) { + return getBlob(date ? `/dashboard/daily-report/archive?date=${date}` : '/dashboard/daily-report/archive') +} diff --git a/dashboard-frontend/src/features/boss-dashboard/components/RankList.tsx b/dashboard-frontend/src/features/boss-dashboard/components/RankList.tsx new file mode 100644 index 0000000..b629948 --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/components/RankList.tsx @@ -0,0 +1,38 @@ +import { RightOutline } from 'antd-mobile-icons' +import { formatMoney, formatNumber } from '../../../utils/format' +import type { RankItem } from '../types' + +type RankListProps = { + title: string + items: RankItem[] + valueType?: 'money' | 'number' +} + +export function RankList({ title, items, valueType = 'money' }: RankListProps) { + return ( +
+
+
+

Top 3

+

{title}

+
+ +
+
+ {items.map((item, index) => ( + + ))} +
+
+ ) +} diff --git a/dashboard-frontend/src/features/boss-dashboard/components/RiskAlertSection.tsx b/dashboard-frontend/src/features/boss-dashboard/components/RiskAlertSection.tsx new file mode 100644 index 0000000..188fbf9 --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/components/RiskAlertSection.tsx @@ -0,0 +1,42 @@ +import { Tag } from 'antd-mobile' +import type { RiskAlert, RiskLevel } from '../types' + +type RiskAlertSectionProps = { + risks: RiskAlert[] +} + +const levelMeta: Record = { + red: { color: 'danger', label: '红色' }, + yellow: { color: 'warning', label: '黄色' }, + gray: { color: 'default', label: '灰色' }, +} + +export function RiskAlertSection({ risks }: RiskAlertSectionProps) { + return ( +
+
+
+

Risk

+

风险预警

+
+ {risks.length} 条 +
+
+ {risks.map((risk) => { + const meta = levelMeta[risk.level] + return ( + + ) + })} +
+
+ ) +} diff --git a/dashboard-frontend/src/features/boss-dashboard/components/TodaySnapshotSection.tsx b/dashboard-frontend/src/features/boss-dashboard/components/TodaySnapshotSection.tsx new file mode 100644 index 0000000..db7f508 --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/components/TodaySnapshotSection.tsx @@ -0,0 +1,64 @@ +import { CapsuleTabs, Tag } from 'antd-mobile' +import { useState } from 'react' +import { formatMoney, formatNumber } from '../../../utils/format' +import type { SnapshotSlot, TodaySnapshot } from '../types' + +type TodaySnapshotSectionProps = { + snapshots: TodaySnapshot[] +} + +const statusMap = { + pending: { color: 'default', label: '待生成' }, + success: { color: 'success', label: '已生成' }, + failed: { color: 'danger', label: '生成失败' }, + temporary: { color: 'warning', label: '临时数据' }, +} as const + +export function TodaySnapshotSection({ snapshots }: TodaySnapshotSectionProps) { + const [activeSlot, setActiveSlot] = useState('1015') + const activeSnapshot = snapshots.find((snapshot) => snapshot.slot === activeSlot) ?? snapshots[0] + const status = statusMap[activeSnapshot.status] + + return ( +
+
+
+

今日节点

+

抢购 / 寄卖快报

+
+ {status.label} +
+ + setActiveSlot(key as SnapshotSlot)}> + {snapshots.map((snapshot) => ( + + ))} + + +
+

{activeSnapshot.message}

+ {activeSnapshot.generatedAt &&

生成时间:{activeSnapshot.generatedAt}

} +
+ + 采购用户{formatNumber(activeSnapshot.purchaseUsers)}人 + + + 订单数{formatNumber(activeSnapshot.orderCount)}单 + + + 成交额{formatMoney(activeSnapshot.dealAmount)} + + + 支付额{formatMoney(activeSnapshot.paidAmount)} + + + 新增商品{formatNumber(activeSnapshot.newMerchandiseCount)}件 + + + 奖金变化{formatMoney(Number(activeSnapshot.selfBonusChange) + Number(activeSnapshot.shareBonusChange))} + +
+
+
+ ) +} diff --git a/dashboard-frontend/src/features/boss-dashboard/mock.ts b/dashboard-frontend/src/features/boss-dashboard/mock.ts new file mode 100644 index 0000000..6fc7d4f --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/mock.ts @@ -0,0 +1,104 @@ +import type { DashboardOverview } from './types' + +export const dashboardMock: DashboardOverview = { + businessDate: '2026-05-10', + generatedAt: '2026-05-11 00:10:12', + summary: '昨日成交保持稳定,采购用户略有增长;资金风险主要集中在大额待提现和积分比例异常。', + kpis: [ + { key: 'dealAmount', title: '昨日成交额', value: 1289360.42, unit: '元', trendLabel: '较前日', trendValue: 8.6, status: 'success', featured: true }, + { key: 'orderCount', title: '昨日订单数', value: 1842, unit: '单', trendLabel: '较前日', trendValue: 4.1, status: 'success' }, + { key: 'purchaseUsers', title: '采购用户', value: 936, unit: '人', trendLabel: '较前日', trendValue: 2.7, status: 'success' }, + { key: 'newUsers', title: '新增用户', value: 318, unit: '人', trendLabel: '较前日', trendValue: -3.2, status: 'warning' }, + { key: 'newMerchandise', title: '新增寄售商品', value: 472, unit: '件', trendLabel: '较前日', trendValue: 12.4, status: 'success' }, + { key: 'selfBonus', title: '个人奖金发放', value: 168230.36, unit: '元', trendLabel: '较前日', trendValue: 6.8, status: 'normal' }, + { key: 'shareBonus', title: '推广奖金发放', value: 82460.18, unit: '元', trendLabel: '较前日', trendValue: 1.9, status: 'normal' }, + { key: 'pendingAmount', title: '待支付/待结算', value: 95620.11, unit: '元', trendLabel: '需关注', status: 'warning' }, + ], + fundPool: [ + { key: 'balance', title: '余额总额', value: 728903.22, unit: '元', status: 'normal' }, + { key: 'coupon', title: '优惠券总额', value: 391082.88, unit: '元', status: 'normal' }, + { key: 'selfBonusPool', title: '个人奖金总额', value: 836942.14, unit: '元', status: 'warning' }, + { key: 'shareBonusPool', title: '推广奖金总额', value: 295402.77, unit: '元', status: 'normal' }, + { key: 'integral', title: '积分总额', value: 418471.07, unit: '分', status: 'normal' }, + { key: 'withdrawPending', title: '待审核提现', value: 63200, unit: '元', status: 'danger' }, + ], + snapshots: [ + { + slot: '1015', + title: '10:15 上午快报', + status: 'success', + generatedAt: '2026-05-11 10:15:08', + message: '上午抢购节点已完成,上一日寄卖商品消化情况良好,采购用户和成交额略高于昨日同节点。', + purchaseUsers: 421, + orderCount: 756, + dealAmount: 526880.2, + paidAmount: 498320.5, + newMerchandiseCount: 185, + selfBonusChange: 64230.3, + shareBonusChange: 31820.1, + }, + { + slot: '1455', + title: '14:55 下午快报', + status: 'pending', + message: '下午寄卖/转卖节点尚未生成,预计 14:55 后可查看用户抢购商品的再次上架情况。', + purchaseUsers: 0, + orderCount: 0, + dealAmount: 0, + paidAmount: 0, + newMerchandiseCount: 0, + selfBonusChange: 0, + shareBonusChange: 0, + }, + ], + trends: [ + { date: '05-04', amount: 948000, orders: 1390, newUsers: 226, bonus: 186000 }, + { date: '05-05', amount: 1024000, orders: 1512, newUsers: 251, bonus: 194000 }, + { date: '05-06', amount: 1119000, orders: 1604, newUsers: 287, bonus: 205000 }, + { date: '05-07', amount: 1086000, orders: 1542, newUsers: 243, bonus: 198000 }, + { date: '05-08', amount: 1198000, orders: 1731, newUsers: 302, bonus: 221000 }, + { date: '05-09', amount: 1187200, orders: 1769, newUsers: 329, bonus: 229000 }, + { date: '05-10', amount: 1289360, orders: 1842, newUsers: 318, bonus: 250690 }, + ], + userRanks: [ + { id: 'u1', name: '刘先生', value: 96520, description: '个人奖金 + 推广奖金 + 积分折算', badge: '高价值' }, + { id: 'u2', name: '陈女士', value: 81230, description: '昨日采购 12 单', badge: '活跃' }, + { id: 'u3', name: '周先生', value: 75880, description: '团队新增 18 人' }, + ], + teamRanks: [ + { id: 't1', name: '华东一队', value: 386200, description: '成交额第一,团队收益 4.8 万', badge: 'TOP1' }, + { id: 't2', name: '苏州团队', value: 318760, description: '采购用户 182 人' }, + { id: 't3', name: '扬州团队', value: 287500, description: '新增成员 36 人' }, + ], + productRanks: [ + { id: 'p1', name: '高端礼盒 A 款', value: 128800, description: '上架 7 天未成交', badge: '滞销' }, + { id: 'p2', name: '精选组合 B 款', value: 98600, description: '高货值待成交' }, + { id: 'p3', name: '会员专享 C 款', value: 83500, description: '浏览高,成交低' }, + ], + risks: [ + { + id: 'r1', + level: 'red', + type: '资金', + title: '大额待审核提现', + description: '当前待审核提现 6.32 万,建议今日处理。', + discoveredAt: '11:00', + }, + { + id: 'r2', + level: 'yellow', + type: '积分', + title: '积分与个人奖金比例异常', + description: '发现 3 名用户积分未接近个人奖金的 1/2。', + discoveredAt: '10:40', + }, + { + id: 'r3', + level: 'gray', + type: '数据', + title: '用户资料不一致', + description: 'wa_users 与 eb_user 有 5 条手机号不一致。', + discoveredAt: '09:55', + }, + ], +} diff --git a/dashboard-frontend/src/features/boss-dashboard/pages/BossDashboardPage.tsx b/dashboard-frontend/src/features/boss-dashboard/pages/BossDashboardPage.tsx new file mode 100644 index 0000000..e6c6fde --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/pages/BossDashboardPage.tsx @@ -0,0 +1,105 @@ +import { Button, DotLoading, ErrorBlock } from 'antd-mobile' +import { KpiCard } from '../../../components/kpi/KpiCard' +import { MiniTrendChart } from '../../../components/charts/MiniTrendChart' +import { formatMoney } from '../../../utils/format' +import { useDashboardOverview } from '../api' +import { RankList } from '../components/RankList' +import { RiskAlertSection } from '../components/RiskAlertSection' +import { TodaySnapshotSection } from '../components/TodaySnapshotSection' + +export function BossDashboardPage() { + const { data, isLoading, isError, refetch } = useDashboardOverview() + + if (isLoading) { + return ( +
+ +

正在生成经营简报...

+
+ ) + } + + if (isError || !data) { + return ( +
+ + +
+ ) + } + + const coreKpis = data.kpis.slice(0, 4) + const moreKpis = data.kpis.slice(4) + + return ( +
+
+
+ 经营驾驶舱 + +
+

数据日期 {data.businessDate}

+

上个工作日经营简报

+

{data.summary}

+
+ 上个工作日成交额 + {formatMoney(data.kpis[0]?.value)} + 生成时间:{data.generatedAt} +
+
+ +
+ {coreKpis.map((metric) => ( + + ))} +
+ +
+
+
+

More

+

更多经营指标

+
+
+
+ {moreKpis.map((metric) => ( + + ))} +
+
+ + + +
+
+
+

Trend

+

近 7 天交易趋势

+
+
+ +
+ +
+
+
+

Fund

+

资金池摘要

+
+
+
+ {data.fundPool.map((metric) => ( + + ))} +
+
+ + + + + +
+ ) +} diff --git a/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx b/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx new file mode 100644 index 0000000..26b37e0 --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx @@ -0,0 +1,468 @@ +import { Button, CapsuleTabs, DotLoading, ErrorBlock, Tag, Toast } from 'antd-mobile' +import { useMemo, useState } from 'react' +import { MiniTrendChart } from '../../../components/charts/MiniTrendChart' +import { KpiCard } from '../../../components/kpi/KpiCard' +import { formatMoney, formatNumber } from '../../../utils/format' +import { downloadDailyReportArchive, useDashboardOverview } from '../api' +import type { DashboardOverview, RiskLevel, SnapshotSlot, TodaySnapshot } from '../types' + +const snapshotStatusMeta = { + pending: { color: 'default', label: '待生成' }, + success: { color: 'success', label: '已生成' }, + failed: { color: 'danger', label: '失败' }, + temporary: { color: 'warning', label: '临时' }, +} as const + +const riskLevelMeta: Record = { + red: { color: 'danger', label: '红色' }, + yellow: { color: 'warning', label: '黄色' }, + gray: { color: 'default', label: '灰色' }, +} + +const snapshotSlotMeta: Record< + SnapshotSlot, + { + title: string + subtitle: string + metricLabels: { + primaryUsers: string + primaryOrders: string + amount: string + paidAmount: string + merchandise: string + bonus: string + } + checklist: string[] + } +> = { + '1015': { + title: '上午抢购快报', + subtitle: '用户集中抢购上一天用户寄卖的商品,重点看成交、付款和采购用户是否达标。', + metricLabels: { + primaryUsers: '抢购用户', + primaryOrders: '抢购订单', + amount: '抢购成交额', + paidAmount: '已支付金额', + merchandise: '成交商品', + bonus: '相关奖金', + }, + checklist: ['抢购成交额是否低于昨日同节点', '采购用户是否异常回落', '付款金额与成交额是否明显偏离', '高货值寄卖商品是否完成消化'], + }, + '1455': { + title: '下午寄卖/转卖快报', + subtitle: '用户把上午抢到的商品继续寄卖或转卖,重点看新增寄售供给和奖金变化是否正常。', + metricLabels: { + primaryUsers: '寄卖用户', + primaryOrders: '转卖订单', + amount: '转卖成交额', + paidAmount: '回款金额', + merchandise: '新增寄售', + bonus: '奖金变化', + }, + checklist: ['抢购商品是否按预期转入寄卖', '新增寄售商品是否满足下午供给', '个人奖金与推广奖金是否同步变化', '转卖回款是否出现异常延迟'], + }, +} + +function QueryState({ + isLoading, + isError, + refetch, + title, +}: { + isLoading: boolean + isError: boolean + refetch: () => void + title: string +}) { + if (isLoading) { + return ( +
+ +

正在加载{title}...

+
+ ) + } + + if (isError) { + return ( +
+ + +
+ ) + } + + return null +} + +function OperationsHeader({ + kicker, + title, + description, + extra, +}: { + kicker: string + title: string + description: string + extra?: string +}) { + return ( +
+

{kicker}

+

{title}

+

{description}

+ {extra && {extra}} +
+ ) +} + +function SnapshotDetailCard({ snapshot }: { snapshot: TodaySnapshot }) { + const status = snapshotStatusMeta[snapshot.status] + const slotMeta = snapshotSlotMeta[snapshot.slot] + + return ( +
+
+
+

{snapshot.slot}

+

{slotMeta.title}

+
+ {status.label} +
+

{slotMeta.subtitle}

+

{snapshot.message}

+ {snapshot.generatedAt &&

生成时间:{snapshot.generatedAt}

} +
+ + {slotMeta.metricLabels.primaryUsers} + {formatNumber(snapshot.purchaseUsers)}人 + + + {slotMeta.metricLabels.primaryOrders} + {formatNumber(snapshot.orderCount)}单 + + + {slotMeta.metricLabels.amount} + {formatMoney(snapshot.dealAmount)} + + + {slotMeta.metricLabels.paidAmount} + {formatMoney(snapshot.paidAmount)} + + + {slotMeta.metricLabels.merchandise} + {formatNumber(snapshot.newMerchandiseCount)}件 + + + {slotMeta.metricLabels.bonus} + {formatMoney(Number(snapshot.selfBonusChange) + Number(snapshot.shareBonusChange))} + +
+
+ ) +} + +function buildDailyReports(data: DashboardOverview) { + return data.trends + .slice(-4) + .reverse() + .map((trend, index) => ({ + ...trend, + status: index === 0 ? '已生成' : '历史快照', + bonusRate: Number(trend.amount) > 0 ? (Number(trend.bonus) / Number(trend.amount)) * 100 : 0, + })) +} + +export function DailyReportPage() { + const { data, isLoading, isError, refetch } = useDashboardOverview() + const [isArchiving, setIsArchiving] = useState(false) + const state = void refetch()} title="经营日报" /> + + if (!data) return state + + const reports = buildDailyReports(data) + + const handleArchive = async () => { + try { + setIsArchiving(true) + const blob = await downloadDailyReportArchive(data.businessDate) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `dashboard-daily-report-${data.businessDate}.html` + document.body.appendChild(link) + link.click() + link.remove() + URL.revokeObjectURL(url) + Toast.show({ icon: 'success', content: '归档 HTML 已生成' }) + } catch { + Toast.show({ icon: 'fail', content: '归档生成失败,请稍后重试' }) + } finally { + setIsArchiving(false) + } + } + + return ( +
+ + +
+
+
+

Workday

+

上个工作日重点

+
+ 已归档 +
+
+ {data.kpis.slice(0, 4).map((metric) => ( + + ))} +
+
+ +
+
+
+

Trend

+

最近 7 天趋势

+
+
+ +
+ +
+
+
+

Archive

+

日报归档

+
+ +
+
+ {reports.map((report) => ( + + ))} +
+
+
+ ) +} + +export function TodaySnapshotPage() { + const { data, isLoading, isError, refetch } = useDashboardOverview() + const state = void refetch()} title="今日快报" /> + + if (!data) return state + + return ( +
+ + +
+
+
+

Timeline

+

节点快报

+
+
+
+ {data.snapshots.map((snapshot) => ( + + ))} +
+
+ +
+
+
+

Checklist

+

节点检查项

+
+
+
+ {data.snapshots.flatMap((snapshot) => + snapshotSlotMeta[snapshot.slot].checklist.map((item) => ( + + {snapshot.slot === '1015' ? '上午' : '下午'} + {item} + + )), + )} +
+
+
+ ) +} + +export function RiskCenterPage() { + const { data, isLoading, isError, refetch } = useDashboardOverview() + const [activeLevel, setActiveLevel] = useState('all') + const state = void refetch()} title="风险中心" /> + + const filteredRisks = useMemo(() => { + if (!data) return [] + if (activeLevel === 'all') return data.risks + return data.risks.filter((risk) => risk.level === activeLevel) + }, [activeLevel, data]) + + if (!data) return state + + const dangerousFunds = data.fundPool.filter((metric) => metric.status === 'warning' || metric.status === 'danger') + + return ( +
+ + +
+ {(['red', 'yellow', 'gray'] as const).map((level) => { + const meta = riskLevelMeta[level] + const count = data.risks.filter((risk) => risk.level === level).length + return ( + + ) + })} +
+ +
+ setActiveLevel(key as RiskLevel | 'all')}> + + + + + +
+ {filteredRisks.map((risk) => { + const meta = riskLevelMeta[risk.level] + return ( + + ) + })} +
+
+ +
+
+
+

Fund Watch

+

资金池关注项

+
+
+
+ {dangerousFunds.map((metric) => ( + + ))} +
+
+
+ ) +} + +export function ProfilePage() { + const { data, isLoading, isError, refetch } = useDashboardOverview() + const state = void refetch()} title="我的" /> + + if (!data) return state + + return ( +
+ + +
+ +
+

老板视角

+

可查看经营概览、今日快报、排行与风险预警。

+
+
+ +
+
+
+

Environment

+

数据环境

+
+ Mock +
+
+ + 数据日期 + {data.businessDate} + + + 生成时间 + {data.generatedAt} + + + API 模式 + {import.meta.env.VITE_MOCK_ENABLED === 'false' ? '真实接口' : 'Mock 演示'} + +
+
+ +
+
+
+

Permissions

+

权限模块

+
+
+
+ 经营概览:可见 + 资金池摘要:可见 + 风险预警:可见 + 导出能力:待接入 +
+
+
+ ) +} diff --git a/dashboard-frontend/src/features/boss-dashboard/types.ts b/dashboard-frontend/src/features/boss-dashboard/types.ts new file mode 100644 index 0000000..b17f8ac --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/types.ts @@ -0,0 +1,72 @@ +export type MetricStatus = 'normal' | 'success' | 'warning' | 'danger' + +export type SnapshotStatus = 'pending' | 'success' | 'failed' | 'temporary' + +export type SnapshotSlot = '1015' | '1455' + +export type KpiMetric = { + key: string + title: string + value: number | string | null + unit?: string + trendLabel?: string + trendValue?: number | string + status: MetricStatus + featured?: boolean +} + +export type TodaySnapshot = { + slot: SnapshotSlot + title: string + status: SnapshotStatus + generatedAt?: string + message: string + purchaseUsers: number + orderCount: number + dealAmount: number | string + paidAmount: number | string + newMerchandiseCount: number + selfBonusChange: number | string + shareBonusChange: number | string +} + +export type TrendPoint = { + date: string + amount: number | string + orders: number + newUsers: number + bonus: number | string +} + +export type RankItem = { + id: string + name: string + value: number | string + description: string + badge?: string +} + +export type RiskLevel = 'red' | 'yellow' | 'gray' + +export type RiskAlert = { + id: string + level: RiskLevel + type: string + title: string + description: string + discoveredAt: string +} + +export type DashboardOverview = { + businessDate: string + generatedAt: string + summary: string + kpis: KpiMetric[] + fundPool: KpiMetric[] + snapshots: TodaySnapshot[] + trends: TrendPoint[] + userRanks: RankItem[] + teamRanks: RankItem[] + productRanks: RankItem[] + risks: RiskAlert[] +} diff --git a/dashboard-frontend/src/features/common/PlaceholderPage.tsx b/dashboard-frontend/src/features/common/PlaceholderPage.tsx new file mode 100644 index 0000000..e5ccc7d --- /dev/null +++ b/dashboard-frontend/src/features/common/PlaceholderPage.tsx @@ -0,0 +1,18 @@ +import { Empty } from 'antd-mobile' + +type PlaceholderPageProps = { + title: string + description: string +} + +export function PlaceholderPage({ title, description }: PlaceholderPageProps) { + return ( +
+
+

经营驾驶舱

+

{title}

+
+ +
+ ) +} diff --git a/dashboard-frontend/src/index.css b/dashboard-frontend/src/index.css new file mode 100644 index 0000000..9bd4109 --- /dev/null +++ b/dashboard-frontend/src/index.css @@ -0,0 +1,589 @@ +:root { + --bg: #fff6f1; + --surface: #ffffff; + --surface-soft: #f6f9fb; + --text: #132033; + --muted: #6b7a90; + --border: rgba(19, 32, 51, 0.08); + --primary: #ff5b36; + --primary-deep: #f04a2a; + --primary-soft: #fff0eb; + --success: #14a46c; + --warning: #ffb000; + --danger: #dc2626; + --shadow: 0 16px 40px rgba(255, 91, 54, 0.14); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 14px; + font-family: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --adm-color-primary: var(--primary); + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + background: + radial-gradient(circle at top left, rgba(255, 91, 54, 0.2), transparent 28rem), + var(--bg); +} + +button { + font: inherit; +} + +button:focus-visible { + outline: 2px solid rgba(255, 91, 54, 0.72); + outline-offset: 2px; +} + +#root { + width: min(100%, 430px); + min-height: 100svh; + margin: 0 auto; + background: var(--bg); + box-shadow: 0 0 0 1px rgba(19, 32, 51, 0.04); +} + +.mobile-shell { + min-height: 100svh; + position: relative; +} + +.mobile-main { + min-height: 100svh; + padding-bottom: calc(74px + env(safe-area-inset-bottom)); +} + +.bottom-nav { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 20; + width: min(100%, 430px); + margin: 0 auto; + background: rgba(255, 255, 255, 0.94); + border-top: 1px solid var(--border); + backdrop-filter: blur(16px); +} + +.dashboard-page { + padding: 14px 14px 24px; +} + +.dashboard-hero { + position: relative; + overflow: hidden; + padding: 20px; + color: #fff; + background: + linear-gradient(145deg, rgba(255, 91, 54, 0.98), rgba(255, 139, 82, 0.92)), + radial-gradient(circle at 90% 10%, rgba(255, 176, 0, 0.42), transparent 18rem); + border-radius: 0 0 var(--radius-xl) var(--radius-xl); + box-shadow: var(--shadow); +} + +.hero-topline, +.section-title-row, +.risk-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.hero-topline span, +.eyebrow, +.section-kicker { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.hero-topline button { + min-height: 34px; + padding: 0 14px; + color: #fff; + background: rgba(255, 255, 255, 0.16); + border: 1px solid rgba(255, 255, 255, 0.24); + border-radius: 999px; +} + +.dashboard-hero h1 { + margin: 18px 0 8px; + font-size: 30px; + line-height: 1.1; +} + +.hero-summary { + margin: 0; + color: rgba(255, 255, 255, 0.78); + font-size: 14px; + line-height: 1.6; +} + +.hero-metric { + margin-top: 20px; + padding: 16px; + background: rgba(255, 255, 255, 0.14); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: var(--radius-lg); +} + +.hero-metric span, +.hero-metric small { + display: block; + color: rgba(255, 255, 255, 0.72); + font-size: 12px; +} + +.hero-metric strong { + display: block; + margin: 6px 0; + font-size: 32px; + line-height: 1; +} + +.kpi-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: 12px; +} + +.kpi-card { + min-height: 112px; + padding: 14px; + text-align: left; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08); +} + +.kpi-card--featured { + grid-column: span 2; +} + +.kpi-title, +.kpi-trend, +.section-kicker, +.snapshot-time, +.rank-content small, +.risk-item p { + margin: 0; + color: var(--muted); +} + +.kpi-value { + display: block; + margin-top: 8px; + color: var(--text); + font-size: 22px; + line-height: 1.08; + word-break: break-all; +} + +.kpi-trend { + margin-top: 8px; + font-size: 12px; +} + +.kpi-trend span { + margin-left: 6px; + color: var(--primary); + font-weight: 700; +} + +.kpi-card--success .kpi-trend span { + color: var(--success); +} + +.kpi-card--warning .kpi-trend span { + color: var(--warning); +} + +.kpi-card--danger .kpi-value, +.kpi-card--danger .kpi-trend span { + color: var(--danger); +} + +.section-block { + margin-top: 14px; + padding: 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08); +} + +.section-title-row h2 { + margin: 2px 0 0; + font-size: 18px; +} + +.compact-section .kpi-grid { + margin-top: 12px; +} + +.snapshot-section .adm-capsule-tabs { + margin: 14px 0; +} + +.snapshot-card { + padding: 14px; + background: var(--surface-soft); + border-radius: var(--radius-lg); +} + +.snapshot-message { + margin: 0 0 8px; + font-weight: 700; + line-height: 1.5; +} + +.snapshot-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: 14px; +} + +.snapshot-grid span { + min-height: 64px; + padding: 10px; + color: var(--muted); + font-size: 12px; + background: #fff; + border-radius: var(--radius-md); +} + +.snapshot-grid strong { + display: block; + margin-top: 5px; + color: var(--text); + font-size: 15px; +} + +.mini-trend-chart { + width: 100%; + height: 240px; +} + +.text-button, +.rank-item, +.risk-item { + appearance: none; + border: 0; + background: none; +} + +.text-button { + display: inline-flex; + align-items: center; + min-height: 36px; + color: var(--primary); + font-size: 13px; + font-weight: 700; +} + +.text-button:disabled { + color: var(--muted); + cursor: not-allowed; +} + +.rank-list, +.risk-list { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.rank-item, +.risk-item { + width: 100%; + min-height: 58px; + padding: 12px; + text-align: left; + background: var(--surface-soft); + border-radius: var(--radius-md); +} + +.rank-item { + display: grid; + grid-template-columns: 28px 1fr auto; + align-items: center; + gap: 10px; +} + +.rank-index { + display: inline-grid; + width: 26px; + height: 26px; + place-items: center; + color: #fff; + font-weight: 800; + background: var(--text); + border-radius: 10px; +} + +.rank-content strong, +.rank-content small { + display: block; +} + +.rank-value { + color: var(--primary); + font-size: 13px; + font-weight: 800; +} + +.risk-count { + color: var(--danger); + font-weight: 800; +} + +.risk-header { + justify-content: flex-start; + color: var(--muted); + font-size: 12px; +} + +.risk-header time { + margin-left: auto; +} + +.risk-item strong { + display: block; + margin: 10px 0 4px; + font-size: 15px; +} + +.risk-item p { + line-height: 1.5; +} + +.placeholder-page, +.loading-page, +.error-page { + padding: 24px 16px; +} + +.mobile-page-header h1 { + margin: 4px 0 24px; +} + +.operations-page { + padding: 14px 14px 24px; +} + +.operations-header { + position: relative; + overflow: hidden; + padding: 20px; + color: #fff; + background: + linear-gradient(145deg, rgba(255, 91, 54, 0.98), rgba(255, 139, 82, 0.92)), + radial-gradient(circle at 90% 10%, rgba(255, 176, 0, 0.42), transparent 18rem); + border-radius: 0 0 var(--radius-xl) var(--radius-xl); + box-shadow: var(--shadow); +} + +.operations-header h1 { + margin: 12px 0 8px; + font-size: 28px; + line-height: 1.12; +} + +.operations-header p { + margin: 0; + color: rgba(255, 255, 255, 0.76); + line-height: 1.6; +} + +.operations-header .eyebrow { + color: rgba(255, 255, 255, 0.68); +} + +.operations-header span { + display: inline-flex; + margin-top: 16px; + padding: 7px 12px; + color: rgba(255, 255, 255, 0.86); + font-size: 12px; + font-weight: 700; + background: rgba(255, 255, 255, 0.14); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 999px; +} + +.report-list, +.check-list, +.info-list { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.report-item { + display: grid; + grid-template-columns: 82px 1fr; + gap: 12px; + width: 100%; + padding: 13px; + text-align: left; + background: var(--surface-soft); + border: 0; + border-radius: var(--radius-md); +} + +.report-item span, +.info-list span { + display: grid; + gap: 4px; +} + +.report-item small, +.info-list small, +.profile-card p { + color: var(--muted); + line-height: 1.45; +} + +.snapshot-stack { + display: grid; + gap: 12px; + margin-top: 14px; +} + +.snapshot-detail-card { + padding: 14px; + background: var(--surface-soft); + border-radius: var(--radius-lg); +} + +.snapshot-detail-subtitle { + margin: 12px 0 0; + color: var(--muted); + font-size: 13px; + line-height: 1.55; +} + +.snapshot-detail-message { + margin: 10px 0 8px; + font-weight: 700; + line-height: 1.55; +} + +.snapshot-grid--wide { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.check-list span, +.info-list span { + padding: 12px; + background: var(--surface-soft); + border-radius: var(--radius-md); +} + +.check-list span { + color: var(--text); + font-weight: 700; +} + +.check-list strong { + margin-right: 8px; + color: var(--primary); +} + +.risk-summary-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 12px; +} + +.risk-summary-card { + display: grid; + min-height: 104px; + padding: 12px; + text-align: left; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08); +} + +.risk-summary-card strong { + margin-top: 8px; + font-size: 28px; + line-height: 1; +} + +.risk-summary-card span:last-child { + color: var(--muted); + font-size: 12px; +} + +.risk-summary-card--red strong { + color: var(--danger); +} + +.risk-summary-card--yellow strong { + color: var(--warning); +} + +.risk-summary-card--gray strong { + color: var(--muted); +} + +.profile-card { + display: grid; + grid-template-columns: 58px 1fr; + align-items: center; + gap: 14px; + margin-top: 14px; + padding: 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: 0 10px 28px rgba(22, 47, 80, 0.08); +} + +.profile-card h2, +.profile-card p { + margin: 0; +} + +.profile-avatar { + display: grid; + width: 58px; + height: 58px; + place-items: center; + color: #fff; + font-size: 22px; + font-weight: 800; + background: linear-gradient(145deg, var(--primary), var(--warning)); + border-radius: 22px; +} + +.loading-page, +.error-page { + display: grid; + min-height: 60svh; + place-content: center; + gap: 14px; + text-align: center; +} diff --git a/dashboard-frontend/src/main.tsx b/dashboard-frontend/src/main.tsx new file mode 100644 index 0000000..568f626 --- /dev/null +++ b/dashboard-frontend/src/main.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import 'antd-mobile/es/global' +import './index.css' +import App from './App.tsx' + +const startApp = async () => { + if (import.meta.env.VITE_MOCK_ENABLED !== 'false') { + const { worker } = await import('./services/mock/browser') + await worker.start({ onUnhandledRequest: 'bypass' }) + } + + createRoot(document.getElementById('root')!).render( + + + , + ) +} + +void startApp() diff --git a/dashboard-frontend/src/services/http/client.ts b/dashboard-frontend/src/services/http/client.ts new file mode 100644 index 0000000..ee09987 --- /dev/null +++ b/dashboard-frontend/src/services/http/client.ts @@ -0,0 +1,26 @@ +import axios from 'axios' + +export const httpClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL ?? '', + timeout: 8000, +}) + +export type ApiResponse = { + code: number + message?: string + msg?: string + data: T +} + +export async function getApiData(url: string): Promise { + const response = await httpClient.get>(url) + if (response.data.code !== 0 && response.data.code !== 200) { + throw new Error(response.data.msg ?? response.data.message ?? '接口请求失败') + } + return response.data.data +} + +export async function getBlob(url: string): Promise { + const response = await httpClient.get(url, { responseType: 'blob' }) + return response.data +} diff --git a/dashboard-frontend/src/services/mock/browser.ts b/dashboard-frontend/src/services/mock/browser.ts new file mode 100644 index 0000000..4dd03f0 --- /dev/null +++ b/dashboard-frontend/src/services/mock/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw/browser' +import { handlers } from './handlers' + +export const worker = setupWorker(...handlers) diff --git a/dashboard-frontend/src/services/mock/handlers.ts b/dashboard-frontend/src/services/mock/handlers.ts new file mode 100644 index 0000000..e8748c3 --- /dev/null +++ b/dashboard-frontend/src/services/mock/handlers.ts @@ -0,0 +1,58 @@ +import { http, HttpResponse } from 'msw' +import { dashboardMock } from '../../features/boss-dashboard/mock' + +function buildArchiveHtml() { + return ` + + + + + 经营日报归档 - ${dashboardMock.businessDate} + + + +
+
+

Daily Report Archive

+

经营日报归档

+

${dashboardMock.summary}

+ 数据日期:${dashboardMock.businessDate} / 生成时间:${dashboardMock.generatedAt} +
+
+

核心经营指标

+
+ ${dashboardMock.kpis + .map((metric) => `
${metric.title}

${metric.value}${metric.unit ?? ''}

${metric.trendLabel ?? ''}
`) + .join('')} +
+
+
+ +` +} + +export const handlers = [ + http.get('/api/admin/dashboard/overview', () => { + return HttpResponse.json({ + code: 0, + msg: 'success', + data: dashboardMock, + }) + }), + http.get('/api/admin/dashboard/daily-report/archive', () => { + return new HttpResponse(buildArchiveHtml(), { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Content-Disposition': `attachment; filename="dashboard-daily-report-${dashboardMock.businessDate}.html"`, + }, + }) + }), +] diff --git a/dashboard-frontend/src/utils/format.test.ts b/dashboard-frontend/src/utils/format.test.ts new file mode 100644 index 0000000..e935f79 --- /dev/null +++ b/dashboard-frontend/src/utils/format.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { formatMetricValue, formatMoney, formatNumber, formatTrend } from './format' + +describe('format helpers', () => { + it('formats money with yuan symbol and two decimals', () => { + expect(formatMoney(1289360.4)).toBe('¥1,289,360.40') + }) + + it('formats metric values based on unit', () => { + expect(formatMetricValue(418471.07, '分')).toBe('418,471.070') + expect(formatMetricValue(936, '人')).toBe('936人') + }) + + it('uses placeholder for empty values', () => { + expect(formatNumber(null)).toBe('--') + }) + + it('adds plus sign for positive trend values', () => { + expect(formatTrend(8.6)).toBe('+8.6%') + expect(formatTrend(-3.2)).toBe('-3.2%') + }) +}) diff --git a/dashboard-frontend/src/utils/format.ts b/dashboard-frontend/src/utils/format.ts new file mode 100644 index 0000000..50310d0 --- /dev/null +++ b/dashboard-frontend/src/utils/format.ts @@ -0,0 +1,33 @@ +export function formatMoney(value: number | string | null | undefined): string { + if (value === null || value === undefined || value === '') return '--' + const numberValue = Number(value) + if (Number.isNaN(numberValue)) return String(value) + return `¥${numberValue.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}` +} + +export function formatNumber(value: number | string | null | undefined, digits = 0): string { + if (value === null || value === undefined || value === '') return '--' + const numberValue = Number(value) + if (Number.isNaN(numberValue)) return String(value) + return numberValue.toLocaleString('zh-CN', { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }) +} + +export function formatMetricValue(value: number | string | null, unit?: string): string { + if (unit === '元') return formatMoney(value) + if (unit === '分') return formatNumber(value, 3) + return `${formatNumber(value)}${unit ?? ''}` +} + +export function formatTrend(value?: number | string): string { + if (value === undefined || value === '') return '' + const numberValue = Number(value) + if (Number.isNaN(numberValue)) return String(value) + const prefix = numberValue > 0 ? '+' : '' + return `${prefix}${numberValue.toFixed(1)}%` +} diff --git a/dashboard-frontend/tsconfig.app.json b/dashboard-frontend/tsconfig.app.json new file mode 100644 index 0000000..1d29c88 --- /dev/null +++ b/dashboard-frontend/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/dashboard-frontend/tsconfig.json b/dashboard-frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/dashboard-frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/dashboard-frontend/tsconfig.node.json b/dashboard-frontend/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/dashboard-frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/dashboard-frontend/vite.config.ts b/dashboard-frontend/vite.config.ts new file mode 100644 index 0000000..8cc3e25 --- /dev/null +++ b/dashboard-frontend/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5174, + proxy: { + '/api': { + target: 'http://localhost:30032', + changeOrigin: true, + }, + }, + }, +}) From 9a4a5f2339d94952b44e9c96d1d3f9c65b629fee Mon Sep 17 00:00:00 2001 From: danaisuiyuan Date: Mon, 11 May 2026 13:21:35 +0800 Subject: [PATCH 3/3] feat(dashboard): archive daily report from page data Generate the standalone daily report HTML from the dashboard data already loaded in the H5 page, keeping the archived page visually aligned with the mobile dashboard. Co-authored-by: Cursor --- .../src/features/boss-dashboard/archive.ts | 215 ++++++++++++++++++ .../boss-dashboard/pages/OperationsPages.tsx | 6 +- 2 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 dashboard-frontend/src/features/boss-dashboard/archive.ts diff --git a/dashboard-frontend/src/features/boss-dashboard/archive.ts b/dashboard-frontend/src/features/boss-dashboard/archive.ts new file mode 100644 index 0000000..3d52a56 --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/archive.ts @@ -0,0 +1,215 @@ +import { formatMetricValue, formatMoney, formatNumber } from '../../utils/format' +import type { DashboardOverview, MetricStatus, RankItem, RiskAlert, RiskLevel, SnapshotSlot, TodaySnapshot } from './types' + +const snapshotTitle: Record = { + '1015': '上午抢购快报', + '1455': '下午寄卖/转卖快报', +} + +const snapshotDescription: Record = { + '1015': '用户集中抢购上一天用户寄卖的商品,重点看成交、付款和采购用户是否达标。', + '1455': '用户把上午抢到的商品继续寄卖或转卖,重点看新增寄售供给和奖金变化是否正常。', +} + +const metricStatusText: Record = { + normal: '正常', + success: '达标', + warning: '关注', + danger: '异常', +} + +const riskLevelText: Record = { + red: '红色', + yellow: '黄色', + gray: '灰色', +} + +function escapeHtml(value: unknown): string { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} + +function serializeStaticData(data: DashboardOverview): string { + return JSON.stringify(data, null, 2).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e') +} + +function renderMetricGrid(metrics: DashboardOverview['kpis']): string { + return metrics + .map( + (metric) => ` +
+ ${escapeHtml(metricStatusText[metric.status])} +

${escapeHtml(metric.title)}

+ ${escapeHtml(formatMetricValue(metric.value, metric.unit))} + ${metric.trendLabel ? `

${escapeHtml(metric.trendLabel)} ${escapeHtml(metric.trendValue ?? '')}%

` : ''} +
`, + ) + .join('') +} + +function renderSnapshots(snapshots: TodaySnapshot[]): string { + return snapshots + .map((snapshot) => { + const bonusChange = Number(snapshot.selfBonusChange) + Number(snapshot.shareBonusChange) + return ` +
+
+
+ ${escapeHtml(snapshot.slot)} +

${escapeHtml(snapshotTitle[snapshot.slot])}

+
+ ${escapeHtml(snapshot.status)} +
+

${escapeHtml(snapshotDescription[snapshot.slot])}

+

${escapeHtml(snapshot.message)}

+ ${snapshot.generatedAt ? `生成时间:${escapeHtml(snapshot.generatedAt)}` : ''} +
+ 用户${escapeHtml(formatNumber(snapshot.purchaseUsers))}人 + 订单${escapeHtml(formatNumber(snapshot.orderCount))}单 + 成交额${escapeHtml(formatMoney(snapshot.dealAmount))} + 已支付${escapeHtml(formatMoney(snapshot.paidAmount))} + 商品${escapeHtml(formatNumber(snapshot.newMerchandiseCount))}件 + 奖金${escapeHtml(formatMoney(bonusChange))} +
+
` + }) + .join('') +} + +function renderRanks(title: string, ranks: RankItem[]): string { + return ` +
+

${escapeHtml(title)}

+
+ ${ranks + .map( + (rank, index) => ` +
+ ${index + 1} + + ${escapeHtml(rank.name)} + ${escapeHtml(rank.description)} + + ${escapeHtml(formatMoney(rank.value))} +
`, + ) + .join('')} +
+
` +} + +function renderRisks(risks: RiskAlert[]): string { + return risks + .map( + (risk) => ` +
+
+ ${escapeHtml(riskLevelText[risk.level])} + ${escapeHtml(risk.type)} + +
+ ${escapeHtml(risk.title)} +

${escapeHtml(risk.description)}

+
`, + ) + .join('') +} + +export function buildDailyReportArchiveHtml(data: DashboardOverview): string { + const generatedAt = new Date().toLocaleString('zh-CN', { hour12: false }) + + return ` + + + + + 经营日报归档 - ${escapeHtml(data.businessDate)} + + + +
+
+

Daily Report Archive

+

经营日报归档 ${escapeHtml(data.businessDate)}

+

${escapeHtml(data.summary)}

+
+ 业务日期:${escapeHtml(data.businessDate)} + 数据生成:${escapeHtml(data.generatedAt)} + 归档生成:${escapeHtml(generatedAt)} +
+
+

核心指标

${renderMetricGrid(data.kpis)}
+

资金池摘要

${renderMetricGrid(data.fundPool)}
+

今日快报

${renderSnapshots(data.snapshots)}
+
+

最近趋势

+
+ ${data.trends + .map( + (trend) => ` +
+ ${escapeHtml(trend.date)} + 成交 ${escapeHtml(formatMoney(trend.amount))} + 订单 ${escapeHtml(formatNumber(trend.orders))} 单 + 奖金 ${escapeHtml(formatMoney(trend.bonus))} +
`, + ) + .join('')} +
+
+ ${renderRanks('高价值用户', data.userRanks)} + ${renderRanks('团队贡献排行', data.teamRanks)} + ${renderRanks('高货值未成交商品', data.productRanks)} +

风险预警

${renderRisks(data.risks)}
+ +
+ +` +} diff --git a/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx b/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx index 26b37e0..9247b64 100644 --- a/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx +++ b/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx @@ -3,7 +3,8 @@ import { useMemo, useState } from 'react' import { MiniTrendChart } from '../../../components/charts/MiniTrendChart' import { KpiCard } from '../../../components/kpi/KpiCard' import { formatMoney, formatNumber } from '../../../utils/format' -import { downloadDailyReportArchive, useDashboardOverview } from '../api' +import { useDashboardOverview } from '../api' +import { buildDailyReportArchiveHtml } from '../archive' import type { DashboardOverview, RiskLevel, SnapshotSlot, TodaySnapshot } from '../types' const snapshotStatusMeta = { @@ -187,7 +188,8 @@ export function DailyReportPage() { const handleArchive = async () => { try { setIsArchiving(true) - const blob = await downloadDailyReportArchive(data.businessDate) + const html = buildDailyReportArchiveHtml(data) + const blob = new Blob([html], { type: 'text/html;charset=utf-8' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url