diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4eeca62..8e1cb95 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -52,7 +52,7 @@ jobs: # 更新数据库结构 - name: Update Database Schema - uses: cloudflare/wrangler-action@v3 + uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7051d46..a9f0454 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,6 +10,8 @@ jobs: docker: runs-on: ubuntu-latest name: "Build and push Docker images" + permissions: + contents: read env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} @@ -20,19 +22,19 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ env.DOCKER_USERNAME }} password: ${{ env.DOCKER_PASSWORD }} - name: Push images to Docker hub - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: true diff --git a/.github/workflows/rclone.yml b/.github/workflows/rclone.yml index aa97318..d3f4278 100644 --- a/.github/workflows/rclone.yml +++ b/.github/workflows/rclone.yml @@ -56,7 +56,7 @@ jobs: steps: # STEP 1: 安装 Rclone - name: 🛠️ Setup Tools - uses: AnimMouse/setup-rclone@v1 + uses: AnimMouse/setup-rclone@02a3642a1ad72d5b765f48873141b9f26e9082f1 # v1 with: version: v1.66.0 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8711fb4..1e75d9b 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -25,7 +25,7 @@ jobs: # 1. 检查代码中的敏感信息 (Gitleaks) - name: Secret Detection id: gitleaks - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2 continue-on-error: true # 2. 检查依赖包漏洞 (Snyk) @@ -39,7 +39,8 @@ jobs: echo "Installing frontend dependencies..." cd frontend && npm install --package-lock-only && cd .. snyk auth ${{ secrets.SNYK_TOKEN }} - snyk test --all-projects > snyk_result.txt || true + # 🚀 UX优化:同时输出人类可读的 TXT 日志与供程序解析的 JSON 数据文件 + snyk test --all-projects --json-file-output=snyk_result.json > snyk_result.txt || true env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} @@ -48,8 +49,12 @@ jobs: if: env.ACT != 'true' uses: github/codeql-action/init@v4 with: - languages: javascript-typescript - token: ${{ secrets.GITHUB_TOKEN }} + languages: javascript-typescript, actions + queries: security-extended,security-and-quality + + - name: Autobuild + if: env.ACT != 'true' + uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis if: env.ACT != 'true' @@ -58,43 +63,80 @@ jobs: upload: true output: sarif-results - # 4. 生成 SECURITY.md 报告 + # 4. 生成 README.md 报告 (🚀 UX 深度优化版) - name: Generate Security Report run: | - REPORT_FILE="SECURITY.md" + REPORT_FILE="README.md" - # 1. 动态获取审计文件计数 (多路径探测 + SARIF 补位) + CODEQL_SARIFS=$(find sarif-results -name "*.sarif" 2>/dev/null) REAL_CSV=$(find . -name "code-scanning-files-extracted.csv" | head -n 1) - SARIF_FILE=$(find . -name "*.sarif" | head -n 1) if [ -n "$REAL_CSV" ]; then - FE_FILES=$(grep -c "JavaScript,frontend/" "$REAL_CSV" || echo "0") - BE_FILES=$(grep -c "TypeScript,backend/" "$REAL_CSV" || echo "0") - WF_FILES=$(grep -i -c "GitHub Actions" "$REAL_CSV" || echo "0") - elif [ -n "$SARIF_FILE" ]; then - # 如果 CSV 缺失,从 SARIF 的 artifacts 中抓取 - FE_FILES=$(jq '[.runs[].artifacts[]? | select(.location.uri | contains("frontend/"))] | length' "$SARIF_FILE" || echo "0") - BE_FILES=$(jq '[.runs[].artifacts[]? | select(.location.uri | contains("backend/"))] | length' "$SARIF_FILE" || echo "0") - WF_FILES=$(jq '[.runs[].artifacts[]? | select(.location.uri | contains(".github/workflows/"))] | length' "$SARIF_FILE" || echo "0") + # Fallback csv 模式 + WF_FILES=$(grep ",GitHub Actions," "$REAL_CSV" | grep "\.github/workflows/" | sort -u | wc -l) + JS_FILES=$(grep ",JavaScript," "$REAL_CSV" | sort -u | wc -l) + TS_FILES=$(grep ",TypeScript," "$REAL_CSV" | sort -u | wc -l) + + gen_csv_table() { + echo "| 文件路径 | 状态 |" + echo "| :--- | :--- |" + if [ "$1" == "GitHub Actions" ]; then + grep ",$1," "$REAL_CSV" | grep "\.github/workflows/" | awk -F',' '{print "| `"$3"` | ✅ 已审计 |"}' | sort -u + else + grep ",$1," "$REAL_CSV" | awk -F',' '{print "| `"$3"` | ✅ 已审计 |"}' | sort -u + fi + } + WF_TABLE=$(gen_csv_table "GitHub Actions") + JS_TABLE=$(gen_csv_table "JavaScript") + TS_TABLE=$(gen_csv_table "TypeScript") + + elif [ -n "$CODEQL_SARIFS" ]; then + WF_FILES=$(jq -s '[.[].runs[].artifacts[]? | select(.location.uri | startswith(".github/workflows/") and test("\\.ya?ml$"))] | unique_by(.location.uri) | length' $CODEQL_SARIFS || echo "0") + JS_FILES=$(jq -s '[.[].runs[].artifacts[]? | select(.location.uri | endswith(".js"))] | unique_by(.location.uri) | length' $CODEQL_SARIFS || echo "0") + TS_FILES=$(jq -s '[.[].runs[].artifacts[]? | select(.location.uri | endswith(".ts"))] | unique_by(.location.uri) | length' $CODEQL_SARIFS || echo "0") + + gen_sarif_table() { + echo "| 文件路径 | 状态 |" + echo "| :--- | :--- |" + if [ "$1" == "workflow" ]; then + jq -s -r '.[].runs[].artifacts[]? | select(.location.uri | startswith(".github/workflows/") and test("\\.ya?ml$")) | .location.uri' $CODEQL_SARIFS | sort -u | awk '{print "| `"$1"` | ✅ 已审计 |"}' + elif [ "$1" == "js" ]; then + jq -s -r '.[].runs[].artifacts[]? | select(.location.uri | endswith(".js")) | .location.uri' $CODEQL_SARIFS | sort -u | awk '{print "| `"$1"` | ✅ 已审计 |"}' + elif [ "$1" == "ts" ]; then + jq -s -r '.[].runs[].artifacts[]? | select(.location.uri | endswith(".ts")) | .location.uri' $CODEQL_SARIFS | sort -u | awk '{print "| `"$1"` | ✅ 已审计 |"}' + fi + } + WF_TABLE=$(gen_sarif_table "workflow") + JS_TABLE=$(gen_sarif_table "js") + TS_TABLE=$(gen_sarif_table "ts") else - FE_FILES="0"; BE_FILES="0"; WF_FILES="0" + WF_FILES="0"; JS_FILES="0"; TS_FILES="0" + WF_TABLE="| No files found | - |" + JS_TABLE="| No files found | - |" + TS_TABLE="| No files found | - |" fi AUDIT_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC") RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - # 状态徽标 BADGE_PASS="![Pass](https://img.shields.io/badge/Status-PASS-success?style=for-the-badge)" BADGE_WARN="![Warning](https://img.shields.io/badge/Status-NOTICE-orange?style=for-the-badge)" BADGE_FAIL="![Fail](https://img.shields.io/badge/Status-FAIL-red?style=for-the-badge)" + # 提取 Gitleaks 结果 GITLEAKS_RES="PASS" [ "${{ steps.gitleaks.outcome }}" != "success" ] && GITLEAKS_RES="FAIL" + # 🚀 UX优化:解析 Snyk JSON 计算实际漏洞数量 + SNYK_VULNS_COUNT=0 + if [ -f "snyk_result.json" ]; then + SNYK_VULNS_COUNT=$(jq 'if type=="array" then [.[].vulnerabilities[]?] | length else (.vulnerabilities // []) | length end' snyk_result.json || echo "0") + fi + SNYK_RES="PASS" - grep -i -q "vulnerabilities found" snyk_result.txt && SNYK_RES="WARN" + [ "$SNYK_VULNS_COUNT" -gt 0 ] && SNYK_RES="WARN" + [ "$SNYK_VULNS_COUNT" -eq 0 ] && grep -i -q "vulnerabilities found" snyk_result.txt && SNYK_RES="WARN" - # 核心等级评估 OVERALL_GRADE="![Grade A+](https://img.shields.io/badge/Security_Grade-A%2B-success?style=for-the-badge)" if [ "$GITLEAKS_RES" == "FAIL" ]; then OVERALL_GRADE="![Grade D](https://img.shields.io/badge/Security_Grade-D-red?style=for-the-badge)" @@ -102,24 +144,23 @@ jobs: OVERALL_GRADE="![Grade B](https://img.shields.io/badge/Security_Grade-B-orange?style=for-the-badge)" fi - # 构建报告 { - echo "# 🛡️ 安全审计与透明度报告 | Security Audit Report" + echo "# 🛡️ 安全审计与透明度报告" echo "" echo " $OVERALL_GRADE" echo "" echo "> [!NOTE]" echo "> 本报告由 **GitHub Actions** 自动生成。为了确保数据主权的绝对透明,我们将所有核心模块的安全扫描结果实时公开。" echo "" - echo "| 📅 审计时间 / Audit Time | 📝 运行编号 / Run ID | 🛠️ 环境 / Environment |" + echo "| 📅 审计时间 | 📝 运行编号 | 🛠️ 环境 |" echo "| :--- | :--- | :--- |" echo "| \`$AUDIT_TIME\` | [#${{ github.run_id }}]($RUN_URL) | \`GitHub CI/CD\` |" echo "" echo "---" echo "" - echo "## 📉 实时安全看板 | Security Dashboard" + echo "## 📉 实时安全看板" echo "" - echo "| 项目 / Scope | 状态 / Status | 风险等级 / Risk |" + echo "| 项目 | 状态 | 风险等级 |" echo "| :--- | :--- | :--- |" echo "| **敏感泄露检查 (Gitleaks)** | $([ "$GITLEAKS_RES" == "PASS" ] && echo "$BADGE_PASS" || echo "$BADGE_FAIL") | \`NONE\` |" echo "| **依赖项漏洞扫描 (Snyk)** | $([ "$SNYK_RES" == "PASS" ] && echo "$BADGE_PASS" || echo "$BADGE_WARN") | \`LOW/MED\` |" @@ -127,70 +168,117 @@ jobs: echo "" echo "---" echo "" - echo "## 🔍 扫描覆盖范围 | Scan Coverage" + echo "## 🔍 扫描覆盖范围" echo "" - echo "| 模块 / Module | 已审计文件数 / Files Audited | 提取状态 / Extraction |" + echo "| 模块 | 已审计文件数 | 提取状态 |" echo "| :--- | :---: | :---: |" - echo "| **前端代码 (Frontend/JS)** | \`$FE_FILES\` | ✨ **100%** |" - echo "| **后端代码 (Backend/TS)** | \`$BE_FILES\` | ✨ **100%** |" - echo "| **流水线 (Workflows)** | \`$WF_FILES\` | ✨ **100%** |" + echo "| **GitHub Actions** | \`$WF_FILES\` | ✨ **100%** |" + echo "| **JavaScript** | \`$JS_FILES\` | ✨ **100%** |" + echo "| **TypeScript** | \`$TS_FILES\` | ✨ **100%** |" echo "" echo "---" echo "" - echo "## 🔍 详细发现 | Detailed Findings" + echo "## 🔍 详细发现" echo "" - echo "### 📦 依赖安全性 (Dependencies)" - echo "
" - echo "点击展开详情 / Click to expand details" + + # 🚀 UX优化:全新的 Snyk 漏洞表格化展示 + echo "### 📦 依赖安全性" + echo "
" + echo "点击查看依赖漏洞详情" echo "" - echo "\`\`\`text" - if [ -f snyk_result.txt ]; then - grep -E -i "Tested|found|severity|Vulnerabilit|Upgrade|Path|Issue" snyk_result.txt + + if [ "$SNYK_VULNS_COUNT" -gt 0 ]; then + echo "> ⚠️ 共发现 **$SNYK_VULNS_COUNT** 个第三方依赖漏洞。" + echo "" + echo "| 依赖包 | 严重程度 | 漏洞描述 | 修复方案 |" + echo "| :--- | :---: | :--- | :--- |" + + jq -r ' + def levelIcon: if . == "critical" then "🔴" elif . == "high" then "🟠" elif . == "medium" then "🟡" else "🔵" end; + (if type=="array" then .[] else . end) | .vulnerabilities[]? | + "| `\(.packageName)@\(.version)` | \(levelIcon) \(.severity) | [\(.title)](\(.url)) | \(.fixedIn // ["无 / N/A"] | join(", ") | if . == "无 / N/A" then . else "Upgrade to `\(.)`" end) |" + ' snyk_result.json | sort -u >> $REPORT_FILE + + elif [ "$SNYK_RES" == "WARN" ]; then + # Fallback 显示 txt 结果 + echo "\`\`\`text" + grep -E -i "Tested|found|severity|Vulnerabilit|Upgrade|Path|Issue" snyk_result.txt || echo "Please check snyk_result.txt for details." + echo "\`\`\`" else - echo "No Snyk logs found." + echo "> ✅ **SAFE**: 未发现第三方依赖包包含已知安全漏洞。" fi - echo "\`\`\`" + echo "
" echo "" - echo "### 🔑 凭证泄露检查 (Secret Leaks)" + + echo "### 🔑 凭证泄露检查" echo "该项探测代码历史记录中是否存在硬编码的 API Key、密码或其他敏感令牌。" echo "
" - echo "扫描结论 / Audit Verdict" + echo "扫描结论" echo "" echo "> ✅ **SAFE**: 未发现代码中包含硬编码的敏感凭证。" - echo "> ✅ **SAFE**: No secrets detected in the audit scope." echo "" echo "---" echo "🔍 *审计范围:全量代码变更与 Git 历史 (Gitleaks Full Scan)*" echo "
" echo "" - echo "### 🔍 静态代码安全分析细节 (CodeQL Technical Details)" + echo "### 📂 审计文件列表" echo "
" - echo "点击展开 100+ 项已检查规则 / Click to expand rules" + echo "GitHub Actions ($WF_FILES 份流水线文件)" + echo "" + echo "$WF_TABLE" + echo "" + echo "
" + echo "" + echo "
" + echo "JavaScript ($JS_FILES 个前端文件)" + echo "" + echo "$JS_TABLE" + echo "" + echo "
" + echo "" + echo "
" + echo "TypeScript ($TS_FILES 个后端文件)" + echo "" + echo "$TS_TABLE" + echo "" + echo "
" + echo "" + echo "### 🔍 CodeQL静态代码安全分析细节" + echo "
" + echo "点击查看发现的代码问题" echo "" } > $REPORT_FILE - if [ -n "$SARIF_FILE" ]; then - ALERTS_COUNT=$(jq '[.runs[].results[]?] | length' "$SARIF_FILE") - RULES_COUNT=$(jq '[.runs[].tool.driver.rules[]?, .runs[].tool.extensions[].rules[]?] | unique_by(.id) | length' "$SARIF_FILE") + if [ -n "$CODEQL_SARIFS" ]; then + ALERTS_COUNT=$(jq -s '[.[].runs[].results[]?] | length' $CODEQL_SARIFS) + ERRORS=$(jq -s '[.[].runs[].results[]? | select((.level // "warning") == "error")] | length' $CODEQL_SARIFS) + WARNINGS=$(jq -s '[.[].runs[].results[]? | select((.level // "warning") == "warning")] | length' $CODEQL_SARIFS) + NOTES=$(jq -s '[.[].runs[].results[]? | select((.level // "warning") == "note")] | length' $CODEQL_SARIFS) + RULES_COUNT=$(jq -s '[.[].runs[].tool.driver.rules[]?, .[].runs[].tool.extensions[].rules[]?] | unique_by(.id) | length' $CODEQL_SARIFS) { - echo "#### 审计摘要 / Audit Summary" - echo "- **总扫描规则 / Total Rules Checked**: \`$RULES_COUNT\`" - echo "- **风险警告 / Security Alerts Found**: \`$ALERTS_COUNT\`" + echo "#### 审计摘要" + echo "- **检查规则总数**: \`$RULES_COUNT\` 项" + echo "- **发现的问题总数**: \`$ALERTS_COUNT\` 处" + echo "- 🔴 高危 (Error): \`$ERRORS\` | 🟠 中危 (Warning): \`$WARNINGS\` | 🔵 建议 (Note): \`$NOTES\` " echo "" - echo "| 规则 ID / Rule ID | 描述 / Description |" - echo "| :--- | :--- |" + echo "| 规则 ID | 严重程度 | 触发位置 | 问题描述 |" + echo "| :--- | :---: | :--- | :--- |" } >> $REPORT_FILE - jq -r ' - [.runs[].tool.driver.rules[]?, .runs[].tool.extensions[].rules[]?] - | unique_by(.id) - | .[] - | "| [\(.id)](https://codeql.github.com/codeql-query-help/javascript/\(.id | gsub("/";"-"))/) | \(.shortDescription.text // .description.text // "No description") |" - ' "$SARIF_FILE" >> $REPORT_FILE + if [ "$ALERTS_COUNT" -gt 0 ]; then + jq -s -r ' + def lang: .ruleId | split("/")[0] | if . == "js" then "javascript" elif . == "ts" then "typescript" else . end; + def levelIcon: (.level // "warning") | if . == "error" then "🔴" elif . == "warning" then "🟠" else "🔵" end; + .[].runs[].results[]? | + "| [\(.ruleId)](https://codeql.github.com/codeql-query-help/\(lang)/\(.ruleId | gsub("/";"-"))/) | \(levelIcon) \(.level // "warning") | `\(.locations[0].physicalLocation.artifactLocation.uri // "Project-level"):\(.locations[0].physicalLocation.region.startLine // "-")` | \(.message.text // "No description") |" + ' $CODEQL_SARIFS >> $REPORT_FILE + else + echo "| - | ✅ 完美 | - | 未检测到任何代码安全与质量问题 |" >> $REPORT_FILE + fi else - echo "CodeQL results not available." >> $REPORT_FILE + echo "> ⚠️ CodeQL SARIF report not found." >> $REPORT_FILE fi { @@ -200,13 +288,10 @@ jobs: echo "" echo "## ⚠️ 行动指引 | Steps to Take" echo "" - echo "如果您看到状态显示为 **FAIL (失败)** 或 **CRITICAL (致命)**:" - echo "1. **开发者**:请立即检查 [GitHub Actions 日志]($RUN_URL) 获取完整路径。" - echo "2. **贡献者**:请提交 Pull Request 修复对应依赖项或通过环境变量覆盖密钥。" - echo "" - echo "If you see a **FAIL** or **CRITICAL** status:" - echo "1. **Developers**: Check [GitHub Actions Logs]($RUN_URL) for full trace." - echo "2. **Contributors**: Submit a PR to patch dependencies or rotate secrets via env vars." + echo "如果您看到状态显示为 **FAIL** 或发现严重代码问题:" + echo "1. **开发者**:请立即通过上方表格的 **触发位置** 定位到具体文件和行号。" + echo "2. **修复**:参考静态分析细节中的官方文档链接了解漏洞详情并提交修复。" + echo "3. **追溯**:完整的 \`.sarif\` 原始数据已附在本分支,可下载后导入本地 IDE 插件进行分析。" echo "" echo "---" echo "💡 *Powered by Gitleaks, Snyk & CodeQL. Transparency is our commitment.*" @@ -214,24 +299,48 @@ jobs: cat $REPORT_FILE >> $GITHUB_STEP_SUMMARY - # 5. 将报告推送到独立的 security-audit 分支 (Orphan Branch) + # 5. 将报告、重命名的 SARIF 文件及 Snyk 结果推送到孤儿分支 - name: Push to Audit Branch if: github.event_name != 'pull_request' run: | - # 创建临时文件夹 mkdir audit_temp - cp SECURITY.md audit_temp/ + + cp README.md audit_temp/ + + if [ -f "snyk_result.txt" ]; then + cp snyk_result.txt audit_temp/ + fi + + # 🚀 UX优化:同步保存 Snyk JSON 文件到审计分支 + if [ -f "snyk_result.json" ]; then + cp snyk_result.json audit_temp/ + fi + + GITLEAKS_FILE=$(find . -name "results.sarif" | head -n 1) + if [ -n "$GITLEAKS_FILE" ]; then + cp "$GITLEAKS_FILE" audit_temp/Gitleaks_results.sarif + fi + + for f in sarif-results/*.sarif; do + if [ -f "$f" ]; then + fname=$(basename "$f") + cp "$f" "audit_temp/CodeQL_${fname}" + fi + done cd audit_temp git init git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - # 创建孤儿分支 git checkout --orphan security-audit - git add SECURITY.md - git commit -m "chore: update security audit report [skip ci]" - # 强制推送到远程的 security-audit 分支 + git add README + [ -f "snyk_result.txt" ] && git add snyk_result.txt + [ -f "snyk_result.json" ] && git add snyk_result.json + ls *.sarif >/dev/null 2>&1 && git add *.sarif + + git commit -m "chore: archive security report, snyk logs and sarif data [skip ci]" + git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} git push -f origin security-audit \ No newline at end of file diff --git a/README.md b/README.md index 77767af..141b669 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ docker run -d --name 2fauth-worker \ 4. **端到端隔离**:前端生成的敏感操作逻辑经过严格过滤,严禁任何 XSS (跨站脚本) 攻击。 5. **隐私第一**:本项目不包含任何追踪代码、分析工具或第三方统计插件。你的数据只属于你。 -> 📊 [**点击查看由 GitHub Actions 自动生成的实时安全审计报告**](https://github.com/nap0o/2fauth-worker/blob/security-audit/SECURITY.md) +> 📊 [**点击查看由 GitHub Actions 自动生成的实时安全审计报告**](https://github.com/nap0o/2fauth-worker/blob/security-audit/README.md) --- diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..eec6156 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,284 @@ +# 2FAuth Worker + +English | [中文](README.md) + +**High-security, lightweight Two-Factor Authentication (2FA/TOTP) management system supporting both Cloudflare Serverless and Docker private deployment.** + +[![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?style=flat&logo=cloudflare)](https://workers.cloudflare.com/) +[![Docker](https://img.shields.io/badge/Docker-Supported-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/r/nap0o/2fauth-worker) +[![GitHub Actions](https://img.shields.io/badge/GitHub-Actions-181717?style=flat&logo=github)](https://github.com/nap0o/2fauth-worker/actions) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-red.svg)](https://www.gnu.org/licenses/agpl-3.0) + +[**Click here for LIVE DEMO**](https://2fa.nezha.pp.ua) + +> 💡 **Tip**: The demo site has open enrollment enabled; data can be modified or deleted. For official deployment, please ensure you configure the allowed users whitelist. + +--- + +## 🌟 Why choose 2FAuth Worker? + +Whether you're leveraging Cloudflare's free tier or deploying on your own NAS/mini-PC, 2FAuth Worker provides the **most secure** and convenient 2FA management experience. + +### 🕊️ True "Dual-Mode" Deployment +* **Cloudflare Lovers**: One-click deployment to Cloudflare Workers – zero cost, zero maintenance, and global acceleration. +* **Private Cloud Geeks**: Supports Docker deployment. Keep your data locked on your own hardware; it even works in offline local networks. + +### 📱 Smooth PWA Experience +* **Immersive Experience**: Install it to your desktop or mobile home screen for a distraction-free environment without browser bars. +* **Offline Ready**: Thanks to advanced Service Worker caching, it opens instantly and generates accurate codes even in extreme environments without internet (like basements or airplanes). + +### 🛡️ "Smart Shield" Security (Health Shield) +* Built-in intelligent health checks. If the system detects critical missing keys or improper configurations, it will immediately "shut down" access to prevent your data from being exposed in an insecure environment. + +### 📦 The Ultimate "Undo Button" (Multi-channel Backup) +* Supports automatic backups via WebDAV, S3 cloud storage, or Telegram bots. If you lose your phone, reinstall your OS, or even lose your Cloudflare account, you can recover everything instantly from your backup. + +--- + +## 📊 Feature Comparison: Why Choose 2FAuth Worker? + +| Feature | **2FAuth Worker** (This Project) | Google / MS Auth | Authy | 2FAS / Aegis | 1Password / Bitwarden | +| :--- | :--- | :--- | :--- | :--- | :--- | +| Data Ownership | ✅ Full Control (Private/NAS) | ❌ Tied to Big Tech | ❌ Vendor-locked | ✅ Local/Private Cloud | ⚠️ Third-party Hosted | +| Cross-Platform | ✅ Web+PWA (Universal) | ⚠️ Mobile App only | ❌ Desktop App closed | ⚠️ Mobile App only | ✅ Full Platform Support | +| Recovery | ✅ Auto Backups (TG/S3) | ⚠️ Manually Exporting | ✅ Cloud Sync | ⚠️ Manage Files Manually | ✅ Cloud Sync | +| Privacy | ✅ Anonymous, No Tracking | ⚠️ Tied to Big Tech ID | ⚠️ Forced Phone Bind | ✅ No Tracking | ✅ Safe but high profile | +| Offline Support | ✅ Yes (PWA Cache) | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | +| Cost | ✅ $0 (CF Serverless) | ✅ Free | ✅ Free | ✅ Free | 💰 Subscription based | +| Vendor Risk | ✅ Source in Hand | ⚠️ Forced migration risks | 🚨 Feature shutdowns (PC) | ✅ Open-source control | ⚠️ Subscription/Closed | + +> 🎯 **Summary**: **2FAuth Worker** combines the multi-device convenience of 1Password with the privacy of open-source apps, and the zero cost of Serverless. **Automatic Telegram backup** completely eliminates "data loss anxiety" common in both web and mobile apps, making security truly accessible. + +--- + +## 🚀 Deployment Guide (Choose One) + +### Option A: Cloudflare Worker (Fastest, Recommended for Beginners) + +The most hassle-free way. No server required; Cloudflare hosts it for you for free. + +#### 1. One-click Auto Deploy +[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/nap0o/2fauth-worker) + +#### 2. Notes & Authorization +* During the deployment wizard, you'll need to authorize Cloudflare to read this repository (it will automatically create a private fork in your account). + +
+Click to view: Authorization Guide Diagrams +
+ + +
+ +#### 3. Add the following Variables and Secrets + * `ENCRYPTION_KEY`: A random key with at least 32 characters. + * `JWT_SECRET`: A random JWT secret with at least 32 characters. + * `OAUTH_ALLOWED_USERS`: your_email@example.com + * `OAUTH_GITHUB_CLIENT_ID`: Your Client ID + * `OAUTH_GITHUB_CLIENT_SECRET`: Your Client Secret + * `OAUTH_GITHUB_REDIRECT_URI`: Your Callback URL + +
+Click to view: Add Variables and Secrets Diagram + +
+ +--- + +### Option B: Docker (For NAS / Private Server Users) + +If you want absolute control over your data or need to run it in a local network. + +#### 1. Using Docker Compose (Recommended) +Create `docker-compose.yml` on your server: +```yaml +services: + 2fauth: + image: nap0o/2fauth-worker:latest + container_name: 2fauth-worker + ports: + - "3000:3000" + volumes: + - ./data:/app/data + environment: + - ENCRYPTION_KEY=Your_32_Character_Random_Key + - JWT_SECRET=Your_Random_JWT_Secret + - OAUTH_ALLOWED_USERS=your_email@example.com + # Configure at least one provider (GitHub example) + - OAUTH_GITHUB_CLIENT_ID=your_id + - OAUTH_GITHUB_CLIENT_SECRET=your_secret + - OAUTH_GITHUB_REDIRECT_URI=https://your-domain.com/oauth/callback + restart: unless-stopped +``` +Run `docker compose up -d`. + +#### 2. Using Docker Run +```bash +docker run -d --name 2fauth-worker \ + -p 3000:3000 \ + -v $(pwd)/data:/app/data \ + -e ENCRYPTION_KEY=Your_32_Character_Random_Key \ + -e JWT_SECRET=Your_32_Character_Random_JWT_Secret \ + -e OAUTH_ALLOWED_USERS=your_email@example.com \ + -e OAUTH_GITHUB_CLIENT_ID= \ + -e OAUTH_GITHUB_CLIENT_SECRET= \ + -e OAUTH_GITHUB_CLIENT_REDIRECT_URI= \ + nap0o/2fauth-worker:latest +``` + +--- + +### Option C: GitHub Actions (For Advanced Users) + +Choose this for continuous deployment or more precise database management. + +#### 1. Prerequisites +* Obtain a **Cloudflare API Token** (requires `Edit Cloudflare Workers` permissions). +
+Click to view: Steps to obtain Cloudflare Worker Token + +1. Log in to Cloudflare Dashboard +2. [Go to API Tokens](https://dash.cloudflare.com/profile/api-tokens) +3. Click "Create Token" +4. Choose "Edit Cloudflare Workers" template +5. Configure "Account Resources" and "Zone Resources" +6. Click "Continue to summary" and "Create Token" +7. Copy the generated token + +image
+ +image +
+ +* Create a D1 database named `2fauth-db` in `Storage & Databases` -> `D1` and record its **Database ID**. + +#### 2. Repository Configuration +1. **Fork** this repository. +2. Go to `Settings` -> `Secrets and variables` -> `Actions`. +3. Add the following secrets: + * `CLOUDFLARE_ACCOUNT_ID`: Your CF Account ID. + * `CLOUDFLARE_API_TOKEN`: Your API Token. + * `CLOUDFLARE_D1_DATABASE_ID`: Your D1 Database ID. + * `CLOUDFLARE_D1_DATABASE_NAME`: `2fauth-db`. + * `ENCRYPTION_KEY`: A 32+ character random key. + * `JWT_SECRET`: A 32+ character random JWT secret. + * `OAUTH_ALLOWED_USERS`: your_email@example.com + * `OAUTH_GITHUB_CLIENT_ID`: your_id + * `OAUTH_GITHUB_CLIENT_SECRET`: your_secret + * `OAUTH_GITHUB_REDIRECT_URI`: your_callback_url + +
+Click to view: Secrets Configuration Example +Secrets Config +
+ +#### 3. Trigger Deployment +* Go to the `Actions` page, manually run the `Deploy to Cloudflare Workers` workflow, or push code to the `main` branch. + +
+Click to view: Manual Deployment Example +Manual Trigger +
+ +--- + +## 🧩 Environment Variables Configuration + +Regardless of the deployment method, these parameters are critical: + +| Variable | Function | Recommendation | +| :--- | :--- | :--- | +| `ENCRYPTION_KEY` | **Core**: Database encryption key | **DO NOT CHANGE** after setup! Requirement: 32+ characters. | +| `JWT_SECRET` | Auth Token Secret | Requirement: 32+ characters. | +| `OAUTH_ALLOWED_USERS` | **Whitelist**: Access Control | Emails or Telegram IDs, separated by commas. | + +### Variable Mapping for Different Providers: + +| Provider | Client ID Variable | Client Secret Variable | Redirect URI (Example: `https://xxx.dev/oauth/callback`) | +| :--- | :--- | :--- | :--- | +| **GitHub** | `OAUTH_GITHUB_CLIENT_ID` | `OAUTH_GITHUB_CLIENT_SECRET` | `OAUTH_GITHUB_REDIRECT_URI` | +| **Telegram** | `OAUTH_TELEGRAM_BOT_NAME` | `OAUTH_TELEGRAM_BOT_TOKEN` | *Not required for Telegram* | +| **Google** | `OAUTH_GOOGLE_CLIENT_ID` | `OAUTH_GOOGLE_CLIENT_SECRET" | `OAUTH_GOOGLE_REDIRECT_URI` | +| **Cloudflare Access** | `OAUTH_CLOUDFLARE_CLIENT_ID` | `OAUTH_CLOUDFLARE_CLIENT_SECRET` | `OAUTH_CLOUDFLARE_REDIRECT_URI` (requires `OAUTH_CLOUDFLARE_ORG_DOMAIN`) | +| **Gitee** | `OAUTH_GITEE_CLIENT_ID` | `OAUTH_GITEE_CLIENT_SECRET" | `OAUTH_GITEE_REDIRECT_URI` | +| **NodeLoc** | `OAUTH_NODELOC_CLIENT_ID` | `OAUTH_NODELOC_CLIENT_SECRET" | `OAUTH_NODELOC_REDIRECT_URI` | + +--- + +#### How to configure GitHub OAuth Login (Recommended)? +1. Visit GitHub `Settings` -> `Developer Settings` -> `OAuth Apps` -> **New OAuth App**. +2. **Authorization callback URL** must be: `https://your-domain.com/oauth/callback`. +3. Fill `Client ID`, `Client Secret` and `OAUTH_GITHUB_REDIRECT_URI` into environment variables. + +
+Click to view: Github OAuth Configuration Diagram +Github OAuth Config +
+ +#### How to configure Telegram Login? +1. Search and add **[@BotFather](https://t.me/BotFather)** on Telegram, following prompts to create a bot. +2. Record the **Token** (`OAUTH_TELEGRAM_BOT_TOKEN`) and **Username** (`OAUTH_TELEGRAM_BOT_NAME`). +3. You MUST send the `/setdomain` command to @BotFather, select your bot, and enter your **application domain** (without `https://`). +4. Since this is a Serverless architecture, you need to manually register the Webhook. Replace `` and `` in the link below and visit it once in your browser: + `https://api.telegram.org/bot/setWebhook?url=https:///api/telegram/webhook` +5. Fill `OAUTH_TELEGRAM_BOT_TOKEN`, `OAUTH_TELEGRAM_BOT_NAME` and `OAUTH_TELEGRAM_REDIRECT_URI` into environment variables. + +#### How to configure Cloudflare Access Login? +1. Enter **Cloudflare Zero Trust Dashboard** -> **Access** -> **Applications**. +2. Create a **SaaS** application (Select "SaaS"). +3. Configure **Application**: + - **Name**: `Anything, e.g., 2fauth` + - **Authentication Protocol**: `OIDC` + - **Redirect URL**: `https://your-domain.com/oauth/callback` +4. Copy and save `Client ID` and `Client Secret`. +5. Access Policy -> Click create new policy -> Add policy: + - **Policy**: `Allow` + - **Selector**: `Everyone` +6. No additional configuration needed, click next through to completion. +7. Fill `OAUTH_CLOUDFLARE_CLIENT_ID`, `OAUTH_CLOUDFLARE_CLIENT_SECRET`, `OAUTH_CLOUDFLARE_REDIRECT_URI` and `OAUTH_CLOUDFLARE_ORG_DOMAIN` into environment variables. + +
+Click to view: Cloudflare Access OAuth Configuration Diagram + + +
+ +--- + +## 🔒 Security Auditing + +To protect your account security, 2FAuth Worker implements a "Multi-Lock" strategy: + +1. **At-Rest Encryption**: All TOTP seeds stored in the database are encrypted using AES. Even if the database file is leaked, unauthorized parties cannot obtain raw keys without the `ENCRYPTION_KEY`. +2. **Whitelist-Only Access**: Traditional registration is disabled. Only accounts in the `OAUTH_ALLOWED_USERS` list can access the system. +3. **Smart Shield Persistence (Health Shield)**: The system continuously monitors the runtime environment. If any anomalies are detected, it enters protection mode and refuses to generate verification codes. +4. **End-to-End Isolation**: Sensitive frontend logic is strictly filtered to prevent XSS (Cross-Site Scripting) attacks. +5. **Privacy First**: This project contains no tracking code, analytics tools, or third-party cookies. Your data is yours alone. + +> 📊 [**Click to view the real-time security audit report generated by GitHub Actions**](https://github.com/nap0o/2fauth-worker/blob/security-audit/README.md) + +--- + +## 🛠️ Local Development + +```bash +# 1. Clone and install dependencies +git clone https://github.com/nap0o/2fauth-worker.git +cd 2fauth-worker +npm install + +# 2. Configure environment +cp example.dev.vars .dev.vars + +# 3. Initialize local SQLite Sandbox +npx wrangler d1 execute 2fauth-db-dev --local --env dev --file=backend/schema.sql + +# 4. Start dev server +npm run dev +``` + +--- + +## 📄 License +This project is open-sourced under the [GNU AGPL v3](LICENSE) license. Since this software involves 2FA security and network services, we maintain open-source fairness: if you run a modified version of this project on a server and provide services to the public, you must open your source code to the users. + diff --git a/backend/src/app/index.ts b/backend/src/app/index.ts index 6342bf4..a30269f 100644 --- a/backend/src/app/index.ts +++ b/backend/src/app/index.ts @@ -7,7 +7,7 @@ import { EnvBindings, CSP_POLICY } from '@/app/config'; // 稍后我们会在这里引入拆分好的路由模块 import authRoutes from '@/features/auth/authRoutes'; import vaultRoutes from '@/features/vault/vaultRoutes'; -import backupRoutes, { handleScheduledBackup } from '@/features/backup/backupRoutes'; +import backupRoutes from '@/features/backup/backupRoutes'; import telegramRoutes from '@/features/telegram/telegramRoutes'; import toolsRoutes from '@/features/tools/toolsRoutes'; import healthRoutes from '@/features/health/healthRoutes'; diff --git a/backend/src/features/backup/backupService.ts b/backend/src/features/backup/backupService.ts index d694d15..ccfc2ce 100644 --- a/backend/src/features/backup/backupService.ts +++ b/backend/src/features/backup/backupService.ts @@ -1,4 +1,4 @@ -import { eq, desc } from 'drizzle-orm'; +import { eq } from 'drizzle-orm'; import { EnvBindings, AppError } from '@/app/config'; import { BackupRepository } from '@/shared/db/repositories/backupRepository'; import { encryptData, decryptData, encryptBackupFile } from '@/shared/utils/crypto'; diff --git a/backend/src/features/health/healthRoutes.ts b/backend/src/features/health/healthRoutes.ts index f3d8f40..89e23a4 100644 --- a/backend/src/features/health/healthRoutes.ts +++ b/backend/src/features/health/healthRoutes.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import { EnvBindings, AppError } from '@/app/config'; +import { EnvBindings } from '@/app/config'; import { runHealthCheck } from '@/shared/utils/health'; const health = new Hono<{ Bindings: EnvBindings }>(); diff --git a/backend/src/features/vault/vaultRoutes.ts b/backend/src/features/vault/vaultRoutes.ts index 80c7804..86d1e55 100644 --- a/backend/src/features/vault/vaultRoutes.ts +++ b/backend/src/features/vault/vaultRoutes.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import { EnvBindings, AppError } from '@/app/config'; +import { EnvBindings } from '@/app/config'; import { authMiddleware } from '@/shared/middleware/auth'; import { VaultService } from '@/features/vault/vaultService'; import { VaultRepository } from '@/shared/db/repositories/vaultRepository'; @@ -18,8 +18,6 @@ vault.use('/*', authMiddleware); // 获取所有账户 (分页+搜索) vault.get('/', async (c) => { - const user = c.get('user'); - // 解析查询参数 const page = parseInt(c.req.query('page') || '1', 10); const limit = parseInt(c.req.query('limit') || '12', 10); @@ -51,7 +49,6 @@ vault.post('/', async (c) => { // 更新账户 vault.put('/:id', async (c) => { - const user = c.get('user'); const id = c.req.param('id'); const data = await c.req.json(); const service = getService(c); @@ -61,7 +58,6 @@ vault.put('/:id', async (c) => { // 删除账户 vault.delete('/:id', async (c) => { - const user = c.get('user'); const id = c.req.param('id'); const service = getService(c); await service.deleteAccount(id); @@ -70,7 +66,6 @@ vault.delete('/:id', async (c) => { // 批量删除账户 vault.post('/batch-delete', async (c) => { - const user = c.get('user'); const { ids } = await c.req.json(); if (!Array.isArray(ids) || ids.length === 0) { return c.json({ success: false, error: 'ids must be a non-empty array' }, 400); @@ -82,7 +77,6 @@ vault.post('/batch-delete', async (c) => { // 导出账户 vault.post('/export', async (c) => { - const user = c.get('user'); const service = getService(c); const { type, password } = await c.req.json(); diff --git a/backend/src/shared/db/repositories/vaultRepository.ts b/backend/src/shared/db/repositories/vaultRepository.ts index 0caaeb4..c7bd87b 100644 --- a/backend/src/shared/db/repositories/vaultRepository.ts +++ b/backend/src/shared/db/repositories/vaultRepository.ts @@ -1,4 +1,4 @@ -import { eq, inArray, desc, like, or, and, sql } from 'drizzle-orm'; +import { eq, inArray, desc, like, or, sql } from 'drizzle-orm'; import { vault, type VaultItem, type NewVaultItem } from '@/shared/db/schema'; export class VaultRepository { diff --git a/frontend/src/app/app.vue b/frontend/src/app/app.vue index 5a86f06..c9a4dde 100644 --- a/frontend/src/app/app.vue +++ b/frontend/src/app/app.vue @@ -31,22 +31,23 @@ const checkMobile = () => { layoutStore.isMobile = window.innerWidth < 768 } -const isOffline = ref(false) -const setOnline = () => { isOffline.value = false } -const setOffline = () => { isOffline.value = true } +const userClosedOfflineBanner = ref(false) +const showOfflineBanner = computed(() => layoutStore.isOffline && !userClosedOfflineBanner.value) + +watch(() => layoutStore.isOffline, (offline) => { + if (offline) { + userClosedOfflineBanner.value = false + } +}) onMounted(() => { checkMobile() - isOffline.value = !navigator.onLine + layoutStore.initNetworkStatus() window.addEventListener('resize', checkMobile) - window.addEventListener('online', setOnline) - window.addEventListener('offline', setOffline) }) onBeforeUnmount(() => { window.removeEventListener('resize', checkMobile) - window.removeEventListener('online', setOnline) - window.removeEventListener('offline', setOffline) }) @@ -54,9 +55,16 @@ onBeforeUnmount(() => {
-
- {{ $t('common.offline_mode') }} -
+ diff --git a/frontend/src/app/styles/main.css b/frontend/src/app/styles/main.css index 8ccec85..384ad29 100644 --- a/frontend/src/app/styles/main.css +++ b/frontend/src/app/styles/main.css @@ -1,1232 +1,30 @@ -/* ========================================= - 0. 强制同步修复 (Force Sync) - ========================================= */ -/* 核心修复:强制禁用所有元素的过渡动画,确保深浅色切换瞬间完成,消除不同步感 */ -*, -*::before, -*::after { - transition: none !important; -} - -/* ========================================= - 0.1 全局语义化变量 (Design Tokens) - ========================================= */ -:root { - /* 玻璃拟态卡片 (登录/加载) */ - --glass-card-bg: rgba(255, 255, 255, 0.95); - --glass-card-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); - - /* GitHub 按钮 */ - --github-btn-bg: #24292e; - --github-btn-border: #24292e; - - /* 状态色块 */ - --success-tip-bg: var(--el-color-success-light-9); - --vault-selected-bg: var(--el-color-primary-light-9); -} - -/* ========================================= - 1. 全局基础样式 (Global Reset & Base) - ========================================= */ -html, -body, -#app { - height: 100%; - margin: 0; - padding: 0; - overflow: hidden; - background-color: var(--el-bg-color-page); - color: var(--el-text-color-primary); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -} - -/* 移动端特殊适配:释放根部滚动以支持原生下拉刷新 */ -@media (max-width: 768px) { - - html, - body, - #app { - height: auto; - min-height: 100%; - overflow: visible; - } -} - -a { - color: inherit; - text-decoration: none; - transition: color 0.2s; -} - -a:hover { - color: #409EFF; -} - -/* 滚动条优化 */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--el-border-color); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--el-text-color-secondary); -} - -/* 通用组件样式 */ -.tab-card-wrapper { - background: var(--el-bg-color); - padding: 40px; - border-radius: 12px; - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); - min-height: 400px; -} - -/* ========================================= - 2. 布局组件 (Layout) - ========================================= */ -/* App.vue */ -.app-container { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} - -@media (max-width: 768px) { - .app-container { - height: auto; - min-height: 100vh; - overflow: visible; - } -} - -main { - flex: 1; - overflow: hidden; - display: flex; - flex-direction: column; -} - -@media (max-width: 768px) { - main { - overflow: visible; - } -} - -/* TheHeader.vue */ -.header { - background: var(--el-bg-color); - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 20px; - height: 60px; - flex-shrink: 0; - z-index: 100; - position: sticky; - top: 0; - border-bottom: 1px solid var(--el-border-color-light); -} - -.logo, -.user-actions, -.user-profile { - display: flex; - align-items: center; -} - -.logo h2 { - margin: 0; - color: var(--el-text-color-primary); - font-size: 20px; -} - -.user-actions { - gap: 20px; -} - -.user-profile { - gap: 10px; - background: var(--el-fill-color-light); - padding: 4px 15px 4px 4px; - border-radius: 20px; -} - -.username { - font-weight: 600; - color: var(--el-text-color-regular); - font-size: 14px; -} - -.menu-icon { - color: var(--el-text-color-regular); -} - -@media (max-width: 768px) { - .header { - padding: 0 15px; - } - - .logo h2 { - font-size: 18px; - } -} - -/* TheFooter.vue */ -.global-footer { - width: 100%; - padding: 0.25rem 0; - background-color: var(--el-bg-color); - border-top: 1px solid var(--el-border-color-light); - text-align: center; - margin-top: auto; -} - -.footer-content { - color: var(--el-text-color-secondary); - font-size: 0.875rem; -} - -.divider { - margin: 0 0.5rem; - color: var(--el-border-color); -} - -/* Home.vue */ -.home-container { - height: 100%; - background-color: var(--el-bg-color-page); -} - -.main-body { - height: 100%; - display: flex; - overflow: hidden; -} - -@media (max-width: 768px) { - - .home-container, - .main-body { - height: auto; - min-height: 100vh; - overflow: visible; - } -} - -.left-aside { - background: var(--el-bg-color); - border-right: 1px solid var(--el-border-color-light); - display: flex; - flex-direction: column; - overflow-x: hidden; - overflow-y: auto; - white-space: nowrap; -} - -/* 视觉优化:隐藏侧边栏滚动条,保持界面清爽 */ -.left-aside::-webkit-scrollbar { - width: 0; - height: 0; -} - -.side-menu { - border-right: none; - padding-top: 10px; - flex: 1; -} - -.main-content { - width: 100%; - background-color: var(--el-bg-color-page); - /* 原为 auto,修改为保留滚动空间防止因高度变化产生的抖动,或直接将滚动条悬浮覆盖 */ - overflow-y: overlay; - scrollbar-gutter: stable; -} - -@media (max-width: 768px) { - .main-content { - overflow: visible !important; - overflow-y: visible !important; - overflow-x: visible !important; - scrollbar-gutter: auto; - } -} - -.sidebar-footer { - padding: 20px; - text-align: center; - display: flex; - justify-content: center; - gap: 10px; -} - -.sidebar-footer .el-button { - margin-left: 0; -} - -.sidebar-footer.is-collapsed { - flex-direction: column; - align-items: center; - padding: 10px 0; -} - -.drawer-content { - height: 100%; - display: flex; - flex-direction: column; -} - -/* ========================================= - 3. 页面/功能组件 (Pages & Features) - ========================================= */ - -/* Login.vue */ -.login-container { - margin: 25px 0px; - display: flex; - justify-content: center; - align-items: center; - flex: 1; - background: transparent; - position: relative; - -} - -.login-card { - width: 92%; - max-width: 400px; - border-radius: 16px; - box-shadow: var(--glass-card-shadow); - background: var(--glass-card-bg); - backdrop-filter: blur(10px); - border: none; -} - -.logo-container { - text-align: center; - margin-bottom: 30px; - padding-top: 10px; -} - -.logo-container h2 { - margin: 16px 0 8px; - color: var(--el-text-color-primary); - font-size: 24px; - font-weight: 600; -} - -.subtitle { - margin: 0; - color: var(--el-text-color-secondary); - font-size: 14px; -} - -.action-container { - display: flex; - flex-direction: column; - gap: 15px; - margin-bottom: 30px; -} - -.oauth-btn { - width: 100%; - height: 50px; - font-size: 16px; - border-radius: 8px; - margin-left: 0 !important; -} - -.oauth-btn:hover, -.oauth-btn:focus { - filter: brightness(1.1); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); -} - -.footer-tips { - margin-top: 10px; -} - -/* OAuthCallback.vue */ -.callback-container { - display: flex; - justify-content: center; - align-items: center; - flex: 1; - background: transparent; -} - -.loading-card { - width: 92%; - max-width: 400px; - border-radius: 16px; - background: var(--glass-card-bg); - backdrop-filter: blur(10px); -} - -/* VaultList.vue */ -.vault-card { - border-radius: 12px; - /* 全局规则已覆盖 transition,此处只需定义样式 */ - border: 1px solid var(--el-border-color-lighter); - cursor: pointer; - background-color: var(--el-bg-color); - height: 100%; - display: flex; - flex-direction: column; -} - -.vault-card:hover { - transform: translateY(-5px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1) !important; -} - -.vault-list-wrapper .card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -/* Flex 容器:限制宽度以支持内部文本截断 */ -.service-info { - display: flex; - align-items: center; - gap: 10px; - flex: 1; - /* 占据剩余空间 */ - min-width: 0; - /* 关键:允许 Flex 子项收缩 */ - margin-right: 10px; -} - -/* 标题截断逻辑 */ -.service-name { - margin: 0; - font-size: 18px; - color: var(--el-text-color-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* 让卡片内容撑开,底部对齐 */ -.vault-card .el-card__body { - flex: 1; - display: flex; - flex-direction: column; -} - -.vault-name { - color: var(--el-text-color-secondary); - font-size: 14px; - margin: 0 0 10px 24px; - word-break: break-all; -} - -.more-icon { - cursor: pointer; - color: var(--el-text-color-secondary); - padding: 5px; -} - -.more-icon:hover { - color: #409EFF; -} - -.vault-card.is-selected { - border: 1px solid #409EFF; - background-color: var(--vault-selected-bg); -} - -.batch-text { - color: var(--el-text-color-regular); - font-size: 14px; -} - -.code-display-area { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: auto; - padding-left: 24px; - padding-top: 15px; -} - -.code-left { - display: flex; - flex-direction: row; - align-items: baseline; - gap: 10px; -} - -.current-code { - font-size: 24px; - font-weight: bold; - color: #409EFF; - letter-spacing: 2px; - line-height: 1; - font-family: monospace; - white-space: nowrap; - overflow: hidden; - text-overflow: clip; -} - -.current-code[data-digits="8"] { - font-size: 20px; - letter-spacing: 0px; -} - -.next-code { - font-size: 16px; - color: var(--el-text-color-secondary); - font-family: monospace; -} - -.next-code[data-digits="8"] { - font-size: 14px; - letter-spacing: 0px; -} - -.timer-text { - font-size: 12px; - color: var(--el-text-color-regular); - font-weight: bold; - display: block; - text-align: center; -} - -.el-progress__text { - top: 0; - left: 0; - width: 100%; - height: 100%; - transform: none; - display: flex; - align-items: center; - justify-content: center; - min-width: 30px; -} - -/* VaultList - QR Dialog */ -.qr-container { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.qr-info { - margin-bottom: 15px; -} - -.qr-service { - margin: 0 0 5px 0; - font-size: 20px; - color: var(--el-text-color-primary); -} - -.qr-account { - margin: 0; - color: var(--el-text-color-secondary); - font-size: 14px; -} - -.qr-image-wrapper { - margin: 10px 0; - padding: 10px; - background: #fff; - border-radius: 8px; - border: 1px solid var(--el-border-color-lighter); -} - -.qr-code-img { - display: block; - width: 200px; - height: 200px; -} - -.qr-tip { - font-size: 12px; - color: var(--el-text-color-regular); - margin: 10px 0 20px 0; -} - -.secret-section { - width: 100%; - margin-bottom: 15px; -} - -.secret-box { - display: flex; - align-items: center; - justify-content: space-between; - background: var(--el-fill-color-light); - padding: 10px 15px; - border-radius: 6px; - border: 1px solid var(--el-border-color-light); -} - -.secret-text { - font-family: monospace; - font-size: 14px; - color: var(--el-text-color-regular); - letter-spacing: 1px; -} - -.secret-actions { - display: flex; - gap: 15px; -} - -.action-icon { - cursor: pointer; - font-size: 16px; - color: var(--el-text-color-secondary); - transition: color 0.2s; -} - -.action-icon:hover { - color: #409EFF; -} - -.uri-link-wrapper { - margin-top: 5px; -} - -/* BackupSettings.vue */ -.header-actions { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; -} - -.provider-card { - height: 100%; -} - -.backup-container .card-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.provider-info { - display: flex; - align-items: center; -} - -.provider-title { - display: flex; - align-items: center; - gap: 8px; - font-weight: bold; - flex-wrap: wrap; -} - -.status-text { - font-size: 13px; - color: var(--el-text-color-regular); - margin-bottom: 15px; -} - -.action-buttons { - display: flex; - gap: 10px; -} - -.form-tip { - font-size: 12px; - color: var(--el-text-color-secondary); - margin-top: 5px; -} - -.success-tip { - padding: 10px; - background-color: var(--success-tip-bg); - border-radius: 4px; - color: var(--el-color-success); - font-size: 13px; - display: flex; - align-items: center; - gap: 8px; - width: 100%; -} - -/* DataImport.vue & DataExport.vue */ -.import-options, -.export-options { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 15px; - margin-top: 20px; -} - -.import-options .el-button, -.export-options .el-button { - margin: 0; - justify-content: flex-start; - padding-left: 20px; - height: auto; - padding-top: 15px; - padding-bottom: 15px; -} - -@media (max-width: 768px) { - - .import-options, - .export-options { - grid-template-columns: 1fr; - } -} - -/* ========================================= - 4. UtilityTools 组件 (工具箱) - ========================================= */ -.tools-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 20px; -} - -.tools-header .back-btn { - padding: 0; - font-size: 16px; - margin-right: 5px; -} - -.tools-header h2 { - margin: 0; - font-size: 18px; - font-weight: 600; - color: var(--el-text-color-primary); -} - -.tools-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 20px; -} - -.tool-card { - cursor: pointer; - transition: all 0.3s; - border: 1px solid var(--el-border-color-lighter); -} - -.tool-card:hover { - transform: translateY(-4px); - box-shadow: var(--el-box-shadow-light); -} - -.tool-card-content { - display: flex; - align-items: center; -} - -.text-info { - flex: 1; -} - -.text-info h3 { - margin: 0 0 4px; - font-size: 16px; - font-weight: 600; - color: var(--el-text-color-primary); -} - -.text-info p { - margin: 0; - font-size: 12px; - color: var(--el-text-color-secondary); - line-height: 1.4; -} - -/* ========================================= - 5. UtilityTools 子组件详情页样式 - ========================================= */ - -/* 通用容器 */ -.tool-pane { - max-width: 600px; - margin: 0 auto; - padding: 10px 0; -} - -/* --- ToolTimeSync (时间校准) --- */ -.time-sync-container { - text-align: center; -} - -.clocks-wrapper { - display: flex; - gap: 20px; - margin-bottom: 30px; -} - -.clock-card { - flex: 1; - background: var(--el-bg-color-overlay); - border: 1px solid var(--el-border-color-light); - border-radius: 12px; - padding: 20px; - text-align: center; - box-shadow: var(--el-box-shadow-light); -} - -.clock-card.local { - border-top: 4px solid #409EFF; -} - -.clock-card.server { - border-top: 4px solid #E6A23C; -} - -.clock-label { - font-size: 14px; - color: var(--el-text-color-secondary); - margin-bottom: 10px; -} - -.clock-time { - font-size: 24px; - font-weight: bold; - font-family: monospace; - color: var(--el-text-color-primary); -} - -.sync-details { - background: var(--el-fill-color-light); - padding: 15px; - border-radius: 8px; - margin-bottom: 20px; - text-align: left; -} - -.sync-details p { - margin: 5px 0; - display: flex; - justify-content: space-between; - color: var(--el-text-color-regular); -} - -/* --- ToolQrParser (二维码解析) --- */ -.qr-parser-container { - display: flex; - flex-direction: column; - align-items: center; -} - -.scan-area, -.upload-area { - width: 100%; - max-width: 500px; - margin-bottom: 20px; -} - -.video-wrapper { - position: relative; - width: 100%; - border-radius: 12px; - overflow: hidden; - background-color: #000; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); - line-height: 0; -} - -.camera-placeholder, -.image-preview { - width: 100%; - height: 250px; - border: 2px dashed var(--el-border-color); - border-radius: 12px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - background: var(--el-fill-color-lighter); - transition: border-color 0.3s; - overflow: hidden; -} - -.camera-placeholder:hover, -.image-preview:hover { - border-color: #409EFF; -} - -.camera-placeholder p, -.upload-placeholder p { - margin-top: 10px; - color: var(--el-text-color-secondary); -} - -.preview-img { - max-width: 100%; - max-height: 100%; - object-fit: contain; -} - -.result-section { - width: 100%; -} - -/* --- ToolPassword (密码生成) --- */ -.result-box { - background: var(--el-fill-color-light); - border: 1px solid var(--el-border-color); - border-radius: 8px; - padding: 20px; - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 30px; -} - -.password-display { - font-family: monospace; - font-size: 20px; - font-weight: bold; - color: var(--el-text-color-primary); - word-break: break-all; - margin-right: 10px; -} - -.controls { - background: var(--el-bg-color); - padding: 10px; -} - -.control-row { - display: flex; - align-items: center; - margin-bottom: 20px; -} - -.control-row.checkboxes { - justify-content: space-between; - flex-wrap: wrap; -} - -.control-row .label { - min-width: 60px; - color: var(--el-text-color-regular); -} - -@media (max-width: 768px) { - .clocks-wrapper { - flex-direction: column; - } - - .control-row.checkboxes { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - } -} - -/* --- ToolTotpSecret (TOTP 密钥工具) --- */ -.totp-layout { - display: flex; - flex-direction: column; - gap: 30px; -} - -.preview-top-section { - display: flex; - gap: 20px; - justify-content: center; - flex-wrap: wrap; -} - -@media (max-width: 768px) { - .preview-top-section { - flex-direction: column; - align-items: center; - } -} - -.qr-wrapper { - width: 200px; - height: 200px; - display: flex; - align-items: center; - justify-content: center; - background: #fff; - border: 1px solid var(--el-border-color); - border-radius: 8px; - padding: 10px; - box-shadow: var(--el-box-shadow-light); - flex-shrink: 0; -} - -.qr-img { - width: 100%; - height: 100%; -} - -.uri-box { - margin-top: 15px; - background: var(--el-fill-color-light); - border-radius: 4px; - padding: 10px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; -} - -.uri-text { - font-family: monospace; - font-size: 12px; - color: var(--el-text-color-regular); - word-break: break-all; - line-height: 1.4; -} - -.config-section { - margin-bottom: 25px; -} - -.section-title { - font-size: 16px; - font-weight: 600; - margin-bottom: 15px; - color: var(--el-text-color-primary); -} - -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -.label-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 5px; -} - -.meta-row { - display: flex; - gap: 15px; - flex-wrap: wrap; -} - -.meta-row .el-input { - flex: 1; - min-width: 200px; -} - -.advanced-row { - display: flex; - gap: 10px; -} - -.label-text { - font-size: 14px; - color: var(--el-text-color-regular); - font-weight: 500; -} - -.result-card { - width: 200px; - height: 200px; - background: var(--el-bg-color-overlay); - border: 1px solid var(--el-border-color-light); - border-radius: 8px; - padding: 10px; - box-shadow: var(--el-box-shadow-light); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex-shrink: 0; -} - -.totp-code { - font-size: 32px; - font-weight: bold; - color: #409EFF; - letter-spacing: 6px; - font-family: monospace; - margin-bottom: 5px; - transition: filter 0.3s; -} - -.totp-code.blur { - filter: blur(4px); - opacity: 0.6; -} - -.totp-timer { - font-size: 14px; - color: var(--el-text-color-secondary); - display: flex; - align-items: center; - justify-content: center; - gap: 5px; -} - -.totp-timer.urgent { - color: #F56C6C; - font-weight: bold; -} - -.tab-actions { - margin-top: 10px; - display: flex; - justify-content: flex-end; - gap: 10px; -} - -.input-label { - margin-right: 12px; -} - -/* ========================================= - 6. VaultUnlock 组件 (解锁 / 创建密码) - ========================================= */ -.vault-unlock-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 400px; - padding: 20px; -} - -.unlock-card { - width: 100%; - max-width: 400px; - text-align: center; - border-radius: 12px; -} - -.icon-wrapper { - background: var(--el-color-primary-light-9); - width: 80px; - height: 80px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-right: 15px; -} - -.title { - margin: 0 0 10px; - font-size: 24px; - color: var(--el-text-color-primary); -} - -.form-container { - display: flex; - flex-direction: column; - gap: 15px; -} - -.error-msg { - color: var(--el-color-danger); - font-size: 13px; - text-align: left; - padding-left: 5px; -} - -.submit-btn { - width: 100%; - margin-top: 10px; - font-weight: bold; -} - -.reset-link { - margin-top: 15px; -} - -/* ========================================= - 7. QrScanner 组件 (摄像头 / 图片上传) - ========================================= */ -.qr-scanner-container { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -} - -/* scan-area 与 upload-area 已并入上方全局规则,下部移除原遗留定义 */ -.scan-overlay { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 70%; - aspect-ratio: 1; - border: 2px solid rgba(64, 158, 255, 0.8); - box-shadow: 0 0 0 4000px rgba(0, 0, 0, 0.4); - border-radius: 12px; - pointer-events: none; -} - -.scan-line { - width: 100%; - height: 2px; - background-color: #409EFF; - box-shadow: 0 0 10px #409EFF; - animation: scan 2s linear infinite; -} - -@keyframes scan { - 0% { - transform: translateY(0); - } - - 50% { - transform: translateY(200px); - } - - 100% { - transform: translateY(0); - } -} - -.upload-placeholder { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 250px; - cursor: pointer; - transition: all 0.3s ease; - border: none; - border-radius: 12px; -} - -.upload-placeholder:hover { - color: var(--el-color-primary); -} - -@media (max-width: 768px) { - - /* 修复 el-dialog 在移动端固定宽溢出 */ - .el-dialog { - width: 90% !important; - } - - .tab-card-wrapper { - padding: 20px; - } -} \ No newline at end of file +/* + * 2FAuth-Worker Global Styles + * Organized using a modular structure (BEM-lite / Pattern based) + */ + +/* 1. Base & Variables */ +@import './modules/base/variables.css'; +@import './modules/base/reset.css'; + +/* 2. Utility Classes (Atomic Utilities) */ +@import './modules/utilities/flex.css'; +@import './modules/utilities/spacing.css'; +@import './modules/utilities/sizing.css'; +@import './modules/utilities/typography.css'; +@import './modules/utilities/misc.css'; + +/* 3. Global Layout Components */ +@import './modules/layout/containers.css'; +@import './modules/layout/header.css'; +@import './modules/layout/sidebar.css'; +@import './modules/layout/footer.css'; + +/* 4. Feature Components Styles */ +@import './modules/components/shared.css'; +@import './modules/components/auth.css'; +@import './modules/components/vault.css'; +@import './modules/components/backup.css'; +@import './modules/components/migration.css'; +@import './modules/components/tools.css'; +@import './modules/components/health.css'; \ No newline at end of file diff --git a/frontend/src/app/styles/modules/base/reset.css b/frontend/src/app/styles/modules/base/reset.css new file mode 100644 index 0000000..256f6d2 --- /dev/null +++ b/frontend/src/app/styles/modules/base/reset.css @@ -0,0 +1,74 @@ +/* ========================================= + 0. 强制同步修复 (Force Sync) + ========================================= */ +/* 核心修复:强制禁用所有元素的过渡动画,确保深浅色切换瞬间完成,消除不同步感 */ +*, +*::before, +*::after { + transition: none !important; +} + +/* ========================================= + 1. 全局基础样式 (Global Reset & Base) + ========================================= */ +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + background-color: var(--el-bg-color-page); + color: var(--el-text-color-primary); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +/* 移动端特殊适配:释放根部滚动以支持原生下拉刷新 */ +@media (max-width: 768px) { + + html, + body, + #app { + height: auto; + min-height: 100%; + overflow: visible; + } +} + +a { + color: inherit; + text-decoration: none; + transition: color 0.2s; +} + +a:hover { + color: #409EFF; +} + +/* 滚动条优化 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--el-border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--el-text-color-secondary); +} + +/* 通用组件样式 */ +.tab-card-wrapper { + background: var(--el-bg-color); + padding: 20px; + border-radius: 12px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); + min-height: 400px; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/base/variables.css b/frontend/src/app/styles/modules/base/variables.css new file mode 100644 index 0000000..2826a43 --- /dev/null +++ b/frontend/src/app/styles/modules/base/variables.css @@ -0,0 +1,16 @@ +/* ========================================= + 0.1 全局语义化变量 (Design Tokens) + ========================================= */ +:root { + /* 玻璃拟态卡片 (登录/加载) */ + --glass-card-bg: rgba(255, 255, 255, 0.95); + --glass-card-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); + + /* GitHub 按钮 */ + --github-btn-bg: #24292e; + --github-btn-border: #24292e; + + /* 状态色块 */ + --success-tip-bg: var(--el-color-success-light-9); + --vault-selected-bg: var(--el-color-primary-light-9); +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/auth.css b/frontend/src/app/styles/modules/components/auth.css new file mode 100644 index 0000000..ac37138 --- /dev/null +++ b/frontend/src/app/styles/modules/components/auth.css @@ -0,0 +1,82 @@ +/* Login.vue */ +.login-container { + margin: 25px 0px; + display: flex; + justify-content: center; + align-items: center; + flex: 1; + background: transparent; + position: relative; +} + +.login-card { + width: 92%; + max-width: 400px; + border-radius: 16px; + box-shadow: var(--glass-card-shadow); + background: var(--glass-card-bg); + backdrop-filter: blur(10px); + border: none; +} + +.logo-container { + text-align: center; + margin-bottom: 30px; + padding-top: 10px; +} + +.logo-container h2 { + margin: 16px 0 8px; + color: var(--el-text-color-primary); + font-size: 24px; + font-weight: 600; +} + +.subtitle { + margin: 0; + color: var(--el-text-color-secondary); + font-size: 14px; +} + +.action-container { + display: flex; + flex-direction: column; + gap: 15px; + margin-bottom: 30px; +} + +.oauth-btn { + width: 100%; + height: 50px; + font-size: 16px; + border-radius: 8px; + margin-left: 0 !important; +} + +.oauth-btn:hover, +.oauth-btn:focus { + filter: brightness(1.1); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.footer-tips { + margin-top: 10px; +} + +/* OAuthCallback.vue */ +.callback-container { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + background: transparent; +} + +.loading-card { + width: 92%; + max-width: 400px; + border-radius: 16px; + background: var(--glass-card-bg); + backdrop-filter: blur(10px); +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/backup.css b/frontend/src/app/styles/modules/components/backup.css new file mode 100644 index 0000000..77ad87b --- /dev/null +++ b/frontend/src/app/styles/modules/components/backup.css @@ -0,0 +1,65 @@ +/* Backup Feature Styles */ +.backup-container { + padding: 10px; +} + +.backup-header-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.backup-provider-card { + height: 100%; + transition: transform 0.2s; +} + +.backup-provider-card:hover { + transform: translateY(-5px); +} + +.backup-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.backup-provider-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: bold; +} + +.backup-status-text { + font-size: 13px; + color: var(--el-text-color-secondary); + margin-bottom: 20px; +} + +.backup-action-buttons { + display: flex; + gap: 10px; +} + +.backup-form-tip { + font-size: 12px; + color: var(--el-text-color-secondary); + margin-top: 5px; + line-height: 1.4; +} + +.backup-success-tip { + display: flex; + align-items: center; + gap: 5px; + color: var(--el-color-success); + font-size: 13px; + padding: 10px; + background-color: var(--success-tip-bg); + border-radius: 4px; + /* + width: 100%; +*/ +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/health.css b/frontend/src/app/styles/modules/components/health.css new file mode 100644 index 0000000..fe21dfa --- /dev/null +++ b/frontend/src/app/styles/modules/components/health.css @@ -0,0 +1,192 @@ +/* HealthCheck.vue */ +.health-check-container { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 20px; + box-sizing: border-box; + overflow-y: auto; +} + +.health-card { + width: 95%; + border-radius: 12px; +} + +:global(html.dark) .health-card { + background-color: #1a1a1a; + border-color: #2c2c2c; +} + +.health-header { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.health-header .alert-title { + margin: 16px 0 0 0; + font-size: 24px; + font-weight: 600; + color: var(--el-text-color-primary); + letter-spacing: 0.5px; +} + +.health-header .alert-divider { + width: 40px; + height: 4px; + background-color: var(--el-color-danger); + border-radius: 2px; + margin: 16px 0; +} + +.health-header .subtitle { + margin: 15px 0px; + padding: 10px; +} + +.health-header .subtitle :deep(.el-alert__content) { + padding: 0; +} + +.health-header .subtitle :deep(.el-alert__title) { + color: var(--el-text-color-regular); + font-size: 16px; + line-height: 20px; + white-space: normal; + word-wrap: break-word; +} + +.issue-alert { + margin-bottom: 20px; + align-items: flex-start; +} + +.issue-content { + margin-top: 5px; +} + +.issue-desc { + font-size: 14px; + margin-bottom: 15px; + line-height: 1.5; +} + +.suggestion-box { + background-color: var(--el-color-primary-light-9); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--el-color-primary-light-7); + border-left: 4px solid var(--el-color-primary); +} + +:global(html.dark) .suggestion-box { + background-color: rgba(64, 158, 255, 0.08); + border: 1px solid rgba(64, 158, 255, 0.2); + border-left: 4px solid var(--el-color-primary); +} + +:global(html.dark) .issue-alert { + background-color: rgba(245, 108, 108, 0.05); + border: 1px solid rgba(245, 108, 108, 0.2); +} + +:global(html.dark) .missing-fields-text { + background-color: rgba(245, 108, 108, 0.1) !important; + border: 1px solid rgba(245, 108, 108, 0.3) !important; +} + +.suggestion-title { + font-weight: bold; + margin-bottom: 8px; + font-size: 13px; + color: var(--el-text-color-primary); +} + +.fix-action p { + margin: 0 0 10px 0; + font-size: 13px; + color: var(--el-text-color-regular); +} + +.deploy-method-guide { + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px dashed var(--el-border-color); +} + +.deploy-item { + display: flex; + align-items: center; + gap: 6px; + margin: 0 0 8px 0; + font-size: 13px; + color: var(--el-text-color-primary); + font-weight: 500; +} + +.text-danger p { + color: var(--el-color-danger); + font-weight: bold; +} + +.key-generator { + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-start; +} + +.mono-font :deep(.el-input__inner) { + font-family: monospace; + font-size: 13px; + letter-spacing: 0.5px; +} + +.mt-2 { + margin-top: 8px; +} + +.passed-checks-section { + margin-top: 30px; + padding-top: 20px; + border-top: 1px dashed var(--el-border-color); +} + +.passed-checks-section h3 { + font-size: 15px; + color: var(--el-text-color-primary); + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 15px; +} + +.passed-checks-section ul { + list-style: none; + padding: 0; + margin: 0; +} + +.passed-checks-section li { + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + color: var(--el-text-color-regular); + margin-bottom: 10px; + padding: 8px 12px; + background-color: var(--el-fill-color-light); + border-radius: 6px; +} + +.missing-fields-text { + display: inline-block; + margin-top: 8px; + color: var(--el-color-danger); + background-color: var(--el-color-danger-light-9); + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/migration.css b/frontend/src/app/styles/modules/components/migration.css new file mode 100644 index 0000000..fe60783 --- /dev/null +++ b/frontend/src/app/styles/modules/components/migration.css @@ -0,0 +1,229 @@ +/* Migration & Backup (DataImport.vue & DataExport.vue) */ + +.migration-export-groups { + max-width: 100%; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 20px; +} + +.migration-export-group-card { + padding: 20px; + background-color: var(--el-bg-color-overlay); + border: 1px solid var(--el-border-color-light); + border-radius: 8px; + display: flex; + flex-direction: column; +} + +.migration-group-header { + display: flex; + align-items: center; + font-size: 16px; + font-weight: bold; + margin-bottom: 15px; + color: var(--el-text-color-primary); +} + +.migration-group-header .el-icon { + margin-right: 8px; + font-size: 18px; + color: var(--el-color-primary); +} + +.migration-button-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.migration-button-row .el-button { + margin-left: 0; +} + +.migration-button-with-icon { + display: inline-flex; + align-items: center; +} + +.migration-button-with-icon .el-icon { + margin-right: 6px; + font-size: 16px; +} + +/* 账号选择列表样式 */ +.migration-account-select-toolbar { + margin-bottom: 10px; +} + +.migration-toolbar-actions { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 5px; + margin-bottom: 10px; +} + +.migration-selected-count { + font-size: 13px; + color: var(--el-text-color-secondary); +} + +.migration-account-list-container { + border: 1px solid var(--el-border-color-light); + border-radius: 4px; + padding: 5px; +} + +.migration-account-item { + padding: 8px 10px; + border-bottom: 1px solid var(--el-border-color-lighter); +} + +.migration-account-item:last-child { + border-bottom: none; +} + +.migration-account-item-content { + display: flex; + flex-direction: column; + margin-left: 8px; + line-height: 1.2; +} + +.migration-account-item .service-name { + font-size: 15px; + font-weight: 600; + color: var(--el-text-color-primary); + margin-bottom: 4px; +} + +.migration-account-item .account-name { + font-size: 13px; + color: var(--el-text-color-regular); +} + +/* DataImport.vue Specific */ +.migration-import-upload { + margin-top: 20px; +} + +.migration-import-upload .el-upload-dragger { + padding: 40px; + background-color: var(--el-fill-color-light); + border-color: var(--el-border-color); +} + +.migration-import-upload .el-upload-dragger:hover { + border-color: var(--el-color-primary); +} + +.migration-import-upload .el-icon--upload { + font-size: 48px; + color: var(--el-text-color-secondary); + margin-bottom: 20px; +} + +.migration-import-tips { + text-align: left; + margin-top: 25px; + background-color: var(--el-fill-color-lighter); + padding: 0px 20px 20px 20px; + border-radius: 8px; + border: 1px dashed var(--el-border-color); +} + +.migration-format-groups { + display: flex; + flex-direction: column; + gap: 15px; +} + +.migration-format-group h4 { + margin: 0 0 10px 0; + color: var(--el-text-color-primary); + font-size: 14px; + font-weight: 600; +} + +.migration-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; +} + +.migration-tags .el-tag { + padding: 4px 8px; + height: auto; + color: var(--el-text-color-regular); + cursor: default; + border-color: transparent; + background-color: var(--el-fill-color-light); +} + +.migration-tags .el-tag .el-icon { + display: inline-flex; + flex-wrap: wrap; + margin-right: 4px; +} + +.migration-ga-tip { + background-color: var(--el-color-primary-light-9); + padding: 10px 12px; + border-radius: 6px; + border-left: 3px solid var(--el-color-primary); + margin-top: 10px; +} + +.migration-ga-tip span { + font-weight: 600; + font-size: 13px; + color: var(--el-color-primary); + display: block; + margin-bottom: 4px; +} + +.migration-ga-tip p { + margin: 0; + font-size: 12px; + color: var(--el-text-color-regular); + line-height: 1.5; +} + +.migration-ms-tip { + background-color: var(--el-color-success-light-9); + padding: 10px 12px; + border-radius: 6px; + border-left: 3px solid var(--el-color-success); + margin-top: 10px; +} + +.migration-ms-tip span { + font-weight: 600; + font-size: 13px; + color: var(--el-color-success); + display: block; + margin-bottom: 4px; +} + +.migration-ms-tip p { + margin: 0; + font-size: 12px; + color: var(--el-color-regular); + line-height: 1.5; +} + +.migration-ms-tip code { + word-wrap: break-word; +} + +/* dataMigration.vue */ +.migration-container { + padding: 10px; +} + +.migration-tabs :deep(.el-tabs__nav-wrap::after) { + height: 1px; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/shared.css b/frontend/src/app/styles/modules/components/shared.css new file mode 100644 index 0000000..cfb9fe8 --- /dev/null +++ b/frontend/src/app/styles/modules/components/shared.css @@ -0,0 +1,197 @@ +/* Shared Component Patterns */ + +/* 1. Confirmation Box (Used in ElMessageBox with h() function) */ +.confirmation-box { + text-align: left; + background: var(--el-fill-color-light); + padding: 16px; + border-radius: 8px; + border: 1px solid var(--el-border-color-lighter); + margin-top: 10px; +} + +.confirmation-row { + margin-bottom: 12px; + display: flex; + align-items: center; +} + +.confirmation-row:last-child { + margin-bottom: 0; +} + +.confirmation-label { + color: var(--el-text-color-secondary); + width: 70px; + flex-shrink: 0; +} + +.confirmation-value { + font-weight: 600; + font-size: 15px; + color: var(--el-text-color-primary); + word-break: break-all; +} + +.confirmation-value.mono { + font-family: monospace; + font-size: 14px; + background: var(--el-fill-color-darker); + padding: 4px 8px; + border-radius: 6px; + color: var(--el-color-primary); +} + +.confirmation-tags { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.confirmation-tag { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + border: 1px solid transparent; +} + +.confirmation-tag-info { + background: var(--el-color-info-light-9); + color: var(--el-color-info); + border-color: var(--el-color-info-light-7); +} + +.confirmation-tag-success { + background: var(--el-color-success-light-9); + color: var(--el-color-success); + border-color: var(--el-color-success-light-7); +} + +.confirmation-tag-warning { + background: var(--el-color-warning-light-9); + color: var(--el-color-warning); + border-color: var(--el-color-warning-light-7); +} + +/* 2. Global Offline Banner (App.vue) */ +.global-offline-banner { + position: sticky; + top: 0; + z-index: 9999; + border: none; + border-radius: 0; + border-bottom: 1px solid var(--el-border-color-lighter); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + padding: 10px 16px !important; +} + +.global-offline-banner .el-alert__content { + padding: 0 8px; +} + +.global-offline-banner .el-alert__title { + font-size: 14px; + line-height: 1.4; +} + +.global-offline-banner .el-alert__close-btn { + top: 50%; + transform: translateY(-50%); +} + +html.dark .global-offline-banner { + background-color: rgba(230, 162, 60, 0.1); + border-bottom-color: rgba(230, 162, 60, 0.2); +} + +/* 3. QrScanner Component */ +.qr-scanner-container { + width: 100%; +} + +.qr-scanner-selector { + margin-bottom: 20px; +} + +.camera-placeholder { + height: 250px; + border: 2px dashed var(--el-border-color); + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + background: var(--el-fill-color-lighter); + transition: all 0.3s; +} + +.camera-placeholder:hover { + border-color: var(--el-color-primary); + background: var(--el-fill-color-light); +} + +.video-wrapper { + position: relative; + width: 100%; + height: 250px; + background: #000; + border-radius: 12px; + overflow: hidden; +} + +.video-preview { + width: 100%; + height: 100%; + object-fit: cover; +} + +.scan-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 2px solid rgba(64, 158, 252, 0.4); + box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.5); +} + +.scan-line { + position: absolute; + width: 100%; + height: 2px; + background: #409eff; + top: 0; + box-shadow: 0 0 10px #409eff; + animation: scan-anim 2s infinite linear; +} + +@keyframes scan-anim { + 0% { + top: 0%; + } + + 100% { + top: 100%; + } +} + +.image-preview { + height: 250px; + border: 2px dashed var(--el-border-color); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.preview-img { + max-width: 100%; + max-height: 100%; + border-radius: 10px; +} + +.upload-placeholder { + text-align: center; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/tools.css b/frontend/src/app/styles/modules/components/tools.css new file mode 100644 index 0000000..3c598c1 --- /dev/null +++ b/frontend/src/app/styles/modules/components/tools.css @@ -0,0 +1,610 @@ +/* ========================================= + 4. UtilityTools 组件 (工具箱) + ========================================= */ +.tools-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.tools-header .back-btn { + padding: 0; + font-size: 16px; + margin-right: 5px; +} + +.tools-header h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--el-text-color-primary); +} + +.tools-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; +} + +.tool-card { + cursor: pointer; + transition: all 0.3s; + border: 1px solid var(--el-border-color-lighter); +} + +.tool-card:hover { + transform: translateY(-4px); + box-shadow: var(--el-box-shadow-light); +} + +.tool-card .el-card__body { + padding: 24px !important; +} + +.icon-wrapper { + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 18px; + flex-shrink: 0; +} + +.tool-card-totp-secret .icon-wrapper { + background-color: var(--el-color-success-light-9) !important; +} + +.tool-card-totp-secret .el-icon { + color: #67C23A !important; +} + +.tool-card-apps-review .icon-wrapper { + background-color: var(--el-color-info-light-9) !important; +} + +.tool-card-apps-review .el-icon { + color: #9c27b0 !important; +} + +.tool-card-password .icon-wrapper { + background-color: var(--el-color-primary-light-9) !important; +} + +.tool-card-password .el-icon { + color: #409EFF !important; +} + +.tool-card-time-sync .icon-wrapper { + background-color: var(--el-color-warning-light-9) !important; +} + +.tool-card-time-sync .el-icon { + color: #E6A23C !important; +} + +.tool-card-qr-parser .icon-wrapper { + background-color: var(--el-color-danger-light-9) !important; +} + +.tool-card-qr-parser .el-icon { + color: #F56C6C !important; +} + +.tool-card-content { + display: flex; + align-items: center; +} + +.text-info { + flex: 1; +} + +.text-info h3 { + margin: 0 0 4px; + font-size: 16px; + font-weight: 600; + color: var(--el-text-color-primary); +} + +.text-info p { + margin: 0; + font-size: 12px; + color: var(--el-text-color-secondary); + line-height: 1.4; +} + +/* ========================================= + 5. UtilityTools 子组件详情页样式 + ========================================= */ + +/* 通用容器 */ +.tool-pane { + max-width: 600px; + margin: 0 auto; + padding: 10px 0; +} + +/* --- ToolTimeSync (时间校准) --- */ +.time-sync-container { + text-align: center; +} + +.clocks-wrapper { + display: flex; + gap: 20px; + margin-bottom: 30px; +} + +.clock-card { + flex: 1; + background: var(--el-bg-color-overlay); + border: 1px solid var(--el-border-color-light); + border-radius: 12px; + padding: 20px; + text-align: center; + box-shadow: var(--el-box-shadow-light); +} + +.clock-card.local { + border-top: 4px solid #409EFF; +} + +.clock-card.server { + border-top: 4px solid #E6A23C; +} + +.clock-label { + font-size: 14px; + color: var(--el-text-color-secondary); + margin-bottom: 10px; + display: block; +} + +.clock-time { + font-size: 28px; + font-weight: bold; + font-family: monospace; + color: var(--el-text-color-primary); + margin-bottom: 5px; + display: block; +} + +.clock-date { + font-size: 14px; + color: var(--el-text-color-regular); +} + +.diff-display { + margin-bottom: 30px; +} + +.diff-value { + font-size: 18px; + font-weight: 600; + color: var(--el-color-primary); +} + +.diff-desc { + font-size: 14px; + color: var(--el-text-color-secondary); + margin-top: 8px; +} + +.sync-actions { + display: flex; + justify-content: center; + gap: 15px; +} + +.sync-tips { + margin-top: 40px; + text-align: left; + background: var(--el-fill-color-lighter); + padding: 20px; + border-radius: 8px; +} + +.sync-tips h4 { + margin: 0 0 10px; + font-size: 15px; + color: var(--el-text-color-primary); +} + +.sync-tips ul { + margin: 0; + padding-left: 20px; + color: var(--el-text-color-regular); + font-size: 13px; + line-height: 1.6; +} + +/* --- ToolQrParser (二维码解析) --- */ +.qr-parser-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.upload-area { + width: 100%; +} + +.upload-dragger { + width: 100%; + height: 250px; + border: 2px dashed var(--el-border-color); + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + background: var(--el-fill-color-lighter); + transition: border-color 0.3s; + overflow: hidden; +} + +.upload-dragger:hover { + border-color: #409EFF; +} + +.upload-placeholder { + display: flex; + flex-direction: column; + align-items: center; +} + +.upload-placeholder p { + margin-top: 10px; + color: var(--el-text-color-secondary); +} + +.preview-img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.result-section { + width: 100%; +} + +/* --- ToolPassword (密码生成) --- */ +.result-box { + background: var(--el-fill-color-light); + border: 1px solid var(--el-border-color); + border-radius: 8px; + padding: 20px; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 30px; +} + +.password-display { + font-family: monospace; + font-size: 20px; + font-weight: bold; + color: var(--el-text-color-primary); + word-break: break-all; + margin-right: 10px; +} + +.controls { + background: var(--el-bg-color); + padding: 10px; +} + +.control-row { + display: flex; + align-items: center; + margin-bottom: 20px; +} + +.control-row.checkboxes { + justify-content: space-between; + flex-wrap: wrap; +} + +.control-row .label { + min-width: 60px; + color: var(--el-text-color-regular); +} + +@media (max-width: 768px) { + .clocks-wrapper { + flex-direction: column; + } + + .control-row.checkboxes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + } +} + +/* --- ToolTotpSecret (TOTP 密钥工具) --- */ +.totp-layout { + display: flex; + flex-direction: column; + gap: 30px; +} + +.preview-top-section { + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; +} + +@media (max-width: 768px) { + .preview-top-section { + flex-direction: column; + align-items: center; + } +} + +.qr-wrapper { + width: 200px; + height: 200px; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + border: 1px solid var(--el-border-color); + border-radius: 8px; + padding: 10px; + box-shadow: var(--el-box-shadow-light); + flex-shrink: 0; +} + +.qr-img { + width: 100%; + height: 100%; +} + +.uri-box { + margin-top: 15px; + background: var(--el-fill-color-light); + border-radius: 4px; + padding: 10px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.uri-text { + font-family: monospace; + font-size: 12px; + color: var(--el-text-color-regular); + word-break: break-all; + line-height: 1.4; +} + +.config-section { + margin-bottom: 25px; +} + +.section-title { + font-size: 16px; + font-weight: 600; + margin-bottom: 15px; + color: var(--el-text-color-primary); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.label-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; +} + +.meta-row { + display: flex; + gap: 15px; + flex-wrap: wrap; +} + +.meta-row .el-input { + flex: 1; + min-width: 200px; +} + +.advanced-row { + display: flex; + gap: 10px; +} + +.label-text { + font-size: 14px; + color: var(--el-text-color-regular); + font-weight: 500; +} + +.result-card { + width: 200px; + height: 200px; + background: var(--el-bg-color-overlay); + border: 1px solid var(--el-border-color-light); + border-radius: 8px; + padding: 10px; + box-shadow: var(--el-box-shadow-light); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex-shrink: 0; +} + +.totp-code { + font-size: 32px; + font-weight: bold; + color: #409EFF; + letter-spacing: 6px; + font-family: monospace; + margin-bottom: 5px; + transition: filter 0.3s; +} + +.totp-code.blur { + filter: blur(4px); + opacity: 0.6; +} + +.totp-timer { + font-size: 14px; + color: var(--el-text-color-secondary); + display: flex; + align-items: center; + justify-content: center; + gap: 5px; +} + +.totp-timer.urgent { + color: #F56C6C; + font-weight: bold; +} + +.tab-actions { + margin-top: 10px; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.input-label { + margin-right: 12px; +} + +/* --- ToolAppsReview (App 评测) --- */ +.apps-review-container { + padding: 10px 0; +} + +.apps-review-card { + height: 100%; + display: flex; + flex-direction: column; + transition: transform 0.2s, box-shadow 0.2s; + border-radius: 10px; +} + +.apps-review-card:hover { + transform: translateY(-2px); +} + +.apps-review-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.apps-review-card-header { + margin-bottom: 12px; +} + +.apps-review-card-name { + margin: 0px 0px 12px 0px; + font-size: 18px; + font-weight: 600; + color: var(--el-text-color-primary); + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.apps-review-card-logo { + flex-shrink: 0; +} + +.apps-review-card-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + /* 空间不够时允许换行 */ + gap: 8px; + margin-bottom: 12px; +} + +.apps-review-platform-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; + color: var(--el-text-color-regular); + font-size: 16px; + text-decoration: none !important; +} + +.apps-review-details { + display: flex; + flex-direction: column; + gap: 10px; + font-size: 14px; + color: var(--el-text-color-regular); +} + +.apps-review-detail-item { + display: flex; + align-items: flex-start; + gap: 8px; + line-height: 1.5; +} + +.apps-review-detail-item .label { + font-weight: 500; + min-width: 85px; + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.apps-review-summary-box { + margin-top: 8px; + padding: 12px; + background-color: var(--el-fill-color-light); + border-radius: 6px; + font-size: 13px; + line-height: 1.5; + border-left: 3px solid var(--el-color-primary); +} + +.apps-review-summary-danger { + background-color: var(--el-color-danger-light-9); + border-left: 3px solid var(--el-color-danger); +} + +:global(html.dark) .apps-review-summary-danger { + background-color: rgba(245, 108, 108, 0.1); +} + +@media (max-width: 768px) { + .apps-review-header { + flex-direction: column; + gap: 8px; + } + + .apps-review-detail-item { + flex-direction: column; + align-items: flex-start; + } + + .apps-review-detail-item .label { + margin-bottom: 2px; + } +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/vault.css b/frontend/src/app/styles/modules/components/vault.css new file mode 100644 index 0000000..55b51d6 --- /dev/null +++ b/frontend/src/app/styles/modules/components/vault.css @@ -0,0 +1,270 @@ +/* 搜索栏吸顶过渡动画 */ +.vault-list-toolbar { + transition: padding 0.3s ease, background-color 0.3s ease; + box-sizing: border-box; +} + +/* 共有吸顶属性 */ +.vault-list-toolbar.is-affixed { + z-index: 2000; + margin-bottom: 0 !important; + background-color: var(--el-bg-color-page); +} + +/* 移动端吸顶效果:全屏、背景、边框 */ +@media (max-width: 767px) { + .vault-list-toolbar.is-affixed { + width: 100vw !important; + margin-left: -20px !important; + /* 抵消 el-main 的内边距 */ + padding: 14px 20px !important; + border-radius: 0 !important; + } +} + +/* PC 端吸顶效果:保持原有宽度,无背景色和下边框 */ +@media (min-width: 768px) { + .vault-list-toolbar.is-affixed { + border-bottom: none !important; + padding: 20px 0px !important; + } +} + +/* VaultList.vue */ +.vault-card { + border-radius: 12px; + /* 全局规则已覆盖 transition,此处只需定义样式 */ + border: 1px solid var(--el-border-color-lighter); + cursor: pointer; + background-color: var(--el-bg-color); + height: 100%; + display: flex; + flex-direction: column; +} + +.vault-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1) !important; +} + +.vault-list-wrapper .card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +/* Flex 容器:限制宽度以支持内部文本截断 */ +.service-info { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + /* 占据剩余空间 */ + min-width: 0; + /* 关键:允许 Flex 子项收缩 */ + margin-right: 10px; +} + +/* 标题截断逻辑 */ +.service-name { + margin: 0; + font-size: 18px; + color: var(--el-text-color-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 让卡片内容撑开,底部对齐 */ +.vault-card .el-card__body { + flex: 1; + display: flex; + flex-direction: column; +} + +.vault-name { + color: var(--el-text-color-secondary); + font-size: 14px; + margin: 0 0 10px 24px; + word-break: break-all; +} + +.more-icon { + cursor: pointer; + color: var(--el-text-color-secondary); + padding: 5px; +} + +.more-icon:hover { + color: #409EFF; +} + +.vault-card.is-selected { + border: 1px solid #409EFF; + background-color: var(--vault-selected-bg); +} + +.batch-text { + color: var(--el-text-color-regular); + font-size: 14px; +} + +.code-display-area { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + padding-left: 24px; + padding-top: 15px; +} + +.code-left { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; +} + +.current-code { + font-size: 24px; + font-weight: bold; + color: #409EFF; + letter-spacing: 2px; + line-height: 1; + font-family: monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: clip; +} + +.current-code[data-digits="8"] { + font-size: 20px; + letter-spacing: 0px; +} + +.next-code { + font-size: 16px; + color: var(--el-text-color-secondary); + font-family: monospace; +} + +.next-code[data-digits="8"] { + font-size: 14px; + letter-spacing: 0px; +} + +.timer-text { + font-size: 12px; + color: var(--el-text-color-regular); + font-weight: bold; + display: block; + text-align: center; +} + +.el-progress__text { + top: 0; + left: 0; + width: 100%; + height: 100%; + transform: none; + display: flex; + align-items: center; + justify-content: center; + min-width: 30px; +} + +/* VaultList - QR Dialog */ +.qr-container { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.qr-info { + margin-bottom: 15px; +} + +.qr-service { + margin: 0 0 5px 0; + font-size: 20px; + color: var(--el-text-color-primary); +} + +.qr-account { + margin: 0; + color: var(--el-text-color-secondary); + font-size: 14px; +} + +.qr-image-wrapper { + margin: 10px 0; + padding: 10px; + background: #fff; + border-radius: 8px; + border: 1px solid var(--el-border-color-lighter); +} + +.qr-code-img { + display: block; + width: 200px; + height: 200px; +} + +.qr-tip { + font-size: 12px; + color: var(--el-text-color-regular); + margin: 10px 0 20px 0; +} + +.secret-section { + width: 100%; + margin-bottom: 15px; +} + +.secret-box { + display: flex; + align-items: center; + justify-content: space-between; + background: var(--el-fill-color-light); + padding: 10px 15px; + border-radius: 6px; + border: 1px solid var(--el-border-color-light); +} + +.secret-text { + font-family: monospace; + font-size: 14px; + color: var(--el-text-color-regular); + letter-spacing: 1px; +} + +.secret-actions { + display: flex; + gap: 15px; +} + +.action-icon { + cursor: pointer; + font-size: 16px; + color: var(--el-text-color-secondary); + transition: color 0.2s; +} + +.action-icon:hover { + color: #409EFF; +} + +.uri-link-wrapper { + margin-top: 5px; +} + +/* AddVaultManual.vue */ +.vault-manual-form-wrapper { + padding: 10px 0; +} + +.vault-manual-submit-btn { + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/layout/containers.css b/frontend/src/app/styles/modules/layout/containers.css new file mode 100644 index 0000000..d78f65f --- /dev/null +++ b/frontend/src/app/styles/modules/layout/containers.css @@ -0,0 +1,67 @@ +/* App.vue */ +.app-container { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +@media (max-width: 768px) { + .app-container { + height: auto; + min-height: 100vh; + overflow: visible; + } +} + +main { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +@media (max-width: 768px) { + main { + overflow: visible; + } +} + +/* Home.vue */ +.home-container { + height: 100%; + background-color: var(--el-bg-color-page); +} + +.main-body { + height: 100%; + display: flex; + overflow: hidden; +} + +@media (max-width: 768px) { + + .home-container, + .main-body { + height: auto; + min-height: 100vh; + overflow: visible; + } +} + +.main-content { + width: 100%; + background-color: var(--el-bg-color-page); + /* 原为 auto,修改为保留滚动空间防止因高度变化产生的抖动,或直接将滚动条悬浮覆盖 */ + overflow-y: overlay; + scrollbar-gutter: stable; +} + +@media (max-width: 768px) { + .main-content { + overflow: visible !important; + overflow-y: visible !important; + overflow-x: visible !important; + scrollbar-gutter: auto; + } +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/layout/footer.css b/frontend/src/app/styles/modules/layout/footer.css new file mode 100644 index 0000000..6ff3dd6 --- /dev/null +++ b/frontend/src/app/styles/modules/layout/footer.css @@ -0,0 +1,19 @@ +/* TheFooter.vue */ +.global-footer { + width: 100%; + padding: 0.25rem 0; + background-color: var(--el-bg-color); + border-top: 1px solid var(--el-border-color-light); + text-align: center; + margin-top: auto; +} + +.footer-content { + color: var(--el-text-color-secondary); + font-size: 0.875rem; +} + +.divider { + margin: 0 0.5rem; + color: var(--el-border-color); +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/layout/header.css b/frontend/src/app/styles/modules/layout/header.css new file mode 100644 index 0000000..5fdd46a --- /dev/null +++ b/frontend/src/app/styles/modules/layout/header.css @@ -0,0 +1,74 @@ +/* TheHeader.vue */ +.header { + background: var(--el-bg-color); + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20px; + height: 60px; + flex-shrink: 0; + z-index: 100; + position: sticky; + top: 0; + border-bottom: 1px solid var(--el-border-color-light); +} + +.logo, +.user-actions, +.user-profile { + display: flex; + align-items: center; +} + +.logo h2 { + margin: 0; + color: var(--el-text-color-primary); + font-size: 20px; +} + +.user-actions { + gap: 20px; +} + +.user-profile { + gap: 10px; + background: var(--el-fill-color-light); + padding: 4px 15px 4px 4px; + border-radius: 20px; +} + +.username { + font-weight: 600; + color: var(--el-text-color-regular); + font-size: 14px; +} + +.menu-icon { + color: var(--el-text-color-regular); +} + +@media (max-width: 768px) { + .header { + padding: 0 15px; + } + + .logo h2 { + font-size: 18px; + } +} + +/* --- TheHeader Button & Link --- */ +.header-menu-btn { + margin-right: 10px; + padding: 5px 8px !important; + border: 1px solid var(--el-border-color) !important; + height: auto !important; +} + +.header-home-link { + display: flex; + align-items: center; + text-decoration: none; + color: inherit; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/layout/sidebar.css b/frontend/src/app/styles/modules/layout/sidebar.css new file mode 100644 index 0000000..8bb60e8 --- /dev/null +++ b/frontend/src/app/styles/modules/layout/sidebar.css @@ -0,0 +1,116 @@ +/* LeftAside, SideMenu */ +.left-aside { + background: var(--el-bg-color); + border-right: 1px solid var(--el-border-color-light); + display: flex; + flex-direction: column; + overflow-x: hidden; + overflow-y: auto; + white-space: nowrap; +} + +/* 视觉优化:隐藏侧边栏滚动条,保持界面清爽 */ +.left-aside::-webkit-scrollbar { + width: 0; + height: 0; +} + +.side-menu { + border-right: none; + padding-top: 10px; + flex: 1; +} + +.sidebar-footer { + padding: 20px; + text-align: center; + display: flex; + justify-content: center; + gap: 10px; +} + +.sidebar-footer .el-button { + margin-left: 0; +} + +.sidebar-footer.is-collapsed { + flex-direction: column; + align-items: center; + padding: 10px 0; +} + +.drawer-content { + height: 100%; + display: flex; + flex-direction: column; +} + +/* --- Mobile Drawer User Info Card --- */ +.user-info-card { + margin: 0px 20px 0px 20px; + padding-bottom: 20px; + border-bottom: 0.2px solid var(--el-border-color-lighter); +} + +.user-card-content { + display: flex; + align-items: center; + gap: 15px; +} + +.user-avatar { + flex-shrink: 0; + border: 2px solid var(--el-color-primary-light-8); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.user-detail { + display: flex; + flex-direction: column; + min-width: 0; +} + +.user-name-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 2px; +} + +.user-name { + font-size: 15px; + font-weight: 600; + color: var(--el-text-color-primary); + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background-color: #909399; + /* 默认灰色 */ + display: inline-block; + flex-shrink: 0; +} + +.status-dot.is-online { + background-color: #67C23A; + /* 在线绿色 */ + box-shadow: 0 0 4px rgba(103, 194, 58, 0.5); +} + +.user-provider { + font-size: 11px; + color: var(--el-text-color-secondary); + background: var(--el-fill-color); + padding: 1px; + border-radius: 2px; + display: inline-block; + width: fit-content; + text-transform: uppercase; + letter-spacing: 0.5px; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/utilities/flex.css b/frontend/src/app/styles/modules/utilities/flex.css new file mode 100644 index 0000000..6b7e502 --- /dev/null +++ b/frontend/src/app/styles/modules/utilities/flex.css @@ -0,0 +1,54 @@ +/* Flexbox Utilities */ +.flex { + display: flex !important; +} + +.flex-column { + display: flex !important; + flex-direction: column !important; +} + +.flex-center { + display: flex !important; + justify-content: center !important; + align-items: center !important; +} + +.flex-between { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; +} + +.flex-items-center { + display: flex !important; + align-items: center !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-1 { + flex: 1 !important; +} + +.gap-5 { + gap: 5px !important; +} + +.gap-8 { + gap: 8px !important; +} + +.gap-10 { + gap: 10px !important; +} + +.gap-15 { + gap: 15px !important; +} + +.gap-20 { + gap: 20px !important; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/utilities/misc.css b/frontend/src/app/styles/modules/utilities/misc.css new file mode 100644 index 0000000..f1b4364 --- /dev/null +++ b/frontend/src/app/styles/modules/utilities/misc.css @@ -0,0 +1,85 @@ +/* Miscellaneous Utilities */ +.pointer-help { + cursor: help !important; +} + +.pointer-none { + pointer-events: none !important; +} + +.display-none { + display: none !important; +} + +.display-block { + display: block !important; +} + +.display-inline-b { + display: inline-block !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.overflow-visible { + overflow: visible !important; +} + +.overflow-auto { + overflow: auto !important; +} + +/* Border Utilities */ +.border-none { + border: none !important; +} + +.border-1 { + border: 1px solid var(--el-border-color) !important; +} + +.rounded-4 { + border-radius: 4px !important; +} + +.rounded-6 { + border-radius: 6px !important; +} + +.rounded-8 { + border-radius: 8px !important; +} + +.rounded-10 { + border-radius: 10px !important; +} + +.rounded-12 { + border-radius: 12px !important; +} + +.rounded-16 { + border-radius: 16px !important; +} + +.rounded-full { + border-radius: 50% !important; +} + +.shadow-light { + box-shadow: var(--el-box-shadow-light) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.ls-1 { + letter-spacing: 1px !important; +} + +.ls-2 { + letter-spacing: 2px !important; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/utilities/sizing.css b/frontend/src/app/styles/modules/utilities/sizing.css new file mode 100644 index 0000000..9944b32 --- /dev/null +++ b/frontend/src/app/styles/modules/utilities/sizing.css @@ -0,0 +1,56 @@ +/* Sizing Utilities */ +.m-auto { + margin: 0 auto !important; +} + +.w-80 { + width: 80px !important; +} + +.w-100 { + width: 100px !important; +} + +.w-120 { + width: 120px !important; +} + +.w-full { + width: 100% !important; +} + +.max-w-400 { + max-width: 400px !important; +} + +.max-w-600 { + max-width: 600px !important; +} + +.max-w-100p { + max-width: 100% !important; +} + +.min-h-100 { + min-height: 100px !important; +} + +.min-h-150 { + min-height: 150px !important; +} + +.min-h-200 { + min-height: 200px !important; +} + +.min-h-400 { + min-height: 400px !important; +} + +.h-32 { + height: 32px !important; +} + +.h-auto { + height: auto !important; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/utilities/spacing.css b/frontend/src/app/styles/modules/utilities/spacing.css new file mode 100644 index 0000000..a70f593 --- /dev/null +++ b/frontend/src/app/styles/modules/utilities/spacing.css @@ -0,0 +1,97 @@ +/* Margin Utilities */ +.mt-5 { + margin-top: 5px !important; +} + +.mt-10 { + margin-top: 10px !important; +} + +.mt-15 { + margin-top: 15px !important; +} + +.mt-20 { + margin-top: 20px !important; +} + +.mt-30 { + margin-top: 30px !important; +} + +.mt-40 { + margin-top: 40px !important; +} + +.mb-5 { + margin-bottom: 5px !important; +} + +.mb-10 { + margin-bottom: 10px !important; +} + +.mb-15 { + margin-bottom: 15px !important; +} + +.mb-20 { + margin-bottom: 20px !important; +} + +.mb-30 { + margin-bottom: 30px !important; +} + +.ml-5 { + margin-left: 5px !important; +} + +.ml-10 { + margin-left: 10px !important; +} + +.mr-10 { + margin-right: 10px !important; +} + +.mx-15 { + margin-left: 15px !important; + margin-right: 15px !important; +} + +.my-10 { + margin-top: 10px !important; + margin-bottom: 10px !important; +} + +/* Padding Utilities */ +.p-0 { + padding: 0 !important; +} + +.p-5 { + padding: 5px !important; +} + +.p-10 { + padding: 10px !important; +} + +.p-16 { + padding: 16px !important; +} + +.p-20 { + padding: 20px !important; +} + +.py-10 { + padding-top: 10px !important; + padding-bottom: 10px !important; +} + +.py-20 { + padding-top: 20px !important; + padding-bottom: 20px !important; +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/utilities/typography.css b/frontend/src/app/styles/modules/utilities/typography.css new file mode 100644 index 0000000..d459ee2 --- /dev/null +++ b/frontend/src/app/styles/modules/utilities/typography.css @@ -0,0 +1,92 @@ +/* Typography Utilities */ +.text-center { + text-align: center !important; +} + +.text-left { + text-align: left !important; +} + +.text-right { + text-align: right !important; +} + +.text-12 { + font-size: 12px !important; +} + +.text-13 { + font-size: 13px !important; +} + +.text-14 { + font-size: 14px !important; +} + +.text-15 { + font-size: 15px !important; +} + +.text-16 { + font-size: 16px !important; +} + +.text-18 { + font-size: 18px !important; +} + +.text-24 { + font-size: 24px !important; +} + +.text-primary { + color: var(--el-text-color-primary) !important; +} + +.text-secondary { + color: var(--el-text-color-secondary) !important; +} + +.text-regular { + color: var(--el-text-color-regular) !important; +} + +.text-warning { + color: var(--el-color-warning) !important; +} + +.text-danger { + color: var(--el-color-danger) !important; +} + +.font-bold { + font-weight: bold !important; +} + +.font-600 { + font-weight: 600 !important; +} + +.font-mono { + font-family: monospace !important; +} + +.ls-1 { + letter-spacing: 1px !important; +} + +.ls-2 { + letter-spacing: 2px !important; +} + +.no-underline { + text-decoration: none !important; +} + +.color-inherit { + color: inherit !important; +} + +.word-break-all { + word-break: break-all !important; +} \ No newline at end of file diff --git a/frontend/src/features/auth/views/login.vue b/frontend/src/features/auth/views/login.vue index c75473e..9889ef1 100644 --- a/frontend/src/features/auth/views/login.vue +++ b/frontend/src/features/auth/views/login.vue @@ -7,12 +7,12 @@

{{ $t('auth.subtitle') }}

-
- -

{{ $t('common.loading_data') }}

+
+ +

{{ $t('common.loading_data') }}

-
+