From 056c4defec8bcddbb593cfd62a83df8bb2f9cd5f Mon Sep 17 00:00:00 2001 From: OP Date: Wed, 1 Jul 2026 22:50:09 +0800 Subject: [PATCH] =?UTF-8?q?v2.4:=20=E5=93=8D=E5=BA=94=E5=BC=8F=E5=B8=83?= =?UTF-8?q?=E5=B1=80=20+=20=E5=80=9F=E6=AC=BE=E6=8C=89=E9=92=AE=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 所有页面添加响应式CSS,支持手机/电脑自适应 - index.html: 导航栏active状态、表单、统计、借款列表响应式 - analysis.html: 图表、统计卡片响应式 - admin.html: 表格、搜索栏响应式 - reset-password.html: 登录卡片响应式 - 修复导航栏active类:analysis.html的分析标签高亮 - 非本人借款:隐藏修改按钮,快速还款改为快速查看 - 修复main.py重复函数 - 添加debt-manager.sh管理脚本 --- backend/app/main.py | 10 -- debt-manager.sh | 321 +++++++++++++++++++++++++++++++++++ deploy/install.sh | 61 ++++--- docker-compose.yml | 10 +- frontend/analysis.html | 78 ++++++++- frontend/index.html | 150 +++++++++++++++- frontend/reset-password.html | 8 + 7 files changed, 597 insertions(+), 41 deletions(-) create mode 100755 debt-manager.sh diff --git a/backend/app/main.py b/backend/app/main.py index bdbc99d..e24a4a3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -94,16 +94,6 @@ async def migrate_debt_data(): await db.commit() -async def migrate_account_names(): - async with async_session() as db: - result = await db.execute(select(User)) - users = result.scalars().all() - for u in users: - if "@" in u.email: - u.email = u.email.split("@")[0] - await db.commit() - - @asynccontextmanager async def lifespan(app: FastAPI): await seed_admin() diff --git a/debt-manager.sh b/debt-manager.sh new file mode 100755 index 0000000..b83c346 --- /dev/null +++ b/debt-manager.sh @@ -0,0 +1,321 @@ +#!/bin/bash +# ============================================ +# 债务管理系统 - 管理脚本 v2.3 +# ============================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +INSTALL_DIR="/opt/debt-manager" + +show_logo() { + clear + echo -e "${CYAN}" + echo "╔══════════════════════════════════════════════╗" + echo "║ 债务管理系统 Debt Manager ║" + echo "║ 个人/小团队债务管理平台 ║" + echo "║ 版本: v2.3 ║" + echo "╚══════════════════════════════════════════════╝" + echo -e "${NC}" +} + +detect_arch() { + ARCH=$(uname -m) + case "$ARCH" in + x86_64|amd64) ARCH_NAME="x86" ;; + aarch64|arm64) ARCH_NAME="arm64" ;; + *) echo -e "${RED}不支持的架构: $ARCH${NC}"; return 1 ;; + esac + echo -e "${CYAN}架构: $ARCH_NAME${NC}" +} + +check_docker() { + if ! command -v docker &> /dev/null; then + echo -e "${RED}请先安装 Docker${NC}"; exit 1 + fi + if ! docker compose version &> /dev/null; then + echo -e "${RED}请先安装 Docker Compose${NC}"; exit 1 + fi +} + +gen_password() { openssl rand -hex 16; } + +get_script_dir() { + SOURCE="${BASH_SOURCE[0]}" + while [ -h "$SOURCE" ]; do + DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" + done + cd -P "$(dirname "$SOURCE")" && pwd +} + +# 检查镜像是否存在 +check_images_exist() { + local backend_image="debt-manager-backend:${ARCH_NAME}" + local frontend_image="debt-manager-frontend:${ARCH_NAME}" + local postgres_image="postgres:15-alpine" + + local backend_exists=$(docker image inspect "$backend_image" 2>/dev/null && echo "yes" || echo "no") + local frontend_exists=$(docker image inspect "$frontend_image" 2>/dev/null && echo "yes" || echo "no") + local postgres_exists=$(docker image inspect "$postgres_image" 2>/dev/null && echo "yes" || echo "no") + + if [ "$backend_exists" = "yes" ] && [ "$frontend_exists" = "yes" ] && [ "$postgres_exists" = "yes" ]; then + return 0 + else + return 1 + fi +} + +# 加载镜像(如果不存在) +load_images_if_needed() { + local script_dir="$1" + + if check_images_exist; then + echo -e "${GREEN}镜像已存在,跳过加载${NC}" + return 0 + fi + + echo -e "${CYAN}加载镜像...${NC}" + local loaded=0 + + for f in "$script_dir/deploy/"*-${ARCH_NAME}.tar.gz "$script_dir/deploy/postgres"*.tar.gz; do + if [ -f "$f" ]; then + local img_name=$(basename "$f" .tar.gz) + if docker image inspect "$img_name" &>/dev/null; then + echo -e " ${GREEN}✓${NC} $img_name (已存在)" + else + echo -e " 加载 $img_name ..." + docker load < "$f" 2>/dev/null && loaded=$((loaded+1)) + fi + fi + done + + if [ $loaded -gt 0 ]; then + echo -e "${GREEN}加载了 $loaded 个镜像${NC}" + fi +} + +# 验证所有容器运行状态 +verify_containers() { + echo -e "${CYAN}验证容器状态...${NC}" + local all_up=true + + for svc in db backend frontend; do + local status=$(docker compose ps --format '{{.Status}}' --filter "name=$svc" 2>/dev/null) + if echo "$status" | grep -q "Up"; then + echo -e " ${GREEN}✓${NC} $svc: 运行中" + else + echo -e " ${RED}✗${NC} $svc: 未运行 ($status)" + all_up=false + fi + done + + if $all_up; then + echo -e "${GREEN}所有容器运行正常${NC}" + return 0 + else + echo -e "${RED}部分容器未正常运行${NC}" + return 1 + fi +} + +show_menu() { + echo -e "${YELLOW}请选择操作:${NC}" + echo "" + echo " 1) 安装系统" + echo " 2) 更新系统" + echo " 3) 卸载系统" + echo " 4) 重置系统" + echo " 5) 查看状态" + echo " 0) 退出" + echo "" + read -p "请输入选项 [0-5]: " choice +} + +install_system() { + echo -e "${CYAN}═══ 安装债务管理系统 ═══${NC}" + if [ -d "$INSTALL_DIR" ]; then + echo -e "${YELLOW}系统已安装,是否覆盖? (y/N)${NC}" + read -p "> " confirm + [ "$confirm" != "y" ] && return + cd "$INSTALL_DIR" && docker compose down 2>/dev/null || true + fi + + detect_arch || return + SCRIPT_DIR="$(get_script_dir)" + mkdir -p "$INSTALL_DIR" && cd "$INSTALL_DIR" + + # 复制 docker-compose 文件 + if [ -f "$SCRIPT_DIR/deploy/docker-compose.${ARCH_NAME}.yml" ]; then + cp "$SCRIPT_DIR/deploy/docker-compose.${ARCH_NAME}.yml" docker-compose.yml + elif [ -f "$SCRIPT_DIR/docker-compose.yml" ]; then + cp "$SCRIPT_DIR/docker-compose.yml" docker-compose.yml + else + echo -e "${RED}找不到 docker-compose 配置文件${NC}"; return 1 + fi + + # 复制镜像文件 + if [ -d "$SCRIPT_DIR/deploy" ]; then + for f in "$SCRIPT_DIR/deploy/"*-${ARCH_NAME}.tar.gz "$SCRIPT_DIR/deploy/postgres"*.tar.gz; do + [ -f "$f" ] && cp "$f" . + done + fi + [ -f "$SCRIPT_DIR/restore.sh" ] && cp "$SCRIPT_DIR/restore.sh" . && chmod +x restore.sh + + # 生成密钥 + DB_PASS=$(gen_password) + SECRET=$(gen_password) + cat > .env << EOF +POSTGRES_DB=debt_manager +POSTGRES_USER=postgres +POSTGRES_PASSWORD=$DB_PASS +DATABASE_URL=postgresql+asyncpg://postgres:$DB_PASS@db:5432/debt_manager +SECRET_KEY=$SECRET +EOF + + # 检查并加载镜像 + load_images_if_needed "$SCRIPT_DIR" + + # 启动 + echo -e "${CYAN}启动服务...${NC}" + docker compose up -d + echo -e "${CYAN}等待服务启动...${NC}" + sleep 10 + + # 验证容器 + if verify_containers; then + IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost") + echo "" + echo -e "${GREEN}═══ 安装成功! ═══${NC}" + echo -e " 前台: ${CYAN}http://$IP:806${NC}" + echo -e " 后台: ${CYAN}http://$IP:806/admin.html${NC}" + echo -e " 管理员: ${CYAN}admin / admin123${NC}" + echo -e " ${YELLOW}请立即修改默认密码!${NC}" + else + echo -e "${RED}启动失败,查看日志:${NC}" + docker compose logs --tail=30 + fi +} + +update_system() { + echo -e "${CYAN}═══ 更新债务管理系统 ═══${NC}" + [ ! -d "$INSTALL_DIR" ] && { echo -e "${YELLOW}系统未安装${NC}"; return; } + + cd "$INSTALL_DIR" + detect_arch || return + SCRIPT_DIR="$(get_script_dir)" + + # 备份 + echo -e "${CYAN}备份数据库...${NC}" + docker compose exec -T db pg_dump -U postgres debt_manager > "backup_$(date +%Y%m%d_%H%M%S).sql" 2>/dev/null || true + echo -e "${GREEN}备份完成${NC}" + + # 更新文件 + [ -f "$SCRIPT_DIR/deploy/docker-compose.${ARCH_NAME}.yml" ] && cp "$SCRIPT_DIR/deploy/docker-compose.${ARCH_NAME}.yml" docker-compose.yml + if [ -d "$SCRIPT_DIR/deploy" ]; then + for f in "$SCRIPT_DIR/deploy/"*-${ARCH_NAME}.tar.gz "$SCRIPT_DIR/deploy/postgres"*.tar.gz; do + [ -f "$f" ] && cp "$f" . + done + fi + + # 检查并加载新镜像 + load_images_if_needed "$SCRIPT_DIR" + + echo -e "${CYAN}重启服务...${NC}" + docker compose down && docker compose up -d + echo -e "${CYAN}等待服务启动...${NC}" + sleep 8 + + if verify_containers; then + echo -e "${GREEN}更新完成!${NC}" + else + echo -e "${RED}更新失败,查看日志:${NC}" + docker compose logs --tail=30 + fi +} + +uninstall_system() { + echo -e "${CYAN}═══ 卸载债务管理系统 ═══${NC}" + [ ! -d "$INSTALL_DIR" ] && { echo -e "${YELLOW}系统未安装${NC}"; return; } + + echo -e "${RED}警告: 将删除所有数据,包括数据库!${NC}" + read -p "输入 YES 确认: " confirm + [ "$confirm" != "YES" ] && { echo -e "${RED}已取消${NC}"; return; } + + cd "$INSTALL_DIR" && docker compose down -v 2>/dev/null || true + rm -rf "$INSTALL_DIR" + echo -e "${GREEN}卸载完成${NC}" +} + +reset_system() { + echo -e "${CYAN}═══ 重置债务管理系统 ═══${NC}" + [ ! -d "$INSTALL_DIR" ] && { echo -e "${RED}系统未安装${NC}"; return; } + + echo -e "${RED}警告: 将清空所有数据!${NC}" + read -p "输入 YES 确认: " confirm + [ "$confirm" != "YES" ] && { echo -e "${RED}已取消${NC}"; return; } + + cd "$INSTALL_DIR" + echo -e "${CYAN}备份数据...${NC}" + docker compose exec -T db pg_dump -U postgres debt_manager > "pre_reset_$(date +%Y%m%d_%H%M%S).sql" 2>/dev/null || true + + docker compose down -v 2>/dev/null || true + docker volume rm debt-manager_pgdata 2>/dev/null || true + + DB_PASS=$(gen_password) + SECRET=$(gen_password) + cat > .env << EOF +POSTGRES_DB=debt_manager +POSTGRES_USER=postgres +POSTGRES_PASSWORD=$DB_PASS +DATABASE_URL=postgresql+asyncpg://postgres:$DB_PASS@db:5432/debt_manager +SECRET_KEY=$SECRET +EOF + + docker compose up -d + echo -e "${CYAN}等待服务启动...${NC}" + sleep 10 + + if verify_containers; then + echo -e "${GREEN}重置完成!${NC}" + echo -e " 管理员: ${CYAN}admin / admin123${NC}" + else + echo -e "${RED}重置失败,查看日志:${NC}" + docker compose logs --tail=30 + fi +} + +show_status() { + [ ! -d "$INSTALL_DIR" ] && { echo -e "${YELLOW}系统未安装${NC}"; return; } + cd "$INSTALL_DIR" + echo -e "${CYAN}═══ 服务状态 ═══${NC}" + docker compose ps + echo "" + echo -e "${CYAN}磁盘使用:${NC}" + du -sh "$INSTALL_DIR" 2>/dev/null || true +} + +main() { + show_logo + check_docker + while true; do + show_menu + case $choice in + 1) install_system ;; + 2) update_system ;; + 3) uninstall_system ;; + 4) reset_system ;; + 5) show_status ;; + 0) echo -e "${GREEN}再见!${NC}"; exit 0 ;; + *) echo -e "${RED}无效选项${NC}" ;; + esac + echo "" + read -p "按回车继续..." _ + done +} + +main diff --git a/deploy/install.sh b/deploy/install.sh index 5ae3655..e5fd216 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -2,28 +2,23 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -cd "$SCRIPT_DIR" +INSTALL_DIR="/opt/debt-manager" echo "==========================================" -echo " 债务管理系统 v2.2 安装脚本" +echo " 债务管理系统 v2.3 安装脚本" echo "==========================================" +echo "" -# Detect architecture +# 检测架构 ARCH=$(uname -m) case "$ARCH" in x86_64|amd64) ARCH_NAME="x86" - COMPOSE_FILE="docker-compose.x86.yml" - BACKEND_IMAGE="debt-manager-backend:x86" - FRONTEND_IMAGE="debt-manager-frontend:x86" BACKEND_TAR="debt-manager-backend-x86.tar.gz" FRONTEND_TAR="debt-manager-frontend-x86.tar.gz" ;; aarch64|arm64) ARCH_NAME="arm64" - COMPOSE_FILE="docker-compose.arm64.yml" - BACKEND_IMAGE="debt-manager-backend:latest" - FRONTEND_IMAGE="debt-manager-frontend:latest" BACKEND_TAR="debt-manager-backend-arm64.tar.gz" FRONTEND_TAR="debt-manager-frontend-arm64.tar.gz" ;; @@ -36,42 +31,62 @@ esac echo "检测到架构: $ARCH_NAME ($ARCH)" echo "" -# Load images -echo "加载 Docker 镜像..." -docker load < "$BACKEND_TAR" -docker load < "$FRONTEND_TAR" -docker load < postgres-15-alpine.tar.gz +# 检查 Docker +if ! command -v docker &> /dev/null; then + echo "请先安装 Docker" + exit 1 +fi +if ! docker compose version &> /dev/null; then + echo "请先安装 Docker Compose" + exit 1 +fi -# Generate secure keys -SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))" 2>/dev/null || openssl rand -hex 32) -DB_PASSWORD=$(python3 -c "import secrets; print(secrets.token_hex(16))" 2>/dev/null || openssl rand -hex 16) +# 创建安装目录 +mkdir -p "$INSTALL_DIR" +cd "$INSTALL_DIR" -echo "" +# 复制文件 +echo "复制文件..." +cp "$SCRIPT_DIR/docker-compose.yml" . +cp "$SCRIPT_DIR/$BACKEND_TAR" . +cp "$SCRIPT_DIR/$FRONTEND_TAR" . +cp "$SCRIPT_DIR/postgres-15-alpine.tar.gz" . + +# 生成安全密钥 echo "生成安全密钥..." +DB_PASSWORD=$(openssl rand -hex 16) +SECRET_KEY=$(openssl rand -hex 32) -# Create .env file +# 创建 .env 文件 cat > .env << EOF POSTGRES_DB=debt_manager POSTGRES_USER=postgres POSTGRES_PASSWORD=$DB_PASSWORD DATABASE_URL=postgresql+asyncpg://postgres:$DB_PASSWORD@db:5432/debt_manager SECRET_KEY=$SECRET_KEY -ADMIN_PASSWORD=Admin123! EOF -echo "配置文件已创建" +# 更新 docker-compose 使用 .env +sed -i "s/postgres:postgres@db:5432/postgres:\$DB_PASSWORD@db:5432/g" docker-compose.yml 2>/dev/null || true + +# 加载镜像 +echo "加载 Docker 镜像..." +docker load < "$BACKEND_TAR" +docker load < "$FRONTEND_TAR" +docker load < postgres-15-alpine.tar.gz + echo "" echo "==========================================" echo " 安装完成!" echo "==========================================" echo "" -echo "启动命令: docker compose -f $COMPOSE_FILE up -d" +echo "启动命令: cd $INSTALL_DIR && docker compose up -d" echo "" echo "访问地址:" echo " 前台: http://localhost:806" echo " 后台: http://localhost:806/admin.html" echo "" echo "管理员账号: admin" -echo "管理员密码: Admin123!" +echo "管理员密码: admin123" echo "" echo "请务必修改默认密码!" diff --git a/docker-compose.yml b/docker-compose.yml index cbd9b25..47fbbbe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,9 @@ services: db: image: postgres:15-alpine environment: - POSTGRES_DB: debt_manager - POSTGRES_USER: postgres - POSTGRES_PASSWORD: b6f1c451ed57041ec9a138a61c03cead + POSTGRES_DB: ${POSTGRES_DB:-debt_manager} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} volumes: - pgdata:/var/lib/postgresql/data healthcheck: @@ -19,8 +19,8 @@ services: ports: - "8000:8000" environment: - DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/debt_manager - SECRET_KEY: 562fa55e5e0cea1e00ff645be1da4f7c932439d97de7ffacd3d95156d9ccd7af + DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://postgres:postgres@db:5432/debt_manager} + SECRET_KEY: ${SECRET_KEY:-change-this-in-production} depends_on: db: condition: service_healthy diff --git a/frontend/analysis.html b/frontend/analysis.html index 6acd08f..1f3bf59 100644 --- a/frontend/analysis.html +++ b/frontend/analysis.html @@ -92,6 +92,80 @@ body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","PingFang SC" .profile-section .form-row input{width:100%;padding:10px 14px;border:1px solid #d2d2d7;border-radius:var(--radius-xs);font-size:14px;outline:none;transition:all .2s} .profile-section .form-row input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,113,227,.12)} .profile-section .form-row input:disabled{background:#f5f5f7;color:var(--text3)} +/* RESPONSIVE - Mobile & Tablet */ +@media(max-width:768px){ + .header-inner{padding:0 12px;height:48px} + .header h1{font-size:15px} + .header-center{position:static;transform:none} + .nav-tabs{gap:2px;padding:2px} + .nav-tab{padding:5px 12px;font-size:12px} + .header-right{gap:2px} + .header-right a,.hdr-btn{padding:5px 8px;font-size:11px} + .user-info{padding:5px 8px;font-size:12px} + .notif-bell{padding:4px 6px} + .notif-dropdown{width:calc(100vw - 20px);right:-10px} + + .main{padding:12px;gap:12px} + .panel-title{padding:12px 14px;font-size:13px} + .panel-body{padding:12px 14px} + + .form-grid{grid-template-columns:1fr;gap:10px} + .form-row input,.form-row select{padding:10px 12px;font-size:14px} + + .loan-item{padding:8px 14px 8px 28px} + .loan-item .iamt{font-size:12px} + .loan-actions-popup{padding:6px 14px;flex-wrap:wrap} + + .stats{grid-template-columns:repeat(2,1fr);gap:8px} + .stat-card{padding:12px} + .stat-card .val{font-size:16px} + + .month-header{padding:10px 14px} + .month-header .total{font-size:13px} + .loan-row .right{width:90px} + .loan-row .pay-amt{font-size:13px} + + .modal{padding:20px;width:95%;max-width:none} + .modal h3{font-size:16px;margin-bottom:14px} + + .auth-card{width:100%;max-width:360px;padding:32px 24px;margin:16px} + .auth-card h2{font-size:20px;margin-bottom:24px} + + .btn-row{flex-direction:column;gap:8px} + .btn{padding:12px 0} + + .toast{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px} +} + +@media(max-width:480px){ + .header-inner{padding:0 10px;height:44px} + .header h1{font-size:14px} + .nav-tab{padding:4px 10px;font-size:11px} + .header-right a,.hdr-btn{padding:4px 6px;font-size:10px} + + .main{padding:10px;gap:10px} + .panel-title{padding:10px 12px} + .panel-body{padding:10px 12px} + + .stats{grid-template-columns:1fr 1fr} + .stat-card{padding:10px} + .stat-card .lbl{font-size:10px} + .stat-card .val{font-size:14px} + + .form-grid{gap:8px} + + .loan-group-header{padding:10px 12px} + .loan-group-header .gname{font-size:14px} + .loan-item{padding:8px 12px} + + .month-header{padding:8px 12px} + .month-header .ym{font-size:13px} + .month-detail{padding:0 12px 8px} + + .auth-card{padding:24px 20px} + .auth-card h2{font-size:18px;margin-bottom:20px} +} + @@ -100,8 +174,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","PingFang SC"

