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 # 鏍煎紡锛歳edis://鐢ㄦ埛鍚?瀵嗙爜@涓绘満:绔彛 # REDIS_URL="redis://default:password@host:port" +# Docker Compose 鑷甫鐨?Redis 瀹瑰櫒锛坈ompose 鑷甫 redis 鏈嶅姟锛屼粎鍦ㄥ惎鐢ㄨ嚜甯?redis 鏃剁敤锛?+# - 鐣欑┖锛堟垨绌哄瓧绗︿覆锛夛細涓嶈缃?Redis 瀵嗙爜锛屽唴缃戦儴缃插父鐢紙榛樿锛?+# - 濉啓寮哄瘑鐮侊細Redis 浼氬惎鐢?requirepass锛岃繛鎺ユ椂 REDIS_URL 閲屼篃瑕佸甫涓婄浉鍚屽瘑鐮?+REDIS_PASSWORD= + # Redis 閿懡鍚嶇┖闂达紝澶氫釜 VoiceHub 鐜鍏辩敤 Redis 鏃跺簲鍒嗗埆璁剧疆 # REDIS_KEY_PREFIX="voicehub:v2:" diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..5f46f56 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,698 @@ +name: 鏋勫缓骞堕儴缃?BYW VoiceHub +run-name: ${{ github.actor }} 瑙﹀彂閮ㄧ讲 路 Run ${{ github.run_number }} + +on: + push: + branches: [main, master] + workflow_dispatch: + inputs: + deploy_target: + 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: true + +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 + 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: "" + # ========== 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 鍓嶇紑鏇挎崲锛圓RG 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 + # 鍏抽敭淇 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銆佸叏閮ㄧ敤浠g悊/鍥藉唴婧愩€嶉鏍煎畬鍏ㄥ榻愶細 + # - Runner 鑷甫 node 鈮?2 鐩存帴鐢?+ # - 鍚﹀垯浼樺厛 npmmirror 鍥藉唴闀滃儚涓嬭浇 Node 22 浜岃繘鍒?+ # - 鏈€鍚庡厹搴曠敤 GH_PROXY_PREFIX 浠g悊 GitHub node-versions Releases 鐩撮摼 + env: + 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" + NITRO_PRESET: "node-server" + DISABLE_ROLLDOWN: "1" + NITRO_MINIFY: "false" + steps: + - name: 瀹夎鍩虹杩愯渚濊禆锛坓it/curl/ca-certificates/tar/xz-utils锛孯unner 鑷甫锛?+ 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 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 鍙敤锛坣pmmirror 浼樺厛 / 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 瑙f瀽 + NODE_VER="${NODE_VERSION_FALLBACK}" + if command -v python3 >/dev/null 2>&1; then + 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' "$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}" + 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锛歯pmmirror 鍥藉唴浜岃繘鍒堕暅鍍忥紙棣栭€夛紝鍥藉唴鏈€蹇級 + URL_1="${NODE_MIRROR_PRIMARY}/v${NODE_VER}/${TAR_XZ}" + # 涓嬭浇 URL 2锛歯odejs.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 涓嬭浇婧愬潎澶辫触锛坣pmmirror + gh-proxy锛夛紝涓" >&2 + exit 1 + fi + + # 瑙e帇骞跺姞鍒?PATH锛孏ITHUB_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: | + set -euxo pipefail + # 鍏ㄥ眬 git 浠g悊锛氫换浣?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 + + - 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: 瀹夎渚濊禆锛坣pm ci锛?+ run: | + set -euxo pipefail + npm config set registry https://registry.npmmirror.com || true + npm config set fund false + npm config set audit false + npm ci --legacy-peer-deps + + - 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 + run: | + 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=10240 --no-warnings --expose-gc" + 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: 鏋勫缓骞舵帹閫佸鍣ㄩ暅鍍?+ 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: 绯荤粺鐜鍑嗗锛圲buntu 22.04 榛樿闀滃儚婧愭崲娓呭崕 + 鍏ㄥ眬 GitHub gh-proxy锛?+ run: | + set -euxo pipefail + # ========== Step 0锛氬厛瑁?ca-certificates锛堥粯璁?HTTP archive.ubuntu.com 涓嶉渶瑕佽瘉涔︼紝瑙i浮鐢熻泲锛?========== + 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|'"${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|'"${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 + # 鍏ㄥ眬 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 + + # ========== 鍏抽敭淇锛氬涓绘満 Docker daemon 鍏堟敞鍏ュ浗鍐?registry mirrors ========== + # 鍘熷洜锛歊unner 浠讳綍 docker pull / buildx create锛坢oby/buildkit 鎷夊熀纭€闀滃儚锛夐兘瑕佸厛缁忚繃 daemon + # 濡傛灉鐩存帴杩?docker.io锛屽浗鍐呬細琚檺閫熷埌 <200KB/s锛屽鑷?Setup / BuildKit 鎷夐暅鍍忎竴鐩村崱銆?+ # 娉ㄦ剰锛氭澶勭敤 PY_CODE 瀛楃涓?+ printf 绠¢亾浼?Python 浠g爜缁?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 + OLD_MIRRORS_JSON="{}" + if [ -f "${DOCKER_DAEMON_CONF}" ]; then + OLD_MIRRORS_JSON="$(sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || echo "{}")" + fi + 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:" + sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || true + # 闈炰镜鍏ュ紡閲嶅惎 docker锛況unner 娌?systemd 鏃剁敤 service 鍏滃簳锛涘け璐ヤ篃璺宠繃锛坆uildkitd.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 + docker buildx version || echo "buildx 鏈畨瑁? + + - 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 + docker version + docker buildx version || echo "buildx 鏈畨瑁咃紝灏嗕娇鐢?docker build --push" + + - name: 璁$畻闀滃儚鏍囩 + id: meta + run: | + set -euo pipefail + # Docker Registry 瑙勮寖锛氫粨搴撹矾寰勩€乷wner銆乺epo 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}/${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 + 鍐呯疆 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)" + # 鐢?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\n' + # 鍏抽敭锛欸itea Container Registry 鐩存帴鐢?Basic Auth 鍑嵁鍐欐鍦?buildkitd.toml + # 缁曞紑 buildkitd 鐨?OAuth2 flow锛圙itea 1.27.x 鏈夋椂杩斿洖绌?token body锛歛uthorization 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 + 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 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}" \ + --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 "image=${BUILDKIT_IMAGE}" + rm -f "${BKK_CFG}" + # 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 + echo "Builder 鍚姩鎴愬姛锛宐uildkitd.toml 宸插唴缃?${GITEA_HOST} Basic Auth 鍑嵁锛宲ush 涓嶅啀璧?OAuth2 flow" + + - name: 纭宸茬櫥褰曪紙鍑嵁宸叉敞鍏?buildkitd.toml 涓庡涓?Docker daemon锛?+ run: | + set -euo pipefail + # 鍓嶉潰銆屽垵濮嬪寲 Docker Buildx builder銆峉tep 宸插畬鎴愪袱娆$櫥褰曪細 + # 1. 瀹夸富 Docker daemon锛堥暅鍍忎綋绉?Step 瑕佺敤锛夛紱2. buildkitd.toml锛坆uildx push 瑕佺敤锛孊asic Auth 涓嶈蛋 OAuth2锛?+ # 杩欓噷鍙仛涓€涓揩閫熸柇瑷€锛岀‘淇濆涓荤櫥褰曟垚鍔?+ docker info 2>/dev/null | grep -E "Username|Registry" || echo "宸插瓨鍦ㄧ櫥褰曞嚟鎹紙璺宠繃璇︾粏杈撳嚭锛? + echo "鐧诲綍纭瀹屾垚锛孯EGISTRY_USER=${REGISTRY_USER:-${GITHUB_ACTOR,,}}锛孏ITEA_HOST=${GITEA_HOST}" + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }} + + - name: 鏋勫缓骞舵帹閫侀暅鍍?+ env: + SKIP_CACHE: ${{ inputs.skip_build_cache }} + IMG_FULL: ${{ steps.meta.outputs.full }} + IMG_LATEST: ${{ steps.meta.outputs.latest }} + OCS_SOURCE_RAW: ${{ github.server_url }}/${{ github.repository }} + OCS_REVISION: ${{ github.sha }} + 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)}" + # 鍐嶆淇濋櫓锛氭墍鏈夐暅鍍忔爣绛惧瓧绗︿覆閮界‘璁ゆ棤澶у啓瀛楁瘝锛圧egistry 纭€ц鑼冿級 + IMG_FULL="${IMG_FULL,,}" + IMG_LATEST="${IMG_LATEST,,}" + + 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 鍒嗗眰缂撳瓨锛坆uildkitd 鎺?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 \ + --load \ + --provenance=false \ + "${BUILD_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}" \ + . + + # 鏂█锛氫袱涓?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 "闀滃儚浣撶Н锛圥re-push锛夛細" + docker image inspect "${IMG_FULL}" --format='{{.Size}}' | awk '{printf " Full image : %.2f MB\n", $1/1024/1024}' + + # ========== 闃舵 2锛氬涓?Docker daemon 鍘熺敓 push锛?00% 璧板涓荤櫥褰曞嚟鎹紝涓嶄細璧?OAuth2锛?========== + echo "寮€濮嬫帹閫侊細${IMG_FULL}" + docker push "${IMG_FULL}" + echo "寮€濮嬫帹閫侊細${IMG_LATEST}" + docker push "${IMG_LATEST}" + echo "鎺ㄩ€佸畬鎴愶細2 涓?tag锛坉ate-sha + latest锛? + + - name: 闀滃儚浣撶Н涓庡眰淇℃伅 + env: + IMG_FULL: ${{ steps.meta.outputs.full }} + run: | + set -euxo pipefail + 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 "${IMG_FULL}" 2>/dev/null | head -20 || true + + deploy: + name: 閮ㄧ讲鍒扮敓浜ф湇鍔″櫒 + needs: build-image + if: ${{ !cancelled() && !failure() && (github.event_name == 'push' || inputs.deploy_target == true) }} + runs-on: ubuntu-latest + timeout-minutes: 25 + environment: + name: production + url: ${{ secrets.NUXT_PUBLIC_SITE_URL }} + steps: + - name: 鏍¢獙閮ㄧ讲鎵€闇€瀵嗛挜 + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + set -euo pipefail + MISSING=() + [ -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 浠撳簱 -> 璁剧疆 -> 宸ヤ綔娴?-> 瀵嗛挜 椤甸潰娣诲姞锛堟敞鎰忔槸 Repository Secrets锛屼笉鏄?Deploy Keys锛? + exit 1 + fi + echo "閮ㄧ讲瀵嗛挜閰嶇疆瀹屾暣锛圖EPLOY_HOST=${DEPLOY_HOST}锛孌EPLOY_USER=${DEPLOY_USER}锛? + + - name: SSH 婊氬姩閮ㄧ讲 + 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 + # 鍏ㄥ皬鍐欒鑼冿紙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)" + 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" + + # 鍏抽敭淇锛氳繙绔?bash 鍓嶇疆 ENV=val 璇硶鍦?dash/sh 杩滅涓嶈锛屽彉閲忓叏绌恒€?+ # 鐢?printf + \n 鎷兼帴瀹屾暣 shell 鑴氭湰閫氳繃 stdin 娉ㄥ叆鍒?ssh 杩滅 cat锛?+ # 涓嶇敤 heredoc锛岄伩鍏?YAML 鍐掑彿/缂╄繘瑙f瀽璇垽銆?+ ENV_SH="/tmp/deploy_env_$$" + 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 杩滅 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 + # shellcheck disable=SC1090 + . "$ENV_SH" + rm -f "$ENV_SH" + fi + + DEPLOY_DIR="${DEPLOY_PATH}" + COMPOSE="docker compose" + + echo "==============================================" + echo "Executor : $(whoami)" + 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}" + + # ========== 鐩綍鍙啓鎬ч妫€鏌ワ紙鏉冮檺鎺掓煡鍙嬪ソ鍖栵級 ========== + TEST_FILE="${DEPLOY_DIR}/.deploy_writable_test_$$" + if ! (touch "${TEST_FILE}" && rm -f "${TEST_FILE}") 2>/dev/null; then + echo "=================================================================" + echo "ERROR锛歞eploy 鐢ㄦ埛瀵归儴缃茬洰褰?${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}" \ + | docker login "${GITEA_HOST}" \ + -u "${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" + + # 鐢?printf 鎷?override.yml锛屽交搴曢伩鍏?bash heredoc 鐨勪笁閲嶅潙锛?+ # 1) 缁撴潫鍒嗛殧绗﹀繀椤昏棣栵紙鍚﹀垯 bash 涓嶈锛屾妸鍚庨潰鏁存浠g爜閮藉啓杩涙枃浠讹級 + # 2) 鍐呭眰 <<'XXX' 鍗曞紩鍙峰寘瑁瑰鑷村彉閲忎笉灞曞紑锛坕mage: ${IMAGE_NEW} 浼氭槸瀛楅潰閲忥級 + # 3) 澶栧眰 .yml parser 鎶?heredoc 閲岀殑 services: 褰?YAML mapping key 瑙f瀽 + 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 鍓嶇紑锛? + 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锛? + fi + + if [ ! -f .env ]; then + echo "=================================================================" + echo "WARNING锛?{DEPLOY_DIR}/.env 涓嶅瓨鍦? + echo "棣栨閮ㄧ讲璇峰湪鏈嶅姟鍣ㄤ笂鍒涘缓 .env锛堣嚦灏戝~鍐?POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL + 5 涓?cpus 鏁板瓧鍏滃簳锛欰PP_CPUS/APP_CPUS_LIMIT/APP_CPUS_RESERVATION/PG_CPUS/REDIS_CPUS锛? + 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 + + 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: \\n' > docker-compose.override.yml" + echo " docker compose up -d" + echo "=================================================================" + REMOTE_SCRIPT_MAIN + + # 娓呯悊绉侀挜涓存椂鏂囦欢 + rm -f "${SSH_KEY_FILE}" 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 鍩虹闀滃儚 ========== +# 鐢ㄦ硶锛歞ocker 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 \ +# ... +# 缂虹渷锛圖OCKER_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 diff --git a/app/assets/css/print-fix.css b/app/assets/css/print-fix.css index 1805e4b..6ef1703 100644 --- a/app/assets/css/print-fix.css +++ b/app/assets/css/print-fix.css @@ -97,18 +97,29 @@ font-size: 14px !important; } - /* 灏侀潰 */ + /* 灏侀潰 - 鎵撳嵃妯″紡涓嬪浐瀹氬昂瀵革紝绉婚櫎鍦嗚闃村奖锛屼娇鐢?contain 闃叉瑁佸垏 */ .schedule-item-print .cover-section { - width: 40px !important; - height: 40px !important; + width: 48px !important; + height: 48px !important; + aspect-ratio: 1 / 1 !important; flex-shrink: 0 !important; } - .schedule-item-print .song-cover, + .schedule-item-print .song-cover { + width: 100% !important; + height: 100% !important; + object-fit: contain !important; + border-radius: 0 !important; + box-shadow: none !important; + } + .schedule-item-print .cover-placeholder { width: 100% !important; height: 100% !important; - border-radius: 4px !important; + aspect-ratio: 1 / 1 !important; + object-fit: contain !important; + border-radius: 0 !important; + box-shadow: none !important; } /* 姝屾洸淇℃伅 */ diff --git a/app/assets/css/variables.css b/app/assets/css/variables.css index 30cf9f1..30dce2a 100644 --- a/app/assets/css/variables.css +++ b/app/assets/css/variables.css @@ -203,7 +203,9 @@ --scrollbar-thumb-hover: #4a4a4a; } -:root[data-theme='light'] { +:root[data-theme='light'], +:root[data-theme='ClassicLight'], +:root[data-theme='ModernLight'] { --bg-primary: #f8fafc; --bg-secondary: #f1f5f9; --bg-tertiary: #e2e8f0; @@ -223,6 +225,177 @@ --modal-backdrop: rgba(0, 0, 0, 0.4); --scrollbar-thumb-hover: #94a3b8; + + --btn-secondary-bg: #f1f5f9; + --btn-secondary-hover: #e2e8f0; + --btn-secondary-text: #0f172a; + --btn-secondary-border: #cbd5e1; + + --card-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +:root[data-theme='ClassicLight'] { + --primary: #1e40af; + --primary-hover: #1e3a8a; + --primary-light: rgba(30, 64, 175, 0.08); + --primary-border: rgba(30, 64, 175, 0.15); + + --bg-primary: #ffffff; + --bg-secondary: #f5f7fa; + --bg-tertiary: #edf0f5; + --bg-quaternary: #d9dee7; + --bg-hover: #f0f3f8; + + --success: #059669; + --success-hover: #047857; + --success-light: rgba(5, 150, 105, 0.08); + --success-border: rgba(5, 150, 105, 0.15); + + --warning: #d97706; + --warning-hover: #b45309; + --warning-light: rgba(217, 119, 6, 0.08); + --warning-border: rgba(217, 119, 6, 0.15); + + --error: #dc2626; + --error-hover: #b91c1c; + --error-light: rgba(220, 38, 38, 0.08); + --error-border: rgba(220, 38, 38, 0.15); + + --info: #2563eb; + --info-hover: #1d4ed8; + --info-light: rgba(37, 99, 235, 0.08); + --info-border: rgba(37, 99, 235, 0.15); + + --text-primary: #1a2332; + --text-secondary: #4a5568; + --text-tertiary: #6b7280; + --text-quaternary: #9ca3af; + --text-disabled: #c5cbd3; + + --border-primary: #e5e7eb; + --border-secondary: #d1d5db; + --border-tertiary: #9ca3af; + --border-quaternary: #6b7280; + + --input-bg: #ffffff; + --input-border: #d1d5db; + --input-border-focus: #1e40af; + --input-text: #1a2332; + --input-placeholder: #9ca3af; + --input-shadow-focus: 0 0 0 3px rgba(30, 64, 175, 0.1); + + --card-bg: #ffffff; + --card-border: #e5e7eb; + --card-hover-border: #d1d5db; + + --table-header-bg: #f5f7fa; + --table-row-hover: #f9fafb; + --table-border: #e5e7eb; + --table-selected: rgba(30, 64, 175, 0.06); + + --scrollbar-track: #f1f5f9; + --scrollbar-thumb: #cbd5e1; + --scrollbar-thumb-hover: #94a3b8; + + --status-pending: #ca8a04; + --status-pending-light: rgba(202, 138, 4, 0.08); + --status-approved: #059669; + --status-approved-light: rgba(5, 150, 105, 0.08); + --status-played: #6b7280; + --status-played-light: rgba(107, 114, 128, 0.08); + + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 2px 4px -2px rgba(0, 0, 0, 0.04); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.06), 0 4px 6px -4px rgba(0, 0, 0, 0.04); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.06), 0 8px 10px -6px rgba(0, 0, 0, 0.04); + + --radius-sm: 3px; + --radius-md: 5px; + --radius-lg: 6px; + --radius-xl: 8px; + --radius-2xl: 10px; +} + +:root[data-theme='ModernLight'] { + --primary: #4f46e5; + --primary-hover: #4338ca; + --primary-light: rgba(79, 70, 229, 0.1); + --primary-border: rgba(79, 70, 229, 0.2); + + --bg-primary: #fafafa; + --bg-secondary: #f4f4f5; + --bg-tertiary: #e4e4e7; + --bg-quaternary: #d4d4d8; + --bg-hover: #eeeeef; + + --success: #16a34a; + --success-hover: #15803d; + --success-light: rgba(22, 163, 74, 0.1); + --success-border: rgba(22, 163, 74, 0.2); + + --warning: #ea580c; + --warning-hover: #c2410c; + --warning-light: rgba(234, 88, 12, 0.1); + --warning-border: rgba(234, 88, 12, 0.2); + + --error: #e11d48; + --error-hover: #be123c; + --error-light: rgba(225, 29, 72, 0.1); + --error-border: rgba(225, 29, 72, 0.2); + + --info: #0ea5e9; + --info-hover: #0284c7; + --info-light: rgba(14, 165, 233, 0.1); + --info-border: rgba(14, 165, 233, 0.2); + + --text-primary: #09090b; + --text-secondary: #3f3f46; + --text-tertiary: #71717a; + --text-quaternary: #a1a1aa; + --text-disabled: #d4d4d8; + + --border-primary: #e4e4e7; + --border-secondary: #d4d4d8; + --border-tertiary: #a1a1aa; + --border-quaternary: #71717a; + + --input-bg: #ffffff; + --input-border: #e4e4e7; + --input-border-focus: #4f46e5; + --input-text: #09090b; + --input-placeholder: #a1a1aa; + --input-shadow-focus: 0 0 0 3px rgba(79, 70, 229, 0.12); + + --card-bg: #ffffff; + --card-border: #e4e4e7; + --card-hover-border: #d4d4d8; + + --table-header-bg: #f4f4f5; + --table-row-hover: #fafafa; + --table-border: #e4e4e7; + --table-selected: rgba(79, 70, 229, 0.08); + + --scrollbar-track: #f4f4f5; + --scrollbar-thumb: #d4d4d8; + --scrollbar-thumb-hover: #a1a1aa; + + --status-pending: #eab308; + --status-pending-light: rgba(234, 179, 8, 0.12); + --status-approved: #22c55e; + --status-approved-light: rgba(34, 197, 94, 0.12); + --status-played: #71717a; + --status-played-light: rgba(113, 113, 122, 0.12); + + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03); + --shadow-md: 0 2px 8px -2px rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.03); + --shadow-lg: 0 8px 24px -4px rgba(0, 0, 0, 0.06), 0 2px 8px -4px rgba(0, 0, 0, 0.04); + --shadow-xl: 0 16px 40px -8px rgba(0, 0, 0, 0.08), 0 4px 16px -8px rgba(0, 0, 0, 0.05); + + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 10px; + --radius-2xl: 12px; } /* 娣辫壊涓婚鐗瑰畾璋冩暣 */ diff --git a/app/components/Admin/ApiKeyManager.vue b/app/components/Admin/ApiKeyManager.vue index 8a21229..b20a430 100644 --- a/app/components/Admin/ApiKeyManager.vue +++ b/app/components/Admin/ApiKeyManager.vue @@ -1026,7 +1026,7 @@ onMounted(() => { width: 24px; height: 24px; border: 2px solid rgba(255, 255, 255, 0.1); - border-top-color: #3b82f6; + border-top-color: var(--border-secondary, #3b82f6); border-radius: 50%; animation: spin 1s linear infinite; } @@ -1044,7 +1044,7 @@ onMounted(() => { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { - background: #27272a; + background: var(--bg-tertiary, #27272a); border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { diff --git a/app/components/Admin/BackupManager.vue b/app/components/Admin/BackupManager.vue index cb9c00a..3d78253 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; } @@ -556,8 +556,8 @@ const formatFileSize = (bytes) => { /* 鎿嶄綔鍗$墖 */ .action-card { - background: #2a2a2a; - border: 1px solid #404040; + background: var(--bg-tertiary, #2a2a2a); + border: 1px solid var(--border-secondary, #404040); border-radius: 12px; padding: 2rem; text-align: center; @@ -566,7 +566,7 @@ const formatFileSize = (bytes) => { .action-card:hover { border-color: #606060; - background: #2f2f2f; + background: var(--bg-tertiary, #2f2f2f); } .action-card .card-content { @@ -587,7 +587,7 @@ const formatFileSize = (bytes) => { display: flex; align-items: center; justify-content: center; - color: #ffffff; + color: var(--text-primary, #ffffff); } .action-icon svg { @@ -599,7 +599,7 @@ const formatFileSize = (bytes) => { .action-card h4 { font-size: 1.5rem; font-weight: 600; - color: #ffffff; + color: var(--text-primary, #ffffff); margin: 0 0 0.75rem 0; } @@ -672,8 +672,8 @@ const formatFileSize = (bytes) => { } .modal { - background: #2a2a2a; - border: 1px solid #404040; + background: var(--bg-tertiary, #2a2a2a); + border: 1px solid var(--border-secondary, #404040); border-radius: 12px; max-width: 600px; width: 100%; @@ -724,7 +724,7 @@ const formatFileSize = (bytes) => { .progress-details h5, .progress-errors h5 { margin: 0 0 10px 0; - color: #ffffff; + color: var(--text-primary, #ffffff); font-size: 14px; font-weight: 600; } @@ -736,14 +736,14 @@ const formatFileSize = (bytes) => { border: 1px solid #505050; border-radius: 6px; padding: 10px; - background: #333333; + background: var(--bg-quaternary, #333333); } .detail-item { padding: 4px 0; color: #a0a0a0; font-size: 13px; - border-bottom: 1px solid #404040; + border-bottom: 1px solid var(--border-secondary, #404040); } .detail-item:last-child { @@ -754,7 +754,7 @@ const formatFileSize = (bytes) => { padding: 4px 0; color: #ff6b6b; font-size: 13px; - border-bottom: 1px solid #4a2a2a; + border-bottom: 1px solid var(--border-secondary, #4a2a2a); } .error-item:last-child { @@ -772,7 +772,7 @@ const formatFileSize = (bytes) => { .modal-title { font-size: 1.75rem; font-weight: 600; - color: #ffffff; + color: var(--text-primary, #ffffff); margin: 0; } @@ -788,7 +788,7 @@ const formatFileSize = (bytes) => { .close-btn:hover { background: #505050; - color: #ffffff; + color: var(--text-primary, #ffffff); } .close-btn svg { @@ -802,11 +802,11 @@ const formatFileSize = (bytes) => { .modal-footer { padding: 1.5rem 2rem; - border-top: 1px solid #404040; + border-top: 1px solid var(--border-secondary, #404040); display: flex; gap: 1rem; justify-content: flex-end; - background: #252525; + background: var(--bg-tertiary, #252525); } .modal-footer button { @@ -822,7 +822,7 @@ const formatFileSize = (bytes) => { display: block; font-size: 1.125rem; font-weight: 600; - color: #ffffff; + color: var(--text-primary, #ffffff); margin-bottom: 1rem; } @@ -838,7 +838,7 @@ const formatFileSize = (bytes) => { align-items: flex-start; gap: 1rem; padding: 1.25rem; - background: #333333; + background: var(--bg-quaternary, #333333); border: 1px solid #505050; border-radius: 8px; cursor: pointer; @@ -870,7 +870,7 @@ const formatFileSize = (bytes) => { display: block; font-size: 1rem; font-weight: 500; - color: #ffffff; + color: var(--text-primary, #ffffff); margin-bottom: 0.25rem; } @@ -907,12 +907,12 @@ const formatFileSize = (bytes) => { text-align: center; cursor: pointer; transition: all 0.2s ease; - background: #333333; + background: var(--bg-quaternary, #333333); } .upload-area:hover, .upload-area.drag-over { - border-color: #4a90e2; + border-color: var(--border-secondary, #4a90e2); background: #383838; } @@ -926,7 +926,7 @@ const formatFileSize = (bytes) => { .upload-area h4 { font-size: 1.25rem; font-weight: 600; - color: #ffffff; + color: var(--text-primary, #ffffff); margin: 0 0 0.5rem 0; } @@ -941,7 +941,7 @@ const formatFileSize = (bytes) => { align-items: center; justify-content: space-between; padding: 1.25rem; - background: #333333; + background: var(--bg-quaternary, #333333); border: 1px solid #505050; border-radius: 8px; margin-top: 1rem; @@ -969,7 +969,7 @@ const formatFileSize = (bytes) => { display: block; font-size: 1rem; font-weight: 500; - color: #ffffff; + color: var(--text-primary, #ffffff); margin-bottom: 0.25rem; } @@ -1056,8 +1056,8 @@ const formatFileSize = (bytes) => { } .notification.success { - background: #2e7d32; - border: 1px solid #4caf50; + background: var(--bg-tertiary, #2e7d32); + border: 1px solid var(--border-secondary, #4caf50); } .notification.error { @@ -1071,8 +1071,8 @@ const formatFileSize = (bytes) => { } .notification.info { - background: #1976d2; - border: 1px solid #2196f3; + background: var(--bg-secondary, #1976d2); + border: 1px solid var(--border-secondary, #2196f3); } @keyframes slideInRight { diff --git a/app/components/Admin/BlacklistManager.vue b/app/components/Admin/BlacklistManager.vue index 17c450e..b5ec8a4 100644 --- a/app/components/Admin/BlacklistManager.vue +++ b/app/components/Admin/BlacklistManager.vue @@ -20,17 +20,17 @@ 娣诲姞榛戝悕鍗曢」
-
+
-
+
+ +
+
+

