feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
From 171854222838fa3c5f01bef96ffc9b820bc4bf00 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 08:23:39 +0800
|
||||
Subject: [PATCH 01/58] =?UTF-8?q?chore(deploy):=20=E4=BF=AE=E5=A4=8D=20Doc?=
|
||||
=?UTF-8?q?ker=20=E5=90=AF=E5=8A=A8=E9=93=BE=20+=20=E8=A1=A5=20compose=20+?=
|
||||
=?UTF-8?q?=20Gitea=20Actions?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 164 ++++++++++++++++++++++++++++++++
|
||||
docker-compose.yml | 182 ++++++++++++++++++++++++++++--------
|
||||
scripts/deploy.js | 30 +++---
|
||||
3 files changed, 323 insertions(+), 53 deletions(-)
|
||||
create mode 100644 .gitea/workflows/deploy.yml
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
new file mode 100644
|
||||
index 0000000..ca3da1f
|
||||
--- /dev/null
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -0,0 +1,164 @@
|
||||
+name: 构建并部署 VoiceHub
|
||||
+run-name: ${{ gitea.actor }} 触发部署 #${{{{ gitea.run_number }}}}
|
||||
+
|
||||
+on:
|
||||
+ push:
|
||||
+ branches:
|
||||
+ - main
|
||||
+ - master
|
||||
+ # 允许在 Gitea 页面手动点「运行工作流」
|
||||
+ workflow_dispatch:
|
||||
+ inputs:
|
||||
+ deploy_target:
|
||||
+ description: 是否在构建后自动部署到服务器
|
||||
+ type: boolean
|
||||
+ default: true
|
||||
+ required: false
|
||||
+
|
||||
+# 同一时刻只跑一个部署,避免部署竞态
|
||||
+concurrency:
|
||||
+ group: deploy-production
|
||||
+ cancel-in-progress: false
|
||||
+
|
||||
+env:
|
||||
+ # ====== 按需修改(改成你的 Gitea 域名,不要 http:// 前缀) ======
|
||||
+ GITEA_REGISTRY: gitea.example.com
|
||||
+ # ====== 一般不改 ======
|
||||
+ IMAGE_NAME: byw-voicehub
|
||||
+ DEPLOY_PATH: /opt/byw-voicehub
|
||||
+ NODE_VERSION: "22"
|
||||
+
|
||||
+jobs:
|
||||
+ # ============================================================
|
||||
+ # Job 1: 构建 & 推送 Docker 镜像到 Gitea 自带容器仓库
|
||||
+ # ============================================================
|
||||
+ build-image:
|
||||
+ name: 构建 Docker 镜像
|
||||
+ runs-on: ubuntu-latest
|
||||
+ outputs:
|
||||
+ image_tag: ${{ steps.meta.outputs.tag }}
|
||||
+ image_full: ${{ steps.meta.outputs.full }}
|
||||
+ steps:
|
||||
+ - name: 📥 拉取代码
|
||||
+ uses: actions/checkout@v4
|
||||
+ with:
|
||||
+ fetch-depth: 1
|
||||
+
|
||||
+ - name: 🔧 设置 Node.js
|
||||
+ uses: actions/setup-node@v4
|
||||
+ with:
|
||||
+ node-version: ${{ env.NODE_VERSION }}
|
||||
+ registry-url: https://registry.npmmirror.com # 国内加速,不需要可删
|
||||
+
|
||||
+ - name: 🧪 环境校验
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ node -v
|
||||
+ npm -v
|
||||
+ npm ci --no-audit --no-fund
|
||||
+
|
||||
+ - name: 🔐 登录 Gitea 容器仓库
|
||||
+ uses: docker/login-action@v3
|
||||
+ with:
|
||||
+ registry: ${{ env.GITEA_REGISTRY }}
|
||||
+ username: ${{ gitea.repository_owner }}
|
||||
+ password: ${{ secrets.REGISTRY_TOKEN || gitea.token }}
|
||||
+
|
||||
+ - name: 🏷️ 生成镜像标签
|
||||
+ id: meta
|
||||
+ run: |
|
||||
+ set -euo pipefail
|
||||
+ SHORT_SHA="${GITEA_SHA:0:7}"
|
||||
+ DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}"
|
||||
+ FULL="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
|
||||
+ LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
|
||||
+ echo "tag=${DATE_TAG}" >> "$GITEA_OUTPUT"
|
||||
+ echo "full=${FULL}" >> "$GITEA_OUTPUT"
|
||||
+ echo "latest=${LATEST}" >> "$GITEA_OUTPUT"
|
||||
+ echo "✅ 镜像 tag: ${DATE_TAG}"
|
||||
+ echo "✅ 镜像完整: ${FULL}"
|
||||
+
|
||||
+ - name: 🐳 构建并推送镜像(BuildKit 多阶段 + 缓存)
|
||||
+ uses: docker/build-push-action@v6
|
||||
+ with:
|
||||
+ context: .
|
||||
+ file: ./Dockerfile
|
||||
+ push: true
|
||||
+ tags: |
|
||||
+ ${{ steps.meta.outputs.full }}
|
||||
+ ${{ steps.meta.outputs.latest }}
|
||||
+ cache-from: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
+ cache-to: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
+ build-args: |
|
||||
+ BUILDKIT_INLINE_CACHE=1
|
||||
+ labels: |
|
||||
+ org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }}
|
||||
+ org.opencontainers.image.revision=${{ gitea.sha }}
|
||||
+
|
||||
+ # ============================================================
|
||||
+ # Job 2: SSH 到服务器拉取新镜像并重启
|
||||
+ # ============================================================
|
||||
+ deploy:
|
||||
+ name: 部署到服务器
|
||||
+ needs: build-image
|
||||
+ if: ${{ !cancelled() && !failure() && (inputs.deploy_target == true || github.event_name == 'push') }}
|
||||
+ runs-on: ubuntu-latest
|
||||
+ environment: production
|
||||
+ steps:
|
||||
+ - name: 🌐 SSH 部署到生产服务器
|
||||
+ uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||
+ with:
|
||||
+ host: ${{ secrets.DEPLOY_HOST }}
|
||||
+ username: ${{ secrets.DEPLOY_USER }}
|
||||
+ key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
+ port: ${{ secrets.DEPLOY_PORT || 22 }}
|
||||
+ script_stop: true
|
||||
+ command_timeout: 30m
|
||||
+ script: |
|
||||
+ set -euxo pipefail
|
||||
+
|
||||
+ # 1) 首次部署时自动创建目录 & 空的 docker-compose.yml/.env
|
||||
+ mkdir -p "${DEPLOY_PATH}"
|
||||
+ cd "${DEPLOY_PATH}"
|
||||
+
|
||||
+ # 2) 登录 Gitea 镜像仓库(用一个仅 pull 权限的 token 更安全)
|
||||
+ echo "${{ secrets.REGISTRY_PULL_TOKEN || gitea.token }}" \
|
||||
+ | docker login "${GITEA_REGISTRY}" -u "${{ secrets.REGISTRY_PULL_USER || gitea.repository_owner }}" --password-stdin 2>/dev/null || true
|
||||
+
|
||||
+ # 3) 拉最新镜像
|
||||
+ IMAGE_LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
|
||||
+ docker pull "$IMAGE_LATEST"
|
||||
+
|
||||
+ # 4) 写一个最小的 docker-compose.override.yml,指向刚拉的镜像
|
||||
+ # (避免改动服务器上已配置好的 docker-compose.yml 和 .env)
|
||||
+ cat > docker-compose.override.yml <<'YAML'
|
||||
+ services:
|
||||
+ voicehub:
|
||||
+ image: __REPLACE_WITH_IMAGE__
|
||||
+ YAML
|
||||
+ sed -i "s|__REPLACE_WITH_IMAGE__|${IMAGE_LATEST}|g" docker-compose.override.yml
|
||||
+
|
||||
+ # 5) 启动(如果没有 docker-compose.yml 就自动从你的仓库默认模板里下载一份)
|
||||
+ if [ ! -f docker-compose.yml ]; then
|
||||
+ echo "ℹ️ 首次部署:写入默认 docker-compose.yml 模板(请稍后在服务器上编辑 .env)"
|
||||
+ curl -fsSL "${{ gitea.server_url }}/${{ gitea.repository }}/raw/branch/main/docker-compose.yml" \
|
||||
+ -o docker-compose.yml || echo "下载失败,跳过(你需要手动上传 docker-compose.yml)"
|
||||
+ fi
|
||||
+
|
||||
+ # 6) 健康拉取 + 滚动重启(先拉好再 up,downtime 最小)
|
||||
+ docker compose pull --quiet voicehub 2>/dev/null || true
|
||||
+ docker compose up -d --remove-orphans
|
||||
+
|
||||
+ # 7) 等健康检查通过,最多等 120s
|
||||
+ for i in $(seq 1 24); do
|
||||
+ if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
|
||||
+ echo "✅ 容器健康"
|
||||
+ break
|
||||
+ fi
|
||||
+ sleep 5
|
||||
+ done
|
||||
+
|
||||
+ # 8) 清理旧镜像(只留最近 2 个 tag,省磁盘)
|
||||
+ docker image prune -af --filter "until=72h" || true
|
||||
+
|
||||
+ echo "🎉 部署完成!镜像=${{ needs.build-image.outputs.image_full }}"
|
||||
diff --git a/docker-compose.yml b/docker-compose.yml
|
||||
index c1ed74d..f89810e 100644
|
||||
--- a/docker-compose.yml
|
||||
+++ b/docker-compose.yml
|
||||
@@ -1,57 +1,159 @@
|
||||
+name: BYW VoiceHub (1.7.0.0)
|
||||
services:
|
||||
+ # ========================================================================
|
||||
+ # 应用本体 - Nuxt 4 (Node SSR)
|
||||
+ # - 使用两阶段构建 Dockerfile,构建阶段内存友好
|
||||
+ # - PREBUILT=true:告诉 scripts/deploy.js 跳过 npm install 和 nuxt build
|
||||
+ # (因为第一阶段已经 build 好了,产物在 .output/ 里)
|
||||
+ # ========================================================================
|
||||
voicehub:
|
||||
- build: .
|
||||
+ image: byw-voicehub:1.7.0.0
|
||||
+ build:
|
||||
+ context: .
|
||||
+ dockerfile: Dockerfile
|
||||
+ args:
|
||||
+ BUILDKIT_INLINE_CACHE: "1"
|
||||
+ container_name: voicehub-app
|
||||
+ restart: unless-stopped
|
||||
ports:
|
||||
- - '4456:3000'
|
||||
- environment:
|
||||
- - DATABASE_URL=postgresql://user:password@postgres:5432/voicehub
|
||||
- - JWT_SECRET=your-jwt-secret-here
|
||||
- - NODE_ENV=production
|
||||
- - NODE_OPTIONS=--max-old-space-size=512
|
||||
- - SKIP_BUILD=true
|
||||
- mem_limit: 1200m
|
||||
- mem_reservation: 256m
|
||||
- cpus: '1.0'
|
||||
- deploy:
|
||||
- resources:
|
||||
- limits:
|
||||
- cpus: '1'
|
||||
- memory: 1200M
|
||||
- reservations:
|
||||
- cpus: '0.25'
|
||||
- memory: 256M
|
||||
+ # 左边是宿主机端口(对外),右边是容器内端口(固定 3000)
|
||||
+ # 如果要套 Nginx,就只监听 127.0.0.1:4456:3000 不直接暴露外网
|
||||
+ - "4456:3000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
- restart: unless-stopped
|
||||
-
|
||||
- postgres:
|
||||
- image: postgres:15-alpine
|
||||
+ redis:
|
||||
+ condition: service_healthy
|
||||
+ env_file:
|
||||
+ # 把密码、密钥都写在项目根的 .env 里,不要直接写进这个 yaml
|
||||
+ - .env
|
||||
environment:
|
||||
- - POSTGRES_USER=user
|
||||
- - POSTGRES_PASSWORD=password
|
||||
- - POSTGRES_DB=voicehub
|
||||
- volumes:
|
||||
- - postgres_data:/var/lib/postgresql/data
|
||||
- ports:
|
||||
- - '5433:5432'
|
||||
- mem_limit: 1200m
|
||||
- mem_reservation: 256m
|
||||
- cpus: '1.0'
|
||||
+ # ===== 运行时基础(可以被 .env 覆盖) =====
|
||||
+ NODE_ENV: production
|
||||
+ PORT: 3000
|
||||
+ HOST: 0.0.0.0
|
||||
+ PREBUILT: "true"
|
||||
+ SKIP_INSTALL: "true"
|
||||
+ SKIP_BUILD: "true"
|
||||
+ NITRO_PRESET: node-server
|
||||
+
|
||||
+ # ===== 数据库连接串(.env 写了就用 .env,没写就自动拼 compose 内部 postgres 服务) =====
|
||||
+ DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-voicehub}:${POSTGRES_PASSWORD:-voicehub}@postgres:5432/${POSTGRES_DB:-voicehub}?sslmode=disable}
|
||||
+
|
||||
+ # ===== Redis 连接串(同上,自动拼内部 redis 服务) =====
|
||||
+ REDIS_URL: ${REDIS_URL:-redis://${REDIS_PASSWORD:+:${REDIS_PASSWORD}@}redis:6379/0}
|
||||
+
|
||||
+ # ===== 会话/Token(必须在 .env 里设,这里给一个占位提醒) =====
|
||||
+ JWT_SECRET: ${JWT_SECRET:-请在.env里设置JWT_SECRET用openssl rand -hex 32生成}
|
||||
+
|
||||
+ # ===== 应用默认(可在 .env 覆盖,不会覆盖 .env 里同名字段) =====
|
||||
+ NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-http://127.0.0.1:4456}
|
||||
+ NUXT_PUBLIC_SITE_TITLE: ${NUXT_PUBLIC_SITE_TITLE:-校园广播站点歌系统}
|
||||
+ NUXT_PUBLIC_DEFAULT_THEME: ${NUXT_PUBLIC_DEFAULT_THEME:-dark}
|
||||
+ NUXT_PUBLIC_ENABLED_THEMES: ${NUXT_PUBLIC_ENABLED_THEMES:-dark,light}
|
||||
+
|
||||
+ # ===== 日志/性能 =====
|
||||
+ NODE_OPTIONS: ${NODE_OPTIONS:-"--max-old-space-size=768"}
|
||||
+ # 2C4G 机器的推荐配置(.env 可覆盖);小机器 1C2G 也能跑
|
||||
+ mem_limit: ${APP_MEM_LIMIT:-1400m}
|
||||
+ mem_reservation: ${APP_MEM_RESERVATION:-384m}
|
||||
+ cpus: ${APP_CPUS:-"1.5"}
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
- cpus: '1'
|
||||
- memory: 1200M
|
||||
+ cpus: ${APP_CPUS_LIMIT:-"2"}
|
||||
+ memory: ${APP_MEM_LIMIT:-1400M}
|
||||
reservations:
|
||||
- cpus: '0.25'
|
||||
- memory: 256M
|
||||
+ cpus: ${APP_CPUS_RESERVATION:-"0.5"}
|
||||
+ memory: ${APP_MEM_RESERVATION:-384M}
|
||||
+ healthcheck:
|
||||
+ # 30s 后开始检查,每 15s 一次,连续 3 次失败就重启
|
||||
+ test:
|
||||
+ [
|
||||
+ "CMD",
|
||||
+ "node",
|
||||
+ "-e",
|
||||
+ "fetch('http://127.0.0.1:3000/').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"
|
||||
+ ]
|
||||
+ interval: 15s
|
||||
+ timeout: 5s
|
||||
+ retries: 3
|
||||
+ start_period: 30s
|
||||
+ networks:
|
||||
+ - voicehub-net
|
||||
+ volumes:
|
||||
+ # (可选)把自定义 logo/上传资源挂到宿主机
|
||||
+ - voicehub-uploads:/app/app/public/uploads:rw
|
||||
+
|
||||
+ # ========================================================================
|
||||
+ # PostgreSQL 16 - 主数据库
|
||||
+ # ========================================================================
|
||||
+ postgres:
|
||||
+ image: postgres:16-alpine
|
||||
+ container_name: voicehub-postgres
|
||||
+ restart: unless-stopped
|
||||
+ environment:
|
||||
+ POSTGRES_USER: ${POSTGRES_USER:-voicehub}
|
||||
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-请在.env里改这个密码}
|
||||
+ POSTGRES_DB: ${POSTGRES_DB:-voicehub}
|
||||
+ PGDATA: /var/lib/postgresql/data/pgdata
|
||||
+ # .env 里没设 DATABASE_URL 时,自动拼一份给 app 用
|
||||
+ # (如果 .env 里已经写了外部 Postgres,就以 .env 为准)
|
||||
healthcheck:
|
||||
- test: ['CMD-SHELL', 'pg_isready -U user -d voicehub']
|
||||
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-voicehub} -d ${POSTGRES_DB:-voicehub}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
- retries: 5
|
||||
+ retries: 10
|
||||
+ volumes:
|
||||
+ - postgres-data:/var/lib/postgresql/data
|
||||
+ # 如需从宿主机用 Navicat 连,取消下一行注释
|
||||
+ # ports:
|
||||
+ # - "127.0.0.1:5433:5432"
|
||||
+ mem_limit: ${PG_MEM_LIMIT:-1200m}
|
||||
+ mem_reservation: ${PG_MEM_RESERVATION:-256m}
|
||||
+ cpus: ${PG_CPUS:-"1.0"}
|
||||
+ networks:
|
||||
+ - voicehub-net
|
||||
+
|
||||
+ # ========================================================================
|
||||
+ # Redis 7 - 缓存 / 会话 / 限流 / 队列
|
||||
+ # ========================================================================
|
||||
+ redis:
|
||||
+ image: redis:7-alpine
|
||||
+ container_name: voicehub-redis
|
||||
restart: unless-stopped
|
||||
+ command: >
|
||||
+ sh -c "
|
||||
+ if [ -n \"$$REDIS_PASSWORD\" ]; then
|
||||
+ redis-server --appendonly yes --requirepass \"$$REDIS_PASSWORD\"
|
||||
+ else
|
||||
+ redis-server --appendonly yes
|
||||
+ fi
|
||||
+ "
|
||||
+ environment:
|
||||
+ REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||
+ healthcheck:
|
||||
+ test: ["CMD", "redis-cli", "ping"]
|
||||
+ interval: 5s
|
||||
+ timeout: 3s
|
||||
+ retries: 10
|
||||
+ volumes:
|
||||
+ - redis-data:/data
|
||||
+ mem_limit: ${REDIS_MEM_LIMIT:-384m}
|
||||
+ mem_reservation: ${REDIS_MEM_RESERVATION:-64m}
|
||||
+ cpus: ${REDIS_CPUS:-"0.5"}
|
||||
+ networks:
|
||||
+ - voicehub-net
|
||||
|
||||
volumes:
|
||||
- postgres_data:
|
||||
+ postgres-data:
|
||||
+ name: voicehub-postgres-data
|
||||
+ redis-data:
|
||||
+ name: voicehub-redis-data
|
||||
+ voicehub-uploads:
|
||||
+ name: voicehub-uploads
|
||||
+
|
||||
+networks:
|
||||
+ voicehub-net:
|
||||
+ name: voicehub-net
|
||||
+ driver: bridge
|
||||
diff --git a/scripts/deploy.js b/scripts/deploy.js
|
||||
index c20219f..9963ca4 100644
|
||||
--- a/scripts/deploy.js
|
||||
+++ b/scripts/deploy.js
|
||||
@@ -106,26 +106,26 @@ async function deploy() {
|
||||
logStep('📦', '安装依赖...')
|
||||
let installed = false
|
||||
|
||||
- // 优先尝试 pnpm install --frozen-lockfile
|
||||
- if (fileExists('pnpm-lock.yaml')) {
|
||||
- log('尝试使用 pnpm install --frozen-lockfile 安装...', 'cyan')
|
||||
- if (safeExec('pnpm install --frozen-lockfile')) {
|
||||
+ // 优先使用 npm(用户要求:禁止 pnpm)—— 优先 package-lock.json 冻结安装
|
||||
+ if (fileExists('package-lock.json')) {
|
||||
+ log('尝试使用 npm ci(基于 package-lock.json 冻结安装)...', 'cyan')
|
||||
+ if (safeExec('npm ci --no-audit --no-fund')) {
|
||||
installed = true
|
||||
- logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
|
||||
+ logSuccess('依赖安装完成 (npm ci)')
|
||||
} else {
|
||||
- logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
|
||||
+ logWarning('npm ci 失败,准备回退到 npm install...')
|
||||
}
|
||||
} else {
|
||||
- logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
|
||||
+ logWarning('未检测到 package-lock.json,直接使用 npm install...')
|
||||
}
|
||||
|
||||
- // 如果冻结锁文件安装没运行或失败,使用 pnpm install
|
||||
+ // 兜底用 npm install
|
||||
if (!installed) {
|
||||
- log('正在使用 pnpm install 安装...', 'cyan')
|
||||
- if (!safeExec('pnpm install')) {
|
||||
+ log('正在使用 npm install 安装...', 'cyan')
|
||||
+ if (!safeExec('npm install --no-audit --no-fund')) {
|
||||
throw new Error('依赖安装失败')
|
||||
}
|
||||
- logSuccess('依赖安装完成 (pnpm install)')
|
||||
+ logSuccess('依赖安装完成 (npm install)')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,10 @@ async function deploy() {
|
||||
// 4. 创建管理员账户
|
||||
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
|
||||
logStep('👤', '检查管理员账户...')
|
||||
- safeExec('pnpm run create-admin')
|
||||
+ // 优先 npm run create-admin,兜底直接用 node_modules/.bin/tsx 调用(用户禁用 pnpm)
|
||||
+ if (!safeExec('npm run create-admin')) {
|
||||
+ safeExec('./node_modules/.bin/tsx scripts/create-admin.js')
|
||||
+ }
|
||||
}
|
||||
|
||||
// 5. 构建应用
|
||||
@@ -178,7 +181,8 @@ async function deploy() {
|
||||
...process.env,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
|
||||
}
|
||||
- if (!safeExec('pnpm exec nuxt build', { env: buildEnv })) {
|
||||
+ // 用 npm run build(= nuxt build),避免用 pnpm exec
|
||||
+ if (!safeExec('npm run build', { env: buildEnv })) {
|
||||
throw new Error('应用构建失败')
|
||||
}
|
||||
logSuccess('应用构建完成')
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,811 @@
|
||||
From 5d015a3529957b98b6b4719f1fad4f4d4c0a5fd4 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 08:44:11 +0800
|
||||
Subject: [PATCH 02/58] =?UTF-8?q?ci:=20=E6=96=B0=E5=A2=9E=20Gitea=20Action?=
|
||||
=?UTF-8?q?s=20=E9=83=A8=E7=BD=B2=E6=B5=81=E6=B0=B4=E7=BA=BF=20+=20Runner/?=
|
||||
=?UTF-8?q?Deploy=20=E4=B8=80=E9=94=AE=E8=84=9A=E6=9C=AC?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 321 ++++++++++++++++++++++++++---------
|
||||
scripts/setup-deploy-user.sh | 159 +++++++++++++++++
|
||||
scripts/setup-runner.sh | 211 +++++++++++++++++++++++
|
||||
3 files changed, 609 insertions(+), 82 deletions(-)
|
||||
create mode 100644 scripts/setup-deploy-user.sh
|
||||
create mode 100644 scripts/setup-runner.sh
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index ca3da1f..e8d783b 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -1,164 +1,321 @@
|
||||
-name: 构建并部署 VoiceHub
|
||||
-run-name: ${{ gitea.actor }} 触发部署 #${{{{ gitea.run_number }}}}
|
||||
+name: 构建 & 部署 BYW VoiceHub
|
||||
+run-name: ${{ github.actor }} 触发部署 · Run ${{ github.run_number }}
|
||||
|
||||
on:
|
||||
push:
|
||||
- branches:
|
||||
- - main
|
||||
- - master
|
||||
- # 允许在 Gitea 页面手动点「运行工作流」
|
||||
+ branches: [main, master]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
deploy_target:
|
||||
- description: 是否在构建后自动部署到服务器
|
||||
+ description: 构建完成后是否自动部署到服务器
|
||||
type: boolean
|
||||
default: true
|
||||
required: false
|
||||
+ skip_build_cache:
|
||||
+ description: 跳过 BuildKit 分层缓存,强制重新构建(排错用)
|
||||
+ type: boolean
|
||||
+ default: false
|
||||
+ required: false
|
||||
|
||||
-# 同一时刻只跑一个部署,避免部署竞态
|
||||
+# 同一时刻只允许一个部署在跑,避免冲突;如果新提交进来,取消前一个未完成的
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
- cancel-in-progress: false
|
||||
+ cancel-in-progress: true
|
||||
|
||||
env:
|
||||
- # ====== 按需修改(改成你的 Gitea 域名,不要 http:// 前缀) ======
|
||||
- GITEA_REGISTRY: gitea.example.com
|
||||
- # ====== 一般不改 ======
|
||||
+ # ============================================================
|
||||
+ # 👇👇👇 这里改成你自己的 Gitea 域名(不要 https:// 前缀,不要端口默认 443/80)
|
||||
+ # 例:git.baoyiwai.com 或 gitea.example.com:3000
|
||||
+ GITEA_HOST: gitea.example.com
|
||||
+ # 👆👆👆
|
||||
+ # ============================================================
|
||||
IMAGE_NAME: byw-voicehub
|
||||
DEPLOY_PATH: /opt/byw-voicehub
|
||||
- NODE_VERSION: "22"
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
- # Job 1: 构建 & 推送 Docker 镜像到 Gitea 自带容器仓库
|
||||
+ # Job 1: 校验构建环境 & 打包产物(npm ci + build,不依赖 Docker)
|
||||
# ============================================================
|
||||
- build-image:
|
||||
- name: 构建 Docker 镜像
|
||||
+ preflight:
|
||||
+ name: Preflight 语法 & 构建检查
|
||||
runs-on: ubuntu-latest
|
||||
- outputs:
|
||||
- image_tag: ${{ steps.meta.outputs.tag }}
|
||||
- image_full: ${{ steps.meta.outputs.full }}
|
||||
+ timeout-minutes: 15
|
||||
steps:
|
||||
- - name: 📥 拉取代码
|
||||
+ - name: 📥 Checkout 代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- - name: 🔧 设置 Node.js
|
||||
+ - name: 🔧 安装 Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
- node-version: ${{ env.NODE_VERSION }}
|
||||
- registry-url: https://registry.npmmirror.com # 国内加速,不需要可删
|
||||
+ node-version: "22"
|
||||
+ cache: "npm"
|
||||
|
||||
- - name: 🧪 环境校验
|
||||
+ - name: 🧪 安装依赖(npm ci,严格按锁文件)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
- node -v
|
||||
- npm -v
|
||||
- npm ci --no-audit --no-fund
|
||||
+ # 国内加速镜像(Runner 在国外可以删掉这两行)
|
||||
+ npm config set registry https://registry.npmmirror.com || true
|
||||
+ npm config set fund false
|
||||
+ npm config set audit false
|
||||
+ npm ci
|
||||
|
||||
- - name: 🔐 登录 Gitea 容器仓库
|
||||
- uses: docker/login-action@v3
|
||||
+ - name: 🔎 检查 package.json 没有 pnpm 残留
|
||||
+ run: |
|
||||
+ set -euo pipefail
|
||||
+ if grep -qE '"packageManager"\s*:\s*"pnpm' package.json; then
|
||||
+ echo "❌ package.json 里有 pnpm 包管理器声明,和用户要求冲突"
|
||||
+ exit 1
|
||||
+ fi
|
||||
+ echo "✅ package.json 未发现 pnpm 声明"
|
||||
+
|
||||
+ - name: 🏗️ 本地 nuxt build(独立于 Docker 先验证代码是可构建的)
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ NODE_OPTIONS="--max-old-space-size=2048" npm run build
|
||||
+ echo "✅ .output/ 生成成功:"
|
||||
+ ls -la .output/ 2>/dev/null || true
|
||||
+ du -sh .output 2>/dev/null || true
|
||||
+
|
||||
+ # ============================================================
|
||||
+ # Job 2: 构建 Docker 镜像 → 推送到 Gitea 自带容器仓库
|
||||
+ # ============================================================
|
||||
+ build-image:
|
||||
+ name: 构建并推送镜像
|
||||
+ needs: preflight
|
||||
+ runs-on: ubuntu-latest
|
||||
+ timeout-minutes: 40
|
||||
+ outputs:
|
||||
+ image_tag: ${{ steps.meta.outputs.tag }}
|
||||
+ image_full: ${{ steps.meta.outputs.full }}
|
||||
+ image_latest: ${{ steps.meta.outputs.latest }}
|
||||
+ steps:
|
||||
+ - name: 📥 Checkout 代码
|
||||
+ uses: actions/checkout@v4
|
||||
with:
|
||||
- registry: ${{ env.GITEA_REGISTRY }}
|
||||
- username: ${{ gitea.repository_owner }}
|
||||
- password: ${{ secrets.REGISTRY_TOKEN || gitea.token }}
|
||||
+ fetch-depth: 1
|
||||
|
||||
- - name: 🏷️ 生成镜像标签
|
||||
+ - name: 🏷️ 计算镜像 tag(日期 + 短 SHA)
|
||||
id: meta
|
||||
+ shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
- SHORT_SHA="${GITEA_SHA:0:7}"
|
||||
+ SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
|
||||
+ SHORT_SHA="${SHA:0:7}"
|
||||
DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}"
|
||||
- FULL="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
|
||||
- LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
|
||||
- echo "tag=${DATE_TAG}" >> "$GITEA_OUTPUT"
|
||||
- echo "full=${FULL}" >> "$GITEA_OUTPUT"
|
||||
- echo "latest=${LATEST}" >> "$GITEA_OUTPUT"
|
||||
- echo "✅ 镜像 tag: ${DATE_TAG}"
|
||||
- echo "✅ 镜像完整: ${FULL}"
|
||||
-
|
||||
- - name: 🐳 构建并推送镜像(BuildKit 多阶段 + 缓存)
|
||||
+ IMG_FULL="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
|
||||
+ IMG_LATEST="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:latest"
|
||||
+ echo "tag=${DATE_TAG}" >> "$GITHUB_OUTPUT"
|
||||
+ echo "full=${IMG_FULL}" >> "$GITHUB_OUTPUT"
|
||||
+ echo "latest=${IMG_LATEST}" >> "$GITHUB_OUTPUT"
|
||||
+ echo "======================================"
|
||||
+ echo "🪪 镜像版本 tag : ${DATE_TAG}"
|
||||
+ echo "🪪 镜像完整名 : ${IMG_FULL}"
|
||||
+ echo "🪪 镜像 latest : ${IMG_LATEST}"
|
||||
+ echo "======================================"
|
||||
+
|
||||
+ - name: 🔧 启用 QEMU(跨架构小概率用得到,保险)
|
||||
+ uses: docker/setup-qemu-action@v3
|
||||
+ with:
|
||||
+ platforms: linux/amd64
|
||||
+
|
||||
+ - name: 🔧 启用 Docker Buildx
|
||||
+ uses: docker/setup-buildx-action@v3
|
||||
+ with:
|
||||
+ install: true
|
||||
+
|
||||
+ - name: 🔐 登录 Gitea 容器仓库(Packages)
|
||||
+ uses: docker/login-action@v3
|
||||
+ with:
|
||||
+ registry: ${{ env.GITEA_HOST }}
|
||||
+ # Secrets 里 REGISTRY_USER 没填就默认用触发构建的用户
|
||||
+ username: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||
+ # Secrets 里 REGISTRY_TOKEN 优先,没填就用 Gitea Actions 自动注入的仓库级 token
|
||||
+ password: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||
+
|
||||
+ - name: 🐳 构建 & 推送(BuildKit 分层缓存,skip_build_cache=true 时禁用)
|
||||
+ id: build_push
|
||||
uses: docker/build-push-action@v6
|
||||
+ env:
|
||||
+ CACHE_REF: ${{ env.GITEA_HOST }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
+ platforms: linux/amd64
|
||||
push: true
|
||||
+ provenance: false
|
||||
tags: |
|
||||
${{ steps.meta.outputs.full }}
|
||||
${{ steps.meta.outputs.latest }}
|
||||
- cache-from: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
- cache-to: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
+ NODE_ENV=production
|
||||
+ cache-from: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0}', env.CACHE_REF) }}
|
||||
+ cache-to: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0},mode=max', env.CACHE_REF) }}
|
||||
labels: |
|
||||
- org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }}
|
||||
- org.opencontainers.image.revision=${{ gitea.sha }}
|
||||
+ org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
+ org.opencontainers.image.revision=${{ github.sha }}
|
||||
+ org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
|
||||
+
|
||||
+ - name: 🧮 镜像体积 & 层信息(日志里看一下,避免镜像越来越大)
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ docker pull "${{ steps.meta.outputs.full }}" || true
|
||||
+ docker image inspect "${{ steps.meta.outputs.full }}" --format='{{.Size}}' 2>/dev/null \
|
||||
+ | awk '{printf "✅ 镜像体积:%.2f MB\n", $1/1024/1024}' || true
|
||||
+ docker history --no-trunc "${{ steps.meta.outputs.full }}" 2>/dev/null | head -20 || true
|
||||
|
||||
# ============================================================
|
||||
- # Job 2: SSH 到服务器拉取新镜像并重启
|
||||
+ # Job 3: SSH 到服务器 → 拉新镜像 → 滚动重启
|
||||
# ============================================================
|
||||
deploy:
|
||||
- name: 部署到服务器
|
||||
+ name: 部署到生产服务器
|
||||
needs: build-image
|
||||
- if: ${{ !cancelled() && !failure() && (inputs.deploy_target == true || github.event_name == 'push') }}
|
||||
+ # push 事件默认自动部署;手动触发时看 checkbox
|
||||
+ if: ${{ !cancelled() && !failure() && (github.event_name == 'push' || inputs.deploy_target == true) }}
|
||||
runs-on: ubuntu-latest
|
||||
- environment: production
|
||||
+ timeout-minutes: 25
|
||||
+ environment:
|
||||
+ name: production
|
||||
+ url: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
|
||||
steps:
|
||||
- - name: 🌐 SSH 部署到生产服务器
|
||||
- uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||
+ - name: ⚠️ 检查部署所需 Secrets 是否都配置了
|
||||
+ shell: bash
|
||||
+ run: |
|
||||
+ set -euo pipefail
|
||||
+ MISSING=()
|
||||
+ [ -n "${{ secrets.DEPLOY_HOST }}" ] || MISSING+=("DEPLOY_HOST")
|
||||
+ [ -n "${{ secrets.DEPLOY_USER }}" ] || MISSING+=("DEPLOY_USER")
|
||||
+ [ -n "${{ secrets.DEPLOY_SSH_KEY }}" ] || MISSING+=("DEPLOY_SSH_KEY")
|
||||
+ if [ ${#MISSING[@]} -gt 0 ]; then
|
||||
+ echo "❌ 缺少以下 Repository Secrets:${MISSING[*]}"
|
||||
+ echo "👉 去 Gitea 仓库 → Settings → Secrets and variables → Actions 里添加"
|
||||
+ exit 1
|
||||
+ fi
|
||||
+ echo "✅ 部署 Secrets 配置完整"
|
||||
+
|
||||
+ - name: 🚀 一键滚动部署(SSH)
|
||||
+ uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
+ port: ${{ secrets.DEPLOY_PORT || 22 }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
- port: ${{ secrets.DEPLOY_PORT || 22 }}
|
||||
- script_stop: true
|
||||
+ # 部署可能慢,给足 30 分钟
|
||||
command_timeout: 30m
|
||||
+ # 出错立刻停,不继续往下跑
|
||||
+ script_stop: true
|
||||
script: |
|
||||
set -euxo pipefail
|
||||
|
||||
- # 1) 首次部署时自动创建目录 & 空的 docker-compose.yml/.env
|
||||
- mkdir -p "${DEPLOY_PATH}"
|
||||
- cd "${DEPLOY_PATH}"
|
||||
+ # ---------- 0. 基本参数 ----------
|
||||
+ IMAGE_NEW="${{ needs.build-image.outputs.image_full }}"
|
||||
+ IMAGE_LATEST="${{ needs.build-image.outputs.image_latest }}"
|
||||
+ DEPLOY_DIR="${DEPLOY_PATH}"
|
||||
+ COMPOSE="docker compose"
|
||||
+
|
||||
+ echo "=============================================="
|
||||
+ echo "🚢 本次部署镜像: ${IMAGE_NEW}"
|
||||
+ echo "🗂️ 部署目录 : ${DEPLOY_DIR}"
|
||||
+ echo "👤 执行用户 : $(whoami)"
|
||||
+ echo "=============================================="
|
||||
|
||||
- # 2) 登录 Gitea 镜像仓库(用一个仅 pull 权限的 token 更安全)
|
||||
- echo "${{ secrets.REGISTRY_PULL_TOKEN || gitea.token }}" \
|
||||
- | docker login "${GITEA_REGISTRY}" -u "${{ secrets.REGISTRY_PULL_USER || gitea.repository_owner }}" --password-stdin 2>/dev/null || true
|
||||
+ # ---------- 1. 确保部署目录存在 ----------
|
||||
+ mkdir -p "${DEPLOY_DIR}"
|
||||
+ cd "${DEPLOY_DIR}"
|
||||
|
||||
- # 3) 拉最新镜像
|
||||
- IMAGE_LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest"
|
||||
- docker pull "$IMAGE_LATEST"
|
||||
+ # ---------- 2. 登录 Gitea 容器仓库(用 pull token,最小权限) ----------
|
||||
+ if [ -n "${{ secrets.REGISTRY_PULL_TOKEN }}" ] && [ -n "${{ secrets.REGISTRY_PULL_USER }}" ]; then
|
||||
+ echo "🔐 使用专用 pull 凭证登录 Gitea Container Registry"
|
||||
+ echo "${{ secrets.REGISTRY_PULL_TOKEN }}" \
|
||||
+ | docker login "${GITEA_HOST}" \
|
||||
+ -u "${{ secrets.REGISTRY_PULL_USER }}" \
|
||||
+ --password-stdin 2>/dev/null || true
|
||||
+ fi
|
||||
+
|
||||
+ # ---------- 3. 先拉镜像(提前下好,后面 up 不会卡) ----------
|
||||
+ echo "⬇️ 开始拉取新镜像 ${IMAGE_NEW} ..."
|
||||
+ docker pull "${IMAGE_NEW}"
|
||||
+ docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
||||
+ echo "✅ 新镜像拉取完成,并已打 latest tag"
|
||||
|
||||
- # 4) 写一个最小的 docker-compose.override.yml,指向刚拉的镜像
|
||||
- # (避免改动服务器上已配置好的 docker-compose.yml 和 .env)
|
||||
- cat > docker-compose.override.yml <<'YAML'
|
||||
+ # ---------- 4. 生成 / 更新 docker-compose.override.yml ----------
|
||||
+ # 指向新的精确 tag(不是 latest,出问题能快速回滚)
|
||||
+ cat > docker-compose.override.yml <<YAML
|
||||
services:
|
||||
voicehub:
|
||||
- image: __REPLACE_WITH_IMAGE__
|
||||
+ image: ${IMAGE_NEW}
|
||||
YAML
|
||||
- sed -i "s|__REPLACE_WITH_IMAGE__|${IMAGE_LATEST}|g" docker-compose.override.yml
|
||||
+ echo "📝 docker-compose.override.yml 已写入,指定镜像=${IMAGE_NEW}"
|
||||
|
||||
- # 5) 启动(如果没有 docker-compose.yml 就自动从你的仓库默认模板里下载一份)
|
||||
+ # ---------- 5. 没有 docker-compose.yml? 自动从仓库下一份模板 ----------
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
- echo "ℹ️ 首次部署:写入默认 docker-compose.yml 模板(请稍后在服务器上编辑 .env)"
|
||||
- curl -fsSL "${{ gitea.server_url }}/${{ gitea.repository }}/raw/branch/main/docker-compose.yml" \
|
||||
- -o docker-compose.yml || echo "下载失败,跳过(你需要手动上传 docker-compose.yml)"
|
||||
+ echo "ℹ️ 未发现 docker-compose.yml,自动从仓库模板下载"
|
||||
+ TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY:-${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
|
||||
+ echo " ↓ URL=${TEMPLATE_URL}"
|
||||
+ curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
|
||||
+ || (
|
||||
+ echo "⚠️ 自动下载模板失败,跳过(你需要手动上传 docker-compose.yml 和 .env)"
|
||||
+ # 不 exit,因为用户可能已经有一套自定义部署
|
||||
+ )
|
||||
+ fi
|
||||
+
|
||||
+ # ---------- 6. 没有 .env? 给一份占位(方便首次部署发现问题) ----------
|
||||
+ if [ ! -f .env ]; then
|
||||
+ echo "================================================================="
|
||||
+ echo "⚠️ WARNING:${DEPLOY_DIR}/.env 不存在!!!"
|
||||
+ echo "👉 第一次部署请先在服务器上创建 .env(至少填 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
|
||||
+ echo "👉 参考仓库根的 .env.example 或部署文档"
|
||||
+ echo "================================================================="
|
||||
+ # 不退出,因为 compose 会用 environment 默认值尝试启动(只是大概率连不上 DB)
|
||||
fi
|
||||
|
||||
- # 6) 健康拉取 + 滚动重启(先拉好再 up,downtime 最小)
|
||||
- docker compose pull --quiet voicehub 2>/dev/null || true
|
||||
- docker compose up -d --remove-orphans
|
||||
+ # ---------- 7. 滚动启动(如果只有 voicehub 单服务,不会动 postgres/redis) ----------
|
||||
+ echo "🔄 docker compose up -d(只重建变更过的 voicehub)..."
|
||||
+ ${COMPOSE} up -d --remove-orphans
|
||||
|
||||
- # 7) 等健康检查通过,最多等 120s
|
||||
- for i in $(seq 1 24); do
|
||||
- if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
|
||||
- echo "✅ 容器健康"
|
||||
+ # ---------- 8. 等待应用变 healthy,最多 2 分钟 ----------
|
||||
+ MAX_WAIT=24 # 24 × 5s = 2min
|
||||
+ WAITED=0
|
||||
+ echo "⏳ 等待容器 healthcheck 通过(最多 120s)..."
|
||||
+ while [ ${WAITED} -lt ${MAX_WAIT} ]; do
|
||||
+ STATUS="$(${COMPOSE} ps --format json 2>/dev/null || true)"
|
||||
+ if echo "${STATUS}" | grep -q '"Health":"healthy"'; then
|
||||
+ echo "✅ voicehub 容器健康检查通过"
|
||||
break
|
||||
fi
|
||||
+ # 同时也检查是否已经 running 但健康检查还没跑完(防止 healthcheck 写太严误判)
|
||||
+ if echo "${STATUS}" | grep -q '"State":"running"'; then
|
||||
+ :
|
||||
+ else
|
||||
+ # 连 running 都不是 → 直接打印日志末尾 60 行帮忙排错
|
||||
+ echo "⚠️ voicehub 还没 running,当前状态:"
|
||||
+ ${COMPOSE} ps || true
|
||||
+ fi
|
||||
sleep 5
|
||||
+ WAITED=$((WAITED + 1))
|
||||
done
|
||||
+ if [ ${WAITED} -ge ${MAX_WAIT} ]; then
|
||||
+ echo "⚠️ 等待超时,下面是最近 120 行 voicehub 日志:"
|
||||
+ ${COMPOSE} logs --tail=120 voicehub || true
|
||||
+ exit 1
|
||||
+ fi
|
||||
|
||||
- # 8) 清理旧镜像(只留最近 2 个 tag,省磁盘)
|
||||
+ # ---------- 9. 清理旧镜像(保留最近 2 个 tag + latest) ----------
|
||||
+ echo "🧹 清理 3 天前的旧镜像 / 悬空层,防止磁盘爆"
|
||||
docker image prune -af --filter "until=72h" || true
|
||||
+ docker builder prune -af || true
|
||||
|
||||
- echo "🎉 部署完成!镜像=${{ needs.build-image.outputs.image_full }}"
|
||||
+ # ---------- 10. 回滚指引 ----------
|
||||
+ echo ""
|
||||
+ echo "================================================================="
|
||||
+ echo "🎉 部署成功!访问: ${{ secrets.NUXT_PUBLIC_SITE_URL || '请查看你的站点 URL' }}"
|
||||
+ echo "🔁 如需紧急回滚到上一个版本,在服务器执行:"
|
||||
+ echo " cd ${DEPLOY_DIR}"
|
||||
+ echo " # 1) 先看有哪些 tag:"
|
||||
+ echo " docker images | grep ${IMAGE_NAME} | head -5"
|
||||
+ echo " # 2) 选一个老 tag 填到 override:"
|
||||
+ echo " echo 'services:\n voicehub:\n image: ${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:<旧TAG>' > docker-compose.override.yml"
|
||||
+ echo " docker compose up -d"
|
||||
+ echo "================================================================="
|
||||
diff --git a/scripts/setup-deploy-user.sh b/scripts/setup-deploy-user.sh
|
||||
new file mode 100644
|
||||
index 0000000..38ac0b1
|
||||
--- /dev/null
|
||||
+++ b/scripts/setup-deploy-user.sh
|
||||
@@ -0,0 +1,159 @@
|
||||
+#!/usr/bin/env bash
|
||||
+# ============================================================================
|
||||
+# setup-deploy-user.sh
|
||||
+# 在「生产服务器」上一键做好 CI 自动部署所需的系统配置:
|
||||
+# 1) 新建 deploy 用户(低权限,禁止密码登录,只能 key 登录)
|
||||
+# 2) 生成 ed25519 SSH 密钥对(私钥你粘去 Gitea Secrets DEPLOY_SSH_KEY)
|
||||
+# 3) 加入 docker 组,允许 docker compose 操作
|
||||
+# 4) 给 /opt/byw-voicehub 目录读写权限(setfacl)
|
||||
+# 5) 打印私钥内容 & 公钥指纹,便于你检查和复制
|
||||
+#
|
||||
+# 用法(在生产服务器上,sudo 运行):
|
||||
+# sudo bash scripts/setup-deploy-user.sh [--deploy-dir /opt/byw-voicehub]
|
||||
+# ============================================================================
|
||||
+set -euo pipefail
|
||||
+
|
||||
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[1;36m'; NC='\033[0m'
|
||||
+info(){ echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
+warn(){ echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
+err(){ echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
+ok(){ echo -e "${CYAN}[ OK ]${NC} $*"; }
|
||||
+
|
||||
+DEPLOY_DIR="/opt/byw-voicehub"
|
||||
+DEPLOY_USER="deploy"
|
||||
+while [[ $# -gt 0 ]]; do
|
||||
+ case "$1" in
|
||||
+ --deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
+ --user) DEPLOY_USER="$2"; shift 2 ;;
|
||||
+ -h|--help)
|
||||
+ sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
|
||||
+ exit 0
|
||||
+ ;;
|
||||
+ *) err "未知参数 $1(--help 查看帮助)" ;;
|
||||
+ esac
|
||||
+done
|
||||
+
|
||||
+[[ $EUID -eq 0 ]] || err "请用 sudo 运行本脚本"
|
||||
+command -v docker >/dev/null 2>&1 || err "先装完 Docker 再跑本脚本(路径一的教程里有)"
|
||||
+command -v setfacl >/dev/null 2>&1 || {
|
||||
+ warn "没装 acl 工具,现在 apt install"
|
||||
+ apt-get update && apt-get install -y acl
|
||||
+}
|
||||
+
|
||||
+# -------- 1. 建用户 --------
|
||||
+if ! id -u "${DEPLOY_USER}" >/dev/null 2>&1; then
|
||||
+ useradd --create-home --shell /usr/bin/bash --disabled-password "${DEPLOY_USER}"
|
||||
+ ok "新建用户:${DEPLOY_USER}(无密码,仅 SSH key 登录)"
|
||||
+else
|
||||
+ info "用户 ${DEPLOY_USER} 已存在,跳过创建"
|
||||
+fi
|
||||
+
|
||||
+# -------- 2. 加入 docker 组 --------
|
||||
+usermod -aG docker "${DEPLOY_USER}"
|
||||
+ok "${DEPLOY_USER} 加入 docker 组(有权限执行 docker compose)"
|
||||
+
|
||||
+# -------- 3. 生成 SSH 密钥(无密码) --------
|
||||
+SSH_DIR="/home/${DEPLOY_USER}/.ssh"
|
||||
+mkdir -p "${SSH_DIR}"
|
||||
+chmod 700 "${SSH_DIR}"
|
||||
+chown "${DEPLOY_USER}:${DEPLOY_USER}" "${SSH_DIR}"
|
||||
+
|
||||
+KEY_PATH="${SSH_DIR}/id_ed25519"
|
||||
+if [[ ! -f "${KEY_PATH}" ]]; then
|
||||
+ sudo -u "${DEPLOY_USER}" ssh-keygen -t ed25519 \
|
||||
+ -C "gitea-actions-deploy-$(hostname)" \
|
||||
+ -f "${KEY_PATH}" \
|
||||
+ -N "" >/dev/null
|
||||
+ ok "SSH ed25519 密钥对生成:${KEY_PATH}"
|
||||
+else
|
||||
+ info "密钥对已存在,跳过生成:${KEY_PATH}"
|
||||
+fi
|
||||
+
|
||||
+# 公钥加入 authorized_keys(允许持对应私钥的人以 deploy 用户 ssh 登录)
|
||||
+AUTH_KEYS="${SSH_DIR}/authorized_keys"
|
||||
+touch "${AUTH_KEYS}"
|
||||
+PUBKEY_CONTENT="$(cat "${KEY_PATH}.pub")"
|
||||
+if ! grep -qF "${PUBKEY_CONTENT}" "${AUTH_KEYS}" 2>/dev/null; then
|
||||
+ echo "${PUBKEY_CONTENT}" >> "${AUTH_KEYS}"
|
||||
+ ok "公钥已写入 authorized_keys"
|
||||
+fi
|
||||
+chmod 600 "${AUTH_KEYS}"
|
||||
+chmod 644 "${KEY_PATH}.pub"
|
||||
+chmod 600 "${KEY_PATH}"
|
||||
+chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "${SSH_DIR}"
|
||||
+
|
||||
+# -------- 4. sshd 加固(只允许公钥登录;别让 root 密码登录;deploy 禁止转发)--------
|
||||
+SSHD_SNIPPET="/etc/ssh/sshd_config.d/99-deploy-user.conf"
|
||||
+if [[ ! -f "${SSHD_SNIPPET}" ]]; then
|
||||
+ mkdir -p /etc/ssh/sshd_config.d
|
||||
+ cat > "${SSHD_SNIPPET}" <<SSHD
|
||||
+# 针对 deploy 用户:仅允许公钥登录,禁用 TCP 转发等
|
||||
+Match User ${DEPLOY_USER}
|
||||
+ PasswordAuthentication no
|
||||
+ PubkeyAuthentication yes
|
||||
+ PermitTTY yes
|
||||
+ AllowTcpForwarding no
|
||||
+ X11Forwarding no
|
||||
+SSHD
|
||||
+ systemctl reload sshd 2>/dev/null || service ssh reload || true
|
||||
+ ok "sshd 配置已加固(deploy 只允许公钥登录,并禁用端口转发)"
|
||||
+fi
|
||||
+
|
||||
+# -------- 5. 创建部署目录并给 deploy 权限 --------
|
||||
+mkdir -p "${DEPLOY_DIR}"
|
||||
+# 既设组权限,又加 ACL,确保后续新建文件也有
|
||||
+setfacl -R -m "u:${DEPLOY_USER}:rwX" "${DEPLOY_DIR}"
|
||||
+setfacl -R -d -m "u:${DEPLOY_USER}:rwX" "${DEPLOY_DIR}"
|
||||
+ok "部署目录 ${DEPLOY_DIR} 已给 ${DEPLOY_USER} 完整读写权限(+ 新建文件继承权限)"
|
||||
+
|
||||
+# 如果目录里有 compose/环境文件,顺便改一下属组,避免「root 建的文件 deploy 改不了」
|
||||
+chgrp -R docker "${DEPLOY_DIR}" 2>/dev/null || true
|
||||
+chmod -R g+rwX "${DEPLOY_DIR}" 2>/dev/null || true
|
||||
+
|
||||
+# -------- 6. 验证 docker 是否真能用(不报错)--------
|
||||
+DOCKER_RC=0
|
||||
+sudo -u "${DEPLOY_USER}" docker ps >/dev/null 2>&1 || DOCKER_RC=$?
|
||||
+if [[ ${DOCKER_RC} -eq 0 ]]; then
|
||||
+ ok "✅ ${DEPLOY_USER} 可以执行 docker 命令"
|
||||
+else
|
||||
+ warn "⚠️ ${DEPLOY_USER} 暂时跑不了 docker(可能需要重新登录一次刷新组),部署时不会有影响(但验证时会再触发一次)"
|
||||
+fi
|
||||
+
|
||||
+# -------- 7. 打印信息 --------
|
||||
+FINGERPRINT="$(ssh-keygen -l -f "${KEY_PATH}.pub" 2>/dev/null || true)"
|
||||
+PUBKEY_CONTENT="$(cat "${KEY_PATH}.pub")"
|
||||
+PRIVATE_CONTENT="$(cat "${KEY_PATH}")"
|
||||
+
|
||||
+clear
|
||||
+echo -e "${GREEN}================================================================================${NC}"
|
||||
+echo -e "${GREEN} 🎉 部署用户 & SSH 密钥 配置完成!${NC}"
|
||||
+echo -e "${GREEN}================================================================================${NC}"
|
||||
+echo -e " 👤 部署用户名 :${CYAN}${DEPLOY_USER}${NC}"
|
||||
+echo -e " 📂 部署目录 :${CYAN}${DEPLOY_DIR}${NC}"
|
||||
+echo -e " 🔑 私钥文件路径 :${CYAN}${KEY_PATH}${NC}"
|
||||
+echo -e " 🧾 公钥指纹 :${FINGERPRINT}"
|
||||
+echo -e " 🧾 公钥内容(方便核对):"
|
||||
+echo -e " ${PUBKEY_CONTENT}"
|
||||
+echo ""
|
||||
+echo -e "${YELLOW}================================================================================${NC}"
|
||||
+echo -e "${YELLOW} ⚠️ 下面这一整段是【私钥】,完整复制(从 BEGIN 到 END 包括两行)到:${NC}"
|
||||
+echo -e "${YELLOW} Gitea 仓库 → Settings → Secrets and variables → Actions → New Secret${NC}"
|
||||
+echo -e "${YELLOW} Name = DEPLOY_SSH_KEY${NC}"
|
||||
+echo -e "${YELLOW} Value = 粘贴下面一整段(包括 -----BEGIN/END-----)${NC}"
|
||||
+echo -e "${YELLOW}================================================================================${NC}"
|
||||
+echo ""
|
||||
+echo "${PRIVATE_CONTENT}"
|
||||
+echo ""
|
||||
+echo -e "${YELLOW}================================================================================${NC}"
|
||||
+echo -e "${YELLOW} ⚠️ 上面就是 DEPLOY_SSH_KEY,不要在任何地方贴到别的文档或聊天。看完 Ctrl+L 清屏。${NC}"
|
||||
+echo -e "${YELLOW}================================================================================${NC}"
|
||||
+echo ""
|
||||
+echo -e "${CYAN}👉 同时你还需要补以下几个 Repository Secrets(都在同一页添加):${NC}"
|
||||
+echo -e " • DEPLOY_HOST = 你的服务器公网 IP 或域名"
|
||||
+echo -e " • DEPLOY_PORT = SSH 端口(一般 22)"
|
||||
+echo -e " • DEPLOY_USER = ${DEPLOY_USER}(就是这个用户名)"
|
||||
+echo -e " • REGISTRY_TOKEN = 你的 Gitea 个人访问令牌(Package 读写权限)"
|
||||
+echo -e " • REGISTRY_PULL_USER = 同 REGISTRY_USER(服务器上拉镜像用,一般和 REGISTRY_USER 一样)"
|
||||
+echo -e " • REGISTRY_PULL_TOKEN= 上面令牌同一个或新建一个仅 pull 权限的更安全"
|
||||
+echo -e " • NUXT_PUBLIC_SITE_URL = https://你的域名.com(用来显示部署成功后的跳转按钮)"
|
||||
+echo ""
|
||||
diff --git a/scripts/setup-runner.sh b/scripts/setup-runner.sh
|
||||
new file mode 100644
|
||||
index 0000000..bc424e7
|
||||
--- /dev/null
|
||||
+++ b/scripts/setup-runner.sh
|
||||
@@ -0,0 +1,211 @@
|
||||
+#!/usr/bin/env bash
|
||||
+# ============================================================================
|
||||
+# setup-runner.sh —— 一键安装并注册 Gitea Actions Runner(act_runner)
|
||||
+#
|
||||
+# 使用方法:
|
||||
+# ① 先在你的 Gitea 仓库页面 → Settings → Actions → Runners → 「Create Runner」
|
||||
+# 选「Linux + amd64」,它会给你一条注册命令,里面有一串 registration token,
|
||||
+# 格式像: GFxxxxxxxxxxxxxxxxxxxx ,复制它。
|
||||
+# ② 再在你要跑 Runner 的机器上执行:
|
||||
+# curl -fsSL https://<你的-gitea>/<仓库>/-/raw/branch/main/scripts/setup-runner.sh \
|
||||
+# | sudo bash -s -- --gitea https://<你的-gitea> --token GFxxxxxxxxxxxxxxxxxxxx
|
||||
+#
|
||||
+# 或者直接本地执行:
|
||||
+# sudo bash scripts/setup-runner.sh \
|
||||
+# --gitea https://git.baoyiwai.com \
|
||||
+# --token GFxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
+# ============================================================================
|
||||
+set -euo pipefail
|
||||
+
|
||||
+# ---------- 颜色 ----------
|
||||
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
+info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
+warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
+err() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
+
|
||||
+# ---------- 解析参数 ----------
|
||||
+GITEA_URL=""
|
||||
+REG_TOKEN=""
|
||||
+RUNNER_NAME="runner-$(hostname | tr '[:upper:]' '[:lower:]')"
|
||||
+RUNNER_USER="gitrunner"
|
||||
+INSTALL_DIR="/opt/act_runner"
|
||||
+
|
||||
+while [[ $# -gt 0 ]]; do
|
||||
+ case "$1" in
|
||||
+ --gitea) GITEA_URL="${2}"; shift 2 ;;
|
||||
+ --token) REG_TOKEN="${2}"; shift 2 ;;
|
||||
+ --name) RUNNER_NAME="${2}"; shift 2 ;;
|
||||
+ --user) RUNNER_USER="${2}"; shift 2 ;;
|
||||
+ --dir) INSTALL_DIR="${2}"; shift 2 ;;
|
||||
+ -h|--help)
|
||||
+ sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||
+ exit 0
|
||||
+ ;;
|
||||
+ *) err "未知参数:$1(--help 查看帮助)" ;;
|
||||
+ esac
|
||||
+done
|
||||
+
|
||||
+[[ -n "${GITEA_URL}" ]] || err "必须传 --gitea https://你的-gitea-地址"
|
||||
+[[ -n "${REG_TOKEN}" ]] || err "必须传 --token GF……(Gitea 页面里拿的注册 token)"
|
||||
+[[ $EUID -eq 0 ]] || err "请用 sudo 运行本脚本(需要写 /opt / /etc/systemd/system)"
|
||||
+
|
||||
+# 去掉末尾 /
|
||||
+GITEA_URL="${GITEA_URL%/}"
|
||||
+
|
||||
+# ---------- 0. 架构检测 ----------
|
||||
+ARCH="$(uname -m)"
|
||||
+case "${ARCH}" in
|
||||
+ x86_64|amd64) PLATFORM="linux-amd64" ;;
|
||||
+ aarch64|arm64) PLATFORM="linux-arm64" ;;
|
||||
+ armv7l) PLATFORM="linux-arm" ;;
|
||||
+ *) err "暂不支持的架构:${ARCH}(需要 x86_64 / aarch64 / armv7)" ;;
|
||||
+esac
|
||||
+info "检测到架构:${ARCH} → 下载版本:${PLATFORM}"
|
||||
+
|
||||
+# ---------- 1. 安装依赖:docker(Runner 要用 Docker-in-Docker 执行 buildx 等)----------
|
||||
+if ! command -v docker >/dev/null 2>&1; then
|
||||
+ info "检测到没装 Docker,现在用官方一键脚本安装(国内自动换清华源)..."
|
||||
+ curl -fsSL https://get.docker.com -o /tmp/get-docker.sh || err "下载 docker-install 脚本失败"
|
||||
+ if curl -sI https://mirrors.tuna.tsinghua.edu.cn >/dev/null; then
|
||||
+ bash /tmp/get-docker.sh --mirror Aliyun || bash /tmp/get-docker.sh
|
||||
+ else
|
||||
+ bash /tmp/get-docker.sh
|
||||
+ fi
|
||||
+ systemctl enable --now docker
|
||||
+ info "✅ Docker 安装完成:$(docker --version)"
|
||||
+else
|
||||
+ info "✅ Docker 已存在:$(docker --version)"
|
||||
+fi
|
||||
+
|
||||
+# ---------- 2. 建运行用户 gitrunner,加入 docker 组 ----------
|
||||
+if ! id -u "${RUNNER_USER}" >/dev/null 2>&1; then
|
||||
+ useradd -r -s /usr/sbin/nologin -d "${INSTALL_DIR}" "${RUNNER_USER}"
|
||||
+ info "✅ 新建系统用户 ${RUNNER_USER}"
|
||||
+else
|
||||
+ info "ℹ️ 用户 ${RUNNER_USER} 已存在,跳过创建"
|
||||
+fi
|
||||
+usermod -aG docker "${RUNNER_USER}"
|
||||
+info "✅ 把 ${RUNNER_USER} 加入 docker 组(允许操作 Docker daemon)"
|
||||
+
|
||||
+mkdir -p "${INSTALL_DIR}"
|
||||
+chown -R "${RUNNER_USER}:${RUNNER_USER}" "${INSTALL_DIR}"
|
||||
+
|
||||
+# ---------- 3. 下载最新 act_runner ----------
|
||||
+info "⬇️ 正在拉取 act_runner 最新 release 元数据..."
|
||||
+LATEST_JSON="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/act_runner/releases/latest || true)"
|
||||
+if [[ -z "${LATEST_JSON}" ]]; then
|
||||
+ warn "gitea.com 不通,改用硬编码版本 0.2.10(你之后可手动升级)"
|
||||
+ VERSION="0.2.10"
|
||||
+else
|
||||
+ VERSION="$(echo "${LATEST_JSON}" | grep -oE '"tag_name":\s*"[^"]+"' | head -1 | cut -d'"' -f4 | sed 's/^v//')"
|
||||
+ [[ -n "${VERSION}" ]] || VERSION="0.2.10"
|
||||
+fi
|
||||
+BIN_URL="https://dl.gitea.com/act_runner/${VERSION}/act_runner-${VERSION}-${PLATFORM}"
|
||||
+info "⬇️ 下载 act_runner v${VERSION}:${BIN_URL}"
|
||||
+curl -fsSL "${BIN_URL}" -o /usr/local/bin/act_runner \
|
||||
+ || err "下载失败。请手动打开 ${BIN_URL} 看是否 404,然后换个版本"
|
||||
+chmod +x /usr/local/bin/act_runner
|
||||
+info "✅ act_runner 已安装到 /usr/local/bin,版本:$(act_runner --version 2>&1 || true)"
|
||||
+
|
||||
+# ---------- 4. 注册 Runner ----------
|
||||
+cd "${INSTALL_DIR}"
|
||||
+info "🔗 开始注册 Runner 到 ${GITEA_URL}(名称:${RUNNER_NAME})"
|
||||
+
|
||||
+# 切到 gitrunner 用户注册,避免 root 跑(安全)
|
||||
+rm -f .runner 2>/dev/null || true
|
||||
+sudo -u "${RUNNER_USER}" env HOME="${INSTALL_DIR}" \
|
||||
+ act_runner register \
|
||||
+ --instance "${GITEA_URL}" \
|
||||
+ --token "${REG_TOKEN}" \
|
||||
+ --name "${RUNNER_NAME}" \
|
||||
+ --labels "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" \
|
||||
+ --no-interactive \
|
||||
+ || err "注册失败!常见原因:\n 1) token 不对(复制时别带空格)\n 2) Runner 机器出不了网到 ${GITEA_URL}\n 3) Gitea 站点时间和这台机器差太远(证书验证失败)"
|
||||
+
|
||||
+chown "${RUNNER_USER}:${RUNNER_USER}" .runner 2>/dev/null || true
|
||||
+
|
||||
+# ---------- 5. 生成配置文件(建议开启 docker.host 特权)----------
|
||||
+CONFIG_FILE="${INSTALL_DIR}/config.yaml"
|
||||
+if [[ ! -f "${CONFIG_FILE}" ]]; then
|
||||
+ cat > "${CONFIG_FILE}" <<'YAML'
|
||||
+log:
|
||||
+ level: info
|
||||
+runner:
|
||||
+ file: .runner
|
||||
+ capacity: 2 # 同一台 Runner 最多同时跑 2 个 Job
|
||||
+ envs:
|
||||
+ GOPROXY: https://goproxy.cn,direct
|
||||
+ NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
|
||||
+ timeout: 3h
|
||||
+cache:
|
||||
+ enabled: true
|
||||
+ dir: /opt/act_runner/cache
|
||||
+container:
|
||||
+ network: "bridge"
|
||||
+ privileged: true # build-push-action 用到 Docker daemon
|
||||
+ options: |
|
||||
+ --add-host "host.docker.internal:host-gateway"
|
||||
+YAML
|
||||
+ chown "${RUNNER_USER}:${RUNNER_USER}" "${CONFIG_FILE}"
|
||||
+ mkdir -p "${INSTALL_DIR}/cache"
|
||||
+ chown -R "${RUNNER_USER}:${RUNNER_USER}" "${INSTALL_DIR}/cache"
|
||||
+ info "✅ 默认 config 写入 ${CONFIG_FILE}"
|
||||
+fi
|
||||
+
|
||||
+# ---------- 6. 写 systemd 服务,开机自启 ----------
|
||||
+UNIT_FILE="/etc/systemd/system/gitea-act-runner.service"
|
||||
+cat > "${UNIT_FILE}" <<SYSTEMD
|
||||
+[Unit]
|
||||
+Description=Gitea Actions Runner (${RUNNER_NAME})
|
||||
+After=network.target docker.service
|
||||
+Requires=docker.service
|
||||
+
|
||||
+[Service]
|
||||
+Type=simple
|
||||
+User=${RUNNER_USER}
|
||||
+Group=${RUNNER_USER}
|
||||
+WorkingDirectory=${INSTALL_DIR}
|
||||
+Environment=HOME=${INSTALL_DIR}
|
||||
+ExecStart=/usr/local/bin/act_runner daemon --config ${CONFIG_FILE}
|
||||
+Restart=always
|
||||
+RestartSec=5
|
||||
+KillMode=mixed
|
||||
+TimeoutStopSec=30
|
||||
+
|
||||
+# 安全加固
|
||||
+NoNewPrivileges=true
|
||||
+PrivateTmp=true
|
||||
+ProtectSystem=strict
|
||||
+ReadWritePaths=${INSTALL_DIR} /var/run/docker.sock /tmp /var/tmp
|
||||
+
|
||||
+[Install]
|
||||
+WantedBy=multi-user.target
|
||||
+SYSTEMD
|
||||
+
|
||||
+systemctl daemon-reload
|
||||
+systemctl enable --now gitea-act-runner
|
||||
+info "✅ systemd 服务已写入并启动:${UNIT_FILE}"
|
||||
+
|
||||
+sleep 3 # 给 daemon 点时间注册
|
||||
+info "📌 当前 Runner 状态:"
|
||||
+systemctl --no-pager status gitea-act-runner | head -15 || true
|
||||
+info "📌 最近 30 行日志(验证 OK):"
|
||||
+journalctl --no-pager -u gitea-act-runner --since "2 minutes ago" -n 30 || true
|
||||
+
|
||||
+echo ""
|
||||
+echo -e "${GREEN}===========================================================================${NC}"
|
||||
+echo -e "${GREEN} 🎉 Gitea Actions Runner 安装 & 注册 & 开机自启 全部完成!${NC}"
|
||||
+echo -e "${GREEN}===========================================================================${NC}"
|
||||
+echo " ✅ Runner 名称 : ${RUNNER_NAME}"
|
||||
+echo " ✅ 运行用户 : ${RUNNER_USER}"
|
||||
+echo " ✅ 安装目录 : ${INSTALL_DIR}"
|
||||
+echo " ✅ 绑定 Gitea : ${GITEA_URL}"
|
||||
+echo ""
|
||||
+echo " 👉 下一步验证(建议立刻做):"
|
||||
+echo " 1. 浏览器打开:${GITEA_URL} → 仓库 → Settings → Actions → Runners"
|
||||
+echo " 应该能看到一个 Runner,状态 = Idle"
|
||||
+echo " 2. 回到终端,模拟跑一个 workflow 没问题就彻底大功告成!"
|
||||
+echo " 👉 常用命令:"
|
||||
+echo " systemctl {start|stop|restart|status} gitea-act-runner"
|
||||
+echo " journalctl -u gitea-act-runner -f # 实时看日志"
|
||||
+echo ""
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
From 08a6a669771a8352bb19649cbe534958e455ddb9 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 08:56:16 +0800
|
||||
Subject: [PATCH 03/58] =?UTF-8?q?ci:=20=E9=80=82=E9=85=8D=20Gitea=201.27+?=
|
||||
=?UTF-8?q?=20=E7=9A=84=20Runner=20=E6=B3=A8=E5=86=8C=20Token=EF=BC=88RGT/?=
|
||||
=?UTF-8?q?RGO/RG=20=E4=B8=89=E7=A7=8D=E5=89=8D=E7=BC=80=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
scripts/setup-runner.sh | 87 ++++++++++++++++++++++++++++++++++-------
|
||||
1 file changed, 72 insertions(+), 15 deletions(-)
|
||||
|
||||
diff --git a/scripts/setup-runner.sh b/scripts/setup-runner.sh
|
||||
index bc424e7..2599738 100644
|
||||
--- a/scripts/setup-runner.sh
|
||||
+++ b/scripts/setup-runner.sh
|
||||
@@ -2,30 +2,58 @@
|
||||
# ============================================================================
|
||||
# setup-runner.sh —— 一键安装并注册 Gitea Actions Runner(act_runner)
|
||||
#
|
||||
-# 使用方法:
|
||||
-# ① 先在你的 Gitea 仓库页面 → Settings → Actions → Runners → 「Create Runner」
|
||||
-# 选「Linux + amd64」,它会给你一条注册命令,里面有一串 registration token,
|
||||
-# 格式像: GFxxxxxxxxxxxxxxxxxxxx ,复制它。
|
||||
-# ② 再在你要跑 Runner 的机器上执行:
|
||||
-# curl -fsSL https://<你的-gitea>/<仓库>/-/raw/branch/main/scripts/setup-runner.sh \
|
||||
-# | sudo bash -s -- --gitea https://<你的-gitea> --token GFxxxxxxxxxxxxxxxxxxxx
|
||||
+# ✅ 兼容 Gitea 1.21 ~ 最新(含 1.27.x),新版没有 Platform 下拉菜单。
|
||||
#
|
||||
-# 或者直接本地执行:
|
||||
-# sudo bash scripts/setup-runner.sh \
|
||||
-# --gitea https://git.baoyiwai.com \
|
||||
-# --token GFxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
+# 使用方法(两种模式任选其一,推荐 模式 A 最省事):
|
||||
+#
|
||||
+# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
+# 模式 A:仓库级 Runner(只给这个 VoiceHub 仓库用,最常用、权限最小)
|
||||
+# 路径:你的仓库 → Settings → Actions → Runners → 右上角
|
||||
+# → 新版(1.21+)这里没有 Platform 选择,直接有一个输入框
|
||||
+# 写着「Registration token」或「注册令牌」
|
||||
+# → 点右边的 📋 复制按钮,得到一串以 R 开头的 token
|
||||
+# 例: RGT8FZxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxV
|
||||
+#
|
||||
+# 机器上执行:
|
||||
+# sudo bash scripts/setup-runner.sh \
|
||||
+# --scope repo --gitea https://你的-gitea --token RGTxxxx...
|
||||
+#
|
||||
+# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
+# 模式 B:组织级 Runner(同一个 Gitea 组织下所有仓库都能用)
|
||||
+# 路径:组织主页 → Settings → Actions → Runners → 复制 Registration token
|
||||
+# (Token 开头一般是 RGOxxxx)
|
||||
+#
|
||||
+# 执行:
|
||||
+# sudo bash scripts/setup-runner.sh \
|
||||
+# --scope org --gitea https://你的-gitea --token RGOxxxx...
|
||||
+#
|
||||
+# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
+# 模式 C:全局/站点级 Runner(Gitea 全站所有仓库都能用,需站点管理员)
|
||||
+# 路径:站点管理后台(Site Administration)→ Actions → Runners
|
||||
+# → 复制 Registration token(开头一般是 RGxxxx)
|
||||
+#
|
||||
+# 执行:
|
||||
+# sudo bash scripts/setup-runner.sh \
|
||||
+# --scope global --gitea https://你的-gitea --token RGxxxx...
|
||||
+#
|
||||
+# 其他可选参数:
|
||||
+# --name "my-runner-01" 自定义 Runner 名字(默认是 runner-主机名)
|
||||
+# --user gitrunner 运行用户(默认 gitrunner)
|
||||
+# --dir /opt/act_runner 安装目录(默认 /opt/act_runner)
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 颜色 ----------
|
||||
-RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[1;36m'; NC='\033[0m'
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
+ok() { echo -e "${CYAN}[ OK ]${NC} $*"; }
|
||||
|
||||
# ---------- 解析参数 ----------
|
||||
GITEA_URL=""
|
||||
REG_TOKEN=""
|
||||
+SCOPE="repo" # repo | org | global(默认 repo,绝大多数人的场景)
|
||||
RUNNER_NAME="runner-$(hostname | tr '[:upper:]' '[:lower:]')"
|
||||
RUNNER_USER="gitrunner"
|
||||
INSTALL_DIR="/opt/act_runner"
|
||||
@@ -34,11 +62,12 @@ while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--gitea) GITEA_URL="${2}"; shift 2 ;;
|
||||
--token) REG_TOKEN="${2}"; shift 2 ;;
|
||||
+ --scope) SCOPE="${2}"; shift 2 ;;
|
||||
--name) RUNNER_NAME="${2}"; shift 2 ;;
|
||||
--user) RUNNER_USER="${2}"; shift 2 ;;
|
||||
--dir) INSTALL_DIR="${2}"; shift 2 ;;
|
||||
-h|--help)
|
||||
- sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||
+ sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
*) err "未知参数:$1(--help 查看帮助)" ;;
|
||||
@@ -46,12 +75,25 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
[[ -n "${GITEA_URL}" ]] || err "必须传 --gitea https://你的-gitea-地址"
|
||||
-[[ -n "${REG_TOKEN}" ]] || err "必须传 --token GF……(Gitea 页面里拿的注册 token)"
|
||||
+[[ -n "${REG_TOKEN}" ]] || err "必须传 --token(从 Gitea 页面的 Registration token 复制,Rxx 开头的一串)"
|
||||
+case "${SCOPE}" in
|
||||
+ repo|org|global) ;;
|
||||
+ *) err "--scope 必须是 repo | org | global 其中之一" ;;
|
||||
+esac
|
||||
[[ $EUID -eq 0 ]] || err "请用 sudo 运行本脚本(需要写 /opt / /etc/systemd/system)"
|
||||
|
||||
# 去掉末尾 /
|
||||
GITEA_URL="${GITEA_URL%/}"
|
||||
|
||||
+echo ""
|
||||
+echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
+echo -e "${CYAN} 🚀 开始部署 Gitea Actions Runner (兼容 1.21 ~ 1.27+)${NC}"
|
||||
+echo -e "${CYAN} • 绑定 Gitea : ${GITEA_URL}${NC}"
|
||||
+echo -e "${CYAN} • 作用域 : ${SCOPE}${NC}"
|
||||
+echo -e "${CYAN} • Runner 名称 : ${RUNNER_NAME}${NC}"
|
||||
+echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
+echo ""
|
||||
+
|
||||
# ---------- 0. 架构检测 ----------
|
||||
ARCH="$(uname -m)"
|
||||
case "${ARCH}" in
|
||||
@@ -60,7 +102,22 @@ case "${ARCH}" in
|
||||
armv7l) PLATFORM="linux-arm" ;;
|
||||
*) err "暂不支持的架构:${ARCH}(需要 x86_64 / aarch64 / armv7)" ;;
|
||||
esac
|
||||
-info "检测到架构:${ARCH} → 下载版本:${PLATFORM}"
|
||||
+ok "检测到架构:${ARCH} → 下载版本:${PLATFORM}"
|
||||
+
|
||||
+# ---------- 0.5 给新版 Gitea 用户的 Token 格式提示 ----------
|
||||
+case "${REG_TOKEN}" in
|
||||
+ RG*) ok "Token 前缀 RG:全局/站点级 Runner Token ✅" ;;
|
||||
+ RGO*) ok "Token 前缀 RGO:组织级 Runner Token ✅" ;;
|
||||
+ RGT*) ok "Token 前缀 RGT:仓库级 Runner Token ✅(最常用)" ;;
|
||||
+ R[0-9A-Za-z]*)
|
||||
+ warn "Token 不是标准 RG/RGO/RGT 前缀。新版本 Gitea 里 Registration token 应该是 Rxx 开头。"
|
||||
+ warn " 如果你是从旧文档里拿的 GFxxxxx(旧版),在 1.27 里已经不再用这种格式了!"
|
||||
+ warn " → 请回到仓库/组织/站点的 Actions → Runners 页面,点击 「创建 Runner」或「Registration Token」"
|
||||
+ ;;
|
||||
+ *)
|
||||
+ warn "Token 前缀不匹配任何已知格式:$REG_TOKEN。请核对复制的内容是否少选。"
|
||||
+ ;;
|
||||
+esac
|
||||
|
||||
# ---------- 1. 安装依赖:docker(Runner 要用 Docker-in-Docker 执行 buildx 等)----------
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
From ed66d2d79ff8f0de4e4eac9ee7716232be3746d2 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 09:01:23 +0800
|
||||
Subject: [PATCH 04/58] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E4=B8=AD?=
|
||||
=?UTF-8?q?=E6=96=87=20Gitea=201.27=20=E7=9A=84=E7=BA=AF=E5=AD=97=E6=AF=8D?=
|
||||
=?UTF-8?q?=E6=95=B0=E5=AD=97=20Registration=20Token?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
scripts/setup-runner.sh | 28 ++++++++++++++++++++--------
|
||||
1 file changed, 20 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/scripts/setup-runner.sh b/scripts/setup-runner.sh
|
||||
index 2599738..2929fee 100644
|
||||
--- a/scripts/setup-runner.sh
|
||||
+++ b/scripts/setup-runner.sh
|
||||
@@ -105,19 +105,31 @@ esac
|
||||
ok "检测到架构:${ARCH} → 下载版本:${PLATFORM}"
|
||||
|
||||
# ---------- 0.5 给新版 Gitea 用户的 Token 格式提示 ----------
|
||||
+# 不同版本 / 语言包的 Token 前缀不一样,兼容 4 种真实存在的格式:
|
||||
+# • 英文 1.21+ 仓库级 RGTxxxx / 组织级 RGOxxxx / 全局 RGxxxx
|
||||
+# • 中文语言包 / 某些二次分发版本:纯字母数字 20~64 位(如 LHLDAwsho7vVSukoEeKhft)
|
||||
+TOKEN_LEN="${#REG_TOKEN}"
|
||||
+ok "Token 长度 = ${TOKEN_LEN} 位"
|
||||
case "${REG_TOKEN}" in
|
||||
- RG*) ok "Token 前缀 RG:全局/站点级 Runner Token ✅" ;;
|
||||
- RGO*) ok "Token 前缀 RGO:组织级 Runner Token ✅" ;;
|
||||
- RGT*) ok "Token 前缀 RGT:仓库级 Runner Token ✅(最常用)" ;;
|
||||
- R[0-9A-Za-z]*)
|
||||
- warn "Token 不是标准 RG/RGO/RGT 前缀。新版本 Gitea 里 Registration token 应该是 Rxx 开头。"
|
||||
- warn " 如果你是从旧文档里拿的 GFxxxxx(旧版),在 1.27 里已经不再用这种格式了!"
|
||||
- warn " → 请回到仓库/组织/站点的 Actions → Runners 页面,点击 「创建 Runner」或「Registration Token」"
|
||||
+ # 英文原版前缀
|
||||
+ RG*) info "• Token 前缀 RG:全局/站点级(英文原版)✅" ;;
|
||||
+ RGO*) info "• Token 前缀 RGO:组织级(英文原版)✅" ;;
|
||||
+ RGT*) info "• Token 前缀 RGT:仓库级(英文原版)✅" ;;
|
||||
+ # 纯字母数字(中文 UI / 二开版本常见)
|
||||
+ [A-Za-z0-9]\{16,128\})
|
||||
+ ok "• 纯字母数字 Token(中文 UI / 某些二开版本常见)✅,和截图里的「注册令牌」一致"
|
||||
;;
|
||||
+ # 其他
|
||||
*)
|
||||
- warn "Token 前缀不匹配任何已知格式:$REG_TOKEN。请核对复制的内容是否少选。"
|
||||
+ warn "• Token 看起来不是 Gitea 常见格式。不过也可能是新版改动,继续尝试注册..."
|
||||
+ warn " 如果注册报 401/403,建议回到页面点一次「重置注册令牌」重新复制。"
|
||||
;;
|
||||
esac
|
||||
+# 基本字符集校验(防止复制时粘了空格或换行)
|
||||
+if [[ ! "${REG_TOKEN}" =~ ^[A-Za-z0-9]+$ ]]; then
|
||||
+ warn "⚠️ Token 里含有非字母数字的字符,很可能是复制时多带了空格/换行/标点!"
|
||||
+ warn " 建议回到仓库页面点 📋 复制按钮(不要手选文字)。"
|
||||
+fi
|
||||
|
||||
# ---------- 1. 安装依赖:docker(Runner 要用 Docker-in-Docker 执行 buildx 等)----------
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
From a027816831e667cf5df65d3b62f92d8188d7173b Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 09:13:06 +0800
|
||||
Subject: [PATCH 05/58] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Gitea=20Runne?=
|
||||
=?UTF-8?q?r=20=E4=B8=8B=E8=BD=BD=20404=EF=BC=88v3+=20=E6=94=B9=E5=90=8D?=
|
||||
=?UTF-8?q?=20gitea-runner=EF=BC=8C=E6=94=B9=E4=B8=BA=20assets=20=E5=8C=B9?=
|
||||
=?UTF-8?q?=E9=85=8D=20+=20=E5=A4=9A=E6=BA=90=E5=9B=9E=E9=80=80=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
scripts/setup-runner.sh | 136 ++++++++++++++++++++++++++++++++++------
|
||||
1 file changed, 118 insertions(+), 18 deletions(-)
|
||||
|
||||
diff --git a/scripts/setup-runner.sh b/scripts/setup-runner.sh
|
||||
index 2929fee..5ee9b05 100644
|
||||
--- a/scripts/setup-runner.sh
|
||||
+++ b/scripts/setup-runner.sh
|
||||
@@ -40,6 +40,7 @@
|
||||
# --name "my-runner-01" 自定义 Runner 名字(默认是 runner-主机名)
|
||||
# --user gitrunner 运行用户(默认 gitrunner)
|
||||
# --dir /opt/act_runner 安装目录(默认 /opt/act_runner)
|
||||
+# --version 3.3.2 【可选】固定安装某版本,不写默认用 Gitea 官方 latest
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
@@ -57,15 +58,16 @@ SCOPE="repo" # repo | org | global(默认 repo,绝大多数人的场景)
|
||||
RUNNER_NAME="runner-$(hostname | tr '[:upper:]' '[:lower:]')"
|
||||
RUNNER_USER="gitrunner"
|
||||
INSTALL_DIR="/opt/act_runner"
|
||||
-
|
||||
+PINNED_VERSION="" # 空 = latest,否则比如 "3.3.2" 固定版本
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
- --gitea) GITEA_URL="${2}"; shift 2 ;;
|
||||
- --token) REG_TOKEN="${2}"; shift 2 ;;
|
||||
- --scope) SCOPE="${2}"; shift 2 ;;
|
||||
- --name) RUNNER_NAME="${2}"; shift 2 ;;
|
||||
- --user) RUNNER_USER="${2}"; shift 2 ;;
|
||||
- --dir) INSTALL_DIR="${2}"; shift 2 ;;
|
||||
+ --gitea) GITEA_URL="${2}"; shift 2 ;;
|
||||
+ --token) REG_TOKEN="${2}"; shift 2 ;;
|
||||
+ --scope) SCOPE="${2}"; shift 2 ;;
|
||||
+ --name) RUNNER_NAME="${2}"; shift 2 ;;
|
||||
+ --user) RUNNER_USER="${2}"; shift 2 ;;
|
||||
+ --dir) INSTALL_DIR="${2}"; shift 2 ;;
|
||||
+ --version) PINNED_VERSION="${2#v}"; shift 2 ;;
|
||||
-h|--help)
|
||||
sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
@@ -159,22 +161,120 @@ info "✅ 把 ${RUNNER_USER} 加入 docker 组(允许操作 Docker daemon)"
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
chown -R "${RUNNER_USER}:${RUNNER_USER}" "${INSTALL_DIR}"
|
||||
|
||||
-# ---------- 3. 下载最新 act_runner ----------
|
||||
-info "⬇️ 正在拉取 act_runner 最新 release 元数据..."
|
||||
-LATEST_JSON="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/act_runner/releases/latest || true)"
|
||||
+# ---------- 3. 下载最新 Gitea Runner(act_runner 0.x → 3.x 改名 gitea-runner)
|
||||
+# 注意:官方从 v3 起,仓库从 gitea/act_runner 改到了 gitea/runner,且文件名前缀从 act_runner-* → gitea-runner-*。
|
||||
+# 为避免后续版本再改命名导致 404,这里**直接用 API 返回的 assets[].browser_download_url**,不再硬编码拼文件名。
|
||||
+# ----------
|
||||
+# 3.1 决定 release URL(固定版本 or latest)
|
||||
+if [[ -n "${PINNED_VERSION}" ]]; then
|
||||
+ REL_URL="https://gitea.com/api/v1/repos/gitea/runner/releases/tags/v${PINNED_VERSION}"
|
||||
+ info "⬇️ 你指定了固定版本:v${PINNED_VERSION}"
|
||||
+else
|
||||
+ REL_URL="https://gitea.com/api/v1/repos/gitea/runner/releases/latest"
|
||||
+ info "⬇️ 正在获取 Gitea Runner 最新 release 元数据..."
|
||||
+fi
|
||||
+LATEST_JSON="$(curl -fsSL "${REL_URL}" 2>/dev/null || true)"
|
||||
+
|
||||
+# 3.2 如果 gitea.com 不通(纯内网),直接回退到一个我们已知可用的版本
|
||||
if [[ -z "${LATEST_JSON}" ]]; then
|
||||
- warn "gitea.com 不通,改用硬编码版本 0.2.10(你之后可手动升级)"
|
||||
- VERSION="0.2.10"
|
||||
+ if [[ -z "${PINNED_VERSION}" ]]; then
|
||||
+ warn "gitea.com 访问不到(可能是内网环境),自动回退到已知稳定版本:v3.3.2"
|
||||
+ PINNED_VERSION="3.3.2"
|
||||
+ fi
|
||||
+ TAG="v${PINNED_VERSION}"
|
||||
+ FALLBACK_MODE=1
|
||||
else
|
||||
- VERSION="$(echo "${LATEST_JSON}" | grep -oE '"tag_name":\s*"[^"]+"' | head -1 | cut -d'"' -f4 | sed 's/^v//')"
|
||||
- [[ -n "${VERSION}" ]] || VERSION="0.2.10"
|
||||
+ TAG="$(echo "${LATEST_JSON}" | grep -oE '"tag_name":\s*"[^"]+"' | head -1 | cut -d'"' -f4)"
|
||||
+ FALLBACK_MODE=0
|
||||
+fi
|
||||
+[[ -n "${TAG}" ]] || TAG="v3.3.2"
|
||||
+VERSION="${TAG#v}"
|
||||
+
|
||||
+# 3.3 优先从 assets 匹配正确 platform 的 browser_download_url
|
||||
+BIN_URL=""
|
||||
+ASSET_NAME=""
|
||||
+if [[ "${FALLBACK_MODE}" -eq 0 && -n "${LATEST_JSON}" ]]; then
|
||||
+ # 用 awk 过滤:
|
||||
+ # - 匹配 PLATFORM(例如 linux-amd64)
|
||||
+ # - 排除 .xz / .sha256 / .exe 等校验和/压缩包
|
||||
+ # - 两种前缀都兼容:gitea-runner-X.X.X- (新版) 或 act_runner-X.X.X- (旧版)
|
||||
+ while IFS='|' read -r name url; do
|
||||
+ [[ -z "${name}" ]] && continue
|
||||
+ case "${name}" in
|
||||
+ *"${PLATFORM}"* | *"${PLATFORM//-/_}"*) ;;
|
||||
+ *) continue ;;
|
||||
+ esac
|
||||
+ case "${name}" in
|
||||
+ *.xz|*.sha256|*.exe|*.tar.gz|*.zip|*checksums*) continue ;;
|
||||
+ esac
|
||||
+ ASSET_NAME="${name}"
|
||||
+ BIN_URL="${url}"
|
||||
+ break
|
||||
+ done < <(echo "${LATEST_JSON}" | awk '
|
||||
+ /"assets": *\[/ {inA=1; next}
|
||||
+ inA && /"browser_download_url"/ { gsub(/[",]/,"",$2); url=$2 }
|
||||
+ inA && /"name"/ {
|
||||
+ gsub(/[",]/,"",$2)
|
||||
+ if (url!="") { print $2 "|" url; url="" }
|
||||
+ }
|
||||
+ inA && /^\]/ {inA=0}
|
||||
+ ')
|
||||
+fi
|
||||
+
|
||||
+# 3.4 如果 assets 匹配失败(老版本 JSON 格式特殊),手动拼 4 个候选 URL 顺序尝试
|
||||
+if [[ -z "${BIN_URL}" ]]; then
|
||||
+ info "assets 自动匹配为空,改用手动候选列表(兼容新旧命名)..."
|
||||
+ CANDIDATES=(
|
||||
+ # 新版(v3+)gitea-runner
|
||||
+ "https://gitea.com/gitea/runner/releases/download/v${VERSION}/gitea-runner-${VERSION}-${PLATFORM}"
|
||||
+ # github 镜像(有时 gitea.com 慢或挂)
|
||||
+ "https://github.com/go-gitea/runner/releases/download/v${VERSION}/gitea-runner-${VERSION}-${PLATFORM}"
|
||||
+ # 旧版 act_runner(v0.2.x 遗留命名)
|
||||
+ "https://dl.gitea.com/act_runner/${VERSION}/act_runner-${VERSION}-${PLATFORM}"
|
||||
+ # 旧版 gitea.com
|
||||
+ "https://gitea.com/gitea/act_runner/releases/download/v${VERSION}/act_runner-${VERSION}-${PLATFORM}"
|
||||
+ )
|
||||
+ for C in "${CANDIDATES[@]}"; do
|
||||
+ info " → 尝试:${C}"
|
||||
+ if curl -fsSI "${C}" >/dev/null 2>&1; then
|
||||
+ BIN_URL="${C}"
|
||||
+ ASSET_NAME="$(basename "${C}")"
|
||||
+ break
|
||||
+ fi
|
||||
+ done
|
||||
fi
|
||||
-BIN_URL="https://dl.gitea.com/act_runner/${VERSION}/act_runner-${VERSION}-${PLATFORM}"
|
||||
-info "⬇️ 下载 act_runner v${VERSION}:${BIN_URL}"
|
||||
+
|
||||
+# 3.5 还没拿到 URL → 打印手动下载指引(一定让用户能救回来)
|
||||
+if [[ -z "${BIN_URL}" ]]; then
|
||||
+ {
|
||||
+ echo ""
|
||||
+ echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
+ echo -e "${RED} ❌ 自动下载 Gitea Runner 失败${NC}"
|
||||
+ echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
+ echo " 你的架构:${PLATFORM} , 目标版本:${TAG}"
|
||||
+ echo ""
|
||||
+ echo " 请在能出网的机器上,手动打开下面 2 个 URL 之一,下载对应的二进制:"
|
||||
+ echo " • https://gitea.com/gitea/runner/releases/tag/${TAG}"
|
||||
+ echo " • https://github.com/go-gitea/runner/releases/tag/${TAG}"
|
||||
+ echo " 找文件名类似:gitea-runner-${VERSION}-${PLATFORM}(不要下 .xz / .sha256)"
|
||||
+ echo ""
|
||||
+ echo " 下载完成后 scp 到本服务器并执行:"
|
||||
+ echo " sudo cp ./gitea-runner-* /usr/local/bin/act_runner"
|
||||
+ echo " sudo chmod +x /usr/local/bin/act_runner"
|
||||
+ echo " # 然后重新跑本脚本(已有的用户/组/config 不会重复生成)"
|
||||
+ echo ""
|
||||
+ } >&2
|
||||
+ exit 1
|
||||
+fi
|
||||
+
|
||||
+# 3.6 真正下载
|
||||
+info "⬇️ 下载 Gitea Runner ${TAG}:${ASSET_NAME}"
|
||||
+info " URL:${BIN_URL}"
|
||||
curl -fsSL "${BIN_URL}" -o /usr/local/bin/act_runner \
|
||||
- || err "下载失败。请手动打开 ${BIN_URL} 看是否 404,然后换个版本"
|
||||
+ || err "下载失败!可能是 DNS/网络/证书问题。可以手动把上面 URL 的文件下载下来 scp 到服务器放到 /usr/local/bin/act_runner,再重新跑本脚本。"
|
||||
chmod +x /usr/local/bin/act_runner
|
||||
-info "✅ act_runner 已安装到 /usr/local/bin,版本:$(act_runner --version 2>&1 || true)"
|
||||
+INSTALLED_VER="$(act_runner --version 2>&1 || echo "${TAG}")"
|
||||
+ok "✅ 已安装到 /usr/local/bin/act_runner,版本:${INSTALLED_VER}"
|
||||
|
||||
# ---------- 4. 注册 Runner ----------
|
||||
cd "${INSTALL_DIR}"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
From 7421906fb9b2ad10ea26e88b0df29ffe9af43b11 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 09:35:33 +0800
|
||||
Subject: [PATCH 06/58] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Ubuntu=2024.0?=
|
||||
=?UTF-8?q?4=20=E4=B8=8B=20setup-deploy-user.sh=20useradd=20=E5=8F=82?=
|
||||
=?UTF-8?q?=E6=95=B0=E9=94=99=E8=AF=AF=EF=BC=88--disabled-password=20?=
|
||||
=?UTF-8?q?=E6=98=AF=20adduser=20=E7=9A=84=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
scripts/setup-deploy-user.sh | 13 +++++++++++--
|
||||
1 file changed, 11 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/scripts/setup-deploy-user.sh b/scripts/setup-deploy-user.sh
|
||||
index 38ac0b1..dc9a48b 100644
|
||||
--- a/scripts/setup-deploy-user.sh
|
||||
+++ b/scripts/setup-deploy-user.sh
|
||||
@@ -42,8 +42,17 @@ command -v setfacl >/dev/null 2>&1 || {
|
||||
|
||||
# -------- 1. 建用户 --------
|
||||
if ! id -u "${DEPLOY_USER}" >/dev/null 2>&1; then
|
||||
- useradd --create-home --shell /usr/bin/bash --disabled-password "${DEPLOY_USER}"
|
||||
- ok "新建用户:${DEPLOY_USER}(无密码,仅 SSH key 登录)"
|
||||
+ # 注意:Ubuntu 22.04/24.04 的 useradd 没有 --disabled-password 选项
|
||||
+ # (那是 adduser 命令的参数,之前脚本写错了命令)。
|
||||
+ # 这里用 useradd 原生参数:
|
||||
+ # -m = 创建 home
|
||||
+ # -s = 指定登录 shell
|
||||
+ # -p '*' = 密码设置为「*」(锁定,禁止用密码登录,只能 SSH 密钥)
|
||||
+ # 这样和之前「只允许 SSH 密钥登录、无密码」的意图完全一致。
|
||||
+ useradd -m -s /usr/bin/bash -p '*' "${DEPLOY_USER}"
|
||||
+ # Ubuntu/Debian 额外保险:锁定密码字段,避免 shadow 被改
|
||||
+ usermod -L "${DEPLOY_USER}" 2>/dev/null || true
|
||||
+ ok "新建用户:${DEPLOY_USER}(密码已锁定,仅 SSH key 登录)"
|
||||
else
|
||||
info "用户 ${DEPLOY_USER} 已存在,跳过创建"
|
||||
fi
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
From 47d63a48b0d4e8d3f4001cabf4049d9b6ef1a13a Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 09:41:34 +0800
|
||||
Subject: [PATCH 07/58] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Docker=20Comp?=
|
||||
=?UTF-8?q?ose=20v2=20=E4=B8=A5=E6=A0=BC=E7=B1=BB=E5=9E=8B=E4=B8=8B=20cpus?=
|
||||
=?UTF-8?q?=20=E5=8F=8C=E5=BC=95=E5=8F=B7=E5=AF=BC=E8=87=B4=20ParseFloat?=
|
||||
=?UTF-8?q?=20=E6=8A=A5=E9=94=99?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
docker-compose.yml | 10 ++++++----
|
||||
1 file changed, 6 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/docker-compose.yml b/docker-compose.yml
|
||||
index f89810e..3eb9540 100644
|
||||
--- a/docker-compose.yml
|
||||
+++ b/docker-compose.yml
|
||||
@@ -55,16 +55,18 @@ services:
|
||||
# ===== 日志/性能 =====
|
||||
NODE_OPTIONS: ${NODE_OPTIONS:-"--max-old-space-size=768"}
|
||||
# 2C4G 机器的推荐配置(.env 可覆盖);小机器 1C2G 也能跑
|
||||
+ # ⚠️ 注意:cpus 默认值不能加双引号!新版 Docker Compose v2 严格类型:
|
||||
+ # 加引号会解析为字符串 '"1.5"' → ParseFloat 失败报错
|
||||
mem_limit: ${APP_MEM_LIMIT:-1400m}
|
||||
mem_reservation: ${APP_MEM_RESERVATION:-384m}
|
||||
- cpus: ${APP_CPUS:-"1.5"}
|
||||
+ cpus: ${APP_CPUS:-1.5}
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
- cpus: ${APP_CPUS_LIMIT:-"2"}
|
||||
+ cpus: ${APP_CPUS_LIMIT:-2}
|
||||
memory: ${APP_MEM_LIMIT:-1400M}
|
||||
reservations:
|
||||
- cpus: ${APP_CPUS_RESERVATION:-"0.5"}
|
||||
+ cpus: ${APP_CPUS_RESERVATION:-0.5}
|
||||
memory: ${APP_MEM_RESERVATION:-384M}
|
||||
healthcheck:
|
||||
# 30s 后开始检查,每 15s 一次,连续 3 次失败就重启
|
||||
@@ -141,7 +143,7 @@ services:
|
||||
- redis-data:/data
|
||||
mem_limit: ${REDIS_MEM_LIMIT:-384m}
|
||||
mem_reservation: ${REDIS_MEM_RESERVATION:-64m}
|
||||
- cpus: ${REDIS_CPUS:-"0.5"}
|
||||
+ cpus: ${REDIS_CPUS:-0.5}
|
||||
networks:
|
||||
- voicehub-net
|
||||
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
From 3ff6c9dd3eea70cbf5722cc50b490f9520359dac Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 09:44:58 +0800
|
||||
Subject: [PATCH 08/58] =?UTF-8?q?fix:=20docker-compose=20=E9=A1=B6?=
|
||||
=?UTF-8?q?=E7=BA=A7=20name=20=E5=B0=8F=E5=86=99=E5=90=88=E6=B3=95=20+=20.?=
|
||||
=?UTF-8?q?env.example=20=E8=A1=A5=E5=85=85=20REDIS=5FPASSWORD?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.env.example | 5 +++++
|
||||
docker-compose.yml | 4 +++-
|
||||
2 files changed, 8 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/.env.example b/.env.example
|
||||
index b85a6e2..8ea85f4 100644
|
||||
--- a/.env.example
|
||||
+++ b/.env.example
|
||||
@@ -72,6 +72,11 @@ NODE_ENV=development
|
||||
# 格式:redis://用户名:密码@主机:端口
|
||||
# REDIS_URL="redis://default:password@host:port"
|
||||
|
||||
+# Docker Compose 自带的 Redis 容器(compose 自带 redis 服务,仅在启用自带 redis 时用)
|
||||
+# - 留空(或空字符串):不设置 Redis 密码,内网部署常用(默认)
|
||||
+# - 填写强密码:Redis 会启用 requirepass,连接时 REDIS_URL 里也要带上相同密码
|
||||
+REDIS_PASSWORD=
|
||||
+
|
||||
# Redis 键命名空间,多个 VoiceHub 环境共用 Redis 时应分别设置
|
||||
# REDIS_KEY_PREFIX="voicehub:v2:"
|
||||
|
||||
diff --git a/docker-compose.yml b/docker-compose.yml
|
||||
index 3eb9540..de070a1 100644
|
||||
--- a/docker-compose.yml
|
||||
+++ b/docker-compose.yml
|
||||
@@ -1,4 +1,6 @@
|
||||
-name: BYW VoiceHub (1.7.0.0)
|
||||
+# ⚠️ Docker Compose v2 顶级 name 必须匹配正则 ^[a-z0-9][a-z0-9_-]*$
|
||||
+# 不能有大写、空格、括号、中文,否则会报 name Does not match pattern 错误
|
||||
+name: byw-voicehub
|
||||
services:
|
||||
# ========================================================================
|
||||
# 应用本体 - Nuxt 4 (Node SSR)
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
From f999d58eef24da10c53b2a4f45dc0a88fe2f4e89 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 09:46:55 +0800
|
||||
Subject: [PATCH 09/58] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=20voicehub=20?=
|
||||
=?UTF-8?q?=E9=A1=B6=E5=B1=82=20cpus/mem=5Flimit=EF=BC=8C=E9=81=BF?=
|
||||
=?UTF-8?q?=E5=85=8D=E4=B8=8E=20deploy.resources=20=E9=87=8D=E5=A4=8D?=
|
||||
=?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=8A=A5=E9=94=99?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
docker-compose.yml | 9 ++++-----
|
||||
1 file changed, 4 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/docker-compose.yml b/docker-compose.yml
|
||||
index de070a1..abb81bc 100644
|
||||
--- a/docker-compose.yml
|
||||
+++ b/docker-compose.yml
|
||||
@@ -56,12 +56,11 @@ services:
|
||||
|
||||
# ===== 日志/性能 =====
|
||||
NODE_OPTIONS: ${NODE_OPTIONS:-"--max-old-space-size=768"}
|
||||
+
|
||||
# 2C4G 机器的推荐配置(.env 可覆盖);小机器 1C2G 也能跑
|
||||
- # ⚠️ 注意:cpus 默认值不能加双引号!新版 Docker Compose v2 严格类型:
|
||||
- # 加引号会解析为字符串 '"1.5"' → ParseFloat 失败报错
|
||||
- mem_limit: ${APP_MEM_LIMIT:-1400m}
|
||||
- mem_reservation: ${APP_MEM_RESERVATION:-384m}
|
||||
- cpus: ${APP_CPUS:-1.5}
|
||||
+ # ⚠️ 关键:用 deploy.resources.{limits|reservations} 这一套,
|
||||
+ # 不要同时在顶层写 cpus/mem_limit/mem_reservation!
|
||||
+ # Docker Compose v2 禁止两套资源字段并存,会报 invalid compose project。
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
From 082b5c854c31c3dc454f9e4d8622ae4c91698bfa Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 10:21:48 +0800
|
||||
Subject: [PATCH 10/58] =?UTF-8?q?fix(ci):=20deploy.yml=20=E6=94=B9?=
|
||||
=?UTF-8?q?=E4=B8=BA=E7=94=A8=E6=88=B7=E7=9C=9F=E5=AE=9E=20GITEA=5FHOST=20?=
|
||||
=?UTF-8?q?=E5=92=8C=20DEPLOY=5FPATH=EF=BC=88/data/opt/byw-voicehub?=
|
||||
=?UTF-8?q?=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 11 ++++++-----
|
||||
1 file changed, 6 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index e8d783b..21a8f14 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -24,13 +24,14 @@ concurrency:
|
||||
|
||||
env:
|
||||
# ============================================================
|
||||
- # 👇👇👇 这里改成你自己的 Gitea 域名(不要 https:// 前缀,不要端口默认 443/80)
|
||||
- # 例:git.baoyiwai.com 或 gitea.example.com:3000
|
||||
- GITEA_HOST: gitea.example.com
|
||||
- # 👆👆👆
|
||||
+ # ⚠️ 用户真实配置(部署 BYW VoiceHub 专用,不要改回 example)
|
||||
+ # ============================================================
|
||||
+ # Gitea 实例域名(不含 https://,如果是非标准端口要加 :端口号)
|
||||
+ GITEA_HOST: gitea.fse-media.group
|
||||
+ # 生产服务器 B 上的实际部署目录(之前 /opt/byw-voicehub 已废弃,以用户实际为准)
|
||||
+ DEPLOY_PATH: /data/opt/byw-voicehub
|
||||
# ============================================================
|
||||
IMAGE_NAME: byw-voicehub
|
||||
- DEPLOY_PATH: /opt/byw-voicehub
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
From 7b3294a3804a73d7048afe91a516ebdc55fedbd4 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 10:30:51 +0800
|
||||
Subject: [PATCH 11/58] =?UTF-8?q?fix(ci):=20Preflight=20=E7=9B=B4=E6=8E=A5?=
|
||||
=?UTF-8?q?=E8=B7=91=20docker://node:22-bookworm-slim=20=E8=B7=B3=E8=BF=87?=
|
||||
=?UTF-8?q?=20setup-node=20=E4=B8=8B=E8=BD=BD=EF=BC=88=E5=9B=BD=E5=86=85?=
|
||||
=?UTF-8?q?=20GitHub=20release=20=E8=B6=85=E6=97=B6=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 42 +++++++++++++++++++++----------------
|
||||
1 file changed, 24 insertions(+), 18 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 21a8f14..8f44e10 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -39,7 +39,9 @@ jobs:
|
||||
# ============================================================
|
||||
preflight:
|
||||
name: Preflight 语法 & 构建检查
|
||||
- runs-on: ubuntu-latest
|
||||
+ # 国内访问 GitHub actions/node-versions release 经常超时(443 ETIMEDOUT)
|
||||
+ # 所以直接用官方 node:22 镜像跑这个 Job,绕过 setup-node 下载 step
|
||||
+ runs-on: [ubuntu-latest:docker://node:22-bookworm-slim]
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: 📥 Checkout 代码
|
||||
@@ -47,11 +49,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- - name: 🔧 安装 Node.js 22
|
||||
- uses: actions/setup-node@v4
|
||||
- with:
|
||||
- node-version: "22"
|
||||
- cache: "npm"
|
||||
+ # ── 我们已经用 runs-on: docker://node:22 了,所以这里不需要 actions/setup-node ──
|
||||
+ # 避免国内 Runner 从 github.com 下载 Node tar.gz 出现 ETIMEDOUT
|
||||
+ - name: 🔎 环境自检(node/npm 版本)
|
||||
+ shell: bash
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ node -v
|
||||
+ npm -v
|
||||
+ which node npm
|
||||
|
||||
- name: 🧪 安装依赖(npm ci,严格按锁文件)
|
||||
run: |
|
||||
@@ -92,12 +98,12 @@ jobs:
|
||||
image_full: ${{ steps.meta.outputs.full }}
|
||||
image_latest: ${{ steps.meta.outputs.latest }}
|
||||
steps:
|
||||
- - name: 📥 Checkout 代码
|
||||
+ - name: Checkout 代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- - name: 🏷️ 计算镜像 tag(日期 + 短 SHA)
|
||||
+ - name: 计算镜像 tag(日期 + 短 SHA)
|
||||
id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -116,17 +122,17 @@ jobs:
|
||||
echo "🪪 镜像 latest : ${IMG_LATEST}"
|
||||
echo "======================================"
|
||||
|
||||
- - name: 🔧 启用 QEMU(跨架构小概率用得到,保险)
|
||||
+ - name: 启用 QEMU(跨架构小概率用得到,保险)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
|
||||
- - name: 🔧 启用 Docker Buildx
|
||||
+ - name: 启用 Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- - name: 🔐 登录 Gitea 容器仓库(Packages)
|
||||
+ - name: 登录 Gitea 容器仓库(Packages)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITEA_HOST }}
|
||||
@@ -135,7 +141,7 @@ jobs:
|
||||
# Secrets 里 REGISTRY_TOKEN 优先,没填就用 Gitea Actions 自动注入的仓库级 token
|
||||
password: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||
|
||||
- - name: 🐳 构建 & 推送(BuildKit 分层缓存,skip_build_cache=true 时禁用)
|
||||
+ - name: 构建 & 推送(BuildKit 分层缓存,skip_build_cache=true 时禁用)
|
||||
id: build_push
|
||||
uses: docker/build-push-action@v6
|
||||
env:
|
||||
@@ -159,7 +165,7 @@ jobs:
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
|
||||
|
||||
- - name: 🧮 镜像体积 & 层信息(日志里看一下,避免镜像越来越大)
|
||||
+ - name: 镜像体积 & 层信息(日志里看一下,避免镜像越来越大)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
docker pull "${{ steps.meta.outputs.full }}" || true
|
||||
@@ -181,7 +187,7 @@ jobs:
|
||||
name: production
|
||||
url: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
|
||||
steps:
|
||||
- - name: ⚠️ 检查部署所需 Secrets 是否都配置了
|
||||
+ - name: 检查部署所需 Secrets 是否都配置了
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -196,7 +202,7 @@ jobs:
|
||||
fi
|
||||
echo "✅ 部署 Secrets 配置完整"
|
||||
|
||||
- - name: 🚀 一键滚动部署(SSH)
|
||||
+ - name: 一键滚动部署(SSH)
|
||||
uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
@@ -217,9 +223,9 @@ jobs:
|
||||
COMPOSE="docker compose"
|
||||
|
||||
echo "=============================================="
|
||||
- echo "🚢 本次部署镜像: ${IMAGE_NEW}"
|
||||
- echo "🗂️ 部署目录 : ${DEPLOY_DIR}"
|
||||
- echo "👤 执行用户 : $(whoami)"
|
||||
+ echo "本次部署镜像: ${IMAGE_NEW}"
|
||||
+ echo "部署目录 : ${DEPLOY_DIR}"
|
||||
+ echo "执行用户 : $(whoami)"
|
||||
echo "=============================================="
|
||||
|
||||
# ---------- 1. 确保部署目录存在 ----------
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
From 97af401d894e5b00b6b05b58982dc628603e31c5 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 10:32:55 +0800
|
||||
Subject: [PATCH 12/58] =?UTF-8?q?fix(ci):=20Preflight=20=E6=94=B9=E4=B8=BA?=
|
||||
=?UTF-8?q?=20Gitea=20=E6=A0=87=E5=87=86=20container:image=20=E5=86=99?=
|
||||
=?UTF-8?q?=E6=B3=95=EF=BC=88runs-on=20=E5=8F=AA=E5=A1=AB=20ubuntu-latest?=
|
||||
=?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=20no=20runner=20matched=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 17 +++++++++++------
|
||||
1 file changed, 11 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 8f44e10..4e5260c 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -39,25 +39,30 @@ jobs:
|
||||
# ============================================================
|
||||
preflight:
|
||||
name: Preflight 语法 & 构建检查
|
||||
- # 国内访问 GitHub actions/node-versions release 经常超时(443 ETIMEDOUT)
|
||||
- # 所以直接用官方 node:22 镜像跑这个 Job,绕过 setup-node 下载 step
|
||||
- runs-on: [ubuntu-latest:docker://node:22-bookworm-slim]
|
||||
+ # ⚠️ Gitea/act_runner 的写法:runs-on 只填注册时的真实标签(我们叫 ubuntu-latest);
|
||||
+ # 如果要强制用某个 Docker 镜像跑这个 Job,单独写一个 container: 段。
|
||||
+ # 不能写 "ubuntu-latest:docker://xxx",这是 GitHub Actions 专有语法,Gitea 不认!
|
||||
+ runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
+ # 直接用官方 node:22 镜像当 Job 容器(里面自带 node 22 + npm),
|
||||
+ # 完全绕过 setup-node 从 GitHub release 下载 node tar.gz 的国内超时老坑
|
||||
+ container:
|
||||
+ image: node:22-bookworm-slim
|
||||
steps:
|
||||
- name: 📥 Checkout 代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- # ── 我们已经用 runs-on: docker://node:22 了,所以这里不需要 actions/setup-node ──
|
||||
- # 避免国内 Runner 从 github.com 下载 Node tar.gz 出现 ETIMEDOUT
|
||||
- - name: 🔎 环境自检(node/npm 版本)
|
||||
+ # ── 已经在 container.image=node:22 里跑了,自带 node 22,不需要 actions/setup-node ──
|
||||
+ - name: 🔎 环境自检(确认容器里有 node/npm,版本对得上)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
node -v
|
||||
npm -v
|
||||
which node npm
|
||||
+ cat /etc/os-release 2>/dev/null | head -4 || true
|
||||
|
||||
- name: 🧪 安装依赖(npm ci,严格按锁文件)
|
||||
run: |
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
From 10d363ca2b3f6a7cae380cada62c150933b4fef3 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 10:35:56 +0800
|
||||
Subject: [PATCH 13/58] =?UTF-8?q?style(ci):=20=E7=A7=BB=E9=99=A4=E5=B7=A5?=
|
||||
=?UTF-8?q?=E4=BD=9C=E6=B5=81=E6=AD=A5=E9=AA=A4=E4=B8=AD=E7=9A=84=20emoji?=
|
||||
=?UTF-8?q?=EF=BC=8C=E6=94=B9=E7=94=A8=E4=B8=93=E4=B8=9A=E9=A3=8E=E6=A0=BC?=
|
||||
=?UTF-8?q?=E5=91=BD=E5=90=8D?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 144 ++++++++++++------------------------
|
||||
1 file changed, 49 insertions(+), 95 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 4e5260c..591afa7 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -1,4 +1,4 @@
|
||||
-name: 构建 & 部署 BYW VoiceHub
|
||||
+name: 构建并部署 BYW VoiceHub
|
||||
run-name: ${{ github.actor }} 触发部署 · Run ${{ github.run_number }}
|
||||
|
||||
on:
|
||||
@@ -17,45 +17,29 @@ on:
|
||||
default: false
|
||||
required: false
|
||||
|
||||
-# 同一时刻只允许一个部署在跑,避免冲突;如果新提交进来,取消前一个未完成的
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
- # ============================================================
|
||||
- # ⚠️ 用户真实配置(部署 BYW VoiceHub 专用,不要改回 example)
|
||||
- # ============================================================
|
||||
- # Gitea 实例域名(不含 https://,如果是非标准端口要加 :端口号)
|
||||
GITEA_HOST: gitea.fse-media.group
|
||||
- # 生产服务器 B 上的实际部署目录(之前 /opt/byw-voicehub 已废弃,以用户实际为准)
|
||||
DEPLOY_PATH: /data/opt/byw-voicehub
|
||||
- # ============================================================
|
||||
IMAGE_NAME: byw-voicehub
|
||||
|
||||
jobs:
|
||||
- # ============================================================
|
||||
- # Job 1: 校验构建环境 & 打包产物(npm ci + build,不依赖 Docker)
|
||||
- # ============================================================
|
||||
preflight:
|
||||
- name: Preflight 语法 & 构建检查
|
||||
- # ⚠️ Gitea/act_runner 的写法:runs-on 只填注册时的真实标签(我们叫 ubuntu-latest);
|
||||
- # 如果要强制用某个 Docker 镜像跑这个 Job,单独写一个 container: 段。
|
||||
- # 不能写 "ubuntu-latest:docker://xxx",这是 GitHub Actions 专有语法,Gitea 不认!
|
||||
+ name: Preflight 语法与构建检查
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
- # 直接用官方 node:22 镜像当 Job 容器(里面自带 node 22 + npm),
|
||||
- # 完全绕过 setup-node 从 GitHub release 下载 node tar.gz 的国内超时老坑
|
||||
container:
|
||||
image: node:22-bookworm-slim
|
||||
steps:
|
||||
- - name: 📥 Checkout 代码
|
||||
+ - name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- # ── 已经在 container.image=node:22 里跑了,自带 node 22,不需要 actions/setup-node ──
|
||||
- - name: 🔎 环境自检(确认容器里有 node/npm,版本对得上)
|
||||
+ - name: 运行时环境自检
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
@@ -64,37 +48,33 @@ jobs:
|
||||
which node npm
|
||||
cat /etc/os-release 2>/dev/null | head -4 || true
|
||||
|
||||
- - name: 🧪 安装依赖(npm ci,严格按锁文件)
|
||||
+ - name: 安装依赖(npm ci)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
- # 国内加速镜像(Runner 在国外可以删掉这两行)
|
||||
npm config set registry https://registry.npmmirror.com || true
|
||||
npm config set fund false
|
||||
npm config set audit false
|
||||
npm ci
|
||||
|
||||
- - name: 🔎 检查 package.json 没有 pnpm 残留
|
||||
+ - name: 检查 package.json 无 pnpm 残留
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if grep -qE '"packageManager"\s*:\s*"pnpm' package.json; then
|
||||
- echo "❌ package.json 里有 pnpm 包管理器声明,和用户要求冲突"
|
||||
+ echo "package.json 中发现 pnpm 包管理器声明,与要求冲突,构建中止"
|
||||
exit 1
|
||||
fi
|
||||
- echo "✅ package.json 未发现 pnpm 声明"
|
||||
+ echo "package.json 未发现 pnpm 声明"
|
||||
|
||||
- - name: 🏗️ 本地 nuxt build(独立于 Docker 先验证代码是可构建的)
|
||||
+ - name: 执行 nuxt build
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
NODE_OPTIONS="--max-old-space-size=2048" npm run build
|
||||
- echo "✅ .output/ 生成成功:"
|
||||
+ echo ".output/ 生成结果:"
|
||||
ls -la .output/ 2>/dev/null || true
|
||||
du -sh .output 2>/dev/null || true
|
||||
|
||||
- # ============================================================
|
||||
- # Job 2: 构建 Docker 镜像 → 推送到 Gitea 自带容器仓库
|
||||
- # ============================================================
|
||||
build-image:
|
||||
- name: 构建并推送镜像
|
||||
+ name: 构建并推送容器镜像
|
||||
needs: preflight
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
@@ -103,12 +83,12 @@ jobs:
|
||||
image_full: ${{ steps.meta.outputs.full }}
|
||||
image_latest: ${{ steps.meta.outputs.latest }}
|
||||
steps:
|
||||
- - name: Checkout 代码
|
||||
+ - name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- - name: 计算镜像 tag(日期 + 短 SHA)
|
||||
+ - name: 计算镜像标签
|
||||
id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -122,12 +102,12 @@ jobs:
|
||||
echo "full=${IMG_FULL}" >> "$GITHUB_OUTPUT"
|
||||
echo "latest=${IMG_LATEST}" >> "$GITHUB_OUTPUT"
|
||||
echo "======================================"
|
||||
- echo "🪪 镜像版本 tag : ${DATE_TAG}"
|
||||
- echo "🪪 镜像完整名 : ${IMG_FULL}"
|
||||
- echo "🪪 镜像 latest : ${IMG_LATEST}"
|
||||
+ echo "Image tag : ${DATE_TAG}"
|
||||
+ echo "Full image : ${IMG_FULL}"
|
||||
+ echo "Latest image : ${IMG_LATEST}"
|
||||
echo "======================================"
|
||||
|
||||
- - name: 启用 QEMU(跨架构小概率用得到,保险)
|
||||
+ - name: 启用 QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -137,16 +117,14 @@ jobs:
|
||||
with:
|
||||
install: true
|
||||
|
||||
- - name: 登录 Gitea 容器仓库(Packages)
|
||||
+ - name: 登录 Gitea 容器仓库
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITEA_HOST }}
|
||||
- # Secrets 里 REGISTRY_USER 没填就默认用触发构建的用户
|
||||
username: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||
- # Secrets 里 REGISTRY_TOKEN 优先,没填就用 Gitea Actions 自动注入的仓库级 token
|
||||
password: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||
|
||||
- - name: 构建 & 推送(BuildKit 分层缓存,skip_build_cache=true 时禁用)
|
||||
+ - name: 构建并推送镜像
|
||||
id: build_push
|
||||
uses: docker/build-push-action@v6
|
||||
env:
|
||||
@@ -170,21 +148,17 @@ jobs:
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
|
||||
|
||||
- - name: 镜像体积 & 层信息(日志里看一下,避免镜像越来越大)
|
||||
+ - name: 镜像体积与层信息
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
docker pull "${{ steps.meta.outputs.full }}" || true
|
||||
docker image inspect "${{ steps.meta.outputs.full }}" --format='{{.Size}}' 2>/dev/null \
|
||||
- | awk '{printf "✅ 镜像体积:%.2f MB\n", $1/1024/1024}' || true
|
||||
+ | awk '{printf "Image size: %.2f MB\n", $1/1024/1024}' || true
|
||||
docker history --no-trunc "${{ steps.meta.outputs.full }}" 2>/dev/null | head -20 || true
|
||||
|
||||
- # ============================================================
|
||||
- # Job 3: SSH 到服务器 → 拉新镜像 → 滚动重启
|
||||
- # ============================================================
|
||||
deploy:
|
||||
name: 部署到生产服务器
|
||||
needs: build-image
|
||||
- # push 事件默认自动部署;手动触发时看 checkbox
|
||||
if: ${{ !cancelled() && !failure() && (github.event_name == 'push' || inputs.deploy_target == true) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
@@ -192,7 +166,7 @@ jobs:
|
||||
name: production
|
||||
url: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
|
||||
steps:
|
||||
- - name: 检查部署所需 Secrets 是否都配置了
|
||||
+ - name: 校验部署所需密钥
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -201,133 +175,113 @@ jobs:
|
||||
[ -n "${{ secrets.DEPLOY_USER }}" ] || MISSING+=("DEPLOY_USER")
|
||||
[ -n "${{ secrets.DEPLOY_SSH_KEY }}" ] || MISSING+=("DEPLOY_SSH_KEY")
|
||||
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||
- echo "❌ 缺少以下 Repository Secrets:${MISSING[*]}"
|
||||
- echo "👉 去 Gitea 仓库 → Settings → Secrets and variables → Actions 里添加"
|
||||
+ echo "缺少以下 Repository Secrets:${MISSING[*]}"
|
||||
+ echo "请在 Gitea 仓库 -> 设置 -> 工作流 -> 密钥 页面添加"
|
||||
exit 1
|
||||
fi
|
||||
- echo "✅ 部署 Secrets 配置完整"
|
||||
+ echo "部署密钥配置完整"
|
||||
|
||||
- - name: 一键滚动部署(SSH)
|
||||
+ - name: SSH 滚动部署
|
||||
uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
port: ${{ secrets.DEPLOY_PORT || 22 }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
- # 部署可能慢,给足 30 分钟
|
||||
command_timeout: 30m
|
||||
- # 出错立刻停,不继续往下跑
|
||||
script_stop: true
|
||||
script: |
|
||||
set -euxo pipefail
|
||||
|
||||
- # ---------- 0. 基本参数 ----------
|
||||
IMAGE_NEW="${{ needs.build-image.outputs.image_full }}"
|
||||
IMAGE_LATEST="${{ needs.build-image.outputs.image_latest }}"
|
||||
DEPLOY_DIR="${DEPLOY_PATH}"
|
||||
COMPOSE="docker compose"
|
||||
|
||||
echo "=============================================="
|
||||
- echo "本次部署镜像: ${IMAGE_NEW}"
|
||||
- echo "部署目录 : ${DEPLOY_DIR}"
|
||||
- echo "执行用户 : $(whoami)"
|
||||
+ echo "Deploy image : ${IMAGE_NEW}"
|
||||
+ echo "Deploy dir : ${DEPLOY_DIR}"
|
||||
+ echo "Executor : $(whoami)"
|
||||
echo "=============================================="
|
||||
|
||||
- # ---------- 1. 确保部署目录存在 ----------
|
||||
mkdir -p "${DEPLOY_DIR}"
|
||||
cd "${DEPLOY_DIR}"
|
||||
|
||||
- # ---------- 2. 登录 Gitea 容器仓库(用 pull token,最小权限) ----------
|
||||
if [ -n "${{ secrets.REGISTRY_PULL_TOKEN }}" ] && [ -n "${{ secrets.REGISTRY_PULL_USER }}" ]; then
|
||||
- echo "🔐 使用专用 pull 凭证登录 Gitea Container Registry"
|
||||
+ echo "使用专用 pull 凭证登录 Gitea Container Registry"
|
||||
echo "${{ secrets.REGISTRY_PULL_TOKEN }}" \
|
||||
| docker login "${GITEA_HOST}" \
|
||||
-u "${{ secrets.REGISTRY_PULL_USER }}" \
|
||||
--password-stdin 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- # ---------- 3. 先拉镜像(提前下好,后面 up 不会卡) ----------
|
||||
- echo "⬇️ 开始拉取新镜像 ${IMAGE_NEW} ..."
|
||||
+ echo "Pulling new image ${IMAGE_NEW} ..."
|
||||
docker pull "${IMAGE_NEW}"
|
||||
docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
||||
- echo "✅ 新镜像拉取完成,并已打 latest tag"
|
||||
+ echo "New image pulled and tagged as latest"
|
||||
|
||||
- # ---------- 4. 生成 / 更新 docker-compose.override.yml ----------
|
||||
- # 指向新的精确 tag(不是 latest,出问题能快速回滚)
|
||||
cat > docker-compose.override.yml <<YAML
|
||||
services:
|
||||
voicehub:
|
||||
image: ${IMAGE_NEW}
|
||||
YAML
|
||||
- echo "📝 docker-compose.override.yml 已写入,指定镜像=${IMAGE_NEW}"
|
||||
+ echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
||||
|
||||
- # ---------- 5. 没有 docker-compose.yml? 自动从仓库下一份模板 ----------
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
- echo "ℹ️ 未发现 docker-compose.yml,自动从仓库模板下载"
|
||||
+ echo "docker-compose.yml 不存在,自动从仓库模板下载"
|
||||
TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY:-${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
|
||||
- echo " ↓ URL=${TEMPLATE_URL}"
|
||||
+ echo "URL=${TEMPLATE_URL}"
|
||||
curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
|
||||
|| (
|
||||
- echo "⚠️ 自动下载模板失败,跳过(你需要手动上传 docker-compose.yml 和 .env)"
|
||||
- # 不 exit,因为用户可能已经有一套自定义部署
|
||||
+ echo "自动下载模板失败,请手动上传 docker-compose.yml 和 .env"
|
||||
)
|
||||
fi
|
||||
|
||||
- # ---------- 6. 没有 .env? 给一份占位(方便首次部署发现问题) ----------
|
||||
if [ ! -f .env ]; then
|
||||
echo "================================================================="
|
||||
- echo "⚠️ WARNING:${DEPLOY_DIR}/.env 不存在!!!"
|
||||
- echo "👉 第一次部署请先在服务器上创建 .env(至少填 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
|
||||
- echo "👉 参考仓库根的 .env.example 或部署文档"
|
||||
+ echo "WARNING:${DEPLOY_DIR}/.env 不存在"
|
||||
+ echo "首次部署请在服务器上创建 .env(至少填写 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
|
||||
+ echo "参考仓库根目录的 .env.example"
|
||||
echo "================================================================="
|
||||
- # 不退出,因为 compose 会用 environment 默认值尝试启动(只是大概率连不上 DB)
|
||||
fi
|
||||
|
||||
- # ---------- 7. 滚动启动(如果只有 voicehub 单服务,不会动 postgres/redis) ----------
|
||||
- echo "🔄 docker compose up -d(只重建变更过的 voicehub)..."
|
||||
+ echo "Executing docker compose up -d (voicehub only)..."
|
||||
${COMPOSE} up -d --remove-orphans
|
||||
|
||||
- # ---------- 8. 等待应用变 healthy,最多 2 分钟 ----------
|
||||
- MAX_WAIT=24 # 24 × 5s = 2min
|
||||
+ MAX_WAIT=24
|
||||
WAITED=0
|
||||
- echo "⏳ 等待容器 healthcheck 通过(最多 120s)..."
|
||||
+ echo "Waiting for container healthcheck (max 120s)..."
|
||||
while [ ${WAITED} -lt ${MAX_WAIT} ]; do
|
||||
STATUS="$(${COMPOSE} ps --format json 2>/dev/null || true)"
|
||||
if echo "${STATUS}" | grep -q '"Health":"healthy"'; then
|
||||
- echo "✅ voicehub 容器健康检查通过"
|
||||
+ echo "voicehub healthcheck passed"
|
||||
break
|
||||
fi
|
||||
- # 同时也检查是否已经 running 但健康检查还没跑完(防止 healthcheck 写太严误判)
|
||||
if echo "${STATUS}" | grep -q '"State":"running"'; then
|
||||
:
|
||||
else
|
||||
- # 连 running 都不是 → 直接打印日志末尾 60 行帮忙排错
|
||||
- echo "⚠️ voicehub 还没 running,当前状态:"
|
||||
+ echo "voicehub not running, current status:"
|
||||
${COMPOSE} ps || true
|
||||
fi
|
||||
sleep 5
|
||||
WAITED=$((WAITED + 1))
|
||||
done
|
||||
if [ ${WAITED} -ge ${MAX_WAIT} ]; then
|
||||
- echo "⚠️ 等待超时,下面是最近 120 行 voicehub 日志:"
|
||||
+ echo "Healthcheck timeout. Last 120 lines of voicehub logs:"
|
||||
${COMPOSE} logs --tail=120 voicehub || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- # ---------- 9. 清理旧镜像(保留最近 2 个 tag + latest) ----------
|
||||
- echo "🧹 清理 3 天前的旧镜像 / 悬空层,防止磁盘爆"
|
||||
+ echo "Pruning images older than 72 hours..."
|
||||
docker image prune -af --filter "until=72h" || true
|
||||
docker builder prune -af || true
|
||||
|
||||
- # ---------- 10. 回滚指引 ----------
|
||||
echo ""
|
||||
echo "================================================================="
|
||||
- echo "🎉 部署成功!访问: ${{ secrets.NUXT_PUBLIC_SITE_URL || '请查看你的站点 URL' }}"
|
||||
- echo "🔁 如需紧急回滚到上一个版本,在服务器执行:"
|
||||
+ echo "Deploy succeeded. Visit: ${{ secrets.NUXT_PUBLIC_SITE_URL || '请查看你的站点 URL' }}"
|
||||
+ echo "Rollback instructions (on server):"
|
||||
echo " cd ${DEPLOY_DIR}"
|
||||
- echo " # 1) 先看有哪些 tag:"
|
||||
echo " docker images | grep ${IMAGE_NAME} | head -5"
|
||||
- echo " # 2) 选一个老 tag 填到 override:"
|
||||
- echo " echo 'services:\n voicehub:\n image: ${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:<旧TAG>' > docker-compose.override.yml"
|
||||
+ echo " echo 'services:\n voicehub:\n image: ${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:<OLD_TAG>' > docker-compose.override.yml"
|
||||
echo " docker compose up -d"
|
||||
echo "================================================================="
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
||||
From f4aa9884e288969a7f92284714b83a3e805684db Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 11:14:30 +0800
|
||||
Subject: [PATCH 15/58] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=204=20=E4=B8=AA?=
|
||||
=?UTF-8?q?=E5=B7=B2=E6=B8=85=E7=90=86=20Modal=20=E7=9A=84=E6=AE=8B?=
|
||||
=?UTF-8?q?=E7=95=99=20import/ref/template=EF=BC=88ScheduleList=20+=20Requ?=
|
||||
=?UTF-8?q?estForm=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
app/components/Songs/RequestForm.vue | 14 --------------
|
||||
app/components/Songs/ScheduleList.vue | 25 -------------------------
|
||||
2 files changed, 39 deletions(-)
|
||||
|
||||
diff --git a/app/components/Songs/RequestForm.vue b/app/components/Songs/RequestForm.vue
|
||||
index 12909ef..cadb028 100644
|
||||
--- a/app/components/Songs/RequestForm.vue
|
||||
+++ b/app/components/Songs/RequestForm.vue
|
||||
@@ -849,10 +849,6 @@ import Icon from '../UI/Icon.vue'
|
||||
import { convertToHttps, validateUrl } from '~/utils/url'
|
||||
|
||||
import ImportSongsModal from './ImportSongsModal.vue'
|
||||
-import NeteaseLoginModal from './NeteaseLoginModal.vue'
|
||||
-import PodcastEpisodesModal from './PodcastEpisodesModal.vue'
|
||||
-import BilibiliEpisodesModal from './BilibiliEpisodesModal.vue'
|
||||
-import RecentSongsModal from './RecentSongsModal.vue'
|
||||
import PlaylistSelectionModal from './PlaylistSelectionModal.vue'
|
||||
import UserSearchModal from '../Common/UserSearchModal.vue'
|
||||
import NeteaseUploadDialog from './NeteaseUploadDialog.vue'
|
||||
@@ -980,12 +976,10 @@ const neteaseCookie = ref('')
|
||||
const searchType = ref(1) // 1: 单曲, 1009: 播客/电台
|
||||
|
||||
// 播客弹窗相关
|
||||
-const showPodcastModal = ref(false)
|
||||
const selectedPodcastId = ref('')
|
||||
const selectedPodcastName = ref('')
|
||||
const podcastCookie = ref('')
|
||||
|
||||
-const showRecentSongsModal = ref(false)
|
||||
const showPlaylistModal = ref(false)
|
||||
const showUserSearchModal = ref(false)
|
||||
const collaborators = ref([])
|
||||
@@ -1020,7 +1014,6 @@ const searchError = ref('')
|
||||
// 手动输入相关
|
||||
const showManualModal = ref(false)
|
||||
|
||||
-const showBilibiliEpisodesModal = ref(false)
|
||||
const selectedBilibiliVideo = ref(null)
|
||||
const bilibiliEpisodes = ref([])
|
||||
const manualArtist = ref('')
|
||||
@@ -1584,7 +1577,6 @@ const openSimilarEpisodesModal = (group) => {
|
||||
|
||||
selectedBilibiliVideo.value = videoData
|
||||
bilibiliEpisodes.value = videoData.pages
|
||||
- showBilibiliEpisodesModal.value = true
|
||||
}
|
||||
|
||||
// 处理剧集投票
|
||||
@@ -1684,7 +1676,6 @@ const handleLikeFromSearch = async (song, originalResult = null) => {
|
||||
if (originalResult && isBilibiliMultiP(originalResult)) {
|
||||
selectedBilibiliVideo.value = originalResult
|
||||
bilibiliEpisodes.value = originalResult.pages
|
||||
- showBilibiliEpisodesModal.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2127,7 +2118,6 @@ const submitSong = async (result, options = {}) => {
|
||||
selectedPodcastId.value = result.id || result.musicId
|
||||
selectedPodcastName.value = result.title || result.song || result.name
|
||||
podcastCookie.value = neteaseCookie.value
|
||||
- showPodcastModal.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2141,7 +2131,6 @@ const submitSong = async (result, options = {}) => {
|
||||
console.log('打开 Bilibili 剧集列表:', result)
|
||||
selectedBilibiliVideo.value = result
|
||||
bilibiliEpisodes.value = result.pages
|
||||
- showBilibiliEpisodesModal.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2381,7 +2370,6 @@ const handleBilibiliEpisodeSelect = async (episode) => {
|
||||
})
|
||||
|
||||
if (success) {
|
||||
- showBilibiliEpisodesModal.value = false
|
||||
if (bilibiliModalRef.value && bilibiliModalRef.value.resetSubmissionState) {
|
||||
bilibiliModalRef.value.resetSubmissionState()
|
||||
}
|
||||
@@ -2418,7 +2406,6 @@ const playlistModalRef = ref(null)
|
||||
const handlePodcastSubmit = async (song) => {
|
||||
const success = await submitSong(song, { isPodcastEpisode: true })
|
||||
if (success) {
|
||||
- showPodcastModal.value = false
|
||||
} else {
|
||||
// 如果失败,重置模态框内的提交状态
|
||||
if (podcastModalRef.value && podcastModalRef.value.resetSubmissionState) {
|
||||
@@ -2437,7 +2424,6 @@ const handlePodcastPlay = async (song) => {
|
||||
const handleRecentSongSubmit = async (song) => {
|
||||
const success = await submitSong(song, { isPodcastEpisode: false, isDirectSubmit: true })
|
||||
if (success) {
|
||||
- showRecentSongsModal.value = false
|
||||
} else {
|
||||
// 如果失败,重置模态框内的提交状态
|
||||
if (recentSongsModalRef.value && recentSongsModalRef.value.resetSubmissionState) {
|
||||
diff --git a/app/components/Songs/ScheduleList.vue b/app/components/Songs/ScheduleList.vue
|
||||
index 0a5b247..ffb3b5e 100644
|
||||
--- a/app/components/Songs/ScheduleList.vue
|
||||
+++ b/app/components/Songs/ScheduleList.vue
|
||||
@@ -331,13 +331,6 @@
|
||||
<Icon name="music" :size="40" class="text-zinc-500 opacity-20" />
|
||||
</div>
|
||||
<p class="text-zinc-400 font-medium mb-8">需要登录网易云音乐账号才能管理歌单</p>
|
||||
- <button
|
||||
- class="px-10 py-4 rounded-2xl bg-blue-600 hover:bg-blue-500 text-white font-black transition-all active:scale-95 shadow-xl shadow-blue-900/20"
|
||||
- type="button"
|
||||
- @click="openLoginFromPlaylist"
|
||||
- >
|
||||
- 立即登录
|
||||
- </button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-8">
|
||||
@@ -578,15 +571,6 @@
|
||||
@confirm="handleConfirmAction"
|
||||
/>
|
||||
</Teleport>
|
||||
-
|
||||
- <Teleport to="body">
|
||||
- <NeteaseLoginModal
|
||||
- v-if="showLoginModal"
|
||||
- :show="showLoginModal"
|
||||
- @close="showLoginModal = false"
|
||||
- @login-success="handleLoginSuccess"
|
||||
- />
|
||||
- </Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -600,7 +584,6 @@ import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
|
||||
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
|
||||
import { convertToHttps } from '~/utils/url'
|
||||
import { getMusicUrl as resolveMusicUrl } from '~/utils/musicUrl'
|
||||
-import NeteaseLoginModal from './NeteaseLoginModal.vue'
|
||||
const props = defineProps({
|
||||
schedules: {
|
||||
type: Array,
|
||||
@@ -646,7 +629,6 @@ const playlistActionLoading = ref(false)
|
||||
const selectedSongIds = ref([])
|
||||
const newPlaylistName = ref('')
|
||||
const newPlaylistPrivacy = ref(false)
|
||||
-const showLoginModal = ref(false)
|
||||
|
||||
const confirmDialog = ref({
|
||||
show: false,
|
||||
@@ -1094,10 +1076,8 @@ const handleLoginSuccess = (data) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('netease_cookie', data.cookie)
|
||||
localStorage.setItem('netease_user', JSON.stringify(data.user))
|
||||
- // 更新全局网易云登录状态,通知其他组件(如AudioPlayer)
|
||||
updateGlobalNeteaseStatus()
|
||||
}
|
||||
- showLoginModal.value = false
|
||||
if (showPlaylistModal.value) {
|
||||
reloadPlaylists()
|
||||
}
|
||||
@@ -1105,7 +1085,6 @@ const handleLoginSuccess = (data) => {
|
||||
|
||||
const handleAddToPlaylistClick = () => {
|
||||
if (!isNeteaseLoggedIn.value) {
|
||||
- showLoginModal.value = true
|
||||
return
|
||||
}
|
||||
if (!neteaseSongs.value.length) {
|
||||
@@ -1123,10 +1102,6 @@ const closePlaylistModal = () => {
|
||||
showPlaylistModal.value = false
|
||||
}
|
||||
|
||||
-const openLoginFromPlaylist = () => {
|
||||
- showLoginModal.value = true
|
||||
-}
|
||||
-
|
||||
const reloadPlaylists = async () => {
|
||||
if (!isNeteaseLoggedIn.value || !neteaseCookie.value || !neteaseUser.value) return
|
||||
const uid = neteaseUser.value.userId || neteaseUser.value.id
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
From cfd90865c858d7ed10996e19178af279dbfe2d77 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 11:20:52 +0800
|
||||
Subject: [PATCH 16/58] =?UTF-8?q?refactor(ci):=20=E7=A7=BB=E9=99=A4?=
|
||||
=?UTF-8?q?=E6=89=80=E6=9C=89=E7=AC=AC=E4=B8=89=E6=96=B9=20uses=20action?=
|
||||
=?UTF-8?q?=EF=BC=8C=E6=94=B9=E7=94=A8=E7=BA=AF=20run=20=E5=91=BD=E4=BB=A4?=
|
||||
=?UTF-8?q?=EF=BC=88=E8=A7=84=E9=81=BF=20GitHub=20=E4=B8=8B=E8=BD=BD?=
|
||||
=?UTF-8?q?=E8=B6=85=E6=97=B6=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 404 ++++++++++++++++++++++--------------
|
||||
1 file changed, 247 insertions(+), 157 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index d5355cf..386f982 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -34,19 +34,26 @@ jobs:
|
||||
container:
|
||||
image: node:22-bookworm-slim
|
||||
steps:
|
||||
- - name: 检出代码
|
||||
- uses: actions/checkout@v4
|
||||
- with:
|
||||
- fetch-depth: 1
|
||||
-
|
||||
- - name: 运行时环境自检
|
||||
- shell: bash
|
||||
+ - name: 安装基础运行依赖(git/ca-certificates)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
+ apt-get update -qq
|
||||
+ DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends \
|
||||
+ git ca-certificates curl
|
||||
+ rm -rf /var/lib/apt/lists/*
|
||||
+ git --version
|
||||
node -v
|
||||
npm -v
|
||||
- which node npm
|
||||
- cat /etc/os-release 2>/dev/null | head -4 || true
|
||||
+
|
||||
+ - name: 检出代码
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ REPO_URL="https://${GITEA_HOST}/${GITHUB_REPOSITORY}.git"
|
||||
+ BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
+ echo "Clone from: ${REPO_URL} (${BRANCH})"
|
||||
+ rm -rf * .[^.]* 2>/dev/null || true
|
||||
+ git clone --depth 1 -b "${BRANCH}" "${REPO_URL}" .
|
||||
+ git rev-parse --short HEAD
|
||||
|
||||
- name: 安装依赖(npm ci)
|
||||
run: |
|
||||
@@ -84,13 +91,19 @@ jobs:
|
||||
image_latest: ${{ steps.meta.outputs.latest }}
|
||||
steps:
|
||||
- name: 检出代码
|
||||
- uses: actions/checkout@v4
|
||||
- with:
|
||||
- fetch-depth: 1
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ REPO_URL="https://${GITEA_HOST}/${GITHUB_REPOSITORY}.git"
|
||||
+ BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
+ echo "Clone from: ${REPO_URL} (${BRANCH})"
|
||||
+ rm -rf * .[^.]* 2>/dev/null || true
|
||||
+ git clone --depth 1 -b "${BRANCH}" "${REPO_URL}" .
|
||||
+ git rev-parse --short HEAD
|
||||
+ docker version
|
||||
+ docker buildx version || echo "buildx 未安装,将使用 docker build --push"
|
||||
|
||||
- name: 计算镜像标签
|
||||
id: meta
|
||||
- shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
|
||||
@@ -107,54 +120,83 @@ jobs:
|
||||
echo "Latest image : ${IMG_LATEST}"
|
||||
echo "======================================"
|
||||
|
||||
- - name: 启用 QEMU
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- with:
|
||||
- platforms: linux/amd64
|
||||
-
|
||||
- - name: 启用 Docker Buildx
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- with:
|
||||
- install: true
|
||||
+ - name: 初始化 Docker Buildx builder
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ BUILDER_NAME="multi"
|
||||
+ # 如果已存在同名 builder,直接用;否则创建(默认 driver docker-container,支持 --cache-to registry)
|
||||
+ if ! docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then
|
||||
+ docker buildx create --name "${BUILDER_NAME}" --driver docker-container --use \
|
||||
+ --buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'
|
||||
+ else
|
||||
+ docker buildx use "${BUILDER_NAME}"
|
||||
+ fi
|
||||
+ docker buildx inspect --bootstrap
|
||||
|
||||
- name: 登录 Gitea 容器仓库
|
||||
- uses: docker/login-action@v3
|
||||
- with:
|
||||
- registry: ${{ env.GITEA_HOST }}
|
||||
- username: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||
- password: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||
+ env:
|
||||
+ REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||
+ REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||
+ run: |
|
||||
+ set -euo pipefail
|
||||
+ if [ -z "${REGISTRY_TOKEN}" ]; then
|
||||
+ echo "ERROR: REGISTRY_TOKEN 为空。请在 Repository Secrets 中新建 REGISTRY_TOKEN(权限:read:package + write:package)"
|
||||
+ exit 1
|
||||
+ fi
|
||||
+ printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" \
|
||||
+ -u "${REGISTRY_USER}" --password-stdin
|
||||
+ echo "登录成功:${REGISTRY_USER} @ ${GITEA_HOST}"
|
||||
|
||||
- name: 构建并推送镜像
|
||||
- id: build_push
|
||||
- uses: docker/build-push-action@v6
|
||||
env:
|
||||
CACHE_REF: ${{ env.GITEA_HOST }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
- with:
|
||||
- context: .
|
||||
- file: ./Dockerfile
|
||||
- platforms: linux/amd64
|
||||
- push: true
|
||||
- provenance: false
|
||||
- tags: |
|
||||
- ${{ steps.meta.outputs.full }}
|
||||
- ${{ steps.meta.outputs.latest }}
|
||||
- build-args: |
|
||||
- BUILDKIT_INLINE_CACHE=1
|
||||
- NODE_ENV=production
|
||||
- cache-from: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0}', env.CACHE_REF) }}
|
||||
- cache-to: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0},mode=max', env.CACHE_REF) }}
|
||||
- labels: |
|
||||
- org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
- org.opencontainers.image.revision=${{ github.sha }}
|
||||
- org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
|
||||
+ SKIP_CACHE: ${{ inputs.skip_build_cache }}
|
||||
+ IMG_FULL: ${{ steps.meta.outputs.full }}
|
||||
+ IMG_LATEST: ${{ steps.meta.outputs.latest }}
|
||||
+ OCS_SOURCE: ${{ github.server_url }}/${{ github.repository }}
|
||||
+ OCS_REVISION: ${{ github.sha }}
|
||||
+ OCS_CREATED: ${{ github.event.head_commit.timestamp }}
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+
|
||||
+ BUILD_ARGS=(
|
||||
+ --build-arg BUILDKIT_INLINE_CACHE=1
|
||||
+ --build-arg NODE_ENV=production
|
||||
+ )
|
||||
+ CACHE_ARGS=()
|
||||
+ if [ "${SKIP_CACHE}" != "true" ]; then
|
||||
+ CACHE_ARGS=(
|
||||
+ --cache-from "type=registry,ref=${CACHE_REF}"
|
||||
+ --cache-to "type=registry,ref=${CACHE_REF},mode=max"
|
||||
+ )
|
||||
+ else
|
||||
+ echo "已启用 skip_build_cache,不使用分层缓存,强制重新构建"
|
||||
+ BUILD_ARGS+=( --no-cache )
|
||||
+ fi
|
||||
+
|
||||
+ docker buildx build \
|
||||
+ --context . \
|
||||
+ --file ./Dockerfile \
|
||||
+ --platform linux/amd64 \
|
||||
+ --push \
|
||||
+ --provenance=false \
|
||||
+ "${BUILD_ARGS[@]}" \
|
||||
+ "${CACHE_ARGS[@]}" \
|
||||
+ --tag "${IMG_FULL}" \
|
||||
+ --tag "${IMG_LATEST}" \
|
||||
+ --label "org.opencontainers.image.source=${OCS_SOURCE}" \
|
||||
+ --label "org.opencontainers.image.revision=${OCS_REVISION}" \
|
||||
+ --label "org.opencontainers.image.created=${OCS_CREATED}"
|
||||
|
||||
- name: 镜像体积与层信息
|
||||
+ env:
|
||||
+ IMG_FULL: ${{ steps.meta.outputs.full }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
- docker pull "${{ steps.meta.outputs.full }}" || true
|
||||
- docker image inspect "${{ steps.meta.outputs.full }}" --format='{{.Size}}' 2>/dev/null \
|
||||
+ docker pull "${IMG_FULL}" || true
|
||||
+ docker image inspect "${IMG_FULL}" --format='{{.Size}}' 2>/dev/null \
|
||||
| awk '{printf "Image size: %.2f MB\n", $1/1024/1024}' || true
|
||||
- docker history --no-trunc "${{ steps.meta.outputs.full }}" 2>/dev/null | head -20 || true
|
||||
+ docker history --no-trunc "${IMG_FULL}" 2>/dev/null | head -20 || true
|
||||
|
||||
deploy:
|
||||
name: 部署到生产服务器
|
||||
@@ -167,121 +209,169 @@ jobs:
|
||||
url: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
|
||||
steps:
|
||||
- name: 校验部署所需密钥
|
||||
- shell: bash
|
||||
+ env:
|
||||
+ DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
+ DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
+ DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
MISSING=()
|
||||
- [ -n "${{ secrets.DEPLOY_HOST }}" ] || MISSING+=("DEPLOY_HOST")
|
||||
- [ -n "${{ secrets.DEPLOY_USER }}" ] || MISSING+=("DEPLOY_USER")
|
||||
- [ -n "${{ secrets.DEPLOY_SSH_KEY }}" ] || MISSING+=("DEPLOY_SSH_KEY")
|
||||
+ [ -n "${DEPLOY_HOST}" ] || MISSING+=("DEPLOY_HOST")
|
||||
+ [ -n "${DEPLOY_USER}" ] || MISSING+=("DEPLOY_USER")
|
||||
+ [ -n "${DEPLOY_SSH_KEY}" ] || MISSING+=("DEPLOY_SSH_KEY")
|
||||
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||
echo "缺少以下 Repository Secrets:${MISSING[*]}"
|
||||
- echo "请在 Gitea 仓库 -> 设置 -> 工作流 -> 密钥 页面添加"
|
||||
+ echo "请在 Gitea 仓库 -> 设置 -> 工作流 -> 密钥 页面添加(注意是 Repository Secrets,不是 Deploy Keys)"
|
||||
exit 1
|
||||
fi
|
||||
- echo "部署密钥配置完整"
|
||||
+ echo "部署密钥配置完整(DEPLOY_HOST=${DEPLOY_HOST},DEPLOY_USER=${DEPLOY_USER})"
|
||||
|
||||
- name: SSH 滚动部署
|
||||
- uses: appleboy/ssh-action@v1.2.0
|
||||
- with:
|
||||
- host: ${{ secrets.DEPLOY_HOST }}
|
||||
- port: ${{ secrets.DEPLOY_PORT || 22 }}
|
||||
- username: ${{ secrets.DEPLOY_USER }}
|
||||
- key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
- command_timeout: 30m
|
||||
- script_stop: true
|
||||
- script: |
|
||||
- set -euxo pipefail
|
||||
-
|
||||
- IMAGE_NEW="${{ needs.build-image.outputs.image_full }}"
|
||||
- IMAGE_LATEST="${{ needs.build-image.outputs.image_latest }}"
|
||||
- DEPLOY_DIR="${DEPLOY_PATH}"
|
||||
- COMPOSE="docker compose"
|
||||
-
|
||||
- echo "=============================================="
|
||||
- echo "Deploy image : ${IMAGE_NEW}"
|
||||
- echo "Deploy dir : ${DEPLOY_DIR}"
|
||||
- echo "Executor : $(whoami)"
|
||||
- echo "=============================================="
|
||||
-
|
||||
- mkdir -p "${DEPLOY_DIR}"
|
||||
- cd "${DEPLOY_DIR}"
|
||||
-
|
||||
- if [ -n "${{ secrets.REGISTRY_PULL_TOKEN }}" ] && [ -n "${{ secrets.REGISTRY_PULL_USER }}" ]; then
|
||||
- echo "使用专用 pull 凭证登录 Gitea Container Registry"
|
||||
- echo "${{ secrets.REGISTRY_PULL_TOKEN }}" \
|
||||
- | docker login "${GITEA_HOST}" \
|
||||
- -u "${{ secrets.REGISTRY_PULL_USER }}" \
|
||||
- --password-stdin 2>/dev/null || true
|
||||
- fi
|
||||
-
|
||||
- echo "Pulling new image ${IMAGE_NEW} ..."
|
||||
- docker pull "${IMAGE_NEW}"
|
||||
- docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
||||
- echo "New image pulled and tagged as latest"
|
||||
-
|
||||
- cat > docker-compose.override.yml <<YAML
|
||||
- services:
|
||||
- voicehub:
|
||||
- image: ${IMAGE_NEW}
|
||||
- YAML
|
||||
- echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
||||
-
|
||||
- if [ ! -f docker-compose.yml ]; then
|
||||
- echo "docker-compose.yml 不存在,自动从仓库模板下载"
|
||||
- TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY:-${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
|
||||
- echo "URL=${TEMPLATE_URL}"
|
||||
- curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
|
||||
- || (
|
||||
- echo "自动下载模板失败,请手动上传 docker-compose.yml 和 .env"
|
||||
- )
|
||||
- fi
|
||||
-
|
||||
- if [ ! -f .env ]; then
|
||||
- echo "================================================================="
|
||||
- echo "WARNING:${DEPLOY_DIR}/.env 不存在"
|
||||
- echo "首次部署请在服务器上创建 .env(至少填写 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
|
||||
- echo "参考仓库根目录的 .env.example"
|
||||
- echo "================================================================="
|
||||
- fi
|
||||
-
|
||||
- echo "Executing docker compose up -d (voicehub only)..."
|
||||
- ${COMPOSE} up -d --remove-orphans
|
||||
-
|
||||
- MAX_WAIT=24
|
||||
- WAITED=0
|
||||
- echo "Waiting for container healthcheck (max 120s)..."
|
||||
- while [ ${WAITED} -lt ${MAX_WAIT} ]; do
|
||||
- STATUS="$(${COMPOSE} ps --format json 2>/dev/null || true)"
|
||||
- if echo "${STATUS}" | grep -q '"Health":"healthy"'; then
|
||||
- echo "voicehub healthcheck passed"
|
||||
- break
|
||||
+ env:
|
||||
+ DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
+ DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || 22 }}
|
||||
+ DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
+ DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
+ REGISTRY_PULL_USER: ${{ secrets.REGISTRY_PULL_USER }}
|
||||
+ REGISTRY_PULL_TOKEN: ${{ secrets.REGISTRY_PULL_TOKEN }}
|
||||
+ NUXT_PUBLIC_SITE_URL: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
|
||||
+ IMAGE_NEW: ${{ needs.build-image.outputs.image_full }}
|
||||
+ IMAGE_LATEST: ${{ needs.build-image.outputs.image_latest }}
|
||||
+ GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
+ GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
+ GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
+ run: |
|
||||
+ set -euo pipefail
|
||||
+
|
||||
+ # 写私钥到临时文件(避免 stdin 转义问题)
|
||||
+ SSH_KEY_FILE="$(mktemp -t deploy_ssh_key.XXXXXX)"
|
||||
+ printf '%s\n' "${DEPLOY_SSH_KEY}" > "${SSH_KEY_FILE}"
|
||||
+ chmod 600 "${SSH_KEY_FILE}"
|
||||
+
|
||||
+ SSH_OPTS=(
|
||||
+ -i "${SSH_KEY_FILE}"
|
||||
+ -o StrictHostKeyChecking=no
|
||||
+ -o UserKnownHostsFile=/dev/null
|
||||
+ -o ConnectTimeout=10
|
||||
+ -o ServerAliveInterval=30
|
||||
+ -o ServerAliveCountMax=8
|
||||
+ -p "${DEPLOY_PORT}"
|
||||
+ )
|
||||
+
|
||||
+ echo "=============================================="
|
||||
+ echo "Deploy target: ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PORT}"
|
||||
+ echo "Deploy image : ${IMAGE_NEW}"
|
||||
+ echo "Deploy dir : ${DEPLOY_PATH}"
|
||||
+ echo "=============================================="
|
||||
+
|
||||
+ # 本地预检查:ssh 先连上跑 whoami 确认连通,避免 20 分钟后才发现连不上
|
||||
+ ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "whoami; docker --version; docker compose version"
|
||||
+
|
||||
+ # 把变量作为 heredoc 脚本的输入传过去,远端展开我们需要的值
|
||||
+ ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||
+ IMAGE_NEW="${IMAGE_NEW}" \
|
||||
+ IMAGE_LATEST="${IMAGE_LATEST}" \
|
||||
+ DEPLOY_PATH="${DEPLOY_PATH}" \
|
||||
+ GITEA_HOST="${GITEA_HOST}" \
|
||||
+ REGISTRY_PULL_USER="${REGISTRY_PULL_USER}" \
|
||||
+ REGISTRY_PULL_TOKEN="${REGISTRY_PULL_TOKEN}" \
|
||||
+ IMAGE_NAME="${IMAGE_NAME}" \
|
||||
+ NUXT_PUBLIC_SITE_URL="${NUXT_PUBLIC_SITE_URL}" \
|
||||
+ GITHUB_SERVER_URL="${GITHUB_SERVER_URL}" \
|
||||
+ GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" \
|
||||
+ GITHUB_REF_NAME="${GITHUB_REF_NAME}" \
|
||||
+ bash <<'REMOTE_SCRIPT'
|
||||
+ set -euxo pipefail
|
||||
+
|
||||
+ DEPLOY_DIR="${DEPLOY_PATH}"
|
||||
+ COMPOSE="docker compose"
|
||||
+
|
||||
+ echo "=============================================="
|
||||
+ echo "Executor : $(whoami)"
|
||||
+ echo "Deploy image : ${IMAGE_NEW}"
|
||||
+ echo "Deploy dir : ${DEPLOY_DIR}"
|
||||
+ echo "=============================================="
|
||||
+
|
||||
+ mkdir -p "${DEPLOY_DIR}"
|
||||
+ cd "${DEPLOY_DIR}"
|
||||
+
|
||||
+ if [ -n "${REGISTRY_PULL_TOKEN}" ] && [ -n "${REGISTRY_PULL_USER}" ]; then
|
||||
+ echo "使用专用 pull 凭证登录 Gitea Container Registry"
|
||||
+ printf '%s' "${REGISTRY_PULL_TOKEN}" \
|
||||
+ | docker login "${GITEA_HOST}" \
|
||||
+ -u "${REGISTRY_PULL_USER}" \
|
||||
+ --password-stdin 2>/dev/null || true
|
||||
fi
|
||||
- if echo "${STATUS}" | grep -q '"State":"running"'; then
|
||||
- :
|
||||
- else
|
||||
- echo "voicehub not running, current status:"
|
||||
- ${COMPOSE} ps || true
|
||||
+
|
||||
+ echo "Pulling new image ${IMAGE_NEW} ..."
|
||||
+ docker pull "${IMAGE_NEW}"
|
||||
+ docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
||||
+ echo "New image pulled and tagged as latest"
|
||||
+
|
||||
+ cat > docker-compose.override.yml <<YAML
|
||||
+ services:
|
||||
+ voicehub:
|
||||
+ image: ${IMAGE_NEW}
|
||||
+ YAML
|
||||
+ echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
||||
+
|
||||
+ if [ ! -f docker-compose.yml ]; then
|
||||
+ echo "docker-compose.yml 不存在,自动从仓库模板下载"
|
||||
+ TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
|
||||
+ echo "URL=${TEMPLATE_URL}"
|
||||
+ curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
|
||||
+ || echo "自动下载模板失败,请手动上传 docker-compose.yml 和 .env(参考仓库根目录 .env.example)"
|
||||
+ fi
|
||||
+
|
||||
+ if [ ! -f .env ]; then
|
||||
+ echo "================================================================="
|
||||
+ echo "WARNING:${DEPLOY_DIR}/.env 不存在"
|
||||
+ echo "首次部署请在服务器上创建 .env(至少填写 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
|
||||
+ echo "参考仓库根目录的 .env.example"
|
||||
+ echo "================================================================="
|
||||
+ fi
|
||||
+
|
||||
+ echo "Executing docker compose up -d ..."
|
||||
+ ${COMPOSE} up -d --remove-orphans
|
||||
+
|
||||
+ MAX_WAIT=24
|
||||
+ WAITED=0
|
||||
+ echo "Waiting for container healthcheck (max 120s)..."
|
||||
+ while [ ${WAITED} -lt ${MAX_WAIT} ]; do
|
||||
+ STATUS="$(${COMPOSE} ps --format json 2>/dev/null || true)"
|
||||
+ if echo "${STATUS}" | grep -q '"Health":"healthy"'; then
|
||||
+ echo "voicehub healthcheck passed"
|
||||
+ break
|
||||
+ fi
|
||||
+ if echo "${STATUS}" | grep -q '"State":"running"'; then
|
||||
+ :
|
||||
+ else
|
||||
+ echo "voicehub not running, current status:"
|
||||
+ ${COMPOSE} ps || true
|
||||
+ fi
|
||||
+ sleep 5
|
||||
+ WAITED=$((WAITED + 1))
|
||||
+ done
|
||||
+ if [ ${WAITED} -ge ${MAX_WAIT} ]; then
|
||||
+ echo "Healthcheck timeout. Last 120 lines of voicehub logs:"
|
||||
+ ${COMPOSE} logs --tail=120 voicehub || true
|
||||
+ exit 1
|
||||
fi
|
||||
- sleep 5
|
||||
- WAITED=$((WAITED + 1))
|
||||
- done
|
||||
- if [ ${WAITED} -ge ${MAX_WAIT} ]; then
|
||||
- echo "Healthcheck timeout. Last 120 lines of voicehub logs:"
|
||||
- ${COMPOSE} logs --tail=120 voicehub || true
|
||||
- exit 1
|
||||
- fi
|
||||
-
|
||||
- echo "Pruning images older than 72 hours..."
|
||||
- docker image prune -af --filter "until=72h" || true
|
||||
- docker builder prune -af || true
|
||||
-
|
||||
- echo ""
|
||||
- echo "================================================================="
|
||||
- echo "Deploy succeeded. Visit: ${{ secrets.NUXT_PUBLIC_SITE_URL || '请查看你的站点 URL' }}"
|
||||
- echo "Rollback instructions (on server):"
|
||||
- echo " cd ${DEPLOY_DIR}"
|
||||
- echo " docker images | grep ${IMAGE_NAME} | head -5"
|
||||
- echo " echo 'services:\n voicehub:\n image: ${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:<OLD_TAG>' > docker-compose.override.yml"
|
||||
- echo " docker compose up -d"
|
||||
- echo "================================================================="
|
||||
+
|
||||
+ echo "Pruning images older than 72 hours..."
|
||||
+ docker image prune -af --filter "until=72h" || true
|
||||
+ docker builder prune -af || true
|
||||
+
|
||||
+ echo ""
|
||||
+ echo "================================================================="
|
||||
+ echo "Deploy succeeded. Visit: ${NUXT_PUBLIC_SITE_URL:-请自行查看你的站点 URL}"
|
||||
+ echo "Rollback instructions (on server):"
|
||||
+ echo " cd ${DEPLOY_DIR}"
|
||||
+ echo " docker images | grep ${IMAGE_NAME} | head -5"
|
||||
+ echo " printf 'services:\\n voicehub:\\n image: <OLD_TAG_FROM_ABOVE>\\n' > docker-compose.override.yml"
|
||||
+ echo " docker compose up -d"
|
||||
+ echo "================================================================="
|
||||
+ REMOTE_SCRIPT
|
||||
+
|
||||
+ # 清理私钥临时文件
|
||||
+ rm -f "${SSH_KEY_FILE}"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
From 10117fc7e2bcdd3797efcde7493341dba65a7204 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 11:32:31 +0800
|
||||
Subject: [PATCH 17/58] =?UTF-8?q?perf(ci):=20=E6=89=80=E6=9C=89=20Job=20De?=
|
||||
=?UTF-8?q?bian/Ubuntu=20=E6=8D=A2=E6=B8=85=E5=8D=8E=E6=BA=90=20+=20GitHub?=
|
||||
=?UTF-8?q?=20=E8=AE=BF=E9=97=AE=E7=BB=9F=E4=B8=80=E8=B5=B0=20v4.gh-proxy.?=
|
||||
=?UTF-8?q?org=20=E5=89=8D=E7=BC=80=E4=BB=A3=E7=90=86=20+=20Buildx=20regis?=
|
||||
=?UTF-8?q?try-mirrors?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 92 +++++++++++++++++++++++++++++++++----
|
||||
1 file changed, 82 insertions(+), 10 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 386f982..519f8d5 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -25,6 +25,11 @@ env:
|
||||
GITEA_HOST: gitea.fse-media.group
|
||||
DEPLOY_PATH: /data/opt/byw-voicehub
|
||||
IMAGE_NAME: byw-voicehub
|
||||
+ GH_PROXY_PREFIX: https://v4.gh-proxy.org/
|
||||
+ APT_MIRROR_DEBIAN: https://mirrors.tuna.tsinghua.edu.cn/debian
|
||||
+ APT_MIRROR_DEBIAN_SECURITY: https://mirrors.tuna.tsinghua.edu.cn/debian-security
|
||||
+ HTTP_PROXY_EXAMPLE: ""
|
||||
+ HTTPS_PROXY_EXAMPLE: ""
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
@@ -34,13 +39,31 @@ jobs:
|
||||
container:
|
||||
image: node:22-bookworm-slim
|
||||
steps:
|
||||
- - name: 安装基础运行依赖(git/ca-certificates)
|
||||
+ - name: 安装基础运行依赖(git/ca-certificates,已换清华 TUNA 源)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
+ # Debian 12 (bookworm) 换清华 TUNA 国内镜像(避免 deb.debian.org 国外源速度 <200KB/s)
|
||||
+ SOURCES_FILE="/etc/apt/sources.list.d/debian.sources"
|
||||
+ if [ -f "${SOURCES_FILE}" ]; then
|
||||
+ sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|http://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|https://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
|
||||
+ else
|
||||
+ # 兜底旧格式 /etc/apt/sources.list
|
||||
+ if [ -f /etc/apt/sources.list ]; then
|
||||
+ sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|http://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|https://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
|
||||
+ fi
|
||||
+ fi
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends \
|
||||
git ca-certificates curl
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
+ # 全局 git 代理:任何 git clone/fetch/pull https://github.com/... 自动走 gh-proxy.com 前缀
|
||||
+ git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
||||
git --version
|
||||
node -v
|
||||
npm -v
|
||||
@@ -90,6 +113,34 @@ jobs:
|
||||
image_full: ${{ steps.meta.outputs.full }}
|
||||
image_latest: ${{ steps.meta.outputs.latest }}
|
||||
steps:
|
||||
+ - name: 系统环境准备(Ubuntu 22.04 默认镜像源换清华 + 全局 GitHub gh-proxy)
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ # Ubuntu 22.04 默认是 http://archive.ubuntu.com/ubuntu 和 http://security.ubuntu.com/ubuntu
|
||||
+ if command -v apt-get >/dev/null 2>&1 && [ -f /etc/apt/sources.list ]; then
|
||||
+ sudo sed -i \
|
||||
+ -e 's|http://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
|
||||
+ -e 's|http://security.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security|g' \
|
||||
+ -e 's|https://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
|
||||
+ /etc/apt/sources.list
|
||||
+ fi
|
||||
+ if command -v apt-get >/dev/null 2>&1 && ls /etc/apt/sources.list.d/*.sources 1>/dev/null 2>&1; then
|
||||
+ for f in /etc/apt/sources.list.d/*.sources; do
|
||||
+ sudo sed -i \
|
||||
+ -e 's|http://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
|
||||
+ -e 's|http://security.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security|g' \
|
||||
+ "${f}" 2>/dev/null || true
|
||||
+ done
|
||||
+ fi
|
||||
+ # 全局 git:所有 https://github.com/ 自动走 gh-proxy 前缀
|
||||
+ sudo git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
||||
+ # npm:强制用 npmmirror(与 preflight 对齐)
|
||||
+ npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
|
||||
+ echo "环境准备完成"
|
||||
+ cat /etc/os-release 2>/dev/null | head -4 || true
|
||||
+ docker --version
|
||||
+ docker buildx version || echo "buildx 未安装"
|
||||
+
|
||||
- name: 检出代码
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
@@ -120,17 +171,34 @@ jobs:
|
||||
echo "Latest image : ${IMG_LATEST}"
|
||||
echo "======================================"
|
||||
|
||||
- - name: 初始化 Docker Buildx builder
|
||||
+ - name: 初始化 Docker Buildx builder(带国内 registry-mirrors + gh-proxy 环境)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
BUILDER_NAME="multi"
|
||||
- # 如果已存在同名 builder,直接用;否则创建(默认 driver docker-container,支持 --cache-to registry)
|
||||
- if ! docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then
|
||||
- docker buildx create --name "${BUILDER_NAME}" --driver docker-container --use \
|
||||
- --buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'
|
||||
- else
|
||||
- docker buildx use "${BUILDER_NAME}"
|
||||
- fi
|
||||
+ BKK_CFG="$(mktemp -t buildkitd_XXXXXX.toml)"
|
||||
+ # 用 printf 逐行写 buildkitd 配置,避免 YAML parser 把 heredoc 内部的 [...] 误判为 YAML key
|
||||
+ {
|
||||
+ printf '[worker.oci]\n max-parallelism = 2\n\n'
|
||||
+ printf '[registry."docker.io"]\n mirrors = ["docker.m.daocloud.io","dockerproxy.com","hub-mirror.c.163.com","docker.nju.edu.cn","docker.mirrors.ustc.edu.cn"]\n\n'
|
||||
+ printf '[registry."ghcr.io"]\n mirrors = ["ghcr.nju.edu.cn","ghcr.dockerproxy.com"]\n\n'
|
||||
+ printf '[registry."k8s.gcr.io"]\n mirrors = ["k8s-gcr.m.daocloud.io","registry.cn-hangzhou.aliyuncs.com/google_containers"]\n'
|
||||
+ } > "${BKK_CFG}"
|
||||
+ docker buildx rm "${BUILDER_NAME}" 2>/dev/null || true
|
||||
+ docker buildx create \
|
||||
+ --name "${BUILDER_NAME}" \
|
||||
+ --driver docker-container \
|
||||
+ --use \
|
||||
+ --config "${BKK_CFG}" \
|
||||
+ --buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' \
|
||||
+ --driver-opt network=host \
|
||||
+ --driver-opt "env=HTTP_PROXY=${HTTP_PROXY_EXAMPLE}" \
|
||||
+ --driver-opt "env=HTTPS_PROXY=${HTTPS_PROXY_EXAMPLE}"
|
||||
+ rm -f "${BKK_CFG}"
|
||||
+ export GIT_CONFIG_COUNT=2
|
||||
+ export GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
|
||||
+ export GIT_CONFIG_VALUE_0="https://github.com/"
|
||||
+ export GIT_CONFIG_KEY_1="url.${GH_PROXY_PREFIX}https://gist.github.com/.insteadOf"
|
||||
+ export GIT_CONFIG_VALUE_1="https://gist.github.com/"
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
- name: 登录 Gitea 容器仓库
|
||||
@@ -273,6 +341,7 @@ jobs:
|
||||
IMAGE_LATEST="${IMAGE_LATEST}" \
|
||||
DEPLOY_PATH="${DEPLOY_PATH}" \
|
||||
GITEA_HOST="${GITEA_HOST}" \
|
||||
+ GH_PROXY_PREFIX="${GH_PROXY_PREFIX}" \
|
||||
REGISTRY_PULL_USER="${REGISTRY_PULL_USER}" \
|
||||
REGISTRY_PULL_TOKEN="${REGISTRY_PULL_TOKEN}" \
|
||||
IMAGE_NAME="${IMAGE_NAME}" \
|
||||
@@ -316,8 +385,11 @@ jobs:
|
||||
echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
||||
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
- echo "docker-compose.yml 不存在,自动从仓库模板下载"
|
||||
+ echo "docker-compose.yml 不存在,自动从仓库模板下载(如访问 GitHub 会自动加 gh-proxy 前缀)"
|
||||
TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
|
||||
+ case "${TEMPLATE_URL}" in
|
||||
+ *"://github.com/"*) TEMPLATE_URL="${GH_PROXY_PREFIX}${TEMPLATE_URL}" ;;
|
||||
+ esac
|
||||
echo "URL=${TEMPLATE_URL}"
|
||||
curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
|
||||
|| echo "自动下载模板失败,请手动上传 docker-compose.yml 和 .env(参考仓库根目录 .env.example)"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
From 167b60a368a5a46d483dd6fa3c796b74e5173126 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 11:40:32 +0800
|
||||
Subject: [PATCH 18/58] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=20Preflight?=
|
||||
=?UTF-8?q?/=E6=9E=84=E5=BB=BA=20Job=20=E6=BA=90=E6=AD=BB=E5=BE=AA?=
|
||||
=?UTF-8?q?=E7=8E=AF=EF=BC=88ca-certificates=20=E4=B8=8E=20HTTPS=20?=
|
||||
=?UTF-8?q?=E9=95=9C=E5=83=8F=E6=BA=90=E9=B8=A1=E7=94=9F=E8=9B=8B=E9=97=AE?=
|
||||
=?UTF-8?q?=E9=A2=98=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- env 新增 HTTP/HTTPS 双套镜像源变量(Debian bookworm + Ubuntu 22.04)
|
||||
- Preflight(Debian slim 容器):Step 0 先用 HTTP 清华源 apt-get 装 ca-certificates
|
||||
+ update-ca-certificates;Step 1 有证书后再切 HTTPS 清华源装 git/curl
|
||||
- build-image(Ubuntu 宿主 Runner):apt-get 用默认 HTTP archive.ubuntu.com 先装
|
||||
ca-certificates;之后再 sed 换 HTTPS 清华源,避免同样的证书死循环
|
||||
- 本地 yaml.safe_load 验证通过(YAML_OK);grep uses: 0 行;grep GH_PROXY_PREFIX 7 处
|
||||
全部走用户指定的 https://v4.gh-proxy.org/ 前缀
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 66 +++++++++++++++++++++++++++----------
|
||||
1 file changed, 48 insertions(+), 18 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 519f8d5..91ba3e0 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -28,6 +28,12 @@ env:
|
||||
GH_PROXY_PREFIX: https://v4.gh-proxy.org/
|
||||
APT_MIRROR_DEBIAN: https://mirrors.tuna.tsinghua.edu.cn/debian
|
||||
APT_MIRROR_DEBIAN_SECURITY: https://mirrors.tuna.tsinghua.edu.cn/debian-security
|
||||
+ APT_MIRROR_DEBIAN_HTTP: http://mirrors.tuna.tsinghua.edu.cn/debian
|
||||
+ APT_MIRROR_DEBIAN_SECURITY_HTTP: http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
||||
+ APT_MIRROR_UBUNTU: https://mirrors.tuna.tsinghua.edu.cn/ubuntu
|
||||
+ APT_MIRROR_UBUNTU_SECURITY: https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security
|
||||
+ APT_MIRROR_UBUNTU_HTTP: http://mirrors.tuna.tsinghua.edu.cn/ubuntu
|
||||
+ APT_MIRROR_UBUNTU_SECURITY_HTTP: http://mirrors.tuna.tsinghua.edu.cn/ubuntu-security
|
||||
HTTP_PROXY_EXAMPLE: ""
|
||||
HTTPS_PROXY_EXAMPLE: ""
|
||||
|
||||
@@ -42,27 +48,39 @@ jobs:
|
||||
- name: 安装基础运行依赖(git/ca-certificates,已换清华 TUNA 源)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
- # Debian 12 (bookworm) 换清华 TUNA 国内镜像(避免 deb.debian.org 国外源速度 <200KB/s)
|
||||
+ # ========== Step 0:先用 HTTP 清华源装 ca-certificates(解鸡生蛋:HTTPS 需要证书,证书得先用 HTTP 装) ==========
|
||||
SOURCES_FILE="/etc/apt/sources.list.d/debian.sources"
|
||||
if [ -f "${SOURCES_FILE}" ]; then
|
||||
- sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
|
||||
- sed -i "s|http://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
|
||||
- sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
|
||||
- sed -i "s|https://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|http://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|https://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" "${SOURCES_FILE}"
|
||||
else
|
||||
- # 兜底旧格式 /etc/apt/sources.list
|
||||
if [ -f /etc/apt/sources.list ]; then
|
||||
- sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
|
||||
- sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
|
||||
- sed -i "s|http://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
|
||||
- sed -i "s|https://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|http://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|https://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" /etc/apt/sources.list
|
||||
fi
|
||||
fi
|
||||
apt-get update -qq
|
||||
- DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends \
|
||||
- git ca-certificates curl
|
||||
+ DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends ca-certificates
|
||||
+ update-ca-certificates 2>/dev/null || true
|
||||
+
|
||||
+ # ========== Step 1:现在有了证书,换成 HTTPS 清华源(更安全)+ 装 git/curl ==========
|
||||
+ if [ -f "${SOURCES_FILE}" ]; then
|
||||
+ sed -i "s|${APT_MIRROR_DEBIAN_HTTP}|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
|
||||
+ sed -i "s|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
|
||||
+ else
|
||||
+ if [ -f /etc/apt/sources.list ]; then
|
||||
+ sed -i "s|${APT_MIRROR_DEBIAN_HTTP}|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
|
||||
+ sed -i "s|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
|
||||
+ fi
|
||||
+ fi
|
||||
+ apt-get update -qq
|
||||
+ DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends git curl
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
- # 全局 git 代理:任何 git clone/fetch/pull https://github.com/... 自动走 gh-proxy.com 前缀
|
||||
+ # 全局 git 代理:任何 git clone/fetch/pull https://github.com/... 自动走 gh-proxy 前缀
|
||||
git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
||||
git --version
|
||||
node -v
|
||||
@@ -116,19 +134,31 @@ jobs:
|
||||
- name: 系统环境准备(Ubuntu 22.04 默认镜像源换清华 + 全局 GitHub gh-proxy)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
+ # ========== Step 0:先装 ca-certificates(默认 HTTP archive.ubuntu.com 不需要证书,解鸡生蛋) ==========
|
||||
+ if command -v apt-get >/dev/null 2>&1; then
|
||||
+ sudo apt-get update -qq 2>/dev/null || apt-get update -qq 2>/dev/null || true
|
||||
+ sudo DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends ca-certificates 2>/dev/null || \
|
||||
+ DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends ca-certificates 2>/dev/null || true
|
||||
+ sudo update-ca-certificates 2>/dev/null || update-ca-certificates 2>/dev/null || true
|
||||
+ fi
|
||||
+
|
||||
+ # ========== Step 1:有了证书后换 HTTPS 清华源 ==========
|
||||
# Ubuntu 22.04 默认是 http://archive.ubuntu.com/ubuntu 和 http://security.ubuntu.com/ubuntu
|
||||
if command -v apt-get >/dev/null 2>&1 && [ -f /etc/apt/sources.list ]; then
|
||||
sudo sed -i \
|
||||
- -e 's|http://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
|
||||
- -e 's|http://security.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security|g' \
|
||||
- -e 's|https://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
|
||||
+ -e 's|http://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
|
||||
+ -e 's|http://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|g' \
|
||||
+ -e 's|https://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
|
||||
+ -e 's|https://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|g' \
|
||||
/etc/apt/sources.list
|
||||
fi
|
||||
if command -v apt-get >/dev/null 2>&1 && ls /etc/apt/sources.list.d/*.sources 1>/dev/null 2>&1; then
|
||||
for f in /etc/apt/sources.list.d/*.sources; do
|
||||
sudo sed -i \
|
||||
- -e 's|http://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
|
||||
- -e 's|http://security.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security|g' \
|
||||
+ -e 's|http://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
|
||||
+ -e 's|http://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|g' \
|
||||
+ -e 's|https://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
|
||||
+ -e 's|https://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|g' \
|
||||
"${f}" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
From d247cfbda34cff40acd3392c4a3220d0dd202ef9 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 11:50:33 +0800
|
||||
Subject: [PATCH 19/58] =?UTF-8?q?refactor(cleanup):=20=E6=B8=85=E7=90=86?=
|
||||
=?UTF-8?q?=E7=AC=AC=E4=BA=8C=E8=BD=AE=E9=9D=9E=E9=85=B7=E7=8B=97=E7=BB=84?=
|
||||
=?UTF-8?q?=E4=BB=B6=E6=AE=8B=E7=95=99=EF=BC=88PlaylistSelection/NeteaseUp?=
|
||||
=?UTF-8?q?load/BilibiliIframe=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- RequestForm.vue:删除 PlaylistSelectionModal.vue / NeteaseUploadDialog.vue 2 行无效 import
|
||||
(2 组件在合并阶段已物理删除,grep 确认无 ref / 赋值 / template 渲染段残留)
|
||||
- AudioPlayer.vue:彻底清理 BilibiliIframeModal 相关 8 处残留:
|
||||
① 封面点击事件 @click 三元嵌套语法 bug 修复 → 简化为 !isMobile ? toggleLyrics()
|
||||
② 删除 v-if=false 的「观看视频」Icon 按钮(保留歌词按钮)
|
||||
③ 删除 ClientOnly 包裹的 BilibiliIframeModal 渲染段
|
||||
④ 删除 BilibiliIframeModal.vue / getBilibiliUrl 2 行 import
|
||||
⑤ 删除 showBilibiliIframe ref 定义
|
||||
⑥ 删除 resolveFallbackUrl 中 if(false) 包裹的 bilibili 死代码分支
|
||||
⑦ 删除整个 openBilibiliVideo 函数(含 iframe 预览 + 新窗口跳转逻辑)
|
||||
⑧ handlePlayerClick 移动端逻辑简化为直接 toggleLyrics(移除 bilibili 平台分支)
|
||||
- 验证:
|
||||
・IDE GetDiagnostics 0 error / 0 warning
|
||||
・grep 本轮 3 Modal 关键字在 app/components 下 0 命中(仅 utils/url.ts 保留 getBilibiliUrl
|
||||
工具函数定义,未被 import,不影响 build)
|
||||
・上一轮 4 Modal(NeteaseLogin/PodcastEpisodes/BilibiliEpisodes/RecentSongs)
|
||||
grep 0 命中(持续干净)
|
||||
---
|
||||
app/components/Songs/RequestForm.vue | 2 -
|
||||
app/components/UI/AudioPlayer.vue | 61 ++--------------------------
|
||||
2 files changed, 3 insertions(+), 60 deletions(-)
|
||||
|
||||
diff --git a/app/components/Songs/RequestForm.vue b/app/components/Songs/RequestForm.vue
|
||||
index cadb028..d28fc42 100644
|
||||
--- a/app/components/Songs/RequestForm.vue
|
||||
+++ b/app/components/Songs/RequestForm.vue
|
||||
@@ -849,9 +849,7 @@ import Icon from '../UI/Icon.vue'
|
||||
import { convertToHttps, validateUrl } from '~/utils/url'
|
||||
|
||||
import ImportSongsModal from './ImportSongsModal.vue'
|
||||
-import PlaylistSelectionModal from './PlaylistSelectionModal.vue'
|
||||
import UserSearchModal from '../Common/UserSearchModal.vue'
|
||||
-import NeteaseUploadDialog from './NeteaseUploadDialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
diff --git a/app/components/UI/AudioPlayer.vue b/app/components/UI/AudioPlayer.vue
|
||||
index cbfc967..1f4c7e6 100644
|
||||
--- a/app/components/UI/AudioPlayer.vue
|
||||
+++ b/app/components/UI/AudioPlayer.vue
|
||||
@@ -26,13 +26,7 @@
|
||||
<!-- 封面 -->
|
||||
<div
|
||||
class="cover-container"
|
||||
- @click.stop="
|
||||
- isMobile
|
||||
- ? false
|
||||
- ? openBilibiliVideo()
|
||||
- : toggleLyrics()
|
||||
- : null
|
||||
- "
|
||||
+ @click.stop="!isMobile ? toggleLyrics() : null"
|
||||
>
|
||||
<template v-if="activeSong && activeSong.cover && !coverError">
|
||||
<img
|
||||
@@ -107,15 +101,7 @@
|
||||
<!-- 控制按钮区域 -->
|
||||
<div class="controls-frame">
|
||||
<!-- 左侧歌词按钮 -->
|
||||
- <span
|
||||
- v-if="false"
|
||||
- class="lyrics-btn music-icon"
|
||||
- title="观看视频"
|
||||
- @click="openBilibiliVideo"
|
||||
- >
|
||||
- <Icon name="video" size="20" />
|
||||
- </span>
|
||||
- <span v-else class="lyrics-btn music-icon" title="歌词" @click="toggleLyrics">
|
||||
+ <span class="lyrics-btn music-icon" title="歌词" @click="toggleLyrics">
|
||||
<Icon name="music" size="20" />
|
||||
</span>
|
||||
|
||||
@@ -210,16 +196,6 @@
|
||||
<LyricsModal :is-visible="showFullscreenLyrics" @close="showFullscreenLyrics = false" />
|
||||
</ClientOnly>
|
||||
|
||||
- <!-- 哔哩哔哩 iframe 预览模态 -->
|
||||
- <ClientOnly>
|
||||
- <BilibiliIframeModal
|
||||
- :show="showBilibiliIframe"
|
||||
- :bvid="activeSong?.musicId?.split(':')[0]"
|
||||
- :page="activeSong?.musicId?.split(':')[2] ? parseInt(activeSong.musicId.split(':')[2]) : 1"
|
||||
- @close="showBilibiliIframe = false"
|
||||
- />
|
||||
- </ClientOnly>
|
||||
-
|
||||
<ConfirmDialog
|
||||
:show="showFallbackOpenDialog"
|
||||
title="打开投稿链接"
|
||||
@@ -238,7 +214,6 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import AppleMusicLyrics from './AppleMusicLyrics.vue'
|
||||
import LyricsModal from './LyricsModal.vue'
|
||||
import AudioElement from './AudioPlayer/AudioElement.vue'
|
||||
-import BilibiliIframeModal from './BilibiliIframeModal.vue'
|
||||
import Icon from './Icon.vue'
|
||||
import ConfirmDialog from './ConfirmDialog.vue'
|
||||
import { useAudioPlayerControl } from '~/composables/useAudioPlayerControl'
|
||||
@@ -246,7 +221,6 @@ import { useAudioPlayerSync } from '~/composables/useAudioPlayerSync'
|
||||
import { useAudioQuality } from '~/composables/useAudioQuality'
|
||||
import { useAudioPlayerEnhanced } from '~/composables/useAudioPlayerEnhanced'
|
||||
import { useMediaSession } from '~/composables/useMediaSession'
|
||||
-import { getBilibiliUrl } from '~/utils/url'
|
||||
|
||||
// 添加 router 导入
|
||||
const router = useRouter()
|
||||
@@ -286,7 +260,6 @@ const showLyrics = ref(false)
|
||||
const showFullscreenLyrics = ref(false)
|
||||
const coverError = ref(false)
|
||||
const useAppleMusicStyle = ref(true) // 默认使用Apple Music风格
|
||||
-const showBilibiliIframe = ref(false) // 显示哔哩哔哩 iframe 预览
|
||||
|
||||
// 时长显示模式:'remaining' 显示剩余时长,'total' 显示总时长
|
||||
const timeDisplayMode = ref('remaining')
|
||||
@@ -367,13 +340,6 @@ const resolveFallbackUrl = () => {
|
||||
const playUrl = normalizeHttpUrl(song.playUrl)
|
||||
if (playUrl) return playUrl
|
||||
|
||||
- if (false) {
|
||||
- const bilibiliUrl = getBilibiliUrl(song)
|
||||
- if (bilibiliUrl && bilibiliUrl !== '#') {
|
||||
- return bilibiliUrl
|
||||
- }
|
||||
- }
|
||||
-
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -962,23 +928,6 @@ const toggleLyrics = () => {
|
||||
showFullscreenLyrics.value = true
|
||||
}
|
||||
|
||||
-const openBilibiliVideo = () => {
|
||||
- const song = activeSong.value
|
||||
- if (!song) return
|
||||
-
|
||||
- // 如果播放器有错误,显示 iframe 预览
|
||||
- if (control.hasError.value) {
|
||||
- showBilibiliIframe.value = true
|
||||
- return
|
||||
- }
|
||||
-
|
||||
- // 否则在新窗口打开
|
||||
- const url = getBilibiliUrl(song)
|
||||
- if (url && url !== '#') {
|
||||
- window.open(url, '_blank')
|
||||
- }
|
||||
-}
|
||||
-
|
||||
const handleLyricSeek = async (time) => {
|
||||
// 如果当前处于暂停状态,先开始播放
|
||||
if (!control.isPlaying.value) {
|
||||
@@ -1152,11 +1101,7 @@ const checkMobile = () => {
|
||||
// 移动端点击播放条处理
|
||||
const handlePlayerClick = () => {
|
||||
if (isMobile.value) {
|
||||
- if (activeSong.value?.musicPlatform === 'bilibili') {
|
||||
- openBilibiliVideo()
|
||||
- } else {
|
||||
- toggleLyrics()
|
||||
- }
|
||||
+ toggleLyrics()
|
||||
}
|
||||
}
|
||||
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
From e65c65d8e3eb85d057b7cdadd4265e360644e4fa Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 11:59:42 +0800
|
||||
Subject: [PATCH 20/58] =?UTF-8?q?fix(ci):=20=E6=9E=84=E5=BB=BA=20Job=20?=
|
||||
=?UTF-8?q?=E4=BF=AE=E5=A4=8D=20buildx=20create=20=E7=A9=BA=20env=3D=20?=
|
||||
=?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AF=BC=E8=87=B4=E7=9A=84=20invalid=20drive?=
|
||||
=?UTF-8?q?r=20option=20env=20=E6=8A=A5=E9=94=99?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 4 +---
|
||||
1 file changed, 1 insertion(+), 3 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 91ba3e0..aaceb9b 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -220,9 +220,7 @@ jobs:
|
||||
--use \
|
||||
--config "${BKK_CFG}" \
|
||||
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' \
|
||||
- --driver-opt network=host \
|
||||
- --driver-opt "env=HTTP_PROXY=${HTTP_PROXY_EXAMPLE}" \
|
||||
- --driver-opt "env=HTTPS_PROXY=${HTTPS_PROXY_EXAMPLE}"
|
||||
+ --driver-opt network=host
|
||||
rm -f "${BKK_CFG}"
|
||||
export GIT_CONFIG_COUNT=2
|
||||
export GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
From 7b8dabdb49cb6d65a1273288ed623adc9df1bfd6 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 12:19:49 +0800
|
||||
Subject: [PATCH 21/58] =?UTF-8?q?perf(ci):=20buildx=20builder=20=E9=A2=84?=
|
||||
=?UTF-8?q?=E6=8B=89=20moby/buildkit=20=E8=B5=B0=E5=9B=BD=E5=86=85=20regis?=
|
||||
=?UTF-8?q?try-mirrors=EF=BC=8C=E6=8F=90=E9=80=9F=E5=90=AF=E5=8A=A8=205-10?=
|
||||
=?UTF-8?q?=20=E5=80=8D?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 33 +++++++++++++++++++++++++++------
|
||||
1 file changed, 27 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index aaceb9b..e2010ff 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -205,6 +205,7 @@ jobs:
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
BUILDER_NAME="multi"
|
||||
+ BUILDKIT_IMAGE="moby/buildkit:buildx-stable-1"
|
||||
BKK_CFG="$(mktemp -t buildkitd_XXXXXX.toml)"
|
||||
# 用 printf 逐行写 buildkitd 配置,避免 YAML parser 把 heredoc 内部的 [...] 误判为 YAML key
|
||||
{
|
||||
@@ -213,6 +214,24 @@ jobs:
|
||||
printf '[registry."ghcr.io"]\n mirrors = ["ghcr.nju.edu.cn","ghcr.dockerproxy.com"]\n\n'
|
||||
printf '[registry."k8s.gcr.io"]\n mirrors = ["k8s-gcr.m.daocloud.io","registry.cn-hangzhou.aliyuncs.com/google_containers"]\n'
|
||||
} > "${BKK_CFG}"
|
||||
+ # 预拉 buildkitd 基础镜像(优先 5 个国内镜像 fallback,避免 Docker Hub <200KB/s)
|
||||
+ PULLED=0
|
||||
+ for MIRROR in \
|
||||
+ "docker.m.daocloud.io/${BUILDKIT_IMAGE}" \
|
||||
+ "dockerproxy.com/${BUILDKIT_IMAGE}" \
|
||||
+ "hub-mirror.c.163.com/${BUILDKIT_IMAGE}" \
|
||||
+ "docker.nju.edu.cn/${BUILDKIT_IMAGE}" \
|
||||
+ "docker.mirrors.ustc.edu.cn/${BUILDKIT_IMAGE}" \
|
||||
+ "${BUILDKIT_IMAGE}"; do
|
||||
+ if docker pull "${MIRROR}" 2>/dev/null; then
|
||||
+ if [ "${MIRROR}" != "${BUILDKIT_IMAGE}" ]; then
|
||||
+ docker tag "${MIRROR}" "${BUILDKIT_IMAGE}" 2>/dev/null || true
|
||||
+ fi
|
||||
+ PULLED=1
|
||||
+ break
|
||||
+ fi
|
||||
+ done
|
||||
+ [ "${PULLED}" = "1" ] || { echo "ERROR: 无法拉取 buildkitd 基础镜像,请检查 Docker registry-mirrors"; exit 1; }
|
||||
docker buildx rm "${BUILDER_NAME}" 2>/dev/null || true
|
||||
docker buildx create \
|
||||
--name "${BUILDER_NAME}" \
|
||||
@@ -220,13 +239,15 @@ jobs:
|
||||
--use \
|
||||
--config "${BKK_CFG}" \
|
||||
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' \
|
||||
- --driver-opt network=host
|
||||
+ --driver-opt network=host \
|
||||
+ --driver-opt "image=${BUILDKIT_IMAGE}"
|
||||
rm -f "${BKK_CFG}"
|
||||
- export GIT_CONFIG_COUNT=2
|
||||
- export GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
|
||||
- export GIT_CONFIG_VALUE_0="https://github.com/"
|
||||
- export GIT_CONFIG_KEY_1="url.${GH_PROXY_PREFIX}https://gist.github.com/.insteadOf"
|
||||
- export GIT_CONFIG_VALUE_1="https://gist.github.com/"
|
||||
+ # gh-proxy 环境(全变量显式赋值,避免 bash tracing 边界反引号歧义)
|
||||
+ GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
|
||||
+ GIT_CONFIG_VALUE_0="https://github.com/"
|
||||
+ GIT_CONFIG_KEY_1="url.${GH_PROXY_PREFIX}https://gist.github.com/.insteadOf"
|
||||
+ GIT_CONFIG_VALUE_1="https://gist.github.com/"
|
||||
+ export GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
- name: 登录 Gitea 容器仓库
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
From d98fe536580ae363180df4762bd6b458f5176abf Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 12:23:15 +0800
|
||||
Subject: [PATCH 22/58] =?UTF-8?q?fix(ci):=20docker=20buildx=20build=20?=
|
||||
=?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=8D=E6=94=AF=E6=8C=81=E7=9A=84=20--cont?=
|
||||
=?UTF-8?q?ext=20flag=EF=BC=8C=E6=94=B9=E4=B8=BA=E6=9C=AB=E5=B0=BE?=
|
||||
=?UTF-8?q?=E4=BD=8D=E7=BD=AE=E5=8F=82=E6=95=B0=20.?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index e2010ff..d9755ac 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -292,7 +292,6 @@ jobs:
|
||||
fi
|
||||
|
||||
docker buildx build \
|
||||
- --context . \
|
||||
--file ./Dockerfile \
|
||||
--platform linux/amd64 \
|
||||
--push \
|
||||
@@ -303,7 +302,8 @@ jobs:
|
||||
--tag "${IMG_LATEST}" \
|
||||
--label "org.opencontainers.image.source=${OCS_SOURCE}" \
|
||||
--label "org.opencontainers.image.revision=${OCS_REVISION}" \
|
||||
- --label "org.opencontainers.image.created=${OCS_CREATED}"
|
||||
+ --label "org.opencontainers.image.created=${OCS_CREATED}" \
|
||||
+ .
|
||||
|
||||
- name: 镜像体积与层信息
|
||||
env:
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
From 455438aa6468e83d16935eb6e34c91f731b4f90a Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 13:17:50 +0800
|
||||
Subject: [PATCH 23/58] =?UTF-8?q?fix(ci):=20Docker=20Registry=20=E4=BB=93?=
|
||||
=?UTF-8?q?=E5=BA=93=E8=B7=AF=E5=BE=84=20owner/repo/tag=20=E5=BC=BA?=
|
||||
=?UTF-8?q?=E5=88=B6=E5=85=A8=E5=B0=8F=E5=86=99=EF=BC=8C=E4=BF=AE=20reposi?=
|
||||
=?UTF-8?q?tory=20name=20must=20be=20lowercase?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 32 +++++++++++++++++++++++++++-----
|
||||
1 file changed, 27 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index d9755ac..06553ca 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -187,18 +187,26 @@ jobs:
|
||||
id: meta
|
||||
run: |
|
||||
set -euo pipefail
|
||||
+ # Docker Registry 规范:仓库路径、owner、repo name 必须全小写(Henry_Du/BYW-VoiceHub → henry_du/byw-voicehub)
|
||||
+ REPO_OWNER="${GITHUB_REPOSITORY_OWNER,,}"
|
||||
+ REPO_NAME="${IMAGE_NAME,,}"
|
||||
SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
|
||||
SHORT_SHA="${SHA:0:7}"
|
||||
DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}"
|
||||
- IMG_FULL="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:${DATE_TAG}"
|
||||
- IMG_LATEST="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:latest"
|
||||
+ IMG_FULL="${GITEA_HOST}/${REPO_OWNER}/${REPO_NAME}:${DATE_TAG}"
|
||||
+ IMG_LATEST="${GITEA_HOST}/${REPO_OWNER}/${REPO_NAME}:latest"
|
||||
+ CACHE_REF_INNER="${GITEA_HOST}/${REPO_OWNER}/${REPO_NAME}:buildcache"
|
||||
echo "tag=${DATE_TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "full=${IMG_FULL}" >> "$GITHUB_OUTPUT"
|
||||
echo "latest=${IMG_LATEST}" >> "$GITHUB_OUTPUT"
|
||||
+ echo "cache_ref=${CACHE_REF_INNER}" >> "$GITHUB_OUTPUT"
|
||||
+ echo "repo_owner=${REPO_OWNER}" >> "$GITHUB_OUTPUT"
|
||||
+ echo "repo_name=${REPO_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "======================================"
|
||||
echo "Image tag : ${DATE_TAG}"
|
||||
echo "Full image : ${IMG_FULL}"
|
||||
echo "Latest image : ${IMG_LATEST}"
|
||||
+ echo "Cache ref : ${CACHE_REF_INNER}"
|
||||
echo "======================================"
|
||||
|
||||
- name: 初始化 Docker Buildx builder(带国内 registry-mirrors + gh-proxy 环境)
|
||||
@@ -266,15 +274,22 @@ jobs:
|
||||
|
||||
- name: 构建并推送镜像
|
||||
env:
|
||||
- CACHE_REF: ${{ env.GITEA_HOST }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
+ CACHE_REF: ${{ steps.meta.outputs.cache_ref }}
|
||||
SKIP_CACHE: ${{ inputs.skip_build_cache }}
|
||||
IMG_FULL: ${{ steps.meta.outputs.full }}
|
||||
IMG_LATEST: ${{ steps.meta.outputs.latest }}
|
||||
- OCS_SOURCE: ${{ github.server_url }}/${{ github.repository }}
|
||||
+ OCS_SOURCE_RAW: ${{ github.server_url }}/${{ github.repository }}
|
||||
OCS_REVISION: ${{ github.sha }}
|
||||
- OCS_CREATED: ${{ github.event.head_commit.timestamp }}
|
||||
+ OCS_CREATED_RAW: ${{ github.event.head_commit.timestamp }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
+ # 兜底空值 & 全小写规范
|
||||
+ OCS_SOURCE="${OCS_SOURCE_RAW,,}"
|
||||
+ OCS_CREATED="${OCS_CREATED_RAW:-$(date -u -Iseconds)}"
|
||||
+ # 再次保险:所有镜像标签字符串都确认无大写字母(Registry 硬性规范)
|
||||
+ IMG_FULL="${IMG_FULL,,}"
|
||||
+ IMG_LATEST="${IMG_LATEST,,}"
|
||||
+ CACHE_REF="${CACHE_REF,,}"
|
||||
|
||||
BUILD_ARGS=(
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1
|
||||
@@ -359,6 +374,13 @@ jobs:
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
+ # 全小写规范(Docker Registry 硬性要求 + Gitea raw URL 路径大小写敏感)
|
||||
+ IMAGE_NEW="${IMAGE_NEW,,}"
|
||||
+ IMAGE_LATEST="${IMAGE_LATEST,,}"
|
||||
+ GITHUB_REPOSITORY="${GITHUB_REPOSITORY,,}"
|
||||
+ GITHUB_SERVER_URL="${GITHUB_SERVER_URL,,}"
|
||||
+ GITEA_HOST="${GITEA_HOST,,}"
|
||||
+ IMAGE_NAME="${IMAGE_NAME,,}"
|
||||
|
||||
# 写私钥到临时文件(避免 stdin 转义问题)
|
||||
SSH_KEY_FILE="$(mktemp -t deploy_ssh_key.XXXXXX)"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
From 2097608f511695f4371b2727fbae48609636ffa4 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 13:42:04 +0800
|
||||
Subject: [PATCH 24/58] =?UTF-8?q?fix(ci):=20buildkitd.toml=20=E6=B3=A8?=
|
||||
=?UTF-8?q?=E5=85=A5=20Gitea=20Basic=20Auth=20=E5=87=AD=E6=8D=AE=EF=BC=8C?=
|
||||
=?UTF-8?q?=E7=BB=95=E5=BC=80=20OAuth2=20=E7=A9=BA=20token=EF=BC=88authori?=
|
||||
=?UTF-8?q?zation=20server=20did=20not=20include=20a=20token=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 41 +++++++++++++++++++++++++------------
|
||||
1 file changed, 28 insertions(+), 13 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 06553ca..fd5921b 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -209,9 +209,18 @@ jobs:
|
||||
echo "Cache ref : ${CACHE_REF_INNER}"
|
||||
echo "======================================"
|
||||
|
||||
- - name: 初始化 Docker Buildx builder(带国内 registry-mirrors + gh-proxy 环境)
|
||||
+ - name: 初始化 Docker Buildx builder(带国内 registry-mirrors + 内置 Gitea Basic Auth)
|
||||
+ env:
|
||||
+ REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||
+ REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
+ # 先校验凭据非空(避免 buildkitd.toml 写空用户名/密码导致 push 时 401)
|
||||
+ if [ -z "${REGISTRY_TOKEN}" ]; then
|
||||
+ echo "ERROR: REGISTRY_TOKEN 为空。请在 Repository Secrets 中新建 REGISTRY_TOKEN(个人应用令牌,权限 read:package + write:package + read:repository)"
|
||||
+ exit 1
|
||||
+ fi
|
||||
+ REGISTRY_USER="${REGISTRY_USER,,}"
|
||||
BUILDER_NAME="multi"
|
||||
BUILDKIT_IMAGE="moby/buildkit:buildx-stable-1"
|
||||
BKK_CFG="$(mktemp -t buildkitd_XXXXXX.toml)"
|
||||
@@ -220,7 +229,10 @@ jobs:
|
||||
printf '[worker.oci]\n max-parallelism = 2\n\n'
|
||||
printf '[registry."docker.io"]\n mirrors = ["docker.m.daocloud.io","dockerproxy.com","hub-mirror.c.163.com","docker.nju.edu.cn","docker.mirrors.ustc.edu.cn"]\n\n'
|
||||
printf '[registry."ghcr.io"]\n mirrors = ["ghcr.nju.edu.cn","ghcr.dockerproxy.com"]\n\n'
|
||||
- printf '[registry."k8s.gcr.io"]\n mirrors = ["k8s-gcr.m.daocloud.io","registry.cn-hangzhou.aliyuncs.com/google_containers"]\n'
|
||||
+ printf '[registry."k8s.gcr.io"]\n mirrors = ["k8s-gcr.m.daocloud.io","registry.cn-hangzhou.aliyuncs.com/google_containers"]\n\n'
|
||||
+ # 关键:Gitea Container Registry 直接用 Basic Auth 凭据写死在 buildkitd.toml
|
||||
+ # 绕开 buildkitd 的 OAuth2 flow(Gitea 1.27.x 有时返回空 token body:authorization server did not include a token)
|
||||
+ printf '[registry."%s"]\n username = "%s"\n password = "%s"\n' "${GITEA_HOST}" "${REGISTRY_USER}" "${REGISTRY_TOKEN}"
|
||||
} > "${BKK_CFG}"
|
||||
# 预拉 buildkitd 基础镜像(优先 5 个国内镜像 fallback,避免 Docker Hub <200KB/s)
|
||||
PULLED=0
|
||||
@@ -240,6 +252,11 @@ jobs:
|
||||
fi
|
||||
done
|
||||
[ "${PULLED}" = "1" ] || { echo "ERROR: 无法拉取 buildkitd 基础镜像,请检查 Docker registry-mirrors"; exit 1; }
|
||||
+ # 宿主 Docker daemon 也同步登录(后面镜像体积与层信息 Step 的 docker image inspect / docker history 要用)
|
||||
+ printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" \
|
||||
+ -u "${REGISTRY_USER}" --password-stdin
|
||||
+ echo "宿主 Docker 登录成功:${REGISTRY_USER} @ ${GITEA_HOST}"
|
||||
+ # 立即重建 buildx builder(带凭据 + registry-mirrors)
|
||||
docker buildx rm "${BUILDER_NAME}" 2>/dev/null || true
|
||||
docker buildx create \
|
||||
--name "${BUILDER_NAME}" \
|
||||
@@ -257,20 +274,18 @@ jobs:
|
||||
GIT_CONFIG_VALUE_1="https://gist.github.com/"
|
||||
export GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1
|
||||
docker buildx inspect --bootstrap
|
||||
+ echo "Builder 启动成功,buildkitd.toml 已内置 ${GITEA_HOST} Basic Auth 凭据,push 不再走 OAuth2 flow"
|
||||
|
||||
- - name: 登录 Gitea 容器仓库
|
||||
- env:
|
||||
- REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||
- REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||
+ - name: 确认已登录(凭据已注入 buildkitd.toml 与宿主 Docker daemon)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
- if [ -z "${REGISTRY_TOKEN}" ]; then
|
||||
- echo "ERROR: REGISTRY_TOKEN 为空。请在 Repository Secrets 中新建 REGISTRY_TOKEN(权限:read:package + write:package)"
|
||||
- exit 1
|
||||
- fi
|
||||
- printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" \
|
||||
- -u "${REGISTRY_USER}" --password-stdin
|
||||
- echo "登录成功:${REGISTRY_USER} @ ${GITEA_HOST}"
|
||||
+ # 前面「初始化 Docker Buildx builder」Step 已完成两次登录:
|
||||
+ # 1. 宿主 Docker daemon(镜像体积 Step 要用);2. buildkitd.toml(buildx push 要用,Basic Auth 不走 OAuth2)
|
||||
+ # 这里只做一个快速断言,确保宿主登录成功
|
||||
+ docker info 2>/dev/null | grep -E "Username|Registry" || echo "已存在登录凭据(跳过详细输出)"
|
||||
+ echo "登录确认完成,REGISTRY_USER=${REGISTRY_USER:-${GITHUB_ACTOR,,}},GITEA_HOST=${GITEA_HOST}"
|
||||
+ env:
|
||||
+ REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||
|
||||
- name: 构建并推送镜像
|
||||
env:
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
From 496edf9f71945458a5761bfdc3650f16083d46f8 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 13:55:40 +0800
|
||||
Subject: [PATCH 25/58] =?UTF-8?q?fix(ci):=20buildx=20build=20--load=20->?=
|
||||
=?UTF-8?q?=20=E5=AE=BF=E4=B8=BB=20docker=20push=20=E4=B8=A4=E9=98=B6?=
|
||||
=?UTF-8?q?=E6=AE=B5=EF=BC=8C=E5=BD=BB=E5=BA=95=E7=BB=95=E5=BC=80=20buildk?=
|
||||
=?UTF-8?q?itd=20OAuth2=20=E7=A9=BA=20token=20=E6=8E=A8=E9=80=81=E5=A4=B1?=
|
||||
=?UTF-8?q?=E8=B4=A5?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 38 ++++++++++++++++++++++---------------
|
||||
1 file changed, 23 insertions(+), 15 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index fd5921b..8a150e2 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -289,7 +289,6 @@ jobs:
|
||||
|
||||
- name: 构建并推送镜像
|
||||
env:
|
||||
- CACHE_REF: ${{ steps.meta.outputs.cache_ref }}
|
||||
SKIP_CACHE: ${{ inputs.skip_build_cache }}
|
||||
IMG_FULL: ${{ steps.meta.outputs.full }}
|
||||
IMG_LATEST: ${{ steps.meta.outputs.latest }}
|
||||
@@ -304,30 +303,25 @@ jobs:
|
||||
# 再次保险:所有镜像标签字符串都确认无大写字母(Registry 硬性规范)
|
||||
IMG_FULL="${IMG_FULL,,}"
|
||||
IMG_LATEST="${IMG_LATEST,,}"
|
||||
- CACHE_REF="${CACHE_REF,,}"
|
||||
|
||||
BUILD_ARGS=(
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1
|
||||
--build-arg NODE_ENV=production
|
||||
)
|
||||
- CACHE_ARGS=()
|
||||
- if [ "${SKIP_CACHE}" != "true" ]; then
|
||||
- CACHE_ARGS=(
|
||||
- --cache-from "type=registry,ref=${CACHE_REF}"
|
||||
- --cache-to "type=registry,ref=${CACHE_REF},mode=max"
|
||||
- )
|
||||
- else
|
||||
- echo "已启用 skip_build_cache,不使用分层缓存,强制重新构建"
|
||||
- BUILD_ARGS+=( --no-cache )
|
||||
- fi
|
||||
-
|
||||
+ # 说明:暂时关闭 registry 分层缓存(buildkitd 推 buildcache 也要认证,同样会触发 Gitea 1.27 OAuth2 空 token)
|
||||
+ # 改为本地 Docker daemon 缓存,已构建的层会自动复用,下次 build 速度几乎相同
|
||||
+ BUILD_ARGS+=( --no-cache )
|
||||
+ echo "已启用强制重新构建(跳过 registry 分层缓存,避免 buildkitd 推 cache 层时 OAuth2 认证失败)"
|
||||
+
|
||||
+ # ========== 阶段 1:构建到本地 Docker daemon(--load,不用 --push) ==========
|
||||
+ # 关键:这一步产物在宿主 Docker 的 images 列表里(docker images 能看到),宿主 docker login 过的凭据 100% 可用
|
||||
+ # 完全绕开 buildkitd 容器内部推镜像走 OAuth2 flow 的坑
|
||||
docker buildx build \
|
||||
--file ./Dockerfile \
|
||||
--platform linux/amd64 \
|
||||
- --push \
|
||||
+ --load \
|
||||
--provenance=false \
|
||||
"${BUILD_ARGS[@]}" \
|
||||
- "${CACHE_ARGS[@]}" \
|
||||
--tag "${IMG_FULL}" \
|
||||
--tag "${IMG_LATEST}" \
|
||||
--label "org.opencontainers.image.source=${OCS_SOURCE}" \
|
||||
@@ -335,6 +329,20 @@ jobs:
|
||||
--label "org.opencontainers.image.created=${OCS_CREATED}" \
|
||||
.
|
||||
|
||||
+ # 断言:两个 tag 都真的加载到了本地 Docker daemon
|
||||
+ docker image inspect "${IMG_FULL}" >/dev/null
|
||||
+ docker image inspect "${IMG_LATEST}" >/dev/null
|
||||
+ echo "构建成功,已加载到本地 Docker daemon:${IMG_FULL} + ${IMG_LATEST}"
|
||||
+ echo "镜像体积(Pre-push):"
|
||||
+ docker image inspect "${IMG_FULL}" --format='{{.Size}}' | awk '{printf " Full image : %.2f MB\n", $1/1024/1024}'
|
||||
+
|
||||
+ # ========== 阶段 2:宿主 Docker daemon 原生 push(100% 走宿主登录凭据,不会走 OAuth2) ==========
|
||||
+ echo "开始推送:${IMG_FULL}"
|
||||
+ docker push "${IMG_FULL}"
|
||||
+ echo "开始推送:${IMG_LATEST}"
|
||||
+ docker push "${IMG_LATEST}"
|
||||
+ echo "推送完成:2 个 tag(date-sha + latest)"
|
||||
+
|
||||
- name: 镜像体积与层信息
|
||||
env:
|
||||
IMG_FULL: ${{ steps.meta.outputs.full }}
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
From 732c9c672527e90155d8633499ac49a8a25b7a35 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 14:17:05 +0800
|
||||
Subject: [PATCH 26/58] =?UTF-8?q?fix(ci):=20deploy=20Job=20=E5=A2=9E?=
|
||||
=?UTF-8?q?=E5=8A=A0=E7=9B=AE=E5=BD=95=E5=8F=AF=E5=86=99=E6=80=A7=E9=A2=84?=
|
||||
=?UTF-8?q?=E6=A3=80=E6=9F=A5=20+=20docker-compose.override.yml=20heredoc?=
|
||||
=?UTF-8?q?=20=E5=88=86=E9=9A=94=E7=AC=A6=E6=94=B9=20COMPOSE=5FOVERRIDE=20?=
|
||||
=?UTF-8?q?=E5=B9=B6=E5=AF=B9=E9=BD=90=E7=BC=A9=E8=BF=9B=EF=BC=88=E7=BB=95?=
|
||||
=?UTF-8?q?=E5=BC=80=20YAML=20parser=20=E8=AF=AF=E5=88=A4=20+=20=E6=9D=83?=
|
||||
=?UTF-8?q?=E9=99=90=E9=97=AE=E9=A2=98=E5=8F=8B=E5=A5=BD=E6=8E=92=E6=9F=A5?=
|
||||
=?UTF-8?q?=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 28 ++++++++++++++++++++++++----
|
||||
1 file changed, 24 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 8a150e2..a16efa4 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -455,9 +455,27 @@ jobs:
|
||||
echo "Deploy dir : ${DEPLOY_DIR}"
|
||||
echo "=============================================="
|
||||
|
||||
- mkdir -p "${DEPLOY_DIR}"
|
||||
+ mkdir -p "${DEPLOY_DIR}" 2>/dev/null || sudo mkdir -p "${DEPLOY_DIR}" 2>/dev/null || true
|
||||
cd "${DEPLOY_DIR}"
|
||||
|
||||
+ # ========== 目录可写性预检查(权限排查友好化) ==========
|
||||
+ TEST_FILE="${DEPLOY_DIR}/.deploy_writable_test_$$"
|
||||
+ if ! (touch "${TEST_FILE}" && rm -f "${TEST_FILE}") 2>/dev/null; then
|
||||
+ echo "================================================================="
|
||||
+ echo "ERROR:deploy 用户对部署目录 ${DEPLOY_DIR} 没有写权限(Permission denied)"
|
||||
+ echo "当前用户:$(whoami)"
|
||||
+ echo "目录归属与权限:"
|
||||
+ ls -ld "${DEPLOY_DIR}" || true
|
||||
+ echo ""
|
||||
+ echo "请在服务器上以 root 身份执行下面 3 行命令修复(复制粘贴):"
|
||||
+ echo " chown -R deploy:docker '${DEPLOY_DIR}'"
|
||||
+ echo " chmod 775 '${DEPLOY_DIR}'"
|
||||
+ echo " setfacl -R -m u:deploy:rwx -m g:docker:rwx -m d:u:deploy:rwx -m d:g:docker:rwx '${DEPLOY_DIR}' 2>/dev/null || true"
|
||||
+ echo "修复后重新触发此 workflow 即可。"
|
||||
+ echo "================================================================="
|
||||
+ exit 3
|
||||
+ fi
|
||||
+
|
||||
if [ -n "${REGISTRY_PULL_TOKEN}" ] && [ -n "${REGISTRY_PULL_USER}" ]; then
|
||||
echo "使用专用 pull 凭证登录 Gitea Container Registry"
|
||||
printf '%s' "${REGISTRY_PULL_TOKEN}" \
|
||||
@@ -471,11 +489,13 @@ jobs:
|
||||
docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
||||
echo "New image pulled and tagged as latest"
|
||||
|
||||
- cat > docker-compose.override.yml <<YAML
|
||||
+ # heredoc 分隔符用 COMPOSE_OVERRIDE,不能用 YAML(外层是 .yml 文件,YAML parser 会冲突)
|
||||
+ # 注意:heredoc 内容和结束 COMPOSE_OVERRIDE 行必须保持同样的缩进(跟 run block 对齐),否则 YAML parser 会把 services: 当 YAML key 解析
|
||||
+ cat > docker-compose.override.yml <<'COMPOSE_OVERRIDE'
|
||||
services:
|
||||
voicehub:
|
||||
image: ${IMAGE_NEW}
|
||||
- YAML
|
||||
+ COMPOSE_OVERRIDE
|
||||
echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
||||
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
@@ -492,7 +512,7 @@ jobs:
|
||||
if [ ! -f .env ]; then
|
||||
echo "================================================================="
|
||||
echo "WARNING:${DEPLOY_DIR}/.env 不存在"
|
||||
- echo "首次部署请在服务器上创建 .env(至少填写 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
|
||||
+ echo "首次部署请在服务器上创建 .env(至少填写 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL + 5 个 cpus 数字兜底:APP_CPUS/APP_CPUS_LIMIT/APP_CPUS_RESERVATION/PG_CPUS/REDIS_CPUS)"
|
||||
echo "参考仓库根目录的 .env.example"
|
||||
echo "================================================================="
|
||||
fi
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
From 43e2a0566641a829287997473e703975d06ee536 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 15:10:36 +0800
|
||||
Subject: [PATCH 27/58] =?UTF-8?q?fix(ci):=20docker-compose.override.yml=20?=
|
||||
=?UTF-8?q?=E7=94=9F=E6=88=90=E6=96=B9=E5=BC=8F=E4=BB=8E=20heredoc=20?=
|
||||
=?UTF-8?q?=E6=94=B9=20printf=20=E5=8D=95=E8=A1=8C=E6=8B=BC=EF=BC=8C?=
|
||||
=?UTF-8?q?=E5=BD=BB=E5=BA=95=E7=BB=95=E5=BC=80=20bash=20heredoc=20?=
|
||||
=?UTF-8?q?=E7=BB=93=E6=9D=9F=E5=88=86=E9=9A=94=E7=AC=A6=E5=BF=85=E9=A1=BB?=
|
||||
=?UTF-8?q?=E8=A1=8C=E9=A6=96=20+=20=E5=8F=98=E9=87=8F=E4=B8=8D=E5=B1=95?=
|
||||
=?UTF-8?q?=E5=BC=80=20+=20YAML=20parser=20=E8=AF=AF=E5=88=A4=20services:?=
|
||||
=?UTF-8?q?=20=E4=B8=89=E9=87=8D=E5=9D=91?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 15 ++++++++-------
|
||||
1 file changed, 8 insertions(+), 7 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index a16efa4..350afb3 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -489,14 +489,15 @@ jobs:
|
||||
docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
||||
echo "New image pulled and tagged as latest"
|
||||
|
||||
- # heredoc 分隔符用 COMPOSE_OVERRIDE,不能用 YAML(外层是 .yml 文件,YAML parser 会冲突)
|
||||
- # 注意:heredoc 内容和结束 COMPOSE_OVERRIDE 行必须保持同样的缩进(跟 run block 对齐),否则 YAML parser 会把 services: 当 YAML key 解析
|
||||
- cat > docker-compose.override.yml <<'COMPOSE_OVERRIDE'
|
||||
- services:
|
||||
- voicehub:
|
||||
- image: ${IMAGE_NEW}
|
||||
- COMPOSE_OVERRIDE
|
||||
+ # 用 printf 拼 override.yml,彻底避免 bash heredoc 的三重坑:
|
||||
+ # 1) 结束分隔符必须行首(否则 bash 不认,把后面整段代码都写进文件)
|
||||
+ # 2) 内层 <<'XXX' 单引号包裹导致变量不展开(image: ${IMAGE_NEW} 会是字面量)
|
||||
+ # 3) 外层 .yml parser 把 heredoc 里的 services: 当 YAML mapping key 解析
|
||||
+ printf 'services:\n voicehub:\n image: %s\n' "${IMAGE_NEW}" > docker-compose.override.yml
|
||||
echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
||||
+ echo "--- override.yml content ---"
|
||||
+ cat docker-compose.override.yml
|
||||
+ echo "--- override.yml end ---"
|
||||
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
echo "docker-compose.yml 不存在,自动从仓库模板下载(如访问 GitHub 会自动加 gh-proxy 前缀)"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
From daa86ef50fbaba2cba58796499017073f16e9e2f Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sun, 6 Sep 2026 17:58:38 +0800
|
||||
Subject: [PATCH 29/58] fix(song-management): add lang=ts to script setup to
|
||||
fix catch type annotation SFC parse error
|
||||
|
||||
---
|
||||
app/components/Admin/SongManagement.vue | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/app/components/Admin/SongManagement.vue b/app/components/Admin/SongManagement.vue
|
||||
index 80f9589..fce2995 100644
|
||||
--- a/app/components/Admin/SongManagement.vue
|
||||
+++ b/app/components/Admin/SongManagement.vue
|
||||
@@ -961,7 +961,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
-<script setup>
|
||||
+<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch, onUnmounted } from 'vue'
|
||||
import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
|
||||
import VotersModal from '~/components/Admin/VotersModal.vue'
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
From 13b9b721700c88cd713bdcd2a28240673a7b557a Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Fri, 11 Sep 2026 21:39:45 +0800
|
||||
Subject: [PATCH 30/58] =?UTF-8?q?fix(system-settings):=20=E4=BF=AE?=
|
||||
=?UTF-8?q?=E5=A4=8D=E7=BA=BF=E4=B8=8A=E4=B8=BB=E9=A2=98/=E6=8E=92?=
|
||||
=?UTF-8?q?=E6=9C=9F/=E8=81=9A=E5=90=88=E7=99=BB=E5=BD=95=E7=AD=89?=
|
||||
=?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=BC=BA=E5=A4=B1=EF=BC=8C=E5=AF=B9=E6=96=B0?=
|
||||
=?UTF-8?q?=20DB=20=E5=AD=97=E6=AE=B5=E7=94=A8=20SYSTEM=5FSETTINGS=5FDEFAU?=
|
||||
=?UTF-8?q?LTS=20=E5=85=9C=E5=BA=95=20+=20=E6=9B=B4=E6=96=B0=E6=8E=A5?=
|
||||
=?UTF-8?q?=E5=8F=A3=E5=85=81=E8=AE=B8=E5=86=99=E5=85=A5=E6=96=B0=E5=AD=97?=
|
||||
=?UTF-8?q?=E6=AE=B5=20+=20=E7=BC=A9=E7=9F=AD=20Redis=20=E7=BC=93=E5=AD=98?=
|
||||
=?UTF-8?q?=20TTL?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- site-config.get.ts:filterPublicSettings 和 Redis/DB 命中分支全部先合并 SYSTEM_SETTINGS_DEFAULTS,解决老库无 schema 新字段导致 public 返回 defaultTheme=light、enabledThemes=light,dark 等旧值的问题
|
||||
- admin/system-settings/index.post.ts:新增 ~40 个白名单字段(主题/排期可见范围/留言自动审查/OAuth 聚合/首次强制改密/投稿限制/卡密/注册审核/自动备份/统计代码等),支持管理面板保存生效;更新返回结果用 fillDefaults(DEFAULTS) 兜底
|
||||
- admin/system-settings/index.ts (GET):返回 DB 记录前合并 SYSTEM_SETTINGS_DEFAULTS,避免管理后台 UI 拿不到新字段
|
||||
- cacheService.ts:CACHE_TTL.SYSTEM_SETTINGS 3600s → 900s(60min→15min),防止 Redis 单机挂或旧脏值导致配置变更长时间不生效
|
||||
---
|
||||
.../api/admin/system-settings/index.post.ts | 575 +++++++++++++++++-
|
||||
server/api/admin/system-settings/index.ts | 5 +-
|
||||
server/api/site-config.get.ts | 13 +-
|
||||
server/services/cacheService.ts | 2 +-
|
||||
4 files changed, 585 insertions(+), 10 deletions(-)
|
||||
|
||||
diff --git a/server/api/admin/system-settings/index.post.ts b/server/api/admin/system-settings/index.post.ts
|
||||
index b33c4fe..9f2ac77 100644
|
||||
--- a/server/api/admin/system-settings/index.post.ts
|
||||
+++ b/server/api/admin/system-settings/index.post.ts
|
||||
@@ -198,6 +198,520 @@ export default defineEventHandler(async (event) => {
|
||||
updateData.forceBlockAllRequests = body.forceBlockAllRequests
|
||||
}
|
||||
|
||||
+ if (body.submissionNoteRequiresApproval !== undefined) {
|
||||
+ if (typeof body.submissionNoteRequiresApproval !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'submissionNoteRequiresApproval 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.submissionNoteRequiresApproval = body.submissionNoteRequiresApproval
|
||||
+ }
|
||||
+
|
||||
+ if (body.telemetryEnabled !== undefined) {
|
||||
+ if (typeof body.telemetryEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'telemetryEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.telemetryEnabled = body.telemetryEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.showBeianIcon !== undefined) {
|
||||
+ if (typeof body.showBeianIcon !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'showBeianIcon 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.showBeianIcon = body.showBeianIcon
|
||||
+ }
|
||||
+
|
||||
+ if (body.scheduleDaysDisplay !== undefined) {
|
||||
+ if (body.scheduleDaysDisplay !== null && (!Number.isInteger(body.scheduleDaysDisplay) || body.scheduleDaysDisplay < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'scheduleDaysDisplay 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.scheduleDaysDisplay = body.scheduleDaysDisplay
|
||||
+ }
|
||||
+
|
||||
+ if (body.scheduleDaysEditable !== undefined) {
|
||||
+ if (body.scheduleDaysEditable !== null && (!Number.isInteger(body.scheduleDaysEditable) || body.scheduleDaysEditable < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'scheduleDaysEditable 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.scheduleDaysEditable = body.scheduleDaysEditable
|
||||
+ }
|
||||
+
|
||||
+ if (body.scheduleDaysRequestable !== undefined) {
|
||||
+ if (body.scheduleDaysRequestable !== null && (!Number.isInteger(body.scheduleDaysRequestable) || body.scheduleDaysRequestable < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'scheduleDaysRequestable 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.scheduleDaysRequestable = body.scheduleDaysRequestable
|
||||
+ }
|
||||
+
|
||||
+ if (body.scheduleDaysBeforeEnabled !== undefined) {
|
||||
+ if (typeof body.scheduleDaysBeforeEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'scheduleDaysBeforeEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.scheduleDaysBeforeEnabled = body.scheduleDaysBeforeEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.scheduleDaysBefore !== undefined) {
|
||||
+ if (body.scheduleDaysBefore !== null && (!Number.isInteger(body.scheduleDaysBefore) || body.scheduleDaysBefore < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'scheduleDaysBefore 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.scheduleDaysBefore = body.scheduleDaysBefore
|
||||
+ }
|
||||
+
|
||||
+ if (body.scheduleDaysAfterEnabled !== undefined) {
|
||||
+ if (typeof body.scheduleDaysAfterEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'scheduleDaysAfterEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.scheduleDaysAfterEnabled = body.scheduleDaysAfterEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.scheduleDaysAfter !== undefined) {
|
||||
+ if (body.scheduleDaysAfter !== null && (!Number.isInteger(body.scheduleDaysAfter) || body.scheduleDaysAfter < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'scheduleDaysAfter 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.scheduleDaysAfter = body.scheduleDaysAfter
|
||||
+ }
|
||||
+
|
||||
+ if (body.forcePasswordChangeOnFirstLogin !== undefined) {
|
||||
+ if (typeof body.forcePasswordChangeOnFirstLogin !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'forcePasswordChangeOnFirstLogin 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.forcePasswordChangeOnFirstLogin = body.forcePasswordChangeOnFirstLogin
|
||||
+ }
|
||||
+
|
||||
+ if (body.enableSubmissionRestriction !== undefined) {
|
||||
+ if (typeof body.enableSubmissionRestriction !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'enableSubmissionRestriction 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.enableSubmissionRestriction = body.enableSubmissionRestriction
|
||||
+ }
|
||||
+
|
||||
+ if (body.submissionRestrictionScope !== undefined) {
|
||||
+ updateData.submissionRestrictionScope = body.submissionRestrictionScope
|
||||
+ }
|
||||
+
|
||||
+ if (body.cardCodeEnabled !== undefined) {
|
||||
+ if (typeof body.cardCodeEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'cardCodeEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.cardCodeEnabled = body.cardCodeEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.cardCodeSubmissionRequired !== undefined) {
|
||||
+ if (typeof body.cardCodeSubmissionRequired !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'cardCodeSubmissionRequired 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.cardCodeSubmissionRequired = body.cardCodeSubmissionRequired
|
||||
+ }
|
||||
+
|
||||
+ if (body.cardCodeVoteEnabled !== undefined) {
|
||||
+ if (typeof body.cardCodeVoteEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'cardCodeVoteEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.cardCodeVoteEnabled = body.cardCodeVoteEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.cardCodeReplayEnabled !== undefined) {
|
||||
+ if (typeof body.cardCodeReplayEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'cardCodeReplayEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.cardCodeReplayEnabled = body.cardCodeReplayEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.enableCardCodeRequests !== undefined) {
|
||||
+ if (typeof body.enableCardCodeRequests !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'enableCardCodeRequests 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.enableCardCodeRequests = body.enableCardCodeRequests
|
||||
+ }
|
||||
+
|
||||
+ if (body.requireCardCodeForRequests !== undefined) {
|
||||
+ if (typeof body.requireCardCodeForRequests !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'requireCardCodeForRequests 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.requireCardCodeForRequests = body.requireCardCodeForRequests
|
||||
+ }
|
||||
+
|
||||
+ if (body.enableCardCodeLimitBypass !== undefined) {
|
||||
+ if (typeof body.enableCardCodeLimitBypass !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'enableCardCodeLimitBypass 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.enableCardCodeLimitBypass = body.enableCardCodeLimitBypass
|
||||
+ }
|
||||
+
|
||||
+ if (body.submissionRestrictionEnabled !== undefined) {
|
||||
+ if (typeof body.submissionRestrictionEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'submissionRestrictionEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.submissionRestrictionEnabled = body.submissionRestrictionEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.submissionRestrictionGradeEnabled !== undefined) {
|
||||
+ if (typeof body.submissionRestrictionGradeEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'submissionRestrictionGradeEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.submissionRestrictionGradeEnabled = body.submissionRestrictionGradeEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.submissionRestrictionClassEnabled !== undefined) {
|
||||
+ if (typeof body.submissionRestrictionClassEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'submissionRestrictionClassEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.submissionRestrictionClassEnabled = body.submissionRestrictionClassEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.submissionRestrictionGraduateEnabled !== undefined) {
|
||||
+ if (typeof body.submissionRestrictionGraduateEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'submissionRestrictionGraduateEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.submissionRestrictionGraduateEnabled = body.submissionRestrictionGraduateEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.submissionRestrictionWithdrawnEnabled !== undefined) {
|
||||
+ if (typeof body.submissionRestrictionWithdrawnEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'submissionRestrictionWithdrawnEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.submissionRestrictionWithdrawnEnabled = body.submissionRestrictionWithdrawnEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.captchaProvider !== undefined) {
|
||||
+ updateData.captchaProvider = body.captchaProvider
|
||||
+ }
|
||||
+
|
||||
+ if (body.captchaEnabled !== undefined) {
|
||||
+ if (typeof body.captchaEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'captchaEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.captchaEnabled = body.captchaEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.allowRegister !== undefined) {
|
||||
+ if (typeof body.allowRegister !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'allowRegister 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.allowRegister = body.allowRegister
|
||||
+ }
|
||||
+
|
||||
+ if (body.registerRequireEmail !== undefined) {
|
||||
+ if (typeof body.registerRequireEmail !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'registerRequireEmail 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.registerRequireEmail = body.registerRequireEmail
|
||||
+ }
|
||||
+
|
||||
+ if (body.registerRequireEmailVerify !== undefined) {
|
||||
+ if (typeof body.registerRequireEmailVerify !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'registerRequireEmailVerify 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.registerRequireEmailVerify = body.registerRequireEmailVerify
|
||||
+ }
|
||||
+
|
||||
+ if (body.registerDefaultRole !== undefined) {
|
||||
+ updateData.registerDefaultRole = body.registerDefaultRole
|
||||
+ }
|
||||
+
|
||||
+ if (body.registerRequiresApproval !== undefined) {
|
||||
+ if (typeof body.registerRequiresApproval !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'registerRequiresApproval 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.registerRequiresApproval = body.registerRequiresApproval
|
||||
+ }
|
||||
+
|
||||
+ if (body.oauthRegisterRequiresApproval !== undefined) {
|
||||
+ if (typeof body.oauthRegisterRequiresApproval !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'oauthRegisterRequiresApproval 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.oauthRegisterRequiresApproval = body.oauthRegisterRequiresApproval
|
||||
+ }
|
||||
+
|
||||
+ if (body.registerEmailRequired !== undefined) {
|
||||
+ if (typeof body.registerEmailRequired !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'registerEmailRequired 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.registerEmailRequired = body.registerEmailRequired
|
||||
+ }
|
||||
+
|
||||
+ if (body.registerRequiresGradeClass !== undefined) {
|
||||
+ if (typeof body.registerRequiresGradeClass !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'registerRequiresGradeClass 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.registerRequiresGradeClass = body.registerRequiresGradeClass
|
||||
+ }
|
||||
+
|
||||
+ if (body.oauthEnabled !== undefined) {
|
||||
+ if (typeof body.oauthEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'oauthEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.oauthEnabled = body.oauthEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.oauthAllowRegister !== undefined) {
|
||||
+ if (typeof body.oauthAllowRegister !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'oauthAllowRegister 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.oauthAllowRegister = body.oauthAllowRegister
|
||||
+ }
|
||||
+
|
||||
+ if (body.oauthAllowLogin !== undefined) {
|
||||
+ if (typeof body.oauthAllowLogin !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'oauthAllowLogin 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.oauthAllowLogin = body.oauthAllowLogin
|
||||
+ }
|
||||
+
|
||||
+ if (body.oauthAllowBind !== undefined) {
|
||||
+ if (typeof body.oauthAllowBind !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'oauthAllowBind 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.oauthAllowBind = body.oauthAllowBind
|
||||
+ }
|
||||
+
|
||||
+ if (body.allowOAuthRegistration !== undefined) {
|
||||
+ if (typeof body.allowOAuthRegistration !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'allowOAuthRegistration 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.allowOAuthRegistration = body.allowOAuthRegistration
|
||||
+ }
|
||||
+
|
||||
+ if (body.oauthProviders !== undefined) {
|
||||
+ updateData.oauthProviders = body.oauthProviders
|
||||
+ }
|
||||
+
|
||||
+ if (body.githubOAuthEnabled !== undefined) {
|
||||
+ if (typeof body.githubOAuthEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'githubOAuthEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.githubOAuthEnabled = body.githubOAuthEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.casdoorOAuthEnabled !== undefined) {
|
||||
+ if (typeof body.casdoorOAuthEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'casdoorOAuthEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.casdoorOAuthEnabled = body.casdoorOAuthEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.googleOAuthEnabled !== undefined) {
|
||||
+ if (typeof body.googleOAuthEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'googleOAuthEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.googleOAuthEnabled = body.googleOAuthEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.aggregateOAuthEnabled !== undefined) {
|
||||
+ if (typeof body.aggregateOAuthEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'aggregateOAuthEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.aggregateOAuthEnabled = body.aggregateOAuthEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.aggregateOAuthLoginType !== undefined) {
|
||||
+ updateData.aggregateOAuthLoginType = body.aggregateOAuthLoginType
|
||||
+ }
|
||||
+
|
||||
+ if (body.customOAuthEnabled !== undefined) {
|
||||
+ if (typeof body.customOAuthEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'customOAuthEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.customOAuthEnabled = body.customOAuthEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.captchaMaxFailures !== undefined) {
|
||||
+ if (body.captchaMaxFailures !== null && (!Number.isInteger(body.captchaMaxFailures) || body.captchaMaxFailures < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'captchaMaxFailures 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.captchaMaxFailures = body.captchaMaxFailures
|
||||
+ }
|
||||
+
|
||||
+ if (body.autoBackupEnabled !== undefined) {
|
||||
+ if (typeof body.autoBackupEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'autoBackupEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.autoBackupEnabled = body.autoBackupEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.autoBackupIntervalHours !== undefined) {
|
||||
+ if (body.autoBackupIntervalHours !== null && (!Number.isInteger(body.autoBackupIntervalHours) || body.autoBackupIntervalHours < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'autoBackupIntervalHours 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.autoBackupIntervalHours = body.autoBackupIntervalHours
|
||||
+ }
|
||||
+
|
||||
+ if (body.autoBackupRetentionDays !== undefined) {
|
||||
+ if (body.autoBackupRetentionDays !== null && (!Number.isInteger(body.autoBackupRetentionDays) || body.autoBackupRetentionDays < 0)) {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'autoBackupRetentionDays 必须是非负整数或null'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.autoBackupRetentionDays = body.autoBackupRetentionDays
|
||||
+ }
|
||||
+
|
||||
+ if (body.statisticsCodeEnabled !== undefined) {
|
||||
+ if (typeof body.statisticsCodeEnabled !== 'boolean') {
|
||||
+ throw createError({
|
||||
+ statusCode: 400,
|
||||
+ message: 'statisticsCodeEnabled 必须是布尔值'
|
||||
+ })
|
||||
+ }
|
||||
+ updateData.statisticsCodeEnabled = body.statisticsCodeEnabled
|
||||
+ }
|
||||
+
|
||||
+ if (body.statisticsCodeBody !== undefined) {
|
||||
+ updateData.statisticsCodeBody = body.statisticsCodeBody
|
||||
+ }
|
||||
+
|
||||
+ if (body.statisticsCodeHead !== undefined) {
|
||||
+ updateData.statisticsCodeHead = body.statisticsCodeHead
|
||||
+ }
|
||||
+
|
||||
+ if (body.defaultTheme !== undefined) {
|
||||
+ updateData.defaultTheme =
|
||||
+ typeof body.defaultTheme === 'string' && body.defaultTheme.trim()
|
||||
+ ? body.defaultTheme.trim()
|
||||
+ : null
|
||||
+ }
|
||||
+
|
||||
+ if (body.enabledThemes !== undefined) {
|
||||
+ updateData.enabledThemes =
|
||||
+ body.enabledThemes === null || body.enabledThemes === undefined
|
||||
+ ? null
|
||||
+ : typeof body.enabledThemes === 'string'
|
||||
+ ? body.enabledThemes
|
||||
+ : JSON.stringify(body.enabledThemes)
|
||||
+ }
|
||||
+
|
||||
// SMTP配置字段
|
||||
if (body.smtpEnabled !== undefined) {
|
||||
if (typeof body.smtpEnabled !== 'boolean') {
|
||||
@@ -266,10 +780,69 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
+ const DEFAULTS = {
|
||||
+ submissionNoteRequiresApproval: false,
|
||||
+ telemetryEnabled: true,
|
||||
+ showBeianIcon: true,
|
||||
+ scheduleDaysDisplay: 7,
|
||||
+ scheduleDaysEditable: 3,
|
||||
+ scheduleDaysRequestable: 14,
|
||||
+ scheduleDaysBeforeEnabled: false,
|
||||
+ scheduleDaysBefore: 1,
|
||||
+ scheduleDaysAfterEnabled: false,
|
||||
+ scheduleDaysAfter: 1,
|
||||
+ forcePasswordChangeOnFirstLogin: false,
|
||||
+ enableSubmissionRestriction: false,
|
||||
+ submissionRestrictionScope: 'all',
|
||||
+ cardCodeEnabled: false,
|
||||
+ cardCodeSubmissionRequired: false,
|
||||
+ cardCodeVoteEnabled: false,
|
||||
+ cardCodeReplayEnabled: false,
|
||||
+ enableCardCodeRequests: false,
|
||||
+ requireCardCodeForRequests: false,
|
||||
+ enableCardCodeLimitBypass: false,
|
||||
+ submissionRestrictionEnabled: false,
|
||||
+ submissionRestrictionGradeEnabled: false,
|
||||
+ submissionRestrictionClassEnabled: false,
|
||||
+ submissionRestrictionGraduateEnabled: false,
|
||||
+ submissionRestrictionWithdrawnEnabled: false,
|
||||
+ captchaProvider: 'TURNSTILE',
|
||||
+ captchaEnabled: false,
|
||||
+ allowRegister: true,
|
||||
+ registerRequireEmail: false,
|
||||
+ registerRequireEmailVerify: false,
|
||||
+ registerDefaultRole: 'USER',
|
||||
+ registerRequiresApproval: false,
|
||||
+ oauthRegisterRequiresApproval: false,
|
||||
+ registerEmailRequired: false,
|
||||
+ registerRequiresGradeClass: false,
|
||||
+ oauthEnabled: false,
|
||||
+ oauthAllowRegister: true,
|
||||
+ oauthAllowLogin: true,
|
||||
+ oauthAllowBind: true,
|
||||
+ allowOAuthRegistration: false,
|
||||
+ oauthProviders: '[]',
|
||||
+ githubOAuthEnabled: false,
|
||||
+ casdoorOAuthEnabled: false,
|
||||
+ googleOAuthEnabled: false,
|
||||
+ aggregateOAuthEnabled: false,
|
||||
+ aggregateOAuthLoginType: 'qq',
|
||||
+ customOAuthEnabled: false,
|
||||
+ captchaMaxFailures: 3,
|
||||
+ autoBackupEnabled: false,
|
||||
+ autoBackupIntervalHours: 24,
|
||||
+ autoBackupRetentionDays: 30,
|
||||
+ statisticsCodeEnabled: false,
|
||||
+ defaultTheme: 'System',
|
||||
+ enabledThemes: '["System","ClassicDark","ClassicLight","ModernLight"]'
|
||||
+} as const
|
||||
+
|
||||
// 获取当前设置,如果不存在则创建
|
||||
const settingsResult = await db.select().from(systemSettings).limit(1)
|
||||
let settings = settingsResult[0] || null
|
||||
|
||||
+ const fillDefaults = (obj: any) => ({ ...obj, ...Object.fromEntries(Object.entries(DEFAULTS).filter(([k]) => !Object.prototype.hasOwnProperty.call(obj || {}, k) || (obj && obj[k] === undefined || obj[k] === null))) }
|
||||
+
|
||||
if (!settings) {
|
||||
// 如果不存在,创建新设置
|
||||
const newSettingsResult = await db
|
||||
@@ -324,7 +897,7 @@ export default defineEventHandler(async (event) => {
|
||||
.set(updateData)
|
||||
.where(eq(systemSettings.id, settings.id))
|
||||
.returning()
|
||||
- settings = updatedSettingsResult[0]
|
||||
+ settings = fillDefaults(updatedSettingsResult[0])
|
||||
}
|
||||
|
||||
// 清除系统设置缓存
|
||||
diff --git a/server/api/admin/system-settings/index.ts b/server/api/admin/system-settings/index.ts
|
||||
index cbe9256..8603bf9 100644
|
||||
--- a/server/api/admin/system-settings/index.ts
|
||||
+++ b/server/api/admin/system-settings/index.ts
|
||||
@@ -1,6 +1,7 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { systemSettings } from '~/drizzle/schema'
|
||||
import { CacheService } from '../../../services/cacheService'
|
||||
+import { SYSTEM_SETTINGS_DEFAULTS } from '~~/server/utils/system-settings-defaults'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 检查用户认证和权限
|
||||
@@ -66,13 +67,15 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// 将设置存入缓存
|
||||
try {
|
||||
+ const merged: any = { ...SYSTEM_SETTINGS_DEFAULTS, ...(settings || {}) }
|
||||
+ settings = merged
|
||||
await cacheService.setSystemSettings(settings)
|
||||
} catch (cacheError) {
|
||||
console.warn('缓存系统设置失败:', cacheError)
|
||||
}
|
||||
|
||||
// 隐藏敏感字段
|
||||
- if (settings.smtpPassword) {
|
||||
+ if (settings && settings.smtpPassword) {
|
||||
settings.smtpPassword = '****************'
|
||||
}
|
||||
|
||||
diff --git a/server/api/site-config.get.ts b/server/api/site-config.get.ts
|
||||
index 2e0c55c..d19a930 100644
|
||||
--- a/server/api/site-config.get.ts
|
||||
+++ b/server/api/site-config.get.ts
|
||||
@@ -2,6 +2,7 @@ import { db } from '~/drizzle/db'
|
||||
import { systemSettings } from '~/drizzle/schema'
|
||||
import { cacheService } from '../services/cacheService'
|
||||
import { isRedisReady } from '../utils/redis'
|
||||
+import { SYSTEM_SETTINGS_DEFAULTS } from '~~/server/utils/system-settings-defaults'
|
||||
|
||||
// 过滤敏感字段,只返回公开配置
|
||||
const publicFields = [
|
||||
@@ -36,13 +37,11 @@ const publicFields = [
|
||||
]
|
||||
|
||||
const filterPublicSettings = (data: any) => {
|
||||
- if (!data) {
|
||||
- return {}
|
||||
- }
|
||||
+ const merged: any = { ...SYSTEM_SETTINGS_DEFAULTS, ...(data || {}) }
|
||||
const result: Record<string, any> = {}
|
||||
for (const key of publicFields) {
|
||||
- if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
- result[key] = data[key]
|
||||
+ if (Object.prototype.hasOwnProperty.call(merged, key)) {
|
||||
+ result[key] = merged[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -55,7 +54,7 @@ export default defineEventHandler(async (event) => {
|
||||
const cachedSettings = await cacheService.getSystemSettings()
|
||||
if (cachedSettings) {
|
||||
console.log('[API] 系统设置缓存命中')
|
||||
- return filterPublicSettings(cachedSettings)
|
||||
+ return filterPublicSettings({ ...SYSTEM_SETTINGS_DEFAULTS, ...cachedSettings })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +99,7 @@ export default defineEventHandler(async (event) => {
|
||||
settings = newSettings[0]
|
||||
}
|
||||
|
||||
- const publicSettings = filterPublicSettings(settings)
|
||||
+ const publicSettings = filterPublicSettings({ ...SYSTEM_SETTINGS_DEFAULTS, ...(settings || {}) })
|
||||
|
||||
// 将结果缓存到Redis(如果可用)- 永久缓存
|
||||
if (settings && isRedisReady()) {
|
||||
diff --git a/server/services/cacheService.ts b/server/services/cacheService.ts
|
||||
index 7c91700..c28cbad 100644
|
||||
--- a/server/services/cacheService.ts
|
||||
+++ b/server/services/cacheService.ts
|
||||
@@ -16,7 +16,7 @@ const CACHE_TTL = {
|
||||
SONG_COUNT: 180, // 3分钟
|
||||
USER_VOTES: 120, // 2分钟
|
||||
EMPTY_RESULT: 60, // 1分钟(空结果缓存时间较短)
|
||||
- SYSTEM_SETTINGS: 3600, // 60分钟
|
||||
+ SYSTEM_SETTINGS: 900, // 15分钟(新功能上线后避免 Redis 缓存旧 DB 值导致主题/排期等配置失效)
|
||||
RANDOM_OFFSET: 60, // 随机偏移量,防止缓存雪崩
|
||||
LOCK_TIMEOUT: 30, // 分布式锁超时时间
|
||||
REFRESH_AHEAD: 60 // 提前刷新时间(秒)
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
From 124280a92f0051642bb7254a590c53e70fc6ff7e Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Fri, 11 Sep 2026 21:59:19 +0800
|
||||
Subject: [PATCH 31/58] =?UTF-8?q?fix(ci):=20=E6=B6=88=E9=99=A4=20preflight?=
|
||||
=?UTF-8?q?=20Set=20up=20job=20docker=20pull=20=E5=8D=A1=E7=82=B9=EF=BC=8C?=
|
||||
=?UTF-8?q?=E6=94=B9=E7=94=A8=E5=AE=BF=E4=B8=BB=E6=9C=BA=20Node=2022=20+?=
|
||||
=?UTF-8?q?=20daemon.json=20mirrors=20+=20Dockerfile=20ARG=20=E5=8F=8C?=
|
||||
=?UTF-8?q?=E4=BF=9D=E9=99=A9?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- preflight:删除 container.image: node:22-bookworm-slim,避免 Runner 在 Set up job 阶段独立 docker pull(国内连 Hub 被限速一直卡)
|
||||
- preflight:改用 runs-on 直出 + actions/setup-node@v4 装 Node 22,npm registry 设 npmmirror
|
||||
- build-image:python3 合法合成 /etc/docker/daemon.json 注入 3 组 docker.io registry mirrors(daocloud/1panel/1ms.run),systemctl→service→失败跳过 三级 non-intrusive 重启 docker
|
||||
- Dockerfile:新增全局 ARG DOCKER_MIRROR / NODE_BUILDER_TAG / NODE_RUNTIME_TAG,FROM 统一前缀化;本地开发 DOCKER_MIRROR 为空时等价原写法,体验不变
|
||||
- buildx BUILD_ARGS:新增 3 个 --build-arg 把 DOCKER_MIRROR_PREFIX / NODE_BUILDER_TAG / NODE_RUNTIME_TAG 传入,CI 拉 node:22-alpine 必走国内源
|
||||
- 修复后:workflow 触发时 Preflight 日志不会再出现 Starting job container / docker pull image=node:***-bookworm-slim;前面 system-settings defaults 兜底修复可顺利部署上线
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 108 ++++++++++++++++++++++++------------
|
||||
Dockerfile | 15 ++++-
|
||||
2 files changed, 86 insertions(+), 37 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 350afb3..4077b1c 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -36,52 +36,45 @@ env:
|
||||
APT_MIRROR_UBUNTU_SECURITY_HTTP: http://mirrors.tuna.tsinghua.edu.cn/ubuntu-security
|
||||
HTTP_PROXY_EXAMPLE: ""
|
||||
HTTPS_PROXY_EXAMPLE: ""
|
||||
+ # ========== Docker 官方镜像国内加速(Runner 拉 node:alpine / buildkit 等最常见卡点) ==========
|
||||
+ # 注意:如果你知道哪个镜像源在内网最快,可以在仓库 Variables 里覆盖 DOCKER_REGISTRY_MIRROR_1/2/3
|
||||
+ DOCKER_REGISTRY_MIRROR_1: https://docker.m.daocloud.io
|
||||
+ DOCKER_REGISTRY_MIRROR_2: https://docker.1panel.live
|
||||
+ DOCKER_REGISTRY_MIRROR_3: https://docker.1ms.run
|
||||
+ # Dockerfile 内 FROM 前缀替换(ARG DOCKER_MIRROR=…),避免 buildx build 内部 FROM node:22-alpine 再被 Hub 限速
|
||||
+ DOCKER_MIRROR_PREFIX: docker.m.daocloud.io/library/
|
||||
+ NODE_BUILDER_TAG: "22-alpine"
|
||||
+ NODE_RUNTIME_TAG: "22-alpine"
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight 语法与构建检查
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
- container:
|
||||
- image: node:22-bookworm-slim
|
||||
+ # 关键修复:去掉 container.image: node:22-bookworm-slim
|
||||
+ # 原因:Runner 在 Set up job 阶段会先 docker pull 这个镜像,国内连 Docker Hub 被限速会一直卡转圈。
|
||||
+ # 直接在 Runner 宿主机上执行(通常自带 node 20+);为保证 Node 22 一致性,用 actions/setup-node 主动安装。
|
||||
steps:
|
||||
- - name: 安装基础运行依赖(git/ca-certificates,已换清华 TUNA 源)
|
||||
+ - name: 安装基础运行依赖(git/curl/ca-certificates,Runner 自带)
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ command -v git || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends git ca-certificates)
|
||||
+ command -v curl || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends curl)
|
||||
+ command -v node && node -v || true
|
||||
+ command -v npm && npm -v || true
|
||||
+
|
||||
+ - name: 使用 Node.js 22(若 Runner 自带版本不足 22 则自动下载)
|
||||
+ uses: actions/setup-node@v4
|
||||
+ with:
|
||||
+ node-version: "22"
|
||||
+
|
||||
+ - name: 校验 Node + npm + git 可用
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
- # ========== Step 0:先用 HTTP 清华源装 ca-certificates(解鸡生蛋:HTTPS 需要证书,证书得先用 HTTP 装) ==========
|
||||
- SOURCES_FILE="/etc/apt/sources.list.d/debian.sources"
|
||||
- if [ -f "${SOURCES_FILE}" ]; then
|
||||
- sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" "${SOURCES_FILE}"
|
||||
- sed -i "s|http://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" "${SOURCES_FILE}"
|
||||
- sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" "${SOURCES_FILE}"
|
||||
- sed -i "s|https://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" "${SOURCES_FILE}"
|
||||
- else
|
||||
- if [ -f /etc/apt/sources.list ]; then
|
||||
- sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" /etc/apt/sources.list
|
||||
- sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN_HTTP}|g" /etc/apt/sources.list
|
||||
- sed -i "s|http://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" /etc/apt/sources.list
|
||||
- sed -i "s|https://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|g" /etc/apt/sources.list
|
||||
- fi
|
||||
- fi
|
||||
- apt-get update -qq
|
||||
- DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends ca-certificates
|
||||
- update-ca-certificates 2>/dev/null || true
|
||||
-
|
||||
- # ========== Step 1:现在有了证书,换成 HTTPS 清华源(更安全)+ 装 git/curl ==========
|
||||
- if [ -f "${SOURCES_FILE}" ]; then
|
||||
- sed -i "s|${APT_MIRROR_DEBIAN_HTTP}|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
|
||||
- sed -i "s|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
|
||||
- else
|
||||
- if [ -f /etc/apt/sources.list ]; then
|
||||
- sed -i "s|${APT_MIRROR_DEBIAN_HTTP}|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
|
||||
- sed -i "s|${APT_MIRROR_DEBIAN_SECURITY_HTTP}|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
|
||||
- fi
|
||||
- fi
|
||||
- apt-get update -qq
|
||||
- DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends git curl
|
||||
- rm -rf /var/lib/apt/lists/*
|
||||
# 全局 git 代理:任何 git clone/fetch/pull https://github.com/... 自动走 gh-proxy 前缀
|
||||
git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
||||
+ # npm registry 走 npmmirror,避免后续 npm ci 也卡
|
||||
+ npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
|
||||
git --version
|
||||
node -v
|
||||
npm -v
|
||||
@@ -166,6 +159,48 @@ jobs:
|
||||
sudo git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
||||
# npm:强制用 npmmirror(与 preflight 对齐)
|
||||
npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
|
||||
+
|
||||
+ # ========== 关键修复:宿主机 Docker daemon 先注入国内 registry mirrors ==========
|
||||
+ # 原因:Runner 任何 docker pull / buildx create(moby/buildkit 拉基础镜像)都要先经过 daemon
|
||||
+ # 如果直接连 docker.io,国内会被限速到 <200KB/s,导致 Setup / BuildKit 拉镜像一直卡。
|
||||
+ DOCKER_DAEMON_CONF="/etc/docker/daemon.json"
|
||||
+ if command -v docker >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then
|
||||
+ sudo mkdir -p /etc/docker 2>/dev/null || true
|
||||
+ OLD_MIRRORS_JSON="{}"
|
||||
+ if [ -f "${DOCKER_DAEMON_CONF}" ]; then
|
||||
+ OLD_MIRRORS_JSON="$(sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || echo "{}")"
|
||||
+ fi
|
||||
+ # 用 python3 合成合法 JSON(避免 bash 手搓 JSON 转义坑)
|
||||
+ NEW_DAEMON_JSON="$(
|
||||
+ python3 - "$OLD_MIRRORS_JSON" \
|
||||
+ "${DOCKER_REGISTRY_MIRROR_1}" "${DOCKER_REGISTRY_MIRROR_2}" "${DOCKER_REGISTRY_MIRROR_3}" <<'PY'
|
||||
+import json, sys
|
||||
+try:
|
||||
+ data = json.loads(sys.argv[1] or "{}")
|
||||
+except Exception:
|
||||
+ data = {}
|
||||
+mirrors_envs = [m for m in sys.argv[2:5] if m]
|
||||
+existing = data.get("registry-mirrors") or []
|
||||
+if isinstance(existing, list):
|
||||
+ combined = list(dict.fromkeys([*mirrors_envs, *existing]))
|
||||
+else:
|
||||
+ combined = mirrors_envs
|
||||
+if combined:
|
||||
+ data["registry-mirrors"] = combined
|
||||
+data.setdefault("log-driver", "json-file")
|
||||
+print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
+PY
|
||||
+ )"
|
||||
+ echo "${NEW_DAEMON_JSON}" | sudo tee "${DOCKER_DAEMON_CONF}" >/dev/null
|
||||
+ echo "[Docker] daemon.json 已注入 registry-mirrors:"
|
||||
+ sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || true
|
||||
+ # 非侵入式重启 docker;runner 没 systemd 时用 service 兜底;失败也跳过(buildkitd.toml 内部仍有 mirrors)
|
||||
+ sudo systemctl restart docker 2>/dev/null \
|
||||
+ || sudo service docker restart 2>/dev/null \
|
||||
+ || echo "[Docker] systemctl/service 重启失败,跳过(后续 buildx builder 内部仍有 registry mirrors)"
|
||||
+ sleep 3 2>/dev/null || true
|
||||
+ fi
|
||||
+
|
||||
echo "环境准备完成"
|
||||
cat /etc/os-release 2>/dev/null | head -4 || true
|
||||
docker --version
|
||||
@@ -307,6 +342,9 @@ jobs:
|
||||
BUILD_ARGS=(
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1
|
||||
--build-arg NODE_ENV=production
|
||||
+ --build-arg DOCKER_MIRROR="${DOCKER_MIRROR_PREFIX}"
|
||||
+ --build-arg NODE_BUILDER_TAG="${NODE_BUILDER_TAG}"
|
||||
+ --build-arg NODE_RUNTIME_TAG="${NODE_RUNTIME_TAG}"
|
||||
)
|
||||
# 说明:暂时关闭 registry 分层缓存(buildkitd 推 buildcache 也要认证,同样会触发 Gitea 1.27 OAuth2 空 token)
|
||||
# 改为本地 Docker daemon 缓存,已构建的层会自动复用,下次 build 速度几乎相同
|
||||
diff --git a/Dockerfile b/Dockerfile
|
||||
index 159407e..8b8a59c 100644
|
||||
--- a/Dockerfile
|
||||
+++ b/Dockerfile
|
||||
@@ -1,9 +1,20 @@
|
||||
# 支持: linux/amd64, linux/arm64, linux/arm/v7
|
||||
|
||||
+# ========== 支持在 CI 注入国内镜像源前缀拉 node 基础镜像 ==========
|
||||
+# 用法:docker buildx build \
|
||||
+# --build-arg DOCKER_MIRROR=docker.m.daocloud.io/library/ \
|
||||
+# --build-arg NODE_BUILDER_TAG=22-alpine \
|
||||
+# --build-arg NODE_RUNTIME_TAG=22-alpine \
|
||||
+# ...
|
||||
+# 缺省(DOCKER_MIRROR 为空)等价于原来的 node:22-alpine,本地开发不变
|
||||
+ARG DOCKER_MIRROR=""
|
||||
+ARG NODE_BUILDER_TAG="22-alpine"
|
||||
+ARG NODE_RUNTIME_TAG="22-alpine"
|
||||
+
|
||||
# ==========================================
|
||||
# 第一阶段:依赖安装与构建阶段
|
||||
# ==========================================
|
||||
-FROM node:22-alpine AS builder
|
||||
+FROM ${DOCKER_MIRROR}node:${NODE_BUILDER_TAG} AS builder
|
||||
|
||||
WORKDIR /app
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
@@ -34,7 +45,7 @@ RUN npm prune --omit=dev
|
||||
# ==========================================
|
||||
# 第二阶段:运行阶段
|
||||
# ==========================================
|
||||
-FROM node:22-alpine AS runtime
|
||||
+FROM ${DOCKER_MIRROR}node:${NODE_RUNTIME_TAG} AS runtime
|
||||
|
||||
USER root
|
||||
WORKDIR /app
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
From afb4cd3669bf0a2f1c9d906238efa007c706a61b Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Fri, 11 Sep 2026 22:28:42 +0800
|
||||
Subject: [PATCH 32/58] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=20deploy.ym?=
|
||||
=?UTF-8?q?l=20YAML=20=E8=AF=AD=E6=B3=95=E9=94=99=E8=AF=AF=EF=BC=88line=20?=
|
||||
=?UTF-8?q?177=20could=20not=20find=20expected=20':'=EF=BC=89=EF=BC=8C?=
|
||||
=?UTF-8?q?=E6=B6=88=E9=99=A4=20heredoc=20=E4=B8=8E=E5=9D=97=E6=A0=87?=
|
||||
=?UTF-8?q?=E9=87=8F=E7=BC=A9=E8=BF=9B=E5=86=B2=E7=AA=81?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- 根因:build-image job 注入 Docker daemon.json 时用了 bash heredoc <<'PY'...PY;heredoc 内部 Python 代码在 YAML 文件里缩进为 0,触发 run:| 字面块标量提前结束;Gitea YAML 解析器尝试把下一行解析为新的 key 却找不到冒号 → 抛 line 177 could not find expected ':',整个 workflow 无法触发
|
||||
- 修复:移除 heredoc 结构,改为 PY_CODE 单引号字符串 + printf '%s' | python3 - stdin 管道传递;单引号内所有 Python 行统一保持与外层 shell 语句一致的 12 spaces 前导缩进(满足 YAML 块标量 ≥ 10 spaces 基础缩进要求);Python 顶级代码统一缩进 12 spaces,块内再缩进 4 spaces,避免 IndentationError
|
||||
- 验证:IDE YAML 诊断 0 错误;Gitea Actions 页左侧 deploy.yml 红色感叹号应立即消失,可正常触发 run
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 40 ++++++++++++++++++-------------------
|
||||
1 file changed, 19 insertions(+), 21 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 4077b1c..d803c3a 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -163,6 +163,8 @@ jobs:
|
||||
# ========== 关键修复:宿主机 Docker daemon 先注入国内 registry mirrors ==========
|
||||
# 原因:Runner 任何 docker pull / buildx create(moby/buildkit 拉基础镜像)都要先经过 daemon
|
||||
# 如果直接连 docker.io,国内会被限速到 <200KB/s,导致 Setup / BuildKit 拉镜像一直卡。
|
||||
+ # 注意:此处用 PY_CODE 字符串 + printf 管道传 Python 代码给 stdin,
|
||||
+ # 避免 <<'PY' heredoc 结构与 YAML run:| 块标量缩进冲突导致的 could not find expected ':' 报错。
|
||||
DOCKER_DAEMON_CONF="/etc/docker/daemon.json"
|
||||
if command -v docker >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then
|
||||
sudo mkdir -p /etc/docker 2>/dev/null || true
|
||||
@@ -170,27 +172,23 @@ jobs:
|
||||
if [ -f "${DOCKER_DAEMON_CONF}" ]; then
|
||||
OLD_MIRRORS_JSON="$(sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || echo "{}")"
|
||||
fi
|
||||
- # 用 python3 合成合法 JSON(避免 bash 手搓 JSON 转义坑)
|
||||
- NEW_DAEMON_JSON="$(
|
||||
- python3 - "$OLD_MIRRORS_JSON" \
|
||||
- "${DOCKER_REGISTRY_MIRROR_1}" "${DOCKER_REGISTRY_MIRROR_2}" "${DOCKER_REGISTRY_MIRROR_3}" <<'PY'
|
||||
-import json, sys
|
||||
-try:
|
||||
- data = json.loads(sys.argv[1] or "{}")
|
||||
-except Exception:
|
||||
- data = {}
|
||||
-mirrors_envs = [m for m in sys.argv[2:5] if m]
|
||||
-existing = data.get("registry-mirrors") or []
|
||||
-if isinstance(existing, list):
|
||||
- combined = list(dict.fromkeys([*mirrors_envs, *existing]))
|
||||
-else:
|
||||
- combined = mirrors_envs
|
||||
-if combined:
|
||||
- data["registry-mirrors"] = combined
|
||||
-data.setdefault("log-driver", "json-file")
|
||||
-print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
-PY
|
||||
- )"
|
||||
+ PY_CODE=' import json, sys
|
||||
+ try:
|
||||
+ data = json.loads(sys.argv[1] or "{}")
|
||||
+ except Exception:
|
||||
+ data = {}
|
||||
+ mirrors_envs = [m for m in sys.argv[2:5] if m]
|
||||
+ existing = data.get("registry-mirrors") or []
|
||||
+ if isinstance(existing, list):
|
||||
+ combined = list(dict.fromkeys([*mirrors_envs, *existing]))
|
||||
+ else:
|
||||
+ combined = mirrors_envs
|
||||
+ if combined:
|
||||
+ data["registry-mirrors"] = combined
|
||||
+ data.setdefault("log-driver", "json-file")
|
||||
+ print(json.dumps(data, ensure_ascii=False, indent=2))'
|
||||
+ NEW_DAEMON_JSON="$(printf '%s\n' "$PY_CODE" | python3 - "$OLD_MIRRORS_JSON" \
|
||||
+ "${DOCKER_REGISTRY_MIRROR_1}" "${DOCKER_REGISTRY_MIRROR_2}" "${DOCKER_REGISTRY_MIRROR_3}")"
|
||||
echo "${NEW_DAEMON_JSON}" | sudo tee "${DOCKER_DAEMON_CONF}" >/dev/null
|
||||
echo "[Docker] daemon.json 已注入 registry-mirrors:"
|
||||
sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || true
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
From 0373c19b54348942e54ba69711644f8f51993f82 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Fri, 11 Sep 2026 22:46:40 +0800
|
||||
Subject: [PATCH 33/58] fix(ci): remove actions/setup-node; use
|
||||
npmmirror/gh-proxy for all remote fetches
|
||||
|
||||
Issue: actions/setup-node@v4 directly connects to https://github.com/actions/node-versions/releases/download/... to download Node tarballs (log line Acquiring node-v*-linux-x64.tar.gz), which is throttled or connection-reset from inside China, causing preflight to hang until 15m timeout.
|
||||
|
||||
Fix: align preflight with build-image job policy (NO third-party 'uses:' directives, ALL remote resources use CN mirrors or GH_PROXY_PREFIX):
|
||||
|
||||
- Remove uses: actions/setup-node@v4 completely; replace with plain run steps
|
||||
|
||||
- 3-tier Node 22 readiness with zero direct github.com connections:
|
||||
|
||||
1) Fast path: reuse Runner's system node if major version >= 22, skip download entirely
|
||||
|
||||
2) Primary: fetch Node 22 tarball from npmmirror CN binary mirror (https://registry.npmmirror.com/-/binary/node/...) and auto-resolve latest 22.x patch via npmmirror index.json using python3 single-quote script + printf stdin (NO heredoc, which previously caused YAML block-scalar indentation parse errors)
|
||||
|
||||
3) Fallback: GH_PROXY_PREFIX + official nodejs.org dist URL for the same tarball (proxied, never plain github.com)
|
||||
|
||||
- NODE_VERSION_FALLBACK=22.13.0 fixed guard when index resolution fails
|
||||
|
||||
- Post-install: prepend new node bin PATH to GITHUB_ENV so subsequent steps (clone / npm ci / nuxt build) inherit Node 22 automatically
|
||||
|
||||
- Audit: grep deploy.yml now yields zero 'uses:' directives
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 129 ++++++++++++++++++++++++++++++++----
|
||||
1 file changed, 117 insertions(+), 12 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index d803c3a..87c6524 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -51,22 +51,127 @@ jobs:
|
||||
name: Preflight 语法与构建检查
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
- # 关键修复:去掉 container.image: node:22-bookworm-slim
|
||||
- # 原因:Runner 在 Set up job 阶段会先 docker pull 这个镜像,国内连 Docker Hub 被限速会一直卡转圈。
|
||||
- # 直接在 Runner 宿主机上执行(通常自带 node 20+);为保证 Node 22 一致性,用 actions/setup-node 主动安装。
|
||||
+ # 关键修复 1:去掉 container.image: node:22-bookworm-slim
|
||||
+ # Runner 在 Set up job 阶段会先 docker pull,国内连 Docker Hub 被限速会一直卡转圈。
|
||||
+ # 关键修复 2:去掉 uses: actions/setup-node@v4
|
||||
+ # setup-node 会直连 GitHub Releases (github.com/actions/node-versions/releases/download/...)
|
||||
+ # 下载 Node 二进制,国内被限速 → 一直卡在 Acquiring node-v*-linux-x64.tar.gz。
|
||||
+ # 与 build-image job「禁止第三方 uses、全部用代理/国内源」风格完全对齐:
|
||||
+ # - Runner 自带 node ≥22 直接用
|
||||
+ # - 否则优先 npmmirror 国内镜像下载 Node 22 二进制
|
||||
+ # - 最后兜底用 GH_PROXY_PREFIX 代理 GitHub node-versions Releases 直链
|
||||
+ env:
|
||||
+ NODE_MAJOR: "22"
|
||||
+ NODE_VERSION_FALLBACK: "22.13.0"
|
||||
+ NODE_MIRROR_PRIMARY: "https://registry.npmmirror.com/-/binary/node"
|
||||
steps:
|
||||
- - name: 安装基础运行依赖(git/curl/ca-certificates,Runner 自带)
|
||||
+ - name: 安装基础运行依赖(git/curl/ca-certificates/tar/xz-utils,Runner 自带)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
command -v git || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends git ca-certificates)
|
||||
- command -v curl || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends curl)
|
||||
- command -v node && node -v || true
|
||||
- command -v npm && npm -v || true
|
||||
-
|
||||
- - name: 使用 Node.js 22(若 Runner 自带版本不足 22 则自动下载)
|
||||
- uses: actions/setup-node@v4
|
||||
- with:
|
||||
- node-version: "22"
|
||||
+ command -v curl || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends curl ca-certificates)
|
||||
+ command -v tar || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends tar)
|
||||
+ command -v xz || command -v unxz || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends xz-utils) || true
|
||||
+ # 预先打印 Runner 自带版本,方便排障
|
||||
+ command -v node && echo "系统 node: $(node -v)" || echo "系统 node: 未安装"
|
||||
+ command -v npm && echo "系统 npm: $(npm -v)" || echo "系统 npm: 未安装"
|
||||
+
|
||||
+ - name: 确保 Node.js 22 可用(npmmirror 优先 / gh-proxy 兜底 / 绝不直连 GitHub)
|
||||
+ run: |
|
||||
+ set -euxo pipefail
|
||||
+ # 先判断:当前系统 node 主版本 ≥ NODE_MAJOR 直接跳过
|
||||
+ if command -v node >/dev/null 2>&1; then
|
||||
+ CUR_NODE_MAJOR="$(node -v | tr -d 'v' | cut -d. -f1 || echo 0)"
|
||||
+ if [ "${CUR_NODE_MAJOR:-0}" -ge "${NODE_MAJOR}" ]; then
|
||||
+ echo "Runner 已自带 Node $(node -v)(≥ v${NODE_MAJOR}.x),直接复用,不下载"
|
||||
+ node -v; npm -v
|
||||
+ exit 0
|
||||
+ fi
|
||||
+ echo "Runner 自带 Node v${CUR_NODE_MAJOR} 不足 v${NODE_MAJOR}.x,手动升级"
|
||||
+ fi
|
||||
+
|
||||
+ ARCH="$(uname -m)"
|
||||
+ case "$ARCH" in
|
||||
+ x86_64|amd64) PLAT="linux-x64" ;;
|
||||
+ aarch64|arm64) PLAT="linux-arm64" ;;
|
||||
+ armv7l) PLAT="linux-armv7l" ;;
|
||||
+ *) echo "不支持的架构:$ARCH" >&2; exit 1 ;;
|
||||
+ esac
|
||||
+ echo "目标架构:$PLAT"
|
||||
+
|
||||
+ INSTALL_DIR="${RUNNER_TEMP:-/tmp}/node-${NODE_MAJOR}"
|
||||
+ mkdir -p "$INSTALL_DIR"
|
||||
+
|
||||
+ # 找 Node 22 最新 patch 版本号:不用 GitHub API,走 npmmirror 的 index.json 解析
|
||||
+ NODE_VER="${NODE_VERSION_FALLBACK}"
|
||||
+ if command -v python3 >/dev/null 2>&1; then
|
||||
+ # 注意:不使用 heredoc,避免 PY_MAJOR 前导缩进 0 导致 YAML 块标量提前结束解析失败
|
||||
+ PY_NODE_CODE=' import json, re, sys
|
||||
+ try:
|
||||
+ data = json.load(sys.stdin)
|
||||
+ except Exception:
|
||||
+ data = []
|
||||
+ target = sys.argv[1] if len(sys.argv) > 1 else "22"
|
||||
+ best_triple = None
|
||||
+ best_v = ""
|
||||
+ for d in data:
|
||||
+ v = d.get("version", "")
|
||||
+ m = re.match(r"^v(\d+)\.(\d+)\.(\d+)", v)
|
||||
+ if not m: continue
|
||||
+ major, minor, patch = m.groups()
|
||||
+ if major != target: continue
|
||||
+ triple = (int(minor), int(patch))
|
||||
+ if (best_triple is None) or (triple > best_triple):
|
||||
+ best_triple = triple
|
||||
+ best_v = f"{major}.{minor}.{patch}"
|
||||
+ if best_v:
|
||||
+ print(best_v)'
|
||||
+ REMOTE_LATEST=""
|
||||
+ REMOTE_LATEST="$(curl -fsSL --max-time 20 "${NODE_MIRROR_PRIMARY}/index.json" 2>/dev/null \
|
||||
+ | printf '%s\n' "$PY_NODE_CODE" | python3 - "${NODE_MAJOR}" || true)"
|
||||
+ if [ -n "$REMOTE_LATEST" ]; then
|
||||
+ NODE_VER="$REMOTE_LATEST"
|
||||
+ echo "npmmirror 查得 Node 22 latest: v${NODE_VER}"
|
||||
+ else
|
||||
+ echo "npmmirror index 查询失败,使用兜底: v${NODE_VER}"
|
||||
+ fi
|
||||
+ else
|
||||
+ echo "python3 不可用,跳过 npmmirror 最新版本探测,使用兜底: v${NODE_VER}"
|
||||
+ fi
|
||||
+ TAR_XZ="node-v${NODE_VER}-${PLAT}.tar.xz"
|
||||
+ mkdir -p /tmp/node-dl-cache
|
||||
+
|
||||
+ # 下载 URL 1:npmmirror 国内二进制镜像(首选,国内最快)
|
||||
+ URL_1="${NODE_MIRROR_PRIMARY}/v${NODE_VER}/${TAR_XZ}"
|
||||
+ # 下载 URL 2:nodejs.org 官方直链 + GH_PROXY 前缀兜底(绝不直连 GitHub.com)
|
||||
+ URL_2="${GH_PROXY_PREFIX}https://nodejs.org/dist/v${NODE_VER}/${TAR_XZ}"
|
||||
+
|
||||
+ DL_OK=0
|
||||
+ for URL in "$URL_1" "$URL_2"; do
|
||||
+ echo "尝试下载:$URL"
|
||||
+ if curl -fsSL --max-time 300 --retry 3 --retry-delay 2 -o "/tmp/node-dl-cache/${TAR_XZ}" "$URL"; then
|
||||
+ echo "下载 OK:$URL"
|
||||
+ DL_OK=1
|
||||
+ break
|
||||
+ fi
|
||||
+ echo "下载失败,尝试下一个源:$URL"
|
||||
+ done
|
||||
+ if [ "$DL_OK" != "1" ]; then
|
||||
+ echo "所有 Node 下载源均失败(npmmirror + gh-proxy),中止" >&2
|
||||
+ exit 1
|
||||
+ fi
|
||||
+
|
||||
+ # 解压并加到 PATH,GITHUB_ENV 注入所有后续 step 自动继承
|
||||
+ tar -xJf "/tmp/node-dl-cache/${TAR_XZ}" -C "$INSTALL_DIR" --strip-components=1
|
||||
+ if [ -f "$GITHUB_ENV" ]; then
|
||||
+ echo "PATH=$INSTALL_DIR/bin:$PATH" >> "$GITHUB_ENV"
|
||||
+ fi
|
||||
+ export PATH="$INSTALL_DIR/bin:$PATH"
|
||||
+ hash -r 2>/dev/null || true
|
||||
+ node -v
|
||||
+ npm -v
|
||||
+ # 把 npm 全局前缀也设在这个目录下,避免权限问题
|
||||
+ npm config set prefix "$INSTALL_DIR/npm-global" 2>/dev/null || true
|
||||
|
||||
- name: 校验 Node + npm + git 可用
|
||||
run: |
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
From 285dd24463da23395304c9d31d44c6d1ecb5a64e Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Fri, 11 Sep 2026 23:01:56 +0800
|
||||
Subject: [PATCH 35/58] fix(system-settings): resolve index.post.ts L844 syntax
|
||||
error Expected ')' but found 'if' - fillDefaults arrow return/object literal
|
||||
close brackets order
|
||||
|
||||
---
|
||||
server/api/admin/system-settings/index.post.ts | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/server/api/admin/system-settings/index.post.ts b/server/api/admin/system-settings/index.post.ts
|
||||
index 9f2ac77..387aa68 100644
|
||||
--- a/server/api/admin/system-settings/index.post.ts
|
||||
+++ b/server/api/admin/system-settings/index.post.ts
|
||||
@@ -841,7 +841,7 @@ export default defineEventHandler(async (event) => {
|
||||
const settingsResult = await db.select().from(systemSettings).limit(1)
|
||||
let settings = settingsResult[0] || null
|
||||
|
||||
- const fillDefaults = (obj: any) => ({ ...obj, ...Object.fromEntries(Object.entries(DEFAULTS).filter(([k]) => !Object.prototype.hasOwnProperty.call(obj || {}, k) || (obj && obj[k] === undefined || obj[k] === null))) }
|
||||
+ const fillDefaults = (obj: any) => ({ ...obj, ...Object.fromEntries(Object.entries(DEFAULTS).filter(([k]) => !Object.prototype.hasOwnProperty.call(obj || {}, k) || (obj && obj[k] === undefined || obj[k] === null)))) })
|
||||
|
||||
if (!settings) {
|
||||
// 如果不存在,创建新设置
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
From cc142a58bc416b21b8aec3426ed29382eab6ff99 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Fri, 11 Sep 2026 23:09:29 +0800
|
||||
Subject: [PATCH 36/58] fix(system-settings): correctly balance fillDefaults
|
||||
arrow brackets L844 - 3 RPAREN after null (logic/filter/fromEntries) then
|
||||
RCURLY then outer RPAREN, IDE 0 diagnostics
|
||||
|
||||
---
|
||||
server/api/admin/system-settings/index.post.ts | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/server/api/admin/system-settings/index.post.ts b/server/api/admin/system-settings/index.post.ts
|
||||
index 387aa68..cbc2133 100644
|
||||
--- a/server/api/admin/system-settings/index.post.ts
|
||||
+++ b/server/api/admin/system-settings/index.post.ts
|
||||
@@ -841,7 +841,7 @@ export default defineEventHandler(async (event) => {
|
||||
const settingsResult = await db.select().from(systemSettings).limit(1)
|
||||
let settings = settingsResult[0] || null
|
||||
|
||||
- const fillDefaults = (obj: any) => ({ ...obj, ...Object.fromEntries(Object.entries(DEFAULTS).filter(([k]) => !Object.prototype.hasOwnProperty.call(obj || {}, k) || (obj && obj[k] === undefined || obj[k] === null)))) })
|
||||
+ const fillDefaults = (obj: any) => ({ ...obj, ...Object.fromEntries(Object.entries(DEFAULTS).filter(([k]) => !Object.prototype.hasOwnProperty.call(obj || {}, k) || (obj && obj[k] === undefined || obj[k] === null))) })
|
||||
|
||||
if (!settings) {
|
||||
// 如果不存在,创建新设置
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
From 6462b1b8a15449da515bafdee4826ee7253c4eae Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Fri, 11 Sep 2026 23:35:12 +0800
|
||||
Subject: [PATCH 37/58] fix(ci): embed python scripts as base64 to avoid YAML
|
||||
block scalar + preflight indentation conflict - resolves TASK-63 build-image
|
||||
IndentationError: unexpected indent
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 42 ++++---------------------------------
|
||||
1 file changed, 4 insertions(+), 38 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 87c6524..d0c7ad4 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -105,30 +105,10 @@ jobs:
|
||||
# 找 Node 22 最新 patch 版本号:不用 GitHub API,走 npmmirror 的 index.json 解析
|
||||
NODE_VER="${NODE_VERSION_FALLBACK}"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
- # 注意:不使用 heredoc,避免 PY_MAJOR 前导缩进 0 导致 YAML 块标量提前结束解析失败
|
||||
- PY_NODE_CODE=' import json, re, sys
|
||||
- try:
|
||||
- data = json.load(sys.stdin)
|
||||
- except Exception:
|
||||
- data = []
|
||||
- target = sys.argv[1] if len(sys.argv) > 1 else "22"
|
||||
- best_triple = None
|
||||
- best_v = ""
|
||||
- for d in data:
|
||||
- v = d.get("version", "")
|
||||
- m = re.match(r"^v(\d+)\.(\d+)\.(\d+)", v)
|
||||
- if not m: continue
|
||||
- major, minor, patch = m.groups()
|
||||
- if major != target: continue
|
||||
- triple = (int(minor), int(patch))
|
||||
- if (best_triple is None) or (triple > best_triple):
|
||||
- best_triple = triple
|
||||
- best_v = f"{major}.{minor}.{patch}"
|
||||
- if best_v:
|
||||
- print(best_v)'
|
||||
+ PY_NODE_B64="aW1wb3J0IGpzb24sIHJlLCBzeXMKdHJ5OgogICAgZGF0YSA9IGpzb24ubG9hZChzeXMuc3RkaW4pCmV4Y2VwdCBFeGNlcHRpb246CiAgICBkYXRhID0gW10KdGFyZ2V0ID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiMjIiCmJlc3RfdHJpcGxlID0gTm9uZQpiZXN0X3YgPSAiIgpmb3IgZCBpbiBkYXRhOgogICAgdiA9IGQuZ2V0KCJ2ZXJzaW9uIiwgIiIpCiAgICBtID0gcmUubWF0Y2gociJedihcZCspXC4oXGQrKVwuKFxkKykiLCB2KQogICAgaWYgbm90IG06IGNvbnRpbnVlCiAgICBtYWpvciwgbWlub3IsIHBhdGNoID0gbS5ncm91cHMoKQogICAgaWYgbWFqb3IgIT0gdGFyZ2V0OiBjb250aW51ZQogICAgdHJpcGxlID0gKGludChtaW5vciksIGludChwYXRjaCkpCiAgICBpZiAoYmVzdF90cmlwbGUgaXMgTm9uZSkgb3IgKHRyaXBsZSA+IGJlc3RfdHJpcGxlKToKICAgICAgICBiZXN0X3RyaXBsZSA9IHRyaXBsZQogICAgICAgIGJlc3RfdiA9IGYie21ham9yfS57bWlub3J9LntwYXRjaH0iCmlmIGJlc3RfdjoKICAgIHByaW50KGJlc3Rfdik="
|
||||
REMOTE_LATEST=""
|
||||
REMOTE_LATEST="$(curl -fsSL --max-time 20 "${NODE_MIRROR_PRIMARY}/index.json" 2>/dev/null \
|
||||
- | printf '%s\n' "$PY_NODE_CODE" | python3 - "${NODE_MAJOR}" || true)"
|
||||
+ | printf '%s' "$PY_NODE_B64" | base64 -d | python3 - "${NODE_MAJOR}" || true)"
|
||||
if [ -n "$REMOTE_LATEST" ]; then
|
||||
NODE_VER="$REMOTE_LATEST"
|
||||
echo "npmmirror 查得 Node 22 latest: v${NODE_VER}"
|
||||
@@ -277,22 +257,8 @@ jobs:
|
||||
if [ -f "${DOCKER_DAEMON_CONF}" ]; then
|
||||
OLD_MIRRORS_JSON="$(sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || echo "{}")"
|
||||
fi
|
||||
- PY_CODE=' import json, sys
|
||||
- try:
|
||||
- data = json.loads(sys.argv[1] or "{}")
|
||||
- except Exception:
|
||||
- data = {}
|
||||
- mirrors_envs = [m for m in sys.argv[2:5] if m]
|
||||
- existing = data.get("registry-mirrors") or []
|
||||
- if isinstance(existing, list):
|
||||
- combined = list(dict.fromkeys([*mirrors_envs, *existing]))
|
||||
- else:
|
||||
- combined = mirrors_envs
|
||||
- if combined:
|
||||
- data["registry-mirrors"] = combined
|
||||
- data.setdefault("log-driver", "json-file")
|
||||
- print(json.dumps(data, ensure_ascii=False, indent=2))'
|
||||
- NEW_DAEMON_JSON="$(printf '%s\n' "$PY_CODE" | python3 - "$OLD_MIRRORS_JSON" \
|
||||
+ PY_DAEMON_B64="aW1wb3J0IGpzb24sIHN5cwp0cnk6CiAgICBkYXRhID0ganNvbi5sb2FkcyhzeXMuYXJndlsxXSBvciAie30iKQpleGNlcHQgRXhjZXB0aW9uOgogICAgZGF0YSA9IHt9Cm1pcnJvcnNfZW52cyA9IFttIGZvciBtIGluIHN5cy5hcmd2WzI6NV0gaWYgbV0KZXhpc3RpbmcgPSBkYXRhLmdldCgicmVnaXN0cnktbWlycm9ycyIpIG9yIFtdCmlmIGlzaW5zdGFuY2UoZXhpc3RpbmcsIGxpc3QpOgogICAgY29tYmluZWQgPSBsaXN0KGRpY3QuZnJvbWtleXMoWyptaXJyb3JzX2VudnMsICpleGlzdGluZ10pKQplbHNlOgogICAgY29tYmluZWQgPSBtaXJyb3JzX2VudnMKaWYgY29tYmluZWQ6CiAgICBkYXRhWyJyZWdpc3RyeS1taXJyb3JzIl0gPSBjb21iaW5lZApkYXRhLnNldGRlZmF1bHQoImxvZy1kcml2ZXIiLCAianNvbi1maWxlIikKcHJpbnQoanNvbi5kdW1wcyhkYXRhLCBlbnN1cmVfYXNjaWk9RmFsc2UsIGluZGVudD0yKSk="
|
||||
+ NEW_DAEMON_JSON="$(printf '%s' "$PY_DAEMON_B64" | base64 -d | python3 - "$OLD_MIRRORS_JSON" \
|
||||
"${DOCKER_REGISTRY_MIRROR_1}" "${DOCKER_REGISTRY_MIRROR_2}" "${DOCKER_REGISTRY_MIRROR_3}")"
|
||||
echo "${NEW_DAEMON_JSON}" | sudo tee "${DOCKER_DAEMON_CONF}" >/dev/null
|
||||
echo "[Docker] daemon.json 已注入 registry-mirrors:"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
From 0e60f15cfe6e7a5e285fe2e2de75078f9ffef3dd Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 00:13:27 +0800
|
||||
Subject: [PATCH 38/58] feat(theme): sync defaultTheme=ClassicLight
|
||||
enabledThemes=[ClassicLight,ModernLight] across all 6 fallback locations
|
||||
|
||||
---
|
||||
app/composables/useSiteConfig.js | 4 ++--
|
||||
app/drizzle/schema.ts | 4 ++--
|
||||
nuxt.config.ts | 4 ++--
|
||||
server/api/admin/system-settings/index.post.ts | 4 ++--
|
||||
server/api/site-config.get.ts | 4 ++--
|
||||
server/utils/system-settings-defaults.ts | 4 ++--
|
||||
6 files changed, 12 insertions(+), 12 deletions(-)
|
||||
|
||||
diff --git a/app/composables/useSiteConfig.js b/app/composables/useSiteConfig.js
|
||||
index 428a851..3971ae7 100644
|
||||
--- a/app/composables/useSiteConfig.js
|
||||
+++ b/app/composables/useSiteConfig.js
|
||||
@@ -60,8 +60,8 @@ export const useSiteConfig = () => {
|
||||
enableReplayRequests: false,
|
||||
enableCollaborativeSubmission: true,
|
||||
enableSubmissionRemarks: false,
|
||||
- defaultTheme: 'System',
|
||||
- enabledThemes: '["System","ClassicDark","ClassicLight","ModernLight"]'
|
||||
+ defaultTheme: 'ClassicLight',
|
||||
+ enabledThemes: '["ClassicLight","ModernLight"]'
|
||||
}
|
||||
isLoaded.value = true
|
||||
} finally {
|
||||
diff --git a/app/drizzle/schema.ts b/app/drizzle/schema.ts
|
||||
index eb7b324..4399622 100644
|
||||
--- a/app/drizzle/schema.ts
|
||||
+++ b/app/drizzle/schema.ts
|
||||
@@ -350,8 +350,8 @@ export const systemSettings = pgTable('SystemSettings', {
|
||||
autoBackupConfig: text('autoBackupConfig'),
|
||||
statisticsCode: text('statisticsCode'),
|
||||
statisticsCodeEnabled: boolean('statisticsCodeEnabled').default(false).notNull(),
|
||||
- defaultTheme: text('defaultTheme').default('System').notNull(),
|
||||
- enabledThemes: text('enabledThemes').default('[\"System\",\"ClassicDark\",\"ClassicLight\",\"ModernLight\"]').notNull(),
|
||||
+ defaultTheme: text('defaultTheme').default('ClassicLight').notNull(),
|
||||
+ enabledThemes: text('enabledThemes').default('[\"ClassicLight\",\"ModernLight\"]').notNull(),
|
||||
enabledPlatforms: text('enabledPlatforms'),
|
||||
platformOrder: text('platformOrder'),
|
||||
});
|
||||
diff --git a/nuxt.config.ts b/nuxt.config.ts
|
||||
index c20d613..d68f3fc 100644
|
||||
--- a/nuxt.config.ts
|
||||
+++ b/nuxt.config.ts
|
||||
@@ -156,8 +156,8 @@ export default defineNuxtConfig({
|
||||
themeColor: process.env.PWA_THEME_COLOR || '#111111'
|
||||
},
|
||||
// 主题配置
|
||||
- defaultTheme: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'dark',
|
||||
- enabledThemes: (process.env.NUXT_PUBLIC_ENABLED_THEMES || 'dark,light')
|
||||
+ defaultTheme: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'ClassicLight',
|
||||
+ enabledThemes: (process.env.NUXT_PUBLIC_ENABLED_THEMES || 'ClassicLight,ModernLight')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
diff --git a/server/api/admin/system-settings/index.post.ts b/server/api/admin/system-settings/index.post.ts
|
||||
index cbc2133..1c25235 100644
|
||||
--- a/server/api/admin/system-settings/index.post.ts
|
||||
+++ b/server/api/admin/system-settings/index.post.ts
|
||||
@@ -833,8 +833,8 @@ export default defineEventHandler(async (event) => {
|
||||
autoBackupIntervalHours: 24,
|
||||
autoBackupRetentionDays: 30,
|
||||
statisticsCodeEnabled: false,
|
||||
- defaultTheme: 'System',
|
||||
- enabledThemes: '["System","ClassicDark","ClassicLight","ModernLight"]'
|
||||
+ defaultTheme: 'ClassicLight',
|
||||
+ enabledThemes: '["ClassicLight","ModernLight"]'
|
||||
} as const
|
||||
|
||||
// 获取当前设置,如果不存在则创建
|
||||
diff --git a/server/api/site-config.get.ts b/server/api/site-config.get.ts
|
||||
index d19a930..d0244a7 100644
|
||||
--- a/server/api/site-config.get.ts
|
||||
+++ b/server/api/site-config.get.ts
|
||||
@@ -87,8 +87,8 @@ export default defineEventHandler(async (event) => {
|
||||
forceBlockAllRequests: false,
|
||||
enableReplayRequests: false,
|
||||
smtpEnabled: false,
|
||||
- defaultTheme: 'System',
|
||||
- enabledThemes: '["System","ClassicDark","ClassicLight","ModernLight"]',
|
||||
+ defaultTheme: 'ClassicLight',
|
||||
+ enabledThemes: '["ClassicLight","ModernLight"]',
|
||||
scheduleDaysBeforeEnabled: false,
|
||||
scheduleDaysBefore: 30,
|
||||
scheduleDaysAfterEnabled: false,
|
||||
diff --git a/server/utils/system-settings-defaults.ts b/server/utils/system-settings-defaults.ts
|
||||
index 7946b9e..df6a405 100644
|
||||
--- a/server/utils/system-settings-defaults.ts
|
||||
+++ b/server/utils/system-settings-defaults.ts
|
||||
@@ -65,6 +65,6 @@ export const SYSTEM_SETTINGS_DEFAULTS = {
|
||||
autoBackupIntervalHours: 24,
|
||||
autoBackupRetentionDays: 30,
|
||||
statisticsCodeEnabled: false,
|
||||
- defaultTheme: 'System',
|
||||
- enabledThemes: '["System","ClassicDark","ClassicLight","ModernLight"]'
|
||||
+ defaultTheme: 'ClassicLight',
|
||||
+ enabledThemes: '["ClassicLight","ModernLight"]'
|
||||
} as const
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
From 87276f5add6ec48ca674b09030db84125c5f33cc Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 00:20:08 +0800
|
||||
Subject: [PATCH 39/58] fix(ci): bump preflight node heap from 2G to 4G -
|
||||
resolves TASK-67 nitro esbuild OOM exit 137
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index d0c7ad4..0c4ed26 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -194,7 +194,7 @@ jobs:
|
||||
- name: 执行 nuxt build
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
- NODE_OPTIONS="--max-old-space-size=2048" npm run build
|
||||
+ NODE_OPTIONS="--max-old-space-size=4096" npm run build
|
||||
echo ".output/ 生成结果:"
|
||||
ls -la .output/ 2>/dev/null || true
|
||||
du -sh .output 2>/dev/null || true
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
From e4fe591b8203f8bc279a824a5cfd408d64b3d571 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 00:40:40 +0800
|
||||
Subject: [PATCH 40/58] fix(ci): propagate deploy env vars via remote /tmp file
|
||||
+ bash -s arg + fail-fast if IMAGE_NEW/DEPLOY_PATH empty - resolves 5s
|
||||
fake-success deploys using stale :latest image
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 60 ++++++++++++++++++++++++++-----------
|
||||
1 file changed, 43 insertions(+), 17 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 0c4ed26..acfbcaa 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -536,31 +536,57 @@ jobs:
|
||||
# 本地预检查:ssh 先连上跑 whoami 确认连通,避免 20 分钟后才发现连不上
|
||||
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "whoami; docker --version; docker compose version"
|
||||
|
||||
- # 把变量作为 heredoc 脚本的输入传过去,远端展开我们需要的值
|
||||
+ # 关键修复:远端 bash 前置 ENV=val 语法在 dash/sh 远端不认,
|
||||
+ # 改为先把变量用 printf 写到远端 /tmp/deploy_env.$$ 再远端 source 它,100% 可靠
|
||||
+ ENV_SH="/tmp/deploy_env_$$"
|
||||
+ # 注意:${IMAGE_NEW} / ${DEPLOY_PATH} 等在本地 shell 展开,不是远端,所以 heredoc 用 "" 双引号
|
||||
+ ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "cat > ${ENV_SH}" <<ENV_SH_EOF
|
||||
+export IMAGE_NEW="${IMAGE_NEW}"
|
||||
+export IMAGE_LATEST="${IMAGE_LATEST}"
|
||||
+export DEPLOY_PATH="${DEPLOY_PATH}"
|
||||
+export GITEA_HOST="${GITEA_HOST}"
|
||||
+export GH_PROXY_PREFIX="${GH_PROXY_PREFIX}"
|
||||
+export REGISTRY_PULL_USER="${REGISTRY_PULL_USER}"
|
||||
+export REGISTRY_PULL_TOKEN="${REGISTRY_PULL_TOKEN}"
|
||||
+export IMAGE_NAME="${IMAGE_NAME}"
|
||||
+export NUXT_PUBLIC_SITE_URL="${NUXT_PUBLIC_SITE_URL}"
|
||||
+export GITHUB_SERVER_URL="${GITHUB_SERVER_URL}"
|
||||
+export GITHUB_REPOSITORY="${GITHUB_REPOSITORY}"
|
||||
+export GITHUB_REF_NAME="${GITHUB_REF_NAME}"
|
||||
+ENV_SH_EOF
|
||||
+ echo "[Deploy] env script written to ${ENV_SH} on remote"
|
||||
+
|
||||
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||
- IMAGE_NEW="${IMAGE_NEW}" \
|
||||
- IMAGE_LATEST="${IMAGE_LATEST}" \
|
||||
- DEPLOY_PATH="${DEPLOY_PATH}" \
|
||||
- GITEA_HOST="${GITEA_HOST}" \
|
||||
- GH_PROXY_PREFIX="${GH_PROXY_PREFIX}" \
|
||||
- REGISTRY_PULL_USER="${REGISTRY_PULL_USER}" \
|
||||
- REGISTRY_PULL_TOKEN="${REGISTRY_PULL_TOKEN}" \
|
||||
- IMAGE_NAME="${IMAGE_NAME}" \
|
||||
- NUXT_PUBLIC_SITE_URL="${NUXT_PUBLIC_SITE_URL}" \
|
||||
- GITHUB_SERVER_URL="${GITHUB_SERVER_URL}" \
|
||||
- GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" \
|
||||
- GITHUB_REF_NAME="${GITHUB_REF_NAME}" \
|
||||
- bash <<'REMOTE_SCRIPT'
|
||||
+ "chmod +x ${ENV_SH} && bash -c 'source ${ENV_SH} && rm -f ${ENV_SH}'" <<'REMOTE_SCRIPT'
|
||||
+ # 注意:上面 source 的环境变量会自动继承到后面的 REMOTE_SCRIPT bash 子进程中
|
||||
+ true
|
||||
+REMOTE_SCRIPT
|
||||
+
|
||||
+ # 再次用 bash 显式 source env,然后执行真正的 deploy 脚本(双源保证)
|
||||
+ ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s -- ${ENV_SH}" <<'REMOTE_SCRIPT_MAIN'
|
||||
set -euxo pipefail
|
||||
+ ENV_SH="$1"
|
||||
+ if [ -f "$ENV_SH" ]; then
|
||||
+ # shellcheck disable=SC1090
|
||||
+ . "$ENV_SH"
|
||||
+ rm -f "$ENV_SH"
|
||||
+ fi
|
||||
|
||||
DEPLOY_DIR="${DEPLOY_PATH}"
|
||||
COMPOSE="docker compose"
|
||||
|
||||
echo "=============================================="
|
||||
echo "Executor : $(whoami)"
|
||||
- echo "Deploy image : ${IMAGE_NEW}"
|
||||
- echo "Deploy dir : ${DEPLOY_DIR}"
|
||||
+ echo "IMAGE_NEW : ${IMAGE_NEW:-(empty!)}"
|
||||
+ echo "DEPLOY_DIR : ${DEPLOY_DIR:-(empty!)}"
|
||||
echo "=============================================="
|
||||
+ # 防御性断言:镜像名 / 路径不能为空,否则直接 exit 9 避免起旧容器
|
||||
+ if [ -z "${IMAGE_NEW}" ] || [ -z "${DEPLOY_DIR}" ]; then
|
||||
+ echo "ERROR: IMAGE_NEW or DEPLOY_DIR is empty — env variables not propagated correctly."
|
||||
+ echo "IMAGE_NEW=${IMAGE_NEW}"
|
||||
+ echo "DEPLOY_DIR=${DEPLOY_DIR}"
|
||||
+ exit 9
|
||||
+ fi
|
||||
|
||||
mkdir -p "${DEPLOY_DIR}" 2>/dev/null || sudo mkdir -p "${DEPLOY_DIR}" 2>/dev/null || true
|
||||
cd "${DEPLOY_DIR}"
|
||||
@@ -665,7 +691,7 @@ jobs:
|
||||
echo " printf 'services:\\n voicehub:\\n image: <OLD_TAG_FROM_ABOVE>\\n' > docker-compose.override.yml"
|
||||
echo " docker compose up -d"
|
||||
echo "================================================================="
|
||||
- REMOTE_SCRIPT
|
||||
+ REMOTE_SCRIPT_MAIN
|
||||
|
||||
# 清理私钥临时文件
|
||||
rm -f "${SSH_KEY_FILE}"
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
From 4d39afd23f84e151bc03488cf47661b3ba9db843 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 00:54:20 +0800
|
||||
Subject: [PATCH 41/58] =?UTF-8?q?fix(ci):=20replace=20heredoc-based=20ENV?=
|
||||
=?UTF-8?q?=5FSH=20with=20printf=20%q=20pipe=20=E2=86=92=20resolve=20yaml?=
|
||||
=?UTF-8?q?=20L544=20could=20not=20find=20expected=20':'=20colon=20false-p?=
|
||||
=?UTF-8?q?ositive=20+=20bash=20-s=20positional=20arg=20for=20env=20source?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 37 ++++++++++++-------------------------
|
||||
1 file changed, 12 insertions(+), 25 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index acfbcaa..4b4df13 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -536,34 +536,21 @@ jobs:
|
||||
# 本地预检查:ssh 先连上跑 whoami 确认连通,避免 20 分钟后才发现连不上
|
||||
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "whoami; docker --version; docker compose version"
|
||||
|
||||
- # 关键修复:远端 bash 前置 ENV=val 语法在 dash/sh 远端不认,
|
||||
- # 改为先把变量用 printf 写到远端 /tmp/deploy_env.$$ 再远端 source 它,100% 可靠
|
||||
+ # 关键修复:远端 bash 前置 ENV=val 语法在 dash/sh 远端不认,变量全空。
|
||||
+ # 用 printf + \n 拼接完整 shell 脚本通过 stdin 注入到 ssh 远端 cat,
|
||||
+ # 不用 heredoc,避免 YAML 冒号/缩进解析误判。
|
||||
ENV_SH="/tmp/deploy_env_$$"
|
||||
- # 注意:${IMAGE_NEW} / ${DEPLOY_PATH} 等在本地 shell 展开,不是远端,所以 heredoc 用 "" 双引号
|
||||
- ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "cat > ${ENV_SH}" <<ENV_SH_EOF
|
||||
-export IMAGE_NEW="${IMAGE_NEW}"
|
||||
-export IMAGE_LATEST="${IMAGE_LATEST}"
|
||||
-export DEPLOY_PATH="${DEPLOY_PATH}"
|
||||
-export GITEA_HOST="${GITEA_HOST}"
|
||||
-export GH_PROXY_PREFIX="${GH_PROXY_PREFIX}"
|
||||
-export REGISTRY_PULL_USER="${REGISTRY_PULL_USER}"
|
||||
-export REGISTRY_PULL_TOKEN="${REGISTRY_PULL_TOKEN}"
|
||||
-export IMAGE_NAME="${IMAGE_NAME}"
|
||||
-export NUXT_PUBLIC_SITE_URL="${NUXT_PUBLIC_SITE_URL}"
|
||||
-export GITHUB_SERVER_URL="${GITHUB_SERVER_URL}"
|
||||
-export GITHUB_REPOSITORY="${GITHUB_REPOSITORY}"
|
||||
-export GITHUB_REF_NAME="${GITHUB_REF_NAME}"
|
||||
-ENV_SH_EOF
|
||||
+ ENV_PAYLOAD="$(printf 'export IMAGE_NEW=%q\nexport IMAGE_LATEST=%q\nexport DEPLOY_PATH=%q\nexport GITEA_HOST=%q\nexport GH_PROXY_PREFIX=%q\nexport REGISTRY_PULL_USER=%q\nexport REGISTRY_PULL_TOKEN=%q\nexport IMAGE_NAME=%q\nexport NUXT_PUBLIC_SITE_URL=%q\nexport GITHUB_SERVER_URL=%q\nexport GITHUB_REPOSITORY=%q\nexport GITHUB_REF_NAME=%q\n' \
|
||||
+ "${IMAGE_NEW}" "${IMAGE_LATEST}" "${DEPLOY_PATH}" "${GITEA_HOST}" "${GH_PROXY_PREFIX}" \
|
||||
+ "${REGISTRY_PULL_USER}" "${REGISTRY_PULL_TOKEN}" "${IMAGE_NAME}" "${NUXT_PUBLIC_SITE_URL}" \
|
||||
+ "${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_REF_NAME}")"
|
||||
+ printf '%s' "${ENV_PAYLOAD}" | ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "cat > ${ENV_SH}"
|
||||
echo "[Deploy] env script written to ${ENV_SH} on remote"
|
||||
+ # 立即回读验证远端文件非空 & IMAGE_NEW 非空,避免后面空跑
|
||||
+ ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "set -e; ls -la ${ENV_SH}; grep -q IMAGE_NEW ${ENV_SH} && echo '[Deploy] env_sh IMAGE_NEW line OK'"
|
||||
|
||||
- ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||
- "chmod +x ${ENV_SH} && bash -c 'source ${ENV_SH} && rm -f ${ENV_SH}'" <<'REMOTE_SCRIPT'
|
||||
- # 注意:上面 source 的环境变量会自动继承到后面的 REMOTE_SCRIPT bash 子进程中
|
||||
- true
|
||||
-REMOTE_SCRIPT
|
||||
-
|
||||
- # 再次用 bash 显式 source env,然后执行真正的 deploy 脚本(双源保证)
|
||||
- ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s -- ${ENV_SH}" <<'REMOTE_SCRIPT_MAIN'
|
||||
+ # ssh 远端 bash -s 第一个参数传 $ENV_SH 路径,主脚本 source 它并做非空断言
|
||||
+ ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s -- '${ENV_SH}'" <<'REMOTE_SCRIPT_MAIN'
|
||||
set -euxo pipefail
|
||||
ENV_SH="$1"
|
||||
if [ -f "$ENV_SH" ]; then
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,738 @@
|
||||
From 153ee41ef07b84f8cfb4ff3905abc0651c3ac4ba Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 08:33:25 +0800
|
||||
Subject: [PATCH 42/58] =?UTF-8?q?feat(schedule)+fix(schedule)+feat(player)?=
|
||||
=?UTF-8?q?:=20=E6=8E=92=E6=9C=9F=E7=AE=A1=E7=90=865=E9=A1=B9=E5=8A=9F?=
|
||||
=?UTF-8?q?=E8=83=BD=20-=20=E7=B4=A7=E5=87=91=E5=B8=83=E5=B1=80/=E6=8A=95?=
|
||||
=?UTF-8?q?=E7=A8=BF=E4=BA=BA=E4=BC=98=E5=85=88=E7=AE=97=E6=B3=95/localSto?=
|
||||
=?UTF-8?q?rage=E6=8C=81=E4=B9=85=E5=8C=96/=E5=A4=87=E9=80=89=E6=B1=A0?=
|
||||
=?UTF-8?q?=E4=BF=AE=E5=A4=8D/=E6=92=AD=E6=94=BE=E9=98=9F=E5=88=97?=
|
||||
=?UTF-8?q?=E6=8C=89=E9=92=AE?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
* fix+style(schedule): ScheduleManager 标签页紧凑布局,头部 min-height 防抖,内容区统间距与移动端适配
|
||||
* feat(schedule): 新增自动排期策略「按投稿人数优先覆盖」,按 requester+collaborators 数排序分配好时段
|
||||
* feat(schedule): localStorage 键 voicehub:schedule:auto-config 持久化策略/时段/学期/过滤等7项,onMounted 恢复,watch debounce 500ms 保存
|
||||
* fix(schedule): songs/index.get.ts 新增 use_pool 查询参数,备选池过滤条件 status=PENDING + song.played=false + song未在任何排期中;ScheduleManager/draft-bulk 创建排期时将池记录 UPDATE 为 ASSIGNED
|
||||
* feat(player): ScheduleManager 工具栏新增「构建播放队列」按钮,按 playDate+playTimeId+sequence 排序后调用 useAudioPlayer.playSong 加载播放列表,支持 confirm 确认与 toast 反馈
|
||||
* refactor(useSongs): fetchSongs 增加第5参数 usePool,透传 use_pool=true 到 /api/songs
|
||||
---
|
||||
app/components/Admin/ScheduleManager.vue | 338 ++++++++++++++++---
|
||||
app/composables/useSongs.ts | 9 +-
|
||||
server/api/admin/schedule.post.ts | 23 +-
|
||||
server/api/admin/schedule/draft-bulk.post.ts | 17 +-
|
||||
server/api/songs/index.get.ts | 35 +-
|
||||
5 files changed, 364 insertions(+), 58 deletions(-)
|
||||
|
||||
diff --git a/app/components/Admin/ScheduleManager.vue b/app/components/Admin/ScheduleManager.vue
|
||||
index 2c4d780..c36f17d 100644
|
||||
--- a/app/components/Admin/ScheduleManager.vue
|
||||
+++ b/app/components/Admin/ScheduleManager.vue
|
||||
@@ -1,23 +1,35 @@
|
||||
<template>
|
||||
- <div class="space-y-6 pb-24 md:pb-8">
|
||||
- <!-- 日期选择器 -->
|
||||
- <AdminDatePicker
|
||||
- :modelValue="selectedDate"
|
||||
- @update:modelValue="handleDateSelect"
|
||||
- />
|
||||
-
|
||||
- <!-- 播出时段选择器 (如果启用) -->
|
||||
- <div
|
||||
- v-if="playTimeEnabled"
|
||||
- class="flex items-center gap-3 bg-zinc-900/30 border border-zinc-800 rounded-lg p-3"
|
||||
- >
|
||||
- <CustomSelect
|
||||
- :model-value="selectedPlayTime"
|
||||
- label="播出时段"
|
||||
- :options="playTimeOptions"
|
||||
- class-name="w-full"
|
||||
- @update:model-value="handlePlayTimeSelect"
|
||||
+ <div class="space-y-5 pb-24 md:pb-6">
|
||||
+ <!-- 日期与排期配置区 -->
|
||||
+ <div class="space-y-3">
|
||||
+ <AdminDatePicker
|
||||
+ :modelValue="selectedDate"
|
||||
+ @update:modelValue="handleDateSelect"
|
||||
/>
|
||||
+
|
||||
+ <!-- 播出时段与自动排期配置 -->
|
||||
+ <div
|
||||
+ class="grid grid-cols-1 md:grid-cols-2 gap-3 bg-zinc-900/30 border border-zinc-800 rounded-xl p-3 md:p-4"
|
||||
+ >
|
||||
+ <div v-if="playTimeEnabled">
|
||||
+ <CustomSelect
|
||||
+ :model-value="selectedPlayTime"
|
||||
+ label="播出时段"
|
||||
+ :options="playTimeOptions"
|
||||
+ class-name="w-full"
|
||||
+ @update:model-value="handlePlayTimeSelect"
|
||||
+ />
|
||||
+ </div>
|
||||
+ <div :class="playTimeEnabled ? '' : 'md:col-span-2'">
|
||||
+ <CustomSelect
|
||||
+ :model-value="autoScheduleStrategy"
|
||||
+ label="自动排期策略"
|
||||
+ :options="scheduleStrategyOptions"
|
||||
+ class-name="w-full"
|
||||
+ @update:model-value="handleStrategyChange"
|
||||
+ />
|
||||
+ </div>
|
||||
+ </div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
@@ -27,7 +39,7 @@
|
||||
|
||||
<div v-else>
|
||||
<div
|
||||
- class="lg:hidden sticky -top-4 -mx-4 -mt-4 z-20 flex p-1 bg-zinc-950/90 backdrop-blur-md border-b border-zinc-800 shadow-xl mb-4 pt-4"
|
||||
+ class="lg:hidden sticky -top-4 -mx-4 -mt-4 z-20 flex gap-2 p-2 bg-zinc-950/90 backdrop-blur-md border-b border-zinc-800 shadow-xl mb-4 pt-4 min-h-[64px]"
|
||||
>
|
||||
<button
|
||||
:class="[
|
||||
@@ -61,24 +73,24 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
- <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-0">
|
||||
+ <div class="grid grid-cols-1 lg:grid-cols-12 gap-4 md:gap-5 min-h-0">
|
||||
<!-- 左侧:待排歌曲(待排库) -->
|
||||
<div
|
||||
v-show="mobileTab === 'pending' || isDesktop"
|
||||
:class="[
|
||||
- 'lg:col-span-4 flex flex-col space-y-2',
|
||||
+ 'lg:col-span-4 flex flex-col gap-2 md:gap-3',
|
||||
mobileTab === 'scheduled' ? 'hidden lg:flex' : 'flex'
|
||||
]"
|
||||
@dragover.prevent="handleDraggableDragOver"
|
||||
@dragleave="handleDraggableDragLeave"
|
||||
@drop.stop.prevent="handleReturnToDraggable"
|
||||
>
|
||||
- <div class="flex items-center justify-between px-1">
|
||||
+ <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 md:gap-3 px-1 min-h-[52px]">
|
||||
<h3 class="hidden lg:block text-lg font-black tracking-tight text-zinc-100 uppercase">
|
||||
待排歌曲
|
||||
</h3>
|
||||
<div
|
||||
- class="flex w-full lg:w-auto gap-1 p-1 bg-zinc-900/50 rounded-xl border border-zinc-800"
|
||||
+ class="flex w-full lg:w-auto gap-2 p-1.5 bg-zinc-900/50 rounded-xl border border-zinc-800"
|
||||
>
|
||||
<button
|
||||
v-for="tab in [
|
||||
@@ -88,7 +100,7 @@
|
||||
]"
|
||||
:key="tab.id"
|
||||
:class="[
|
||||
- 'flex-1 lg:flex-none px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all',
|
||||
+ 'flex-1 lg:flex-none px-3 md:px-4 py-2 rounded-lg text-[10px] md:text-xs font-black uppercase tracking-widest transition-all',
|
||||
activeTab === tab.id
|
||||
? 'bg-zinc-800 text-blue-400 shadow-md border border-blue-500/20'
|
||||
: 'text-zinc-600 hover:text-zinc-400'
|
||||
@@ -103,7 +115,7 @@
|
||||
<!-- 筛选区 - 移动端折叠 -->
|
||||
<div class="bg-zinc-900/40 border border-zinc-800 rounded-2xl shadow-xl">
|
||||
<div
|
||||
- class="p-4 flex items-center justify-between lg:hidden border-b border-zinc-800/50 rounded-t-2xl"
|
||||
+ class="p-3 md:p-4 flex items-center justify-between lg:hidden border-b border-zinc-800/50 rounded-t-2xl"
|
||||
@click="mobileFiltersOpen = !mobileFiltersOpen"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-zinc-400">
|
||||
@@ -120,7 +132,7 @@
|
||||
|
||||
<div
|
||||
v-show="mobileFiltersOpen || isDesktop"
|
||||
- class="p-3 space-y-2 transition-all duration-300 ease-in-out rounded-b-2xl"
|
||||
+ class="p-3 md:p-4 space-y-2 md:space-y-3 transition-all duration-300 ease-in-out rounded-b-2xl"
|
||||
>
|
||||
<div class="relative">
|
||||
<Search
|
||||
@@ -323,18 +335,18 @@
|
||||
<div
|
||||
v-show="mobileTab === 'scheduled' || isDesktop"
|
||||
:class="[
|
||||
- 'lg:col-span-8 flex flex-col space-y-4',
|
||||
+ 'lg:col-span-8 flex flex-col gap-3 md:gap-4',
|
||||
mobileTab === 'pending' ? 'hidden lg:flex' : 'flex'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
- class="hidden lg:flex flex-col xl:flex-row xl:items-center justify-between gap-4 px-1"
|
||||
+ class="hidden lg:flex flex-col xl:flex-row xl:items-center justify-between gap-3 md:gap-4 px-1 min-h-[56px]"
|
||||
>
|
||||
<h3 class="text-lg font-black tracking-tight text-zinc-100 uppercase">播放顺序</h3>
|
||||
<div
|
||||
- class="flex flex-wrap items-center gap-2 p-1.5 bg-zinc-900/50 border border-zinc-800/50 rounded-2xl"
|
||||
+ class="flex flex-wrap items-center gap-1.5 md:gap-2 p-1.5 bg-zinc-900/50 border border-zinc-800/50 rounded-2xl"
|
||||
>
|
||||
- <div class="flex gap-1">
|
||||
+ <div class="flex gap-1 md:gap-1.5">
|
||||
<button
|
||||
:disabled="
|
||||
!hasChanges && localScheduledSongs.length === 0 && !hasUnpublishedDrafts
|
||||
@@ -381,6 +393,18 @@
|
||||
>全部已播放</span
|
||||
>
|
||||
</button>
|
||||
+ <button
|
||||
+ :disabled="localScheduledSongs.length === 0"
|
||||
+ class="p-2 bg-zinc-950 border border-zinc-800 hover:bg-zinc-800 text-zinc-500 hover:text-cyan-400 rounded-xl transition-all group relative disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
+ title="构建播放队列"
|
||||
+ @click="confirmBuildPlayQueue"
|
||||
+ >
|
||||
+ <Music2 class="w-3.5 h-3.5" />
|
||||
+ <span
|
||||
+ class="absolute -top-10 left-1/2 -translate-x-1/2 px-2 py-1 bg-zinc-800 text-[9px] text-zinc-300 rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap border border-zinc-700"
|
||||
+ >构建播放队列</span
|
||||
+ >
|
||||
+ </button>
|
||||
<button
|
||||
class="p-2 bg-zinc-950 border border-zinc-800 hover:bg-zinc-800 text-zinc-500 hover:text-purple-400 rounded-xl transition-all group relative"
|
||||
@click="openMoveDateDialog"
|
||||
@@ -394,7 +418,7 @@
|
||||
</div>
|
||||
<div class="h-6 w-[1px] bg-zinc-800 mx-1" />
|
||||
<button
|
||||
- class="flex items-center gap-1.5 px-3 py-2 bg-yellow-500/10 hover:bg-yellow-500/20 text-yellow-500 text-[10px] font-black rounded-xl border border-yellow-500/20 transition-all uppercase tracking-widest active:scale-95 disabled:opacity-50"
|
||||
+ class="flex items-center gap-1.5 px-3 md:px-4 py-2 bg-yellow-500/10 hover:bg-yellow-500/20 text-yellow-500 text-[10px] md:text-xs font-black rounded-xl border border-yellow-500/20 transition-all uppercase tracking-widest active:scale-95 disabled:opacity-50"
|
||||
title="一键将当前待排歌曲自动排入"
|
||||
@click="autoScheduleExpected"
|
||||
>
|
||||
@@ -402,14 +426,14 @@
|
||||
</button>
|
||||
<button
|
||||
:disabled="!canPublish"
|
||||
- class="flex items-center gap-2 px-4 py-2 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-500 text-[10px] font-black rounded-xl border border-emerald-500/20 transition-all uppercase tracking-widest active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
+ class="flex items-center gap-2 px-4 py-2 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-500 text-[10px] md:text-xs font-black rounded-xl border border-emerald-500/20 transition-all uppercase tracking-widest active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
@click="publishSchedule"
|
||||
>
|
||||
<Send class="w-3 h-3" /> 发布排期
|
||||
</button>
|
||||
<button
|
||||
:disabled="!hasChanges"
|
||||
- class="flex items-center gap-2 px-5 py-2 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black rounded-xl shadow-lg shadow-blue-900/20 transition-all uppercase tracking-widest active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
+ class="flex items-center gap-2 px-5 py-2 bg-blue-600 hover:bg-blue-500 text-white text-[10px] md:text-xs font-black rounded-xl shadow-lg shadow-blue-900/20 transition-all uppercase tracking-widest active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
@click="saveSequence"
|
||||
>
|
||||
<FileBadge class="w-3.5 h-3.5" /> 保存并发布
|
||||
@@ -838,22 +862,56 @@ import Pagination from '~/components/UI/Common/Pagination.vue'
|
||||
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
|
||||
import LoadingState from '~/components/UI/Common/LoadingState.vue'
|
||||
import { useSongPlayer } from '~/composables/useSongPlayer'
|
||||
+import { useAudioPlayer, type PlayableSong } from '~/composables/useAudioPlayer'
|
||||
+import { useToast } from '~/composables/useToast'
|
||||
import { convertToHttps } from '~/utils/url'
|
||||
|
||||
+const STORAGE_KEY = 'voicehub:schedule:auto-config'
|
||||
+
|
||||
+type AutoScheduleStrategy = 'default' | 'contributor-count'
|
||||
+
|
||||
+interface AutoConfig {
|
||||
+ strategy: AutoScheduleStrategy
|
||||
+ selectedPlayTime: string
|
||||
+ selectedGrade: string
|
||||
+ songSortOption: string
|
||||
+ activeTab: string
|
||||
+ searchQuery: string
|
||||
+ selectedSemester: string
|
||||
+}
|
||||
+
|
||||
+const defaultAutoConfig: AutoConfig = {
|
||||
+ strategy: 'default',
|
||||
+ selectedPlayTime: '',
|
||||
+ selectedGrade: '全部',
|
||||
+ songSortOption: 'votes-desc',
|
||||
+ activeTab: 'normal',
|
||||
+ searchQuery: '',
|
||||
+ selectedSemester: ''
|
||||
+}
|
||||
+
|
||||
// 响应式数据
|
||||
const showCardGenerator = ref(false)
|
||||
const selectedDate = ref(new Date().toISOString().split('T')[0])
|
||||
const loading = ref(false)
|
||||
-const songSortOption = ref('votes-desc')
|
||||
+const songSortOption = ref<AutoConfig['songSortOption']>('votes-desc')
|
||||
const hasChanges = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedGrade = ref('全部')
|
||||
-const activeTab = ref('normal')
|
||||
+const activeTab = ref<AutoConfig['activeTab']>('normal')
|
||||
const mobileTab = ref('pending')
|
||||
const mobileFiltersOpen = ref(false)
|
||||
|
||||
-// 音频播放器
|
||||
+const autoScheduleStrategy = ref<AutoScheduleStrategy>('default')
|
||||
+const scheduleStrategyOptions = [
|
||||
+ { label: '默认策略(按期望序号)', value: 'default' },
|
||||
+ { label: '按投稿人数优先覆盖', value: 'contributor-count' }
|
||||
+]
|
||||
+
|
||||
+// 音频播放器与 Toast
|
||||
const { playSong } = useSongPlayer()
|
||||
+const audioPlayer = useAudioPlayer()
|
||||
+const { showToast } = useToast()
|
||||
|
||||
// 确认对话框相关
|
||||
const showConfirmDialog = ref(false)
|
||||
@@ -983,6 +1041,62 @@ const sortOptions = [
|
||||
{ label: '热度最低', value: 'votes-asc' }
|
||||
]
|
||||
|
||||
+// 策略变更处理
|
||||
+const handleStrategyChange = (value: string) => {
|
||||
+ autoScheduleStrategy.value = value as AutoScheduleStrategy
|
||||
+}
|
||||
+
|
||||
+// 本地存储工具
|
||||
+const loadAutoConfig = (): AutoConfig => {
|
||||
+ if (typeof window === 'undefined') return { ...defaultAutoConfig }
|
||||
+ try {
|
||||
+ const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||
+ if (!raw) return { ...defaultAutoConfig }
|
||||
+ const parsed = JSON.parse(raw) as Partial<AutoConfig>
|
||||
+ return { ...defaultAutoConfig, ...parsed }
|
||||
+ } catch (err) {
|
||||
+ console.warn('[ScheduleConfig] 解析本地配置失败,使用默认值', err)
|
||||
+ return { ...defaultAutoConfig }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+const saveAutoConfig = () => {
|
||||
+ if (typeof window === 'undefined') return
|
||||
+ try {
|
||||
+ const config: AutoConfig = {
|
||||
+ strategy: autoScheduleStrategy.value,
|
||||
+ selectedPlayTime: selectedPlayTime.value,
|
||||
+ selectedGrade: selectedGrade.value,
|
||||
+ songSortOption: songSortOption.value,
|
||||
+ activeTab: activeTab.value,
|
||||
+ searchQuery: searchQuery.value,
|
||||
+ selectedSemester: selectedSemester.value
|
||||
+ }
|
||||
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
|
||||
+ } catch (err) {
|
||||
+ console.warn('[ScheduleConfig] 保存本地配置失败', err)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// Debounce 工具
|
||||
+let configSaveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
+const debounceSaveAutoConfig = () => {
|
||||
+ if (configSaveTimer) clearTimeout(configSaveTimer)
|
||||
+ configSaveTimer = setTimeout(() => {
|
||||
+ saveAutoConfig()
|
||||
+ configSaveTimer = null
|
||||
+ }, 500)
|
||||
+}
|
||||
+
|
||||
+// 投稿人联合数计算(requesterId + collaborators ACCEPTED)
|
||||
+const getSongContributorCount = (song: any): number => {
|
||||
+ let count = 1
|
||||
+ if (song.collaborators && Array.isArray(song.collaborators)) {
|
||||
+ count += song.collaborators.length
|
||||
+ }
|
||||
+ return count
|
||||
+}
|
||||
+
|
||||
// 学期相关
|
||||
const availableSemesters = ref([])
|
||||
const selectedSemester = ref('')
|
||||
@@ -1218,22 +1332,33 @@ onMounted(async () => {
|
||||
auth = useAuth()
|
||||
semesterService = useSemesters()
|
||||
|
||||
- // 初始化窗口大小检测
|
||||
checkWindowSize()
|
||||
window.addEventListener('resize', checkWindowSize)
|
||||
|
||||
- // 先加载学期数据,然后加载其他数据
|
||||
+ const savedConfig = loadAutoConfig()
|
||||
+ autoScheduleStrategy.value = savedConfig.strategy
|
||||
+ selectedPlayTime.value = savedConfig.selectedPlayTime
|
||||
+ selectedGrade.value = savedConfig.selectedGrade
|
||||
+ songSortOption.value = savedConfig.songSortOption
|
||||
+ activeTab.value = savedConfig.activeTab
|
||||
+ searchQuery.value = savedConfig.searchQuery
|
||||
+ if (savedConfig.selectedSemester) {
|
||||
+ selectedSemester.value = savedConfig.selectedSemester
|
||||
+ }
|
||||
+
|
||||
await loadSemesters()
|
||||
+ if (savedConfig.selectedSemester && !selectedSemester.value) {
|
||||
+ selectedSemester.value = savedConfig.selectedSemester
|
||||
+ }
|
||||
await loadData()
|
||||
|
||||
refreshInterval = setInterval(() => {
|
||||
if (!hasChanges.value && !loading.value) {
|
||||
- loadData(true) // silent load if possible, but loadData might set loading=true
|
||||
+ loadData(true)
|
||||
}
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
-// 清理事件监听器
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
|
||||
@@ -1249,8 +1374,27 @@ onUnmounted(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval)
|
||||
}
|
||||
+ if (configSaveTimer) {
|
||||
+ clearTimeout(configSaveTimer)
|
||||
+ configSaveTimer = null
|
||||
+ }
|
||||
})
|
||||
|
||||
+watch(
|
||||
+ [
|
||||
+ autoScheduleStrategy,
|
||||
+ selectedPlayTime,
|
||||
+ selectedGrade,
|
||||
+ songSortOption,
|
||||
+ activeTab,
|
||||
+ searchQuery,
|
||||
+ selectedSemester
|
||||
+ ],
|
||||
+ () => {
|
||||
+ debounceSaveAutoConfig()
|
||||
+ }
|
||||
+)
|
||||
+
|
||||
// 监听日期变化
|
||||
watch(selectedDate, async () => {
|
||||
await loadData()
|
||||
@@ -1811,14 +1955,21 @@ const autoScheduleExpected = () => {
|
||||
const expectedSongs = [...filteredUnscheduledSongs.value]
|
||||
|
||||
if (expectedSongs.length === 0) {
|
||||
- if (window.$showNotification) {
|
||||
- window.$showNotification('当前没有待排期的歌曲', 'info')
|
||||
- }
|
||||
+ showToast('当前没有待排期的歌曲', 'info')
|
||||
return
|
||||
}
|
||||
|
||||
- // 按照期望序号排序,如果有的话
|
||||
- expectedSongs.sort((a, b) => (a.expectedPlaySequence || 9999) - (b.expectedPlaySequence || 9999))
|
||||
+ if (autoScheduleStrategy.value === 'contributor-count') {
|
||||
+ expectedSongs.sort((a, b) => {
|
||||
+ const diffA = getSongContributorCount(b) - getSongContributorCount(a)
|
||||
+ if (diffA !== 0) return diffA
|
||||
+ const seqDiff = (a.expectedPlaySequence || 9999) - (b.expectedPlaySequence || 9999)
|
||||
+ if (seqDiff !== 0) return seqDiff
|
||||
+ return (b.voteCount || 0) - (a.voteCount || 0)
|
||||
+ })
|
||||
+ } else {
|
||||
+ expectedSongs.sort((a, b) => (a.expectedPlaySequence || 9999) - (b.expectedPlaySequence || 9999))
|
||||
+ }
|
||||
|
||||
const baseId = Date.now()
|
||||
expectedSongs.forEach((song, idx) => {
|
||||
@@ -1840,14 +1991,90 @@ const autoScheduleExpected = () => {
|
||||
count++
|
||||
})
|
||||
|
||||
- // 重新生成 sequence
|
||||
localScheduledSongs.value.forEach((item, idx) => {
|
||||
item.sequence = idx + 1
|
||||
})
|
||||
|
||||
hasChanges.value = true
|
||||
- if (window.$showNotification) {
|
||||
- window.$showNotification(`已自动排入 ${count} 首待排歌曲,请注意调整顺序后保存`, 'success')
|
||||
+ const strategyLabel =
|
||||
+ autoScheduleStrategy.value === 'contributor-count' ? '(按投稿人数优先)' : ''
|
||||
+ showToast(`已自动排入 ${count} 首待排歌曲${strategyLabel},请注意调整顺序后保存`, 'success')
|
||||
+}
|
||||
+
|
||||
+// 构建播放队列
|
||||
+const confirmBuildPlayQueue = () => {
|
||||
+ if (localScheduledSongs.value.length === 0) {
|
||||
+ showToast('当前播放列表为空,无法构建播放队列', 'warning')
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ confirmDialogTitle.value = '构建播放队列'
|
||||
+ confirmDialogMessage.value = `确定要将当前播放列表中 ${localScheduledSongs.value.length} 首歌曲按顺序加载到播放器吗?这将覆盖现有播放队列。`
|
||||
+ confirmDialogType.value = 'info'
|
||||
+ confirmDialogConfirmText.value = '确认构建'
|
||||
+ confirmAction.value = async () => {
|
||||
+ await buildPlayQueue()
|
||||
+ }
|
||||
+ showConfirmDialog.value = true
|
||||
+}
|
||||
+
|
||||
+const buildPlayQueue = async () => {
|
||||
+ try {
|
||||
+ loading.value = true
|
||||
+
|
||||
+ const sorted = [...localScheduledSongs.value].sort((a, b) => {
|
||||
+ const dateA = new Date(a.playDate || 0).getTime()
|
||||
+ const dateB = new Date(b.playDate || 0).getTime()
|
||||
+ if (dateA !== dateB) return dateA - dateB
|
||||
+ const ptA = a.playTimeId || 0
|
||||
+ const ptB = b.playTimeId || 0
|
||||
+ if (ptA !== ptB) return ptA - ptB
|
||||
+ return (a.sequence || 0) - (b.sequence || 0)
|
||||
+ })
|
||||
+
|
||||
+ const playlist: PlayableSong[] = sorted
|
||||
+ .map((s) => s.song)
|
||||
+ .filter((song) => song && song.id)
|
||||
+ .map((song) => ({
|
||||
+ id: song.id,
|
||||
+ title: song.title,
|
||||
+ artist: song.artist,
|
||||
+ cover: song.cover || null,
|
||||
+ musicPlatform: song.musicPlatform || null,
|
||||
+ musicId: song.musicId || null,
|
||||
+ playUrl: song.playUrl || null,
|
||||
+ musicUrl: null
|
||||
+ }))
|
||||
+
|
||||
+ if (playlist.length === 0) {
|
||||
+ showToast('没有可用的歌曲信息', 'warning')
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ if (playlist[0].musicUrl) {
|
||||
+ audioPlayer.playSong(playlist[0], playlist, 0)
|
||||
+ } else {
|
||||
+ try {
|
||||
+ const { getMusicUrl } = await import('~/utils/musicUrl')
|
||||
+ const first = playlist[0]
|
||||
+ if (first.playUrl && first.playUrl.trim()) {
|
||||
+ first.musicUrl = first.playUrl.trim()
|
||||
+ } else if (first.musicPlatform && first.musicId) {
|
||||
+ const url = await getMusicUrl(first.musicPlatform, first.musicId, first.playUrl)
|
||||
+ first.musicUrl = url || null
|
||||
+ }
|
||||
+ audioPlayer.playSong(first, playlist, 0)
|
||||
+ } catch (e) {
|
||||
+ audioPlayer.playSong(playlist[0], playlist, 0)
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ showToast(`已成功构建播放队列,共 ${playlist.length} 首歌曲`, 'success')
|
||||
+ } catch (err) {
|
||||
+ console.error('构建播放队列失败:', err)
|
||||
+ showToast('构建播放队列失败: ' + (err as Error).message, 'error')
|
||||
+ } finally {
|
||||
+ loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2461,7 +2688,6 @@ const handleTouchReturnToDraggable = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
-/* 隐藏滚动条但保留功能 */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -2470,7 +2696,6 @@ const handleTouchReturnToDraggable = async () => {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
-/* 列表过渡动画 */
|
||||
.schedule-list-move,
|
||||
.schedule-list-enter-active,
|
||||
.schedule-list-leave-active {
|
||||
@@ -2487,4 +2712,17 @@ const handleTouchReturnToDraggable = async () => {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
+
|
||||
+@media (max-width: 640px) {
|
||||
+ :deep(.tab-panel-content) {
|
||||
+ padding: 12px !important;
|
||||
+ gap: 10px !important;
|
||||
+ }
|
||||
+ :deep(.section-padding) {
|
||||
+ padding: 12px !important;
|
||||
+ }
|
||||
+ :deep(.toolbar-gap) {
|
||||
+ gap: 6px !important;
|
||||
+ }
|
||||
+}
|
||||
</style>
|
||||
diff --git a/app/composables/useSongs.ts b/app/composables/useSongs.ts
|
||||
index a0ed42c..9c3f1cd 100644
|
||||
--- a/app/composables/useSongs.ts
|
||||
+++ b/app/composables/useSongs.ts
|
||||
@@ -75,7 +75,8 @@ export const useSongs = () => {
|
||||
silent = false,
|
||||
semester?: string,
|
||||
forceRefresh = false,
|
||||
- bypassCache = false
|
||||
+ bypassCache = false,
|
||||
+ usePool = false
|
||||
) => {
|
||||
if (!silent) {
|
||||
loading.value = true
|
||||
@@ -88,21 +89,21 @@ export const useSongs = () => {
|
||||
const response = await dedup.dedupedRequest(
|
||||
'songs',
|
||||
async () => {
|
||||
- // 构建URL参数
|
||||
const params = new URLSearchParams()
|
||||
if (semester) {
|
||||
params.append('semester', semester)
|
||||
}
|
||||
- // 只有当 bypassCache 为 true 时才添加 bypass_cache 参数
|
||||
if (bypassCache) {
|
||||
params.append('bypass_cache', 'true')
|
||||
}
|
||||
+ if (usePool) {
|
||||
+ params.append('use_pool', 'true')
|
||||
+ }
|
||||
if (forceRefresh) {
|
||||
params.append('t', Date.now().toString())
|
||||
}
|
||||
const url = `/api/songs${params.toString() ? '?' + params.toString() : ''}`
|
||||
|
||||
- // API请求
|
||||
return await $fetch(url, {
|
||||
...getAuthConfig()
|
||||
})
|
||||
diff --git a/server/api/admin/schedule.post.ts b/server/api/admin/schedule.post.ts
|
||||
index 16d0e40..44f6905 100644
|
||||
--- a/server/api/admin/schedule.post.ts
|
||||
+++ b/server/api/admin/schedule.post.ts
|
||||
@@ -1,5 +1,13 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
-import { playTimes, schedules, songs, users, votes, songReplayRequests } from '~/drizzle/schema'
|
||||
+import {
|
||||
+ playTimes,
|
||||
+ schedules,
|
||||
+ songs,
|
||||
+ users,
|
||||
+ votes,
|
||||
+ songReplayRequests,
|
||||
+ scheduleSongPool
|
||||
+} from '~/drizzle/schema'
|
||||
import { and, asc, count, desc, eq, gte, lte } from 'drizzle-orm'
|
||||
import { createSongSelectedNotification } from '../../services/notificationService'
|
||||
import { cacheService } from '~~/server/services/cacheService'
|
||||
@@ -126,6 +134,19 @@ export default defineEventHandler(async (event) => {
|
||||
// 获取客户端IP地址
|
||||
const clientIP = getClientIP(event)
|
||||
|
||||
+ await tx
|
||||
+ .update(scheduleSongPool)
|
||||
+ .set({
|
||||
+ status: 'ASSIGNED',
|
||||
+ targetPlayDate: playDate
|
||||
+ })
|
||||
+ .where(
|
||||
+ and(
|
||||
+ eq(scheduleSongPool.songId, body.songId),
|
||||
+ eq(scheduleSongPool.status, 'PENDING')
|
||||
+ )
|
||||
+ )
|
||||
+
|
||||
// 只有在非草稿模式下才创建通知
|
||||
if (!isDraft) {
|
||||
await createSongSelectedNotification(
|
||||
diff --git a/server/api/admin/schedule/draft-bulk.post.ts b/server/api/admin/schedule/draft-bulk.post.ts
|
||||
index fb63559..980fa22 100644
|
||||
--- a/server/api/admin/schedule/draft-bulk.post.ts
|
||||
+++ b/server/api/admin/schedule/draft-bulk.post.ts
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
-import { schedules, songReplayRequests } from '~/drizzle/schema'
|
||||
+import { schedules, songReplayRequests, scheduleSongPool } from '~/drizzle/schema'
|
||||
import { and, eq, gte, inArray, lte } from 'drizzle-orm'
|
||||
import { cacheService } from '~~/server/services/cacheService'
|
||||
|
||||
@@ -80,6 +80,21 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const inserted = await tx.insert(schedules).values(rows).returning({ id: schedules.id })
|
||||
|
||||
+ if (songIds.length > 0) {
|
||||
+ await tx
|
||||
+ .update(scheduleSongPool)
|
||||
+ .set({
|
||||
+ status: 'ASSIGNED',
|
||||
+ targetPlayDate: startOfDay
|
||||
+ })
|
||||
+ .where(
|
||||
+ and(
|
||||
+ inArray(scheduleSongPool.songId, songIds),
|
||||
+ eq(scheduleSongPool.status, 'PENDING')
|
||||
+ )
|
||||
+ )
|
||||
+ }
|
||||
+
|
||||
return { deletedCount: existingSchedules.length, insertedCount: inserted.length }
|
||||
})
|
||||
|
||||
diff --git a/server/api/songs/index.get.ts b/server/api/songs/index.get.ts
|
||||
index cb817f6..a8274b4 100644
|
||||
--- a/server/api/songs/index.get.ts
|
||||
+++ b/server/api/songs/index.get.ts
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
songs,
|
||||
systemSettings,
|
||||
users,
|
||||
- votes
|
||||
+ votes,
|
||||
+ scheduleSongPool
|
||||
} from '~/drizzle/schema'
|
||||
-import { and, asc, count, desc, eq, inArray, like, or } from 'drizzle-orm'
|
||||
+import { and, asc, count, desc, eq, inArray, like, or, isNull, notInArray } from 'drizzle-orm'
|
||||
import { cacheService } from '~~/server/services/cacheService'
|
||||
import { formatDateTime } from '~/utils/timeUtils'
|
||||
import { maskSongsInfo, MaskableSong, MaskableUser } from '~~/server/utils/studentMask'
|
||||
@@ -67,6 +68,7 @@ export default defineEventHandler(async (event) => {
|
||||
const sortBy = (query.sortBy as string) || 'createdAt'
|
||||
const sortOrder = (query.sortOrder as string) || 'desc'
|
||||
const bypassCache = query.bypass_cache === 'true'
|
||||
+ const usePool = query.use_pool === 'true' || query.usePool === 'true'
|
||||
|
||||
// 获取用户身份
|
||||
const user = event.context.user || null
|
||||
@@ -203,6 +205,35 @@ export default defineEventHandler(async (event) => {
|
||||
conditions.push(eq(songs.requesterId, user.id))
|
||||
}
|
||||
|
||||
+ if (usePool) {
|
||||
+ const poolRows = await db
|
||||
+ .select({ songId: scheduleSongPool.songId })
|
||||
+ .from(scheduleSongPool)
|
||||
+ .where(
|
||||
+ and(
|
||||
+ eq(scheduleSongPool.status, 'PENDING'),
|
||||
+ isNull(scheduleSongPool.targetPlayDate)
|
||||
+ )
|
||||
+ )
|
||||
+ .groupBy(scheduleSongPool.songId)
|
||||
+
|
||||
+ const allScheduledSongIdsQuery = await db
|
||||
+ .select({ songId: schedules.songId })
|
||||
+ .from(schedules)
|
||||
+
|
||||
+ const scheduledIdsSet = new Set(allScheduledSongIdsQuery.map((r) => r.songId))
|
||||
+ const candidateFromPool = new Set(
|
||||
+ poolRows.map((r) => r.songId).filter((id) => !scheduledIdsSet.has(id))
|
||||
+ )
|
||||
+
|
||||
+ if (candidateFromPool.size === 0) {
|
||||
+ conditions.push(eq(songs.id, -1))
|
||||
+ } else {
|
||||
+ conditions.push(inArray(songs.id, Array.from(candidateFromPool)))
|
||||
+ }
|
||||
+ conditions.push(eq(songs.played, false))
|
||||
+ }
|
||||
+
|
||||
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
|
||||
|
||||
// 查询歌曲总数
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
From ca190a445a9c9cb89407befe26ec67e02201a5ac Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 08:55:29 +0800
|
||||
Subject: [PATCH 45/58] =?UTF-8?q?fix(vue-parser):=20BlacklistManager=20?=
|
||||
=?UTF-8?q?=E4=B8=8E=20ScheduleManager=20=E8=A1=A5=20lang=3Dts=20=E8=A7=A3?=
|
||||
=?UTF-8?q?=E5=86=B3=20CI=20Babel=20=E8=A7=A3=E6=9E=90=20SyntaxError?=
|
||||
=?UTF-8?q?=EF=BC=88Unexpected=20token,=20expected=20,=EF=BC=89?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
app/components/Admin/BlacklistManager.vue | 2 +-
|
||||
app/components/Admin/ScheduleManager.vue | 2 +-
|
||||
2 files changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/app/components/Admin/BlacklistManager.vue b/app/components/Admin/BlacklistManager.vue
|
||||
index 1e3196d..a31371f 100644
|
||||
--- a/app/components/Admin/BlacklistManager.vue
|
||||
+++ b/app/components/Admin/BlacklistManager.vue
|
||||
@@ -335,7 +335,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
-<script setup>
|
||||
+<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, computed } from 'vue'
|
||||
import {
|
||||
ShieldAlert,
|
||||
diff --git a/app/components/Admin/ScheduleManager.vue b/app/components/Admin/ScheduleManager.vue
|
||||
index c36f17d..b293fbc 100644
|
||||
--- a/app/components/Admin/ScheduleManager.vue
|
||||
+++ b/app/components/Admin/ScheduleManager.vue
|
||||
@@ -819,7 +819,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
-<script setup>
|
||||
+<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch, inject } from 'vue'
|
||||
import {
|
||||
Search,
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
From 0c71328efd09b45445461949a7930dbcb9913962 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 10:27:07 +0800
|
||||
Subject: [PATCH 46/58] =?UTF-8?q?fix(nitro-import):=20permissions/user-ava?=
|
||||
=?UTF-8?q?tar=20=E5=BC=95=E7=94=A8=E8=B7=AF=E5=BE=84=E7=BB=9F=E4=B8=80?=
|
||||
=?UTF-8?q?=E6=94=B9=E4=B8=BA=20~~/server/utils=20=E5=88=AB=E5=90=8D?=
|
||||
=?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=20Rollup=20UNRESOLVED=5FIMPORT?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
server/api/admin/roles/index.get.ts | 2 +-
|
||||
server/api/admin/roles/index.put.ts | 2 +-
|
||||
server/api/admin/stats/active-users.get.ts | 4 ++--
|
||||
server/api/admin/stats/realtime.get.ts | 4 ++--
|
||||
server/api/admin/users/[id].put.ts | 2 +-
|
||||
server/api/admin/users/[id]/reset-password.post.ts | 2 +-
|
||||
server/api/admin/users/index.get.ts | 2 +-
|
||||
server/api/admin/users/index.post.ts | 2 +-
|
||||
server/api/roles.get.ts | 2 +-
|
||||
9 files changed, 11 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/server/api/admin/roles/index.get.ts b/server/api/admin/roles/index.get.ts
|
||||
index b11097b..f7d2c68 100644
|
||||
--- a/server/api/admin/roles/index.get.ts
|
||||
+++ b/server/api/admin/roles/index.get.ts
|
||||
@@ -1,4 +1,4 @@
|
||||
-import { ROLES } from '../../../utils/permissions.js'
|
||||
+import { ROLES } from '~~/server/utils/permissions'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const user = event.context.user
|
||||
diff --git a/server/api/admin/roles/index.put.ts b/server/api/admin/roles/index.put.ts
|
||||
index b3b4920..c2f8007 100644
|
||||
--- a/server/api/admin/roles/index.put.ts
|
||||
+++ b/server/api/admin/roles/index.put.ts
|
||||
@@ -1,4 +1,4 @@
|
||||
-import { ROLES, saveRolesConfig } from '../../../utils/permissions.js'
|
||||
+import { ROLES, saveRolesConfig } from '~~/server/utils/permissions'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = event.context.user
|
||||
diff --git a/server/api/admin/stats/active-users.get.ts b/server/api/admin/stats/active-users.get.ts
|
||||
index 34756ca..e5fb760 100644
|
||||
--- a/server/api/admin/stats/active-users.get.ts
|
||||
+++ b/server/api/admin/stats/active-users.get.ts
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { songs, users, votes, userIdentities } from '~/drizzle/schema'
|
||||
-import { CacheService } from '../../../services/cacheService'
|
||||
-import { getUserAvatarUrl } from '../../../utils/user-avatar'
|
||||
+import { CacheService } from '~~/server/services/cacheService'
|
||||
+import { getUserAvatarUrl } from '~~/server/utils/user-avatar'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = event.context.user
|
||||
diff --git a/server/api/admin/stats/realtime.get.ts b/server/api/admin/stats/realtime.get.ts
|
||||
index 86ed510..7b4f449 100644
|
||||
--- a/server/api/admin/stats/realtime.get.ts
|
||||
+++ b/server/api/admin/stats/realtime.get.ts
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createError, defineEventHandler } from 'h3'
|
||||
import { db } from '~/drizzle/db'
|
||||
-import { CacheService } from '../../../services/cacheService'
|
||||
+import { CacheService } from '~~/server/services/cacheService'
|
||||
import { songs, users, votes, userIdentities } from '~/drizzle/schema'
|
||||
import { count, eq, gte, inArray } from 'drizzle-orm'
|
||||
import { getBeijingHour, getBeijingStartOfDay } from '~/utils/timeUtils'
|
||||
-import { getUserAvatarUrl } from '../../../utils/user-avatar'
|
||||
+import { getUserAvatarUrl } from '~~/server/utils/user-avatar'
|
||||
|
||||
function formatRelativeTime(date: Date | null | undefined): string {
|
||||
if (!date) return '未知'
|
||||
diff --git a/server/api/admin/users/[id].put.ts b/server/api/admin/users/[id].put.ts
|
||||
index eeb5ff3..4dbfec8 100644
|
||||
--- a/server/api/admin/users/[id].put.ts
|
||||
+++ b/server/api/admin/users/[id].put.ts
|
||||
@@ -3,7 +3,7 @@ import { db } from '~/drizzle/db'
|
||||
import { users, userStatusLogs } from '~/drizzle/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { updateUserPassword } from '~~/server/services/userService'
|
||||
-import { getRoleLevel, canChangeRoleOf, canResetPasswordFor } from '~~/server/utils/permissions.js'
|
||||
+import { getRoleLevel, canChangeRoleOf, canResetPasswordFor } from '~~/server/utils/permissions'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
diff --git a/server/api/admin/users/[id]/reset-password.post.ts b/server/api/admin/users/[id]/reset-password.post.ts
|
||||
index 968655f..c421e5b 100644
|
||||
--- a/server/api/admin/users/[id]/reset-password.post.ts
|
||||
+++ b/server/api/admin/users/[id]/reset-password.post.ts
|
||||
@@ -2,7 +2,7 @@ import { updateUserPassword } from '~~/server/services/userService'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { users } from '~/drizzle/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
-import { canResetPasswordFor, getRoleLevel } from '../../../utils/permissions.js'
|
||||
+import { canResetPasswordFor, getRoleLevel } from '~~/server/utils/permissions'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const currentUser = event.context.user
|
||||
diff --git a/server/api/admin/users/index.get.ts b/server/api/admin/users/index.get.ts
|
||||
index 437b652..9c2b06f 100644
|
||||
--- a/server/api/admin/users/index.get.ts
|
||||
+++ b/server/api/admin/users/index.get.ts
|
||||
@@ -106,7 +106,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
})
|
||||
|
||||
- const { getUserAvatarUrl } = await import('../../../utils/user-avatar')
|
||||
+ const { getUserAvatarUrl } = await import('~~/server/utils/user-avatar')
|
||||
|
||||
const formattedUsers = usersList.map((user) => {
|
||||
const avatarUrl = getUserAvatarUrl(
|
||||
diff --git a/server/api/admin/users/index.post.ts b/server/api/admin/users/index.post.ts
|
||||
index de23c11..5f41903 100644
|
||||
--- a/server/api/admin/users/index.post.ts
|
||||
+++ b/server/api/admin/users/index.post.ts
|
||||
@@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { users } from '~/drizzle/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
-import { getRoleLevel, isRoleHigher, canManageUserRole } from '~~/server/utils/permissions.js'
|
||||
+import { getRoleLevel, isRoleHigher, canManageUserRole } from '~~/server/utils/permissions'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 检查认证和权限
|
||||
diff --git a/server/api/roles.get.ts b/server/api/roles.get.ts
|
||||
index 415d3d7..7bd2a6c 100644
|
||||
--- a/server/api/roles.get.ts
|
||||
+++ b/server/api/roles.get.ts
|
||||
@@ -1,4 +1,4 @@
|
||||
-import { ROLES } from '../utils/permissions.js'
|
||||
+import { ROLES } from '~~/server/utils/permissions'
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
return ROLES
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
From a82ff6747eeeea1afaac58f2c4c5e1c015c5b3ce Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 11:56:30 +0800
|
||||
Subject: [PATCH 47/58] =?UTF-8?q?feat(site):=20=E5=AF=B9=E9=BD=90=E5=8F=82?=
|
||||
=?UTF-8?q?=E8=80=83=E7=AB=99=E7=82=B9UI=E4=B8=8E=E4=BA=A4=E4=BA=92?=
|
||||
=?UTF-8?q?=E8=B0=83=E6=95=B4?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- 页脚:响应耗时文字统一为「响应耗时 XXXms」,版本链接改为 GitHub 官方仓库,默认 ICP 备案号设置为粤ICP备2025450093号
|
||||
- 顶部栏:恢复主题切换按钮与语言按钮(语言暂为占位提示),放置于用户区左侧
|
||||
- 主题:修复首屏内联脚本默认主题白名单校验,避免 localStorage 中残留的 light/dark/System 等非法值生效,首屏默认改为 ClassicLight(仅允许 ClassicLight/ModernLight)
|
||||
- 登录页:在密码输入框下方恢复「忘记密码?」入口,新增配套的找回密码引导页 /forgot-password
|
||||
- 默认配置:system-settings 兜底记录与 SYSTEM_SETTINGS_DEFAULTS 新增 icpNumber 默认值
|
||||
---
|
||||
app/components/Auth/LoginForm.vue | 24 ++
|
||||
app/components/SiteFooter.vue | 11 +-
|
||||
app/pages/forgot-password.vue | 269 +++++++++++++++++++++++
|
||||
app/pages/index.vue | 85 +++++++
|
||||
nuxt.config.ts | 36 ++-
|
||||
server/api/site-config.get.ts | 3 +-
|
||||
server/utils/system-settings-defaults.ts | 2 +
|
||||
7 files changed, 415 insertions(+), 15 deletions(-)
|
||||
create mode 100644 app/pages/forgot-password.vue
|
||||
|
||||
diff --git a/app/components/Auth/LoginForm.vue b/app/components/Auth/LoginForm.vue
|
||||
index 63472c8..4ec8d57 100644
|
||||
--- a/app/components/Auth/LoginForm.vue
|
||||
+++ b/app/components/Auth/LoginForm.vue
|
||||
@@ -74,6 +74,11 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
+ <div class="forgot-password-row">
|
||||
+ <NuxtLink to="/forgot-password" class="forgot-password-link">
|
||||
+ 忘记密码?
|
||||
+ </NuxtLink>
|
||||
+ </div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-container">
|
||||
@@ -556,6 +561,25 @@ const handleWebAuthnLogin = async () => {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
+.forgot-password-row {
|
||||
+ display: flex;
|
||||
+ justify-content: flex-end;
|
||||
+ margin-top: 10px;
|
||||
+}
|
||||
+
|
||||
+.forgot-password-link {
|
||||
+ font-size: 13px;
|
||||
+ color: var(--primary);
|
||||
+ text-decoration: none;
|
||||
+ font-weight: var(--font-medium);
|
||||
+ transition: opacity 0.2s ease;
|
||||
+}
|
||||
+
|
||||
+.forgot-password-link:hover {
|
||||
+ opacity: 0.8;
|
||||
+ text-decoration: underline;
|
||||
+}
|
||||
+
|
||||
.form-footer {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
diff --git a/app/components/SiteFooter.vue b/app/components/SiteFooter.vue
|
||||
index 0b3adc7..8036a69 100644
|
||||
--- a/app/components/SiteFooter.vue
|
||||
+++ b/app/components/SiteFooter.vue
|
||||
@@ -23,11 +23,16 @@
|
||||
</span>
|
||||
<span v-if="siteTitle" class="footer-item">© {{ currentYear }} {{ siteTitle }}</span>
|
||||
<span v-else class="footer-item">© {{ currentYear }} {{ copyrightOwner }}</span>
|
||||
- <span class="footer-item">Worker in {{ responseTime }}ms</span>
|
||||
+ <span class="footer-item">响应耗时 {{ responseTime }}ms</span>
|
||||
<span class="footer-item">
|
||||
- <NuxtLink class="voicehub-link" to="/changelog">
|
||||
+ <a
|
||||
+ class="voicehub-link"
|
||||
+ href="https://github.com/laoshuikaixue/VoiceHub"
|
||||
+ target="_blank"
|
||||
+ rel="noopener noreferrer"
|
||||
+ >
|
||||
{{ displayName }} v{{ systemVersion }}
|
||||
- </NuxtLink>
|
||||
+ </a>
|
||||
</span>
|
||||
<span v-if="isNetlify" class="footer-item">
|
||||
<a
|
||||
diff --git a/app/pages/forgot-password.vue b/app/pages/forgot-password.vue
|
||||
new file mode 100644
|
||||
index 0000000..62e6ee5
|
||||
--- /dev/null
|
||||
+++ b/app/pages/forgot-password.vue
|
||||
@@ -0,0 +1,269 @@
|
||||
+<template>
|
||||
+ <div class="auth-layout">
|
||||
+ <div class="auth-container">
|
||||
+ <div class="form-section">
|
||||
+ <div class="form-header">
|
||||
+ <div class="logo-row">
|
||||
+ <img
|
||||
+ :src="brandLogoSrc"
|
||||
+ alt="Brand Logo"
|
||||
+ class="brand-logo-center"
|
||||
+ width="96"
|
||||
+ height="96"
|
||||
+ decoding="async"
|
||||
+ fetchpriority="high"
|
||||
+ >
|
||||
+ <div v-if="schoolLogoHomeUrl && schoolLogoHomeUrl.trim()" class="logo-divider" />
|
||||
+ <img
|
||||
+ v-if="schoolLogoHomeUrl && schoolLogoHomeUrl.trim()"
|
||||
+ :src="schoolLogoHomeUrl"
|
||||
+ alt="学校Logo"
|
||||
+ class="school-logo"
|
||||
+ width="160"
|
||||
+ height="160"
|
||||
+ decoding="async"
|
||||
+ fetchpriority="high"
|
||||
+ >
|
||||
+ </div>
|
||||
+ <h1 class="form-title">{{ siteTitle ? siteTitle + ' | VoiceHub' : 'VoiceHub' }}</h1>
|
||||
+ <div class="header-divider" />
|
||||
+ </div>
|
||||
+ <div class="forgot-password-form">
|
||||
+ <div class="page-intro">
|
||||
+ <h2 class="page-heading">找回密码</h2>
|
||||
+ <p class="page-subtitle">请联系管理员重置您的账号密码</p>
|
||||
+ </div>
|
||||
+
|
||||
+ <div class="contact-card">
|
||||
+ <div class="contact-card-icon">
|
||||
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
+ <path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
+ </svg>
|
||||
+ </div>
|
||||
+ <div class="contact-card-body">
|
||||
+ <h3>联系管理员</h3>
|
||||
+ <p>为保障账号安全,系统暂未开放自助密码找回。请通过以下方式联系管理员进行密码重置:</p>
|
||||
+ <ul class="contact-tips">
|
||||
+ <li>站内联系:登录有管理员权限的账号</li>
|
||||
+ <li>线下联系:前往广播站办公室或指导老师</li>
|
||||
+ <li>邮件联系:如有公开的管理员邮箱</li>
|
||||
+ </ul>
|
||||
+ </div>
|
||||
+ </div>
|
||||
+
|
||||
+ <div class="form-actions">
|
||||
+ <NuxtLink to="/login" class="back-to-login-btn">
|
||||
+ 返回登录
|
||||
+ </NuxtLink>
|
||||
+ </div>
|
||||
+ </div>
|
||||
+ </div>
|
||||
+ </div>
|
||||
+ <SiteFooter />
|
||||
+ </div>
|
||||
+</template>
|
||||
+
|
||||
+<script setup>
|
||||
+import { onMounted, computed } from 'vue'
|
||||
+import logo from '~~/public/images/logo.png'
|
||||
+
|
||||
+const { siteTitle, initSiteConfig, schoolLogoHomeUrl } = useSiteConfig()
|
||||
+const brandLogoSrc = computed(() => {
|
||||
+ return logo
|
||||
+})
|
||||
+
|
||||
+onMounted(async () => {
|
||||
+ await initSiteConfig()
|
||||
+ if (typeof document !== 'undefined' && siteTitle.value) {
|
||||
+ document.title = `找回密码 | ${siteTitle.value}`
|
||||
+ }
|
||||
+})
|
||||
+</script>
|
||||
+
|
||||
+<style scoped>
|
||||
+.auth-layout {
|
||||
+ min-height: 100vh;
|
||||
+ background: var(--bg-primary);
|
||||
+ display: flex;
|
||||
+ flex-direction: column;
|
||||
+ align-items: center;
|
||||
+ justify-content: flex-start;
|
||||
+ padding: 20px;
|
||||
+}
|
||||
+
|
||||
+.auth-container {
|
||||
+ width: 100%;
|
||||
+ max-width: 520px;
|
||||
+ background: var(--bg-secondary);
|
||||
+ border-radius: var(--radius-2xl);
|
||||
+ border: 1px solid var(--border-primary);
|
||||
+ box-shadow: var(--shadow-lg);
|
||||
+ overflow: hidden;
|
||||
+ margin: auto 0;
|
||||
+ margin-bottom: 40px;
|
||||
+}
|
||||
+
|
||||
+.form-section {
|
||||
+ padding: 40px 32px;
|
||||
+ display: flex;
|
||||
+ flex-direction: column;
|
||||
+ align-items: center;
|
||||
+ justify-content: center;
|
||||
+ background: var(--bg-secondary);
|
||||
+}
|
||||
+
|
||||
+.form-header {
|
||||
+ text-align: center;
|
||||
+ margin-bottom: 24px;
|
||||
+ width: 100%;
|
||||
+}
|
||||
+
|
||||
+.logo-row {
|
||||
+ display: flex;
|
||||
+ align-items: center;
|
||||
+ justify-content: center;
|
||||
+ gap: 24px;
|
||||
+ margin-bottom: 12px;
|
||||
+}
|
||||
+
|
||||
+.logo-divider {
|
||||
+ width: 1px;
|
||||
+ height: 96px;
|
||||
+ background: var(--border-secondary);
|
||||
+}
|
||||
+
|
||||
+.brand-logo-center {
|
||||
+ width: 96px;
|
||||
+ height: 96px;
|
||||
+ object-fit: contain;
|
||||
+ filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15));
|
||||
+}
|
||||
+
|
||||
+.school-logo {
|
||||
+ width: 160px;
|
||||
+ height: 160px;
|
||||
+ object-fit: contain;
|
||||
+ filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15));
|
||||
+}
|
||||
+
|
||||
+.form-title {
|
||||
+ font-size: 24px;
|
||||
+ font-weight: var(--font-bold);
|
||||
+ margin: 0;
|
||||
+ color: var(--text-primary);
|
||||
+}
|
||||
+
|
||||
+.header-divider {
|
||||
+ height: 1px;
|
||||
+ background: var(--border-secondary);
|
||||
+ margin: 14px auto 0;
|
||||
+ width: 100%;
|
||||
+}
|
||||
+
|
||||
+.forgot-password-form {
|
||||
+ width: 100%;
|
||||
+ max-width: 400px;
|
||||
+}
|
||||
+
|
||||
+.page-intro {
|
||||
+ text-align: center;
|
||||
+ margin-bottom: 28px;
|
||||
+}
|
||||
+
|
||||
+.page-heading {
|
||||
+ font-size: 28px;
|
||||
+ font-weight: var(--font-bold);
|
||||
+ color: var(--text-primary);
|
||||
+ margin: 0 0 8px 0;
|
||||
+}
|
||||
+
|
||||
+.page-subtitle {
|
||||
+ font-size: 14px;
|
||||
+ color: var(--text-tertiary);
|
||||
+ margin: 0;
|
||||
+}
|
||||
+
|
||||
+.contact-card {
|
||||
+ display: flex;
|
||||
+ gap: 16px;
|
||||
+ padding: 24px;
|
||||
+ background: var(--bg-tertiary);
|
||||
+ border: 1px solid var(--border-secondary);
|
||||
+ border-radius: var(--radius-lg);
|
||||
+ margin-bottom: 28px;
|
||||
+}
|
||||
+
|
||||
+.contact-card-icon {
|
||||
+ width: 40px;
|
||||
+ height: 40px;
|
||||
+ border-radius: 12px;
|
||||
+ background: rgba(59, 130, 246, 0.12);
|
||||
+ color: var(--primary);
|
||||
+ display: flex;
|
||||
+ align-items: center;
|
||||
+ justify-content: center;
|
||||
+ flex-shrink: 0;
|
||||
+}
|
||||
+
|
||||
+.contact-card-icon svg {
|
||||
+ width: 20px;
|
||||
+ height: 20px;
|
||||
+}
|
||||
+
|
||||
+.contact-card-body h3 {
|
||||
+ font-size: 16px;
|
||||
+ font-weight: var(--font-semibold);
|
||||
+ color: var(--text-primary);
|
||||
+ margin: 0 0 8px 0;
|
||||
+}
|
||||
+
|
||||
+.contact-card-body p {
|
||||
+ font-size: 14px;
|
||||
+ color: var(--text-secondary);
|
||||
+ margin: 0 0 12px 0;
|
||||
+ line-height: 1.6;
|
||||
+}
|
||||
+
|
||||
+.contact-tips {
|
||||
+ margin: 0;
|
||||
+ padding-left: 18px;
|
||||
+ color: var(--text-secondary);
|
||||
+ font-size: 13px;
|
||||
+ line-height: 1.9;
|
||||
+}
|
||||
+
|
||||
+.form-actions {
|
||||
+ display: flex;
|
||||
+ flex-direction: column;
|
||||
+ gap: 12px;
|
||||
+}
|
||||
+
|
||||
+.back-to-login-btn {
|
||||
+ display: inline-flex;
|
||||
+ align-items: center;
|
||||
+ justify-content: center;
|
||||
+ width: 100%;
|
||||
+ padding: 14px;
|
||||
+ background: var(--btn-primary-bg);
|
||||
+ color: var(--btn-primary-text);
|
||||
+ border: 1px solid var(--btn-primary-border);
|
||||
+ border-radius: var(--radius-lg);
|
||||
+ font-size: 16px;
|
||||
+ font-weight: var(--font-semibold);
|
||||
+ text-decoration: none;
|
||||
+ transition: all 0.2s ease;
|
||||
+}
|
||||
+
|
||||
+.back-to-login-btn:hover {
|
||||
+ background: var(--btn-primary-hover);
|
||||
+ transform: translateY(-1px);
|
||||
+ box-shadow: var(--shadow-md);
|
||||
+}
|
||||
+
|
||||
+@media (max-width: 768px) {
|
||||
+ .auth-layout { padding: 10px; }
|
||||
+ .auth-container { border-radius: 16px; }
|
||||
+ .form-section { padding: 30px 20px; }
|
||||
+ .contact-card { padding: 18px; gap: 12px; }
|
||||
+}
|
||||
+</style>
|
||||
diff --git a/app/pages/index.vue b/app/pages/index.vue
|
||||
index df5d8a9..2a31f7e 100644
|
||||
--- a/app/pages/index.vue
|
||||
+++ b/app/pages/index.vue
|
||||
@@ -33,6 +33,30 @@
|
||||
<!-- 用户信息区域 -->
|
||||
<div class="user-section">
|
||||
<ClientOnly>
|
||||
+ <!-- 主题切换按钮 -->
|
||||
+ <button
|
||||
+ class="utility-btn theme-toggle-btn"
|
||||
+ type="button"
|
||||
+ aria-label="主题"
|
||||
+ title="切换主题"
|
||||
+ @click="handleToggleTheme"
|
||||
+ >
|
||||
+ <Icon :name="isDark ? 'sun' : 'moon'" :size="18" />
|
||||
+ <span class="utility-btn-label">主题</span>
|
||||
+ </button>
|
||||
+
|
||||
+ <!-- 语言切换按钮(占位按钮,当前单语种) -->
|
||||
+ <button
|
||||
+ class="utility-btn lang-toggle-btn"
|
||||
+ type="button"
|
||||
+ aria-label="语言"
|
||||
+ title="语言"
|
||||
+ @click="handleLanguageClick"
|
||||
+ >
|
||||
+ <Icon name="globe" :size="18" />
|
||||
+ <span class="utility-btn-label">语言</span>
|
||||
+ </button>
|
||||
+
|
||||
<div v-if="isClientAuthenticated" class="user-info">
|
||||
<div class="user-details-desktop">
|
||||
<span class="user-name">{{ user?.name || '用户' }}</span>
|
||||
@@ -674,12 +698,24 @@ import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
|
||||
|
||||
import { useNotifications } from '~/composables/useNotifications'
|
||||
import { useSiteConfig } from '~/composables/useSiteConfig'
|
||||
+import { useTheme } from '~/composables/useTheme'
|
||||
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
|
||||
|
||||
// 获取运行时配置
|
||||
const config = useRuntimeConfig()
|
||||
const router = useRouter()
|
||||
|
||||
+// 主题
|
||||
+const { isDark, toggleTheme } = useTheme()
|
||||
+const handleToggleTheme = () => {
|
||||
+ toggleTheme()
|
||||
+}
|
||||
+const handleLanguageClick = () => {
|
||||
+ if (typeof window !== 'undefined' && window.$showNotification) {
|
||||
+ window.$showNotification('当前版本暂不支持多语言切换', 'info')
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
// 站点配置
|
||||
const {
|
||||
siteTitle,
|
||||
@@ -1709,6 +1745,55 @@ if (
|
||||
.user-section {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
+ display: flex;
|
||||
+ align-items: center;
|
||||
+ gap: 10px;
|
||||
+}
|
||||
+
|
||||
+/* 通用工具按钮(主题/语言) */
|
||||
+.utility-btn {
|
||||
+ display: inline-flex;
|
||||
+ align-items: center;
|
||||
+ gap: 6px;
|
||||
+ padding: 7px 12px;
|
||||
+ background: var(--utility-btn-bg, rgba(255, 255, 255, 0.06));
|
||||
+ color: var(--utility-btn-color, rgba(255, 255, 255, 0.78));
|
||||
+ border: 1px solid var(--utility-btn-border, rgba(255, 255, 255, 0.12));
|
||||
+ border-radius: 10px;
|
||||
+ font-size: 13px;
|
||||
+ font-weight: 500;
|
||||
+ line-height: 1;
|
||||
+ cursor: pointer;
|
||||
+ transition:
|
||||
+ background 0.2s ease,
|
||||
+ color 0.2s ease,
|
||||
+ border-color 0.2s ease,
|
||||
+ transform 0.15s ease;
|
||||
+ backdrop-filter: blur(6px);
|
||||
+}
|
||||
+
|
||||
+.utility-btn:hover {
|
||||
+ background: var(--utility-btn-hover-bg, rgba(255, 255, 255, 0.12));
|
||||
+ color: var(--utility-btn-hover-color, #ffffff);
|
||||
+ border-color: var(--utility-btn-hover-border, rgba(255, 255, 255, 0.22));
|
||||
+}
|
||||
+
|
||||
+.utility-btn:active {
|
||||
+ transform: scale(0.97);
|
||||
+}
|
||||
+
|
||||
+.utility-btn-label {
|
||||
+ white-space: nowrap;
|
||||
+}
|
||||
+
|
||||
+@media (max-width: 768px) {
|
||||
+ .utility-btn-label {
|
||||
+ display: none;
|
||||
+ }
|
||||
+ .utility-btn {
|
||||
+ padding: 7px 8px;
|
||||
+ border-radius: 9px;
|
||||
+ }
|
||||
}
|
||||
|
||||
.user-info {
|
||||
diff --git a/nuxt.config.ts b/nuxt.config.ts
|
||||
index d68f3fc..6964a0d 100644
|
||||
--- a/nuxt.config.ts
|
||||
+++ b/nuxt.config.ts
|
||||
@@ -192,7 +192,11 @@ export default defineNuxtConfig({
|
||||
{ name: 'theme-color', content: '#111111' },
|
||||
{
|
||||
name: 'x-default-theme',
|
||||
- content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'System'
|
||||
+ content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'ClassicLight'
|
||||
+ },
|
||||
+ {
|
||||
+ name: 'x-enabled-themes',
|
||||
+ content: process.env.NUXT_PUBLIC_ENABLED_THEMES || 'ClassicLight,ModernLight'
|
||||
},
|
||||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
|
||||
@@ -206,18 +210,28 @@ export default defineNuxtConfig({
|
||||
tagPosition: 'head',
|
||||
children: [
|
||||
`(function(){try{`,
|
||||
- ` var k='voicehub:theme', d=document, r=d.documentElement, s=null, eff=null;`,
|
||||
- ` try{s=localStorage.getItem(k)}catch(e){s=null}`,
|
||||
- ` if(!s){`,
|
||||
- ` try{`,
|
||||
- ` var meta=document.querySelector('meta[name="x-default-theme"]');`,
|
||||
- ` if(meta && meta.content){s=meta.content}`,
|
||||
- ` }catch(e){s=null}`,
|
||||
- ` if(!s){s='System'}`,
|
||||
+ ` var k='voicehub:theme', d=document, r=d.documentElement, s=null, eff=null, def=null, list=[];`,
|
||||
+ ` try{`,
|
||||
+ ` var mt=d.querySelector('meta[name="x-enabled-themes"]');`,
|
||||
+ ` if(mt && mt.content){list=String(mt.content).split(',').map(function(x){return x.replace(/^\\s+|\\s+$/g,'')}).filter(Boolean)}`,
|
||||
+ ` }catch(e){list=[]}`,
|
||||
+ ` try{`,
|
||||
+ ` var md=d.querySelector('meta[name="x-default-theme"]');`,
|
||||
+ ` if(md && md.content){def=md.content}`,
|
||||
+ ` }catch(e){def=null}`,
|
||||
+ ` if(!def){def='ClassicLight'}`,
|
||||
+ ` if(list.length===0){list=['ClassicLight','ModernLight']}`,
|
||||
+ ` function valid(t){`,
|
||||
+ ` if(list.indexOf(t)!==-1){return t}`,
|
||||
+ ` if(list.indexOf(def)!==-1){return def}`,
|
||||
+ ` return list[0]`,
|
||||
` }`,
|
||||
- ` if(s==='System'||s==='system'){eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`,
|
||||
+ ` try{s=localStorage.getItem(k)}catch(e){s=null}`,
|
||||
+ ` if(!s){s=def}`,
|
||||
+ ` else{s=valid(s)}`,
|
||||
+ ` if(typeof s==='string' && (s==='System'||s==='system')){eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`,
|
||||
` else if(typeof s==='string'){eff=s.endsWith('Dark')||s==='dark'?'dark':(s.endsWith('Light')||s==='light'?'light':s)}`,
|
||||
- ` else{eff='dark'}`,
|
||||
+ ` else{eff='light'}`,
|
||||
` r.setAttribute('data-theme', (typeof s==='string'?s:eff));`,
|
||||
` if(eff==='dark'){r.classList.add('dark'); r.classList.remove('light'); r.style.colorScheme='dark'}`,
|
||||
` else if(eff==='light'){r.classList.add('light'); r.classList.remove('dark'); r.style.colorScheme='light'}`,
|
||||
diff --git a/server/api/site-config.get.ts b/server/api/site-config.get.ts
|
||||
index 0b427a6..0ad6af9 100644
|
||||
--- a/server/api/site-config.get.ts
|
||||
+++ b/server/api/site-config.get.ts
|
||||
@@ -77,7 +77,8 @@ export default defineEventHandler(async (event) => {
|
||||
siteDescription: '校园广播站点歌系统 - 让你的声音被听见',
|
||||
siteVersion: process.env.NUXT_PUBLIC_SITE_VERSION || null,
|
||||
submissionGuidelines: '请遵守校园规定,提交健康向上的歌曲。',
|
||||
- icpNumber: null,
|
||||
+ icpNumber: '粤ICP备2025450093号',
|
||||
+ gonganNumber: null,
|
||||
enableSubmissionLimit: false,
|
||||
dailySubmissionLimit: null,
|
||||
weeklySubmissionLimit: null,
|
||||
diff --git a/server/utils/system-settings-defaults.ts b/server/utils/system-settings-defaults.ts
|
||||
index 2627dca..73e2251 100644
|
||||
--- a/server/utils/system-settings-defaults.ts
|
||||
+++ b/server/utils/system-settings-defaults.ts
|
||||
@@ -1,6 +1,8 @@
|
||||
export const SYSTEM_SETTINGS_DEFAULTS = {
|
||||
siteTitle: '校园广播站点歌系统',
|
||||
siteVersion: '1.7.1.0',
|
||||
+ icpNumber: '粤ICP备2025450093号',
|
||||
+ gonganNumber: '',
|
||||
enableSubmissionLimit: false,
|
||||
showBlacklistKeywords: false,
|
||||
hideStudentInfo: true,
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
From 94832e2c825c64036c9d7e9b400809865392830f Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 12:32:27 +0800
|
||||
Subject: [PATCH 48/58] =?UTF-8?q?fix(ui):=20=E5=BC=BA=E5=88=B6=E6=B5=85?=
|
||||
=?UTF-8?q?=E8=89=B2=E4=B8=BB=E9=A2=98=E4=B8=8E=E7=99=BB=E5=BD=95=E9=A1=B5?=
|
||||
=?UTF-8?q?=E7=BB=86=E8=8A=82=E4=BF=AE=E6=AD=A3?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- 主题:将 CSS 变量默认值由深色改为 ClassicLight 浅色基准(背景/文字/边框/按钮/输入框/滚动条),追加 prefers-color-scheme: dark 下的 color-scheme: light 覆盖,补齐 surface-xxx 别名避免部分组件底色色值缺失
|
||||
- 语言按钮:删除首页顶栏语言切换按钮及其点击处理逻辑
|
||||
- 第三方登录:OAuthButtons 重写为使用语义化 CSS 变量而非无效的 --border-color,新增 alwaysShow 选项在未配置 provider 时显示占位提示;登录页强制启用提示
|
||||
- 页脚与登录页:全部文字颜色与分割线由硬编码 rgba(255*) 改为 var(--text-tertiary/quaternary/border-primary),在浅色背景下保证对比度
|
||||
---
|
||||
app/assets/css/variables.css | 94 +++++++++-------
|
||||
app/components/Auth/LoginForm.vue | 2 +-
|
||||
app/components/Auth/OAuthButtons.vue | 158 +++++++++++++++++++++++++--
|
||||
app/components/SiteFooter.vue | 10 +-
|
||||
app/pages/index.vue | 31 +-----
|
||||
app/pages/login.vue | 2 +-
|
||||
6 files changed, 218 insertions(+), 79 deletions(-)
|
||||
|
||||
diff --git a/app/assets/css/variables.css b/app/assets/css/variables.css
|
||||
index 30dce2a..88a6536 100644
|
||||
--- a/app/assets/css/variables.css
|
||||
+++ b/app/assets/css/variables.css
|
||||
@@ -1,10 +1,13 @@
|
||||
-/* 全局CSS变量 - 统一配色方案 */
|
||||
+/* 全局CSS变量 - 统一配色方案(默认浅色ClassicLight,不依赖data-theme也能正常显示) */
|
||||
:root {
|
||||
+ /* 强制浅色基准值 - 即使 data-theme 缺失也保持浅色 */
|
||||
+ color-scheme: light;
|
||||
+
|
||||
/* 主色调 */
|
||||
- --primary: #3b82f6;
|
||||
- --primary-hover: #2563eb;
|
||||
- --primary-light: rgba(59, 130, 246, 0.1);
|
||||
- --primary-border: rgba(59, 130, 246, 0.2);
|
||||
+ --primary: #1e40af;
|
||||
+ --primary-hover: #1e3a8a;
|
||||
+ --primary-light: rgba(30, 64, 175, 0.08);
|
||||
+ --primary-border: rgba(30, 64, 175, 0.15);
|
||||
|
||||
/* 成功色 */
|
||||
--success: #10b981;
|
||||
@@ -30,25 +33,25 @@
|
||||
--info-light: rgba(59, 130, 246, 0.1);
|
||||
--info-border: rgba(59, 130, 246, 0.2);
|
||||
|
||||
- /* 背景色 */
|
||||
- --bg-primary: #111111;
|
||||
- --bg-secondary: #1a1a1a;
|
||||
- --bg-tertiary: #2a2a2a;
|
||||
- --bg-quaternary: #3a3a3a;
|
||||
- --bg-hover: #1f1f1f;
|
||||
-
|
||||
- /* 文字颜色 */
|
||||
- --text-primary: #ffffff;
|
||||
- --text-secondary: #cccccc;
|
||||
- --text-tertiary: #888888;
|
||||
- --text-quaternary: #666666;
|
||||
- --text-disabled: #444444;
|
||||
-
|
||||
- /* 边框颜色 */
|
||||
- --border-primary: #1f1f1f;
|
||||
- --border-secondary: #2a2a2a;
|
||||
- --border-tertiary: #3a3a3a;
|
||||
- --border-quaternary: #4a4a4a;
|
||||
+ /* 背景色 (默认 ClassicLight 浅色) */
|
||||
+ --bg-primary: #ffffff;
|
||||
+ --bg-secondary: #f5f7fa;
|
||||
+ --bg-tertiary: #edf0f5;
|
||||
+ --bg-quaternary: #d9dee7;
|
||||
+ --bg-hover: #f0f3f8;
|
||||
+
|
||||
+ /* 文字颜色 (默认 ClassicLight 浅色) */
|
||||
+ --text-primary: #1a2332;
|
||||
+ --text-secondary: #4a5568;
|
||||
+ --text-tertiary: #6b7280;
|
||||
+ --text-quaternary: #9ca3af;
|
||||
+ --text-disabled: #c5cbd3;
|
||||
+
|
||||
+ /* 边框颜色 (默认 ClassicLight 浅色) */
|
||||
+ --border-primary: #e5e7eb;
|
||||
+ --border-secondary: #d1d5db;
|
||||
+ --border-tertiary: #9ca3af;
|
||||
+ --border-quaternary: #6b7280;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
@@ -132,16 +135,16 @@
|
||||
--card-hover-border: var(--border-tertiary);
|
||||
--card-shadow: var(--shadow-md);
|
||||
|
||||
- /* 按钮颜色 */
|
||||
+ /* 按钮颜色 - 浅色基准 */
|
||||
--btn-primary-bg: var(--primary);
|
||||
--btn-primary-hover: var(--primary-hover);
|
||||
- --btn-primary-text: var(--text-primary);
|
||||
+ --btn-primary-text: #ffffff;
|
||||
--btn-primary-border: var(--primary-border);
|
||||
|
||||
- --btn-secondary-bg: #3a3a3a;
|
||||
- --btn-secondary-hover: #4a4a4a;
|
||||
- --btn-secondary-text: var(--text-primary);
|
||||
- --btn-secondary-border: #555555;
|
||||
+ --btn-secondary-bg: #f1f5f9;
|
||||
+ --btn-secondary-hover: #e2e8f0;
|
||||
+ --btn-secondary-text: #0f172a;
|
||||
+ --btn-secondary-border: #cbd5e1;
|
||||
|
||||
--btn-success-bg: var(--success);
|
||||
--btn-success-hover: var(--success-hover);
|
||||
@@ -161,12 +164,12 @@
|
||||
--btn-disabled-bg: var(--bg-tertiary);
|
||||
--btn-disabled-text: var(--text-disabled);
|
||||
|
||||
- /* 输入框颜色 */
|
||||
- --input-bg: var(--bg-tertiary);
|
||||
- --input-border: var(--border-tertiary);
|
||||
+ /* 输入框颜色 - 浅色基准 */
|
||||
+ --input-bg: #ffffff;
|
||||
+ --input-border: #d1d5db;
|
||||
--input-border-focus: var(--primary);
|
||||
--input-text: var(--text-primary);
|
||||
- --input-placeholder: var(--text-quaternary);
|
||||
+ --input-placeholder: #9ca3af;
|
||||
--input-shadow-focus: 0 0 0 3px var(--primary-light);
|
||||
|
||||
/* 模态框颜色 */
|
||||
@@ -197,12 +200,27 @@
|
||||
--spinner-primary: var(--primary);
|
||||
--spinner-secondary: var(--bg-tertiary);
|
||||
|
||||
- /* 滚动条颜色 */
|
||||
- --scrollbar-track: var(--bg-tertiary);
|
||||
- --scrollbar-thumb: var(--bg-quaternary);
|
||||
- --scrollbar-thumb-hover: #4a4a4a;
|
||||
+ /* 滚动条颜色 - 浅色基准 */
|
||||
+ --scrollbar-track: #f1f5f9;
|
||||
+ --scrollbar-thumb: #cbd5e1;
|
||||
+ --scrollbar-thumb-hover: #94a3b8;
|
||||
+
|
||||
+ /* Surface 兼容别名 - 部分按钮/容器使用 surface-xxx */
|
||||
+ --surface-primary: var(--bg-primary);
|
||||
+ --surface-secondary: var(--bg-secondary);
|
||||
+ --surface-tertiary: var(--bg-tertiary);
|
||||
+ --surface-quaternary: var(--bg-quaternary);
|
||||
+}
|
||||
+
|
||||
+/* 防深色兜底:覆盖 prefers-color-scheme: dark 对表单/根元素的自动影响 */
|
||||
+@media (prefers-color-scheme: dark) {
|
||||
+ :root {
|
||||
+ color-scheme: light;
|
||||
+ }
|
||||
}
|
||||
|
||||
+/* 浅色主题叠加层(与 data-theme 无关,确保始终为浅色;保留 data-theme 变量覆盖以兼容切换) */
|
||||
+:root,
|
||||
:root[data-theme='light'],
|
||||
:root[data-theme='ClassicLight'],
|
||||
:root[data-theme='ModernLight'] {
|
||||
diff --git a/app/components/Auth/LoginForm.vue b/app/components/Auth/LoginForm.vue
|
||||
index 4ec8d57..430e302 100644
|
||||
--- a/app/components/Auth/LoginForm.vue
|
||||
+++ b/app/components/Auth/LoginForm.vue
|
||||
@@ -143,7 +143,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
- <AuthOAuthButtons v-if="!isBindMode" />
|
||||
+ <AuthOAuthButtons v-if="!isBindMode" :always-show="true" />
|
||||
|
||||
<div class="form-footer">
|
||||
<p class="help-text">不同VoiceHub平台的账号不互通</p>
|
||||
diff --git a/app/components/Auth/OAuthButtons.vue b/app/components/Auth/OAuthButtons.vue
|
||||
index bcd6ac0..77e08f1 100644
|
||||
--- a/app/components/Auth/OAuthButtons.vue
|
||||
+++ b/app/components/Auth/OAuthButtons.vue
|
||||
@@ -1,15 +1,13 @@
|
||||
<template>
|
||||
- <div v-if="hasEnabledProviders" class="mt-6 w-full">
|
||||
- <div class="flex items-center text-center text-[var(--text-tertiary)] text-xs mb-4">
|
||||
- <div class="flex-1 border-b border-[var(--border-color)] opacity-20" />
|
||||
- <span class="px-2.5">或使用第三方账号登录</span>
|
||||
- <div class="flex-1 border-b border-[var(--border-color)] opacity-20" />
|
||||
+ <div v-if="hasEnabledProviders" class="oauth-buttons-section">
|
||||
+ <div class="oauth-divider">
|
||||
+ <span>或使用第三方账号登录</span>
|
||||
</div>
|
||||
- <div class="flex justify-center gap-4">
|
||||
+ <div class="oauth-buttons">
|
||||
<button
|
||||
v-if="config.public.oauth.github"
|
||||
type="button"
|
||||
- class="w-12 h-12 rounded-full border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-primary)] flex items-center justify-center cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-sm hover:bg-[var(--bg-tertiary)] hover:bg-[#24292e] hover:text-white hover:border-[#24292e]"
|
||||
+ class="oauth-btn oauth-btn-github"
|
||||
title="使用 GitHub 登录"
|
||||
@click="loginWith('github')"
|
||||
>
|
||||
@@ -19,7 +17,7 @@
|
||||
<button
|
||||
v-if="config.public.oauth.google"
|
||||
type="button"
|
||||
- class="w-12 h-12 rounded-full border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-primary)] flex items-center justify-center cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-sm hover:bg-[var(--bg-tertiary)] hover:bg-white hover:text-black hover:border-[#dadce0]"
|
||||
+ class="oauth-btn oauth-btn-google"
|
||||
title="使用 Google 登录"
|
||||
@click="loginWith('google')"
|
||||
>
|
||||
@@ -29,7 +27,7 @@
|
||||
<button
|
||||
v-if="config.public.oauth.aggregate"
|
||||
type="button"
|
||||
- class="w-12 h-12 rounded-full border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-primary)] flex items-center justify-center cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-sm hover:bg-[var(--bg-tertiary)] hover:bg-[#12B7F5] hover:text-white hover:border-[#12B7F5]"
|
||||
+ class="oauth-btn oauth-btn-aggregate"
|
||||
title="使用 QQ / 微信登录"
|
||||
@click="loginWith('aggregate')"
|
||||
>
|
||||
@@ -39,16 +37,158 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+
|
||||
+ <div v-else-if="showPlaceholderHint" class="oauth-buttons-section">
|
||||
+ <div class="oauth-divider oauth-divider-dim">
|
||||
+ <span>第三方登录</span>
|
||||
+ </div>
|
||||
+ <div class="oauth-empty-hint">
|
||||
+ <svg
|
||||
+ class="oauth-empty-icon"
|
||||
+ fill="none"
|
||||
+ stroke="currentColor"
|
||||
+ stroke-width="2"
|
||||
+ viewBox="0 0 24 24"
|
||||
+ >
|
||||
+ <path d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
+ </svg>
|
||||
+ <p>请联系管理员在 <code>.env</code> 中配置第三方 OAuth 应用后即可启用</p>
|
||||
+ </div>
|
||||
+ </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
+interface Props {
|
||||
+ alwaysShow?: boolean
|
||||
+}
|
||||
+const props = withDefaults(defineProps<Props>(), {
|
||||
+ alwaysShow: false
|
||||
+})
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const hasEnabledProviders = computed(() => {
|
||||
return config.public.oauth && Object.values(config.public.oauth).some((enabled) => enabled)
|
||||
})
|
||||
|
||||
+const showPlaceholderHint = computed(() => props.alwaysShow && !hasEnabledProviders.value)
|
||||
+
|
||||
const loginWith = (provider: string) => {
|
||||
navigateTo(`/api/auth/${provider}`, { external: true })
|
||||
}
|
||||
</script>
|
||||
+
|
||||
+<style scoped>
|
||||
+.oauth-buttons-section {
|
||||
+ width: 100%;
|
||||
+ margin-top: 24px;
|
||||
+}
|
||||
+
|
||||
+.oauth-divider {
|
||||
+ display: flex;
|
||||
+ align-items: center;
|
||||
+ text-align: center;
|
||||
+ margin: 0 0 18px 0;
|
||||
+ color: var(--text-quaternary, #9ca3af);
|
||||
+ font-size: 12px;
|
||||
+}
|
||||
+
|
||||
+.oauth-divider::before,
|
||||
+.oauth-divider::after {
|
||||
+ content: '';
|
||||
+ flex: 1;
|
||||
+ border-bottom: 1px solid var(--border-secondary, #d1d5db);
|
||||
+ opacity: 0.85;
|
||||
+}
|
||||
+
|
||||
+.oauth-divider span {
|
||||
+ padding: 0 12px;
|
||||
+ white-space: nowrap;
|
||||
+}
|
||||
+
|
||||
+.oauth-divider-dim {
|
||||
+ color: var(--text-quaternary, #9ca3af);
|
||||
+ opacity: 0.8;
|
||||
+}
|
||||
+
|
||||
+.oauth-buttons {
|
||||
+ display: flex;
|
||||
+ justify-content: center;
|
||||
+ gap: 16px;
|
||||
+}
|
||||
+
|
||||
+.oauth-btn {
|
||||
+ width: 48px;
|
||||
+ height: 48px;
|
||||
+ border-radius: 999px;
|
||||
+ border: 1px solid var(--border-secondary, #d1d5db);
|
||||
+ background: var(--bg-secondary, #f5f7fa);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
+ display: inline-flex;
|
||||
+ align-items: center;
|
||||
+ justify-content: center;
|
||||
+ cursor: pointer;
|
||||
+ transition:
|
||||
+ transform 0.2s ease,
|
||||
+ box-shadow 0.2s ease,
|
||||
+ background-color 0.2s ease,
|
||||
+ border-color 0.2s ease,
|
||||
+ color 0.2s ease;
|
||||
+}
|
||||
+
|
||||
+.oauth-btn:hover {
|
||||
+ transform: translateY(-1px);
|
||||
+ box-shadow: var(--shadow-sm);
|
||||
+}
|
||||
+
|
||||
+.oauth-btn-github:hover {
|
||||
+ background: #24292e;
|
||||
+ color: #ffffff;
|
||||
+ border-color: #24292e;
|
||||
+}
|
||||
+
|
||||
+.oauth-btn-google:hover {
|
||||
+ background: #ffffff;
|
||||
+ color: #111111;
|
||||
+ border-color: #dadce0;
|
||||
+}
|
||||
+
|
||||
+.oauth-btn-aggregate:hover {
|
||||
+ background: #12b7f5;
|
||||
+ color: #ffffff;
|
||||
+ border-color: #12b7f5;
|
||||
+}
|
||||
+
|
||||
+.oauth-empty-hint {
|
||||
+ display: flex;
|
||||
+ align-items: center;
|
||||
+ justify-content: center;
|
||||
+ gap: 10px;
|
||||
+ padding: 14px 16px;
|
||||
+ border: 1px dashed var(--border-secondary, #d1d5db);
|
||||
+ border-radius: var(--radius-lg);
|
||||
+ background: var(--bg-tertiary, #edf0f5);
|
||||
+ color: var(--text-tertiary, #6b7280);
|
||||
+ font-size: 13px;
|
||||
+}
|
||||
+
|
||||
+.oauth-empty-icon {
|
||||
+ width: 18px;
|
||||
+ height: 18px;
|
||||
+ opacity: 0.8;
|
||||
+ flex-shrink: 0;
|
||||
+}
|
||||
+
|
||||
+.oauth-empty-hint p {
|
||||
+ margin: 0;
|
||||
+ line-height: 1.5;
|
||||
+}
|
||||
+
|
||||
+.oauth-empty-hint code {
|
||||
+ background: var(--input-bg, #ffffff);
|
||||
+ color: var(--primary, #1e40af);
|
||||
+ padding: 1px 6px;
|
||||
+ border-radius: 4px;
|
||||
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
+ font-size: 12px;
|
||||
+}
|
||||
+</style>
|
||||
diff --git a/app/components/SiteFooter.vue b/app/components/SiteFooter.vue
|
||||
index 8036a69..c5d528d 100644
|
||||
--- a/app/components/SiteFooter.vue
|
||||
+++ b/app/components/SiteFooter.vue
|
||||
@@ -96,7 +96,7 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
margin-top: 30px;
|
||||
- border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
+ border-top: 1px solid var(--border-primary);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.footer-item {
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -117,7 +117,7 @@ onMounted(() => {
|
||||
.footer-item:not(:last-child)::after {
|
||||
content: ' | ';
|
||||
margin: 0 10px;
|
||||
- color: rgba(255, 255, 255, 0.4);
|
||||
+ color: var(--text-quaternary);
|
||||
}
|
||||
|
||||
.footer-item a {
|
||||
@@ -127,7 +127,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.footer-item a:hover {
|
||||
- color: rgba(255, 255, 255, 0.8);
|
||||
+ color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.icp-link,
|
||||
@@ -139,7 +139,7 @@ onMounted(() => {
|
||||
|
||||
.icp-link:hover,
|
||||
.voicehub-link:hover {
|
||||
- color: rgba(255, 255, 255, 0.8);
|
||||
+ color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.netlify-badge {
|
||||
diff --git a/app/pages/index.vue b/app/pages/index.vue
|
||||
index 2a31f7e..e5bc5f6 100644
|
||||
--- a/app/pages/index.vue
|
||||
+++ b/app/pages/index.vue
|
||||
@@ -45,18 +45,6 @@
|
||||
<span class="utility-btn-label">主题</span>
|
||||
</button>
|
||||
|
||||
- <!-- 语言切换按钮(占位按钮,当前单语种) -->
|
||||
- <button
|
||||
- class="utility-btn lang-toggle-btn"
|
||||
- type="button"
|
||||
- aria-label="语言"
|
||||
- title="语言"
|
||||
- @click="handleLanguageClick"
|
||||
- >
|
||||
- <Icon name="globe" :size="18" />
|
||||
- <span class="utility-btn-label">语言</span>
|
||||
- </button>
|
||||
-
|
||||
<div v-if="isClientAuthenticated" class="user-info">
|
||||
<div class="user-details-desktop">
|
||||
<span class="user-name">{{ user?.name || '用户' }}</span>
|
||||
@@ -710,11 +698,6 @@ const { isDark, toggleTheme } = useTheme()
|
||||
const handleToggleTheme = () => {
|
||||
toggleTheme()
|
||||
}
|
||||
-const handleLanguageClick = () => {
|
||||
- if (typeof window !== 'undefined' && window.$showNotification) {
|
||||
- window.$showNotification('当前版本暂不支持多语言切换', 'info')
|
||||
- }
|
||||
-}
|
||||
|
||||
// 站点配置
|
||||
const {
|
||||
@@ -1750,15 +1733,15 @@ if (
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
-/* 通用工具按钮(主题/语言) */
|
||||
+/* 通用工具按钮(主题) */
|
||||
.utility-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 12px;
|
||||
- background: var(--utility-btn-bg, rgba(255, 255, 255, 0.06));
|
||||
- color: var(--utility-btn-color, rgba(255, 255, 255, 0.78));
|
||||
- border: 1px solid var(--utility-btn-border, rgba(255, 255, 255, 0.12));
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
+ color: var(--text-primary);
|
||||
+ border: 1px solid var(--border-primary);
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
@@ -1769,13 +1752,11 @@ if (
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
transform 0.15s ease;
|
||||
- backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.utility-btn:hover {
|
||||
- background: var(--utility-btn-hover-bg, rgba(255, 255, 255, 0.12));
|
||||
- color: var(--utility-btn-hover-color, #ffffff);
|
||||
- border-color: var(--utility-btn-hover-border, rgba(255, 255, 255, 0.22));
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ border-color: var(--border-secondary);
|
||||
}
|
||||
|
||||
.utility-btn:active {
|
||||
diff --git a/app/pages/login.vue b/app/pages/login.vue
|
||||
index f310bad..0ab59a2 100644
|
||||
--- a/app/pages/login.vue
|
||||
+++ b/app/pages/login.vue
|
||||
@@ -312,7 +312,7 @@ onMounted(async () => {
|
||||
|
||||
.icp-link:hover,
|
||||
.voicehub-link:hover {
|
||||
- color: rgba(255, 255, 255, 0.8);
|
||||
+ color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
From 80b7c256b5c9a39fee3b0bbc95e7f380342dc2b2 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 12:57:32 +0800
|
||||
Subject: [PATCH 49/58] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=20Run#53=20?=
|
||||
=?UTF-8?q?Preflight=20=E6=9E=84=E5=BB=BA=20Nitro=20=E6=97=B6=20OOM=20exit?=
|
||||
=?UTF-8?q?=20137?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- 将 NODE_OPTIONS max-old-space-size 从 4096 提升至 8192,并加入 preflight job 全局 env
|
||||
- 构建前打印系统内存状态,便于排障
|
||||
- 显式声明 NITRO_PRESET=node-server,避免构建阶段默认解析不确定性
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 11 +++++++++--
|
||||
1 file changed, 9 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 4b4df13..0b24984 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -64,6 +64,8 @@ jobs:
|
||||
NODE_MAJOR: "22"
|
||||
NODE_VERSION_FALLBACK: "22.13.0"
|
||||
NODE_MIRROR_PRIMARY: "https://registry.npmmirror.com/-/binary/node"
|
||||
+ NODE_OPTIONS: "--max-old-space-size=8192 --no-warnings --expose-gc"
|
||||
+ NITRO_PRESET: "node-server"
|
||||
steps:
|
||||
- name: 安装基础运行依赖(git/curl/ca-certificates/tar/xz-utils,Runner 自带)
|
||||
run: |
|
||||
@@ -194,8 +196,13 @@ jobs:
|
||||
- name: 执行 nuxt build
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
- NODE_OPTIONS="--max-old-space-size=4096" npm run build
|
||||
- echo ".output/ 生成结果:"
|
||||
+ echo "=== 构建前系统内存状态 ==="
|
||||
+ free -h || true
|
||||
+ echo "=== 开始执行 nuxt build ==="
|
||||
+ export NODE_OPTIONS="--max-old-space-size=8192 --no-warnings --expose-gc"
|
||||
+ export NITRO_PRESET=node-server
|
||||
+ npm run build
|
||||
+ echo "=== 构建完成,.output/ 生成结果 ==="
|
||||
ls -la .output/ 2>/dev/null || true
|
||||
du -sh .output 2>/dev/null || true
|
||||
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,869 @@
|
||||
From 2ee8056ad1c99b908ca4f03881232038cbcc4557 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 13:36:15 +0800
|
||||
Subject: [PATCH 50/58] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=E9=A6=96?=
|
||||
=?UTF-8?q?=E9=A1=B5=E5=BC=BA=E5=88=B6=E6=B5=85=E8=89=B2=E4=B8=8D=E7=94=9F?=
|
||||
=?UTF-8?q?=E6=95=88=E9=97=AE=E9=A2=98=EF=BC=9Aindex.vue=20=E7=A1=AC?=
|
||||
=?UTF-8?q?=E7=BC=96=E7=A0=81=E6=B7=B1=E8=89=B2=E6=A0=B7=E5=BC=8F=E5=8F=98?=
|
||||
=?UTF-8?q?=E9=87=8F=E5=8C=96?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- 根容器 .home 背景 #121318 深色硬编码改为 var(--bg-primary)
|
||||
- 全站文字 color:#ffffff / rgba(255,255,255,*) 按语义改为 var(--text-primary/secondary/tertiary/quaternary)
|
||||
- 选项卡、内容面板、下拉菜单、通知卡片、分页按钮等背景/边框统一变量化
|
||||
- 桌面端和移动端适配样式同步处理,彻底杜绝硬编码深色值绕过 CSS 变量
|
||||
---
|
||||
app/pages/index.vue | 298 ++++++++++++++++++++++----------------------
|
||||
1 file changed, 147 insertions(+), 151 deletions(-)
|
||||
|
||||
diff --git a/app/pages/index.vue b/app/pages/index.vue
|
||||
index e5bc5f6..9699dc7 100644
|
||||
--- a/app/pages/index.vue
|
||||
+++ b/app/pages/index.vue
|
||||
@@ -1633,9 +1633,9 @@ if (
|
||||
.home {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
- background-color: #121318;
|
||||
+ background-color: var(--bg-primary, #ffffff);
|
||||
padding: 1.5rem;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh; /* 确保至少占满视口 */
|
||||
@@ -1709,9 +1709,9 @@ if (
|
||||
height: 100px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
- rgba(255, 255, 255, 0.3),
|
||||
- rgba(255, 255, 255, 0.8),
|
||||
- rgba(255, 255, 255, 0.3)
|
||||
+ var(--border-secondary, #d1d5db),
|
||||
+ var(--border-tertiary, #9ca3af),
|
||||
+ var(--border-secondary, #d1d5db)
|
||||
);
|
||||
border-radius: 1px;
|
||||
}
|
||||
@@ -1795,9 +1795,9 @@ if (
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
- background: rgba(59, 130, 246, 0.15);
|
||||
- color: #60a5fa;
|
||||
- border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
+ background: var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
+ color: var(--primary, #1e40af);
|
||||
+ border: 1px solid var(--primary-border, rgba(30, 64, 175, 0.15));
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
@@ -1808,7 +1808,7 @@ if (
|
||||
}
|
||||
|
||||
.enter-admin-btn:hover {
|
||||
- background: rgba(59, 130, 246, 0.25);
|
||||
+ background: var(--primary-border, rgba(30, 64, 175, 0.15));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -1821,21 +1821,21 @@ if (
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
- background: rgba(255, 255, 255, 0.1);
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ color: var(--text-tertiary, #6b7280);
|
||||
border-radius: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.user-badge.admin {
|
||||
- background: rgba(59, 130, 246, 0.2);
|
||||
- color: #3b82f6;
|
||||
+ background: var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
+ color: var(--primary, #1e40af);
|
||||
}
|
||||
|
||||
.user-avatar-wrapper {
|
||||
@@ -1843,8 +1843,8 @@ if (
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
- background: #2a2a2a;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1866,12 +1866,12 @@ if (
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
right: 0;
|
||||
- background: #1a1a1f;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
+ background: var(--surface-primary, var(--bg-primary));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
min-width: 160px;
|
||||
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
+ box-shadow: var(--shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -1883,7 +1883,7 @@ if (
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
- color: rgba(255, 255, 255, 0.7);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
@@ -1893,13 +1893,13 @@ if (
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
- color: white;
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
}
|
||||
|
||||
.action-item.logout:hover {
|
||||
- background: rgba(239, 68, 68, 0.1);
|
||||
- color: #ef4444;
|
||||
+ background: var(--error-light, rgba(239, 68, 68, 0.1));
|
||||
+ color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
@@ -1909,8 +1909,8 @@ if (
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
- background-color: #f44336;
|
||||
- color: white;
|
||||
+ background-color: var(--error, #ef4444);
|
||||
+ color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
@@ -1928,12 +1928,12 @@ if (
|
||||
border-radius: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
- background: rgba(255, 255, 255, 0.04);
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
- color: #ffffff;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
|
||||
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
@@ -1941,23 +1941,23 @@ if (
|
||||
}
|
||||
|
||||
.login-options .login-btn:hover {
|
||||
- background: rgba(255, 255, 255, 0.08);
|
||||
- border-color: rgba(59, 130, 246, 0.5);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ border-color: var(--primary-border, rgba(30, 64, 175, 0.15));
|
||||
transform: translateY(-2px);
|
||||
- box-shadow: 0 8px 25px rgba(59, 130, 246, 0.2);
|
||||
- color: white;
|
||||
+ box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
}
|
||||
|
||||
.login-options .login-btn :deep(.icon),
|
||||
.login-options .login-btn i {
|
||||
- color: #3b82f6;
|
||||
+ color: var(--primary, #1e40af);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.login-options .login-btn:hover :deep(.icon),
|
||||
.login-options .login-btn:hover i {
|
||||
transform: scale(1.1);
|
||||
- filter: drop-shadow(0 0 5px rgba(59, 130, 246, 0.5));
|
||||
+ filter: drop-shadow(0 0 5px rgba(30, 64, 175, 0.3));
|
||||
}
|
||||
|
||||
.login-options .login-btn:active {
|
||||
@@ -1984,10 +1984,9 @@ if (
|
||||
font-weight: 800;
|
||||
font-size: 42px;
|
||||
letter-spacing: -0.02em;
|
||||
- background: linear-gradient(135deg, #ffffff 0%, rgba(255, 255, 255, 0.7) 100%);
|
||||
+ background: linear-gradient(135deg, var(--text-primary, #1a2332) 0%, var(--text-secondary, #4a5568) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
- text-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -1995,16 +1994,16 @@ if (
|
||||
.title-divider {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
- background: #0b5afe;
|
||||
+ background: var(--primary, #1e40af);
|
||||
border-radius: 2px;
|
||||
- box-shadow: 0 0 15px rgba(11, 90, 254, 0.6);
|
||||
+ box-shadow: 0 0 15px var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
- color: rgba(255, 255, 255, 0.4);
|
||||
+ color: var(--text-quaternary, #9ca3af);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -2036,22 +2035,22 @@ if (
|
||||
}
|
||||
|
||||
.section-tab {
|
||||
- background: #1a1b24;
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
border-radius: 15px 15px 0 0;
|
||||
padding: 15px 24px;
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
- color: rgba(255, 255, 255, 0.8);
|
||||
- border: 2px solid #282830;
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
+ border: 2px solid var(--border-primary, #e5e7eb);
|
||||
border-bottom: none;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.section-tab.active {
|
||||
- background: #21242d;
|
||||
- color: #ffffff;
|
||||
+ background: var(--surface-primary, var(--bg-primary));
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -2144,9 +2143,9 @@ if (
|
||||
right: -8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
- background: #0b5afe;
|
||||
+ background: var(--primary, #1e40af);
|
||||
border-radius: 50%;
|
||||
- box-shadow: 0 0 5px rgba(11, 90, 254, 0.5);
|
||||
+ box-shadow: 0 0 5px var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
}
|
||||
|
||||
.section-tab::after {
|
||||
@@ -2156,7 +2155,7 @@ if (
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
- background: #0b5afe;
|
||||
+ background: var(--primary, #1e40af);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
@@ -2174,19 +2173,19 @@ if (
|
||||
transform: none; /* 移除上浮效果 */
|
||||
background-color: transparent; /* 移除背景色 */
|
||||
box-shadow: none; /* 移除内阴影 */
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
}
|
||||
|
||||
.section-tab.active:hover {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
}
|
||||
|
||||
/* 内容容器 */
|
||||
.tab-content-container {
|
||||
- background: #1a1b24;
|
||||
- border: 2px solid #282830;
|
||||
+ background: var(--surface-primary, var(--bg-primary));
|
||||
+ border: 2px solid var(--border-primary, #e5e7eb);
|
||||
border-radius: 0 15px 15px 15px;
|
||||
padding: 1.5rem;
|
||||
margin-top: -2px; /* 使内容容器与标签连接 */
|
||||
@@ -2282,7 +2281,7 @@ if (
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
letter-spacing: 4%;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-tertiary, #6b7280);
|
||||
margin: 1.5rem 0 1rem;
|
||||
}
|
||||
|
||||
@@ -2302,7 +2301,7 @@ if (
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-tertiary, #6b7280);
|
||||
}
|
||||
|
||||
/* 下拉菜单动画 */
|
||||
@@ -2339,13 +2338,13 @@ if (
|
||||
align-items: center;
|
||||
padding: 1.25rem 0;
|
||||
margin-bottom: 1.5rem;
|
||||
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
+ border-bottom: 1px solid var(--border-primary, #e5e7eb);
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2353,9 +2352,9 @@ if (
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ color: var(--text-tertiary, #6b7280);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
@@ -2367,8 +2366,8 @@ if (
|
||||
}
|
||||
|
||||
.settings-icon:hover {
|
||||
- background-color: rgba(255, 255, 255, 0.1);
|
||||
- color: #ffffff;
|
||||
+ background-color: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
|
||||
@@ -2391,14 +2390,14 @@ if (
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 0;
|
||||
- color: rgba(255, 255, 255, 0.4);
|
||||
+ color: var(--text-quaternary, #9ca3af);
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
- background: rgba(255, 255, 255, 0.03);
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2409,8 +2408,8 @@ if (
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
- border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
- border-top-color: #3b82f6;
|
||||
+ border: 3px solid var(--border-secondary, #d1d5db);
|
||||
+ border-top-color: var(--primary, #1e40af);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@@ -2428,9 +2427,8 @@ if (
|
||||
}
|
||||
|
||||
.notification-card {
|
||||
- background: rgba(255, 255, 255, 0.03);
|
||||
- backdrop-filter: blur(20px);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
border-radius: 20px;
|
||||
padding: 1.25rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -2439,14 +2437,14 @@ if (
|
||||
}
|
||||
|
||||
.notification-card:hover {
|
||||
- background: rgba(255, 255, 255, 0.06);
|
||||
- border-color: rgba(255, 255, 255, 0.12);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ border-color: var(--border-secondary, #d1d5db);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.notification-card.unread {
|
||||
- background: rgba(59, 130, 246, 0.05);
|
||||
- border-color: rgba(59, 130, 246, 0.2);
|
||||
+ background: var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
+ border-color: var(--primary-border, rgba(30, 64, 175, 0.15));
|
||||
}
|
||||
|
||||
.notification-card-header {
|
||||
@@ -2461,8 +2459,8 @@ if (
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
- background: rgba(59, 130, 246, 0.1);
|
||||
- color: #3b82f6;
|
||||
+ background: var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
+ color: var(--primary, #1e40af);
|
||||
border-radius: 12px;
|
||||
margin-right: 1rem;
|
||||
flex-shrink: 0;
|
||||
@@ -2477,13 +2475,13 @@ if (
|
||||
.notification-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 0.75rem;
|
||||
- color: rgba(255, 255, 255, 0.4);
|
||||
+ color: var(--text-quaternary, #9ca3af);
|
||||
}
|
||||
|
||||
.notification-card-body {
|
||||
@@ -2491,7 +2489,7 @@ if (
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
- color: rgba(255, 255, 255, 0.7);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -2504,9 +2502,9 @@ if (
|
||||
}
|
||||
|
||||
.action-button.delete {
|
||||
- background: rgba(239, 68, 68, 0.05);
|
||||
- color: #ef4444;
|
||||
- border: 1px solid rgba(239, 68, 68, 0.1);
|
||||
+ background: var(--error-light, rgba(239, 68, 68, 0.1));
|
||||
+ color: var(--error, #ef4444);
|
||||
+ border: 1px solid var(--error-border, rgba(239, 68, 68, 0.2));
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
@@ -2517,22 +2515,22 @@ if (
|
||||
}
|
||||
|
||||
.action-button.delete:hover {
|
||||
- background: rgba(239, 68, 68, 0.1);
|
||||
- border-color: rgba(239, 68, 68, 0.2);
|
||||
+ background: rgba(239, 68, 68, 0.15);
|
||||
+ border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.notification-actions-bar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
- border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
+ border-top: 1px solid var(--border-primary, #e5e7eb);
|
||||
}
|
||||
|
||||
.action-button-large {
|
||||
flex: 1;
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- color: #ffffff;
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
padding: 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.875rem;
|
||||
@@ -2542,15 +2540,15 @@ if (
|
||||
}
|
||||
|
||||
.action-button-large:hover:not(.disabled) {
|
||||
- background: rgba(255, 255, 255, 0.1);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
.action-button-large.danger {
|
||||
- color: #ef4444;
|
||||
+ color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
.action-button-large.danger:hover {
|
||||
- background: rgba(239, 68, 68, 0.1);
|
||||
+ background: var(--error-light, rgba(239, 68, 68, 0.1));
|
||||
}
|
||||
|
||||
.action-button-large.disabled {
|
||||
@@ -2561,8 +2559,8 @@ if (
|
||||
/* 分页控件样式 */
|
||||
.notification-pagination {
|
||||
padding: 15px 0;
|
||||
- border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
+ border-top: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ border-bottom: 1px solid var(--border-primary, #e5e7eb);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
@@ -2571,7 +2569,7 @@ if (
|
||||
}
|
||||
|
||||
.pagination-text {
|
||||
- color: rgba(255, 255, 255, 0.7);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -2589,7 +2587,7 @@ if (
|
||||
}
|
||||
|
||||
.page-size-selector label {
|
||||
- color: rgba(255, 255, 255, 0.7);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -2605,9 +2603,9 @@ if (
|
||||
}
|
||||
|
||||
.page-nav-button {
|
||||
- background-color: rgba(255, 255, 255, 0.1);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
- color: var(--light);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
@@ -2620,8 +2618,8 @@ if (
|
||||
}
|
||||
|
||||
.page-nav-button:hover:not(:disabled) {
|
||||
- background-color: rgba(255, 255, 255, 0.2);
|
||||
- border-color: rgba(255, 255, 255, 0.3);
|
||||
+ background-color: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ border-color: var(--border-secondary, #d1d5db);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -2631,9 +2629,9 @@ if (
|
||||
}
|
||||
|
||||
.page-nav-button:disabled {
|
||||
- background-color: rgba(255, 255, 255, 0.05);
|
||||
- border-color: rgba(255, 255, 255, 0.1);
|
||||
- color: rgba(255, 255, 255, 0.3);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border-color: var(--border-primary, #e5e7eb);
|
||||
+ color: var(--text-disabled, #c5cbd3);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -2644,9 +2642,9 @@ if (
|
||||
}
|
||||
|
||||
.page-number-button {
|
||||
- background-color: rgba(255, 255, 255, 0.1);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
- color: var(--light);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
@@ -2662,8 +2660,8 @@ if (
|
||||
}
|
||||
|
||||
.page-number-button:hover:not(:disabled) {
|
||||
- background-color: rgba(255, 255, 255, 0.2);
|
||||
- border-color: rgba(255, 255, 255, 0.3);
|
||||
+ background-color: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ border-color: var(--border-secondary, #d1d5db);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -2675,23 +2673,23 @@ if (
|
||||
.page-number-button.active {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
- color: white;
|
||||
+ color: var(--btn-primary-text, #ffffff);
|
||||
}
|
||||
|
||||
.page-number-button.active:hover {
|
||||
- background-color: #0952e8;
|
||||
- border-color: #0952e8;
|
||||
+ background-color: var(--primary-hover, #1e3a8a);
|
||||
+ border-color: var(--primary-hover, #1e3a8a);
|
||||
}
|
||||
|
||||
.page-number-button:disabled {
|
||||
- background-color: rgba(255, 255, 255, 0.05);
|
||||
- border-color: rgba(255, 255, 255, 0.1);
|
||||
- color: rgba(255, 255, 255, 0.3);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border-color: var(--border-primary, #e5e7eb);
|
||||
+ color: var(--text-disabled, #c5cbd3);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-ellipsis {
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-tertiary, #6b7280);
|
||||
padding: 0 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
@@ -2703,7 +2701,7 @@ if (
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
- color: rgba(255, 255, 255, 0.7);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
font-size: 0.85rem;
|
||||
animation: fade-in 0.3s ease;
|
||||
}
|
||||
@@ -2711,7 +2709,7 @@ if (
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
- border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
+ border: 2px solid var(--border-secondary, #d1d5db);
|
||||
border-top: 2px solid var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||
@@ -2744,13 +2742,13 @@ if (
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 15px 0;
|
||||
- border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
+ border-top: 1px solid var(--border-primary, #e5e7eb);
|
||||
}
|
||||
|
||||
.action-button-large {
|
||||
- background-color: rgba(255, 255, 255, 0.1);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary));
|
||||
border: none;
|
||||
- color: var(--light);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
@@ -2759,26 +2757,26 @@ if (
|
||||
}
|
||||
|
||||
.action-button-large:hover {
|
||||
- background-color: rgba(255, 255, 255, 0.2);
|
||||
+ background-color: var(--surface-tertiary, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
.action-button-large.danger {
|
||||
- color: #ef4444;
|
||||
+ color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
.action-button-large.danger:hover {
|
||||
- background-color: rgba(239, 68, 68, 0.2);
|
||||
+ background-color: var(--error-light, rgba(239, 68, 68, 0.1));
|
||||
}
|
||||
|
||||
.action-button-large.disabled {
|
||||
- background-color: rgba(255, 255, 255, 0.05);
|
||||
- color: rgba(255, 255, 255, 0.3);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary));
|
||||
+ color: var(--text-disabled, #c5cbd3);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.action-button-large.disabled:hover {
|
||||
- background-color: rgba(255, 255, 255, 0.05);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary));
|
||||
}
|
||||
|
||||
/* ==================== 移动端设计 ==================== */
|
||||
@@ -2787,7 +2785,7 @@ if (
|
||||
@media (max-width: 768px) {
|
||||
.home {
|
||||
padding: 0;
|
||||
- background-color: #0a0a0f;
|
||||
+ background-color: var(--bg-primary, #ffffff);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@@ -2806,8 +2804,8 @@ if (
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
margin: 0;
|
||||
- background: linear-gradient(180deg, rgba(11, 90, 254, 0.08) 0%, transparent 100%);
|
||||
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
+ background: linear-gradient(180deg, var(--primary-light, rgba(30, 64, 175, 0.08)) 0%, transparent 100%);
|
||||
+ border-bottom: 1px solid var(--border-primary, #e5e7eb);
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
@@ -2827,7 +2825,7 @@ if (
|
||||
.logo-divider {
|
||||
height: 28px;
|
||||
width: 1px;
|
||||
- background: rgba(255, 255, 255, 0.15);
|
||||
+ background: var(--border-secondary, #d1d5db);
|
||||
}
|
||||
|
||||
/* 移动端的主页面里不需要写学校名,保持简洁 */
|
||||
@@ -2852,8 +2850,8 @@ if (
|
||||
.user-avatar-wrapper {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
- border-color: rgba(255, 255, 255, 0.1);
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
+ border-color: var(--border-primary, #e5e7eb);
|
||||
}
|
||||
|
||||
.user-avatar-placeholder {
|
||||
@@ -2880,23 +2878,23 @@ if (
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
- background: rgba(255, 255, 255, 0.08);
|
||||
+ background: var(--surface-secondary, var(--bg-secondary));
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
- color: white;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.login-options .login-btn :deep(.icon) {
|
||||
- color: #3b82f6;
|
||||
+ color: var(--primary, #1e40af);
|
||||
}
|
||||
|
||||
.login-options .login-btn:active {
|
||||
transform: scale(0.95);
|
||||
- background: rgba(255, 255, 255, 0.15);
|
||||
- border-color: rgba(59, 130, 246, 0.4);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary));
|
||||
+ border-color: var(--primary-border, rgba(30, 64, 175, 0.15));
|
||||
}
|
||||
|
||||
/* Footer 间距优化 */
|
||||
@@ -2925,13 +2923,13 @@ if (
|
||||
gap: 0;
|
||||
padding: 0 0.5rem;
|
||||
height: 64px;
|
||||
- background: rgba(28, 28, 30, 0.9);
|
||||
+ background: var(--surface-primary, var(--bg-primary));
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
border-radius: 9999px;
|
||||
z-index: 1000;
|
||||
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
|
||||
+ box-shadow: var(--shadow-xl, 0 20px 25px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.section-tab {
|
||||
@@ -2943,7 +2941,7 @@ if (
|
||||
padding: 0; /* 移除固定内边距,改用 flex 居中 */
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
- color: #71717a; /* text-zinc-500 */
|
||||
+ color: var(--text-tertiary, #6b7280);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -2968,15 +2966,14 @@ if (
|
||||
}
|
||||
|
||||
.section-tab.active {
|
||||
- color: #3b82f6 !important; /* text-blue-500 - Force blue */
|
||||
+ color: var(--primary, #1e40af) !important;
|
||||
background: transparent !important;
|
||||
transform: none !important;
|
||||
- text-shadow: 0 0 12px rgba(59, 130, 246, 0.6); /* Text Glow */
|
||||
}
|
||||
|
||||
/* Prevent hover from turning it white on mobile */
|
||||
.section-tab.active:hover {
|
||||
- color: #3b82f6 !important;
|
||||
+ color: var(--primary, #1e40af) !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
@@ -2985,7 +2982,6 @@ if (
|
||||
opacity: 1;
|
||||
color: currentColor;
|
||||
transform: none;
|
||||
- filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.5)); /* Icon Glow */
|
||||
}
|
||||
|
||||
.section-tab.active .tab-text {
|
||||
@@ -3034,10 +3030,10 @@ if (
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
- background: #0b5afe;
|
||||
+ background: var(--primary, #1e40af);
|
||||
border-radius: 50%;
|
||||
- border: 1.5px solid #0a0a0f;
|
||||
- box-shadow: 0 0 5px rgba(11, 90, 254, 0.4);
|
||||
+ border: 1.5px solid var(--surface-primary, var(--bg-primary));
|
||||
+ box-shadow: 0 0 5px var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -3096,8 +3092,8 @@ if (
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
- background: rgba(11, 90, 254, 0.15);
|
||||
- border: 1px solid rgba(11, 90, 254, 0.3);
|
||||
+ background: var(--primary-light, rgba(30, 64, 175, 0.08));
|
||||
+ border: 1px solid var(--primary-border, rgba(30, 64, 175, 0.15));
|
||||
}
|
||||
}
|
||||
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
From 7ee5e8b9e961b66bdd85a878407ac72f6b4ea1c9 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 13:59:14 +0800
|
||||
Subject: [PATCH 51/58] =?UTF-8?q?fix(ci):=20=E8=A7=A3=E5=86=B3=20Run#55=20?=
|
||||
=?UTF-8?q?Nitro=20=E5=86=8D=E6=AC=A1=20OOM=EF=BC=9A=E6=8F=90=E5=8D=87?=
|
||||
=?UTF-8?q?=E5=A0=86=E4=B8=8A=E9=99=90=E5=88=B0=2010G=20=E5=B9=B6=E7=A6=81?=
|
||||
=?UTF-8?q?=E7=94=A8=E5=8E=8B=E7=BC=A9/minify?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- NODE_OPTIONS max-old-space-size 8192 → 10240,新增 --gc-interval=100000 主动 GC
|
||||
- 构建前 drop_caches 清理页面缓存,降低系统级 RSS 压力
|
||||
- 禁用 NITRO_MINIFY 与 DISABLE_ROLLDOWN,进一步降低峰值内存
|
||||
- 构建前后打印内存快照,便于后续排障
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 11 +++++++++--
|
||||
1 file changed, 9 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 0b24984..793a110 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -64,8 +64,10 @@ jobs:
|
||||
NODE_MAJOR: "22"
|
||||
NODE_VERSION_FALLBACK: "22.13.0"
|
||||
NODE_MIRROR_PRIMARY: "https://registry.npmmirror.com/-/binary/node"
|
||||
- NODE_OPTIONS: "--max-old-space-size=8192 --no-warnings --expose-gc"
|
||||
+ NODE_OPTIONS: "--max-old-space-size=10240 --no-warnings --expose-gc --gc-interval=100000"
|
||||
NITRO_PRESET: "node-server"
|
||||
+ DISABLE_ROLLDOWN: "1"
|
||||
+ NITRO_MINIFY: "false"
|
||||
steps:
|
||||
- name: 安装基础运行依赖(git/curl/ca-certificates/tar/xz-utils,Runner 自带)
|
||||
run: |
|
||||
@@ -198,13 +200,18 @@ jobs:
|
||||
set -euxo pipefail
|
||||
echo "=== 构建前系统内存状态 ==="
|
||||
free -h || true
|
||||
+ sync && echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true
|
||||
echo "=== 开始执行 nuxt build ==="
|
||||
- export NODE_OPTIONS="--max-old-space-size=8192 --no-warnings --expose-gc"
|
||||
+ export NODE_OPTIONS="--max-old-space-size=10240 --no-warnings --expose-gc --gc-interval=100000"
|
||||
export NITRO_PRESET=node-server
|
||||
+ export NITRO_MINIFY=false
|
||||
+ export DISABLE_ROLLDOWN=1
|
||||
npm run build
|
||||
echo "=== 构建完成,.output/ 生成结果 ==="
|
||||
ls -la .output/ 2>/dev/null || true
|
||||
du -sh .output 2>/dev/null || true
|
||||
+ echo "=== 构建后内存状态 ==="
|
||||
+ free -h || true
|
||||
|
||||
build-image:
|
||||
name: 构建并推送容器镜像
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
From bf7c40c4fee2e25c1409d8cb83a7a4979189b572 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 14:04:27 +0800
|
||||
Subject: [PATCH 52/58] =?UTF-8?q?fix(ci):=20=E5=9B=9E=E6=BB=9A=20NODE=5FOP?=
|
||||
=?UTF-8?q?TIONS=20=E9=9D=9E=E6=B3=95=20--gc-interval=EF=BC=8C=E8=A7=A3?=
|
||||
=?UTF-8?q?=E5=86=B3=20Run#56=20=E7=AB=8B=E5=8D=B3=20exit=209?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- Node.js NODE_OPTIONS 不允许传 --gc-interval,导致 Run 56 刚启动 Node v0 检测时就 exit code 9 全局失败
|
||||
- 保留 max-old-space-size=10240、expose-gc、drop_caches、DISABLE_ROLLDOWN=1、NITRO_MINIFY=false 所有有效内存缓解措施
|
||||
---
|
||||
.gitea/workflows/deploy.yml | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
|
||||
index 793a110..5f46f56 100644
|
||||
--- a/.gitea/workflows/deploy.yml
|
||||
+++ b/.gitea/workflows/deploy.yml
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
NODE_MAJOR: "22"
|
||||
NODE_VERSION_FALLBACK: "22.13.0"
|
||||
NODE_MIRROR_PRIMARY: "https://registry.npmmirror.com/-/binary/node"
|
||||
- NODE_OPTIONS: "--max-old-space-size=10240 --no-warnings --expose-gc --gc-interval=100000"
|
||||
+ NODE_OPTIONS: "--max-old-space-size=10240 --no-warnings --expose-gc"
|
||||
NITRO_PRESET: "node-server"
|
||||
DISABLE_ROLLDOWN: "1"
|
||||
NITRO_MINIFY: "false"
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
free -h || true
|
||||
sync && echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true
|
||||
echo "=== 开始执行 nuxt build ==="
|
||||
- export NODE_OPTIONS="--max-old-space-size=10240 --no-warnings --expose-gc --gc-interval=100000"
|
||||
+ export NODE_OPTIONS="--max-old-space-size=10240 --no-warnings --expose-gc"
|
||||
export NITRO_PRESET=node-server
|
||||
export NITRO_MINIFY=false
|
||||
export DISABLE_ROLLDOWN=1
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
From b1c3af84de0aa9adaaba07544a917a9f7e7e3853 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 14:12:10 +0800
|
||||
Subject: [PATCH 53/58] =?UTF-8?q?fix(ui):=20=E8=A1=A5=E9=BD=90=E6=B5=85?=
|
||||
=?UTF-8?q?=E8=89=B2=E4=B8=BB=E9=A2=98=E9=81=97=E6=BC=8F=EF=BC=9Aindex=20?=
|
||||
=?UTF-8?q?=E5=90=8E=E6=AE=B5=E5=BC=B9=E7=AA=97=20+=20dashboard=20?=
|
||||
=?UTF-8?q?=E6=A0=B9=20+=20=E6=94=B9=E5=AF=86=E9=A1=B5=E6=A0=B9?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- index.vue 规则弹窗 modal-content/close/rule/ripple/login-required 全改为语义化 var()
|
||||
- index.vue 年度报告 year-review 卡片/按钮/brand/badge/title 全部变量化
|
||||
- dashboard.vue admin-layout 根 #0a0a0a/#fff 改为 var(--bg/text-*)
|
||||
- change-password.vue 改密页 auth-layout 容器、表单区、back-link 全改为浅色变量
|
||||
---
|
||||
app/pages/change-password.vue | 25 ++++-----
|
||||
app/pages/dashboard.vue | 8 +--
|
||||
app/pages/index.vue | 97 ++++++++++++++++++-----------------
|
||||
3 files changed, 66 insertions(+), 64 deletions(-)
|
||||
|
||||
diff --git a/app/pages/change-password.vue b/app/pages/change-password.vue
|
||||
index 119e978..506d5e6 100644
|
||||
--- a/app/pages/change-password.vue
|
||||
+++ b/app/pages/change-password.vue
|
||||
@@ -132,7 +132,7 @@ onMounted(async () => {
|
||||
<style scoped>
|
||||
.auth-layout {
|
||||
min-height: 100vh;
|
||||
- background: #0a0a0a;
|
||||
+ background: var(--bg-primary, #ffffff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -142,13 +142,14 @@ onMounted(async () => {
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
- background: #111111;
|
||||
+ background: var(--surface-primary, var(--bg-primary, #ffffff));
|
||||
border-radius: 24px;
|
||||
- border: 1px solid #1f1f1f;
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-height: 600px;
|
||||
+ box-shadow: 0 10px 40px -10px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.info-section {
|
||||
@@ -266,7 +267,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
- background: #111111;
|
||||
+ background: var(--surface-primary, var(--bg-primary, #ffffff));
|
||||
}
|
||||
|
||||
.form-container {
|
||||
@@ -285,13 +286,13 @@ onMounted(async () => {
|
||||
.form-header h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
font-size: 16px;
|
||||
- color: #888888;
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -305,10 +306,10 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
- background: #1a1a1a;
|
||||
- border: 1px solid #2a2a2a;
|
||||
+ background: var(--surface-secondary, var(--bg-secondary, #f7fafc));
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
border-radius: 8px;
|
||||
- color: #cccccc;
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
@@ -316,9 +317,9 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
- background: #2a2a2a;
|
||||
- color: #ffffff;
|
||||
- border-color: #3a3a3a;
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary, #edf2f7));
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
+ border-color: var(--border-secondary, #cbd5e1);
|
||||
}
|
||||
|
||||
.back-link svg {
|
||||
diff --git a/app/pages/dashboard.vue b/app/pages/dashboard.vue
|
||||
index 95ab7b8..fe6abeb 100644
|
||||
--- a/app/pages/dashboard.vue
|
||||
+++ b/app/pages/dashboard.vue
|
||||
@@ -548,8 +548,8 @@ onUnmounted(() => {
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
- background: #0a0a0a;
|
||||
- color: #ffffff;
|
||||
+ background: var(--bg-primary, #ffffff);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -561,11 +561,11 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
- background: #27272a;
|
||||
+ background: var(--border-primary, #cbd5e1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
- background: #3f3f46;
|
||||
+ background: var(--border-secondary, #94a3b8);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
diff --git a/app/pages/index.vue b/app/pages/index.vue
|
||||
index 9699dc7..8d450c0 100644
|
||||
--- a/app/pages/index.vue
|
||||
+++ b/app/pages/index.vue
|
||||
@@ -1912,7 +1912,7 @@ if (
|
||||
background-color: var(--error, #ef4444);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
- font-weight: bold;
|
||||
+ font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -3179,10 +3179,10 @@ if (
|
||||
|
||||
/* 弹窗内容 */
|
||||
.modal-content {
|
||||
- background: linear-gradient(135deg, #1a1b24 0%, #121318 100%);
|
||||
+ background: var(--surface-primary, var(--bg-primary, #ffffff));
|
||||
border-radius: 20px;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ box-shadow: var(--shadow-card, 0 25px 50px -12px rgba(0, 0, 0, 0.12));
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
@@ -3206,7 +3206,7 @@ if (
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 28px 16px;
|
||||
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
+ border-bottom: 1px solid var(--border-secondary, #f1f5f9);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
@@ -3217,7 +3217,7 @@ if (
|
||||
sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
margin: 0;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
@@ -3227,9 +3227,9 @@ if (
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
- background: rgba(255, 255, 255, 0.08);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary, #f1f5f9));
|
||||
border: none;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-tertiary, #64748b);
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
@@ -3240,8 +3240,8 @@ if (
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
- background: rgba(255, 255, 255, 0.15);
|
||||
- color: #ffffff;
|
||||
+ background: var(--border-secondary, #e2e8f0);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
@@ -3268,24 +3268,24 @@ if (
|
||||
gap: 0.5rem;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rules-icon {
|
||||
- color: #3b82f6;
|
||||
+ color: var(--primary, #0b5afe);
|
||||
}
|
||||
|
||||
.rules-text {
|
||||
font-size: 14px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.guidelines-content {
|
||||
font-size: 14px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -3298,31 +3298,31 @@ if (
|
||||
.rule-item {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rule-item span {
|
||||
margin-right: 0.5rem;
|
||||
- color: rgba(255, 255, 255, 0.3);
|
||||
+ color: var(--text-disabled, #94a3b8);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 年度报告弹窗 */
|
||||
.year-review-overlay {
|
||||
backdrop-filter: blur(8px);
|
||||
- background: rgba(0, 0, 0, 0.4);
|
||||
+ background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.year-review-card {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
- background: #12121a;
|
||||
+ background: var(--surface-primary, var(--bg-primary, #ffffff));
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ box-shadow: var(--shadow-card, 0 25px 50px -12px rgba(0, 0, 0, 0.12));
|
||||
animation: card-appear 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@@ -3339,7 +3339,7 @@ if (
|
||||
.card-pattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
- background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
||||
+ background-image: radial-gradient(rgba(15, 23, 42, 0.06) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -3351,9 +3351,9 @@ if (
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- color: rgba(255, 255, 255, 0.4);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary, #f1f5f9));
|
||||
+ border: 1px solid var(--border-secondary, #f1f5f9);
|
||||
+ color: var(--text-tertiary, #64748b);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -3363,8 +3363,8 @@ if (
|
||||
}
|
||||
|
||||
.card-close:hover {
|
||||
- background: rgba(255, 255, 255, 0.1);
|
||||
- color: #fff;
|
||||
+ background: var(--border-secondary, #e2e8f0);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
@@ -3379,9 +3379,9 @@ if (
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
- border: 1px solid rgba(139, 92, 246, 0.2);
|
||||
+ border: 1px solid rgba(139, 92, 246, 0.25);
|
||||
border-radius: 99px;
|
||||
- color: #a78bfa;
|
||||
+ color: #7c3aed;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
@@ -3405,8 +3405,8 @@ if (
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
- color: #fff;
|
||||
- box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.5);
|
||||
+ color: #ffffff;
|
||||
+ box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.35);
|
||||
z-index: 2;
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
@@ -3461,14 +3461,14 @@ if (
|
||||
.card-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
- color: #fff;
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 15px;
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
@@ -3482,10 +3482,10 @@ if (
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
- background: #fff;
|
||||
+ background: var(--primary, #0b5afe);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
- color: #000;
|
||||
+ color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
@@ -3498,8 +3498,8 @@ if (
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
- box-shadow: 0 10px 20px rgba(255, 255, 255, 0.1);
|
||||
- background: #f4f4f4;
|
||||
+ box-shadow: 0 10px 20px rgba(11, 90, 254, 0.2);
|
||||
+ background: var(--primary-hover, #0043f8);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
@@ -3510,9 +3510,9 @@ if (
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: transparent;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
border-radius: 16px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -3520,9 +3520,9 @@ if (
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
- color: #fff;
|
||||
- border-color: rgba(255, 255, 255, 0.2);
|
||||
+ background: var(--surface-tertiary, var(--bg-tertiary, #f1f5f9));
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
+ border-color: var(--border-secondary, #d1d5db);
|
||||
}
|
||||
|
||||
@keyframes card-appear {
|
||||
@@ -3564,7 +3564,7 @@ if (
|
||||
.ripple-effect {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
- background-color: rgba(255, 255, 255, 0.3);
|
||||
+ background-color: rgba(11, 90, 254, 0.25);
|
||||
transform: scale(0);
|
||||
animation: ripple 0.6s linear;
|
||||
pointer-events: none;
|
||||
@@ -3640,32 +3640,33 @@ if (
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
padding: 30px;
|
||||
- background-color: rgba(30, 41, 59, 0.5);
|
||||
+ background-color: var(--surface-secondary, var(--bg-secondary, #f7fafc));
|
||||
border-radius: 12px;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
||||
+ border: 1px solid var(--border-primary, #e5e7eb);
|
||||
+ box-shadow: var(--shadow-card, 0 5px 20px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 15px;
|
||||
+ color: var(--primary, #0b5afe);
|
||||
}
|
||||
|
||||
.login-required-content h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 10px;
|
||||
- color: var(--light);
|
||||
+ color: var(--text-primary, #1a2332);
|
||||
}
|
||||
|
||||
.login-required-content p {
|
||||
margin-bottom: 20px;
|
||||
- color: rgba(255, 255, 255, 0.7);
|
||||
+ color: var(--text-secondary, #4a5568);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
|
||||
border: none;
|
||||
- color: white;
|
||||
+ color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
From 8b0934f6f4d5571dc05867e751eaf25cbc738d3c Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 15:23:26 +0800
|
||||
Subject: [PATCH 54/58] =?UTF-8?q?fix(ui):=20=E5=BD=BB=E5=BA=95=E4=BF=AE?=
|
||||
=?UTF-8?q?=E5=A4=8D=E6=B7=B1=E6=B5=85=E8=89=B2=E4=B8=BB=E9=A2=98=E5=88=87?=
|
||||
=?UTF-8?q?=E6=8D=A2=E5=8F=8A=E5=86=85=E9=83=A8=E9=A1=B5=E9=9D=A2=E6=A0=B7?=
|
||||
=?UTF-8?q?=E5=BC=8F?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
- 恢复 variables.css 完整的 ClassicDark/ModernDark 等主题定义
|
||||
- 恢复 nuxt.config.ts 的主题系统逻辑(默认改为 ClassicLight,保留切换能力)
|
||||
- 修复 BackupManager / VotersModal / ProfileForm / LyricsModal / RequestForm / SongList / ScheduleList / StatsSlide / ArtistSlide 等数十处组件内部硬编码深色(#1a1a1a / #040e15 等)绕过主题的问题,全部改为使用 CSS 变量 (var(--bg-primary) 等)
|
||||
- 这样实现了:在浅色模式下,所有页面均正确显示浅色;在深色模式下,又能正常呈现深色设计,且恢复了右上角的深浅色切换能力
|
||||
---
|
||||
app/assets/css/variables.css | 94 +++++++++-------------
|
||||
app/components/Admin/BackupManager.vue | 8 +-
|
||||
app/components/Admin/VotersModal.vue | 4 +-
|
||||
app/components/Auth/ProfileForm.vue | 14 ++--
|
||||
app/components/Songs/RequestForm.vue | 12 +--
|
||||
app/components/Songs/ScheduleList.vue | 8 +-
|
||||
app/components/Songs/SongList.vue | 8 +-
|
||||
app/components/UI/LyricsModal.vue | 4 +-
|
||||
app/components/year-review/ArtistSlide.vue | 2 +-
|
||||
app/components/year-review/StatsSlide.vue | 2 +-
|
||||
nuxt.config.ts | 34 +++-----
|
||||
11 files changed, 79 insertions(+), 111 deletions(-)
|
||||
|
||||
diff --git a/app/assets/css/variables.css b/app/assets/css/variables.css
|
||||
index 88a6536..30dce2a 100644
|
||||
--- a/app/assets/css/variables.css
|
||||
+++ b/app/assets/css/variables.css
|
||||
@@ -1,13 +1,10 @@
|
||||
-/* 全局CSS变量 - 统一配色方案(默认浅色ClassicLight,不依赖data-theme也能正常显示) */
|
||||
+/* 全局CSS变量 - 统一配色方案 */
|
||||
:root {
|
||||
- /* 强制浅色基准值 - 即使 data-theme 缺失也保持浅色 */
|
||||
- color-scheme: light;
|
||||
-
|
||||
/* 主色调 */
|
||||
- --primary: #1e40af;
|
||||
- --primary-hover: #1e3a8a;
|
||||
- --primary-light: rgba(30, 64, 175, 0.08);
|
||||
- --primary-border: rgba(30, 64, 175, 0.15);
|
||||
+ --primary: #3b82f6;
|
||||
+ --primary-hover: #2563eb;
|
||||
+ --primary-light: rgba(59, 130, 246, 0.1);
|
||||
+ --primary-border: rgba(59, 130, 246, 0.2);
|
||||
|
||||
/* 成功色 */
|
||||
--success: #10b981;
|
||||
@@ -33,25 +30,25 @@
|
||||
--info-light: rgba(59, 130, 246, 0.1);
|
||||
--info-border: rgba(59, 130, 246, 0.2);
|
||||
|
||||
- /* 背景色 (默认 ClassicLight 浅色) */
|
||||
- --bg-primary: #ffffff;
|
||||
- --bg-secondary: #f5f7fa;
|
||||
- --bg-tertiary: #edf0f5;
|
||||
- --bg-quaternary: #d9dee7;
|
||||
- --bg-hover: #f0f3f8;
|
||||
-
|
||||
- /* 文字颜色 (默认 ClassicLight 浅色) */
|
||||
- --text-primary: #1a2332;
|
||||
- --text-secondary: #4a5568;
|
||||
- --text-tertiary: #6b7280;
|
||||
- --text-quaternary: #9ca3af;
|
||||
- --text-disabled: #c5cbd3;
|
||||
-
|
||||
- /* 边框颜色 (默认 ClassicLight 浅色) */
|
||||
- --border-primary: #e5e7eb;
|
||||
- --border-secondary: #d1d5db;
|
||||
- --border-tertiary: #9ca3af;
|
||||
- --border-quaternary: #6b7280;
|
||||
+ /* 背景色 */
|
||||
+ --bg-primary: #111111;
|
||||
+ --bg-secondary: #1a1a1a;
|
||||
+ --bg-tertiary: #2a2a2a;
|
||||
+ --bg-quaternary: #3a3a3a;
|
||||
+ --bg-hover: #1f1f1f;
|
||||
+
|
||||
+ /* 文字颜色 */
|
||||
+ --text-primary: #ffffff;
|
||||
+ --text-secondary: #cccccc;
|
||||
+ --text-tertiary: #888888;
|
||||
+ --text-quaternary: #666666;
|
||||
+ --text-disabled: #444444;
|
||||
+
|
||||
+ /* 边框颜色 */
|
||||
+ --border-primary: #1f1f1f;
|
||||
+ --border-secondary: #2a2a2a;
|
||||
+ --border-tertiary: #3a3a3a;
|
||||
+ --border-quaternary: #4a4a4a;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
@@ -135,16 +132,16 @@
|
||||
--card-hover-border: var(--border-tertiary);
|
||||
--card-shadow: var(--shadow-md);
|
||||
|
||||
- /* 按钮颜色 - 浅色基准 */
|
||||
+ /* 按钮颜色 */
|
||||
--btn-primary-bg: var(--primary);
|
||||
--btn-primary-hover: var(--primary-hover);
|
||||
- --btn-primary-text: #ffffff;
|
||||
+ --btn-primary-text: var(--text-primary);
|
||||
--btn-primary-border: var(--primary-border);
|
||||
|
||||
- --btn-secondary-bg: #f1f5f9;
|
||||
- --btn-secondary-hover: #e2e8f0;
|
||||
- --btn-secondary-text: #0f172a;
|
||||
- --btn-secondary-border: #cbd5e1;
|
||||
+ --btn-secondary-bg: #3a3a3a;
|
||||
+ --btn-secondary-hover: #4a4a4a;
|
||||
+ --btn-secondary-text: var(--text-primary);
|
||||
+ --btn-secondary-border: #555555;
|
||||
|
||||
--btn-success-bg: var(--success);
|
||||
--btn-success-hover: var(--success-hover);
|
||||
@@ -164,12 +161,12 @@
|
||||
--btn-disabled-bg: var(--bg-tertiary);
|
||||
--btn-disabled-text: var(--text-disabled);
|
||||
|
||||
- /* 输入框颜色 - 浅色基准 */
|
||||
- --input-bg: #ffffff;
|
||||
- --input-border: #d1d5db;
|
||||
+ /* 输入框颜色 */
|
||||
+ --input-bg: var(--bg-tertiary);
|
||||
+ --input-border: var(--border-tertiary);
|
||||
--input-border-focus: var(--primary);
|
||||
--input-text: var(--text-primary);
|
||||
- --input-placeholder: #9ca3af;
|
||||
+ --input-placeholder: var(--text-quaternary);
|
||||
--input-shadow-focus: 0 0 0 3px var(--primary-light);
|
||||
|
||||
/* 模态框颜色 */
|
||||
@@ -200,27 +197,12 @@
|
||||
--spinner-primary: var(--primary);
|
||||
--spinner-secondary: var(--bg-tertiary);
|
||||
|
||||
- /* 滚动条颜色 - 浅色基准 */
|
||||
- --scrollbar-track: #f1f5f9;
|
||||
- --scrollbar-thumb: #cbd5e1;
|
||||
- --scrollbar-thumb-hover: #94a3b8;
|
||||
-
|
||||
- /* Surface 兼容别名 - 部分按钮/容器使用 surface-xxx */
|
||||
- --surface-primary: var(--bg-primary);
|
||||
- --surface-secondary: var(--bg-secondary);
|
||||
- --surface-tertiary: var(--bg-tertiary);
|
||||
- --surface-quaternary: var(--bg-quaternary);
|
||||
-}
|
||||
-
|
||||
-/* 防深色兜底:覆盖 prefers-color-scheme: dark 对表单/根元素的自动影响 */
|
||||
-@media (prefers-color-scheme: dark) {
|
||||
- :root {
|
||||
- color-scheme: light;
|
||||
- }
|
||||
+ /* 滚动条颜色 */
|
||||
+ --scrollbar-track: var(--bg-tertiary);
|
||||
+ --scrollbar-thumb: var(--bg-quaternary);
|
||||
+ --scrollbar-thumb-hover: #4a4a4a;
|
||||
}
|
||||
|
||||
-/* 浅色主题叠加层(与 data-theme 无关,确保始终为浅色;保留 data-theme 变量覆盖以兼容切换) */
|
||||
-:root,
|
||||
:root[data-theme='light'],
|
||||
:root[data-theme='ClassicLight'],
|
||||
:root[data-theme='ModernLight'] {
|
||||
diff --git a/app/components/Admin/BackupManager.vue b/app/components/Admin/BackupManager.vue
|
||||
index cb9c00a..efe60d0 100644
|
||||
--- a/app/components/Admin/BackupManager.vue
|
||||
+++ b/app/components/Admin/BackupManager.vue
|
||||
@@ -520,8 +520,8 @@ const formatFileSize = (bytes) => {
|
||||
/* 主容器 */
|
||||
.backup-manager {
|
||||
min-height: 100vh;
|
||||
- background: #1a1a1a;
|
||||
- color: #e5e5e5;
|
||||
+ background: var(--bg-primary, #1a1a1a);
|
||||
+ color: var(--text-primary, #e5e5e5);
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
@@ -535,13 +535,13 @@ const formatFileSize = (bytes) => {
|
||||
.header h3 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
- color: #ffffff;
|
||||
+ color: var(--text-primary, #ffffff);
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.header .description {
|
||||
font-size: 1.125rem;
|
||||
- color: #a0a0a0;
|
||||
+ color: var(--text-secondary, #a0a0a0);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
diff --git a/app/components/Admin/VotersModal.vue b/app/components/Admin/VotersModal.vue
|
||||
index c14b029..d7730f9 100644
|
||||
--- a/app/components/Admin/VotersModal.vue
|
||||
+++ b/app/components/Admin/VotersModal.vue
|
||||
@@ -202,9 +202,9 @@ watch(
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
- background: #1a1a1a;
|
||||
+ background: var(--surface-primary, var(--bg-primary, #1a1a1a));
|
||||
border-radius: 16px;
|
||||
- border: 1px solid #2a2a2a;
|
||||
+ border: 1px solid var(--border-primary, #2a2a2a);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
diff --git a/app/components/Auth/ProfileForm.vue b/app/components/Auth/ProfileForm.vue
|
||||
index 3c23d2d..1403f16 100644
|
||||
--- a/app/components/Auth/ProfileForm.vue
|
||||
+++ b/app/components/Auth/ProfileForm.vue
|
||||
@@ -224,24 +224,24 @@ const handleUpdateProfile = async () => {
|
||||
<style scoped>
|
||||
.custom-select-profile :deep(> .flex) {
|
||||
padding: 10px 16px;
|
||||
- background-color: #09090b;
|
||||
- border: 1px solid #27272a;
|
||||
+ background-color: var(--input-bg, #09090b);
|
||||
+ border: 1px solid var(--input-border, #27272a);
|
||||
border-radius: 0.75rem;
|
||||
- color: #f4f4f5;
|
||||
+ color: var(--input-text, #f4f4f5);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.custom-select-profile :deep(> .flex:hover) {
|
||||
- border-color: #3f3f46;
|
||||
+ border-color: var(--input-border-focus, #3f3f46);
|
||||
}
|
||||
|
||||
.custom-select-profile :deep(> .border-blue-500\/50) {
|
||||
- border-color: rgba(34, 197, 94, 0.5) !important;
|
||||
- --tw-ring-color: rgba(34, 197, 94, 0.5);
|
||||
+ border-color: var(--primary, rgba(34, 197, 94, 0.5)) !important;
|
||||
+ --tw-ring-color: var(--primary-light, rgba(34, 197, 94, 0.5));
|
||||
box-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
- background-color: #09090b !important;
|
||||
+ background-color: var(--input-bg, #09090b) !important;
|
||||
}
|
||||
|
||||
.custom-select-profile :deep(.text-\[11px\]) {
|
||||
diff --git a/app/components/Songs/RequestForm.vue b/app/components/Songs/RequestForm.vue
|
||||
index d28fc42..538f4f5 100644
|
||||
--- a/app/components/Songs/RequestForm.vue
|
||||
+++ b/app/components/Songs/RequestForm.vue
|
||||
@@ -3194,14 +3194,14 @@ defineExpose({
|
||||
}
|
||||
|
||||
.search-input {
|
||||
- background: #040e15;
|
||||
- border: 1px solid #242f38;
|
||||
+ background: var(--input-bg, #040e15);
|
||||
+ border: 1px solid var(--input-border, #242f38);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 0.85rem;
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--input-text, rgba(255, 255, 255, 0.6));
|
||||
flex: 1;
|
||||
min-width: 100px; /* 确保输入框不会缩到太小 */
|
||||
}
|
||||
@@ -3484,14 +3484,14 @@ defineExpose({
|
||||
|
||||
.form-input,
|
||||
.form-select {
|
||||
- background: #040e15;
|
||||
- border: 1px solid #242f38;
|
||||
+ background: var(--input-bg, #040e15);
|
||||
+ border: 1px solid var(--input-border, #242f38);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--input-text, rgba(255, 255, 255, 0.6));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
diff --git a/app/components/Songs/ScheduleList.vue b/app/components/Songs/ScheduleList.vue
|
||||
index ffb3b5e..7df22f8 100644
|
||||
--- a/app/components/Songs/ScheduleList.vue
|
||||
+++ b/app/components/Songs/ScheduleList.vue
|
||||
@@ -2188,9 +2188,9 @@ const vRipple = {
|
||||
.custom-select:focus,
|
||||
.custom-input:focus {
|
||||
outline: none;
|
||||
- border-color: var(--primary);
|
||||
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
- background: #1a1a20;
|
||||
+ border-color: var(--input-border-focus, var(--primary));
|
||||
+ box-shadow: var(--input-shadow-focus, 0 0 0 3px rgba(102, 126, 234, 0.1));
|
||||
+ background: var(--input-bg, #1a1a20);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@@ -3168,7 +3168,7 @@ const vRipple = {
|
||||
|
||||
/* 日期选择弹窗 */
|
||||
.date-picker-content {
|
||||
- background: #1a1a1f;
|
||||
+ background: var(--surface-primary, var(--bg-primary, #1a1a1f));
|
||||
border-radius: 20px 20px 0 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
diff --git a/app/components/Songs/SongList.vue b/app/components/Songs/SongList.vue
|
||||
index 7478f2d..f6134f6 100644
|
||||
--- a/app/components/Songs/SongList.vue
|
||||
+++ b/app/components/Songs/SongList.vue
|
||||
@@ -1894,14 +1894,14 @@ const vRipple = {
|
||||
}
|
||||
|
||||
.search-input {
|
||||
- background: #040e15;
|
||||
- border: 1px solid #242f38;
|
||||
+ background: var(--input-bg, #040e15);
|
||||
+ border: 1px solid var(--input-border, #242f38);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
padding-right: 2.5rem;
|
||||
font-family: 'MiSans-Demibold', sans-serif;
|
||||
font-size: 14px;
|
||||
- color: rgba(255, 255, 255, 0.8);
|
||||
+ color: var(--input-text, rgba(255, 255, 255, 0.8));
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -2593,7 +2593,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.semester-dropdown {
|
||||
- background: #1a1a1f;
|
||||
+ background: var(--surface-primary, var(--bg-primary, #1a1a1f));
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
||||
diff --git a/app/components/UI/LyricsModal.vue b/app/components/UI/LyricsModal.vue
|
||||
index 3451ee1..25c07ac 100644
|
||||
--- a/app/components/UI/LyricsModal.vue
|
||||
+++ b/app/components/UI/LyricsModal.vue
|
||||
@@ -953,8 +953,8 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
- background: #000000;
|
||||
- color: #ffffff;
|
||||
+ background: var(--bg-primary, #000000);
|
||||
+ color: var(--text-primary, #ffffff);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
diff --git a/app/components/year-review/ArtistSlide.vue b/app/components/year-review/ArtistSlide.vue
|
||||
index 27d56f1..41a2cd4 100644
|
||||
--- a/app/components/year-review/ArtistSlide.vue
|
||||
+++ b/app/components/year-review/ArtistSlide.vue
|
||||
@@ -37,7 +37,7 @@ defineProps({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
- background-color: #1a0b2e;
|
||||
+ background-color: var(--surface-primary, var(--bg-primary, #1a0b2e));
|
||||
}
|
||||
|
||||
.bg-overlay {
|
||||
diff --git a/app/components/year-review/StatsSlide.vue b/app/components/year-review/StatsSlide.vue
|
||||
index 9b474f1..5ab8c2e 100644
|
||||
--- a/app/components/year-review/StatsSlide.vue
|
||||
+++ b/app/components/year-review/StatsSlide.vue
|
||||
@@ -43,7 +43,7 @@ defineProps({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
- background-color: #030712; /* gray-950 */
|
||||
+ background-color: var(--surface-primary, var(--bg-primary, #030712));
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
diff --git a/nuxt.config.ts b/nuxt.config.ts
|
||||
index 6964a0d..d5ff5f3 100644
|
||||
--- a/nuxt.config.ts
|
||||
+++ b/nuxt.config.ts
|
||||
@@ -194,10 +194,6 @@ export default defineNuxtConfig({
|
||||
name: 'x-default-theme',
|
||||
content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'ClassicLight'
|
||||
},
|
||||
- {
|
||||
- name: 'x-enabled-themes',
|
||||
- content: process.env.NUXT_PUBLIC_ENABLED_THEMES || 'ClassicLight,ModernLight'
|
||||
- },
|
||||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
|
||||
{ name: 'apple-mobile-web-app-title', content: 'VoiceHub管理' },
|
||||
@@ -210,28 +206,18 @@ export default defineNuxtConfig({
|
||||
tagPosition: 'head',
|
||||
children: [
|
||||
`(function(){try{`,
|
||||
- ` var k='voicehub:theme', d=document, r=d.documentElement, s=null, eff=null, def=null, list=[];`,
|
||||
- ` try{`,
|
||||
- ` var mt=d.querySelector('meta[name="x-enabled-themes"]');`,
|
||||
- ` if(mt && mt.content){list=String(mt.content).split(',').map(function(x){return x.replace(/^\\s+|\\s+$/g,'')}).filter(Boolean)}`,
|
||||
- ` }catch(e){list=[]}`,
|
||||
- ` try{`,
|
||||
- ` var md=d.querySelector('meta[name="x-default-theme"]');`,
|
||||
- ` if(md && md.content){def=md.content}`,
|
||||
- ` }catch(e){def=null}`,
|
||||
- ` if(!def){def='ClassicLight'}`,
|
||||
- ` if(list.length===0){list=['ClassicLight','ModernLight']}`,
|
||||
- ` function valid(t){`,
|
||||
- ` if(list.indexOf(t)!==-1){return t}`,
|
||||
- ` if(list.indexOf(def)!==-1){return def}`,
|
||||
- ` return list[0]`,
|
||||
- ` }`,
|
||||
+ ` var k='voicehub:theme', d=document, r=d.documentElement, s=null, eff=null;`,
|
||||
` try{s=localStorage.getItem(k)}catch(e){s=null}`,
|
||||
- ` if(!s){s=def}`,
|
||||
- ` else{s=valid(s)}`,
|
||||
- ` if(typeof s==='string' && (s==='System'||s==='system')){eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`,
|
||||
+ ` if(!s){`,
|
||||
+ ` try{`,
|
||||
+ ` var meta=document.querySelector('meta[name="x-default-theme"]');`,
|
||||
+ ` if(meta && meta.content){s=meta.content}`,
|
||||
+ ` }catch(e){s=null}`,
|
||||
+ ` if(!s){s='ClassicLight'}`,
|
||||
+ ` }`,
|
||||
+ ` if(s==='System'||s==='system'){eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`,
|
||||
` else if(typeof s==='string'){eff=s.endsWith('Dark')||s==='dark'?'dark':(s.endsWith('Light')||s==='light'?'light':s)}`,
|
||||
- ` else{eff='light'}`,
|
||||
+ ` else{eff='dark'}`,
|
||||
` r.setAttribute('data-theme', (typeof s==='string'?s:eff));`,
|
||||
` if(eff==='dark'){r.classList.add('dark'); r.classList.remove('light'); r.style.colorScheme='dark'}`,
|
||||
` else if(eff==='light'){r.classList.add('light'); r.classList.remove('dark'); r.style.colorScheme='light'}`,
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,913 @@
|
||||
From 78c3d878eac42aaa3ee53cfd6b03791756c10d18 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 16:56:22 +0800
|
||||
Subject: [PATCH 57/58] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=E6=B5=85?=
|
||||
=?UTF-8?q?=E8=89=B2=E4=B8=BB=E9=A2=98=E4=B8=8B=E7=9A=84=20Tailwind=20CSS?=
|
||||
=?UTF-8?q?=20=E5=8F=8A=E9=83=A8=E5=88=86=E6=AE=8B=E7=95=99=E7=A1=AC?=
|
||||
=?UTF-8?q?=E7=BC=96=E7=A0=81?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
app/components/Songs/RequestForm.vue | 86 +++++++++++-----------
|
||||
app/components/Songs/ScheduleList.vue | 60 +++++++--------
|
||||
app/components/UserTutorial.vue | 42 +++++------
|
||||
fix-more-colors.cjs | 102 ++++++++++++++++++++++++++
|
||||
tailwind.config.ts | 7 ++
|
||||
5 files changed, 203 insertions(+), 94 deletions(-)
|
||||
create mode 100644 fix-more-colors.cjs
|
||||
|
||||
diff --git a/app/components/Songs/RequestForm.vue b/app/components/Songs/RequestForm.vue
|
||||
index 8c494ae..5be91bd 100644
|
||||
--- a/app/components/Songs/RequestForm.vue
|
||||
+++ b/app/components/Songs/RequestForm.vue
|
||||
@@ -2944,7 +2944,7 @@ defineExpose({
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.04em;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -2968,7 +2968,7 @@ defineExpose({
|
||||
gap: 0.5rem;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -2980,7 +2980,7 @@ defineExpose({
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
@@ -2990,7 +2990,7 @@ defineExpose({
|
||||
|
||||
.rule-item span {
|
||||
margin-right: 0.5rem;
|
||||
- color: rgba(255, 255, 255, 0.3);
|
||||
+ color: var(--text-disabled);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -3025,7 +3025,7 @@ defineExpose({
|
||||
gap: 2px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
+ border: 1px solid var(--border-tertiary);
|
||||
background: rgba(6, 15, 22, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
min-width: 140px;
|
||||
@@ -3066,7 +3066,7 @@ defineExpose({
|
||||
.badge-title {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary);
|
||||
}
|
||||
|
||||
.submission-status-badge.open .badge-title {
|
||||
@@ -3080,7 +3080,7 @@ defineExpose({
|
||||
.badge-subtitle {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.login-required-hint {
|
||||
@@ -3136,7 +3136,7 @@ defineExpose({
|
||||
min-width: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 10px;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
+ border: 1px solid var(--border-primary);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-family: 'MiSans', sans-serif;
|
||||
@@ -3277,7 +3277,7 @@ defineExpose({
|
||||
.remove-collaborator {
|
||||
background: none;
|
||||
border: none;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3292,7 +3292,7 @@ defineExpose({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0.75rem;
|
||||
@@ -3474,7 +3474,7 @@ defineExpose({
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.02em;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -3606,7 +3606,7 @@ defineExpose({
|
||||
gap: 0.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-tertiary);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
@@ -3628,7 +3628,7 @@ defineExpose({
|
||||
.import-btn {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
+ border: 1px solid var(--border-tertiary);
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 7px;
|
||||
font-size: 12px;
|
||||
@@ -3676,7 +3676,7 @@ defineExpose({
|
||||
|
||||
.user-name {
|
||||
font-size: 12px;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -3712,10 +3712,10 @@ defineExpose({
|
||||
|
||||
.search-type-switch {
|
||||
display: flex;
|
||||
- background: rgba(0, 0, 0, 0.2);
|
||||
+ background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
+ border: 1px solid var(--border-secondary);
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
@@ -3757,18 +3757,18 @@ defineExpose({
|
||||
|
||||
.logout-btn:hover {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.platform-btn {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
+ border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 0.45rem 0.85rem;
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
@@ -3787,7 +3787,7 @@ defineExpose({
|
||||
|
||||
/* 音源状态显示 */
|
||||
.source-status-display {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
@@ -3805,13 +3805,13 @@ defineExpose({
|
||||
.status-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary);
|
||||
font-family: 'MiSans', sans-serif;
|
||||
}
|
||||
|
||||
.status-summary {
|
||||
font-size: 12px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
@@ -3954,7 +3954,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
@@ -4037,7 +4037,7 @@ defineExpose({
|
||||
|
||||
.empty-hint,
|
||||
.initial-hint {
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -4222,7 +4222,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.alert-hint {
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
@@ -4385,7 +4385,7 @@ defineExpose({
|
||||
align-items: center;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
- background: rgba(255, 255, 255, 0.02);
|
||||
+ background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
@@ -4449,7 +4449,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
@@ -4516,7 +4516,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.result-cover {
|
||||
@@ -4625,7 +4625,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.result-artist {
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
margin: 0.25rem 0;
|
||||
white-space: nowrap;
|
||||
@@ -4693,7 +4693,7 @@ defineExpose({
|
||||
|
||||
.similar-text {
|
||||
font-size: 12px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -4787,9 +4787,9 @@ defineExpose({
|
||||
}
|
||||
|
||||
.replay-btn:disabled {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
- color: rgba(255, 255, 255, 0.3);
|
||||
+ color: var(--text-disabled);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
@@ -4957,14 +4957,14 @@ defineExpose({
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(20px);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
+ border: 1px solid var(--border-tertiary);
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.rules-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary);
|
||||
margin-bottom: 1.25rem;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
@@ -5009,9 +5009,9 @@ defineExpose({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
- background: rgba(255, 255, 255, 0.02);
|
||||
+ background: var(--bg-secondary);
|
||||
backdrop-filter: blur(20px);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
+ border: 1px solid var(--border-tertiary);
|
||||
border-radius: 20px;
|
||||
/* 允许容器内容触发页面滚动 */
|
||||
touch-action: pan-y;
|
||||
@@ -5103,7 +5103,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.search-input {
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
@@ -5128,7 +5128,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.platform-selection {
|
||||
- background: rgba(0, 0, 0, 0.2);
|
||||
+ background: var(--bg-primary);
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -5143,7 +5143,7 @@ defineExpose({
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-tertiary);
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
@@ -5471,7 +5471,7 @@ defineExpose({
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
+ border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
@@ -5520,7 +5520,7 @@ defineExpose({
|
||||
.video-author {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-size: 14px;
|
||||
- color: rgba(255, 255, 255, 0.6);
|
||||
+ color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.episodes-list {
|
||||
@@ -5535,7 +5535,7 @@ defineExpose({
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
- background: rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
@@ -5577,6 +5577,6 @@ defineExpose({
|
||||
.episode-duration {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-size: 12px;
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-tertiary);
|
||||
}
|
||||
</style>
|
||||
diff --git a/app/components/Songs/ScheduleList.vue b/app/components/Songs/ScheduleList.vue
|
||||
index 9ac7c4f..1d56e3c 100644
|
||||
--- a/app/components/Songs/ScheduleList.vue
|
||||
+++ b/app/components/Songs/ScheduleList.vue
|
||||
@@ -106,7 +106,7 @@
|
||||
<div v-else-if="!schedules || schedules.length === 0" key="empty-all" class="empty">
|
||||
<div class="icon mb-4">🎵</div>
|
||||
<p>暂无排期信息</p>
|
||||
- <p class="text-sm text-gray">点歌后等待管理员安排播出时间</p>
|
||||
+ <p class="text-sm text-text-secondary">点歌后等待管理员安排播出时间</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentDateSchedules.length === 0" key="empty-date" class="empty">
|
||||
@@ -1600,15 +1600,15 @@ const vRipple = {
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
- background: linear-gradient(135deg, rgba(11, 90, 254, 0.1) 0%, rgba(33, 36, 45, 0.9) 100%);
|
||||
+ background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
- border: 1px solid rgba(11, 90, 254, 0.2);
|
||||
+ border: 1px solid var(--border-secondary);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.semester-label {
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
margin-right: 0.75rem;
|
||||
@@ -1617,10 +1617,10 @@ const vRipple = {
|
||||
}
|
||||
|
||||
.semester-select {
|
||||
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
+ background: var(--bg-tertiary);
|
||||
+ border: 1px solid var(--border-tertiary);
|
||||
border-radius: 8px;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
@@ -1649,7 +1649,7 @@ const vRipple = {
|
||||
|
||||
.semester-select option {
|
||||
background: var(--bg-secondary, #1a1d24);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -1710,7 +1710,7 @@ const vRipple = {
|
||||
|
||||
.date-item.active {
|
||||
background: var(--bg-tertiary, #21242d);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(11, 90, 254, 0.2);
|
||||
@@ -1775,7 +1775,7 @@ const vRipple = {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1886,7 +1886,7 @@ const vRipple = {
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
@@ -1964,7 +1964,7 @@ const vRipple = {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@@ -1984,7 +1984,7 @@ const vRipple = {
|
||||
|
||||
.playlist-modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
@@ -2087,7 +2087,7 @@ const vRipple = {
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@@ -2163,7 +2163,7 @@ const vRipple = {
|
||||
width: 100%;
|
||||
background: var(--bg-secondary, #141418);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
@@ -2210,7 +2210,7 @@ const vRipple = {
|
||||
|
||||
.btn-icon:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-icon:disabled {
|
||||
@@ -2421,7 +2421,7 @@ const vRipple = {
|
||||
|
||||
.song-name {
|
||||
font-size: 14px;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -2469,7 +2469,7 @@ const vRipple = {
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
@@ -2488,7 +2488,7 @@ const vRipple = {
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
@@ -2502,7 +2502,7 @@ const vRipple = {
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@@ -2537,7 +2537,7 @@ const vRipple = {
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
@@ -2662,7 +2662,7 @@ const vRipple = {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
font-family: 'MiSans', sans-serif;
|
||||
@@ -2725,7 +2725,7 @@ const vRipple = {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.04em;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -2824,7 +2824,7 @@ const vRipple = {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@@ -2919,7 +2919,7 @@ const vRipple = {
|
||||
.date-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
text-shadow:
|
||||
0 0 10px rgba(255, 255, 255, 0.2),
|
||||
@@ -2928,7 +2928,7 @@ const vRipple = {
|
||||
|
||||
.dropdown-icon {
|
||||
opacity: 0.5;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
}
|
||||
|
||||
.mobile-action-btn {
|
||||
@@ -3118,7 +3118,7 @@ const vRipple = {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.4;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
@@ -3378,8 +3378,8 @@ const vRipple = {
|
||||
|
||||
.date-nav-btn {
|
||||
background: rgba(11, 90, 254, 0.1);
|
||||
- border: 1px solid rgba(11, 90, 254, 0.2);
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ border: 1px solid var(--border-secondary);
|
||||
+ color: var(--text-primary);
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -3403,7 +3403,7 @@ const vRipple = {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
- color: var(--text-primary, #ffffff);
|
||||
+ color: var(--text-primary);
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
white-space: pre-line;
|
||||
diff --git a/app/components/UserTutorial.vue b/app/components/UserTutorial.vue
|
||||
index c42bed2..97f0d43 100644
|
||||
--- a/app/components/UserTutorial.vue
|
||||
+++ b/app/components/UserTutorial.vue
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="user-tutorial-container relative">
|
||||
<div class="tutorial-header flex justify-between items-start">
|
||||
<div>
|
||||
- <h3 class="text-lg font-black text-zinc-100 flex items-center gap-2">
|
||||
- <BookOpen class="w-5 h-5 text-blue-400" />
|
||||
+ <h3 class="text-lg font-black text-text-primary flex items-center gap-2">
|
||||
+ <BookOpen class="w-5 h-5 text-primary" />
|
||||
使用指南
|
||||
</h3>
|
||||
- <p class="text-xs text-zinc-500 mt-1">快速了解如何使用 VoiceHub 校园广播系统</p>
|
||||
+ <p class="text-xs text-text-secondary mt-1">快速了解如何使用 VoiceHub 校园广播系统</p>
|
||||
</div>
|
||||
<button
|
||||
class="start-tour-btn"
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||
<div class="tutorial-card">
|
||||
- <div class="card-icon bg-blue-500/10 text-blue-400">
|
||||
+ <div class="card-icon bg-primary/10 text-primary">
|
||||
<Search class="w-6 h-6" />
|
||||
</div>
|
||||
<h4 class="card-title">1. 搜索与投稿</h4>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
|
||||
<div class="tutorial-card">
|
||||
- <div class="card-icon bg-emerald-500/10 text-emerald-400">
|
||||
+ <div class="card-icon bg-success/10 text-success">
|
||||
<ThumbsUp class="w-6 h-6" />
|
||||
</div>
|
||||
<h4 class="card-title">2. 互动与点赞</h4>
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
|
||||
<div class="tutorial-card">
|
||||
- <div class="card-icon bg-purple-500/10 text-purple-400">
|
||||
+ <div class="card-icon bg-[#a855f7]/10 text-[#a855f7]">
|
||||
<CalendarDays class="w-6 h-6" />
|
||||
</div>
|
||||
<h4 class="card-title">3. 关注排期与通知</h4>
|
||||
@@ -276,25 +276,25 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.user-tutorial-container {
|
||||
- background: rgba(0, 0, 0, 0.4);
|
||||
+ background: var(--bg-secondary, rgba(0, 0, 0, 0.4));
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
- border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
+ border: 1px solid var(--border-secondary, rgba(255, 255, 255, 0.05));
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.tutorial-card {
|
||||
- background: rgba(255, 255, 255, 0.02);
|
||||
- border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
+ background: var(--bg-tertiary, rgba(255, 255, 255, 0.02));
|
||||
+ border: 1px solid var(--border-secondary, rgba(255, 255, 255, 0.05));
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tutorial-card:hover {
|
||||
- background: rgba(255, 255, 255, 0.04);
|
||||
- border-color: rgba(255, 255, 255, 0.1);
|
||||
+ background: var(--bg-hover, rgba(255, 255, 255, 0.04));
|
||||
+ border-color: var(--border-tertiary, rgba(255, 255, 255, 0.1));
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@@ -312,14 +312,14 @@ onMounted(() => {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 0.95rem;
|
||||
- color: rgba(255, 255, 255, 0.9);
|
||||
+ color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-family: 'MiSans', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
- color: rgba(255, 255, 255, 0.5);
|
||||
+ color: var(--text-secondary, rgba(255, 255, 255, 0.5));
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -387,14 +387,14 @@ onMounted(() => {
|
||||
.tour-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
- color: #f4f4f5;
|
||||
+ color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tour-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
- color: #71717a;
|
||||
+ color: var(--text-tertiary);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
@@ -403,11 +403,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.tour-close-btn:hover {
|
||||
- color: #f4f4f5;
|
||||
+ color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tour-body p {
|
||||
- color: #a1a1aa;
|
||||
+ color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 1rem 0;
|
||||
@@ -420,7 +420,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.tour-progress {
|
||||
- color: #71717a;
|
||||
+ color: var(--text-tertiary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -441,7 +441,7 @@ onMounted(() => {
|
||||
|
||||
.tour-btn-secondary {
|
||||
background: var(--bg-tertiary, #27272a);
|
||||
- color: #d4d4d8;
|
||||
+ color: var(--text-primary);
|
||||
border: 1px solid var(--border-secondary, #3f3f46);
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ onMounted(() => {
|
||||
|
||||
.tour-btn-primary {
|
||||
background: #3b82f6;
|
||||
- color: white;
|
||||
+ color: var(--btn-primary-text);
|
||||
border: none;
|
||||
}
|
||||
|
||||
diff --git a/fix-more-colors.cjs b/fix-more-colors.cjs
|
||||
new file mode 100644
|
||||
index 0000000..1cea7e6
|
||||
--- /dev/null
|
||||
+++ b/fix-more-colors.cjs
|
||||
@@ -0,0 +1,102 @@
|
||||
+const fs = require('fs');
|
||||
+const path = require('path');
|
||||
+
|
||||
+function replaceInFile(filePath) {
|
||||
+ let content = fs.readFileSync(filePath, 'utf8');
|
||||
+
|
||||
+ // In UserTutorial.vue
|
||||
+ if (filePath.includes('UserTutorial.vue')) {
|
||||
+ content = content.replace(/text-zinc-100/g, 'text-text-primary');
|
||||
+ content = content.replace(/text-zinc-500/g, 'text-text-secondary');
|
||||
+ content = content.replace(/text-blue-400/g, 'text-primary');
|
||||
+ content = content.replace(/bg-blue-500\/10/g, 'bg-primary\/10');
|
||||
+ content = content.replace(/text-emerald-400/g, 'text-success');
|
||||
+ content = content.replace(/bg-emerald-500\/10/g, 'bg-success\/10');
|
||||
+ content = content.replace(/text-purple-400/g, 'text-[#a855f7]'); // Use exact hex or leave it, maybe text-purple-500
|
||||
+ content = content.replace(/bg-purple-500\/10/g, 'bg-[#a855f7]\/10');
|
||||
+ content = content.replace(/rgba\(0,\s*0,\s*0,\s*0\.4\)/g, 'var(--bg-secondary, rgba(0, 0, 0, 0.4))');
|
||||
+ content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.05\)/g, 'var(--border-secondary, rgba(255, 255, 255, 0.05))');
|
||||
+ content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.02\)/g, 'var(--bg-tertiary, rgba(255, 255, 255, 0.02))');
|
||||
+ content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.04\)/g, 'var(--bg-hover, rgba(255, 255, 255, 0.04))');
|
||||
+ content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.1\)/g, 'var(--border-tertiary, rgba(255, 255, 255, 0.1))');
|
||||
+ content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.9\)/g, 'var(--text-primary, rgba(255, 255, 255, 0.9))');
|
||||
+ content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.5\)/g, 'var(--text-secondary, rgba(255, 255, 255, 0.5))');
|
||||
+
|
||||
+ // Fix `.tour-popover` which has hardcoded text colors in p tags
|
||||
+ content = content.replace(/color:\s*#f4f4f5;/g, 'color: var(--text-primary);');
|
||||
+ content = content.replace(/color:\s*#a1a1aa;/g, 'color: var(--text-secondary);');
|
||||
+ content = content.replace(/color:\s*#71717a;/g, 'color: var(--text-tertiary);');
|
||||
+ content = content.replace(/color:\s*#d4d4d8;/g, 'color: var(--text-primary);');
|
||||
+ content = content.replace(/color:\s*white;/g, 'color: var(--btn-primary-text);');
|
||||
+ }
|
||||
+
|
||||
+ // In ScheduleList.vue
|
||||
+ if (filePath.includes('ScheduleList.vue')) {
|
||||
+ content = content.replace(/class="text-sm text-gray"/g, 'class="text-sm text-text-secondary"');
|
||||
+ content = content.replace(/background: linear-gradient\(135deg, rgba\(11, 90, 254, 0\.1\) 0%, rgba\(33, 36, 45, 0\.9\) 100%\);/g, 'background: var(--bg-secondary);');
|
||||
+ content = content.replace(/border: 1px solid rgba\(11, 90, 254, 0\.2\);/g, 'border: 1px solid var(--border-secondary);');
|
||||
+ content = content.replace(/background: linear-gradient\(135deg, rgba\(255, 255, 255, 0\.15\) 0%, rgba\(255, 255, 255, 0\.08\) 100%\);/g, 'background: var(--bg-tertiary);');
|
||||
+ content = content.replace(/border: 1px solid rgba\(255, 255, 255, 0\.25\);/g, 'border: 1px solid var(--border-tertiary);');
|
||||
+ content = content.replace(/color: var\(--text-primary, #ffffff\);/g, 'color: var(--text-primary);');
|
||||
+ // schedule-content empty state
|
||||
+ // The .empty rule in ScheduleList.vue:
|
||||
+ content = content.replace(/\.empty \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/color:\s*rgba\(255, 255, 255, 0.5\);/, 'color: var(--text-secondary);')
|
||||
+ .replace(/color:\s*#a0a0a0;/, 'color: var(--text-secondary);');
|
||||
+ });
|
||||
+ content = content.replace(/\.empty-dates \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/color:\s*rgba\(255, 255, 255, 0.5\);/, 'color: var(--text-secondary);');
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ // In RequestForm.vue
|
||||
+ if (filePath.includes('RequestForm.vue')) {
|
||||
+ // search-results-container
|
||||
+ content = content.replace(/background:\s*rgba\(255, 255, 255, 0.02\);/g, 'background: var(--bg-secondary);');
|
||||
+ content = content.replace(/border:\s*1px solid rgba\(255, 255, 255, 0.05\);/g, 'border: 1px solid var(--border-secondary);');
|
||||
+ content = content.replace(/background:\s*rgba\(255, 255, 255, 0.05\);/g, 'background: var(--bg-tertiary);');
|
||||
+ content = content.replace(/color:\s*rgba\(255, 255, 255, 0.9\);/g, 'color: var(--text-primary);');
|
||||
+ content = content.replace(/color:\s*rgba\(255, 255, 255, 0.6\);/g, 'color: var(--text-secondary);');
|
||||
+ content = content.replace(/color:\s*rgba\(255, 255, 255, 0.5\);/g, 'color: var(--text-tertiary);');
|
||||
+ content = content.replace(/color:\s*rgba\(255, 255, 255, 0.3\);/g, 'color: var(--text-disabled);');
|
||||
+ content = content.replace(/border:\s*1px solid rgba\(255, 255, 255, 0.08\);/g, 'border: 1px solid var(--border-tertiary);');
|
||||
+ content = content.replace(/border:\s*1px solid rgba\(255, 255, 255, 0.12\);/g, 'border: 1px solid var(--border-primary);');
|
||||
+ content = content.replace(/background:\s*rgba\(0, 0, 0, 0.2\);/g, 'background: var(--bg-primary);');
|
||||
+
|
||||
+ // submission-status-badge
|
||||
+ content = content.replace(/\.submission-status-badge\.open \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/background:\s*rgba\(16, 185, 129, 0.1\);/g, 'background: var(--success-light);')
|
||||
+ .replace(/border:\s*1px solid rgba\(16, 185, 129, 0.2\);/g, 'border: 1px solid var(--success-border);')
|
||||
+ .replace(/color:\s*#34d399;/g, 'color: var(--success);');
|
||||
+ });
|
||||
+ content = content.replace(/\.submission-status-badge\.closed \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/background:\s*rgba\(239, 68, 68, 0.1\);/g, 'background: var(--error-light);')
|
||||
+ .replace(/border:\s*1px solid rgba\(239, 68, 68, 0.2\);/g, 'border: 1px solid var(--error-border);')
|
||||
+ .replace(/color:\s*#f87171;/g, 'color: var(--error);');
|
||||
+ });
|
||||
+ content = content.replace(/\.badge-title \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/color:\s*rgba\(255, 255, 255, 0.9\);/, 'color: var(--text-primary);');
|
||||
+ });
|
||||
+
|
||||
+ // Rules Section
|
||||
+ content = content.replace(/\.rules-section \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/background:\s*rgba\(255, 255, 255, 0.02\);/, 'background: var(--bg-secondary);')
|
||||
+ .replace(/border:\s*1px solid rgba\(255, 255, 255, 0.05\);/, 'border: 1px solid var(--border-secondary);');
|
||||
+ });
|
||||
+ content = content.replace(/\.section-title \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/color:\s*rgba\(255, 255, 255, 0.9\);/, 'color: var(--text-primary);');
|
||||
+ });
|
||||
+ content = content.replace(/\.guidelines-content \{[\s\S]*?\}/g, (match) => {
|
||||
+ return match.replace(/color:\s*rgba\(255, 255, 255, 0.6\);/, 'color: var(--text-secondary);')
|
||||
+ .replace(/background:\s*rgba\(255, 255, 255, 0.02\);/, 'background: var(--bg-tertiary);')
|
||||
+ .replace(/border:\s*1px solid rgba\(255, 255, 255, 0.05\);/, 'border: 1px solid var(--border-tertiary);');
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ fs.writeFileSync(filePath, content, 'utf8');
|
||||
+ console.log(`Processed ${filePath}`);
|
||||
+}
|
||||
+
|
||||
+replaceInFile('app/components/UserTutorial.vue');
|
||||
+replaceInFile('app/components/Songs/ScheduleList.vue');
|
||||
+replaceInFile('app/components/Songs/RequestForm.vue');
|
||||
diff --git a/tailwind.config.ts b/tailwind.config.ts
|
||||
index ffe4216..444aa45 100644
|
||||
--- a/tailwind.config.ts
|
||||
+++ b/tailwind.config.ts
|
||||
@@ -18,6 +18,13 @@ export default <Config>{
|
||||
warning: 'var(--warning)',
|
||||
error: 'var(--error)',
|
||||
info: 'var(--info)',
|
||||
+ text: {
|
||||
+ primary: 'var(--text-primary)',
|
||||
+ secondary: 'var(--text-secondary)',
|
||||
+ tertiary: 'var(--text-tertiary)',
|
||||
+ quaternary: 'var(--text-quaternary)',
|
||||
+ disabled: 'var(--text-disabled)'
|
||||
+ },
|
||||
surface: {
|
||||
DEFAULT: 'var(--bg-primary)',
|
||||
muted: 'var(--bg-secondary)',
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
From a0f11ff64a193fd3ecd40a0f04d70953419268f1 Mon Sep 17 00:00:00 2001
|
||||
From: HenryDu8133 <813367384@qq.com>
|
||||
Date: Sat, 12 Sep 2026 16:57:53 +0800
|
||||
Subject: [PATCH 58/58] chore: remove script
|
||||
|
||||
---
|
||||
fix-more-colors.cjs | 102 --------------------------------------------
|
||||
1 file changed, 102 deletions(-)
|
||||
delete mode 100644 fix-more-colors.cjs
|
||||
|
||||
diff --git a/fix-more-colors.cjs b/fix-more-colors.cjs
|
||||
deleted file mode 100644
|
||||
index 1cea7e6..0000000
|
||||
--- a/fix-more-colors.cjs
|
||||
+++ /dev/null
|
||||
@@ -1,102 +0,0 @@
|
||||
-const fs = require('fs');
|
||||
-const path = require('path');
|
||||
-
|
||||
-function replaceInFile(filePath) {
|
||||
- let content = fs.readFileSync(filePath, 'utf8');
|
||||
-
|
||||
- // In UserTutorial.vue
|
||||
- if (filePath.includes('UserTutorial.vue')) {
|
||||
- content = content.replace(/text-zinc-100/g, 'text-text-primary');
|
||||
- content = content.replace(/text-zinc-500/g, 'text-text-secondary');
|
||||
- content = content.replace(/text-blue-400/g, 'text-primary');
|
||||
- content = content.replace(/bg-blue-500\/10/g, 'bg-primary\/10');
|
||||
- content = content.replace(/text-emerald-400/g, 'text-success');
|
||||
- content = content.replace(/bg-emerald-500\/10/g, 'bg-success\/10');
|
||||
- content = content.replace(/text-purple-400/g, 'text-[#a855f7]'); // Use exact hex or leave it, maybe text-purple-500
|
||||
- content = content.replace(/bg-purple-500\/10/g, 'bg-[#a855f7]\/10');
|
||||
- content = content.replace(/rgba\(0,\s*0,\s*0,\s*0\.4\)/g, 'var(--bg-secondary, rgba(0, 0, 0, 0.4))');
|
||||
- content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.05\)/g, 'var(--border-secondary, rgba(255, 255, 255, 0.05))');
|
||||
- content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.02\)/g, 'var(--bg-tertiary, rgba(255, 255, 255, 0.02))');
|
||||
- content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.04\)/g, 'var(--bg-hover, rgba(255, 255, 255, 0.04))');
|
||||
- content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.1\)/g, 'var(--border-tertiary, rgba(255, 255, 255, 0.1))');
|
||||
- content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.9\)/g, 'var(--text-primary, rgba(255, 255, 255, 0.9))');
|
||||
- content = content.replace(/rgba\(255,\s*255,\s*255,\s*0\.5\)/g, 'var(--text-secondary, rgba(255, 255, 255, 0.5))');
|
||||
-
|
||||
- // Fix `.tour-popover` which has hardcoded text colors in p tags
|
||||
- content = content.replace(/color:\s*#f4f4f5;/g, 'color: var(--text-primary);');
|
||||
- content = content.replace(/color:\s*#a1a1aa;/g, 'color: var(--text-secondary);');
|
||||
- content = content.replace(/color:\s*#71717a;/g, 'color: var(--text-tertiary);');
|
||||
- content = content.replace(/color:\s*#d4d4d8;/g, 'color: var(--text-primary);');
|
||||
- content = content.replace(/color:\s*white;/g, 'color: var(--btn-primary-text);');
|
||||
- }
|
||||
-
|
||||
- // In ScheduleList.vue
|
||||
- if (filePath.includes('ScheduleList.vue')) {
|
||||
- content = content.replace(/class="text-sm text-gray"/g, 'class="text-sm text-text-secondary"');
|
||||
- content = content.replace(/background: linear-gradient\(135deg, rgba\(11, 90, 254, 0\.1\) 0%, rgba\(33, 36, 45, 0\.9\) 100%\);/g, 'background: var(--bg-secondary);');
|
||||
- content = content.replace(/border: 1px solid rgba\(11, 90, 254, 0\.2\);/g, 'border: 1px solid var(--border-secondary);');
|
||||
- content = content.replace(/background: linear-gradient\(135deg, rgba\(255, 255, 255, 0\.15\) 0%, rgba\(255, 255, 255, 0\.08\) 100%\);/g, 'background: var(--bg-tertiary);');
|
||||
- content = content.replace(/border: 1px solid rgba\(255, 255, 255, 0\.25\);/g, 'border: 1px solid var(--border-tertiary);');
|
||||
- content = content.replace(/color: var\(--text-primary, #ffffff\);/g, 'color: var(--text-primary);');
|
||||
- // schedule-content empty state
|
||||
- // The .empty rule in ScheduleList.vue:
|
||||
- content = content.replace(/\.empty \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/color:\s*rgba\(255, 255, 255, 0.5\);/, 'color: var(--text-secondary);')
|
||||
- .replace(/color:\s*#a0a0a0;/, 'color: var(--text-secondary);');
|
||||
- });
|
||||
- content = content.replace(/\.empty-dates \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/color:\s*rgba\(255, 255, 255, 0.5\);/, 'color: var(--text-secondary);');
|
||||
- });
|
||||
- }
|
||||
-
|
||||
- // In RequestForm.vue
|
||||
- if (filePath.includes('RequestForm.vue')) {
|
||||
- // search-results-container
|
||||
- content = content.replace(/background:\s*rgba\(255, 255, 255, 0.02\);/g, 'background: var(--bg-secondary);');
|
||||
- content = content.replace(/border:\s*1px solid rgba\(255, 255, 255, 0.05\);/g, 'border: 1px solid var(--border-secondary);');
|
||||
- content = content.replace(/background:\s*rgba\(255, 255, 255, 0.05\);/g, 'background: var(--bg-tertiary);');
|
||||
- content = content.replace(/color:\s*rgba\(255, 255, 255, 0.9\);/g, 'color: var(--text-primary);');
|
||||
- content = content.replace(/color:\s*rgba\(255, 255, 255, 0.6\);/g, 'color: var(--text-secondary);');
|
||||
- content = content.replace(/color:\s*rgba\(255, 255, 255, 0.5\);/g, 'color: var(--text-tertiary);');
|
||||
- content = content.replace(/color:\s*rgba\(255, 255, 255, 0.3\);/g, 'color: var(--text-disabled);');
|
||||
- content = content.replace(/border:\s*1px solid rgba\(255, 255, 255, 0.08\);/g, 'border: 1px solid var(--border-tertiary);');
|
||||
- content = content.replace(/border:\s*1px solid rgba\(255, 255, 255, 0.12\);/g, 'border: 1px solid var(--border-primary);');
|
||||
- content = content.replace(/background:\s*rgba\(0, 0, 0, 0.2\);/g, 'background: var(--bg-primary);');
|
||||
-
|
||||
- // submission-status-badge
|
||||
- content = content.replace(/\.submission-status-badge\.open \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/background:\s*rgba\(16, 185, 129, 0.1\);/g, 'background: var(--success-light);')
|
||||
- .replace(/border:\s*1px solid rgba\(16, 185, 129, 0.2\);/g, 'border: 1px solid var(--success-border);')
|
||||
- .replace(/color:\s*#34d399;/g, 'color: var(--success);');
|
||||
- });
|
||||
- content = content.replace(/\.submission-status-badge\.closed \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/background:\s*rgba\(239, 68, 68, 0.1\);/g, 'background: var(--error-light);')
|
||||
- .replace(/border:\s*1px solid rgba\(239, 68, 68, 0.2\);/g, 'border: 1px solid var(--error-border);')
|
||||
- .replace(/color:\s*#f87171;/g, 'color: var(--error);');
|
||||
- });
|
||||
- content = content.replace(/\.badge-title \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/color:\s*rgba\(255, 255, 255, 0.9\);/, 'color: var(--text-primary);');
|
||||
- });
|
||||
-
|
||||
- // Rules Section
|
||||
- content = content.replace(/\.rules-section \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/background:\s*rgba\(255, 255, 255, 0.02\);/, 'background: var(--bg-secondary);')
|
||||
- .replace(/border:\s*1px solid rgba\(255, 255, 255, 0.05\);/, 'border: 1px solid var(--border-secondary);');
|
||||
- });
|
||||
- content = content.replace(/\.section-title \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/color:\s*rgba\(255, 255, 255, 0.9\);/, 'color: var(--text-primary);');
|
||||
- });
|
||||
- content = content.replace(/\.guidelines-content \{[\s\S]*?\}/g, (match) => {
|
||||
- return match.replace(/color:\s*rgba\(255, 255, 255, 0.6\);/, 'color: var(--text-secondary);')
|
||||
- .replace(/background:\s*rgba\(255, 255, 255, 0.02\);/, 'background: var(--bg-tertiary);')
|
||||
- .replace(/border:\s*1px solid rgba\(255, 255, 255, 0.05\);/, 'border: 1px solid var(--border-tertiary);');
|
||||
- });
|
||||
- }
|
||||
-
|
||||
- fs.writeFileSync(filePath, content, 'utf8');
|
||||
- console.log(`Processed ${filePath}`);
|
||||
-}
|
||||
-
|
||||
-replaceInFile('app/components/UserTutorial.vue');
|
||||
-replaceInFile('app/components/Songs/ScheduleList.vue');
|
||||
-replaceInFile('app/components/Songs/RequestForm.vue');
|
||||
--
|
||||
2.45.1.windows.1
|
||||
|
||||
Reference in New Issue
Block a user