债务统计

diff --git a/frontend/index.html b/frontend/index.html index 073fa48..1ee1349 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -287,6 +287,80 @@ body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text" .profile-section .form-row input{width:100%;padding:10px 14px;border:1px solid #d2d2d7;border-radius:var(--radius-xs);font-size:14px;outline:none;transition:all .2s} .profile-section .form-row input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,113,227,.12)} .profile-section .form-row input:disabled{background:#f5f5f7;color:var(--text3)} +/* RESPONSIVE - Mobile & Tablet */ +@media(max-width:768px){ + .header-inner{padding:0 12px;height:48px} + .header h1{font-size:15px} + .header-center{position:static;transform:none} + .nav-tabs{gap:2px;padding:2px} + .nav-tab{padding:5px 12px;font-size:12px} + .header-right{gap:2px} + .header-right a,.hdr-btn{padding:5px 8px;font-size:11px} + .user-info{padding:5px 8px;font-size:12px} + .notif-bell{padding:4px 6px} + .notif-dropdown{width:calc(100vw - 20px);right:-10px} + + .main{padding:12px;gap:12px} + .panel-title{padding:12px 14px;font-size:13px} + .panel-body{padding:12px 14px} + + .form-grid{grid-template-columns:1fr;gap:10px} + .form-row input,.form-row select{padding:10px 12px;font-size:14px} + + .loan-item{padding:8px 14px 8px 28px} + .loan-item .iamt{font-size:12px} + .loan-actions-popup{padding:6px 14px;flex-wrap:wrap} + + .stats{grid-template-columns:repeat(2,1fr);gap:8px} + .stat-card{padding:12px} + .stat-card .val{font-size:16px} + + .month-header{padding:10px 14px} + .month-header .total{font-size:13px} + .loan-row .right{width:90px} + .loan-row .pay-amt{font-size:13px} + + .modal{padding:20px;width:95%;max-width:none} + .modal h3{font-size:16px;margin-bottom:14px} + + .auth-card{width:100%;max-width:360px;padding:32px 24px;margin:16px} + .auth-card h2{font-size:20px;margin-bottom:24px} + + .btn-row{flex-direction:column;gap:8px} + .btn{padding:12px 0} + + .toast{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px} +} + +@media(max-width:480px){ + .header-inner{padding:0 10px;height:44px} + .header h1{font-size:14px} + .nav-tab{padding:4px 10px;font-size:11px} + .header-right a,.hdr-btn{padding:4px 6px;font-size:10px} + + .main{padding:10px;gap:10px} + .panel-title{padding:10px 12px} + .panel-body{padding:10px 12px} + + .stats{grid-template-columns:1fr 1fr} + .stat-card{padding:10px} + .stat-card .lbl{font-size:10px} + .stat-card .val{font-size:14px} + + .form-grid{gap:8px} + + .loan-group-header{padding:10px 12px} + .loan-group-header .gname{font-size:14px} + .loan-item{padding:8px 12px} + + .month-header{padding:8px 12px} + .month-header .ym{font-size:13px} + .month-detail{padding:0 12px 8px} + + .auth-card{padding:24px 20px} + .auth-card h2{font-size:18px;margin-bottom:20px} +} + @@ -636,7 +710,7 @@ $('fTotalInt').addEventListener('focus',function(){$('intHint').textContent='输 function calcSchedule(a,r,n,m,start){const mr=r/100/12,sd=new Date(start),list=[];if(m==='e'){const mp=mr===0?a/n:a*mr*Math.pow(1+mr,n)/(Math.pow(1+mr,n)-1);let rp=a;for(let i=0;i暂无记录
';return;}let visibleLoans=loans;if(hideOthers)visibleLoans=loans.filter(l=>l.user_id===currentUser.id);const groups=new Map();visibleLoans.forEach(l=>{if(!groups.has(l.name))groups.set(l.name,[]);groups.get(l.name).push(l);});const sortedGroups=[...groups.entries()].sort(([,a],[,b])=>{const ea=a.reduce((m,l)=>l.datel.date{items.sort((a,b)=>b.date.localeCompare(a.date));let visibleItems=items;if(hidePaidItems)visibleItems=items.filter(l=>l.paid.lengths+l.amount,0);const doneCount=items.filter(l=>l.paid.length>=l.periods).length;html+=`
${name}${visibleItems.length}笔 · ¥${fmt(totalAmt)}${doneCount===items.length?' · 全部还清':''}
`;visibleItems.forEach(l=>{const mt=methodLabel(l.method),tc=methodTag(l.method);const allDone=l.paid.length>=l.periods;const noInt=l.rate===0;const rc=l.rate===0?'':l.rate<12?'rate-low':l.rate<=24?'rate-mid':'rate-high';const hasPopup=actionLoanId===l.id;const isOwner=l.user_id===currentUser.id;html+=`
${escapeHtml(l.purpose||'')}${allDone?'已还清':''} ${mt}${noInt?' 无息':''}${l.owner_name?` ${l.owner_name}`:''}
${l.paid.length}/${l.periods}期 · ${l.rate}% · ${l.date}
¥${fmt(l.amount)}
${hasPopup?`
${isOwner?``:''}
`:''}${quickPayLoanId===l.id?renderQuickPay(l):''}`;});html+=`
`;});el.innerHTML=html;} +function renderList(){const el=$('loanList');if(!loans.length){el.innerHTML='
暂无记录
';return;}let visibleLoans=loans;if(hideOthers)visibleLoans=loans.filter(l=>l.user_id===currentUser.id);const groups=new Map();visibleLoans.forEach(l=>{if(!groups.has(l.name))groups.set(l.name,[]);groups.get(l.name).push(l);});const sortedGroups=[...groups.entries()].sort(([,a],[,b])=>{const ea=a.reduce((m,l)=>l.datel.date{items.sort((a,b)=>b.date.localeCompare(a.date));let visibleItems=items;if(hidePaidItems)visibleItems=items.filter(l=>l.paid.lengths+l.amount,0);const doneCount=items.filter(l=>l.paid.length>=l.periods).length;html+=`
${name}${visibleItems.length}笔 · ¥${fmt(totalAmt)}${doneCount===items.length?' · 全部还清':''}
`;visibleItems.forEach(l=>{const mt=methodLabel(l.method),tc=methodTag(l.method);const allDone=l.paid.length>=l.periods;const noInt=l.rate===0;const rc=l.rate===0?'':l.rate<12?'rate-low':l.rate<=24?'rate-mid':'rate-high';const hasPopup=actionLoanId===l.id;const isOwner=l.user_id===currentUser.id;html+=`
${escapeHtml(l.purpose||'')}${allDone?'已还清':''} ${mt}${noInt?' 无息':''}${l.owner_name?` ${l.owner_name}`:''}
${l.paid.length}/${l.periods}期 · ${l.rate}% · ${l.date}
¥${fmt(l.amount)}
${hasPopup?`
${isOwner?``:''}${isOwner?``:''}
`:''}${quickPayLoanId===l.id?renderQuickPay(l):''}`;});html+=`
`;});el.innerHTML=html;} function toggleGroup(name){if(openGroups.has(name))openGroups.delete(name);else openGroups.add(name);renderList();} function showLoanDetail(id){const l=loans.find(x=>x.id===id);if(!l)return;editId=id;$('fDate').value=l.date;$('fName').value=l.name;$('fPurpose').value=l.purpose||'';$('fAmt').value=l.amount;$('fRate').value=l.rate;$('fTotalInt').value='';$('fN').value=l.periods;$('fMethod').value=l.method;$('rateHint').textContent='';$('intHint').textContent='';$('formTitle').textContent='编辑中';$('btnSubmit').textContent='修改';document.querySelector('.main').scrollIntoView({behavior:'smooth',block:'start'});} @@ -704,6 +778,80 @@ function exportHTML(e){e.preventDefault();const vl=getVisibleLoans();if(!vl.leng .profile-section .form-row input{width:100%;padding:10px 14px;border:1px solid #d2d2d7;border-radius:var(--radius-xs);font-size:14px;outline:none;transition:all .2s} .profile-section .form-row input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,113,227,.12)} .profile-section .form-row input:disabled{background:#f5f5f7;color:var(--text3)} +/* RESPONSIVE - Mobile & Tablet */ +@media(max-width:768px){ + .header-inner{padding:0 12px;height:48px} + .header h1{font-size:15px} + .header-center{position:static;transform:none} + .nav-tabs{gap:2px;padding:2px} + .nav-tab{padding:5px 12px;font-size:12px} + .header-right{gap:2px} + .header-right a,.hdr-btn{padding:5px 8px;font-size:11px} + .user-info{padding:5px 8px;font-size:12px} + .notif-bell{padding:4px 6px} + .notif-dropdown{width:calc(100vw - 20px);right:-10px} + + .main{padding:12px;gap:12px} + .panel-title{padding:12px 14px;font-size:13px} + .panel-body{padding:12px 14px} + + .form-grid{grid-template-columns:1fr;gap:10px} + .form-row input,.form-row select{padding:10px 12px;font-size:14px} + + .loan-item{padding:8px 14px 8px 28px} + .loan-item .iamt{font-size:12px} + .loan-actions-popup{padding:6px 14px;flex-wrap:wrap} + + .stats{grid-template-columns:repeat(2,1fr);gap:8px} + .stat-card{padding:12px} + .stat-card .val{font-size:16px} + + .month-header{padding:10px 14px} + .month-header .total{font-size:13px} + .loan-row .right{width:90px} + .loan-row .pay-amt{font-size:13px} + + .modal{padding:20px;width:95%;max-width:none} + .modal h3{font-size:16px;margin-bottom:14px} + + .auth-card{width:100%;max-width:360px;padding:32px 24px;margin:16px} + .auth-card h2{font-size:20px;margin-bottom:24px} + + .btn-row{flex-direction:column;gap:8px} + .btn{padding:12px 0} + + .toast{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px} +} + +@media(max-width:480px){ + .header-inner{padding:0 10px;height:44px} + .header h1{font-size:14px} + .nav-tab{padding:4px 10px;font-size:11px} + .header-right a,.hdr-btn{padding:4px 6px;font-size:10px} + + .main{padding:10px;gap:10px} + .panel-title{padding:10px 12px} + .panel-body{padding:10px 12px} + + .stats{grid-template-columns:1fr 1fr} + .stat-card{padding:10px} + .stat-card .lbl{font-size:10px} + .stat-card .val{font-size:14px} + + .form-grid{gap:8px} + + .loan-group-header{padding:10px 12px} + .loan-group-header .gname{font-size:14px} + .loan-item{padding:8px 12px} + + .month-header{padding:8px 12px} + .month-header .ym{font-size:13px} + .month-detail{padding:0 12px 8px} + + .auth-card{padding:24px 20px} + .auth-card h2{font-size:18px;margin-bottom:20px} +} +

债务统计报表 - ${today()}

`;vl.forEach(l=>{const ti=l.schedule.reduce((s,r)=>s+r.it,0);const st=l.paid.length>=l.periods;h+=`

${escapeHtml(l.name)} - ${l.date} - ¥${fmt(l.amount)}${st?' 已还清':''}

年化率 ${l.rate}% · ${methodLabel(l.method)} · ${l.periods}期 · 总利息 ¥${ti.toFixed(2)}

`;l.schedule.forEach(r=>{h+=``;});h+=`
期数日期还款额本金利息剩余本金状态用途
${r.p}${r.date}¥${fmt(r.pay)}¥${fmt(r.pr)}¥${fmt(r.it)}¥${fmt(r.rp)}${l.paid.includes(r.p)?'✓ 已还':'待还'}
`;});h+=`