+ + +

+
+
@@ -142,27 +165,21 @@
- + + +
- {{ item.type === 'SONG' ? '鍏蜂綋姝屾洸' : '鍏抽敭璇? }} + {{ getTypeLabel(item.type) }}
{{ item.value }} @@ -318,7 +335,7 @@
- diff --git a/app/components/Admin/SchedulePrinter.vue b/app/components/Admin/SchedulePrinter.vue index 71bf445..9b26695 100644 --- a/app/components/Admin/SchedulePrinter.vue +++ b/app/components/Admin/SchedulePrinter.vue @@ -1383,15 +1383,15 @@ watch( /* 棰勮鍖哄煙鍜屾墦鍗版牱寮忕殑鏍稿績CSS - 淇濇寔鍘熺敓CSS浠ョ‘淇濈簿纭帶鍒?*/ .preview-content { - background: #ffffff; + background: var(--card-bg, #ffffff); padding: 0; position: relative; } /* 鎵撳嵃椤甸潰鏍峰紡 */ .print-page { - background: #ffffff; - color: #000000; + background: var(--card-bg, #ffffff); + color: var(--text-primary, #000000); width: 100%; margin: 0 auto; padding: 30px; @@ -1426,7 +1426,7 @@ watch( .logo-divider { width: 2px; height: 60px; - background: linear-gradient(to bottom, #ddd, #999, #ddd); + background: linear-gradient(to bottom, var(--border-secondary, #dddddd), #999, #ddd); border-radius: 1px; margin: 0 4px; } @@ -1445,19 +1445,19 @@ watch( font-size: 24px; font-weight: normal; margin: 0 0 4px 0; - color: #000; + color: var(--text-primary, #000); } .title-section h2 { font-size: 18px; font-weight: 500; margin: 0; - color: #666; + color: var(--text-quaternary, #666666); } .date-info { font-size: 14px; - color: #666; + color: var(--text-quaternary, #666666); text-align: right; display: flex; align-items: flex-start; @@ -1477,14 +1477,14 @@ watch( .no-data-message { text-align: center; padding: 60px 20px; - color: #666; + color: var(--text-quaternary, #666666); } .no-data-icon { width: 64px; height: 64px; margin: 0 auto 20px; - color: #ccc; + color: var(--text-secondary, #cccccc); } .no-data-icon svg { @@ -1496,7 +1496,7 @@ watch( font-size: 18px; font-weight: 500; margin: 0 0 12px 0; - color: #333; + color: var(--text-quaternary, #333333); } .no-data-message p { @@ -1512,15 +1512,15 @@ watch( font-weight: bold; margin: 24px 0 12px 0; padding: 8px 12px; - border-bottom: 2px solid #ddd; + border-bottom: 2px solid var(--border-secondary, #ddd); background: #f8f9fa; - color: #333; + color: var(--text-quaternary, #333333); border-radius: 4px 4px 0 0; } .playtime-group .group-title { background: #e3f2fd; - border-bottom-color: #2196f3; + border-bottom-color: var(--border-secondary, #2196f3); color: #1565c0; } @@ -1533,7 +1533,7 @@ watch( .group-count { font-size: 14px; font-weight: normal; - color: #666; + color: var(--text-quaternary, #666666); margin-left: 8px; } @@ -1547,7 +1547,7 @@ watch( margin: 16px 0 8px 0; padding: 6px 10px; background: #f0f8ff; - border-left: 3px solid #2196f3; + border-left: 3px solid var(--border-secondary, #2196f3); color: #1565c0; border-radius: 0 4px 4px 0; } @@ -1555,7 +1555,7 @@ watch( .playtime-count { font-size: 12px; font-weight: normal; - color: #666; + color: var(--text-quaternary, #666666); margin-left: 6px; } @@ -1570,7 +1570,7 @@ watch( padding-top: 16px; border-top: 1px solid #e5e5e5; font-size: 12px; - color: #666; + color: var(--text-quaternary, #666666); } .footer-left { diff --git a/app/components/Admin/Sidebar.vue b/app/components/Admin/Sidebar.vue index d4dc22a..369f26c 100644 --- a/app/components/Admin/Sidebar.vue +++ b/app/components/Admin/Sidebar.vue @@ -279,7 +279,7 @@ const getRoleDisplayName = (role) => { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { - background: #27272a; + background: var(--bg-tertiary, #27272a); border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { diff --git a/app/components/Admin/SongDownloadDialog.vue b/app/components/Admin/SongDownloadDialog.vue index bb81bf2..9268c5a 100644 --- a/app/components/Admin/SongDownloadDialog.vue +++ b/app/components/Admin/SongDownloadDialog.vue @@ -1228,7 +1228,7 @@ onUnmounted(() => { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { - background: #27272a; + background: var(--bg-tertiary, #27272a); border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { diff --git a/app/components/Admin/SongManagement.vue b/app/components/Admin/SongManagement.vue index ad4445c..a630cc2 100644 --- a/app/components/Admin/SongManagement.vue +++ b/app/components/Admin/SongManagement.vue @@ -61,6 +61,12 @@ > 鏈挱鏀?({{ selectedSongs.length }}) +
@@ -578,15 +571,6 @@ @confirm="handleConfirmAction" /> - - - - + + diff --git a/app/pages/index.vue b/app/pages/index.vue index 33fe93f..7ec75dd 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -33,6 +33,18 @@
+ + +