Files
BYW-VoiceHub/custom_changes.patch
T

23803 lines
842 KiB
Diff

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: <OLD_TAG_FROM_ABOVE>\\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 @@
娣诲姞榛戝悕鍗曢」
</h3>
<div class="grid grid-cols-1 xl:grid-cols-12 gap-6 items-end">
- <div class="xl:col-span-2 space-y-2">
+ <div class="xl:col-span-3 space-y-2">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>灏佺绫诲瀷</label
>
- <div class="flex p-1 bg-zinc-950 border border-zinc-800 rounded-lg">
+ <div class="flex p-1 bg-zinc-950 border border-zinc-800 rounded-lg gap-1">
<button
class="flex-1 flex items-center justify-center gap-2 py-2 rounded-md text-[11px] font-bold transition-all"
:class="newItem.type === 'SONG' ? 'bg-zinc-800 text-blue-400' : 'text-zinc-600'"
@click="newItem.type = 'SONG'"
>
- <Music :size="12" /> 鍏蜂綋姝屾洸
+ <Music :size="12" /> 姝屾洸
</button>
<button
class="flex-1 flex items-center justify-center gap-2 py-2 rounded-md text-[11px] font-bold transition-all"
@@ -39,23 +39,46 @@
>
<Type :size="12" /> 鍏抽敭璇? </button>
+ <button
+ class="flex-1 flex items-center justify-center gap-2 py-2 rounded-md text-[11px] font-bold transition-all"
+ :class="newItem.type === 'LANGUAGE' ? 'bg-zinc-800 text-amber-400' : 'text-zinc-600'"
+ @click="newItem.type = 'LANGUAGE'"
+ >
+ <Languages :size="12" /> 璇
+ </button>
+ <button
+ class="flex-1 flex items-center justify-center gap-2 py-2 rounded-md text-[11px] font-bold transition-all"
+ :class="newItem.type === 'GENRE' ? 'bg-zinc-800 text-emerald-400' : 'text-zinc-600'"
+ @click="newItem.type = 'GENRE'"
+ >
+ <Disc3 :size="12" /> 鏇查
+ </button>
</div>
</div>
<div class="xl:col-span-4 space-y-2">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1">
- {{ newItem.type === 'SONG' ? '姝屾洸鍚嶇О - 鑹烘湳瀹? : '鍏抽敭璇嶅唴瀹? }}
+ {{ typeValueLabel }}
</label>
<input
v-model="newItem.value"
type="text"
- :placeholder="
- newItem.type === 'SONG' ? '渚嬪: 姝屾洸鍚?- 鑹烘湳瀹? : '渚嬪: 鏁忔劅璇嶃€佺壒瀹氶鏍?
- "
+ :placeholder="typePlaceholder"
class="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-5 py-3 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30 placeholder:text-zinc-700 transition-all"
>
</div>
+ <div v-if="newItem.type === 'LANGUAGE' || newItem.type === 'GENRE'" class="xl:col-span-5 xl:col-start-auto space-y-1">
+ <p class="text-[10px] text-zinc-600 font-bold leading-relaxed px-1">
+ <template v-if="newItem.type === 'LANGUAGE'">
+ 馃挕 鏀寔澶氳绉嶅尮閰嶏細涓枃 / 鑻辨枃 / 鏃ユ枃 / 闊╂枃 / 淇勬枃 / 闃挎媺浼枃 绛夛紝鏀寔閫楀彿鍒嗛殧锛屼緥濡傦細<span class="text-amber-400">鏃ユ枃,闊╂枃</span>
+ </template>
+ <template v-else>
+ 馃挕 鏀寔鏇查鍏抽敭璇嶏紝鍙€楀彿鍒嗛殧锛屼緥濡傦細<span class="text-emerald-400">DJ,鎽囨粴,rap</span>
+ </template>
+ </p>
+ </div>
+
<div class="xl:col-span-4 space-y-2">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>灏佺鍘熷洜 (鍙€?</label
@@ -103,7 +126,7 @@
<CustomSelect
label="绫诲瀷绛涢€?
:value="typeFilterLabel"
- :options="['鍏ㄩ儴绫诲瀷', '鍏蜂綋姝屾洸', '鍏抽敭璇?]"
+ :options="['鍏ㄩ儴绫诲瀷', '鍏蜂綋姝屾洸', '鍏抽敭璇?, '璇', '鏇查']"
class="flex-1 md:w-48"
@change="handleTypeFilterChange"
/>
@@ -142,27 +165,21 @@
<div class="flex-1 flex items-start gap-5">
<div
class="shrink-0 w-12 h-12 rounded-lg flex items-center justify-center border transition-all"
- :class="[
- item.type === 'SONG'
- ? 'bg-blue-600/10 text-blue-500 border-blue-500/20 shadow-lg shadow-blue-900/5'
- : 'bg-purple-600/10 text-purple-500 border-purple-500/20 shadow-lg shadow-purple-900/5'
- ]"
+ :class="getTypeIconClass(item.type)"
>
<Music v-if="item.type === 'SONG'" :size="22" />
- <Type v-else :size="22" />
+ <Type v-else-if="item.type === 'KEYWORD'" :size="22" />
+ <Languages v-else-if="item.type === 'LANGUAGE'" :size="22" />
+ <Disc3 v-else :size="22" />
</div>
<div class="space-y-1.5 min-w-0">
<div class="flex items-center gap-3">
<span
class="text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded border"
- :class="[
- item.type === 'SONG'
- ? 'bg-blue-600/10 text-blue-400 border-blue-500/20'
- : 'bg-purple-600/10 text-purple-400 border-purple-500/20'
- ]"
+ :class="getTypeBadgeClass(item.type)"
>
- {{ item.type === 'SONG' ? '鍏蜂綋姝屾洸' : '鍏抽敭璇? }}
+ {{ getTypeLabel(item.type) }}
</span>
<h5 class="text-base font-black text-zinc-100 truncate tracking-tight">
{{ item.value }}
@@ -318,7 +335,7 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { onMounted, reactive, ref, computed } from 'vue'
import {
ShieldAlert,
@@ -334,7 +351,9 @@ import {
Ban,
ChevronLeft,
ChevronRight,
- ChevronDown
+ ChevronDown,
+ Languages,
+ Disc3
} from '@lucide/vue'
import { useToast } from '~/composables/useToast'
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
@@ -371,9 +390,65 @@ const pagination = reactive({
const typeFilterLabel = computed(() => {
if (filters.type === 'SONG') return '鍏蜂綋姝屾洸'
if (filters.type === 'KEYWORD') return '鍏抽敭璇?
+ if (filters.type === 'LANGUAGE') return '璇'
+ if (filters.type === 'GENRE') return '鏇查'
return '鍏ㄩ儴绫诲瀷'
})
+const typeValueLabel = computed(() => {
+ if (newItem.type === 'SONG') return '姝屾洸鍚嶇О - 鑹烘湳瀹?
+ if (newItem.type === 'KEYWORD') return '鍏抽敭璇嶅唴瀹?
+ if (newItem.type === 'LANGUAGE') return '璇鍚嶇О'
+ if (newItem.type === 'GENRE') return '鏇查鍚嶇О'
+ return '鍐呭'
+})
+
+const typePlaceholder = computed(() => {
+ if (newItem.type === 'SONG') return '渚嬪: 姝屾洸鍚?- 鑹烘湳瀹?
+ if (newItem.type === 'KEYWORD') return '渚嬪: 鏁忔劅璇嶃€佺壒瀹氶鏍?
+ if (newItem.type === 'LANGUAGE') return '渚嬪: 鏃ユ枃 鎴?涓枃,鑻辨枃,鏃ユ枃'
+ if (newItem.type === 'GENRE') return '渚嬪: DJ 鎴?娴佽,鎽囨粴,rap'
+ return '璇疯緭鍏ュ唴瀹?
+})
+
+const getTypeLabel = (type: string) => {
+ if (type === 'SONG') return '鍏蜂綋姝屾洸'
+ if (type === 'KEYWORD') return '鍏抽敭璇?
+ if (type === 'LANGUAGE') return '璇'
+ if (type === 'GENRE') return '鏇查'
+ return type
+}
+
+const getTypeIconClass = (type: string) => {
+ switch (type) {
+ case 'SONG':
+ return 'bg-blue-600/10 text-blue-500 border-blue-500/20 shadow-lg shadow-blue-900/5'
+ case 'KEYWORD':
+ return 'bg-purple-600/10 text-purple-500 border-purple-500/20 shadow-lg shadow-purple-900/5'
+ case 'LANGUAGE':
+ return 'bg-amber-600/10 text-amber-500 border-amber-500/20 shadow-lg shadow-amber-900/5'
+ case 'GENRE':
+ return 'bg-emerald-600/10 text-emerald-500 border-emerald-500/20 shadow-lg shadow-emerald-900/5'
+ default:
+ return 'bg-zinc-600/10 text-zinc-500 border-zinc-500/20'
+ }
+}
+
+const getTypeBadgeClass = (type: string) => {
+ switch (type) {
+ case 'SONG':
+ return 'bg-blue-600/10 text-blue-400 border-blue-500/20'
+ case 'KEYWORD':
+ return 'bg-purple-600/10 text-purple-400 border-purple-500/20'
+ case 'LANGUAGE':
+ return 'bg-amber-600/10 text-amber-400 border-amber-500/20'
+ case 'GENRE':
+ return 'bg-emerald-600/10 text-emerald-400 border-emerald-500/20'
+ default:
+ return 'bg-zinc-600/10 text-zinc-400 border-zinc-500/20'
+ }
+}
+
let searchTimeout = null
// 鍔犺浇榛戝悕鍗?@@ -404,10 +479,11 @@ const loadBlacklist = async () => {
}
}
-// 澶勭悊绫诲瀷绛涢€夊彉鏇? const handleTypeFilterChange = (label) => {
if (label === '鍏蜂綋姝屾洸') filters.type = 'SONG'
else if (label === '鍏抽敭璇?) filters.type = 'KEYWORD'
+ else if (label === '璇') filters.type = 'LANGUAGE'
+ else if (label === '鏇查') filters.type = 'GENRE'
else filters.type = ''
pagination.page = 1
@@ -569,7 +645,7 @@ onMounted(() => {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
- background: #27272a;
+ background: var(--bg-tertiary, #27272a);
border-radius: 10px;
}
</style>
diff --git a/app/components/Admin/DataAnalysisPanel.vue b/app/components/Admin/DataAnalysisPanel.vue
index d4d91c8..0fa7fb6 100644
--- a/app/components/Admin/DataAnalysisPanel.vue
+++ b/app/components/Admin/DataAnalysisPanel.vue
@@ -395,43 +395,53 @@
</div>
<div
v-else-if="activeUsers.length > 0"
- class="flex-1 space-y-3 overflow-y-auto custom-scrollbar pr-2"
+ class="relative max-h-[400px] overflow-hidden"
>
- <div
- v-for="(user, i) in activeUsers.slice(0, 5)"
- :key="i"
- class="relative p-5 bg-zinc-950/30 border border-zinc-800/50 rounded-3xl overflow-hidden group"
- >
- <div class="flex items-center gap-4 relative z-10">
- <div
- class="w-12 h-12 rounded-2xl bg-zinc-800 flex items-center justify-center font-black text-zinc-500 group-hover:text-zinc-200 transition-colors"
- >
- {{ user.name.charAt(0) }}
- </div>
- <div class="flex-1">
- <h4 class="text-sm font-bold text-zinc-100">{{ user.name }}</h4>
- <p class="text-xs text-zinc-600 font-medium mt-1">
- {{ user.contributions }}棣栨姇绋?路 {{ user.likes }}娆$偣璧?- </p>
- </div>
- <div class="text-right">
- <span
- class="text-xl font-black text-zinc-300 group-hover:text-purple-400 transition-colors"
- >{{ user.activityScore }}</span
+ <div class="space-y-3 overflow-y-auto custom-scrollbar pr-2 max-h-[400px] pb-6">
+ <div
+ v-for="(user, i) in activeUsers.slice(0, 5)"
+ :key="i"
+ class="relative p-5 bg-zinc-950/30 border border-zinc-800/50 rounded-3xl overflow-hidden group"
+ >
+ <div class="flex items-center gap-4 relative z-10">
+ <img
+ v-if="user.avatarUrl && !failedAvatars[user.id]"
+ :src="user.avatarUrl"
+ :alt="user.name"
+ class="w-12 h-12 rounded-2xl object-cover border border-zinc-700/50 shrink-0"
+ @error="handleAvatarError($event, user)"
>
- <p class="text-[10px] font-black text-zinc-700 uppercase tracking-widest">
- 娲昏穬搴?- </p>
+ <div
+ v-else
+ class="w-12 h-12 rounded-2xl bg-zinc-800 flex items-center justify-center font-black text-zinc-500 group-hover:text-zinc-200 transition-colors shrink-0"
+ >
+ {{ user.name.charAt(0) }}
+ </div>
+ <div class="flex-1 min-w-0">
+ <h4 class="text-sm font-bold text-zinc-100 truncate">{{ user.name }}</h4>
+ <p class="text-xs text-zinc-600 font-medium mt-1 truncate">
+ {{ user.contributions }}棣栨姇绋?路 {{ user.likes }}娆$偣璧?+ </p>
+ </div>
+ <div class="text-right shrink-0">
+ <span
+ class="text-xl font-black text-zinc-300 group-hover:text-purple-400 transition-colors"
+ >{{ user.activityScore }}</span
+ >
+ <p class="text-[10px] font-black text-zinc-700 uppercase tracking-widest">
+ 娲昏穬搴?+ </p>
+ </div>
</div>
+ <div
+ class="absolute bottom-0 left-0 h-1 bg-purple-500/20 group-hover:bg-purple-500/40 transition-all"
+ :style="{
+ width: `${(user.activityScore / Math.max(...activeUsers.map((u) => u.activityScore), 1)) * 100}%`
+ }"
+ />
</div>
- <!-- 杩涘害鏉℃寚绀哄櫒 -->
- <div
- class="absolute bottom-0 left-0 h-1 bg-purple-500/20 group-hover:bg-purple-500/40 transition-all"
- :style="{
- width: `${(user.activityScore / Math.max(...activeUsers.map((u) => u.activityScore), 1)) * 100}%`
- }"
- />
</div>
+ <div class="absolute bottom-0 left-0 right-0 h-16 pointer-events-none bg-gradient-to-t from-zinc-900/90 via-zinc-900/40 to-transparent rounded-b-[2rem]" />
</div>
<div v-else class="flex-1 flex flex-col items-center justify-center text-zinc-700">
<Users :size="32" class="opacity-20 mb-2" />
@@ -537,14 +547,14 @@
@mouseleave="handleTooltipMouseLeave"
>
<div
- class="bg-zinc-900 border border-zinc-800 rounded-3xl shadow-2xl overflow-hidden min-w-[320px] backdrop-blur-xl bg-opacity-90 animate-in fade-in zoom-in duration-200"
+ class="bg-zinc-900 border border-zinc-800 rounded-3xl shadow-2xl overflow-hidden min-w-[320px] max-w-[380px] backdrop-blur-xl bg-opacity-90 animate-in fade-in zoom-in duration-200"
>
<div
- class="p-6 border-b border-zinc-800/50 bg-gradient-to-br from-blue-500/10 to-transparent"
+ class="p-6 border-b border-zinc-800/50 bg-gradient-to-br from-blue-500/10 to-transparent max-h-[500px] overflow-y-auto custom-scrollbar"
>
<div class="flex items-center justify-between mb-4">
- <h4 class="text-sm font-black text-white uppercase tracking-widest">娲昏穬鐢ㄦ埛璇︽儏</h4>
- <div class="flex items-center gap-2 px-2 py-1 bg-blue-500/20 rounded-full">
+ <h4 class="text-sm font-black text-white uppercase tracking-widest truncate max-h-[20px] overflow-hidden">娲昏穬鐢ㄦ埛璇︽儏</h4>
+ <div class="flex items-center gap-2 px-2 py-1 bg-blue-500/20 rounded-full shrink-0">
<div class="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
<span class="text-[10px] font-black text-blue-400"
>{{ realtimeStats.activeUsers }} 鍦ㄧ嚎</span
@@ -554,44 +564,52 @@
<div
v-if="realtimeStats.activeUsersList && realtimeStats.activeUsersList.length > 0"
- class="space-y-4"
+ class="space-y-3 max-h-[350px] overflow-y-auto custom-scrollbar pr-1"
>
<div
v-for="user in realtimeStats.activeUsersList.slice(0, 5)"
:key="user.id"
class="flex items-center gap-3 p-3 bg-zinc-950/50 border border-zinc-800/50 rounded-2xl group hover:border-blue-500/30 transition-all"
>
+ <img
+ v-if="user.avatarUrl && !failedAvatars[user.id]"
+ :src="user.avatarUrl"
+ :alt="user.name"
+ class="w-10 h-10 rounded-xl object-cover border border-zinc-700/50 shrink-0"
+ @error="handleAvatarError($event, user)"
+ >
<div
- class="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center font-black text-zinc-500 group-hover:text-blue-400 transition-colors"
+ v-else
+ class="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center font-black text-zinc-500 group-hover:text-blue-400 transition-colors shrink-0"
>
{{ user.name.charAt(0) }}
</div>
<div class="flex-1 min-w-0">
- <div class="text-xs font-bold text-zinc-200 truncate">{{ user.name }}</div>
- <div class="text-[10px] text-zinc-500 font-medium truncate">
+ <div class="text-xs font-bold text-zinc-200 truncate max-h-[16px] overflow-hidden">{{ user.name }}</div>
+ <div class="text-[10px] text-zinc-500 font-medium truncate max-h-[14px] overflow-hidden">
@{{ user.username }}
</div>
</div>
- <div class="text-[10px] font-black text-zinc-600 bg-zinc-900 px-2 py-1 rounded-lg">
+ <div class="text-[10px] font-black text-zinc-600 bg-zinc-900 px-2 py-1 rounded-lg shrink-0">
{{ user.lastActive }}
</div>
</div>
<div v-if="realtimeStats.activeUsersList.length > 5" class="text-center py-2">
- <span class="text-[10px] font-black text-zinc-600 uppercase tracking-widest"
+ <span class="text-[10px] font-black text-zinc-600 uppercase tracking-widest max-h-[14px] overflow-hidden block"
>鍙婂叾浠?{{ realtimeStats.activeUsersList.length - 5 }} 浣嶇敤鎴?/span
>
</div>
</div>
<div v-else class="py-10 flex flex-col items-center justify-center text-zinc-600">
<Users :size="32" class="opacity-20 mb-3" />
- <p class="text-xs font-black uppercase tracking-widest">鏆傛棤鍦ㄧ嚎鐢ㄦ埛</p>
+ <p class="text-xs font-black uppercase tracking-widest truncate max-h-[16px] overflow-hidden">鏆傛棤鍦ㄧ嚎鐢ㄦ埛</p>
</div>
</div>
<div class="px-6 py-4 bg-zinc-950/50 flex items-center justify-between">
- <span class="text-[10px] font-black text-zinc-600 uppercase tracking-widest"
+ <span class="text-[10px] font-black text-zinc-600 uppercase tracking-widest truncate max-h-[14px] overflow-hidden"
>瀹炴椂鏁版嵁鍚屾涓?/span
>
- <Activity :size="12" class="text-blue-500 animate-pulse" />
+ <Activity :size="12" class="text-blue-500 animate-pulse shrink-0" />
</div>
</div>
</div>
@@ -600,7 +618,7 @@
</template>
<script setup>
-import { onMounted, ref, computed } from 'vue'
+import { onMounted, ref, computed, reactive } from 'vue'
import {
TrendingUp,
Users,
@@ -727,6 +745,17 @@ const tooltip = ref({
}
})
+// 澶村儚鍔犺浇澶辫触鐘舵€佺鐞?+const failedAvatars = reactive({})
+const handleAvatarError = (event, user) => {
+ if (user && user.id) {
+ failedAvatars[user.id] = true
+ }
+ if (event && event.target) {
+ event.target.style.display = 'none'
+ }
+}
+
// 榧犳爣杩涘叆浜嬩欢澶勭悊
const handleMouseEnter = (event) => {
const rect = event.target.getBoundingClientRect()
diff --git a/app/components/Admin/EmailTemplateManager.vue b/app/components/Admin/EmailTemplateManager.vue
index 89fda18..a995f31 100644
--- a/app/components/Admin/EmailTemplateManager.vue
+++ b/app/components/Admin/EmailTemplateManager.vue
@@ -336,7 +336,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/NotificationSender.vue b/app/components/Admin/NotificationSender.vue
index de4cd46..23bd005 100644
--- a/app/components/Admin/NotificationSender.vue
+++ b/app/components/Admin/NotificationSender.vue
@@ -742,7 +742,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/OverviewDashboard.vue b/app/components/Admin/OverviewDashboard.vue
index 7ac7086..148318c 100644
--- a/app/components/Admin/OverviewDashboard.vue
+++ b/app/components/Admin/OverviewDashboard.vue
@@ -366,7 +366,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/RequestTimeManager.vue b/app/components/Admin/RequestTimeManager.vue
index bf35bb6..762281f 100644
--- a/app/components/Admin/RequestTimeManager.vue
+++ b/app/components/Admin/RequestTimeManager.vue
@@ -842,7 +842,7 @@ const cancelForm = () => {
background: transparent;
}
::-webkit-scrollbar-thumb {
- background: #27272a;
+ background: var(--bg-tertiary, #27272a);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
diff --git a/app/components/Admin/ScheduleItemPrint.vue b/app/components/Admin/ScheduleItemPrint.vue
index 5d131b1..703e42a 100644
--- a/app/components/Admin/ScheduleItemPrint.vue
+++ b/app/components/Admin/ScheduleItemPrint.vue
@@ -7,16 +7,16 @@
</div>
<!-- 姝屾洸灏侀潰 -->
- <div v-if="settings.showCover" class="cover-section">
+ <div v-if="settings.showCover" class="cover-section aspect-square">
<img
v-if="schedule.song.cover"
:alt="schedule.song.title"
:src="convertToHttps(schedule.song.cover)"
- class="song-cover"
+ class="song-cover object-cover"
referrerpolicy="no-referrer"
@error="handleImageError"
>
- <div class="cover-placeholder" :class="{ 'show': !schedule.song.cover }">
+ <div class="cover-placeholder object-contain" :class="{ 'show': !schedule.song.cover }">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="3" />
<path d="M12 1v6m0 6v6" />
@@ -112,13 +112,14 @@ const handleImageError = (event) => {
justify-content: center;
font-weight: bold;
font-size: 14px;
- color: #333;
+ color: var(--text-quaternary, #333333);
flex-shrink: 0;
}
.cover-section {
width: 40px;
height: 40px;
+ aspect-ratio: 1 / 1;
flex-shrink: 0;
}
@@ -127,17 +128,20 @@ const handleImageError = (event) => {
height: 100%;
object-fit: cover;
border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.cover-placeholder {
width: 100%;
height: 100%;
+ aspect-ratio: 1 / 1;
+ object-fit: contain;
background: #f5f5f5;
border-radius: 4px;
display: none;
align-items: center;
justify-content: center;
- color: #999;
+ color: var(--text-tertiary, #999999);
}
.cover-placeholder.show {
@@ -157,7 +161,7 @@ const handleImageError = (event) => {
.song-title {
font-weight: bold;
font-size: 16px;
- color: #333;
+ color: var(--text-quaternary, #333333);
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
@@ -172,7 +176,7 @@ const handleImageError = (event) => {
display: inline-block;
padding: 1px 4px;
background: #e3f2fd;
- border: 1px solid #2196f3;
+ border: 1px solid var(--border-secondary, #2196f3);
border-radius: 3px;
color: #1976d2;
font-size: 10px;
@@ -182,7 +186,7 @@ const handleImageError = (event) => {
.song-artist {
font-size: 14px;
- color: #666;
+ color: var(--text-quaternary, #666666);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -193,7 +197,7 @@ const handleImageError = (event) => {
display: flex;
align-items: center;
font-size: 12px;
- color: #666;
+ color: var(--text-quaternary, #666666);
white-space: nowrap;
}
@@ -203,14 +207,14 @@ const handleImageError = (event) => {
}
.value {
- color: #333;
+ color: var(--text-quaternary, #333333);
}
/* 鎵撳嵃鏍峰紡 */
@media print {
.schedule-item-print {
- color: #000 !important;
- background: #fff !important;
+ color: var(--text-primary, #000) !important;
+ background: var(--card-bg, #fff) !important;
width: 100% !important;
max-width: none !important;
box-sizing: border-box !important;
@@ -226,25 +230,41 @@ const handleImageError = (event) => {
.sequence-number {
background: #f0f0f0 !important;
- color: #000 !important;
+ color: var(--text-primary, #000) !important;
width: 30px !important;
height: 30px !important;
flex-shrink: 0 !important;
}
.cover-section {
- width: 40px !important;
- height: 40px !important;
+ width: 48px !important;
+ height: 48px !important;
+ aspect-ratio: 1 / 1 !important;
flex-shrink: 0 !important;
}
+ .song-cover {
+ width: 100% !important;
+ height: 100% !important;
+ object-fit: contain !important;
+ border-radius: 0 !important;
+ box-shadow: none !important;
+ }
+
+ .cover-placeholder {
+ width: 100% !important;
+ height: 100% !important;
+ object-fit: contain !important;
+ border-radius: 0 !important;
+ }
+
.song-info {
flex: 1 !important;
min-width: 0 !important;
}
.song-title {
- color: #000 !important;
+ color: var(--text-primary, #000) !important;
font-size: 16px !important;
white-space: nowrap !important;
overflow: hidden !important;
@@ -254,11 +274,11 @@ const handleImageError = (event) => {
.song-artist,
.label,
.value {
- color: #333 !important;
+ color: var(--text-quaternary, #333333) !important;
}
.time-range {
- color: #666 !important;
+ color: var(--text-quaternary, #666666) !important;
}
.requester-info,
diff --git a/app/components/Admin/ScheduleManager.vue b/app/components/Admin/ScheduleManager.vue
index 2c4d780..b293fbc 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" /> 淇濆瓨骞跺彂甯?@@ -795,7 +819,7 @@
/>
</template>
-<script setup>
+<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch, inject } from 'vue'
import {
Search,
@@ -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] 瑙f瀽鏈湴閰嶇疆澶辫触锛屼娇鐢ㄩ粯璁ゅ€?, 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)
+}
+
+// 鎶曠浜鸿仈鍚堟暟璁$畻锛坮equesterId + 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()
- // 鍒濆鍖栫獥鍙eぇ灏忔娴? 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/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 @@
>
<RotateCcw :size="14" class="text-blue-500/70" /> 鏈挱鏀?({{ selectedSongs.length }})
</button>
+ <button
+ class="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-amber-400 text-[11px] font-black rounded-lg border border-zinc-700/50 transition-all active:scale-95 uppercase tracking-widest"
+ @click="batchReject"
+ >
+ <X :size="14" class="text-amber-500/70" /> 椹冲洖 ({{ selectedSongs.length }})
+ </button>
<button
class="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-red-400 text-[11px] font-black rounded-lg border border-zinc-700/50 transition-all active:scale-95 uppercase tracking-widest"
@click="batchDelete"
@@ -408,7 +414,7 @@
@click.stop
>
<div class="px-8 py-6 border-b border-zinc-800/50 flex items-center justify-between">
- <h3 class="text-xl font-black text-zinc-100">椹冲洖姝屾洸</h3>
+ <h3 class="text-xl font-black text-zinc-100">{{ isBatchReject ? `鎵归噺椹冲洖姝屾洸 (${selectedSongs.length} 棣?` : '椹冲洖姝屾洸' }}</h3>
<button
class="text-zinc-500 hover:text-zinc-300 transition-colors"
@click="cancelReject"
@@ -418,7 +424,7 @@
</div>
<div class="p-8 space-y-6">
- <div class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
+ <div v-if="!isBatchReject" class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
<div
class="w-10 h-10 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center border border-red-500/10"
>
@@ -429,6 +435,17 @@
<p class="text-xs text-zinc-500">鎶曠浜? {{ rejectSongInfo.requester }}</p>
</div>
</div>
+ <div v-else class="p-5 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center gap-4">
+ <div
+ class="w-10 h-10 rounded-lg bg-amber-500/10 text-amber-500 flex items-center justify-center border border-amber-500/10"
+ >
+ <ListMusic :size="18" />
+ </div>
+ <div>
+ <h4 class="font-bold text-zinc-100 text-sm">鍗冲皢鎵归噺椹冲洖 {{ selectedSongs.length }} 棣栨瓕鏇?/h4>
+ <p class="text-xs text-zinc-500">椹冲洖鍚庡皢涓€骞舵竻鐞嗘姇绁ㄣ€佹帓鏈熻褰曞苟鍙戦€侀€氱煡缁欏搴旀姇绋夸汉</p>
+ </div>
+ </div>
<div class="space-y-2">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
@@ -944,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'
@@ -961,6 +978,7 @@ import {
X,
Trash2,
Music,
+ ListMusic,
Heart,
Download,
ChevronDown,
@@ -1040,6 +1058,7 @@ const showRejectDialog = ref(false)
const rejectLoading = ref(false)
const rejectReason = ref('')
const addToBlacklist = ref(false)
+const isBatchReject = ref(false)
const rejectSongInfo = ref({
id: null,
title: '',
@@ -1475,6 +1494,7 @@ const rejectSong = (songId) => {
const song = songs.value.find((s) => s.id === songId)
if (!song) return
+ isBatchReject.value = false
rejectSongInfo.value = {
id: song.id,
title: song.title || '',
@@ -1487,6 +1507,22 @@ const rejectSong = (songId) => {
showRejectDialog.value = true
}
+// 鎵归噺椹冲洖姝屾洸
+const batchReject = () => {
+ if (selectedSongs.value.length === 0) return
+
+ isBatchReject.value = true
+ rejectSongInfo.value = {
+ id: null,
+ title: '',
+ artist: '',
+ requester: ''
+ }
+ rejectReason.value = ''
+ addToBlacklist.value = false
+ showRejectDialog.value = true
+}
+
// 纭椹冲洖
const confirmReject = async () => {
if (!rejectReason.value.trim()) {
@@ -1496,26 +1532,40 @@ const confirmReject = async () => {
rejectLoading.value = true
try {
- await $fetch('/api/admin/songs/reject', {
- method: 'POST',
- body: {
- songId: rejectSongInfo.value.id,
- reason: rejectReason.value.trim(),
- addToBlacklist: addToBlacklist.value
+ if (isBatchReject.value) {
+ await $fetch('/api/admin/songs/batch-reject', {
+ method: 'POST',
+ body: {
+ songIds: selectedSongs.value,
+ reason: rejectReason.value.trim(),
+ addToBlacklist: addToBlacklist.value
+ }
+ })
+ await refreshSongs(true)
+ selectedSongs.value = []
+ showNotification(`鎵归噺椹冲洖鎴愬姛锛屽叡澶勭悊 ${selectedSongs.value.length} 棣栨瓕鏇瞏, 'success')
+ } else {
+ await $fetch('/api/admin/songs/reject', {
+ method: 'POST',
+ body: {
+ songId: rejectSongInfo.value.id,
+ reason: rejectReason.value.trim(),
+ addToBlacklist: addToBlacklist.value
+ }
+ })
+
+ await refreshSongs(true)
+
+ const index = selectedSongs.value.indexOf(rejectSongInfo.value.id)
+ if (index > -1) {
+ selectedSongs.value.splice(index, 1)
}
- })
- await refreshSongs(true)
-
- const index = selectedSongs.value.indexOf(rejectSongInfo.value.id)
- if (index > -1) {
- selectedSongs.value.splice(index, 1)
+ showNotification('姝屾洸椹冲洖鎴愬姛锛屽凡閫氱煡鎶曠浜?, 'success')
}
showRejectDialog.value = false
-
- showNotification('姝屾洸椹冲洖鎴愬姛锛屽凡閫氱煡鎶曠浜?, 'success')
- } catch (error) {
+ } catch (error: any) {
console.error('椹冲洖姝屾洸澶辫触:', error)
showNotification('椹冲洖澶辫触: ' + (error.data?.message || error.message), 'error')
} finally {
@@ -1528,6 +1578,7 @@ const cancelReject = () => {
showRejectDialog.value = false
rejectReason.value = ''
addToBlacklist.value = false
+ isBatchReject.value = false
rejectSongInfo.value = {
id: null,
title: '',
@@ -2128,7 +2179,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/UserManager.vue b/app/components/Admin/UserManager.vue
index 7252dc2..7af836d 100644
--- a/app/components/Admin/UserManager.vue
+++ b/app/components/Admin/UserManager.vue
@@ -221,9 +221,9 @@
class="flex items-center justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-all action-buttons"
>
<button
- :disabled="isSelf(user)"
+ :disabled="isSelf(user) || !canManageTargetUser(user)"
class="p-2 bg-zinc-950 border border-zinc-800 rounded-xl text-zinc-500 hover:text-blue-400 transition-colors disabled:opacity-20 disabled:cursor-not-allowed action-btn"
- title="缂栬緫鐢ㄦ埛"
+ :title="!canManageTargetUser(user) ? '鏉冮檺涓嶈冻锛氭棤娉曠紪杈戣鐢ㄦ埛' : '缂栬緫鐢ㄦ埛'"
@click="editUser(user)"
>
<Edit2 :size="13" />
@@ -236,17 +236,17 @@
<Music :size="13" />
</button>
<button
- :disabled="isSelf(user)"
+ :disabled="isSelf(user) || !canResetPasswordFor(user)"
class="p-2 bg-zinc-950 border border-zinc-800 rounded-xl text-zinc-500 hover:text-amber-400 transition-colors disabled:opacity-20 disabled:cursor-not-allowed action-btn"
- title="閲嶇疆瀵嗙爜"
+ :title="!canResetPasswordFor(user) ? '鏉冮檺涓嶈冻锛氭棤娉曢噸缃鐢ㄦ埛鐨勫瘑鐮? : '閲嶇疆瀵嗙爜'"
@click="resetPassword(user)"
>
<Lock :size="13" />
</button>
<button
- :disabled="isSelf(user)"
+ :disabled="isSelf(user) || !canManageTargetUser(user)"
class="p-2 bg-zinc-950 border border-zinc-800 rounded-xl text-zinc-500 hover:text-red-400 transition-colors disabled:opacity-20 disabled:cursor-not-allowed action-btn"
- title="鍒犻櫎鐢ㄦ埛"
+ :title="!canManageTargetUser(user) ? '鏉冮檺涓嶈冻锛氭棤娉曞垹闄よ鐢ㄦ埛' : '鍒犻櫎鐢ㄦ埛'"
@click="confirmDeleteUser(user)"
>
<Trash2 :size="13" />
@@ -363,8 +363,9 @@
<div class="flex gap-2 action-buttons">
<button
- :disabled="isSelf(user)"
+ :disabled="isSelf(user) || !canManageTargetUser(user)"
class="flex-1 py-2.5 bg-zinc-950 border border-zinc-800 rounded-lg text-zinc-400 flex items-center justify-center gap-2 text-[10px] font-black uppercase tracking-widest active:bg-blue-600 active:text-white transition-colors disabled:opacity-20 action-btn"
+ :title="!canManageTargetUser(user) ? '鏉冮檺涓嶈冻锛氭棤娉曠紪杈戣鐢ㄦ埛' : '缂栬緫鐢ㄦ埛'"
@click="editUser(user)"
>
<Edit2 :size="12" /> 缂栬緫
@@ -376,15 +377,17 @@
<Music :size="12" /> 璁板綍
</button>
<button
- :disabled="isSelf(user)"
+ :disabled="isSelf(user) || !canResetPasswordFor(user)"
class="flex-1 py-2.5 bg-zinc-950 border border-zinc-800 rounded-lg text-zinc-400 flex items-center justify-center gap-2 text-[10px] font-black uppercase tracking-widest active:bg-amber-600 active:text-white transition-colors disabled:opacity-20 action-btn"
+ :title="!canResetPasswordFor(user) ? '鏉冮檺涓嶈冻锛氭棤娉曢噸缃鐢ㄦ埛鐨勫瘑鐮? : '閲嶇疆瀵嗙爜'"
@click="resetPassword(user)"
>
<Lock :size="12" /> 閲嶇疆
</button>
<button
- :disabled="isSelf(user)"
+ :disabled="isSelf(user) || !canManageTargetUser(user)"
class="px-3 py-2.5 bg-zinc-950 border border-zinc-800 rounded-lg text-zinc-400 active:bg-red-600 active:text-white transition-colors disabled:opacity-20 action-btn"
+ :title="!canManageTargetUser(user) ? '鏉冮檺涓嶈冻锛氭棤娉曞垹闄よ鐢ㄦ埛' : '鍒犻櫎鐢ㄦ埛'"
@click="confirmDeleteUser(user)"
>
<Trash2 :size="14" />
@@ -1493,6 +1496,25 @@ const passwordForm = ref({
const auth = useAuth()
const permissions = usePermissions()
+// 鍓嶇渚ц鑹茬骇鍒槧灏勶紙涓?server/utils/permissions.js 鐨?ROLE_LEVELS 淇濇寔涓€鑷达級
+const ROLE_LEVELS = {
+ SUPER_ADMIN: 100,
+ ADMIN: 80,
+ MINISTER: 80,
+ VICE_MINISTER: 70,
+ SONG_ADMIN: 60,
+ STAFF: 60,
+ RESIDENT: 50,
+ TEACHER: 40,
+ USER: 10
+}
+
+const getRoleLevel = (roleName) => {
+ if (!roleName) return 0
+ const name = String(roleName).toUpperCase()
+ return ROLE_LEVELS[name] ?? 0
+}
+
// 鍒ゆ柇鏄惁涓哄綋鍓嶇櫥褰曠敤鎴? const isSelf = (user) => {
return auth.user.value?.id === user?.id
@@ -1503,14 +1525,53 @@ const isSuperAdmin = computed(() => {
return auth.user.value?.role === 'SUPER_ADMIN'
})
+const currentUserRoleLevel = computed(() => getRoleLevel(auth.user.value?.role))
+
+// 鏄惁涓烘櫘閫氱鐞嗗憳锛圓DMIN/MINISTER 绾у埆锛岄潪 SUPER锛?+const isAdminOnly = computed(() => {
+ return !isSuperAdmin.value && currentUserRoleLevel.value >= 80
+})
+
+// 鎿嶄綔鑰呰兘鍚﹀鐩爣鐢ㄦ埛鎵ц绠$悊鎿嶄綔锛堢洰鏍囩骇鍒繀椤讳弗鏍间綆浜庢搷浣滆€咃紝涓旂洰鏍囦笉鏄?ID=1锛?+const canManageTargetUser = (targetUser) => {
+ if (!targetUser) return false
+ if (isSelf(targetUser)) return false
+ if (String(targetUser.id) === '1') return false
+ if (isSuperAdmin.value) return true
+ const targetLevel = getRoleLevel(targetUser.role)
+ return currentUserRoleLevel.value > targetLevel
+}
+
+// 鎿嶄綔鑰呰兘鍚﹂噸缃洰鏍囩敤鎴风殑瀵嗙爜锛圓DMIN 浠呭彲閲嶇疆 USER 鍙婁互涓嬶級
+const canResetPasswordFor = (targetUser) => {
+ if (!canManageTargetUser(targetUser)) return false
+ if (isSuperAdmin.value) return true
+ // 鏅€?ADMIN 鍙兘閲嶇疆绾у埆 <= USER(10) 鐨勭敤鎴?+ const targetLevel = getRoleLevel(targetUser.role)
+ return targetLevel <= ROLE_LEVELS.USER
+}
+
+// 鎿嶄綔鑰呰兘鍚﹀皢瑙掕壊鏀逛负鎸囧畾 newRole
+const canAssignRole = (newRole) => {
+ if (!newRole) return false
+ if (isSuperAdmin.value) {
+ return String(newRole).toUpperCase() !== 'SUPER_ADMIN'
+ }
+ const targetLevel = getRoleLevel(newRole)
+ return currentUserRoleLevel.value > targetLevel
+}
+
const availableRoles = computed(() => {
if (isSuperAdmin.value) {
// 瓒呯骇绠$悊鍛樺彲浠ュ垎閰嶉櫎鑷繁浠ュ鐨勬墍鏈夎鑹? return allRoles.value.filter((role) => role.name !== 'SUPER_ADMIN')
- } else {
- // 鍏朵粬瑙掕壊涓嶈兘鍒嗛厤瑙掕壊锛岃繑鍥炵┖鏁扮粍
- return []
}
+ // 鏅€?ADMIN/MINISTER 绛夊彧鑳藉垎閰嶇骇鍒弗鏍间綆浜庤嚜宸辩殑瑙掕壊锛圫UPER_ADMIN 宸插湪涓婂眰鎺掗櫎锛?+ return allRoles.value.filter(
+ (role) =>
+ role.name !== 'SUPER_ADMIN' &&
+ getRoleLevel(role.name) < currentUserRoleLevel.value
+ )
})
// 鐢变簬浣跨敤鏈嶅姟鍣ㄧ鍒嗛〉锛岃繖浜涜绠楀睘鎬х畝鍖栦簡
@@ -1594,13 +1655,18 @@ const getStatusDisplayName = (status) => {
}
const editUser = (user) => {
- // 绂佹缂栬緫鑷韩
if (isSelf(user)) {
if (window.$showNotification) {
window.$showNotification('绂佹鍦ㄧ敤鎴风鐞嗕腑淇敼鑷繁鐨勮处鎴?, 'warning')
}
return
}
+ if (!canManageTargetUser(user)) {
+ if (window.$showNotification) {
+ window.$showNotification('鏉冮檺涓嶈冻锛氭棤娉曠紪杈戣鐢ㄦ埛', 'warning')
+ }
+ return
+ }
editingUser.value = user
userForm.value = {
name: user.name,
@@ -1614,13 +1680,18 @@ const editUser = (user) => {
}
const resetPassword = (user) => {
- // 绂佹閲嶇疆鑷韩瀵嗙爜
if (isSelf(user)) {
if (window.$showNotification) {
window.$showNotification('绂佹鍦ㄧ敤鎴风鐞嗕腑閲嶇疆鑷繁鐨勫瘑鐮?, 'warning')
}
return
}
+ if (!canResetPasswordFor(user)) {
+ if (window.$showNotification) {
+ window.$showNotification('鏉冮檺涓嶈冻锛氭棤娉曢噸缃鐢ㄦ埛鐨勫瘑鐮?, 'warning')
+ }
+ return
+ }
resetPasswordUser.value = user
passwordForm.value = {
password: '',
@@ -1629,13 +1700,18 @@ const resetPassword = (user) => {
}
const confirmDeleteUser = (user) => {
- // 绂佹鍒犻櫎鑷韩
if (isSelf(user)) {
if (window.$showNotification) {
window.$showNotification('涓嶈兘鍒犻櫎鑷繁鐨勮处鎴?, 'warning')
}
return
}
+ if (!canManageTargetUser(user)) {
+ if (window.$showNotification) {
+ window.$showNotification('鏉冮檺涓嶈冻锛氭棤娉曞垹闄よ鐢ㄦ埛', 'warning')
+ }
+ return
+ }
deletingUser.value = user
showDeleteModal.value = true
}
@@ -1687,6 +1763,20 @@ const closeDeleteModal = () => {
const confirmDelete = async () => {
if (!deletingUser.value) return
+ if (isSelf(deletingUser.value)) {
+ if (window.$showNotification) {
+ window.$showNotification('涓嶈兘鍒犻櫎鑷繁鐨勮处鎴?, 'warning')
+ }
+ closeDeleteModal()
+ return
+ }
+ if (!canManageTargetUser(deletingUser.value)) {
+ if (window.$showNotification) {
+ window.$showNotification('鏉冮檺涓嶈冻锛氭棤娉曞垹闄よ鐢ㄦ埛', 'warning')
+ }
+ closeDeleteModal()
+ return
+ }
deleting.value = true
@@ -1737,11 +1827,22 @@ const closeResetPassword = () => {
}
const saveUser = async () => {
- // 淇濇姢锛氱姝繚瀛橀拡瀵硅嚜韬殑鏇存敼
if (editingUser.value && isSelf(editingUser.value)) {
formError.value = '绂佹鍦ㄧ敤鎴风鐞嗕腑淇敼鑷繁鐨勮处鎴?
return
}
+ if (editingUser.value && !canManageTargetUser(editingUser.value)) {
+ formError.value = '鏉冮檺涓嶈冻锛氭棤娉曠紪杈戣鐢ㄦ埛'
+ return
+ }
+ if (userForm.value.role && !canAssignRole(userForm.value.role)) {
+ formError.value = `鏉冮檺涓嶈冻锛氭棤娉曞垎閰嶈鑹?${userForm.value.role}`
+ return
+ }
+ if (userForm.value.password && editingUser.value && !canResetPasswordFor(editingUser.value)) {
+ formError.value = '鏉冮檺涓嶈冻锛氭棤娉曢噸缃鐢ㄦ埛鐨勫瘑鐮?
+ return
+ }
if (!userForm.value.name || !userForm.value.username) {
formError.value = '璇峰~鍐欏繀瑕佷俊鎭?
return
diff --git a/app/components/Admin/VotersModal.vue b/app/components/Admin/VotersModal.vue
index c14b029..52fe766 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;
@@ -223,14 +223,14 @@ watch(
.modal-title {
font-size: 20px;
font-weight: 600;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
margin: 0;
}
.close-btn {
background: none;
border: none;
- color: #888;
+ color: var(--text-tertiary, #888888);
cursor: pointer;
padding: 8px;
border-radius: 8px;
@@ -238,8 +238,8 @@ watch(
}
.close-btn:hover {
- background: #2a2a2a;
- color: #ffffff;
+ background: var(--bg-tertiary, #2a2a2a);
+ color: var(--text-primary, #ffffff);
}
.close-btn svg {
@@ -254,22 +254,22 @@ watch(
}
.song-info {
- background: #2a2a2a;
+ background: var(--bg-tertiary, #2a2a2a);
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
- border: 1px solid #3a3a3a;
+ border: 1px solid var(--border-secondary, #3a3a3a);
}
.song-title {
font-size: 18px;
font-weight: 600;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
margin: 0 0 8px 0;
}
.song-artist {
- color: #888;
+ color: var(--text-tertiary, #888888);
margin: 0 0 16px 0;
font-size: 14px;
}
@@ -304,7 +304,7 @@ watch(
.spinner {
width: 32px;
height: 32px;
- border: 3px solid #3a3a3a;
+ border: 3px solid var(--border-secondary, #3a3a3a);
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
@@ -366,12 +366,12 @@ watch(
align-items: center;
justify-content: space-between;
padding-bottom: 12px;
- border-bottom: 1px solid #3a3a3a;
+ border-bottom: 1px solid var(--border-secondary, #3a3a3a);
}
.voters-title {
font-weight: 600;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-size: 16px;
}
@@ -388,15 +388,15 @@ watch(
align-items: center;
justify-content: space-between;
padding: 12px 16px;
- background: #2a2a2a;
+ background: var(--bg-tertiary, #2a2a2a);
border-radius: 8px;
- border: 1px solid #3a3a3a;
+ border: 1px solid var(--border-secondary, #3a3a3a);
transition: all 0.2s ease;
}
.voter-item:hover {
- background: #333333;
- border-color: #4a4a4a;
+ background: var(--bg-quaternary, #333333);
+ border-color: var(--border-secondary, #4a4a4a);
}
.voter-info {
@@ -426,18 +426,18 @@ watch(
}
.voter-name {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-weight: 500;
font-size: 14px;
}
.vote-time {
- color: #888;
+ color: var(--text-tertiary, #888888);
font-size: 12px;
}
.voter-number {
- color: #888;
+ color: var(--text-tertiary, #888888);
font-size: 12px;
font-weight: 500;
}
@@ -454,11 +454,11 @@ watch(
.empty-icon {
width: 48px;
height: 48px;
- color: #666;
+ color: var(--text-quaternary, #666666);
}
.empty-state p {
- color: #888;
+ color: var(--text-tertiary, #888888);
margin: 0;
}
@@ -469,8 +469,8 @@ watch(
}
.close-button {
- background: #3a3a3a;
- color: #ffffff;
+ background: var(--bg-quaternary, #3a3a3a);
+ color: var(--text-primary, #ffffff);
border: none;
padding: 12px 24px;
border-radius: 8px;
@@ -490,7 +490,7 @@ watch(
}
.voters-container::-webkit-scrollbar-track {
- background: #2a2a2a;
+ background: var(--bg-tertiary, #2a2a2a);
border-radius: 3px;
}
diff --git a/app/components/Auth/LoginForm.vue b/app/components/Auth/LoginForm.vue
index 63472c8..430e302 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">
@@ -138,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>
@@ -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/Auth/OAuthButtons.vue b/app/components/Auth/OAuthButtons.vue
index 770f19d..d7a169f 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,27 +17,178 @@
<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')"
>
<AuthProvidersGoogleIcon />
</button>
- <!-- 鍏朵粬鎻愪緵鍟嗗彲鍦ㄦ娣诲姞 -->
+ <button
+ v-if="config.public.oauth.aggregate"
+ type="button"
+ class="oauth-btn oauth-btn-aggregate"
+ title="浣跨敤 QQ / 寰俊鐧诲綍"
+ @click="loginWith('aggregate')"
+ >
+ <svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true">
+ <path d="M8.5 4C4.9 4 2 6.8 2 10.3c0 2 1.1 3.7 2.8 4.9L4 18l3-1.5c.5.1 1 .2 1.5.2h.6c-.1-.4-.1-.8 0-1.2H8.5C5.5 15.5 3 13.2 3 10.3 3 7.4 5.5 5 8.5 5h1.6c.3-1.1 1.3-1.9 2.5-1.9.5 0 1 .2 1.4.5C13.3 3 10.7 4 8.5 4zM10.1 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8.4 1.2c-2.5 0-4.5 2-4.5 4.5s2 4.5 4.5 4.5c.6 0 1.2-.1 1.7-.3L22 17.5l-.8-2.3c1.4-1 2.3-2.6 2.3-4.3C23.5 9.2 21.5 7.2 19 7.2c-.5 0-1 0-1.4.1C16.9 7.8 17.9 7.2 19 7.2zM17 9.7c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5z"/>
+ </svg>
+ </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) => {
- // 澶栭儴瀵艰埅鍒?API 绔偣
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: var(--bg-tertiary, #24292e);
+ color: var(--text-primary, #ffffff);
+ border-color: var(--border-secondary, #24292e);
+}
+
+.oauth-btn-google:hover {
+ background: var(--card-bg, #ffffff);
+ color: var(--text-primary, #111111);
+ border-color: #dadce0;
+}
+
+.oauth-btn-aggregate:hover {
+ background: var(--bg-secondary, #12b7f5);
+ color: var(--text-primary, #ffffff);
+ border-color: var(--border-secondary, #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/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/Auth/RegisterForm.vue b/app/components/Auth/RegisterForm.vue
index c9f0dc0..ee8fbf2 100644
--- a/app/components/Auth/RegisterForm.vue
+++ b/app/components/Auth/RegisterForm.vue
@@ -575,7 +575,7 @@ const handleRegister = async () => {
width: 100%;
padding: 16px;
background: var(--primary);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
border: none;
border-radius: var(--radius-lg);
font-size: 16px;
diff --git a/app/components/Notifications/NotificationSettings.vue b/app/components/Notifications/NotificationSettings.vue
index e585ebd..b6137f7 100644
--- a/app/components/Notifications/NotificationSettings.vue
+++ b/app/components/Notifications/NotificationSettings.vue
@@ -209,7 +209,7 @@ input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
- background: #2563eb; /* blue-600 */
+ background: var(--bg-tertiary, #2563eb); /* blue-600 */
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
@@ -218,7 +218,7 @@ input[type='range']::-webkit-slider-thumb {
input[type='range']::-moz-range-thumb {
width: 18px;
height: 18px;
- background: #2563eb;
+ background: var(--bg-tertiary, #2563eb);
border-radius: 50%;
cursor: pointer;
border: none;
diff --git a/app/components/Player/PlayerLyric/AMLyric.vue b/app/components/Player/PlayerLyric/AMLyric.vue
index 58ba74a..93ee383 100644
--- a/app/components/Player/PlayerLyric/AMLyric.vue
+++ b/app/components/Player/PlayerLyric/AMLyric.vue
@@ -160,7 +160,7 @@ const jumpSeek = (line: LyricLineMouseEvent) => {
:deep(.am-lyric .is-active),
:deep(.am-lyric .lyric-line.current),
:deep(.am-lyric .lyric-line.is-current) {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
text-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
}
}
diff --git a/app/components/Player/PlayerLyric/DefaultLyric.vue b/app/components/Player/PlayerLyric/DefaultLyric.vue
index 156fec0..259724f 100644
--- a/app/components/Player/PlayerLyric/DefaultLyric.vue
+++ b/app/components/Player/PlayerLyric/DefaultLyric.vue
@@ -824,6 +824,6 @@ onBeforeUnmount(() => {
align-items: center;
justify-content: center;
font-size: 22px;
- color: #fff;
+ color: var(--text-primary, #ffffff);
}
</style>
diff --git a/app/components/SiteFooter.vue b/app/components/SiteFooter.vue
index 0b3adc7..c5d528d 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
@@ -91,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%;
}
@@ -104,7 +109,7 @@ onMounted(() => {
}
.footer-item {
- color: rgba(255, 255, 255, 0.6);
+ color: var(--text-tertiary);
font-size: 12px;
white-space: nowrap;
}
@@ -112,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 {
@@ -122,7 +127,7 @@ onMounted(() => {
}
.footer-item a:hover {
- color: rgba(255, 255, 255, 0.8);
+ color: var(--text-secondary);
}
.icp-link,
@@ -134,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/components/Songs/RequestForm.vue b/app/components/Songs/RequestForm.vue
index 12909ef..5be91bd 100644
--- a/app/components/Songs/RequestForm.vue
+++ b/app/components/Songs/RequestForm.vue
@@ -849,13 +849,7 @@ 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'
const props = defineProps({
loading: {
@@ -980,12 +974,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 +1012,6 @@ const searchError = ref('')
// 鎵嬪姩杈撳叆鐩稿叧
const showManualModal = ref(false)
-const showBilibiliEpisodesModal = ref(false)
const selectedBilibiliVideo = ref(null)
const bilibiliEpisodes = ref([])
const manualArtist = ref('')
@@ -1584,7 +1575,6 @@ const openSimilarEpisodesModal = (group) => {
selectedBilibiliVideo.value = videoData
bilibiliEpisodes.value = videoData.pages
- showBilibiliEpisodesModal.value = true
}
// 澶勭悊鍓ч泦鎶曠エ
@@ -1684,7 +1674,6 @@ const handleLikeFromSearch = async (song, originalResult = null) => {
if (originalResult && isBilibiliMultiP(originalResult)) {
selectedBilibiliVideo.value = originalResult
bilibiliEpisodes.value = originalResult.pages
- showBilibiliEpisodesModal.value = true
return
}
@@ -2127,7 +2116,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 +2129,6 @@ const submitSong = async (result, options = {}) => {
console.log('鎵撳紑 Bilibili 鍓ч泦鍒楄〃:', result)
selectedBilibiliVideo.value = result
bilibiliEpisodes.value = result.pages
- showBilibiliEpisodesModal.value = true
return
}
@@ -2381,7 +2368,6 @@ const handleBilibiliEpisodeSelect = async (episode) => {
})
if (success) {
- showBilibiliEpisodesModal.value = false
if (bilibiliModalRef.value && bilibiliModalRef.value.resetSubmissionState) {
bilibiliModalRef.value.resetSubmissionState()
}
@@ -2418,7 +2404,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 +2422,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) {
@@ -2928,7 +2912,7 @@ defineExpose({
<style scoped>
.request-form {
width: 100%;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
display: flex;
gap: 2rem;
height: calc(100vh - 160px);
@@ -2960,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;
}
@@ -2970,7 +2954,7 @@ defineExpose({
font-size: 15px;
line-height: 1.7;
letter-spacing: 0.04em;
- color: #fff;
+ color: var(--text-primary, #ffffff);
}
.rules-content-desktop p {
@@ -2984,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;
}
@@ -2996,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 {
@@ -3006,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;
}
@@ -3041,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;
@@ -3082,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 {
@@ -3096,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 {
@@ -3152,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;
@@ -3197,7 +3181,7 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 15px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
white-space: nowrap;
flex-shrink: 0; /* 闃叉鏍囩琚帇缂?*/
}
@@ -3210,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; /* 纭繚杈撳叆妗嗕笉浼氱缉鍒板お灏?*/
}
@@ -3228,11 +3212,11 @@ defineExpose({
}
.search-button {
- background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
padding: 0.75rem 1.5rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
@@ -3266,7 +3250,7 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 16px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
white-space: nowrap;
flex-shrink: 0; /* 闃叉鏍囩琚帇缂?*/
}
@@ -3287,13 +3271,13 @@ defineExpose({
border-radius: 6px;
padding: 0.25rem 0.5rem;
font-size: 14px;
- color: #fff;
+ color: var(--text-primary, #ffffff);
}
.remove-collaborator {
background: none;
border: none;
- color: rgba(255, 255, 255, 0.6);
+ color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
@@ -3301,14 +3285,14 @@ defineExpose({
}
.remove-collaborator:hover {
- color: #fff;
+ color: var(--text-primary, #ffffff);
}
.add-collaborator-btn {
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;
@@ -3320,7 +3304,7 @@ defineExpose({
.add-collaborator-btn:hover {
background: rgba(255, 255, 255, 0.1);
- color: #fff;
+ color: var(--text-primary, #ffffff);
}
/* 鑷畾涔夊閫夋鏍峰紡 */
@@ -3343,7 +3327,7 @@ defineExpose({
width: 14px;
height: 14px;
border-radius: 4px;
- border: 1px solid #3f3f46;
+ border: 1px solid var(--border-secondary, #3f3f46);
background: rgba(24, 24, 27, 0.5);
transition: all 0.2s ease;
}
@@ -3359,7 +3343,7 @@ defineExpose({
.custom-checkbox-input:checked + .custom-checkbox-box {
background: #3b82f6;
- border-color: #3b82f6;
+ border-color: var(--border-secondary, #3b82f6);
}
.custom-checkbox-input:checked + .custom-checkbox-box .custom-checkbox-icon {
@@ -3444,7 +3428,7 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 13px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
.status-item-horizontal .status-value {
@@ -3490,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;
}
@@ -3500,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%;
}
@@ -3622,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;
@@ -3631,7 +3615,7 @@ defineExpose({
}
.header-btn:hover {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
background: rgba(255, 255, 255, 0.08);
}
@@ -3644,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;
@@ -3661,7 +3645,7 @@ defineExpose({
.import-btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.15);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
.user-status {
@@ -3692,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;
@@ -3722,16 +3706,16 @@ defineExpose({
.action-btn-compact:hover {
background: rgba(255, 255, 255, 0.08);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
border-color: rgba(255, 255, 255, 0.12);
}
.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 {
@@ -3747,7 +3731,7 @@ defineExpose({
}
.radio-label.active {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-weight: 600;
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
@@ -3773,27 +3757,27 @@ 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;
}
.platform-btn.active {
- background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border-color: rgba(255, 255, 255, 0.16);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
.platform-btn:hover:not(.active) {
@@ -3803,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;
@@ -3821,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;
@@ -3889,7 +3873,7 @@ defineExpose({
}
.source-item.healthy .source-indicator {
- background: #22c55e;
+ background: var(--bg-tertiary, #22c55e);
box-shadow: 0 0 6px rgba(34, 197, 94, 0.6);
}
@@ -3970,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;
@@ -4047,13 +4031,13 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 15px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
margin: 0;
}
.empty-hint,
.initial-hint {
- color: rgba(255, 255, 255, 0.6);
+ color: var(--text-secondary);
font-size: 13px;
margin: 0;
}
@@ -4093,7 +4077,7 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.3s ease;
}
@@ -4119,14 +4103,14 @@ defineExpose({
}
.submit-button {
- background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
padding: 0.5rem 1.5rem;
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.3s ease;
}
@@ -4141,7 +4125,7 @@ defineExpose({
transform: none;
}
.similar-song-alert {
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
border-radius: 10px;
padding: 0.5rem 0.75rem;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.3);
@@ -4173,7 +4157,7 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 16px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
.alert-content {
@@ -4206,7 +4190,7 @@ defineExpose({
.song-title {
margin: 0 0 0.25rem 0;
font-size: 14px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-weight: 500;
}
@@ -4238,7 +4222,7 @@ defineExpose({
}
.alert-hint {
- color: rgba(255, 255, 255, 0.6);
+ color: var(--text-secondary);
font-size: 14px;
margin-top: 0.5rem;
}
@@ -4260,11 +4244,11 @@ defineExpose({
}
.vote-btn {
- background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
padding: 0.5rem 1rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
@@ -4288,7 +4272,7 @@ defineExpose({
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
padding: 0.5rem 1rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
@@ -4401,12 +4385,12 @@ 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 {
margin: 0;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 18px;
@@ -4431,7 +4415,7 @@ defineExpose({
.close-btn:hover {
background: rgba(255, 255, 255, 0.1);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
transform: rotate(90deg);
}
@@ -4465,21 +4449,21 @@ 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);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
border-color: rgba(255, 255, 255, 0.2);
}
.btn-primary {
- background: linear-gradient(135deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(135deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border-color: rgba(255, 255, 255, 0.1);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
box-shadow: 0 4px 12px rgba(0, 67, 248, 0.3);
}
@@ -4532,7 +4516,7 @@ defineExpose({
}
.result-item:hover {
- background: rgba(255, 255, 255, 0.05);
+ background: var(--bg-tertiary);
}
.result-cover {
@@ -4543,7 +4527,7 @@ defineExpose({
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
- background: #18181b;
+ background: var(--bg-secondary, #18181b);
}
.cover-img {
@@ -4616,7 +4600,7 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 15px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
@@ -4641,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;
@@ -4678,7 +4662,7 @@ defineExpose({
display: flex;
align-items: center;
justify-content: center;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
@@ -4709,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;
}
@@ -4729,7 +4713,7 @@ defineExpose({
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 6px;
padding: 0.4rem 0.8rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 12px;
@@ -4766,11 +4750,11 @@ defineExpose({
}
.select-btn {
- background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
padding: 0.5rem 1rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
@@ -4803,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;
}
@@ -4828,7 +4812,7 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.3s ease;
}
@@ -4851,7 +4835,7 @@ defineExpose({
font-weight: 600;
font-size: 18px;
margin-bottom: 1rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
.manual-form {
@@ -4871,7 +4855,7 @@ defineExpose({
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
padding: 0.5rem 1rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
@@ -4884,11 +4868,11 @@ defineExpose({
}
.manual-confirm-btn {
- background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
padding: 0.5rem 1rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 14px;
@@ -4973,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;
}
@@ -5025,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;
@@ -5119,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;
@@ -5144,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;
@@ -5159,13 +5143,13 @@ defineExpose({
border-radius: 10px;
background: transparent;
border: none;
- color: rgba(255, 255, 255, 0.5);
+ color: var(--text-tertiary);
min-width: auto;
}
.platform-btn.active {
- background: #0b5afe;
- color: #ffffff;
+ background: var(--bg-secondary, #0b5afe);
+ color: var(--text-primary, #ffffff);
box-shadow: 0 2px 8px rgba(11, 90, 254, 0.3);
}
@@ -5487,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);
@@ -5503,7 +5487,7 @@ defineExpose({
.import-semester-btn:hover {
background: rgba(255, 255, 255, 0.15);
- color: #fff;
+ color: var(--text-primary, #ffffff);
border-color: rgba(255, 255, 255, 0.2);
}
@@ -5529,14 +5513,14 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 500;
font-size: 16px;
- color: #fff;
+ color: var(--text-primary, #ffffff);
margin-bottom: 0.5rem;
}
.video-author {
font-family: 'MiSans', sans-serif;
font-size: 14px;
- color: rgba(255, 255, 255, 0.6);
+ color: var(--text-secondary);
}
.episodes-list {
@@ -5551,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;
@@ -5587,12 +5571,12 @@ defineExpose({
font-family: 'MiSans', sans-serif;
font-weight: 500;
font-size: 14px;
- color: #fff;
+ color: var(--text-primary, #ffffff);
}
.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 0a5b247..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">
@@ -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
@@ -1625,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: #ffffff;
+ color: var(--text-primary);
font-size: 15px;
font-weight: 500;
margin-right: 0.75rem;
@@ -1642,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: #ffffff;
+ color: var(--text-primary);
padding: 0.6rem 1rem;
font-size: 14px;
font-weight: 500;
@@ -1673,8 +1648,8 @@ const vRipple = {
}
.semester-select option {
- background: #1a1d24;
- color: #ffffff;
+ background: var(--bg-secondary, #1a1d24);
+ color: var(--text-primary);
padding: 0.5rem;
}
@@ -1728,14 +1703,14 @@ const vRipple = {
}
.date-item:hover {
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.date-item.active {
- background: #21242d;
- color: #ffffff;
+ background: var(--bg-tertiary, #21242d);
+ color: var(--text-primary);
font-weight: 600;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(11, 90, 254, 0.2);
@@ -1800,7 +1775,7 @@ const vRipple = {
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 20px;
- color: #ffffff;
+ color: var(--text-primary);
margin: 0;
}
@@ -1809,7 +1784,7 @@ const vRipple = {
padding: 3rem;
text-align: center;
border-radius: 10px;
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
margin: 1rem 0;
color: rgba(255, 255, 255, 0.6);
display: flex;
@@ -1844,7 +1819,7 @@ const vRipple = {
padding: 2rem;
text-align: center;
border-radius: 10px;
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
margin: 1rem 0;
color: rgba(255, 255, 255, 0.6);
}
@@ -1894,7 +1869,7 @@ const vRipple = {
.song-card {
width: 320px;
flex-shrink: 0;
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
border-radius: 10px;
overflow: hidden;
position: relative;
@@ -1911,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: #ffffff;
+ color: var(--text-primary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
@@ -1955,7 +1930,7 @@ const vRipple = {
width: 100%;
max-width: 580px;
max-height: 85vh;
- background: #1e1e24;
+ background: var(--bg-secondary, #1e1e24);
border-radius: 20px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.08);
@@ -1989,7 +1964,7 @@ const vRipple = {
margin: 0;
font-size: 18px;
font-weight: 600;
- color: #ffffff;
+ color: var(--text-primary);
letter-spacing: 0.5px;
}
@@ -2009,7 +1984,7 @@ const vRipple = {
.playlist-modal-close:hover {
background: rgba(255, 255, 255, 0.1);
- color: #ffffff;
+ color: var(--text-primary);
transform: rotate(90deg);
}
@@ -2018,7 +1993,7 @@ const vRipple = {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
- background: #1e1e24;
+ background: var(--bg-secondary, #1e1e24);
}
/* 婊氬姩鏉$編鍖?*/
@@ -2091,7 +2066,7 @@ const vRipple = {
height: 40px;
border-radius: 50%;
overflow: hidden;
- background: #2a2a32;
+ background: var(--bg-tertiary, #2a2a32);
display: flex;
align-items: center;
justify-content: center;
@@ -2112,7 +2087,7 @@ const vRipple = {
.user-name {
font-weight: 600;
- color: #ffffff;
+ color: var(--text-primary);
font-size: 15px;
}
@@ -2186,9 +2161,9 @@ const vRipple = {
.custom-select,
.custom-input {
width: 100%;
- background: #141418;
+ background: var(--bg-secondary, #141418);
border: 1px solid rgba(255, 255, 255, 0.1);
- color: #ffffff;
+ color: var(--text-primary);
padding: 0.75rem 1rem;
border-radius: 10px;
font-size: 14px;
@@ -2213,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 {
@@ -2235,7 +2210,7 @@ const vRipple = {
.btn-icon:hover {
background: rgba(255, 255, 255, 0.1);
- color: #ffffff;
+ color: var(--text-primary);
}
.btn-icon:disabled {
@@ -2446,7 +2421,7 @@ const vRipple = {
.song-name {
font-size: 14px;
- color: #ffffff;
+ color: var(--text-primary);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
@@ -2494,7 +2469,7 @@ const vRipple = {
.btn-primary {
background: var(--primary);
- color: #ffffff;
+ color: var(--text-primary);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25);
}
@@ -2513,7 +2488,7 @@ const vRipple = {
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
- color: #ffffff;
+ color: var(--text-primary);
}
.btn-secondary:hover {
@@ -2527,7 +2502,7 @@ const vRipple = {
.btn-ghost:hover {
background: rgba(255, 255, 255, 0.05);
- color: #ffffff;
+ color: var(--text-primary);
}
/* 鍔ㄧ敾瀹氫箟 */
@@ -2562,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: #ffffff;
+ color: var(--text-primary);
cursor: pointer;
margin-right: 0.5rem;
backdrop-filter: blur(10px);
@@ -2686,8 +2661,8 @@ const vRipple = {
display: flex;
align-items: center;
justify-content: center;
- background: linear-gradient(135deg, #0043f8 0%, #0075f8 100%);
- color: #ffffff;
+ background: linear-gradient(135deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
+ color: var(--text-primary);
font-size: 28px;
font-weight: bold;
font-family: 'MiSans', sans-serif;
@@ -2750,7 +2725,7 @@ const vRipple = {
font-weight: 600;
font-size: 16px;
letter-spacing: 0.04em;
- color: #ffffff;
+ color: var(--text-primary);
margin-bottom: 0.5rem;
display: flex;
align-items: flex-start;
@@ -2849,7 +2824,7 @@ const vRipple = {
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 12px;
- color: #ffffff;
+ color: var(--text-primary);
opacity: 0.4;
}
@@ -2944,7 +2919,7 @@ const vRipple = {
.date-text {
font-size: 15px;
font-weight: 600;
- color: #ffffff;
+ color: var(--text-primary);
white-space: nowrap;
text-shadow:
0 0 10px rgba(255, 255, 255, 0.2),
@@ -2953,7 +2928,7 @@ const vRipple = {
.dropdown-icon {
opacity: 0.5;
- color: #ffffff;
+ color: var(--text-primary);
}
.mobile-action-btn {
@@ -2962,7 +2937,7 @@ const vRipple = {
display: flex;
align-items: center;
justify-content: center;
- background: linear-gradient(135deg, #0b5afe 0%, #0043f8 100%);
+ background: linear-gradient(135deg, var(--bg-secondary, #0b5afe) 0%, #0043f8 100%);
border: none;
border-radius: 16px;
box-shadow: 0 4px 15px rgba(11, 90, 254, 0.4);
@@ -3143,7 +3118,7 @@ const vRipple = {
font-weight: 600;
margin-bottom: 2px;
line-height: 1.4;
- color: #ffffff;
+ color: var(--text-primary);
letter-spacing: 0.01em;
}
@@ -3193,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%;
@@ -3391,7 +3366,7 @@ const vRipple = {
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
border-radius: 10px;
padding: 0.75rem 1rem;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
@@ -3403,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: #ffffff;
+ border: 1px solid var(--border-secondary);
+ color: var(--text-primary);
border-radius: 50%;
width: 36px;
height: 36px;
@@ -3428,7 +3403,7 @@ const vRipple = {
font-family: 'MiSans', sans-serif;
font-weight: 600;
font-size: 16px;
- color: #ffffff;
+ color: var(--text-primary);
text-align: center;
flex: 1;
white-space: pre-line;
@@ -3478,7 +3453,7 @@ const vRipple = {
width: 85%;
max-width: 350px;
max-height: 70vh;
- background: #1a1d24;
+ background: var(--bg-secondary, #1a1d24);
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
diff --git a/app/components/Songs/SongList.vue b/app/components/Songs/SongList.vue
index 7478f2d..22c31ac 100644
--- a/app/components/Songs/SongList.vue
+++ b/app/components/Songs/SongList.vue
@@ -1691,14 +1691,14 @@ const vRipple = {
border-radius: 12px;
padding: 12px 16px 12px 42px;
font-size: 14px;
- color: #fff;
+ color: var(--text-primary, #ffffff);
width: 100%;
transition: all 0.2s ease;
}
.mobile-search-input:focus {
background: rgba(255, 255, 255, 0.08);
- border-color: #3b82f6;
+ border-color: var(--border-secondary, #3b82f6);
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
@@ -1785,7 +1785,7 @@ const vRipple = {
}
.tab-button.active {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
border-bottom-color: #0b5afe;
transform: none;
box-shadow: none;
@@ -1834,7 +1834,7 @@ const vRipple = {
justify-content: center;
width: 36px;
height: 36px;
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
cursor: pointer;
@@ -1845,7 +1845,7 @@ const vRipple = {
}
.semester-toggle-btn:hover {
- background: #2a2e38;
+ background: var(--bg-tertiary, #2a2e38);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
color: #0b5afe;
@@ -1855,7 +1855,7 @@ const vRipple = {
position: absolute;
top: calc(100% + 8px);
right: 0;
- background: #1a1d24;
+ background: var(--bg-secondary, #1a1d24);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
@@ -1879,7 +1879,7 @@ const vRipple = {
.semester-option:hover {
background: rgba(11, 90, 254, 0.1);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
.semester-option.active {
@@ -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;
}
@@ -1925,7 +1925,7 @@ const vRipple = {
justify-content: center;
width: 36px;
height: 36px;
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
cursor: pointer;
@@ -1934,7 +1934,7 @@ const vRipple = {
}
.refresh-button:hover {
- background: #2a2e38;
+ background: var(--bg-tertiary, #2a2e38);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
@@ -2028,7 +2028,7 @@ const vRipple = {
.song-card-main {
padding: 1rem 0 1rem 1rem; /* 绉婚櫎鍙充晶鍐呰竟璺濓紝淇濈暀宸︿晶銆佷笂涓嬪唴杈硅窛 */
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
position: relative;
height: 100px; /* 鍑忓皬鍗$墖楂樺害 */
@@ -2076,8 +2076,8 @@ const vRipple = {
display: flex;
align-items: center;
justify-content: center;
- background: linear-gradient(135deg, #0043f8 0%, #0075f8 100%);
- color: #ffffff;
+ background: linear-gradient(135deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
+ color: var(--text-primary, #ffffff);
font-size: 28px;
font-weight: bold;
font-family: 'MiSans-Demibold', sans-serif;
@@ -2139,7 +2139,7 @@ const vRipple = {
font-weight: 600;
font-size: 16px;
letter-spacing: 0.04em;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
margin-bottom: 0.5rem;
width: 100%; /* 纭繚鏍囬鍗犳弧鏁翠釜瀹瑰櫒瀹藉害 */
display: flex;
@@ -2215,7 +2215,7 @@ const vRipple = {
font-family: 'MiSans-Demibold', sans-serif;
font-weight: 600;
font-size: 12px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
opacity: 0.4;
}
@@ -2231,7 +2231,7 @@ const vRipple = {
justify-content: center;
width: 48px;
height: 45px;
- background: linear-gradient(180deg, #0043f8 0%, #0075f8 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
cursor: pointer;
@@ -2239,8 +2239,8 @@ const vRipple = {
}
.like-button.liked {
- background: #1a1d24;
- border-color: #242f38;
+ background: var(--bg-secondary, #1a1d24);
+ border-color: var(--border-secondary, #242f38);
background-image: none;
}
@@ -2329,7 +2329,7 @@ const vRipple = {
display: flex;
align-items: center;
justify-content: space-between;
- background: #21242d;
+ background: var(--bg-tertiary, #21242d);
border-radius: 0 0 10px 10px;
padding: 0.5rem 1rem;
width: 95%;
@@ -2355,7 +2355,7 @@ const vRipple = {
font-family: 'MiSans-Demibold', sans-serif;
font-weight: 600;
font-size: 12px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.3s ease;
height: 27px;
@@ -2368,12 +2368,12 @@ const vRipple = {
}
.replay-cancel-btn {
- background: linear-gradient(180deg, #0b5afe 0%, #3d7fff 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0b5afe) 0%, #3d7fff 100%);
min-width: 75px;
}
.replay-request-btn {
- background: linear-gradient(180deg, #0b5afe 0%, #3d7fff 100%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0b5afe) 0%, #3d7fff 100%);
min-width: 75px;
}
@@ -2408,13 +2408,13 @@ button:disabled {
border-radius: 4px;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.2s ease;
}
.page-number.active {
- background: #0b5afe;
+ background: var(--bg-secondary, #0b5afe);
border-color: #0b5afe;
}
@@ -2444,7 +2444,7 @@ button:disabled {
border-radius: 4px;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
width: 60px;
text-align: center;
transition: all 0.2s ease;
@@ -2467,7 +2467,7 @@ button:disabled {
border-radius: 4px;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
@@ -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);
@@ -2705,7 +2705,7 @@ button:disabled {
font-weight: 700;
margin-bottom: 4px;
line-height: 1.2;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
letter-spacing: 0.01em;
}
@@ -2930,7 +2930,7 @@ button:disabled {
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
- color: #fff;
+ color: var(--text-primary, #ffffff);
display: flex;
align-items: center;
justify-content: center;
@@ -2964,7 +2964,7 @@ button:disabled {
width: 40px;
background: transparent;
border: none;
- color: #fff;
+ color: var(--text-primary, #ffffff);
text-align: center;
font-size: 14px;
font-weight: 600;
@@ -2991,7 +2991,7 @@ button:disabled {
.page-nav-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.1);
- border-color: #3b82f6;
+ border-color: var(--border-secondary, #3b82f6);
}
.page-nav-btn:disabled {
diff --git a/app/components/UI/AudioPlayer.vue b/app/components/UI/AudioPlayer.vue
index cbfc967..ff92789 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()
}
}
@@ -1583,7 +1528,7 @@ const getFirstChar = (text) => {
.mobile-top-progress .progress-fill {
height: 100%;
- background: #0b5afe;
+ background: var(--bg-secondary, #0b5afe);
box-shadow: 0 0 8px rgba(11, 90, 254, 0.6);
border-radius: 0 1px 1px 0;
}
@@ -1784,7 +1729,7 @@ const getFirstChar = (text) => {
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-size: 18px;
font-weight: 600;
font-family:
@@ -1811,7 +1756,7 @@ const getFirstChar = (text) => {
line-height: 22px;
padding-bottom: 2px;
letter-spacing: -0.4px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-family:
'SF Pro', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica,
sans-serif;
@@ -1831,7 +1776,7 @@ const getFirstChar = (text) => {
overflow: hidden;
line-height: 20px;
letter-spacing: -0.4px;
- color: #ffffff75;
+ color: var(--text-primary, #ffffff)75;
font-family:
'SF Pro', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica,
sans-serif;
@@ -1852,7 +1797,7 @@ const getFirstChar = (text) => {
align-items: center;
justify-content: center;
border-radius: 100px;
- background: #00000042;
+ background: var(--bg-primary, #111111)42;
padding: 6px;
width: 32px;
height: 32px;
@@ -1869,7 +1814,7 @@ const getFirstChar = (text) => {
}
.close-icon {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
font-size: 15px;
line-height: 1;
font-family:
@@ -1906,7 +1851,7 @@ const getFirstChar = (text) => {
display: flex;
flex-shrink: 0;
align-items: center;
- border: 1px solid #1a1a1a;
+ border: 1px solid var(--border-secondary, #1a1a1a);
border-radius: 7px;
height: 8px;
width: 100%;
@@ -1919,14 +1864,14 @@ const getFirstChar = (text) => {
}
.ios-progress-bar:hover {
- border-color: #333;
+ border-color: var(--border-secondary, #333);
box-shadow:
0 0 20px rgba(255, 255, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.progress-fill {
- background: linear-gradient(90deg, #ffffffb2, #ffffff);
+ background: linear-gradient(90deg, var(--card-bg, #ffffff)b2, #ffffff);
height: 8px;
transition: width 0.1s linear;
border-radius: 7px;
@@ -1971,7 +1916,7 @@ const getFirstChar = (text) => {
}
.clickable-time:hover {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
transform: scale(1.05);
}
@@ -1986,10 +1931,10 @@ const getFirstChar = (text) => {
display: flex;
align-items: center;
justify-content: center;
- background: #ffffff1a;
+ background: var(--card-bg, #ffffff)1a;
border-radius: 4px;
padding: 0 8px;
- color: #ffffffb3;
+ color: var(--text-primary, #ffffff)b3;
font-size: 10px;
font-weight: 600;
backdrop-filter: blur(10px);
@@ -1999,8 +1944,8 @@ const getFirstChar = (text) => {
}
.audio-quality-badge:hover {
- background: #ffffff26;
- color: #ffffff;
+ background: var(--card-bg, #ffffff)26;
+ color: var(--text-primary, #ffffff);
}
/* 鎺у埗鎸夐挳鍖哄煙 */
@@ -2017,7 +1962,7 @@ const getFirstChar = (text) => {
.ios-control-btn {
background: none;
border: none;
- color: #ffffff92;
+ color: var(--text-primary, #ffffff)92;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
display: flex;
@@ -2052,7 +1997,7 @@ const getFirstChar = (text) => {
}
.ios-control-btn:hover {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
transform: scale(1.1);
box-shadow:
0 0 20px rgba(255, 255, 255, 0.2),
@@ -2251,7 +2196,7 @@ const getFirstChar = (text) => {
width: 60px;
line-height: 18px;
letter-spacing: 0.42px;
- color: #ffffffb3;
+ color: var(--text-primary, #ffffff)b3;
font-family:
'SF Pro', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica,
sans-serif;
@@ -2270,10 +2215,10 @@ const getFirstChar = (text) => {
display: flex;
align-items: center;
justify-content: center;
- background: #ffffff1a;
+ background: var(--card-bg, #ffffff)1a;
border-radius: 4px;
padding: 0 8px;
- color: #ffffffb3;
+ color: var(--text-primary, #ffffff)b3;
font-size: 10px;
font-weight: 600;
backdrop-filter: blur(10px);
@@ -2283,8 +2228,8 @@ const getFirstChar = (text) => {
}
.audio-quality:hover {
- background: #ffffff26;
- color: #ffffff;
+ background: var(--card-bg, #ffffff)26;
+ color: var(--text-primary, #ffffff);
}
.audio-quality:active {
@@ -2306,13 +2251,13 @@ const getFirstChar = (text) => {
width: 39px;
font-size: 25px;
text-align: center;
- color: #ffffff92;
+ color: var(--text-primary, #ffffff)92;
cursor: pointer;
transition: all 0.2s ease;
}
.lyrics-btn:hover {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
transform: scale(1.1);
}
@@ -2320,7 +2265,7 @@ const getFirstChar = (text) => {
.control-btn {
line-height: 1;
font-size: 24px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
@@ -2345,7 +2290,7 @@ const getFirstChar = (text) => {
.play-pause-btn {
line-height: 44px;
font-size: 37px;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
diff --git a/app/components/UI/Common/CustomSelect.vue b/app/components/UI/Common/CustomSelect.vue
index 80231e9..98ac764 100644
--- a/app/components/UI/Common/CustomSelect.vue
+++ b/app/components/UI/Common/CustomSelect.vue
@@ -216,7 +216,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/UI/LyricsModal.vue b/app/components/UI/LyricsModal.vue
index 3451ee1..7863815 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;
@@ -1136,7 +1136,7 @@ onUnmounted(() => {
.default-cover {
width: 100%;
height: 100%;
- background: linear-gradient(135deg, #333, #555);
+ background: linear-gradient(135deg, var(--bg-quaternary, #333333), #555);
display: flex;
align-items: center;
justify-content: center;
@@ -1152,7 +1152,7 @@ onUnmounted(() => {
font-size: 2.2rem;
font-weight: 700;
margin: 0 0 0.5rem 0;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
line-height: 1.2;
letter-spacing: -0.02em;
white-space: normal;
@@ -1246,7 +1246,7 @@ onUnmounted(() => {
.progress-fill {
height: 100%;
- background: #ffffff;
+ background: var(--card-bg, #ffffff);
border-radius: 3px;
pointer-events: none;
}
@@ -1256,7 +1256,7 @@ onUnmounted(() => {
top: 50%;
width: 14px;
height: 14px;
- background: #ffffff;
+ background: var(--card-bg, #ffffff);
border-radius: 50%;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.1s ease;
@@ -1306,7 +1306,7 @@ onUnmounted(() => {
.secondary-btn.active {
opacity: 1;
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
text-shadow: 0 0 8px rgba(255, 255, 255, 0.4);
}
@@ -1472,7 +1472,7 @@ onUnmounted(() => {
width: 100%;
text-align: center;
background: transparent;
- color: #333; /* 娣辫壊鏂囧瓧 */
+ color: var(--text-quaternary, #333333); /* 娣辫壊鏂囧瓧 */
padding: 8px 12px; /* 鍑忓皬鍐呰竟璺?*/
border-radius: 8px;
font-size: 0.85rem; /* 绋嶅井鍑忓皬瀛椾綋 */
@@ -1490,7 +1490,7 @@ onUnmounted(() => {
.badge-quality-option.active {
color: #007aff; /* 钃濊壊瀛?*/
- background: #ffffff;
+ background: var(--card-bg, #ffffff);
font-weight: 600;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
@@ -1522,7 +1522,7 @@ onUnmounted(() => {
width: 20px;
height: 5px;
border-radius: 3px;
- background: #ffffff;
+ background: var(--card-bg, #ffffff);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
@@ -1759,7 +1759,7 @@ onUnmounted(() => {
}
.progress-fill {
- background: #ffffff;
+ background: var(--card-bg, #ffffff);
}
.progress-thumb {
diff --git a/app/components/UI/Notification.vue b/app/components/UI/Notification.vue
index b16b050..a2f69a2 100644
--- a/app/components/UI/Notification.vue
+++ b/app/components/UI/Notification.vue
@@ -70,8 +70,8 @@ defineEmits(['close'])
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
- background: #21242d;
- color: #ffffff;
+ background: var(--bg-tertiary, #21242d);
+ color: var(--text-primary, #ffffff);
max-width: 400px;
min-width: 300px;
font-family: 'MiSans', sans-serif;
@@ -79,7 +79,7 @@ defineEmits(['close'])
}
.notification.success {
- border-left: 4px solid #10b981;
+ border-left: 4px solid var(--border-secondary, #10b981);
}
.notification.error {
@@ -140,7 +140,7 @@ defineEmits(['close'])
}
.notification-close:hover {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
/* 杩涘害鏉?*/
@@ -161,7 +161,7 @@ defineEmits(['close'])
}
.success .notification-progress-bar {
- background-color: #10b981;
+ background-color: var(--bg-secondary, #10b981);
}
.error .notification-progress-bar {
@@ -169,7 +169,7 @@ defineEmits(['close'])
}
.info .notification-progress-bar {
- background-color: #0b5afe;
+ background-color: var(--bg-secondary, #0b5afe);
}
@keyframes progress-shrink {
diff --git a/app/components/UI/NotificationContainer.vue b/app/components/UI/NotificationContainer.vue
index 146ac16..f782fa7 100644
--- a/app/components/UI/NotificationContainer.vue
+++ b/app/components/UI/NotificationContainer.vue
@@ -82,8 +82,8 @@ onMounted(() => {
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
- background: #21242d;
- color: #ffffff;
+ background: var(--bg-tertiary, #21242d);
+ color: var(--text-primary, #ffffff);
max-width: 400px;
min-width: 300px;
font-family: 'MiSans', sans-serif;
@@ -94,7 +94,7 @@ onMounted(() => {
}
.notification-item.success {
- border-left: 4px solid #10b981;
+ border-left: 4px solid var(--border-secondary, #10b981);
}
.notification-item.error {
@@ -158,7 +158,7 @@ onMounted(() => {
}
.notification-close:hover {
- color: #ffffff;
+ color: var(--text-primary, #ffffff);
}
/* 杩涘害鏉?*/
@@ -179,7 +179,7 @@ onMounted(() => {
}
.success .notification-progress-bar {
- background-color: #10b981;
+ background-color: var(--bg-secondary, #10b981);
}
.error .notification-progress-bar {
@@ -187,7 +187,7 @@ onMounted(() => {
}
.info .notification-progress-bar {
- background-color: #0b5afe;
+ background-color: var(--bg-secondary, #0b5afe);
}
@keyframes progress-shrink {
diff --git a/app/components/UserTutorial.vue b/app/components/UserTutorial.vue
index 3ad45ac..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">蹇€熶簡瑙e浣曚娇鐢?VoiceHub 鏍″洯骞挎挱绯荤粺</p>
+ <p class="text-xs text-text-secondary mt-1">蹇€熶簡瑙e浣曚娇鐢?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;
}
@@ -359,15 +359,15 @@ onMounted(() => {
z-index: 9999;
border-radius: 8px;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5), 0 0 15px rgba(0, 0, 0, 0.5);
- border: 2px solid #3b82f6;
+ border: 2px solid var(--border-secondary, #3b82f6);
pointer-events: none;
transition: all 0.4s ease;
background: transparent;
}
.tour-popover {
- background: #18181b;
- border: 1px solid #27272a;
+ background: var(--bg-secondary, #18181b);
+ border: 1px solid var(--border-secondary, #27272a);
border-radius: 12px;
padding: 1.25rem;
width: 320px;
@@ -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;
}
@@ -440,9 +440,9 @@ onMounted(() => {
}
.tour-btn-secondary {
- background: #27272a;
- color: #d4d4d8;
- border: 1px solid #3f3f46;
+ background: var(--bg-tertiary, #27272a);
+ color: var(--text-primary);
+ border: 1px solid var(--border-secondary, #3f3f46);
}
.tour-btn-secondary:hover {
@@ -451,11 +451,11 @@ onMounted(() => {
.tour-btn-primary {
background: #3b82f6;
- color: white;
+ color: var(--btn-primary-text);
border: none;
}
.tour-btn-primary:hover {
- background: #2563eb;
+ background: var(--bg-tertiary, #2563eb);
}
</style>
diff --git a/app/components/year-review/ArtistSlide.vue b/app/components/year-review/ArtistSlide.vue
index 27d56f1..1b060d8 100644
--- a/app/components/year-review/ArtistSlide.vue
+++ b/app/components/year-review/ArtistSlide.vue
@@ -37,13 +37,13 @@ defineProps({
align-items: center;
justify-content: center;
overflow: hidden;
- background-color: #1a0b2e;
+ background-color: var(--surface-primary, var(--bg-primary, #1a0b2e));
}
.bg-overlay {
position: absolute;
inset: 0;
- background: linear-gradient(to top, #000, transparent, transparent);
+ background: linear-gradient(to top, var(--bg-primary, #111111), transparent, transparent);
opacity: 0.8;
z-index: 10;
}
diff --git a/app/components/year-review/FirstSongSlide.vue b/app/components/year-review/FirstSongSlide.vue
index 630ef15..9ab57af 100644
--- a/app/components/year-review/FirstSongSlide.vue
+++ b/app/components/year-review/FirstSongSlide.vue
@@ -74,7 +74,7 @@ const formatDate = (dateStr) => {
align-items: center;
justify-content: center;
overflow: hidden;
- background-color: #000;
+ background-color: var(--bg-primary, #111111);
}
.bg-blur {
@@ -120,7 +120,7 @@ const formatDate = (dateStr) => {
}
.highlight {
- color: #fff;
+ color: var(--text-primary, #ffffff);
font-weight: 700;
margin: 0 0.5rem;
}
@@ -141,7 +141,7 @@ const formatDate = (dateStr) => {
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
- background: #111827;
+ background: var(--bg-secondary, #111827);
display: flex;
align-items: center;
justify-content: center;
@@ -181,7 +181,7 @@ const formatDate = (dateStr) => {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- color: #fff;
+ color: var(--text-primary, #ffffff);
}
.song-artist {
diff --git a/app/components/year-review/IntroSlide.vue b/app/components/year-review/IntroSlide.vue
index 266bbb7..4c5e496 100644
--- a/app/components/year-review/IntroSlide.vue
+++ b/app/components/year-review/IntroSlide.vue
@@ -31,7 +31,7 @@ defineProps({
align-items: center;
justify-content: center;
overflow: hidden;
- background: radial-gradient(circle at center, #111827, #000000);
+ background: radial-gradient(circle at center, var(--bg-secondary, #111827), #000000);
}
.blob {
diff --git a/app/components/year-review/MiscSlide.vue b/app/components/year-review/MiscSlide.vue
index cc65ec1..6221775 100644
--- a/app/components/year-review/MiscSlide.vue
+++ b/app/components/year-review/MiscSlide.vue
@@ -54,7 +54,7 @@ const formatPlatform = (platform) => {
align-items: center;
justify-content: center;
overflow: hidden;
- background-color: #111827; /* gray-900 */
+ background-color: var(--bg-secondary, #111827); /* gray-900 */
}
.bg-gradient {
@@ -63,7 +63,7 @@ const formatPlatform = (platform) => {
left: 0;
width: 100%;
height: 50%;
- background: linear-gradient(to bottom, #1f2937, transparent);
+ background: linear-gradient(to bottom, var(--bg-secondary, #1f2937), transparent);
opacity: 0.2;
}
diff --git a/app/components/year-review/OutroSlide.vue b/app/components/year-review/OutroSlide.vue
index 0fe78e7..f62251c 100644
--- a/app/components/year-review/OutroSlide.vue
+++ b/app/components/year-review/OutroSlide.vue
@@ -56,13 +56,13 @@ defineEmits(['share'])
align-items: center;
justify-content: center;
overflow: hidden;
- background-color: #000;
+ background-color: var(--bg-primary, #111111);
}
.bg-gradient {
position: absolute;
inset: 0;
- background: radial-gradient(circle at center, #1f2937, #000, #000);
+ background: radial-gradient(circle at center, var(--bg-secondary, #1f2937), #000, #000);
opacity: 0.5;
}
@@ -132,8 +132,8 @@ defineEmits(['share'])
width: 100%;
padding: 1rem 2rem;
border-radius: 9999px;
- background: #fff;
- color: #000;
+ background: var(--card-bg, #fff);
+ color: var(--text-primary, #000);
font-weight: 700;
font-size: 1.125rem;
overflow: hidden;
@@ -173,7 +173,7 @@ defineEmits(['share'])
border-radius: 9999px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
- color: #fff;
+ color: var(--text-primary, #ffffff);
font-weight: 700;
font-size: 1.125rem;
backdrop-filter: blur(12px);
diff --git a/app/components/year-review/ShareCard.vue b/app/components/year-review/ShareCard.vue
index b1f2e91..bd33395 100644
--- a/app/components/year-review/ShareCard.vue
+++ b/app/components/year-review/ShareCard.vue
@@ -63,7 +63,7 @@ defineExpose({
height: 100%;
min-height: 600px;
overflow: hidden;
- background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%);
+ background: linear-gradient(135deg, var(--bg-secondary, #0f0f23) 0%, #1a1a2e 50%, #16213e 100%);
}
.card-background {
@@ -127,7 +127,7 @@ defineExpose({
.main-title {
font-size: 3.5rem;
font-weight: 900;
- background: linear-gradient(135deg, #ffffff 0%, #e5e7eb 50%, #9ca3af 100%);
+ background: linear-gradient(135deg, var(--card-bg, #ffffff) 0%, #e5e7eb 50%, #9ca3af 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
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/app/composables/useSiteConfig.js b/app/composables/useSiteConfig.js
index 3202f60..fcc8598 100644
--- a/app/composables/useSiteConfig.js
+++ b/app/composables/useSiteConfig.js
@@ -19,7 +19,11 @@ const siteConfig = ref({
siteVersion: '',
submissionGuidelines: '',
icpNumber: '',
- gonganNumber: ''
+ gonganNumber: '',
+ defaultTheme: null,
+ enabledThemes: null,
+ statisticsCode: '',
+ statisticsCodeEnabled: false
})
const isLoaded = ref(false)
@@ -57,7 +61,11 @@ export const useSiteConfig = () => {
gonganNumber: '',
enableReplayRequests: false,
enableCollaborativeSubmission: true,
- enableSubmissionRemarks: false
+ enableSubmissionRemarks: false,
+ defaultTheme: 'ClassicLight',
+ enabledThemes: '["ClassicLight","ModernLight"]',
+ statisticsCode: '',
+ statisticsCodeEnabled: false
}
isLoaded.value = true
} finally {
@@ -95,6 +103,34 @@ export const useSiteConfig = () => {
)
const enableSubmissionRemarks = computed(() => siteConfig.value.enableSubmissionRemarks === true)
const smtpEnabled = computed(() => !!siteConfig.value.smtpEnabled)
+ const statisticsCode = computed(() => siteConfig.value.statisticsCode || '')
+ const statisticsCodeEnabled = computed(() => !!siteConfig.value.statisticsCodeEnabled)
+
+ function parseEnabledThemes(raw) {
+ if (Array.isArray(raw)) return raw.filter(Boolean)
+ if (typeof raw === 'string') {
+ const s = raw.trim()
+ if (!s) return []
+ if (s.startsWith('[') || s.startsWith('{')) {
+ try {
+ const parsed = JSON.parse(s)
+ if (Array.isArray(parsed)) return parsed.filter(Boolean)
+ if (parsed && Array.isArray(parsed.items)) return parsed.items.filter(Boolean)
+ } catch (_) {
+ // fallback to comma split
+ }
+ }
+ return s.split(',').map((x) => x.trim()).filter(Boolean)
+ }
+ return []
+ }
+
+ const defaultTheme = computed(() =>
+ typeof siteConfig.value.defaultTheme === 'string' && siteConfig.value.defaultTheme
+ ? siteConfig.value.defaultTheme
+ : null
+ )
+ const enabledThemes = computed(() => parseEnabledThemes(siteConfig.value.enabledThemes))
// 鍒濆鍖栭厤缃紙浠呭湪瀹㈡埛绔墽琛岋級
const initSiteConfig = async () => {
@@ -126,6 +162,10 @@ export const useSiteConfig = () => {
enableCollaborativeSubmission,
enableSubmissionRemarks,
smtpEnabled,
+ defaultTheme,
+ enabledThemes,
+ statisticsCode,
+ statisticsCodeEnabled,
fetchSiteConfig,
initSiteConfig,
refreshSiteConfig
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/app/composables/useTheme.ts b/app/composables/useTheme.ts
index 9c25b36..5cdca88 100644
--- a/app/composables/useTheme.ts
+++ b/app/composables/useTheme.ts
@@ -1,4 +1,4 @@
-import { computed, ref, watch } from 'vue'
+import { computed, ref, watch, nextTick } from 'vue'
const STORAGE_KEY = 'voicehub:theme'
type ThemeName = string & {}
@@ -15,6 +15,25 @@ function getMetaThemeColor() {
return document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
}
+function parseEnabledThemesRaw(raw: unknown): string[] {
+ if (Array.isArray(raw)) return raw.filter(Boolean) as string[]
+ if (typeof raw === 'string') {
+ const s = raw.trim()
+ if (!s) return []
+ if (s.startsWith('[') || s.startsWith('{')) {
+ try {
+ const parsed = JSON.parse(s)
+ if (Array.isArray(parsed)) return parsed.filter(Boolean) as string[]
+ if (parsed && Array.isArray((parsed as any).items)) return (parsed as any).items.filter(Boolean) as string[]
+ } catch (_) {
+ // fallback
+ }
+ }
+ return s.split(',').map((x) => x.trim()).filter(Boolean)
+ }
+ return []
+}
+
function applyTheme(theme: string) {
if (!isBrowser()) return
@@ -24,11 +43,12 @@ function applyTheme(theme: string) {
root.setAttribute('data-theme', theme)
if (appRoot) appRoot.setAttribute('data-theme', theme)
- if (theme === 'dark') {
+ const effective = resolveEffectiveTheme(theme)
+ if (effective === 'dark') {
root.classList.add('dark')
root.classList.remove('light')
root.style.colorScheme = 'dark'
- } else if (theme === 'light') {
+ } else if (effective === 'light') {
root.classList.add('light')
root.classList.remove('dark')
root.style.colorScheme = 'light'
@@ -39,11 +59,7 @@ function applyTheme(theme: string) {
const meta = getMetaThemeColor()
if (meta) {
- if (theme === 'light') {
- meta.content = '#f8fafc'
- } else {
- meta.content = '#111111'
- }
+ meta.content = effective === 'light' ? '#f8fafc' : '#111111'
}
try {
@@ -59,18 +75,37 @@ function detectSystemTheme(): 'dark' | 'light' {
return prefersDark ? 'dark' : 'light'
}
+function resolveEffectiveTheme(theme: string): 'dark' | 'light' | string {
+ if (theme === 'System' || theme === 'system') {
+ return detectSystemTheme()
+ }
+ if (theme === 'dark' || theme.endsWith('Dark')) return 'dark'
+ if (theme === 'light' || theme.endsWith('Light')) return 'light'
+ return theme
+}
+
export function useTheme() {
const config = useRuntimeConfig()
+ const { isLoaded: siteConfigLoaded, defaultTheme: siteDefaultTheme, enabledThemes: siteEnabledThemes, initSiteConfig } = useSiteConfig()
const enabledThemes = computed<string[]>(() => {
- const list = (config.public?.enabledThemes as unknown) as string[] | undefined
- return Array.isArray(list) && list.length > 0 ? list : ['dark', 'light']
+ if (siteConfigLoaded.value && siteEnabledThemes.value.length > 0) {
+ return [...new Set(siteEnabledThemes.value)]
+ }
+ const fromRuntime = parseEnabledThemesRaw(config.public?.enabledThemes)
+ if (fromRuntime.length > 0) return fromRuntime
+ return ['System', 'dark', 'light']
})
const defaultTheme = computed<string>(() => {
- const value = config.public?.defaultTheme as string | undefined
- if (value && enabledThemes.value.includes(value)) return value
- return enabledThemes.value.includes('dark') ? 'dark' : enabledThemes.value[0] || 'dark'
+ if (siteConfigLoaded.value && siteDefaultTheme.value) {
+ return siteDefaultTheme.value
+ }
+ const fromRuntime = config.public?.defaultTheme
+ if (typeof fromRuntime === 'string' && fromRuntime) {
+ return fromRuntime
+ }
+ return 'System'
})
const theme = computed<string>({
@@ -79,21 +114,22 @@ export function useTheme() {
})
const isDark = computed(() => {
- const t = theme.value
- if (t === 'dark') return true
- if (t === 'light') return false
- return detectSystemTheme() === 'dark'
+ return resolveEffectiveTheme(theme.value) === 'dark'
})
function isThemeEnabled(name: string) {
return enabledThemes.value.includes(name)
}
+ function firstValidTheme(candidate: string): string {
+ const list = enabledThemes.value
+ if (list.includes(candidate)) return candidate
+ if (list.includes(defaultTheme.value)) return defaultTheme.value
+ return list[0] ?? 'System'
+ }
+
function setTheme(next: string, { persist = true } = {}) {
- let candidate = next
- if (!enabledThemes.value.includes(candidate)) {
- candidate = defaultTheme.value
- }
+ const candidate = firstValidTheme(next)
if (currentTheme.value === candidate && initialized.value) return
@@ -110,7 +146,7 @@ export function useTheme() {
}
function toggleTheme() {
- const order = enabledThemes.value.length >= 2 ? enabledThemes.value : ['dark', 'light']
+ const order = enabledThemes.value.length >= 2 ? enabledThemes.value : ['System', 'dark', 'light']
const current = theme.value
const idx = order.indexOf(current)
const next = order[(idx + 1) % order.length]
@@ -121,8 +157,31 @@ export function useTheme() {
setTheme(defaultTheme.value, { persist: false })
}
- function init() {
+ function onSystemThemeChange() {
+ const stored = (() => {
+ try {
+ return localStorage.getItem(STORAGE_KEY)
+ } catch {
+ return null
+ }
+ })()
+ if (!stored) {
+ applyTheme(theme.value)
+ }
+ }
+
+ async function init() {
if (initialized.value || !isBrowser()) return
+
+ if (!siteConfigLoaded.value) {
+ try {
+ await initSiteConfig()
+ await nextTick()
+ } catch (_) {
+ /* ignore */
+ }
+ }
+
initialized.value = true
let stored: string | null = null
@@ -132,34 +191,43 @@ export function useTheme() {
stored = null
}
- if (stored && enabledThemes.value.includes(stored)) {
- setTheme(stored)
- return
+ if (stored) {
+ const valid = firstValidTheme(stored)
+ if (valid === stored || enabledThemes.value.includes(stored)) {
+ setTheme(stored)
+ watchSystemTheme()
+ return
+ }
}
setTheme(defaultTheme.value, { persist: false })
+ watchSystemTheme()
+ }
- if (window.matchMedia) {
- const mql = window.matchMedia('(prefers-color-scheme: dark)')
- const onChange = () => {
- let s: string | null = null
- try {
- s = localStorage.getItem(STORAGE_KEY)
- } catch {
- s = null
- }
- if (!s) {
- setTheme(defaultTheme.value, { persist: false })
- }
- }
- if (typeof mql.addEventListener === 'function') {
- mql.addEventListener('change', onChange)
- } else if (typeof mql.addListener === 'function') {
- mql.addListener(onChange)
- }
+ function watchSystemTheme() {
+ if (!isBrowser() || !window.matchMedia) return
+ const mql = window.matchMedia('(prefers-color-scheme: dark)')
+ if (typeof mql.addEventListener === 'function') {
+ mql.addEventListener('change', onSystemThemeChange)
+ } else if (typeof (mql as any).addListener === 'function') {
+ ;(mql as any).addListener(onSystemThemeChange)
}
}
+ watch(defaultTheme, () => {
+ if (!initialized.value) return
+ const stored = (() => {
+ try {
+ return localStorage.getItem(STORAGE_KEY)
+ } catch {
+ return null
+ }
+ })()
+ if (!stored) {
+ setTheme(defaultTheme.value, { persist: false })
+ }
+ })
+
return {
theme,
currentTheme,
diff --git a/app/drizzle/db.ts b/app/drizzle/db.ts
index 6d2b88f..abe7f4d 100644
--- a/app/drizzle/db.ts
+++ b/app/drizzle/db.ts
@@ -20,17 +20,21 @@ const connectionString = process.env.DATABASE_URL;
// 妫€娴嬫暟鎹簱绫诲瀷
const isNeonDatabase = connectionString.includes('neon.tech') || connectionString.includes('neon.database.com');
-// 鏍规嵁鏁版嵁搴撶被鍨嬮€夋嫨閰嶇疆
+// 妫€娴?Serverless 閮ㄧ讲鐜
+const isServerlessEnv = process.env.NETLIFY === 'true' || !!process.env.VERCEL;
+
+// 鏍规嵁鏁版嵁搴撶被鍨嬪拰閮ㄧ讲鐜閫夋嫨閰嶇疆
const getDatabaseConfig = () => {
- if (isNeonDatabase) {
- // Neon Database Serverless 浼樺寲閰嶇疆
+ if (isNeonDatabase || isServerlessEnv) {
+ // Neon Database / Serverless 鐜 (Netlify/Vercel) 浼樺寲閰嶇疆
return {
- max: 1, // Serverless 鐜涓嬫瘡涓疄渚嬩繚鎸佹渶灏忚繛鎺ユ暟锛屽埄鐢?Neon 鑷韩鐨勮繛鎺ユ睜
- idle_timeout: 0, // 绔嬪嵆閲婃斁绌洪棽杩炴帴锛岄€傚簲 Serverless 鐨勫揩閫熷喕缁撶壒鎬?- connect_timeout: 10, // Neon 杩炴帴閫熷害蹇紝鍑忓皯瓒呮椂鏃堕棿
+ max: 1, // Serverless 鐜涓嬫瘡涓疄渚嬩繚鎸佹渶灏忚繛鎺ユ暟锛屽埄鐢ㄥ钩鍙拌嚜韬殑杩炴帴姹?+ idle_timeout: 1, // 灏藉揩閲婃斁绌洪棽杩炴帴锛岄€傚簲 Serverless 鐨勫揩閫熷喕缁撶壒鎬?+ connect_timeout: 10, // 鍑忓皯杩炴帴瓒呮椂鏃堕棿
max_lifetime: 3600, // 杩炴帴鏈€澶х敓鍛藉懆鏈燂紙1灏忔椂锛?- ssl: 'require', // Neon 榛樿闇€瑕?SSL
+ ssl: isNeonDatabase ? 'require' : (connectionString.includes('sslmode=require') || connectionString.includes('ssl=true') ? 'require' : false),
prepare: false, // 绂佺敤棰勫鐞嗚鍙ヤ互鎻愰珮鍏煎鎬?+ keep_alive: false, // 绂佺敤 TCP keep-alive锛岄伩鍏?Serverless 鐜涓嬭繛鎺ユ畫鐣? transform: {
undefined: null, // 灏唘ndefined杞崲涓簄ull
},
@@ -41,11 +45,11 @@ const getDatabaseConfig = () => {
debug: process.env.NODE_ENV === 'development' && process.env.DEBUG_SQL === 'true'
};
} else {
- // 鏍囧噯 PostgreSQL 鏁版嵁搴撻厤缃?+ // 鏍囧噯 PostgreSQL 鏁版嵁搴撻厤缃紙鏈湴寮€鍙?/ 甯歌 Node Server锛? return {
- max: process.env.NODE_ENV === 'production' ? 10 : 5, // 鏅€歅ostgreSQL鍙互鏀寔鏇村杩炴帴
- idle_timeout: 20, // 澧炲姞绌洪棽瓒呮椂鏃堕棿
- connect_timeout: 30, // 澧炲姞杩炴帴瓒呮椂鏃堕棿浠ラ€傚簲缃戠粶寤惰繜
+ max: 10, // 鏅€歅ostgreSQL鍙互鏀寔鏇村杩炴帴
+ idle_timeout: 20, // 绌洪棽瓒呮椂鏃堕棿
+ connect_timeout: 30, // 杩炴帴瓒呮椂鏃堕棿浠ラ€傚簲缃戠粶寤惰繜
max_lifetime: 3600, // 杩炴帴鏈€澶х敓鍛藉懆鏈燂紙1灏忔椂锛? ssl: connectionString.includes('sslmode=require') || connectionString.includes('ssl=true') ? 'require' : false,
prepare: false, // 绂佺敤棰勫鐞嗚鍙ヤ互鎻愰珮鍏煎鎬?diff --git a/app/drizzle/migrations/20260906084643_auto-migration.sql b/app/drizzle/migrations/20260906084643_auto-migration.sql
new file mode 100644
index 0000000..e854472
--- /dev/null
+++ b/app/drizzle/migrations/20260906084643_auto-migration.sql
@@ -0,0 +1,318 @@
+CREATE TYPE "public"."card_code_status" AS ENUM('AVAILABLE', 'LOCKED', 'REDEEMED', 'INVALID');--> statement-breakpoint
+ALTER TYPE "public"."BlacklistType" ADD VALUE 'LANGUAGE';--> statement-breakpoint
+ALTER TYPE "public"."BlacklistType" ADD VALUE 'GENRE';--> statement-breakpoint
+ALTER TYPE "public"."user_status" ADD VALUE 'pending';--> statement-breakpoint
+ALTER TYPE "public"."user_status" ADD VALUE 'rejected';--> statement-breakpoint
+CREATE TABLE "auth_sessions" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" integer NOT NULL,
+ "session_token" varchar(255) NOT NULL,
+ "expires_at" timestamp with time zone NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "ip_address" text,
+ "user_agent" text,
+ "last_active_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "is_revoked" boolean DEFAULT false NOT NULL,
+ "browser" text,
+ "operating_system" text,
+ "device" text,
+ "login_method" text DEFAULT 'password' NOT NULL,
+ "revoked_at" timestamp with time zone,
+ "revoked_reason" text,
+ CONSTRAINT "auth_sessions_session_token_unique" UNIQUE("session_token")
+);
+--> statement-breakpoint
+CREATE TABLE "backup_records" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "filename" text NOT NULL,
+ "file_path" text NOT NULL,
+ "file_size" bigint NOT NULL,
+ "type" text DEFAULT 'MANUAL' NOT NULL,
+ "status" text DEFAULT 'COMPLETED' NOT NULL,
+ "error_message" text,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "started_at" timestamp with time zone,
+ "completed_at" timestamp with time zone,
+ "created_by" integer,
+ "expires_at" timestamp with time zone
+);
+--> statement-breakpoint
+CREATE TABLE "card_code_redemptions" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "card_code_id" integer NOT NULL,
+ "user_id" integer NOT NULL,
+ "redeemed_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "song_id" integer,
+ "purpose" text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "card_codes" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "code" varchar(64) NOT NULL,
+ "status" "card_code_status" DEFAULT 'AVAILABLE' NOT NULL,
+ "type" text NOT NULL,
+ "value" integer DEFAULT 1 NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "expires_at" timestamp with time zone,
+ "locked_at" timestamp with time zone,
+ "locked_by" integer,
+ "batch_id" varchar(64),
+ "note" text,
+ CONSTRAINT "card_codes_code_unique" UNIQUE("code")
+);
+--> statement-breakpoint
+CREATE TABLE "ChangelogEntry" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL,
+ "version" text NOT NULL,
+ "title" text NOT NULL,
+ "content" text NOT NULL,
+ "published" boolean DEFAULT false NOT NULL,
+ "publishedAt" timestamp,
+ "createdBy" integer
+);
+--> statement-breakpoint
+CREATE TABLE "ExpectedPlayDate" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL,
+ "date" text NOT NULL,
+ "availableSequences" text NOT NULL,
+ "enabled" boolean DEFAULT true NOT NULL,
+ CONSTRAINT "ExpectedPlayDate_date_unique" UNIQUE("date")
+);
+--> statement-breakpoint
+CREATE TABLE "GradeClass" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL,
+ "grade" text NOT NULL,
+ "class" text NOT NULL,
+ "enabled" boolean DEFAULT true NOT NULL,
+ CONSTRAINT "grade_class_unique" UNIQUE("grade","class")
+);
+--> statement-breakpoint
+CREATE TABLE "oauth_providers" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "displayName" text,
+ "enabled" boolean DEFAULT true NOT NULL,
+ "clientId" text,
+ "clientSecret" text,
+ "authorizationEndpoint" text,
+ "tokenEndpoint" text,
+ "userInfoEndpoint" text,
+ "scope" text,
+ "redirectUri" text,
+ "usernameClaim" text DEFAULT 'preferred_username',
+ "nameClaim" text DEFAULT 'name',
+ "emailClaim" text DEFAULT 'email',
+ "avatarClaim" text,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ "sortOrder" integer DEFAULT 0 NOT NULL,
+ "icon" text
+);
+--> statement-breakpoint
+CREATE TABLE "passkey_challenges" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "challenge" text NOT NULL,
+ "type" text NOT NULL,
+ "user_id" integer,
+ "expires_at" timestamp with time zone NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "schedule_song_pool" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL,
+ "song_id" integer NOT NULL,
+ "target_play_date" timestamp,
+ "priority" integer DEFAULT 0 NOT NULL,
+ "added_by" integer,
+ "note" text,
+ "status" text DEFAULT 'PENDING' NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "SongComment" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL,
+ "songId" integer NOT NULL,
+ "userId" integer NOT NULL,
+ "content" text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "submission_restriction_exemptions" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "user_id" integer NOT NULL,
+ "exemption_type" text NOT NULL,
+ "reason" text,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "expires_at" timestamp with time zone,
+ "created_by" integer,
+ CONSTRAINT "submission_restriction_exemptions_user_type_unique" UNIQUE("user_id","exemption_type")
+);
+--> statement-breakpoint
+CREATE TABLE "user_passkeys" (
+ "id" text PRIMARY KEY NOT NULL,
+ "user_id" integer NOT NULL,
+ "name" text,
+ "public_key" text NOT NULL,
+ "credential_device_type" text NOT NULL,
+ "credential_backed_up" boolean NOT NULL,
+ "transports" text,
+ "counter" integer DEFAULT 0 NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "last_used_at" timestamp with time zone
+);
+--> statement-breakpoint
+ALTER TABLE "User" ALTER COLUMN "forcePasswordChange" SET DEFAULT false;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "batch_id" uuid;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "source" text DEFAULT 'SYSTEM' NOT NULL;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "sender_id" integer;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "sender_name" text;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "sender_username" text;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "title" text;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "important" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "Notification" ADD COLUMN "user_deleted" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "Schedule" ADD COLUMN "replay_request_id" integer;--> statement-breakpoint
+ALTER TABLE "Song" ADD COLUMN "expectedPlayDate" text;--> statement-breakpoint
+ALTER TABLE "Song" ADD COLUMN "expectedPlaySequence" integer;--> statement-breakpoint
+ALTER TABLE "Song" ADD COLUMN "durationSeconds" integer;--> statement-breakpoint
+ALTER TABLE "Song" ADD COLUMN "submissionNotePublicStatus" text;--> statement-breakpoint
+ALTER TABLE "Song" ADD COLUMN "cardCodeId" integer;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "siteVersion" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "submissionNoteRequiresApproval" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "instanceId" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "telemetryEnabled" boolean DEFAULT true NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "showBeianIcon" boolean DEFAULT true NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysDisplay" integer DEFAULT 7 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysEditable" integer DEFAULT 3 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysRequestable" integer DEFAULT 14 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysBeforeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysBefore" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysAfterEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "scheduleDaysAfter" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "forcePasswordChangeOnFirstLogin" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "enableSubmissionRestriction" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionScope" text DEFAULT 'all' NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "sameSongRestrictionHours" integer;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "sameArtistRestrictionHours" integer;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeSubmissionRequired" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeVoteEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "cardCodeReplayEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "enableCardCodeRequests" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "requireCardCodeForRequests" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "enableCardCodeLimitBypass" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionGradeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionClassEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionGraduateEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "submissionRestrictionWithdrawnEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "captchaProvider" text DEFAULT 'TURNSTILE';--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "captchaEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "turnstileSiteKey" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "turnstileSecretKey" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "allowRegister" boolean DEFAULT true NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "registerRequireEmail" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "registerRequireEmailVerify" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "registerDefaultRole" text DEFAULT 'USER';--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "registerEnabledGradeClasses" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "registerRequiresApproval" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthRegisterRequiresApproval" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "registerEmailRequired" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "registerRequiresGradeClass" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthAllowRegister" boolean DEFAULT true NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthAllowLogin" boolean DEFAULT true NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthAllowBind" boolean DEFAULT true NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "allowOAuthRegistration" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthRedirectUri" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthStateSecret" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "oauthProviders" text DEFAULT '[]';--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "githubOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "githubClientId" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "githubClientSecret" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "casdoorOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "casdoorServerUrl" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "casdoorClientId" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "casdoorClientSecret" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "casdoorOrganizationName" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "googleOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "googleClientId" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "googleClientSecret" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthAppId" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthAppKey" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthLoginType" text DEFAULT 'qq';--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "aggregateOAuthEndpoint" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthDisplayName" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthAuthorizeUrl" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthTokenUrl" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthUserInfoUrl" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthScope" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthClientId" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthClientSecret" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthUserIdField" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthUsernameField" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthNameField" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthEmailField" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "customOAuthAvatarField" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "captchaMaxFailures" integer DEFAULT 3 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupIntervalHours" integer DEFAULT 24 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupRetentionDays" integer DEFAULT 30 NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "autoBackupConfig" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "statisticsCode" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "statisticsCodeEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "defaultTheme" text DEFAULT 'System' NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "enabledThemes" text DEFAULT '["System","ClassicDark","ClassicLight","ModernLight"]' NOT NULL;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "enabledPlatforms" text;--> statement-breakpoint
+ALTER TABLE "SystemSettings" ADD COLUMN "platformOrder" text;--> statement-breakpoint
+ALTER TABLE "User" ADD COLUMN "studentId" text;--> statement-breakpoint
+ALTER TABLE "User" ADD COLUMN "tokenVersion" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
+ALTER TABLE "User" ADD COLUMN "avatarProvider" text;--> statement-breakpoint
+ALTER TABLE "User" ADD COLUMN "avatarProviderUserId" text;--> statement-breakpoint
+ALTER TABLE "User" ADD COLUMN "remark" text;--> statement-breakpoint
+ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "card_code_redemptions" ADD CONSTRAINT "card_code_redemptions_card_code_id_card_codes_id_fk" FOREIGN KEY ("card_code_id") REFERENCES "public"."card_codes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "card_code_redemptions" ADD CONSTRAINT "card_code_redemptions_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "card_code_redemptions" ADD CONSTRAINT "card_code_redemptions_song_id_Song_id_fk" FOREIGN KEY ("song_id") REFERENCES "public"."Song"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "schedule_song_pool" ADD CONSTRAINT "schedule_song_pool_song_id_Song_id_fk" FOREIGN KEY ("song_id") REFERENCES "public"."Song"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "submission_restriction_exemptions" ADD CONSTRAINT "submission_restriction_exemptions_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user_passkeys" ADD CONSTRAINT "user_passkeys_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "auth_sessions_user_active_idx" ON "auth_sessions" USING btree ("user_id","is_revoked","expires_at");--> statement-breakpoint
+CREATE INDEX "auth_sessions_expires_at_idx" ON "auth_sessions" USING btree ("expires_at");--> statement-breakpoint
+CREATE INDEX "auth_sessions_last_active_idx" ON "auth_sessions" USING btree ("last_active_at");--> statement-breakpoint
+CREATE UNIQUE INDEX "auth_sessions_session_token_unique" ON "auth_sessions" USING btree ("session_token");--> statement-breakpoint
+CREATE INDEX "backup_records_created_at_idx" ON "backup_records" USING btree ("created_at");--> statement-breakpoint
+CREATE INDEX "backup_records_status_idx" ON "backup_records" USING btree ("status");--> statement-breakpoint
+CREATE INDEX "card_code_redemptions_user_id_idx" ON "card_code_redemptions" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "card_code_redemptions_card_code_id_idx" ON "card_code_redemptions" USING btree ("card_code_id");--> statement-breakpoint
+CREATE INDEX "card_codes_status_idx" ON "card_codes" USING btree ("status");--> statement-breakpoint
+CREATE INDEX "card_codes_batch_id_idx" ON "card_codes" USING btree ("batch_id");--> statement-breakpoint
+CREATE INDEX "card_codes_expires_at_idx" ON "card_codes" USING btree ("expires_at");--> statement-breakpoint
+CREATE INDEX "passkey_challenges_challenge_idx" ON "passkey_challenges" USING btree ("challenge");--> statement-breakpoint
+CREATE INDEX "passkey_challenges_expires_at_idx" ON "passkey_challenges" USING btree ("expires_at");--> statement-breakpoint
+CREATE INDEX "schedule_song_pool_song_id_idx" ON "schedule_song_pool" USING btree ("song_id");--> statement-breakpoint
+CREATE INDEX "schedule_song_pool_target_date_idx" ON "schedule_song_pool" USING btree ("target_play_date");--> statement-breakpoint
+CREATE INDEX "submission_restriction_exemptions_user_id_idx" ON "submission_restriction_exemptions" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "user_passkeys_user_id_idx" ON "user_passkeys" USING btree ("user_id");--> statement-breakpoint
+ALTER TABLE "Song" ADD CONSTRAINT "Song_cardCodeId_card_codes_id_fk" FOREIGN KEY ("cardCodeId") REFERENCES "public"."card_codes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "notification_user_important_read_created_idx" ON "Notification" USING btree ("userId","important","read","createdAt");--> statement-breakpoint
+CREATE INDEX "notification_batch_id_idx" ON "Notification" USING btree ("batch_id");--> statement-breakpoint
+CREATE INDEX "schedule_published_song_idx" ON "Schedule" USING btree ("publishedAt","songId");--> statement-breakpoint
+CREATE INDEX "schedule_published_date_idx" ON "Schedule" USING btree ("publishedAt","playDate");--> statement-breakpoint
+CREATE INDEX "song_card_code_id_idx" ON "Song" USING btree ("cardCodeId");--> statement-breakpoint
+CREATE INDEX "song_semester_created_at_idx" ON "Song" USING btree ("semester","createdAt");--> statement-breakpoint
+CREATE INDEX "song_requester_id_idx" ON "Song" USING btree ("requesterId");--> statement-breakpoint
+CREATE UNIQUE INDEX "User_username_unique" ON "User" USING btree ("username");--> statement-breakpoint
+CREATE INDEX "vote_user_song_idx" ON "Vote" USING btree ("userId","songId");--> statement-breakpoint
+ALTER TABLE "Vote" ADD CONSTRAINT "vote_song_user_unique" UNIQUE("songId","userId");
\ No newline at end of file
diff --git a/app/drizzle/migrations/meta/20260906084643_snapshot.json b/app/drizzle/migrations/meta/20260906084643_snapshot.json
new file mode 100644
index 0000000..3d7de32
--- /dev/null
+++ b/app/drizzle/migrations/meta/20260906084643_snapshot.json
@@ -0,0 +1,3953 @@
+{
+ "id": "e55ad241-91ce-4c7e-9d3a-79433b19976e",
+ "prevId": "9a604776-4a2c-47e2-ac0f-87bdc9c94abf",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.api_key_permissions": {
+ "name": "api_key_permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "api_key_id": {
+ "name": "api_key_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_keys": {
+ "name": "api_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "key_hash": {
+ "name": "key_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key_prefix": {
+ "name": "key_prefix",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "usage_count": {
+ "name": "usage_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_keys_key_hash_unique": {
+ "name": "api_keys_key_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "key_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_logs": {
+ "name": "api_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "api_key_id": {
+ "name": "api_key_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "method": {
+ "name": "method",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_code": {
+ "name": "status_code",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_time_ms": {
+ "name": "response_time_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_body": {
+ "name": "request_body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_body": {
+ "name": "response_body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_sessions": {
+ "name": "auth_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_active_at": {
+ "name": "last_active_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "is_revoked": {
+ "name": "is_revoked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "browser": {
+ "name": "browser",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "operating_system": {
+ "name": "operating_system",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "device": {
+ "name": "device",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "login_method": {
+ "name": "login_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'password'"
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_reason": {
+ "name": "revoked_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "auth_sessions_user_active_idx": {
+ "name": "auth_sessions_user_active_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_revoked",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auth_sessions_expires_at_idx": {
+ "name": "auth_sessions_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auth_sessions_last_active_idx": {
+ "name": "auth_sessions_last_active_idx",
+ "columns": [
+ {
+ "expression": "last_active_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auth_sessions_session_token_unique": {
+ "name": "auth_sessions_session_token_unique",
+ "columns": [
+ {
+ "expression": "session_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "auth_sessions_user_id_User_id_fk": {
+ "name": "auth_sessions_user_id_User_id_fk",
+ "tableFrom": "auth_sessions",
+ "tableTo": "User",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "auth_sessions_session_token_unique": {
+ "name": "auth_sessions_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "session_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.backup_records": {
+ "name": "backup_records",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'MANUAL'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'COMPLETED'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "backup_records_created_at_idx": {
+ "name": "backup_records_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "backup_records_status_idx": {
+ "name": "backup_records_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.card_code_redemptions": {
+ "name": "card_code_redemptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "card_code_id": {
+ "name": "card_code_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "redeemed_at": {
+ "name": "redeemed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "song_id": {
+ "name": "song_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "card_code_redemptions_user_id_idx": {
+ "name": "card_code_redemptions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "card_code_redemptions_card_code_id_idx": {
+ "name": "card_code_redemptions_card_code_id_idx",
+ "columns": [
+ {
+ "expression": "card_code_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "card_code_redemptions_card_code_id_card_codes_id_fk": {
+ "name": "card_code_redemptions_card_code_id_card_codes_id_fk",
+ "tableFrom": "card_code_redemptions",
+ "tableTo": "card_codes",
+ "columnsFrom": [
+ "card_code_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "card_code_redemptions_user_id_User_id_fk": {
+ "name": "card_code_redemptions_user_id_User_id_fk",
+ "tableFrom": "card_code_redemptions",
+ "tableTo": "User",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "card_code_redemptions_song_id_Song_id_fk": {
+ "name": "card_code_redemptions_song_id_Song_id_fk",
+ "tableFrom": "card_code_redemptions",
+ "tableTo": "Song",
+ "columnsFrom": [
+ "song_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.card_codes": {
+ "name": "card_codes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "card_code_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'AVAILABLE'"
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_at": {
+ "name": "locked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_by": {
+ "name": "locked_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "batch_id": {
+ "name": "batch_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "card_codes_status_idx": {
+ "name": "card_codes_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "card_codes_batch_id_idx": {
+ "name": "card_codes_batch_id_idx",
+ "columns": [
+ {
+ "expression": "batch_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "card_codes_expires_at_idx": {
+ "name": "card_codes_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "card_codes_code_unique": {
+ "name": "card_codes_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ChangelogEntry": {
+ "name": "ChangelogEntry",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "published": {
+ "name": "published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "publishedAt": {
+ "name": "publishedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdBy": {
+ "name": "createdBy",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collaboration_logs": {
+ "name": "collaboration_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "collaborator_id": {
+ "name": "collaborator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "operator_id": {
+ "name": "operator_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.EmailTemplate": {
+ "name": "EmailTemplate",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "html": {
+ "name": "html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updatedByUserId": {
+ "name": "updatedByUserId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ExpectedPlayDate": {
+ "name": "ExpectedPlayDate",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "date": {
+ "name": "date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "availableSequences": {
+ "name": "availableSequences",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ExpectedPlayDate_date_unique": {
+ "name": "ExpectedPlayDate_date_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "date"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.GradeClass": {
+ "name": "GradeClass",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "grade": {
+ "name": "grade",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "class": {
+ "name": "class",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "grade_class_unique": {
+ "name": "grade_class_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "grade",
+ "class"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.NotificationSettings": {
+ "name": "NotificationSettings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "songRequestEnabled": {
+ "name": "songRequestEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "songVotedEnabled": {
+ "name": "songVotedEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "songPlayedEnabled": {
+ "name": "songPlayedEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "refreshInterval": {
+ "name": "refreshInterval",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 60
+ },
+ "songVotedThreshold": {
+ "name": "songVotedThreshold",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Notification": {
+ "name": "Notification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "read": {
+ "name": "read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "songId": {
+ "name": "songId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "batch_id": {
+ "name": "batch_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'SYSTEM'"
+ },
+ "sender_id": {
+ "name": "sender_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sender_name": {
+ "name": "sender_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sender_username": {
+ "name": "sender_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "important": {
+ "name": "important",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "user_deleted": {
+ "name": "user_deleted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {
+ "notification_user_important_read_created_idx": {
+ "name": "notification_user_important_read_created_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "important",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "createdAt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notification_batch_id_idx": {
+ "name": "notification_batch_id_idx",
+ "columns": [
+ {
+ "expression": "batch_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_providers": {
+ "name": "oauth_providers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "displayName": {
+ "name": "displayName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "clientId": {
+ "name": "clientId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "clientSecret": {
+ "name": "clientSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "authorizationEndpoint": {
+ "name": "authorizationEndpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokenEndpoint": {
+ "name": "tokenEndpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userInfoEndpoint": {
+ "name": "userInfoEndpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirectUri": {
+ "name": "redirectUri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "usernameClaim": {
+ "name": "usernameClaim",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'preferred_username'"
+ },
+ "nameClaim": {
+ "name": "nameClaim",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'name'"
+ },
+ "emailClaim": {
+ "name": "emailClaim",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'email'"
+ },
+ "avatarClaim": {
+ "name": "avatarClaim",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "sortOrder": {
+ "name": "sortOrder",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.passkey_challenges": {
+ "name": "passkey_challenges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "challenge": {
+ "name": "challenge",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "passkey_challenges_challenge_idx": {
+ "name": "passkey_challenges_challenge_idx",
+ "columns": [
+ {
+ "expression": "challenge",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "passkey_challenges_expires_at_idx": {
+ "name": "passkey_challenges_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.PlayTime": {
+ "name": "PlayTime",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "startTime": {
+ "name": "startTime",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endTime": {
+ "name": "endTime",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.RequestTime": {
+ "name": "RequestTime",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(6)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(6)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "startTime": {
+ "name": "startTime",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endTime": {
+ "name": "endTime",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expected": {
+ "name": "expected",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "accepted": {
+ "name": "accepted",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "past": {
+ "name": "past",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.schedule_song_pool": {
+ "name": "schedule_song_pool",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "song_id": {
+ "name": "song_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_play_date": {
+ "name": "target_play_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "added_by": {
+ "name": "added_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ }
+ },
+ "indexes": {
+ "schedule_song_pool_song_id_idx": {
+ "name": "schedule_song_pool_song_id_idx",
+ "columns": [
+ {
+ "expression": "song_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "schedule_song_pool_target_date_idx": {
+ "name": "schedule_song_pool_target_date_idx",
+ "columns": [
+ {
+ "expression": "target_play_date",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "schedule_song_pool_song_id_Song_id_fk": {
+ "name": "schedule_song_pool_song_id_Song_id_fk",
+ "tableFrom": "schedule_song_pool",
+ "tableTo": "Song",
+ "columnsFrom": [
+ "song_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Schedule": {
+ "name": "Schedule",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "songId": {
+ "name": "songId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "playDate": {
+ "name": "playDate",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "played": {
+ "name": "played",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sequence": {
+ "name": "sequence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "playTimeId": {
+ "name": "playTimeId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isDraft": {
+ "name": "isDraft",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "publishedAt": {
+ "name": "publishedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replay_request_id": {
+ "name": "replay_request_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "schedule_published_song_idx": {
+ "name": "schedule_published_song_idx",
+ "columns": [
+ {
+ "expression": "publishedAt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "songId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "schedule_published_date_idx": {
+ "name": "schedule_published_date_idx",
+ "columns": [
+ {
+ "expression": "publishedAt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "playDate",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Semester": {
+ "name": "Semester",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "isActive": {
+ "name": "isActive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.SongBlacklist": {
+ "name": "SongBlacklist",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "type": {
+ "name": "type",
+ "type": "BlacklistType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isActive": {
+ "name": "isActive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "createdBy": {
+ "name": "createdBy",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.song_collaborators": {
+ "name": "song_collaborators",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "song_id": {
+ "name": "song_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "collaborator_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.SongComment": {
+ "name": "SongComment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "songId": {
+ "name": "songId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.song_replay_requests": {
+ "name": "song_replay_requests",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "song_id": {
+ "name": "song_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "status": {
+ "name": "status",
+ "type": "replay_request_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "song_replay_requests_song_id_user_id_unique": {
+ "name": "song_replay_requests_song_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "song_id",
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Song": {
+ "name": "Song",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "kg_hash": {
+ "name": "kg_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "singer": {
+ "name": "singer",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_url": {
+ "name": "cover_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "artist": {
+ "name": "artist",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "requesterId": {
+ "name": "requesterId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "played": {
+ "name": "played",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "playedAt": {
+ "name": "playedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "semester": {
+ "name": "semester",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preferredPlayTimeId": {
+ "name": "preferredPlayTimeId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover": {
+ "name": "cover",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "playUrl": {
+ "name": "playUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "musicPlatform": {
+ "name": "musicPlatform",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "musicId": {
+ "name": "musicId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submissionNote": {
+ "name": "submissionNote",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submissionNotePublic": {
+ "name": "submissionNotePublic",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "hitRequestId": {
+ "name": "hitRequestId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expectedPlayDate": {
+ "name": "expectedPlayDate",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expectedPlaySequence": {
+ "name": "expectedPlaySequence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "durationSeconds": {
+ "name": "durationSeconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submissionNotePublicStatus": {
+ "name": "submissionNotePublicStatus",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cardCodeId": {
+ "name": "cardCodeId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "song_card_code_id_idx": {
+ "name": "song_card_code_id_idx",
+ "columns": [
+ {
+ "expression": "cardCodeId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "song_semester_created_at_idx": {
+ "name": "song_semester_created_at_idx",
+ "columns": [
+ {
+ "expression": "semester",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "createdAt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "song_requester_id_idx": {
+ "name": "song_requester_id_idx",
+ "columns": [
+ {
+ "expression": "requesterId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Song_cardCodeId_card_codes_id_fk": {
+ "name": "Song_cardCodeId_card_codes_id_fk",
+ "tableFrom": "Song",
+ "tableTo": "card_codes",
+ "columnsFrom": [
+ "cardCodeId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.submission_restriction_exemptions": {
+ "name": "submission_restriction_exemptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "exemption_type": {
+ "name": "exemption_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "submission_restriction_exemptions_user_id_idx": {
+ "name": "submission_restriction_exemptions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "submission_restriction_exemptions_user_id_User_id_fk": {
+ "name": "submission_restriction_exemptions_user_id_User_id_fk",
+ "tableFrom": "submission_restriction_exemptions",
+ "tableTo": "User",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "submission_restriction_exemptions_user_type_unique": {
+ "name": "submission_restriction_exemptions_user_type_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id",
+ "exemption_type"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.SystemSettings": {
+ "name": "SystemSettings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "enablePlayTimeSelection": {
+ "name": "enablePlayTimeSelection",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "siteTitle": {
+ "name": "siteTitle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "siteLogoUrl": {
+ "name": "siteLogoUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schoolLogoHomeUrl": {
+ "name": "schoolLogoHomeUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schoolLogoPrintUrl": {
+ "name": "schoolLogoPrintUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "siteDescription": {
+ "name": "siteDescription",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "siteVersion": {
+ "name": "siteVersion",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submissionGuidelines": {
+ "name": "submissionGuidelines",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icpNumber": {
+ "name": "icpNumber",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gonganNumber": {
+ "name": "gonganNumber",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enableSubmissionLimit": {
+ "name": "enableSubmissionLimit",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "dailySubmissionLimit": {
+ "name": "dailySubmissionLimit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "weeklySubmissionLimit": {
+ "name": "weeklySubmissionLimit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "monthlySubmissionLimit": {
+ "name": "monthlySubmissionLimit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "showBlacklistKeywords": {
+ "name": "showBlacklistKeywords",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "hideStudentInfo": {
+ "name": "hideStudentInfo",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "smtpEnabled": {
+ "name": "smtpEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "smtpHost": {
+ "name": "smtpHost",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtpPort": {
+ "name": "smtpPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 587
+ },
+ "smtpSecure": {
+ "name": "smtpSecure",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "smtpUsername": {
+ "name": "smtpUsername",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtpPassword": {
+ "name": "smtpPassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtpFromEmail": {
+ "name": "smtpFromEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtpFromName": {
+ "name": "smtpFromName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'鏍″洯骞挎挱绔?"
+ },
+ "enableRequestTimeLimitation": {
+ "name": "enableRequestTimeLimitation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "forceBlockAllRequests": {
+ "name": "forceBlockAllRequests",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enableReplayRequests": {
+ "name": "enableReplayRequests",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enableCollaborativeSubmission": {
+ "name": "enableCollaborativeSubmission",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "enableSubmissionRemarks": {
+ "name": "enableSubmissionRemarks",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "submissionNoteRequiresApproval": {
+ "name": "submissionNoteRequiresApproval",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "instanceId": {
+ "name": "instanceId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telemetryEnabled": {
+ "name": "telemetryEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "showBeianIcon": {
+ "name": "showBeianIcon",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "scheduleDaysDisplay": {
+ "name": "scheduleDaysDisplay",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 7
+ },
+ "scheduleDaysEditable": {
+ "name": "scheduleDaysEditable",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "scheduleDaysRequestable": {
+ "name": "scheduleDaysRequestable",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 14
+ },
+ "scheduleDaysBeforeEnabled": {
+ "name": "scheduleDaysBeforeEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "scheduleDaysBefore": {
+ "name": "scheduleDaysBefore",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "scheduleDaysAfterEnabled": {
+ "name": "scheduleDaysAfterEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "scheduleDaysAfter": {
+ "name": "scheduleDaysAfter",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "forcePasswordChangeOnFirstLogin": {
+ "name": "forcePasswordChangeOnFirstLogin",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enableSubmissionRestriction": {
+ "name": "enableSubmissionRestriction",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "submissionRestrictionScope": {
+ "name": "submissionRestrictionScope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'all'"
+ },
+ "sameSongRestrictionHours": {
+ "name": "sameSongRestrictionHours",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sameArtistRestrictionHours": {
+ "name": "sameArtistRestrictionHours",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cardCodeEnabled": {
+ "name": "cardCodeEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cardCodeSubmissionRequired": {
+ "name": "cardCodeSubmissionRequired",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cardCodeVoteEnabled": {
+ "name": "cardCodeVoteEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cardCodeReplayEnabled": {
+ "name": "cardCodeReplayEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enableCardCodeRequests": {
+ "name": "enableCardCodeRequests",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "requireCardCodeForRequests": {
+ "name": "requireCardCodeForRequests",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enableCardCodeLimitBypass": {
+ "name": "enableCardCodeLimitBypass",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "submissionRestrictionEnabled": {
+ "name": "submissionRestrictionEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "submissionRestrictionGradeEnabled": {
+ "name": "submissionRestrictionGradeEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "submissionRestrictionClassEnabled": {
+ "name": "submissionRestrictionClassEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "submissionRestrictionGraduateEnabled": {
+ "name": "submissionRestrictionGraduateEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "submissionRestrictionWithdrawnEnabled": {
+ "name": "submissionRestrictionWithdrawnEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "captchaProvider": {
+ "name": "captchaProvider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'TURNSTILE'"
+ },
+ "captchaEnabled": {
+ "name": "captchaEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "turnstileSiteKey": {
+ "name": "turnstileSiteKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "turnstileSecretKey": {
+ "name": "turnstileSecretKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowRegister": {
+ "name": "allowRegister",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "registerRequireEmail": {
+ "name": "registerRequireEmail",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "registerRequireEmailVerify": {
+ "name": "registerRequireEmailVerify",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "registerDefaultRole": {
+ "name": "registerDefaultRole",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'USER'"
+ },
+ "registerEnabledGradeClasses": {
+ "name": "registerEnabledGradeClasses",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "registerRequiresApproval": {
+ "name": "registerRequiresApproval",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "oauthRegisterRequiresApproval": {
+ "name": "oauthRegisterRequiresApproval",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "registerEmailRequired": {
+ "name": "registerEmailRequired",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "registerRequiresGradeClass": {
+ "name": "registerRequiresGradeClass",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "oauthEnabled": {
+ "name": "oauthEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "oauthAllowRegister": {
+ "name": "oauthAllowRegister",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "oauthAllowLogin": {
+ "name": "oauthAllowLogin",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "oauthAllowBind": {
+ "name": "oauthAllowBind",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "allowOAuthRegistration": {
+ "name": "allowOAuthRegistration",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "oauthRedirectUri": {
+ "name": "oauthRedirectUri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauthStateSecret": {
+ "name": "oauthStateSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauthProviders": {
+ "name": "oauthProviders",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "githubOAuthEnabled": {
+ "name": "githubOAuthEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "githubClientId": {
+ "name": "githubClientId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubClientSecret": {
+ "name": "githubClientSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "casdoorOAuthEnabled": {
+ "name": "casdoorOAuthEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "casdoorServerUrl": {
+ "name": "casdoorServerUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "casdoorClientId": {
+ "name": "casdoorClientId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "casdoorClientSecret": {
+ "name": "casdoorClientSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "casdoorOrganizationName": {
+ "name": "casdoorOrganizationName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "googleOAuthEnabled": {
+ "name": "googleOAuthEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "googleClientId": {
+ "name": "googleClientId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "googleClientSecret": {
+ "name": "googleClientSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aggregateOAuthEnabled": {
+ "name": "aggregateOAuthEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "aggregateOAuthAppId": {
+ "name": "aggregateOAuthAppId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aggregateOAuthAppKey": {
+ "name": "aggregateOAuthAppKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aggregateOAuthLoginType": {
+ "name": "aggregateOAuthLoginType",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'qq'"
+ },
+ "aggregateOAuthEndpoint": {
+ "name": "aggregateOAuthEndpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthEnabled": {
+ "name": "customOAuthEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "customOAuthDisplayName": {
+ "name": "customOAuthDisplayName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthAuthorizeUrl": {
+ "name": "customOAuthAuthorizeUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthTokenUrl": {
+ "name": "customOAuthTokenUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthUserInfoUrl": {
+ "name": "customOAuthUserInfoUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthScope": {
+ "name": "customOAuthScope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthClientId": {
+ "name": "customOAuthClientId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthClientSecret": {
+ "name": "customOAuthClientSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthUserIdField": {
+ "name": "customOAuthUserIdField",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthUsernameField": {
+ "name": "customOAuthUsernameField",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthNameField": {
+ "name": "customOAuthNameField",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthEmailField": {
+ "name": "customOAuthEmailField",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customOAuthAvatarField": {
+ "name": "customOAuthAvatarField",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "captchaMaxFailures": {
+ "name": "captchaMaxFailures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "autoBackupEnabled": {
+ "name": "autoBackupEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "autoBackupIntervalHours": {
+ "name": "autoBackupIntervalHours",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 24
+ },
+ "autoBackupRetentionDays": {
+ "name": "autoBackupRetentionDays",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 30
+ },
+ "autoBackupConfig": {
+ "name": "autoBackupConfig",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "statisticsCode": {
+ "name": "statisticsCode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "statisticsCodeEnabled": {
+ "name": "statisticsCodeEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "defaultTheme": {
+ "name": "defaultTheme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'System'"
+ },
+ "enabledThemes": {
+ "name": "enabledThemes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"System\",\"ClassicDark\",\"ClassicLight\",\"ModernLight\"]'"
+ },
+ "enabledPlatforms": {
+ "name": "enabledPlatforms",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "platformOrder": {
+ "name": "platformOrder",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.UserIdentity": {
+ "name": "UserIdentity",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerUserId": {
+ "name": "providerUserId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerUsername": {
+ "name": "providerUsername",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "UserIdentity_userId_User_id_fk": {
+ "name": "UserIdentity_userId_User_id_fk",
+ "tableFrom": "UserIdentity",
+ "tableTo": "User",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "UserIdentity_provider_providerUserId_unique": {
+ "name": "UserIdentity_provider_providerUserId_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "provider",
+ "providerUserId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_passkeys": {
+ "name": "user_passkeys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_device_type": {
+ "name": "credential_device_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_backed_up": {
+ "name": "credential_backed_up",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "transports": {
+ "name": "transports",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "counter": {
+ "name": "counter",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_passkeys_user_id_idx": {
+ "name": "user_passkeys_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_passkeys_user_id_User_id_fk": {
+ "name": "user_passkeys_user_id_User_id_fk",
+ "tableFrom": "user_passkeys",
+ "tableTo": "User",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_status_logs": {
+ "name": "user_status_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "old_status": {
+ "name": "old_status",
+ "type": "user_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "new_status": {
+ "name": "new_status",
+ "type": "user_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "operator_id": {
+ "name": "operator_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.User": {
+ "name": "User",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grade": {
+ "name": "grade",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "class": {
+ "name": "class",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "studentId": {
+ "name": "studentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'USER'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "lastLogin": {
+ "name": "lastLogin",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastLoginIp": {
+ "name": "lastLoginIp",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "passwordChangedAt": {
+ "name": "passwordChangedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "forcePasswordChange": {
+ "name": "forcePasswordChange",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "meowNickname": {
+ "name": "meowNickname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meowBoundAt": {
+ "name": "meowBoundAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "user_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "statusChangedAt": {
+ "name": "statusChangedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "statusChangedBy": {
+ "name": "statusChangedBy",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokenVersion": {
+ "name": "tokenVersion",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "avatarProvider": {
+ "name": "avatarProvider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatarProviderUserId": {
+ "name": "avatarProviderUserId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remark": {
+ "name": "remark",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "User_username_unique": {
+ "name": "User_username_unique",
+ "columns": [
+ {
+ "expression": "username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Vote": {
+ "name": "Vote",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "songId": {
+ "name": "songId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "vote_user_song_idx": {
+ "name": "vote_user_song_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "songId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "vote_song_user_unique": {
+ "name": "vote_song_user_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "songId",
+ "userId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.BlacklistType": {
+ "name": "BlacklistType",
+ "schema": "public",
+ "values": [
+ "SONG",
+ "KEYWORD",
+ "LANGUAGE",
+ "GENRE"
+ ]
+ },
+ "public.card_code_status": {
+ "name": "card_code_status",
+ "schema": "public",
+ "values": [
+ "AVAILABLE",
+ "LOCKED",
+ "REDEEMED",
+ "INVALID"
+ ]
+ },
+ "public.collaborator_status": {
+ "name": "collaborator_status",
+ "schema": "public",
+ "values": [
+ "PENDING",
+ "ACCEPTED",
+ "REJECTED"
+ ]
+ },
+ "public.replay_request_status": {
+ "name": "replay_request_status",
+ "schema": "public",
+ "values": [
+ "PENDING",
+ "FULFILLED",
+ "REJECTED"
+ ]
+ },
+ "public.user_status": {
+ "name": "user_status",
+ "schema": "public",
+ "values": [
+ "active",
+ "withdrawn",
+ "graduate",
+ "pending",
+ "rejected"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/app/drizzle/migrations/meta/_journal.json b/app/drizzle/migrations/meta/_journal.json
index fd5d1f7..654606c 100644
--- a/app/drizzle/migrations/meta/_journal.json
+++ b/app/drizzle/migrations/meta/_journal.json
@@ -141,6 +141,13 @@
"when": 1776518400000,
"tag": "20260418112000_add_song_comments",
"breakpoints": true
+ },
+ {
+ "idx": 20,
+ "version": "7",
+ "when": 1788684403309,
+ "tag": "20260906084643_auto-migration",
+ "breakpoints": true
}
]
-}
+}
\ No newline at end of file
diff --git a/app/drizzle/schema.ts b/app/drizzle/schema.ts
index f705696..4399622 100644
--- a/app/drizzle/schema.ts
+++ b/app/drizzle/schema.ts
@@ -51,9 +51,16 @@ export const authSessions = pgTable('auth_sessions', {
userAgent: text('user_agent'),
lastActiveAt: timestamp('last_active_at', { withTimezone: true }).defaultNow().notNull(),
isRevoked: boolean('is_revoked').default(false).notNull(),
+ browser: text('browser'),
+ operatingSystem: text('operating_system'),
+ device: text('device'),
+ loginMethod: text('login_method').default('password').notNull(),
+ revokedAt: timestamp('revoked_at', { withTimezone: true }),
+ revokedReason: text('revoked_reason')
}, (table) => [
- index('auth_sessions_user_id_idx').on(table.userId),
+ index('auth_sessions_user_active_idx').on(table.userId, table.isRevoked, table.expiresAt),
index('auth_sessions_expires_at_idx').on(table.expiresAt),
+ index('auth_sessions_last_active_idx').on(table.lastActiveAt),
uniqueIndex('auth_sessions_session_token_unique').on(table.sessionToken),
]);
@@ -258,16 +265,29 @@ export const systemSettings = pgTable('SystemSettings', {
enableReplayRequests: boolean('enableReplayRequests').default(false).notNull(),
enableCollaborativeSubmission: boolean('enableCollaborativeSubmission').default(true).notNull(),
enableSubmissionRemarks: boolean('enableSubmissionRemarks').default(false).notNull(),
+ submissionNoteRequiresApproval: boolean('submissionNoteRequiresApproval').default(false).notNull(),
instanceId: text('instanceId'),
telemetryEnabled: boolean('telemetryEnabled').default(true).notNull(),
showBeianIcon: boolean('showBeianIcon').default(true).notNull(),
scheduleDaysDisplay: integer('scheduleDaysDisplay').default(7).notNull(),
scheduleDaysEditable: integer('scheduleDaysEditable').default(3).notNull(),
scheduleDaysRequestable: integer('scheduleDaysRequestable').default(14).notNull(),
+ scheduleDaysBeforeEnabled: boolean('scheduleDaysBeforeEnabled').default(false).notNull(),
+ scheduleDaysBefore: integer('scheduleDaysBefore').default(1).notNull(),
+ scheduleDaysAfterEnabled: boolean('scheduleDaysAfterEnabled').default(false).notNull(),
+ scheduleDaysAfter: integer('scheduleDaysAfter').default(1).notNull(),
+ forcePasswordChangeOnFirstLogin: boolean('forcePasswordChangeOnFirstLogin').default(false).notNull(),
+ enableSubmissionRestriction: boolean('enableSubmissionRestriction').default(false).notNull(),
+ submissionRestrictionScope: text('submissionRestrictionScope').default('all').notNull(),
+ sameSongRestrictionHours: integer('sameSongRestrictionHours'),
+ sameArtistRestrictionHours: integer('sameArtistRestrictionHours'),
cardCodeEnabled: boolean('cardCodeEnabled').default(false).notNull(),
cardCodeSubmissionRequired: boolean('cardCodeSubmissionRequired').default(false).notNull(),
cardCodeVoteEnabled: boolean('cardCodeVoteEnabled').default(false).notNull(),
cardCodeReplayEnabled: boolean('cardCodeReplayEnabled').default(false).notNull(),
+ enableCardCodeRequests: boolean('enableCardCodeRequests').default(false).notNull(),
+ requireCardCodeForRequests: boolean('requireCardCodeForRequests').default(false).notNull(),
+ enableCardCodeLimitBypass: boolean('enableCardCodeLimitBypass').default(false).notNull(),
submissionRestrictionEnabled: boolean('submissionRestrictionEnabled').default(false).notNull(),
submissionRestrictionGradeEnabled: boolean('submissionRestrictionGradeEnabled').default(false).notNull(),
submissionRestrictionClassEnabled: boolean('submissionRestrictionClassEnabled').default(false).notNull(),
@@ -282,16 +302,56 @@ export const systemSettings = pgTable('SystemSettings', {
registerRequireEmailVerify: boolean('registerRequireEmailVerify').default(false).notNull(),
registerDefaultRole: text('registerDefaultRole').default('USER'),
registerEnabledGradeClasses: text('registerEnabledGradeClasses'),
+ registerRequiresApproval: boolean('registerRequiresApproval').default(false).notNull(),
+ oauthRegisterRequiresApproval: boolean('oauthRegisterRequiresApproval').default(false).notNull(),
+ registerEmailRequired: boolean('registerEmailRequired').default(false).notNull(),
+ registerRequiresGradeClass: boolean('registerRequiresGradeClass').default(false).notNull(),
oauthEnabled: boolean('oauthEnabled').default(false).notNull(),
oauthAllowRegister: boolean('oauthAllowRegister').default(true).notNull(),
oauthAllowLogin: boolean('oauthAllowLogin').default(true).notNull(),
oauthAllowBind: boolean('oauthAllowBind').default(true).notNull(),
+ allowOAuthRegistration: boolean('allowOAuthRegistration').default(false).notNull(),
+ oauthRedirectUri: text('oauthRedirectUri'),
+ oauthStateSecret: text('oauthStateSecret'),
+ oauthProviders: text('oauthProviders').default('[]'),
+ githubOAuthEnabled: boolean('githubOAuthEnabled').default(false).notNull(),
+ githubClientId: text('githubClientId'),
+ githubClientSecret: text('githubClientSecret'),
+ casdoorOAuthEnabled: boolean('casdoorOAuthEnabled').default(false).notNull(),
+ casdoorServerUrl: text('casdoorServerUrl'),
+ casdoorClientId: text('casdoorClientId'),
+ casdoorClientSecret: text('casdoorClientSecret'),
+ casdoorOrganizationName: text('casdoorOrganizationName'),
+ googleOAuthEnabled: boolean('googleOAuthEnabled').default(false).notNull(),
+ googleClientId: text('googleClientId'),
+ googleClientSecret: text('googleClientSecret'),
+ aggregateOAuthEnabled: boolean('aggregateOAuthEnabled').default(false).notNull(),
+ aggregateOAuthAppId: text('aggregateOAuthAppId'),
+ aggregateOAuthAppKey: text('aggregateOAuthAppKey'),
+ aggregateOAuthLoginType: text('aggregateOAuthLoginType').default('qq'),
+ aggregateOAuthEndpoint: text('aggregateOAuthEndpoint'),
+ customOAuthEnabled: boolean('customOAuthEnabled').default(false).notNull(),
+ customOAuthDisplayName: text('customOAuthDisplayName'),
+ customOAuthAuthorizeUrl: text('customOAuthAuthorizeUrl'),
+ customOAuthTokenUrl: text('customOAuthTokenUrl'),
+ customOAuthUserInfoUrl: text('customOAuthUserInfoUrl'),
+ customOAuthScope: text('customOAuthScope'),
+ customOAuthClientId: text('customOAuthClientId'),
+ customOAuthClientSecret: text('customOAuthClientSecret'),
+ customOAuthUserIdField: text('customOAuthUserIdField'),
+ customOAuthUsernameField: text('customOAuthUsernameField'),
+ customOAuthNameField: text('customOAuthNameField'),
+ customOAuthEmailField: text('customOAuthEmailField'),
+ customOAuthAvatarField: text('customOAuthAvatarField'),
+ captchaMaxFailures: integer('captchaMaxFailures').default(3).notNull(),
autoBackupEnabled: boolean('autoBackupEnabled').default(false).notNull(),
autoBackupIntervalHours: integer('autoBackupIntervalHours').default(24).notNull(),
autoBackupRetentionDays: integer('autoBackupRetentionDays').default(30).notNull(),
+ autoBackupConfig: text('autoBackupConfig'),
statisticsCode: text('statisticsCode'),
- defaultTheme: text('defaultTheme').default('light'),
- enabledThemes: text('enabledThemes').default('light,dark'),
+ statisticsCodeEnabled: boolean('statisticsCodeEnabled').default(false).notNull(),
+ defaultTheme: text('defaultTheme').default('ClassicLight').notNull(),
+ enabledThemes: text('enabledThemes').default('[\"ClassicLight\",\"ModernLight\"]').notNull(),
enabledPlatforms: text('enabledPlatforms'),
platformOrder: text('platformOrder'),
});
diff --git a/app/pages/change-password.vue b/app/pages/change-password.vue
index 119e978..7cad842 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 {
@@ -194,7 +195,7 @@ onMounted(async () => {
font-size: 36px;
font-weight: 700;
margin: 0;
- background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%);
+ background: linear-gradient(135deg, var(--card-bg, #ffffff) 0%, #f0f0f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -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/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 33fe93f..7ec75dd 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -33,6 +33,18 @@
<!-- 鐢ㄦ埛淇℃伅鍖哄煙 -->
<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>
+
<div v-if="isClientAuthenticated" class="user-info">
<div class="user-details-desktop">
<span class="user-name">{{ user?.name || '鐢ㄦ埛' }}</span>
@@ -653,6 +665,13 @@
</div>
</Transition>
</Teleport>
+
+ <div
+ v-if="statisticsCodeEnabled && statisticsCode"
+ id="site-statistics-code"
+ class="hidden"
+ v-html="statisticsCode"
+ />
</div>
</template>
@@ -667,12 +686,19 @@ 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 {
siteTitle,
@@ -680,6 +706,8 @@ const {
guidelines: submissionGuidelines,
icp: icpNumber,
schoolLogoHomeUrl,
+ statisticsCode,
+ statisticsCodeEnabled,
initSiteConfig
} = useSiteConfig()
@@ -1174,6 +1202,15 @@ onMounted(async () => {
}
})
}
+
+ // 娉ㄥ叆绔欑偣缁熻浠g爜锛堝厹搴曟柟妗堬細鍔ㄦ€佹彃鍏?script 鍒?body 鏈熬锛?+ if (statisticsCodeEnabled.value && statisticsCode.value) {
+ try {
+ injectStatisticsCode(statisticsCode.value)
+ } catch (e) {
+ console.warn('缁熻浠g爜娉ㄥ叆澶辫触:', e)
+ }
+ }
} catch (error) {
console.error('棣栭〉鍒濆鍖栧け璐?', error)
await Promise.allSettled([songs.fetchPublicSchedules(), songs.fetchSongCount()])
@@ -1522,6 +1559,38 @@ const showLoginNotice = () => {
}
}
+// 娉ㄥ叆绔欑偣缁熻浠g爜锛堝厹搴曟柟妗堬細鍔ㄦ€佽В鏋愬苟鎻掑叆 script 鍏冪礌鍒?body 鏈熬锛?+const injectStatisticsCode = (code) => {
+ if (typeof document === 'undefined' || !code) return
+
+ // 鍏堝皾璇曠敤涓存椂瀹瑰櫒瑙f瀽 HTML锛屾彁鍙栧叾涓殑 script 鏍囩
+ const tempDiv = document.createElement('div')
+ tempDiv.innerHTML = code
+
+ // 灏嗛潪 script 鐨勫厓绱犲厛闄勫姞鍒板鍣ㄤ腑
+ const container = document.getElementById('site-statistics-code')
+ if (container) {
+ Array.from(tempDiv.childNodes).forEach((node) => {
+ if (node.nodeType === 1 && node.tagName && node.tagName.toLowerCase() !== 'script') {
+ container.appendChild(node.cloneNode(true))
+ }
+ })
+ }
+
+ // 鍗曠嫭澶勭悊 script 鏍囩锛氬姩鎬佸垱寤哄苟鎻掑叆鍒?body 鏈熬锛岀‘淇濊兘姝g‘鎵ц
+ const scripts = tempDiv.querySelectorAll('script')
+ scripts.forEach((oldScript) => {
+ const newScript = document.createElement('script')
+ Array.from(oldScript.attributes).forEach((attr) => {
+ newScript.setAttribute(attr.name, attr.value)
+ })
+ if (oldScript.textContent) {
+ newScript.textContent = oldScript.textContent
+ }
+ document.body.appendChild(newScript)
+ })
+}
+
// 妫€鏌ョ敤鎴锋槸鍚﹂渶瑕佷慨鏀瑰瘑鐮? const checkPasswordChangeRequired = async (user = null) => {
try {
@@ -1564,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; /* 纭繚鑷冲皯鍗犳弧瑙嗗彛 */
@@ -1640,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;
}
@@ -1659,6 +1728,53 @@ 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(--surface-secondary, var(--bg-secondary));
+ color: var(--text-primary);
+ border: 1px solid var(--border-primary);
+ 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;
+}
+
+.utility-btn:hover {
+ background: var(--surface-tertiary, var(--bg-tertiary));
+ border-color: var(--border-secondary);
+}
+
+.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 {
@@ -1679,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;
@@ -1692,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);
}
@@ -1705,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 {
@@ -1727,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;
@@ -1750,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;
@@ -1767,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;
@@ -1777,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 {
@@ -1793,10 +1909,10 @@ if (
min-width: 18px;
height: 18px;
border-radius: 9px;
- background-color: #f44336;
- color: white;
+ background-color: var(--error, #ef4444);
+ color: var(--text-primary, #ffffff);
font-size: 10px;
- font-weight: bold;
+ font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
@@ -1812,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;
@@ -1825,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 {
@@ -1868,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;
}
@@ -1879,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;
}
@@ -1920,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;
}
@@ -2028,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 {
@@ -2040,7 +2155,7 @@ if (
left: 50%;
width: 0;
height: 2px;
- background: #0b5afe;
+ background: var(--primary, #1e40af);
transition: all 0.3s ease;
transform: translateX(-50%);
}
@@ -2058,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; /* 浣垮唴瀹瑰鍣ㄤ笌鏍囩杩炴帴 */
@@ -2166,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;
}
@@ -2186,7 +2301,7 @@ if (
.empty-state {
text-align: center;
padding: 2rem;
- color: rgba(255, 255, 255, 0.6);
+ color: var(--text-tertiary, #6b7280);
}
/* 涓嬫媺鑿滃崟鍔ㄧ敾 */
@@ -2223,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;
@@ -2237,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;
@@ -2251,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);
}
@@ -2275,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;
@@ -2293,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;
}
@@ -2312,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);
@@ -2323,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 {
@@ -2345,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;
@@ -2361,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 {
@@ -2375,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;
}
@@ -2388,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;
@@ -2401,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;
@@ -2426,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 {
@@ -2445,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 {
@@ -2455,7 +2569,7 @@ if (
}
.pagination-text {
- color: rgba(255, 255, 255, 0.7);
+ color: var(--text-secondary, #4a5568);
font-size: 0.85rem;
}
@@ -2473,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;
}
@@ -2489,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;
@@ -2504,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);
}
@@ -2515,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;
}
@@ -2528,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;
@@ -2546,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);
}
@@ -2559,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;
}
@@ -2587,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;
}
@@ -2595,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;
@@ -2628,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;
@@ -2643,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));
}
/* ==================== 绉诲姩绔璁?==================== */
@@ -2671,7 +2785,7 @@ if (
@media (max-width: 768px) {
.home {
padding: 0;
- background-color: #0a0a0f;
+ background-color: var(--bg-primary, #ffffff);
}
.main-content {
@@ -2690,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 {
@@ -2711,7 +2825,7 @@ if (
.logo-divider {
height: 28px;
width: 1px;
- background: rgba(255, 255, 255, 0.15);
+ background: var(--border-secondary, #d1d5db);
}
/* 绉诲姩绔殑涓婚〉闈㈤噷涓嶉渶瑕佸啓瀛︽牎鍚嶏紝淇濇寔绠€娲?*/
@@ -2736,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 {
@@ -2764,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 闂磋窛浼樺寲 */
@@ -2809,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 {
@@ -2827,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;
@@ -2852,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;
}
@@ -2869,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 {
@@ -2918,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;
}
@@ -2980,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));
}
}
@@ -3067,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;
@@ -3094,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 {
@@ -3105,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;
}
@@ -3115,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;
@@ -3128,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);
}
@@ -3156,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;
}
@@ -3186,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);
}
@@ -3227,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;
}
@@ -3239,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;
@@ -3251,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);
}
@@ -3267,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;
@@ -3293,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: var(--text-primary, #ffffff);
+ box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.35);
z-index: 2;
transform: rotate(-5deg);
}
@@ -3349,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;
}
@@ -3370,10 +3482,10 @@ if (
.btn-primary {
width: 100%;
padding: 16px;
- background: #fff;
+ background: var(--primary, #0b5afe);
border: none;
border-radius: 16px;
- color: #000;
+ color: var(--text-primary, #ffffff);
font-size: 16px;
font-weight: 700;
display: flex;
@@ -3386,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 {
@@ -3398,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;
@@ -3408,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 {
@@ -3452,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;
@@ -3488,7 +3600,7 @@ if (
width: 8px;
height: 8px;
border-radius: 50%;
- background-color: #0b5afe;
+ background-color: var(--bg-secondary, #0b5afe);
display: inline-block;
z-index: 2;
}
@@ -3528,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%);
+ background: linear-gradient(180deg, var(--bg-secondary, #0043f8) 0%, #0075f8 100%);
border: none;
- color: white;
+ color: var(--text-primary, #ffffff);
padding: 10px 20px;
border-radius: 6px;
font-weight: 600;
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) {
diff --git a/app/pages/year-review.vue b/app/pages/year-review.vue
index aae3219..398dd2c 100644
--- a/app/pages/year-review.vue
+++ b/app/pages/year-review.vue
@@ -211,8 +211,8 @@ const handleTouchEnd = (e) => {
.review-page {
position: fixed;
inset: 0;
- background-color: #000;
- color: #fff;
+ background-color: var(--bg-primary, #111111);
+ color: var(--text-primary, #ffffff);
z-index: 9999;
font-family:
'MiSans',
@@ -230,7 +230,7 @@ const handleTouchEnd = (e) => {
flex-direction: column;
align-items: center;
justify-content: center;
- background-color: #000;
+ background-color: var(--bg-primary, #111111);
z-index: 50;
}
@@ -285,7 +285,7 @@ const handleTouchEnd = (e) => {
.empty .empty-icon-circle {
width: 5rem;
height: 5rem;
- background: #111827;
+ background: var(--bg-secondary, #111827);
border-radius: 50%;
display: flex;
align-items: center;
@@ -364,7 +364,7 @@ const handleTouchEnd = (e) => {
.indicator-dot.active {
height: 1.5rem;
- background: #fff;
+ background: var(--card-bg, #fff);
}
/* 婊氬姩鎻愮ず */
diff --git a/app/plugins/sentry.client.ts b/app/plugins/sentry.client.ts
new file mode 100644
index 0000000..5217478
--- /dev/null
+++ b/app/plugins/sentry.client.ts
@@ -0,0 +1,29 @@
+import * as Sentry from '@sentry/vue'
+
+export default defineNuxtPlugin((nuxtApp) => {
+ const config = useRuntimeConfig()
+ const dsn = config.public.sentry?.dsn
+
+ if (!dsn) return
+
+ const app = nuxtApp.vueApp
+ const router = nuxtApp.$router
+
+ Sentry.init({
+ app,
+ dsn,
+ environment: config.sentry?.environment || 'production',
+ release: config.sentry?.release || undefined,
+ tracesSampleRate: config.public.sentry?.tracesSampleRate ?? 1.0,
+ integrations: router
+ ? [
+ Sentry.browserTracingIntegration({ router }),
+ Sentry.replayIntegration()
+ ]
+ : [
+ Sentry.replayIntegration()
+ ],
+ replaysSessionSampleRate: config.public.sentry?.replaySessionSampleRate ?? 0,
+ replaysOnErrorSampleRate: config.public.sentry?.replayOnErrorSampleRate ?? 0
+ })
+})
diff --git a/app/utils/password-policy.ts b/app/utils/password-policy.ts
new file mode 100644
index 0000000..f84c3d4
--- /dev/null
+++ b/app/utils/password-policy.ts
@@ -0,0 +1,2 @@
+export const PASSWORD_MIN_LENGTH = 6
+export const PASSWORD_MAX_LENGTH = 64
diff --git a/docker-compose.yml b/docker-compose.yml
index c1ed74d..abb81bc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,57 +1,162 @@
+# 鈿狅笍 Docker Compose v2 椤剁骇 name 蹇呴』鍖归厤姝e垯 ^[a-z0-9][a-z0-9_-]*$
+# 涓嶈兘鏈夊ぇ鍐欍€佺┖鏍笺€佹嫭鍙枫€佷腑鏂囷紝鍚﹀垯浼氭姤 name Does not match pattern 閿欒
+name: byw-voicehub
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
+ # 宸﹁竟鏄涓绘満绔彛锛堝澶栵級锛屽彸杈规槸瀹瑰櫒鍐呯鍙o紙鍥哄畾 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閲岃缃甁WT_SECRET鐢╫penssl 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 涔熻兘璺?+ # 鈿狅笍 鍏抽敭锛氱敤 deploy.resources.{limits|reservations} 杩欎竴濂楋紝
+ # 涓嶈鍚屾椂鍦ㄩ《灞傚啓 cpus/mem_limit/mem_reservation锛?+ # Docker Compose v2 绂佹涓ゅ璧勬簮瀛楁骞跺瓨锛屼細鎶?invalid compose project銆? 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/nuxt.config.ts b/nuxt.config.ts
index a141ae2..d5ff5f3 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -40,11 +40,51 @@ export default defineNuxtConfig({
},
modules: [
'@nuxtjs/tailwindcss',
- ...(process.env.NODE_ENV === 'development' || process.env.npm_lifecycle_event?.includes('lint')
+ ...((process.env.NODE_ENV === 'development' || process.env.npm_lifecycle_event?.includes('lint'))
? ['@nuxt/eslint']
+ : []),
+ ...((process.env.PWA_ENABLED || 'false') === 'true'
+ ? ['@vite-pwa/nuxt']
: [])
],
+ pwa: (process.env.PWA_ENABLED || 'false') === 'true'
+ ? {
+ registerType: 'autoUpdate',
+ manifest: {
+ name: process.env.NUXT_PUBLIC_SITE_TITLE || '鏍″洯骞挎挱绔欑偣姝岀郴缁?,
+ short_name: process.env.NUXT_PUBLIC_SITE_TITLE || 'VoiceHub',
+ description: process.env.NUXT_PUBLIC_SITE_DESCRIPTION || '鏍″洯骞挎挱绔欑偣姝岀郴缁?,
+ display: (process.env.PWA_DISPLAY as any) || 'standalone',
+ background_color: process.env.PWA_BACKGROUND_COLOR || '#000000',
+ theme_color: process.env.PWA_THEME_COLOR || '#111111',
+ icons: [
+ {
+ src: '/images/logo.png',
+ sizes: '192x192',
+ type: 'image/png'
+ },
+ {
+ src: '/images/logo.png',
+ sizes: '512x512',
+ type: 'image/png'
+ }
+ ]
+ },
+ workbox: {
+ navigateFallback: '/',
+ globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico,webp,woff2}']
+ },
+ client: {
+ installPrompt: true,
+ periodicSyncForUpdates: 3600
+ },
+ devOptions: {
+ enabled: process.env.NODE_ENV === 'development'
+ }
+ }
+ : {},
+
// 寮曞叆鍏ㄥ眬CSS
css: [
'~/assets/css/variables.css',
@@ -116,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),
@@ -150,6 +190,10 @@ export default defineNuxtConfig({
},
// 绉诲姩绔紭鍖? { name: 'theme-color', content: '#111111' },
+ {
+ name: 'x-default-theme',
+ content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'ClassicLight'
+ },
{ 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绠$悊' },
@@ -162,22 +206,25 @@ export default defineNuxtConfig({
tagPosition: 'head',
children: [
`(function(){try{`,
- ` var k='voicehub:theme', d=document, r=d.documentElement, s=null;`,
+ ` 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=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`,
+ ` if(!s){s='ClassicLight'}`,
` }`,
- ` r.setAttribute('data-theme', s);`,
- ` if(s==='dark'){r.classList.add('dark'); r.style.colorScheme='dark'}`,
- ` else if(s==='light'){r.classList.add('light'); r.style.colorScheme='light'}`,
- ` else{r.style.colorScheme='light dark'}`,
+ ` 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='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'}`,
+ ` else{r.classList.remove('dark','light'); r.style.colorScheme='light dark'}`,
` try{`,
` var tm=d.querySelector('meta[name="theme-color"]');`,
- ` if(tm){tm.content=(s==='light'?'#f8fafc':'#111111')}`,
+ ` if(tm){tm.content=(eff==='light'?'#f8fafc':'#111111')}`,
` }catch(e){}`,
`}catch(e){}})();`
].join('')
diff --git a/package-lock.json b/package-lock.json
index 087f52b..5866f60 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,8 @@
"@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.3",
"@pixi/sprite": "^7.4.3",
- "@sansenjian/qq-music-api": "2.6.0",
+ "@sentry/node": "^10.73.0",
+ "@sentry/vue": "^10.73.0",
"@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.3",
"@types/crypto-js": "^4.2.2",
@@ -36,6 +37,7 @@
"@types/pg": "^8.20.0",
"@ungap/structured-clone": "^1.3.0",
"@vercel/speed-insights": "^1.3.1",
+ "@vite-pwa/nuxt": "^1.1.1",
"axios": "^1.20.0",
"bcryptjs": "^2.4.3",
"bezier-easing": "^2.1.0",
@@ -78,6 +80,7 @@
"zod": "^4.5.4"
},
"devDependencies": {
+ "@clack/prompts": "^1.1.0",
"@nuxt/eslint": "^1.17.0",
"eslint": "^9.39.5",
"prettier": "^3.9.6",
@@ -421,7 +424,6 @@
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -539,6 +541,48 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz",
+ "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "regexpu-core": "^6.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
+ "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "debug": "^4.4.3",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.22.11"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
"node_modules/@babel/helper-globals": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
@@ -574,15 +618,986 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-module-transforms": {
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
+ "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz",
+ "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-wrap-function": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
+ "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.29.7",
+ "@babel/helper-optimise-call-expression": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
+ "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz",
+ "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz",
+ "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz",
+ "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz",
+ "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz",
+ "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz",
+ "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/plugin-transform-optional-chaining": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz",
+ "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz",
+ "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz",
+ "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
+ "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
+ "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz",
+ "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz",
+ "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-remap-async-to-generator": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz",
+ "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-remap-async-to-generator": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz",
+ "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz",
+ "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz",
+ "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz",
+ "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz",
+ "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz",
+ "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/template": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz",
+ "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz",
+ "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz",
+ "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz",
+ "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz",
+ "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-explicit-resource-management": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz",
+ "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz",
+ "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz",
+ "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz",
+ "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz",
+ "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz",
+ "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz",
+ "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz",
+ "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz",
+ "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz",
+ "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz",
+ "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz",
+ "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.8"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz",
+ "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz",
+ "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz",
+ "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz",
+ "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz",
+ "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz",
+ "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7",
+ "@babel/plugin-transform-parameters": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz",
+ "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz",
+ "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz",
+ "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz",
+ "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz",
+ "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz",
+ "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz",
+ "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz",
+ "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regexp-modifiers": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz",
+ "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -591,118 +1606,154 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/helper-optimise-call-expression": {
+ "node_modules/@babel/plugin-transform-reserved-words": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
- "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz",
+ "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.7"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/helper-plugin-utils": {
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
- "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz",
+ "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/helper-replace-supers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
- "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz",
+ "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-member-expression-to-functions": "^7.29.7",
- "@babel/helper-optimise-call-expression": "^7.29.7",
- "@babel/traverse": "^7.29.7"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^7.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "node_modules/@babel/plugin-transform-sticky-regex": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
- "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz",
+ "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/helper-string-parser": {
+ "node_modules/@babel/plugin-transform-template-literals": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
+ "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/helper-validator-identifier": {
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz",
+ "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/helper-validator-option": {
+ "node_modules/@babel/plugin-transform-typescript": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
+ "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/plugin-syntax-typescript": "^7.29.7"
+ },
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/helpers": {
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz",
+ "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==",
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/parser": {
- "version": "7.29.8",
- "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz",
- "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz",
+ "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.8"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": ">=6.0.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-syntax-jsx": {
+ "node_modules/@babel/plugin-transform-unicode-regex": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
- "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz",
+ "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==",
"license": "MIT",
"dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
"@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
@@ -712,33 +1763,99 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-syntax-typescript": {
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
- "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz",
+ "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==",
"license": "MIT",
- "peer": true,
"dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
"@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^7.0.0-0"
+ "@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/plugin-transform-typescript": {
+ "node_modules/@babel/preset-env": {
"version": "7.29.7",
- "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
- "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
+ "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.29.7.tgz",
+ "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^7.29.7",
- "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-plugin-utils": "^7.29.7",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
- "@babel/plugin-syntax-typescript": "^7.29.7"
+ "@babel/helper-validator-option": "^7.29.7",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7",
+ "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.29.7",
+ "@babel/plugin-syntax-import-attributes": "^7.29.7",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.29.7",
+ "@babel/plugin-transform-async-generator-functions": "^7.29.7",
+ "@babel/plugin-transform-async-to-generator": "^7.29.7",
+ "@babel/plugin-transform-block-scoped-functions": "^7.29.7",
+ "@babel/plugin-transform-block-scoping": "^7.29.7",
+ "@babel/plugin-transform-class-properties": "^7.29.7",
+ "@babel/plugin-transform-class-static-block": "^7.29.7",
+ "@babel/plugin-transform-classes": "^7.29.7",
+ "@babel/plugin-transform-computed-properties": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7",
+ "@babel/plugin-transform-dotall-regex": "^7.29.7",
+ "@babel/plugin-transform-duplicate-keys": "^7.29.7",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7",
+ "@babel/plugin-transform-dynamic-import": "^7.29.7",
+ "@babel/plugin-transform-explicit-resource-management": "^7.29.7",
+ "@babel/plugin-transform-exponentiation-operator": "^7.29.7",
+ "@babel/plugin-transform-export-namespace-from": "^7.29.7",
+ "@babel/plugin-transform-for-of": "^7.29.7",
+ "@babel/plugin-transform-function-name": "^7.29.7",
+ "@babel/plugin-transform-json-strings": "^7.29.7",
+ "@babel/plugin-transform-literals": "^7.29.7",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.29.7",
+ "@babel/plugin-transform-member-expression-literals": "^7.29.7",
+ "@babel/plugin-transform-modules-amd": "^7.29.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.29.7",
+ "@babel/plugin-transform-modules-systemjs": "^7.29.7",
+ "@babel/plugin-transform-modules-umd": "^7.29.7",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7",
+ "@babel/plugin-transform-new-target": "^7.29.7",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7",
+ "@babel/plugin-transform-numeric-separator": "^7.29.7",
+ "@babel/plugin-transform-object-rest-spread": "^7.29.7",
+ "@babel/plugin-transform-object-super": "^7.29.7",
+ "@babel/plugin-transform-optional-catch-binding": "^7.29.7",
+ "@babel/plugin-transform-optional-chaining": "^7.29.7",
+ "@babel/plugin-transform-parameters": "^7.29.7",
+ "@babel/plugin-transform-private-methods": "^7.29.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.29.7",
+ "@babel/plugin-transform-property-literals": "^7.29.7",
+ "@babel/plugin-transform-regenerator": "^7.29.7",
+ "@babel/plugin-transform-regexp-modifiers": "^7.29.7",
+ "@babel/plugin-transform-reserved-words": "^7.29.7",
+ "@babel/plugin-transform-shorthand-properties": "^7.29.7",
+ "@babel/plugin-transform-spread": "^7.29.7",
+ "@babel/plugin-transform-sticky-regex": "^7.29.7",
+ "@babel/plugin-transform-template-literals": "^7.29.7",
+ "@babel/plugin-transform-typeof-symbol": "^7.29.7",
+ "@babel/plugin-transform-unicode-escapes": "^7.29.7",
+ "@babel/plugin-transform-unicode-property-regex": "^7.29.7",
+ "@babel/plugin-transform-unicode-regex": "^7.29.7",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.29.7",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.15",
+ "babel-plugin-polyfill-corejs3": "^0.14.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.6",
+ "core-js-compat": "^3.48.0",
+ "semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
@@ -747,6 +1864,29 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/preset-env/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
@@ -808,31 +1948,24 @@
"license": "LGPL-3.0"
},
"node_modules/@clack/core": {
- "version": "1.4.3",
- "resolved": "https://registry.npmmirror.com/@clack/core/-/core-1.4.3.tgz",
- "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@clack/core/-/core-1.1.0.tgz",
+ "integrity": "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "fast-wrap-ansi": "^0.2.0",
"sisteransi": "^1.0.5"
- },
- "engines": {
- "node": ">= 20.12.0"
}
},
"node_modules/@clack/prompts": {
- "version": "1.7.0",
- "resolved": "https://registry.npmmirror.com/@clack/prompts/-/prompts-1.7.0.tgz",
- "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@clack/prompts/-/prompts-1.1.0.tgz",
+ "integrity": "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@clack/core": "1.4.3",
- "fast-string-width": "^3.0.2",
- "fast-wrap-ansi": "^0.2.0",
+ "@clack/core": "1.1.0",
"sisteransi": "^1.0.5"
- },
- "engines": {
- "node": ">= 20.12.0"
}
},
"node_modules/@cloudflare/kv-asset-handler": {
@@ -974,17 +2107,6 @@
"integrity": "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==",
"license": "MIT"
},
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.3",
- "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
- "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
"node_modules/@es-joy/jsdoccomment": {
"version": "0.91.0",
"resolved": "https://registry.npmmirror.com/@es-joy/jsdoccomment/-/jsdoccomment-0.91.0.tgz",
@@ -1853,7 +2975,7 @@
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
@@ -1872,7 +2994,7 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1885,7 +3007,7 @@
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -1929,7 +3051,7 @@
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
"integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.7",
@@ -1944,14 +3066,14 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -1962,7 +3084,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -1975,7 +3097,7 @@
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0"
@@ -2046,7 +3168,7 @@
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
@@ -2080,7 +3202,7 @@
"version": "3.3.7",
"resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.7.tgz",
"integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.14.0",
@@ -2104,14 +3226,14 @@
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.18",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -2122,7 +3244,7 @@
"version": "14.0.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2135,7 +3257,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -2148,7 +3270,7 @@
"version": "9.39.5",
"resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.5.tgz",
"integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2161,7 +3283,7 @@
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2171,7 +3293,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0",
@@ -2191,7 +3313,7 @@
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
@@ -2201,7 +3323,7 @@
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
@@ -2215,7 +3337,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
@@ -2229,7 +3351,7 @@
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
@@ -2700,12 +3822,39 @@
}
}
},
+ "node_modules/@nuxt/cli/node_modules/@clack/core": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/@clack/core/-/core-1.4.3.tgz",
+ "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-wrap-ansi": "^0.2.0",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 20.12.0"
+ }
+ },
+ "node_modules/@nuxt/cli/node_modules/@clack/prompts": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/@clack/prompts/-/prompts-1.7.0.tgz",
+ "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@clack/core": "1.4.3",
+ "fast-string-width": "^3.0.2",
+ "fast-wrap-ansi": "^0.2.0",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 20.12.0"
+ }
+ },
"node_modules/@nuxt/cli/node_modules/citty": {
"version": "0.2.2",
"resolved": "https://registry.npmmirror.com/citty/-/citty-0.2.2.tgz",
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@nuxt/devalue": {
"version": "2.0.2",
@@ -2798,6 +3947,34 @@
"devtools-wizard": "cli.mjs"
}
},
+ "node_modules/@nuxt/devtools-wizard/node_modules/@clack/core": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/@clack/core/-/core-1.4.3.tgz",
+ "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-wrap-ansi": "^0.2.0",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 20.12.0"
+ }
+ },
+ "node_modules/@nuxt/devtools-wizard/node_modules/@clack/prompts": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/@clack/prompts/-/prompts-1.7.0.tgz",
+ "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@clack/core": "1.4.3",
+ "fast-string-width": "^3.0.2",
+ "fast-wrap-ansi": "^0.2.0",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 20.12.0"
+ }
+ },
"node_modules/@nuxt/devtools/node_modules/isexe": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-4.0.0.tgz",
@@ -2890,10 +4067,40 @@
"eslint": "^9.0.0 || ^10.0.0",
"eslint-plugin-format": "*"
},
- "peerDependenciesMeta": {
- "eslint-plugin-format": {
- "optional": true
- }
+ "peerDependenciesMeta": {
+ "eslint-plugin-format": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@nuxt/eslint-config/node_modules/@clack/core": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/@clack/core/-/core-1.4.3.tgz",
+ "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-wrap-ansi": "^0.2.0",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 20.12.0"
+ }
+ },
+ "node_modules/@nuxt/eslint-config/node_modules/@clack/prompts": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/@clack/prompts/-/prompts-1.7.0.tgz",
+ "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@clack/core": "1.4.3",
+ "fast-string-width": "^3.0.2",
+ "fast-wrap-ansi": "^0.2.0",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 20.12.0"
}
},
"node_modules/@nuxt/eslint-config/node_modules/@eslint/js": {
@@ -3017,7 +4224,6 @@
"resolved": "https://registry.npmmirror.com/@nuxt/kit/-/kit-4.5.2.tgz",
"integrity": "sha512-l66LU9DcJYjmNwqwAj2I5UGRrUbnG2DOKGChnN70zIGtn0eq/z87gi/FRgha6eMb9/FmB1PFHgtx6PWVml1C2Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"c12": "^3.3.4",
"consola": "^3.4.2",
@@ -3179,7 +4385,6 @@
"resolved": "https://registry.npmmirror.com/@nuxt/schema/-/schema-4.5.2.tgz",
"integrity": "sha512-h9g1kt9O2iU9nX6HDFu9gvwtpn+8oSJX0RSTWRBnEx/Pkz+WlOHtjuS0SbkTAXw5aT1+H1GysWVwNTjJfBY/0w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@vue/shared": "^3.5.40",
"defu": "^6.1.7",
@@ -3358,6 +4563,119 @@
"node": ">= 4"
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.1.tgz",
+ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz",
+ "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.11.0.tgz",
+ "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz",
+ "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.220.0",
+ "import-in-the-middle": "^3.0.0",
+ "require-in-the-middle": "^8.0.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.11.0.tgz",
+ "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz",
+ "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/resources": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz",
+ "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/resources": "2.11.0",
+ "@opentelemetry/sdk-trace": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmmirror.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@otplib/core": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
@@ -3413,7 +4731,6 @@
"resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.148.0.tgz",
"integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/oxc-project"
}
@@ -3427,6 +4744,41 @@
"@noble/hashes": "^1.1.5"
}
},
+ "node_modules/@parcel/watcher": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.6.0.tgz",
+ "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3",
+ "is-glob": "^4.0.3",
+ "node-addon-api": "^7.0.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.6.0",
+ "@parcel/watcher-darwin-arm64": "2.6.0",
+ "@parcel/watcher-darwin-x64": "2.6.0",
+ "@parcel/watcher-freebsd-x64": "2.6.0",
+ "@parcel/watcher-linux-arm-glibc": "2.6.0",
+ "@parcel/watcher-linux-arm-musl": "2.6.0",
+ "@parcel/watcher-linux-arm64-glibc": "2.6.0",
+ "@parcel/watcher-linux-arm64-musl": "2.6.0",
+ "@parcel/watcher-linux-x64-glibc": "2.6.0",
+ "@parcel/watcher-linux-x64-musl": "2.6.0",
+ "@parcel/watcher-win32-arm64": "2.6.0",
+ "@parcel/watcher-win32-x64": "2.6.0"
+ }
+ },
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
@@ -3857,7 +5209,6 @@
"resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.4.3.tgz",
"integrity": "sha512-opyWMuO0Ir8pf1DYUR++wAA6ZfNU+nIX2z95R2OD172HbcdhB4/HD7leLIIAny/LciEdMqlWEBhXK7N93YWbdg==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3",
"@pixi/display": "7.4.3"
@@ -3889,7 +5240,6 @@
"resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.3.tgz",
"integrity": "sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@pixi/color": "7.4.3",
"@pixi/constants": "7.4.3",
@@ -3910,7 +5260,6 @@
"resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.4.3.tgz",
"integrity": "sha512-b5m2dAaoNAVdxz1oDaxl3XZ059NEOcNtGkxTOZ4EYCw/jcp9sZXkgSROHRzsGn4k+NugH7+9MP4Id2Z0kkdUhw==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3"
}
@@ -3926,7 +5275,6 @@
"resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.4.3.tgz",
"integrity": "sha512-ZFzS9L/whdRbs5A/EUgF3yQaBcxNarmbuwaMgrfnpQ84mRczkGByqDLGToadiufyals07ufTrXBGRle9lbtEDA==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3"
}
@@ -3936,7 +5284,6 @@
"resolved": "https://registry.npmjs.org/@pixi/filter-bulge-pinch/-/filter-bulge-pinch-5.1.1.tgz",
"integrity": "sha512-80I3g813td7Fnzi7IJSiR3z8gZlKblk6WN+5z6WnscQROcNEpck6lgWS/Lf/IdeHB/FtUKJCbx7RzxkUhiRTvA==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@pixi/core": "^7.0.0-X"
}
@@ -3946,7 +5293,6 @@
"resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-7.4.3.tgz",
"integrity": "sha512-TNu0h20SrzjUWIb5v19dAp1vPpqtG0w2XF9kIHN91bMNaf3R1jzhpWG6TtaVO9eo1IolWcEJLw38jIohyC+KNw==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3"
}
@@ -3979,7 +5325,6 @@
"resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.4.3.tgz",
"integrity": "sha512-iNBrpOFF9nXDT6m2jcyYy6l/sRzklLDDck1eFHprHZwvNquY2nzRfh+RGBCecxhBcijiLJ3fsZN33fP0LDXkvw==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.3",
"@pixi/display": "7.4.3"
@@ -4079,7 +5424,6 @@
"resolved": "https://registry.npmmirror.com/@redis/client/-/client-5.12.1.tgz",
"integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2"
},
@@ -4407,6 +5751,32 @@
}
}
},
+ "node_modules/@rollup/plugin-babel": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz",
+ "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.18.6",
+ "@rollup/pluginutils": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0",
+ "@types/babel__core": "^7.1.9",
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/babel__core": {
+ "optional": true
+ },
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/plugin-commonjs": {
"version": "29.0.3",
"resolved": "https://registry.npmmirror.com/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz",
@@ -4906,58 +6276,210 @@
"win32"
]
},
- "node_modules/@sansenjian/qq-music-api": {
- "version": "2.6.0",
- "resolved": "https://registry.npmmirror.com/@sansenjian/qq-music-api/-/qq-music-api-2.6.0.tgz",
- "integrity": "sha512-eOKIcXLL4TL4Iv0lt7O5bZbp+t6bYzoZtp4fXumjGBj6sT58BnEqoi5tSBHWELMibakUljbpBdkZzAXfFQN9iQ==",
+ "node_modules/@sentry/browser": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/browser/-/browser-10.73.0.tgz",
+ "integrity": "sha512-HqTe1S5RrWLufhX2LaFP3yNoMxfNDroh120bq1zdGHZfFDBMJQ0CDXxHO+L4UJfQ5dWdCCzWbXIAiZuWGa/DFQ==",
"license": "MIT",
- "workspaces": [
- "packages/mcp"
- ],
"dependencies": {
- "@koa/router": "^15.5.0",
- "axios": "^1.16.1",
- "koa": "^2.16.4"
+ "@sentry/browser-utils": "10.73.0",
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0",
+ "@sentry/feedback": "10.73.0",
+ "@sentry/replay": "10.73.0",
+ "@sentry/replay-canvas": "10.73.0"
},
- "bin": {
- "qq-music-api": "dist/cli.js"
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/browser-utils": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/browser-utils/-/browser-utils-10.73.0.tgz",
+ "integrity": "sha512-qQygxJZ+RV779+iL1+lrJ4f4sZLgbgW0/JWPNp0YlcEAE62yCsdKbqoTEjB/EugdS4mSjBMX0chZC6rblu2Ycw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0"
},
"engines": {
- "node": "^20.17.0 || >=22.9.0",
- "npm": "^11.0.0"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/conventions": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/conventions/-/conventions-0.16.0.tgz",
+ "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
}
},
- "node_modules/@sansenjian/qq-music-api/node_modules/@koa/router": {
- "version": "15.7.0",
- "resolved": "https://registry.npmmirror.com/@koa/router/-/router-15.7.0.tgz",
- "integrity": "sha512-WaAlk4TOl/O0rhTpOR0l052gz03syPMmI6Pe2gd7v3ubjfv5UcSGcnb0Y/J5NNC/ln+5FiUqPJTc/a15I+XqAA==",
+ "node_modules/@sentry/core": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/core/-/core-10.73.0.tgz",
+ "integrity": "sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA==",
"license": "MIT",
"dependencies": {
- "debug": "^4.4.3",
- "http-errors": "^2.0.1",
- "koa-compose": "^4.1.0",
- "path-to-regexp": "^8.4.2"
+ "@sentry/conventions": "^0.16.0"
},
"engines": {
- "node": ">= 20"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/feedback": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/feedback/-/feedback-10.73.0.tgz",
+ "integrity": "sha512-D6nSngX+e46Mae2/oh2bxBvxNK1z2NERbuMAhB5sx9x4xMBWIyGnYYTECehvEqV9+AqGAgxxhOZoYIG3AmRwww==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/node": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/node/-/node-10.73.0.tgz",
+ "integrity": "sha512-jiMJ6GgXDw6UMGzJY+o0c8OoeA9OfHqZ/xEpHfDqy75hn+9CEkRkbNGCIdMvsc7wW/Se1Os394hHfTpU+YEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.1",
+ "@opentelemetry/instrumentation": "^0.220.0",
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0",
+ "@sentry/node-core": "10.73.0",
+ "@sentry/opentelemetry": "10.73.0",
+ "@sentry/server-utils": "10.73.0",
+ "import-in-the-middle": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/node-core": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/node-core/-/node-core-10.73.0.tgz",
+ "integrity": "sha512-GHAGUmZPmm6FKfxfv2maVVJ/A99YAmd7oFOuKMDmITI6O/Msl6JB3vPTEpopVAHLW0LYJQBOjddL9C7o+Jt44g==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0",
+ "@sentry/opentelemetry": "10.73.0",
+ "import-in-the-middle": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
},
"peerDependencies": {
- "koa": "^2.0.0 || ^3.0.0"
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/core": "^1.30.1 || ^2.1.0",
+ "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1",
+ "@opentelemetry/instrumentation": ">=0.57.1 <1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0"
},
"peerDependenciesMeta": {
- "koa": {
- "optional": false
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@opentelemetry/core": {
+ "optional": true
+ },
+ "@opentelemetry/exporter-trace-otlp-http": {
+ "optional": true
+ },
+ "@opentelemetry/instrumentation": {
+ "optional": true
+ },
+ "@opentelemetry/sdk-trace-base": {
+ "optional": true
}
}
},
- "node_modules/@sansenjian/qq-music-api/node_modules/path-to-regexp": {
- "version": "8.4.2",
- "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
- "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "node_modules/@sentry/opentelemetry": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/opentelemetry/-/opentelemetry-10.73.0.tgz",
+ "integrity": "sha512-fQouPQKsH0CQrw6oAn1k0Z2I+tgyochCovifr5qNS69i0OzjknLa03WJyiZ/IuzXc4AVa5jAKfOeE9slABz8Qw==",
"license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/core": "^1.30.1 || ^2.1.0",
+ "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0"
+ }
+ },
+ "node_modules/@sentry/replay": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/replay/-/replay-10.73.0.tgz",
+ "integrity": "sha512-nN2wjN/Y0J5BOJV5hqRHUEBfxwUsipp1PKjcDHh6Fpxnrtfldu3Y99E8cQInseo5heFdzEvrOHBBrqWZXOHVKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser-utils": "10.73.0",
+ "@sentry/core": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/replay-canvas": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/replay-canvas/-/replay-canvas-10.73.0.tgz",
+ "integrity": "sha512-sxa2lKkHPfF/j5xFpW7gocthWXRqyoHz8KCPy6yGc8plT477nl57iGSKhQDYsx1Ny10TLjs7YkIuPP1GY/Ax2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.73.0",
+ "@sentry/replay": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/server-utils": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/server-utils/-/server-utils-10.73.0.tgz",
+ "integrity": "sha512-QskripdKFbM/+gipC6mpa2crLwL7+VbkX84IpHg2z9UlYQ1kNKd3aMT+Qk9NLRSw/zu1rSIAvlbWfx4D3rgNAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/vue": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmmirror.com/@sentry/vue/-/vue-10.73.0.tgz",
+ "integrity": "sha512-pbUt65YO7mcLT8q0geK62K4HxlKQkya3ZNqXo+BRz99uMvC78BdNSK+ME4jvTYeHwRJdLqQfAwrcXvKgAQlweg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser": "10.73.0",
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@tanstack/vue-router": "^1.64.0",
+ "pinia": "2.x || 3.x || 4.x",
+ "vue": "2.x || 3.x"
+ },
+ "peerDependenciesMeta": {
+ "@tanstack/vue-router": {
+ "optional": true
+ },
+ "pinia": {
+ "optional": true
+ }
}
},
"node_modules/@simple-git/args-pathspec": {
@@ -5056,7 +6578,6 @@
"integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/types": "^8.56.0",
@@ -5281,7 +6802,6 @@
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -5301,6 +6821,21 @@
"integrity": "sha512-zeSORFArxqUwfVMTRHu8AN9k9LlfSn0CKDSzLhJDITpgLoS0xpnocxsgMjQjUcVYDgO47r9zLP49HEjH/iGsFg==",
"license": "Apache-2.0"
},
+ "node_modules/@trickfilm400/rollup-plugin-off-main-thread": {
+ "version": "3.0.0-pre1",
+ "resolved": "https://registry.npmmirror.com/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz",
+ "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ejs": "^3.1.10",
+ "json5": "^2.2.3",
+ "magic-string": "^0.30.21",
+ "string.prototype.matchall": "^4.0.12"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.3",
"resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
@@ -5388,9 +6923,8 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "devOptional": true,
- "license": "MIT",
- "peer": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
@@ -5461,7 +6995,6 @@
"resolved": "https://registry.npmmirror.com/@types/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@@ -5516,8 +7049,7 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "license": "MIT",
- "optional": true
+ "license": "MIT"
},
"node_modules/@types/xml2js": {
"version": "0.4.14",
@@ -5573,7 +7105,6 @@
"integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.69.0",
"@typescript-eslint/types": "8.69.0",
@@ -6282,6 +7813,82 @@
}
}
},
+ "node_modules/@vite-pwa/nuxt": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@vite-pwa/nuxt/-/nuxt-1.1.1.tgz",
+ "integrity": "sha512-8RkdxNJ3NubtwmS96bPVvDclFmysH3VADKWqlaBaUu6PLsoHM3Wq7ThdYBsAT6EpqzWS+iFC2SF6lkJ4xDhKLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@nuxt/kit": "^3.9.0",
+ "pathe": "^1.1.1",
+ "ufo": "^1.3.2",
+ "vite-plugin-pwa": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vite-pwa/assets-generator": "^1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@vite-pwa/assets-generator": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vite-pwa/nuxt/node_modules/@nuxt/kit": {
+ "version": "3.21.11",
+ "resolved": "https://registry.npmmirror.com/@nuxt/kit/-/kit-3.21.11.tgz",
+ "integrity": "sha512-0Xi3tgwN77w43Q8GCPIrvWmF1J7Peehkts44E0uKNIml9lB8WoUn8YxyUjxBv47XtVR86NWoWALAT+/IEMHJEA==",
+ "license": "MIT",
+ "dependencies": {
+ "c12": "^3.3.4",
+ "consola": "^3.4.2",
+ "defu": "^6.1.7",
+ "destr": "^2.0.5",
+ "errx": "^0.1.2",
+ "exsolve": "^1.1.1",
+ "ignore": "^7.0.6",
+ "jiti": "^2.7.0",
+ "klona": "^2.0.6",
+ "knitwork": "^1.3.0",
+ "mlly": "^1.8.2",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "pkg-types": "^2.3.1",
+ "rc9": "^3.0.1",
+ "scule": "^1.3.0",
+ "semver": "^7.8.5",
+ "tinyglobby": "^0.2.17",
+ "ufo": "^1.6.4",
+ "unctx": "^2.5.0",
+ "untyped": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/@vite-pwa/nuxt/node_modules/@nuxt/kit/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
+ "node_modules/@vite-pwa/nuxt/node_modules/ignore": {
+ "version": "7.0.8",
+ "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.8.tgz",
+ "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@vite-pwa/nuxt/node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "license": "MIT"
+ },
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.8",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
@@ -6423,7 +8030,6 @@
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
"integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/parser": "^7.29.8",
"@vue/compiler-core": "3.5.42",
@@ -6585,7 +8191,6 @@
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6606,7 +8211,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -6634,7 +8239,7 @@
"version": "6.15.0",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz",
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -6907,9 +8512,46 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "devOptional": true,
+ "dev": true,
"license": "Python-2.0"
},
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@@ -6969,6 +8611,15 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/async-sema": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/async-sema/-/async-sema-3.1.1.tgz",
@@ -7026,6 +8677,21 @@
"postcss": "^8.1.0"
}
},
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/axios": {
"version": "1.20.0",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.20.0.tgz",
@@ -7077,6 +8743,54 @@
}
}
},
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.17",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
+ "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
+ "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "core-js-compat": "^3.48.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
+ "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -7091,7 +8805,6 @@
"resolved": "https://registry.npmmirror.com/bare-events/-/bare-events-2.9.2.tgz",
"integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==",
"license": "Apache-2.0",
- "peer": true,
"peerDependencies": {
"bare-abort-controller": "*"
},
@@ -7300,7 +9013,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.11.20",
"caniuse-lite": "^1.0.30001810",
@@ -7477,6 +9189,24 @@
"node": ">= 6.0.0"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -7510,7 +9240,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -7686,6 +9416,12 @@
"consola": "^3.2.3"
}
},
+ "node_modules/cjs-module-lexer": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz",
+ "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==",
+ "license": "MIT"
+ },
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -7774,6 +9510,15 @@
"node": ">= 12.0.0"
}
},
+ "node_modules/common-tags": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz",
+ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz",
@@ -7963,7 +9708,6 @@
"version": "3.50.0",
"resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.50.0.tgz",
"integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.7"
@@ -8080,6 +9824,15 @@
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
+ "node_modules/crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
@@ -8263,6 +10016,57 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.23",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz",
@@ -8345,7 +10149,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
@@ -8385,6 +10189,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
@@ -8397,6 +10218,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.7.tgz",
@@ -8519,7 +10357,6 @@
"integrity": "sha512-aypfsr6t0uNvkqaZc6zvBfXzC6pLI0/sIulpkV6RwCVtZqG5ebBzv4weImKK0VNCj91Wl9F5j7p5WU4MNrybng==",
"dev": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"srvx": ">=0.11.5"
},
@@ -8564,7 +10401,6 @@
"integrity": "sha512-aOuk+yNJA61yA6eJJvQJxpJLIxz090hKMqDGzd35tGsvwaodi1RPuYSFUp5EINaJp7snjVwLkO2RXVMPZBYnnA==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"srvx": "bin/srvx.mjs"
},
@@ -9193,7 +11029,6 @@
"resolved": "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
"integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
"license": "Apache-2.0",
- "peer": true,
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
"@cloudflare/workers-types": ">=4",
@@ -9361,6 +11196,21 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.422",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz",
@@ -9409,6 +11259,92 @@
"integrity": "sha512-chfpPHmCerdo/rXr/nNvPZRkV4WwDRwzwnsJ0Uzz3tVi8Z41tDctRjduYy1138ii77AFlts1qvWtX3g/Acg91Q==",
"license": "MIT"
},
+ "node_modules/es-abstract": {
+ "version": "1.24.2",
+ "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-abstract-get": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/es-abstract-get/-/es-abstract-get-1.0.0.tgz",
+ "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.2",
+ "is-callable": "^1.2.7",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -9434,9 +11370,9 @@
"license": "MIT"
},
"node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -9460,13 +11396,32 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-to-primitive": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.4.tgz",
+ "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-abstract-get": "^1.0.0",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.1.0",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -9521,7 +11476,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -9535,9 +11490,8 @@
"resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.5.tgz",
"integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9892,7 +11846,7 @@
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@@ -9926,7 +11880,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -9939,14 +11893,14 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -9957,7 +11911,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -9974,7 +11928,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
@@ -9991,7 +11945,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
@@ -10007,7 +11961,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -10020,7 +11974,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
@@ -10036,7 +11990,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
@@ -10052,7 +12006,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -10065,7 +12019,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -10078,7 +12032,7 @@
"version": "10.4.0",
"resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.15.0",
@@ -10096,7 +12050,7 @@
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
@@ -10109,7 +12063,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@@ -10122,7 +12076,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -10138,12 +12092,23 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/eta": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmmirror.com/eta/-/eta-4.6.0.tgz",
+ "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/bgub/eta?sponsor=1"
+ }
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
@@ -10219,7 +12184,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/fast-fifo": {
@@ -10260,14 +12224,13 @@
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/fast-npm-meta": {
@@ -10311,6 +12274,22 @@
"fast-string-truncated-width": "^3.0.2"
}
},
+ "node_modules/fast-uri": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.7.tgz",
+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fast-wrap-ansi": {
"version": "0.2.2",
"resolved": "https://registry.npmmirror.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
@@ -10356,7 +12335,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^4.0.0"
@@ -10371,6 +12350,42 @@
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
+ "node_modules/filelist": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz",
+ "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/filelist/node_modules/brace-expansion": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz",
+ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -10417,7 +12432,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
@@ -10431,7 +12446,7 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz",
"integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC"
},
"node_modules/fnv1a-64": {
@@ -10460,6 +12475,21 @@
}
}
},
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -10597,6 +12627,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/function.prototype.name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
+ "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2",
+ "hasown": "^2.0.4",
+ "is-callable": "^1.2.7",
+ "is-document.all": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/fuse.js": {
"version": "7.5.0",
"resolved": "https://registry.npmmirror.com/fuse.js/-/fuse.js-7.5.0.tgz",
@@ -10688,6 +12750,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-own-enumerable-property-symbols": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
+ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+ "license": "ISC"
+ },
"node_modules/get-port-please": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
@@ -10719,6 +12787,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-tsconfig": {
"version": "4.13.6",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
@@ -10803,6 +12888,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/globby": {
"version": "16.2.4",
"resolved": "https://registry.npmmirror.com/globby/-/globby-16.2.4.tgz",
@@ -10895,6 +12996,18 @@
"uncrypto": "^0.1.3"
}
},
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -10904,6 +13017,33 @@
"node": ">=8"
}
},
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -11116,6 +13256,12 @@
"node": ">=16.17.0"
}
},
+ "node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
+ "license": "ISC"
+ },
"node_modules/identifier-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/identifier-regex/-/identifier-regex-1.1.0.tgz",
@@ -11183,7 +13329,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -11196,6 +13342,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/import-in-the-middle": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmmirror.com/import-in-the-middle/-/import-in-the-middle-3.4.0.tgz",
+ "integrity": "sha512-Xfjwfarhe+LGmoaof+sexeNo3sGRysb5x56WrZLwHtmqT0OilSKtVWkv0lrCcMgSKyKP9oBogBAisHZvtJH0aw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "cjs-module-lexer": "^2.2.0",
+ "es-module-lexer": "^2.2.0",
+ "module-details-from-path": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/impound": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/impound/-/impound-1.2.0.tgz",
@@ -11213,7 +13373,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
@@ -11258,6 +13418,20 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
@@ -11295,6 +13469,57 @@
"url": "https://github.com/sponsors/brc-dd"
}
},
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -11307,6 +13532,22 @@
"node": ">=8"
}
},
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-builtin-module": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
@@ -11323,6 +13564,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -11338,6 +13591,39 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz",
@@ -11353,13 +13639,43 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-document.all": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/is-document.all/-/is-document.all-1.0.0.tgz",
+ "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": {
@@ -11465,12 +13781,36 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
},
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -11480,6 +13820,31 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz",
+ "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-path-inside": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-4.0.0.tgz",
@@ -11519,6 +13884,42 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz",
+ "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz",
@@ -11531,6 +13932,97 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-wsl": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz",
@@ -11579,6 +14071,23 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz",
@@ -11598,7 +14107,7 @@
"version": "4.3.2",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
- "devOptional": true,
+ "dev": true,
"funding": [
{
"type": "github",
@@ -11643,7 +14152,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/json-schema-to-typescript-lite": {
@@ -11661,14 +14170,14 @@
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/json5": {
@@ -11695,6 +14204,15 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/jsonpointer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/jsonpointer/-/jsonpointer-5.0.1.tgz",
+ "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
@@ -11789,7 +14307,7 @@
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
@@ -11848,7 +14366,6 @@
"resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz",
"integrity": "sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^1.3.5",
"cache-content-type": "^1.0.0",
@@ -12041,11 +14558,20 @@
"node": ">= 0.6.3"
}
},
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
@@ -12069,7 +14595,6 @@
"resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"license": "MPL-2.0",
- "peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -12442,6 +14967,12 @@
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -12482,7 +15013,7 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
@@ -12770,6 +15301,12 @@
"integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==",
"license": "MIT"
},
+ "node_modules/module-details-from-path": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
+ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
+ "license": "MIT"
+ },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
@@ -12886,7 +15423,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/negotiator": {
@@ -13657,7 +16194,6 @@
"resolved": "https://registry.npmmirror.com/nuxt/-/nuxt-4.5.2.tgz",
"integrity": "sha512-tR3fcqeHlHmmkLMpIg3V7Y+1ltr302lW8djMw/iy+myfo7QSSz+BVJDuQhg5j73b9oteSyBfOKTDYTgvMtj6TA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@dxup/nuxt": "^0.5.6",
"@nuxt/cli": "^3.37.0",
@@ -13796,7 +16332,6 @@
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-1.2.3.tgz",
"integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
@@ -13963,6 +16498,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/obug": {
"version": "2.1.4",
"resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.4.tgz",
@@ -14082,9 +16646,8 @@
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -14108,6 +16671,24 @@
"@otplib/preset-v11": "^12.0.1"
}
},
+ "node_modules/own-keys": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.2.tgz",
+ "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/oxc-walker": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/oxc-walker/-/oxc-walker-1.1.1.tgz",
@@ -14223,7 +16804,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -14457,6 +17038,15 @@
"node": ">= 10.12"
}
},
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.28",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.28.tgz",
@@ -14476,7 +17066,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.18",
"picocolors": "^1.1.1",
@@ -15022,7 +17611,6 @@
"resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz",
"integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -15073,7 +17661,6 @@
"resolved": "https://registry.npmmirror.com/postgres/-/postgres-3.4.9.tgz",
"integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==",
"license": "Unlicense",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -15137,7 +17724,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
@@ -15229,7 +17816,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -15515,6 +18102,46 @@
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
+ "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
@@ -15533,14 +18160,56 @@
"refa": "^0.12.1"
},
"engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-6.4.0.tgz",
+ "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.2.2",
+ "regjsgen": "^0.8.0",
+ "regjsparser": "^0.13.0",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=4"
}
},
+ "node_modules/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
+ "license": "MIT"
+ },
"node_modules/regjsparser": {
"version": "0.13.2",
"resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.13.2.tgz",
"integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==",
- "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"jsesc": "~3.1.0"
@@ -15719,6 +18388,28 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-in-the-middle": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz",
+ "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "module-details-from-path": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=9.3.0 || >=8.10.0 <9.0.0"
+ }
+ },
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -15762,7 +18453,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -15869,7 +18560,6 @@
"resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.2.7.tgz",
"integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@oxc-project/types": "=0.148.0",
"@rolldown/pluginutils": "^1.0.0"
@@ -15935,7 +18625,6 @@
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.1.tgz",
"integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.9"
},
@@ -16217,6 +18906,31 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/safe-array-concat": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
+ "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-array-concat/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -16237,6 +18951,28 @@
],
"license": "MIT"
},
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-push-apply/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
@@ -16447,6 +19183,52 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -16493,14 +19275,14 @@
}
},
"node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@@ -16512,13 +19294,13 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -16767,6 +19549,19 @@
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
"license": "MIT"
},
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -16830,6 +19625,104 @@
"node": ">=8"
}
},
+ "node_modules/string.prototype.matchall": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz",
+ "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.2",
+ "get-intrinsic": "^1.3.0",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.4",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz",
+ "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-object-atoms": "^1.1.2",
+ "has-property-descriptors": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz",
+ "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/stringify-object": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmmirror.com/stringify-object/-/stringify-object-3.3.0.tgz",
+ "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "get-own-enumerable-property-symbols": "^3.0.0",
+ "is-obj": "^1.0.1",
+ "is-regexp": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -16855,6 +19748,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-comments/-/strip-comments-2.0.1.tgz",
+ "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -16884,7 +19786,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -17142,7 +20044,6 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -17243,6 +20144,57 @@
"streamx": "^2.12.5"
}
},
+ "node_modules/temp-dir": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz",
+ "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tempy": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/tempy/-/tempy-0.6.0.tgz",
+ "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-stream": "^2.0.0",
+ "temp-dir": "^2.0.0",
+ "type-fest": "^0.16.0",
+ "unique-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tempy/node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tempy/node_modules/type-fest": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.16.0.tgz",
+ "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/terser": {
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
@@ -17469,7 +20421,6 @@
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@@ -17506,7 +20457,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
@@ -17543,6 +20494,80 @@
"node": ">= 0.6"
}
},
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.8.tgz",
+ "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "for-each": "^0.3.5",
+ "gopd": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "possible-typed-array-names": "^1.1.0",
+ "reflect.getprototypeof": "^1.0.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@@ -17553,9 +20578,8 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17576,6 +20600,24 @@
"integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==",
"license": "MIT"
},
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
@@ -17647,7 +20689,6 @@
"resolved": "https://registry.npmmirror.com/unhead/-/unhead-3.4.0.tgz",
"integrity": "sha512-OlgpyYjY+NkFyQCaMihg2uTFbbvyr5lZr6TokS68VdFTXJ5jo4JwFtGR8PwKy/wRpjbQYgKF7ZYO+pbRgxtSmw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"hookable": "^6.1.1",
"unplugin": "^3.3.0"
@@ -17664,6 +20705,46 @@
}
}
},
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
+ "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
@@ -17743,6 +20824,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "license": "MIT",
+ "dependencies": {
+ "crypto-random-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -17851,7 +20944,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"napi-postinstall": "^0.3.4"
},
@@ -18055,6 +21147,16 @@
"pkg-types": "^2.3.0"
}
},
+ "node_modules/upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4",
+ "yarn": "*"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.3.2",
"resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
@@ -18095,7 +21197,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
@@ -18181,7 +21283,6 @@
"resolved": "https://registry.npmmirror.com/vite/-/vite-8.2.2.tgz",
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.5",
@@ -18478,6 +21579,48 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/vite-plugin-pwa": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz",
+ "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.6",
+ "pretty-bytes": "^6.1.1",
+ "tinyglobby": "^0.2.10",
+ "workbox-build": "^7.4.1",
+ "workbox-window": "^7.4.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vite-pwa/assets-generator": "^1.0.0",
+ "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "workbox-build": "^7.4.1",
+ "workbox-window": "^7.4.1"
+ },
+ "peerDependenciesMeta": {
+ "@vite-pwa/assets-generator": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-pwa/node_modules/pretty-bytes": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
+ "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/vite-plugin-top-level-await": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz",
@@ -18545,7 +21688,6 @@
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.42.tgz",
"integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.42",
"@vue/compiler-sfc": "3.5.42",
@@ -18589,7 +21731,6 @@
"integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"debug": "^4.4.0",
"eslint-scope": "^8.2.0 || ^9.0.0",
@@ -18613,7 +21754,6 @@
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
@@ -18680,12 +21820,103 @@
"node": ">= 8"
}
},
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
+ "node_modules/which-typed-array": {
+ "version": "1.1.22",
+ "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.22.tgz",
+ "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
@@ -18708,12 +21939,309 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/workbox-background-sync": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz",
+ "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==",
+ "license": "MIT",
+ "dependencies": {
+ "idb": "^7.0.1",
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-broadcast-update": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz",
+ "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-build": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-build/-/workbox-build-7.4.1.tgz",
+ "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@apideck/better-ajv-errors": "^0.3.1",
+ "@babel/core": "^7.24.4",
+ "@babel/preset-env": "^7.11.0",
+ "@babel/runtime": "^7.11.2",
+ "@rollup/plugin-babel": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^16.0.3",
+ "@rollup/plugin-replace": "^6.0.3",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1",
+ "ajv": "^8.6.0",
+ "common-tags": "^1.8.0",
+ "eta": "^4.5.1",
+ "fast-json-stable-stringify": "^2.1.0",
+ "fs-extra": "^9.0.1",
+ "glob": "^11.0.1",
+ "pretty-bytes": "^5.3.0",
+ "rollup": "^4.53.3",
+ "source-map": "^0.8.0-beta.0",
+ "stringify-object": "^3.3.0",
+ "strip-comments": "^2.0.1",
+ "tempy": "^0.6.0",
+ "upath": "^1.2.0",
+ "workbox-background-sync": "7.4.1",
+ "workbox-broadcast-update": "7.4.1",
+ "workbox-cacheable-response": "7.4.1",
+ "workbox-core": "7.4.1",
+ "workbox-expiration": "7.4.1",
+ "workbox-google-analytics": "7.4.1",
+ "workbox-navigation-preload": "7.4.1",
+ "workbox-precaching": "7.4.1",
+ "workbox-range-requests": "7.4.1",
+ "workbox-recipes": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1",
+ "workbox-streams": "7.4.1",
+ "workbox-sw": "7.4.1",
+ "workbox-window": "7.4.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmmirror.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz",
+ "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==",
+ "license": "MIT",
+ "dependencies": {
+ "jsonpointer": "^5.0.1",
+ "leven": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "ajv": ">=8"
+ }
+ },
+ "node_modules/workbox-build/node_modules/@isaacs/cliui": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-9.0.0.tgz",
+ "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/workbox-build/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/workbox-build/node_modules/glob": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-11.1.0.tgz",
+ "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "foreground-child": "^3.3.1",
+ "jackspeak": "^4.1.1",
+ "minimatch": "^10.1.1",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/workbox-build/node_modules/jackspeak": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.2.3.tgz",
+ "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^9.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/workbox-build/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/workbox-build/node_modules/pretty-bytes": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/workbox-build/node_modules/source-map": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.8.0.tgz",
+ "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/workbox-cacheable-response": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz",
+ "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-core": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-core/-/workbox-core-7.4.1.tgz",
+ "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==",
+ "license": "MIT"
+ },
+ "node_modules/workbox-expiration": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-expiration/-/workbox-expiration-7.4.1.tgz",
+ "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==",
+ "license": "MIT",
+ "dependencies": {
+ "idb": "^7.0.1",
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-google-analytics": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz",
+ "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-background-sync": "7.4.1",
+ "workbox-core": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1"
+ }
+ },
+ "node_modules/workbox-navigation-preload": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz",
+ "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-precaching": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-precaching/-/workbox-precaching-7.4.1.tgz",
+ "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1"
+ }
+ },
+ "node_modules/workbox-range-requests": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz",
+ "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-recipes": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-recipes/-/workbox-recipes-7.4.1.tgz",
+ "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-cacheable-response": "7.4.1",
+ "workbox-core": "7.4.1",
+ "workbox-expiration": "7.4.1",
+ "workbox-precaching": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1"
+ }
+ },
+ "node_modules/workbox-routing": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-routing/-/workbox-routing-7.4.1.tgz",
+ "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-strategies": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-strategies/-/workbox-strategies-7.4.1.tgz",
+ "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1"
+ }
+ },
+ "node_modules/workbox-streams": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-streams/-/workbox-streams-7.4.1.tgz",
+ "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "7.4.1",
+ "workbox-routing": "7.4.1"
+ }
+ },
+ "node_modules/workbox-sw": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-sw/-/workbox-sw-7.4.1.tgz",
+ "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==",
+ "license": "MIT"
+ },
+ "node_modules/workbox-window": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmmirror.com/workbox-window/-/workbox-window-7.4.1.tgz",
+ "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2",
+ "workbox-core": "7.4.1"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -18901,9 +22429,8 @@
"version": "2.9.0",
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
- "peer": true,
"bin": {
"yaml": "bin.mjs"
},
diff --git a/package.json b/package.json
index d371580..cf0977c 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,8 @@
"@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.3",
"@pixi/sprite": "^7.4.3",
- "@sansenjian/qq-music-api": "2.6.0",
+ "@sentry/node": "^10.73.0",
+ "@sentry/vue": "^10.73.0",
"@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.3",
"@types/crypto-js": "^4.2.2",
@@ -61,6 +62,7 @@
"@types/pg": "^8.20.0",
"@ungap/structured-clone": "^1.3.0",
"@vercel/speed-insights": "^1.3.1",
+ "@vite-pwa/nuxt": "^1.1.1",
"axios": "^1.20.0",
"bcryptjs": "^2.4.3",
"bezier-easing": "^2.1.0",
@@ -106,6 +108,7 @@
"node": ">=22.11.0"
},
"devDependencies": {
+ "@clack/prompts": "^1.1.0",
"@nuxt/eslint": "^1.17.0",
"eslint": "^9.39.5",
"prettier": "^3.9.6",
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('姝e湪浣跨敤 pnpm install 瀹夎...', 'cyan')
- if (!safeExec('pnpm install')) {
+ log('姝e湪浣跨敤 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('搴旂敤鏋勫缓瀹屾垚')
diff --git a/scripts/setup-deploy-user.sh b/scripts/setup-deploy-user.sh
new file mode 100644
index 0000000..dc9a48b
--- /dev/null
+++ b/scripts/setup-deploy-user.sh
@@ -0,0 +1,168 @@
+#!/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 鐩綍璇诲啓鏉冮檺锛坰etfacl锛?+# 5) 鎵撳嵃绉侀挜鍐呭 & 鍏挜鎸囩汗锛屼究浜庝綘妫€鏌ュ拰澶嶅埗
+#
+# 鐢ㄦ硶锛堝湪鐢熶骇鏈嶅姟鍣ㄤ笂锛宻udo 杩愯锛夛細
+# 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
+ # 娉ㄦ剰锛歎buntu 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. 鍔犲叆 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 瀵嗙爜鐧诲綍锛沝eploy 绂佹杞彂锛?-------
+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 鍙厑璁稿叕閽ョ櫥褰曪紝骞剁鐢ㄧ鍙h浆鍙戯級"
+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/鐜鏂囦欢锛岄『渚挎敼涓€涓嬪睘缁勶紝閬垮厤銆宺oot 寤虹殑鏂囦欢 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 涓汉璁块棶浠ょ墝锛圥ackage 璇诲啓鏉冮檺锛?
+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..5ee9b05
--- /dev/null
+++ b/scripts/setup-runner.sh
@@ -0,0 +1,380 @@
+#!/usr/bin/env bash
+# ============================================================================
+# setup-runner.sh 鈥斺€?涓€閿畨瑁呭苟娉ㄥ唽 Gitea Actions Runner锛坅ct_runner锛?+#
+# 鉁?鍏煎 Gitea 1.21 ~ 鏈€鏂帮紙鍚?1.27.x锛夛紝鏂扮増娌℃湁 Platform 涓嬫媺鑿滃崟銆?+#
+# 浣跨敤鏂规硶锛堜袱绉嶆ā寮忎换閫夊叾涓€锛屾帹鑽?妯″紡 A 鏈€鐪佷簨锛夛細
+#
+# 鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣
+# 妯″紡 A锛氫粨搴撶骇 Runner锛堝彧缁欒繖涓?VoiceHub 浠撳簱鐢紝鏈€甯哥敤銆佹潈闄愭渶灏忥級
+# 璺緞锛氫綘鐨勪粨搴?鈫?Settings 鈫?Actions 鈫?Runners 鈫?鍙充笂瑙?+# 鈫?鏂扮増锛?.21+锛夎繖閲屾病鏈?Platform 閫夋嫨锛岀洿鎺ユ湁涓€涓緭鍏ユ
+# 鍐欑潃銆孯egistration 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
+# 锛圱oken 寮€澶翠竴鑸槸 RGOxxxx锛?+#
+# 鎵ц锛?+# sudo bash scripts/setup-runner.sh \
+# --scope org --gitea https://浣犵殑-gitea --token RGOxxxx...
+#
+# 鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣鈹佲攣
+# 妯″紡 C锛氬叏灞€/绔欑偣绾?Runner锛圙itea 鍏ㄧ珯鎵€鏈変粨搴撻兘鑳界敤锛岄渶绔欑偣绠$悊鍛橈級
+# 璺緞锛氱珯鐐圭鐞嗗悗鍙帮紙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锛?+# --version 3.3.2 銆愬彲閫夈€戝浐瀹氬畨瑁呮煇鐗堟湰锛屼笉鍐欓粯璁ょ敤 Gitea 瀹樻柟 latest
+# ============================================================================
+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} $*"; }
+
+# ---------- 瑙f瀽鍙傛暟 ----------
+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"
+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 ;;
+ --version) PINNED_VERSION="${2#v}"; shift 2 ;;
+ -h|--help)
+ sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//'
+ exit 0
+ ;;
+ *) err "鏈煡鍙傛暟锛?1锛?-help 鏌ョ湅甯姪锛? ;;
+ esac
+done
+
+[[ -n "${GITEA_URL}" ]] || err "蹇呴』浼?--gitea https://浣犵殑-gitea-鍦板潃"
+[[ -n "${REG_TOKEN}" ]] || err "蹇呴』浼?--token锛堜粠 Gitea 椤甸潰鐨?Registration token 澶嶅埗锛孯xx 寮€澶寸殑涓€涓诧級"
+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
+ x86_64|amd64) PLATFORM="linux-amd64" ;;
+ aarch64|arm64) PLATFORM="linux-arm64" ;;
+ armv7l) PLATFORM="linux-arm" ;;
+ *) err "鏆備笉鏀寔鐨勬灦鏋勶細${ARCH}锛堥渶瑕?x86_64 / aarch64 / armv7锛? ;;
+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*) info "鈥?Token 鍓嶇紑 RG锛氬叏灞€/绔欑偣绾э紙鑻辨枃鍘熺増锛夆渽" ;;
+ RGO*) info "鈥?Token 鍓嶇紑 RGO锛氱粍缁囩骇锛堣嫳鏂囧師鐗堬級鉁? ;;
+ RGT*) info "鈥?Token 鍓嶇紑 RGT锛氫粨搴撶骇锛堣嫳鏂囧師鐗堬級鉁? ;;
+ # 绾瓧姣嶆暟瀛楋紙涓枃 UI / 浜屽紑鐗堟湰甯歌锛?+ [A-Za-z0-9]\{16,128\})
+ ok "鈥?绾瓧姣嶆暟瀛?Token锛堜腑鏂?UI / 鏌愪簺浜屽紑鐗堟湰甯歌锛夆渽锛屽拰鎴浘閲岀殑銆屾敞鍐屼护鐗屻€嶄竴鑷?
+ ;;
+ # 鍏朵粬
+ *)
+ warn "鈥?Token 鐪嬭捣鏉ヤ笉鏄?Gitea 甯歌鏍煎紡銆備笉杩囦篃鍙兘鏄柊鐗堟敼鍔紝缁х画灏濊瘯娉ㄥ唽..."
+ warn " 濡傛灉娉ㄥ唽鎶?401/403锛屽缓璁洖鍒伴〉闈㈢偣涓€娆°€岄噸缃敞鍐屼护鐗屻€嶉噸鏂板鍒躲€?
+ ;;
+esac
+# 鍩烘湰瀛楃闆嗘牎楠岋紙闃叉澶嶅埗鏃剁矘浜嗙┖鏍兼垨鎹㈣锛?+if [[ ! "${REG_TOKEN}" =~ ^[A-Za-z0-9]+$ ]]; then
+ warn "鈿狅笍 Token 閲屽惈鏈夐潪瀛楁瘝鏁板瓧鐨勫瓧绗︼紝寰堝彲鑳芥槸澶嶅埗鏃跺甯︿簡绌烘牸/鎹㈣/鏍囩偣锛?
+ warn " 寤鸿鍥炲埌浠撳簱椤甸潰鐐?馃搵 澶嶅埗鎸夐挳锛堜笉瑕佹墜閫夋枃瀛楋級銆?
+fi
+
+# ---------- 1. 瀹夎渚濊禆锛歞ocker锛圧unner 瑕佺敤 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. 涓嬭浇鏈€鏂?Gitea Runner锛坅ct_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 "猬囷笍 浣犳寚瀹氫簡鍥哄畾鐗堟湰锛歷${PINNED_VERSION}"
+else
+ REL_URL="https://gitea.com/api/v1/repos/gitea/runner/releases/latest"
+ info "猬囷笍 姝e湪鑾峰彇 Gitea Runner 鏈€鏂?release 鍏冩暟鎹?.."
+fi
+LATEST_JSON="$(curl -fsSL "${REL_URL}" 2>/dev/null || true)"
+
+# 3.2 濡傛灉 gitea.com 涓嶉€氾紙绾唴缃戯級锛岀洿鎺ュ洖閫€鍒颁竴涓垜浠凡鐭ュ彲鐢ㄧ殑鐗堟湰
+if [[ -z "${LATEST_JSON}" ]]; then
+ 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
+ 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 鍖归厤姝g‘ 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=(
+ # 鏂扮増锛坴3+锛塯itea-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锛坴0.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
+
+# 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 " 鎵炬枃浠跺悕绫讳技锛歡itea-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 "涓嬭浇澶辫触锛佸彲鑳芥槸 DNS/缃戠粶/璇佷功闂銆傚彲浠ユ墜鍔ㄦ妸涓婇潰 URL 鐨勬枃浠朵笅杞戒笅鏉?scp 鍒版湇鍔″櫒鏀惧埌 /usr/local/bin/act_runner锛屽啀閲嶆柊璺戞湰鑴氭湰銆?
+chmod +x /usr/local/bin/act_runner
+INSTALLED_VER="$(act_runner --version 2>&1 || echo "${TAG}")"
+ok "鉁?宸插畨瑁呭埌 /usr/local/bin/act_runner锛岀増鏈細${INSTALLED_VER}"
+
+# ---------- 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 ""
diff --git a/server/api/admin/blacklist/index.post.ts b/server/api/admin/blacklist/index.post.ts
index 515328a..cded730 100644
--- a/server/api/admin/blacklist/index.post.ts
+++ b/server/api/admin/blacklist/index.post.ts
@@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
})
}
- if (!['SONG', 'KEYWORD'].includes(type)) {
+ if (!['SONG', 'KEYWORD', 'LANGUAGE', 'GENRE'].includes(type)) {
throw createError({
statusCode: 400,
message: '鏃犳晥鐨勯粦鍚嶅崟绫诲瀷'
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/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) => {
// 鑾峰彇瀹㈡埛绔疘P鍦板潃
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/admin/songs/batch-reject.post.ts b/server/api/admin/songs/batch-reject.post.ts
new file mode 100644
index 0000000..2f0af7a
--- /dev/null
+++ b/server/api/admin/songs/batch-reject.post.ts
@@ -0,0 +1,135 @@
+import { db } from '~/drizzle/db'
+import { schedules, songBlacklists, songs, votes, requestTimes } from '~/drizzle/schema'
+import { and, eq, inArray, sql } from 'drizzle-orm'
+import { cacheService } from '../../../services/cacheService'
+import { createSongRejectedNotification } from '../../../services/notificationService'
+import { requireSongAdmin } from '~~/server/utils/requireSongAdmin'
+import { createApiError } from '~~/server/utils/apiError'
+import { SERVER_ERROR_CODES } from '~~/server/config/constants'
+
+const MAX_BATCH_REJECT_COUNT = 1000
+
+export default defineEventHandler(async (event) => {
+ const user = requireSongAdmin(event)
+
+ const body = await readBody(event)
+
+ const songIds = Array.isArray(body?.songIds)
+ ? [...new Set(body.songIds.filter((id: any) => Number.isInteger(id) && id > 0))]
+ : []
+ const reason = typeof body?.reason === 'string' ? body.reason.trim() : ''
+ const addToBlacklist = body?.addToBlacklist === true
+
+ if (songIds.length === 0) {
+ throw createApiError(400, SERVER_ERROR_CODES.SONG_BATCH_REJECT_IDS_REQUIRED, '姝屾洸ID鍒楄〃涓嶈兘涓虹┖')
+ }
+
+ if (songIds.length > MAX_BATCH_REJECT_COUNT) {
+ throw createApiError(
+ 400,
+ SERVER_ERROR_CODES.SONG_BATCH_REJECT_LIMIT_EXCEEDED,
+ `鍗曟鏈€澶氶┏鍥?${MAX_BATCH_REJECT_COUNT} 棣栨瓕鏇瞏,
+ { params: [MAX_BATCH_REJECT_COUNT] }
+ )
+ }
+
+ if (!reason) {
+ throw createApiError(400, SERVER_ERROR_CODES.SONG_BATCH_REJECT_REASON_REQUIRED, '椹冲洖鍘熷洜涓嶈兘涓虹┖')
+ }
+
+ try {
+ let rejectedSongs: any[] = []
+
+ await db.transaction(async (tx) => {
+ const existingSongs = await tx.select().from(songs).where(inArray(songs.id, songIds))
+
+ if (existingSongs.length === 0) {
+ throw createApiError(404, SERVER_ERROR_CODES.SONG_NOT_FOUND, '姝屾洸涓嶅瓨鍦ㄦ垨宸茶鍒犻櫎')
+ }
+
+ const existingIds = existingSongs.map((song) => song.id)
+ console.log(`寮€濮嬫壒閲忛┏鍥炴瓕鏇诧紝鍏?${existingIds.length} 棣朻)
+
+ if (addToBlacklist) {
+ const blacklistValues = [...new Set(existingSongs.map((song: any) => `${song.title} - ${song.artist}`))]
+ const existingBlacklist = await tx
+ .select({ value: songBlacklists.value })
+ .from(songBlacklists)
+ .where(and(eq(songBlacklists.type, 'SONG'), inArray(songBlacklists.value, blacklistValues)))
+ const existingSet = new Set(existingBlacklist.map((item) => item.value))
+ const valuesToInsert = blacklistValues.filter((value) => !existingSet.has(value))
+ for (const value of valuesToInsert) {
+ try {
+ await tx.insert(songBlacklists).values({
+ type: 'SONG',
+ value,
+ reason: `姝屾洸椹冲洖: ${reason}`,
+ createdBy: user.id
+ })
+ } catch (error: any) {
+ console.log(`榛戝悕鍗曟坊鍔犲け璐ワ紝鍙兘宸插瓨鍦? ${error.message}`)
+ }
+ }
+ console.log(`榛戝悕鍗曟坊鍔犲鐞嗗畬鎴恅)
+ }
+
+ await tx.delete(votes).where(inArray(votes.songId, existingIds))
+ console.log(`鍒犻櫎浜嗘姇绁ㄨ褰昤)
+
+ await tx.delete(schedules).where(inArray(schedules.songId, existingIds))
+ console.log(`鍒犻櫎浜嗘帓鏈熻褰昤)
+
+ const hitRequestIds = [...new Set(existingSongs.map((song: any) => song.hitRequestId).filter(Boolean))]
+ for (const hitRequestId of hitRequestIds) {
+ const count = existingSongs.filter((song: any) => song.hitRequestId === hitRequestId).length
+ try {
+ await tx
+ .update(requestTimes)
+ .set({
+ accepted: sql`GREATEST(0, accepted - ${count})`
+ })
+ .where(eq(requestTimes.id, hitRequestId as any))
+ } catch (error: any) {
+ console.error(`鍑忓皯鎶曠鏃舵 ${hitRequestId} 鎺ョ撼鏁伴噺澶辫触: ${error.message}`)
+ }
+ }
+
+ rejectedSongs = await tx.delete(songs).where(inArray(songs.id, existingIds)).returning()
+ console.log(`姝屾洸鎵归噺鍒犻櫎瀹屾垚`)
+ })
+
+ const songsToNotify = rejectedSongs.filter((song) => song.requesterId)
+ for (const song of songsToNotify) {
+ createSongRejectedNotification(
+ song.requesterId,
+ { title: song.title, artist: song.artist },
+ reason
+ ).catch((error: any) => {
+ console.error(`鍙戦€侀┏鍥為€氱煡澶辫触: ${error.message}`)
+ })
+ }
+
+ await cacheService.clearSongsCache()
+ await cacheService.clearSchedulesCache()
+
+ console.log(`鎵归噺椹冲洖鎿嶄綔瀹屾垚锛屽叡 ${rejectedSongs.length} 棣朻)
+
+ return {
+ success: true,
+ message: '姝屾洸鎵归噺椹冲洖鎴愬姛',
+ data: {
+ rejected: rejectedSongs.length,
+ missing: songIds.length - rejectedSongs.length,
+ notificationSent: songsToNotify.length
+ }
+ }
+ } catch (error: any) {
+ console.error('鎵归噺椹冲洖姝屾洸澶辫触:', error)
+
+ if (error.statusCode) {
+ throw error
+ }
+
+ throw createApiError(500, SERVER_ERROR_CODES.SONG_OPERATION_FAILED, '鎵归噺椹冲洖澶辫触: ' + error.message)
+ }
+})
diff --git a/server/api/admin/stats/active-users.get.ts b/server/api/admin/stats/active-users.get.ts
index ac05201..e5fb760 100644
--- a/server/api/admin/stats/active-users.get.ts
+++ b/server/api/admin/stats/active-users.get.ts
@@ -1,10 +1,10 @@
import { createError, defineEventHandler, getQuery } from 'h3'
import { db } from '~/drizzle/db'
-import { songs, users, votes } from '~/drizzle/schema'
-import { CacheService } from '../../../services/cacheService'
+import { songs, users, votes, userIdentities } from '~/drizzle/schema'
+import { CacheService } from '~~/server/services/cacheService'
+import { getUserAvatarUrl } from '~~/server/utils/user-avatar'
export default defineEventHandler(async (event) => {
- // 妫€鏌ヨ璇佸拰鏉冮檺
const user = event.context.user
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw createError({
@@ -18,7 +18,6 @@ export default defineEventHandler(async (event) => {
const limit = parseInt(query.limit as string) || 20
try {
- // 灏濊瘯浠庣紦瀛樿幏鍙栨暟鎹? const cacheService = CacheService.getInstance()
const cachedStats = await cacheService.getActiveUsersStats(semester, limit)
if (cachedStats) {
@@ -26,18 +25,24 @@ export default defineEventHandler(async (event) => {
return cachedStats
}
- // 鏋勫缓鏌ヨ鏉′欢 - 濡傛灉娌℃湁鎸囧畾瀛︽湡鎴栧鏈熶负'all'锛屾煡璇㈡墍鏈夌敤鎴? let songWhere = {}
if (semester && semester !== 'all') {
songWhere = { semester: semester }
}
- // 鑾峰彇娲昏穬鐢ㄦ埛鏁版嵁 - 鏈夋姇绋挎瓕鏇茬殑鐢ㄦ埛
const allUsers = await db.select().from(users)
const allSongs = await db.select().from(songs)
const allVotes = await db.select().from(votes)
+ const allIdentities = await db.select().from(userIdentities)
+
+ const identitiesMap = new Map<number, typeof allIdentities>()
+ allIdentities.forEach((identity) => {
+ if (!identitiesMap.has(identity.userId)) {
+ identitiesMap.set(identity.userId, [])
+ }
+ identitiesMap.get(identity.userId)!.push(identity)
+ })
- // 杩囨护鏈夋姇绋挎瓕鏇茬殑鐢ㄦ埛
const activeUsers = allUsers
.filter((user) => {
const userSongs = allSongs.filter((song) => {
@@ -65,44 +70,48 @@ export default defineEventHandler(async (event) => {
}
})
- // 濡傛灉娌℃湁娲昏穬鐢ㄦ埛锛岃繑鍥炵┖鏁扮粍
if (activeUsers.length === 0) {
const emptyResult = []
- // 缂撳瓨绌虹粨鏋? await cacheService.setActiveUsersStats(semester, limit, emptyResult)
console.log('[Cache] 娲昏穬鐢ㄦ埛缁熻鏁版嵁宸茬紦瀛橈紙绌虹粨鏋滐級')
return emptyResult
}
- // 璁$畻姣忎釜鐢ㄦ埛鐨勬椿璺冨害鏁版嵁
const userStats = activeUsers.map((user) => {
const contributions = user.songs.length
const likes = user.songs.reduce((total, song) => total + song.votes.length, 0)
-
- // 娲昏穬搴﹁绠楋細鎶曠鏁?* 2 + 鐐硅禐鏁?* 0.5
const activityScore = Math.round(contributions * 2 + likes * 0.5)
+ const userIdentitiesData = identitiesMap.get(user.id) || []
+ const avatarUrl = getUserAvatarUrl(
+ {
+ id: user.id,
+ name: user.name,
+ username: user.username,
+ email: user.email,
+ avatarProvider: user.avatarProvider,
+ avatarProviderUserId: user.avatarProviderUserId
+ },
+ userIdentitiesData
+ )
- const userStat = {
+ return {
id: user.id,
- name: user.name || user.username, // 浼樺厛鏄剧ず鐪熷疄濮撳悕锛屽鏋滄病鏈夊垯鏄剧ず鐧诲綍鍚?+ name: user.name || user.username,
contributions,
likes,
- activityScore
+ activityScore,
+ avatarUrl
}
-
- return userStat
})
- // 鎸夋椿璺冨害鍒嗘暟闄嶅簭鎺掑垪骞堕檺鍒舵暟閲? const sortedUsers = userStats.sort((a, b) => b.activityScore - a.activityScore).slice(0, limit)
- // 缂撳瓨缁撴灉
await cacheService.setActiveUsersStats(semester, limit, sortedUsers)
console.log('[Cache] 娲昏穬鐢ㄦ埛缁熻鏁版嵁宸茬紦瀛?)
return sortedUsers
} catch (error) {
- // 杩斿洖绌烘暟缁勮€屼笉鏄姏鍑洪敊璇紝閬垮厤鍓嶇鏄剧ず閿欒
+ console.error('[Stats] 鑾峰彇娲昏穬鐢ㄦ埛澶辫触:', error)
return []
}
})
diff --git a/server/api/admin/stats/realtime.get.ts b/server/api/admin/stats/realtime.get.ts
index 030e2a8..7b4f449 100644
--- a/server/api/admin/stats/realtime.get.ts
+++ b/server/api/admin/stats/realtime.get.ts
@@ -1,12 +1,24 @@
import { createError, defineEventHandler } from 'h3'
import { db } from '~/drizzle/db'
-import { CacheService } from '../../../services/cacheService'
-import { songs, users, votes } from '~/drizzle/schema'
-import { count, eq, gte } from 'drizzle-orm'
+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 '~~/server/utils/user-avatar'
+
+function formatRelativeTime(date: Date | null | undefined): string {
+ if (!date) return '鏈煡'
+ const now = new Date()
+ const diff = now.getTime() - new Date(date).getTime()
+ const minutes = Math.floor(diff / 60000)
+ if (minutes < 1) return '鍒氬垰'
+ if (minutes < 60) return `${minutes}鍒嗛挓鍓峘
+ const hours = Math.floor(minutes / 60)
+ if (hours < 24) return `${hours}灏忔椂鍓峘
+ return `${Math.floor(hours / 24)}澶╁墠`
+}
export default defineEventHandler(async (event) => {
- // 妫€鏌ヨ璇佸拰鏉冮檺
const user = event.context.user
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw createError({
@@ -16,97 +28,172 @@ export default defineEventHandler(async (event) => {
}
try {
- // 灏濊瘯浠庣紦瀛樿幏鍙栨暟鎹? const cacheService = CacheService.getInstance()
const cachedStats = await cacheService.getRealtimeStats()
if (cachedStats) {
console.log('[Cache] 瀹炴椂缁熻鏁版嵁缂撳瓨鍛戒腑')
return cachedStats
}
- // 鑾峰彇褰撳墠鏃堕棿鐩稿叧鐨勬棩鏈? const now = new Date()
- const today = getBeijingStartOfDay() // 浣跨敤鍖椾含鏃堕棿鐨勪竴澶╁紑濮?+ const today = getBeijingStartOfDay()
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000)
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
- // 骞惰鑾峰彇瀹炴椂缁熻鏁版嵁
const [activeUsersData, todayRequests, popularGenres, peakHours] = await Promise.all([
- // 娲昏穬鐢ㄦ埛鏁板拰鐢ㄦ埛鍒楄〃 (鏈€杩?灏忔椂鐐规瓕銆佺櫥褰曟垨鐐硅禐鐨勭敤鎴?
(async () => {
try {
- // 鑾峰彇鏈€杩?灏忔椂鍐呯偣姝岀殑鐢ㄦ埛
const recentSongUsers = await db
.select({
requesterId: songs.requesterId,
requesterName: users.name,
requesterUsername: users.username,
- requesterId2: users.id
+ requesterId2: users.id,
+ requesterLastActive: songs.createdAt,
+ requesterAvatarProvider: users.avatarProvider,
+ requesterAvatarProviderUserId: users.avatarProviderUserId,
+ requesterEmail: users.email
})
.from(songs)
.innerJoin(users, eq(songs.requesterId, users.id))
.where(gte(songs.createdAt, oneHourAgo))
- .groupBy(songs.requesterId, users.id, users.name, users.username)
+ .groupBy(songs.requesterId, users.id, users.name, users.username, users.lastLogin, users.avatarProvider, users.avatarProviderUserId, users.email, songs.createdAt)
- // 鑾峰彇鏈€杩?灏忔椂鍐呯櫥褰曠殑鐢ㄦ埛
const recentLoginUsers = await db
.select({
id: users.id,
username: users.username,
- name: users.name
+ name: users.name,
+ lastLogin: users.lastLogin,
+ avatarProvider: users.avatarProvider,
+ avatarProviderUserId: users.avatarProviderUserId,
+ email: users.email
})
.from(users)
.where(gte(users.lastLogin, oneHourAgo))
- // 鑾峰彇鏈€杩?灏忔椂鍐呯偣璧炶繃姝屾洸鐨勭敤鎴? const recentVoteUsers = await db
.select({
userId: votes.userId,
userName: users.name,
userUsername: users.username,
- userId2: users.id
+ userId2: users.id,
+ voteCreatedAt: votes.createdAt,
+ userAvatarProvider: users.avatarProvider,
+ userAvatarProviderUserId: users.avatarProviderUserId,
+ userEmail: users.email
})
.from(votes)
.innerJoin(users, eq(votes.userId, users.id))
.where(gte(votes.createdAt, oneHourAgo))
- .groupBy(votes.userId, users.id, users.name, users.username)
+ .groupBy(votes.userId, users.id, users.name, users.username, users.avatarProvider, users.avatarProviderUserId, users.email, votes.createdAt)
- // 鍚堝苟骞跺幓閲嶇敤鎴峰垪琛?- const userMap = new Map()
+ const userMap = new Map<number, any>()
- // 娣诲姞鐐规瓕鐢ㄦ埛
recentSongUsers.forEach((song) => {
- userMap.set(song.requesterId2, {
- id: song.requesterId2,
+ const id = song.requesterId2
+ const existing = userMap.get(id)
+ const lastActive = existing?.lastActive
+ ? new Date(existing.lastActive) > new Date(song.requesterLastActive)
+ ? existing.lastActive
+ : song.requesterLastActive
+ : song.requesterLastActive
+ userMap.set(id, {
+ id,
username: song.requesterUsername,
- name: song.requesterName
+ name: song.requesterName,
+ email: song.requesterEmail,
+ avatarProvider: song.requesterAvatarProvider,
+ avatarProviderUserId: song.requesterAvatarProviderUserId,
+ lastActive
})
})
- // 娣诲姞鐧诲綍鐢ㄦ埛
- recentLoginUsers.forEach((user) => {
- userMap.set(user.id, {
- id: user.id,
- username: user.username,
- name: user.name
+ recentLoginUsers.forEach((u) => {
+ const existing = userMap.get(u.id)
+ const lastActive = existing?.lastActive
+ ? new Date(existing.lastActive) > new Date(u.lastLogin)
+ ? existing.lastActive
+ : u.lastLogin
+ : u.lastLogin
+ userMap.set(u.id, {
+ id: u.id,
+ username: u.username,
+ name: u.name,
+ email: u.email,
+ avatarProvider: u.avatarProvider,
+ avatarProviderUserId: u.avatarProviderUserId,
+ lastActive
})
})
- // 娣诲姞鐐硅禐鐢ㄦ埛
recentVoteUsers.forEach((vote) => {
- userMap.set(vote.userId2, {
- id: vote.userId2,
+ const id = vote.userId2
+ const existing = userMap.get(id)
+ const lastActive = existing?.lastActive
+ ? new Date(existing.lastActive) > new Date(vote.voteCreatedAt)
+ ? existing.lastActive
+ : vote.voteCreatedAt
+ : vote.voteCreatedAt
+ userMap.set(id, {
+ id,
username: vote.userUsername,
- name: vote.userName
+ name: vote.userName,
+ email: vote.userEmail,
+ avatarProvider: vote.userAvatarProvider,
+ avatarProviderUserId: vote.userAvatarProviderUserId,
+ lastActive
})
})
- const activeUsersList = Array.from(userMap.values())
+ const userIds = Array.from(userMap.keys())
+ let identitiesData: any[] = []
+ if (userIds.length > 0) {
+ try {
+ identitiesData = await db
+ .select()
+ .from(userIdentities)
+ .where(inArray(userIdentities.userId, userIds))
+ } catch (e) {
+ identitiesData = []
+ }
+ }
+
+ const identitiesMap = new Map<number, any[]>()
+ identitiesData.forEach((identity) => {
+ if (!identitiesMap.has(identity.userId)) {
+ identitiesMap.set(identity.userId, [])
+ }
+ identitiesMap.get(identity.userId)!.push(identity)
+ })
+
+ const activeUsersList = Array.from(userMap.values()).map((u) => {
+ const userIdentitiesData = identitiesMap.get(u.id) || []
+ const avatarUrl = getUserAvatarUrl(
+ {
+ id: u.id,
+ name: u.name,
+ username: u.username,
+ email: u.email,
+ avatarProvider: u.avatarProvider,
+ avatarProviderUserId: u.avatarProviderUserId
+ },
+ userIdentitiesData
+ )
+ return {
+ id: u.id,
+ username: u.username,
+ name: u.name,
+ avatarUrl,
+ lastActive: formatRelativeTime(u.lastActive)
+ }
+ })
return {
count: activeUsersList.length,
users: activeUsersList
}
} catch (error) {
+ console.error('[Realtime] 鑾峰彇娲昏穬鐢ㄦ埛澶辫触:', error)
return {
count: 0,
users: []
@@ -114,7 +201,6 @@ export default defineEventHandler(async (event) => {
}
})(),
- // 浠婃棩鐐规瓕鏁? (async () => {
try {
const todayCountResult = await db
@@ -127,12 +213,10 @@ export default defineEventHandler(async (event) => {
}
})(),
- // 鐑棬娴佹淳 (Song妯″瀷涓病鏈塯enre瀛楁锛岃繑鍥炵┖鏁扮粍)
(async () => {
return []
})(),
- // 楂樺嘲鏃舵 (鏈€杩?澶╃殑灏忔椂缁熻)
(async () => {
try {
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
@@ -143,7 +227,6 @@ export default defineEventHandler(async (event) => {
.from(songs)
.where(gte(songs.createdAt, sevenDaysAgo))
- // 鎸夊皬鏃剁粺璁? const hourCount = songsData.reduce(
(acc, song) => {
const hour = getBeijingHour(song.createdAt)
@@ -153,7 +236,6 @@ export default defineEventHandler(async (event) => {
{} as Record<number, number>
)
- // 杩斿洖鍓?涓珮宄版椂娈? return Object.entries(hourCount)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
@@ -177,12 +259,12 @@ export default defineEventHandler(async (event) => {
timestamp: now.toISOString()
}
- // 缂撳瓨缁撴灉
await cacheService.setRealtimeStats(result)
console.log('[Cache] 瀹炴椂缁熻鏁版嵁宸茬紦瀛?)
return result
} catch (error) {
+ console.error('[Realtime] 鑾峰彇澶辫触:', error)
throw createError({
statusCode: 500,
message: '鑾峰彇瀹炴椂缁熻鏁版嵁澶辫触'
diff --git a/server/api/admin/system-settings/index.post.ts b/server/api/admin/system-settings/index.post.ts
index b33c4fe..1c25235 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: 'ClassicLight',
+ enabledThemes: '["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/admin/users/[id].put.ts b/server/api/admin/users/[id].put.ts
index a9cc874..4dbfec8 100644
--- a/server/api/admin/users/[id].put.ts
+++ b/server/api/admin/users/[id].put.ts
@@ -3,6 +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'
export default defineEventHandler(async (event) => {
try {
@@ -51,6 +52,8 @@ export default defineEventHandler(async (event) => {
}
const targetUser = existingUser[0]
+ const currentLevel = getRoleLevel(user.role)
+ const targetLevel = getRoleLevel(targetUser.role)
// 1. 绂佹淇敼绯荤粺鍒濆瓒呯骇绠$悊鍛?(ID: 1)
if (targetUser.id === 1) {
@@ -61,7 +64,6 @@ export default defineEventHandler(async (event) => {
}
// 2. 绂佹瀵硅嚜韬繘琛屼换浣曠敤鎴风鐞嗘搷浣滐紙鍖呮嫭瑙掕壊銆佽祫鏂欍€佺姸鎬併€佸瘑鐮佺瓑锛?- // 浣跨敤 String 杞崲纭繚 ID 姣旇緝鐨勫噯纭€? if (String(userId) === String(user.id)) {
throw createError({
statusCode: 400,
@@ -69,12 +71,11 @@ export default defineEventHandler(async (event) => {
})
}
- // 3. 瓒婄骇淇敼淇濇姢
- // 濡傛灉鐩爣鐢ㄦ埛鏄?SUPER_ADMIN锛屾搷浣滆€呭繀椤绘槸 SUPER_ADMIN
- if (targetUser.role === 'SUPER_ADMIN' && user.role !== 'SUPER_ADMIN') {
+ // 3. 闈炶秴绾х鐞嗗憳蹇呴』涓ユ牸楂樹簬鐩爣瑙掕壊绾у埆
+ if (user.role !== 'SUPER_ADMIN' && currentLevel <= targetLevel) {
throw createError({
statusCode: 403,
- statusMessage: '鏉冮檺涓嶈冻锛氭櫘閫氱鐞嗗憳鏃犳硶淇敼瓒呯骇绠$悊鍛樹俊鎭?
+ statusMessage: `鏉冮檺涓嶈冻锛?{user.role} 鏃犳硶鎿嶄綔鍚岀骇鎴栨洿楂樿鑹茬殑鐢ㄦ埛`
})
}
@@ -94,31 +95,26 @@ export default defineEventHandler(async (event) => {
}
}
- // 瑙掕壊鏉冮檺鎺у埗
- let validRole = 'USER'
- if (role && ['USER', 'TEACHER', 'ADMIN', 'SONG_ADMIN', 'SUPER_ADMIN'].includes(role)) {
- // 瓒呯骇绠$悊鍛樺彲浠ヨ缃换浣曡鑹?- if (user.role === 'SUPER_ADMIN') {
- validRole = role
- }
- // 绠$悊鍛樺彧鑳借缃鐞嗗憳浠ヤ笅鐨勮鑹诧紙USER, SONG_ADMIN锛?- else if (user.role === 'ADMIN') {
- if (['USER', 'TEACHER', 'SONG_ADMIN'].includes(role)) {
- validRole = role
- } else {
- throw createError({
- statusCode: 403,
- statusMessage: '绠$悊鍛樺彧鑳借缃敤鎴枫€佹暀甯堝拰姝屾洸绠$悊鍛樿鑹?
- })
- }
- }
- // 鍏朵粬瑙掕壊涓嶈兘璁剧疆瑙掕壊
- else {
+ // 瑙掕壊鏉冮檺鎺у埗锛堢粺涓€浣跨敤 canChangeRoleOf 璇箟鍖栨鏌ワ級
+ let validRole = targetUser.role
+ if (role && role !== targetUser.role) {
+ if (!canChangeRoleOf(user, targetUser, role)) {
throw createError({
statusCode: 403,
- statusMessage: '娌℃湁鏉冮檺璁剧疆鐢ㄦ埛瑙掕壊'
+ statusMessage: user.role === 'SUPER_ADMIN'
+ ? '瓒呯骇绠$悊鍛樹笉鑳藉皢鐩爣鐢ㄦ埛鎻愬崌涓?SUPER_ADMIN'
+ : `鏉冮檺涓嶈冻锛?{user.role} 鏃犳硶灏嗙敤鎴疯鑹叉洿鏀逛负 ${role}`
})
}
+ validRole = role
+ }
+
+ // 濡傛灉璇锋眰涓寘鍚瘑鐮佹洿鏂帮紝鏍¢獙瀵嗙爜閲嶇疆鏉冮檺
+ if (password && !canResetPasswordFor(user, targetUser)) {
+ throw createError({
+ statusCode: 403,
+ statusMessage: `鏉冮檺涓嶈冻锛?{user.role} 鏃犳硶閲嶇疆鐩爣鐢ㄦ埛鐨勫瘑鐮乣
+ })
}
// 楠岃瘉status瀛楁鐨勬湁鏁堟€?diff --git a/server/api/admin/users/[id]/reset-password.post.ts b/server/api/admin/users/[id]/reset-password.post.ts
index 18e9cc7..c421e5b 100644
--- a/server/api/admin/users/[id]/reset-password.post.ts
+++ b/server/api/admin/users/[id]/reset-password.post.ts
@@ -1,16 +1,18 @@
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 '~~/server/utils/permissions'
export default defineEventHandler(async (event) => {
- // 妫€鏌ヨ璇佸拰鏉冮檺
- const user = event.context.user
- if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
+ const currentUser = event.context.user
+ if (!currentUser || !['ADMIN', 'SUPER_ADMIN'].includes(currentUser.role)) {
throw createError({
statusCode: 403,
message: '娌℃湁鏉冮檺璁块棶'
})
}
- // 瀹夊叏鑾峰彇ID鍙傛暟
const params = event.context.params || {}
const id = parseInt(params.id || '')
@@ -31,15 +33,54 @@ export default defineEventHandler(async (event) => {
}
try {
- // 绂佹瀵硅嚜韬繘琛屽瘑鐮侀噸缃紙閫氳繃鐢ㄦ埛绠$悊鐣岄潰锛?- if (id === user.id) {
+ if (id === currentUser.id) {
throw createError({
statusCode: 400,
message: '绂佹鍦ㄧ敤鎴风鐞嗕腑閲嶇疆鑷繁鐨勫瘑鐮?
})
}
-
- // 浣跨敤缁熶竴鏈嶅姟閲嶇疆瀵嗙爜 (forceReset = true)
+
+ const targetUserResult = await db
+ .select({ id: users.id, role: users.role, username: users.username })
+ .from(users)
+ .where(eq(users.id, id))
+ .limit(1)
+
+ if (targetUserResult.length === 0) {
+ throw createError({
+ statusCode: 404,
+ message: '鐢ㄦ埛涓嶅瓨鍦?
+ })
+ }
+
+ const targetUser = targetUserResult[0]
+
+ if (targetUser.id === 1) {
+ throw createError({
+ statusCode: 403,
+ message: '鏃犳硶閲嶇疆绯荤粺鍒濆瓒呯骇绠$悊鍛樼殑瀵嗙爜'
+ })
+ }
+
+ const currentLevel = getRoleLevel(currentUser.role)
+ const targetLevel = getRoleLevel(targetUser.role)
+
+ if (currentUser.role !== 'SUPER_ADMIN') {
+ if (currentLevel <= targetLevel) {
+ throw createError({
+ statusCode: 403,
+ message: '鏉冮檺涓嶈冻锛氫笉鑳介噸缃鑹茬骇鍒ぇ浜庣瓑浜庤嚜宸辩殑鐢ㄦ埛瀵嗙爜'
+ })
+ }
+ }
+
+ if (!canResetPasswordFor(currentUser, targetUser)) {
+ throw createError({
+ statusCode: 403,
+ message: '鏉冮檺涓嶈冻锛氬綋鍓嶈鑹叉棤娉曢噸缃鐢ㄦ埛鐨勫瘑鐮?
+ })
+ }
+
await updateUserPassword(id, body.newPassword, true)
return {
@@ -47,7 +88,10 @@ export default defineEventHandler(async (event) => {
message: '瀵嗙爜閲嶇疆鎴愬姛'
}
} catch (error) {
- console.error('閲嶇疆瀵嗙爜澶辫触:', error)
+ console.error('[ResetPassword] 澶辫触:', error)
+ if (error.statusCode) {
+ throw error
+ }
throw createError({
statusCode: 500,
message: '閲嶇疆瀵嗙爜澶辫触'
diff --git a/server/api/admin/users/index.get.ts b/server/api/admin/users/index.get.ts
index 950aa84..9c2b06f 100644
--- a/server/api/admin/users/index.get.ts
+++ b/server/api/admin/users/index.get.ts
@@ -96,26 +96,36 @@ export default defineEventHandler(async (event) => {
meowNickname: true,
meowBoundAt: true,
createdAt: true,
- updatedAt: true
+ updatedAt: true,
+ avatarProvider: true,
+ avatarProviderUserId: true,
+ email: true
},
with: {
- identities: {
- columns: {
- provider: true,
- providerUsername: true
- },
- where: (identities, { eq }) => eq(identities.provider, 'github')
- }
+ identities: true
}
})
- // 澶勭悊鐢ㄦ埛鍒楄〃锛屾坊鍔犲ご鍍忓瓧娈?- const formattedUsers = usersList.map((user) => ({
- ...user,
- avatar: user.identities?.[0]?.providerUsername
- ? `https://github.com/${user.identities[0].providerUsername}.png`
- : null
- }))
+ const { getUserAvatarUrl } = await import('~~/server/utils/user-avatar')
+
+ const formattedUsers = usersList.map((user) => {
+ const avatarUrl = getUserAvatarUrl(
+ {
+ id: user.id,
+ name: user.name,
+ username: user.username,
+ email: user.email,
+ avatarProvider: user.avatarProvider,
+ avatarProviderUserId: user.avatarProviderUserId
+ },
+ user.identities || []
+ )
+ return {
+ ...user,
+ avatar: avatarUrl,
+ avatarUrl
+ }
+ })
// 璁$畻鍒嗛〉淇℃伅
const totalPages = Math.ceil(total / limitNum)
diff --git a/server/api/admin/users/index.post.ts b/server/api/admin/users/index.post.ts
index dd11826..5f41903 100644
--- a/server/api/admin/users/index.post.ts
+++ b/server/api/admin/users/index.post.ts
@@ -2,6 +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'
export default defineEventHandler(async (event) => {
// 妫€鏌ヨ璇佸拰鏉冮檺
@@ -51,31 +52,21 @@ export default defineEventHandler(async (event) => {
// 鍔犲瘑瀵嗙爜
const hashedPassword = await bcrypt.hash(body.password, 10)
- // 瑙掕壊鏉冮檺鎺у埗
+ // 瑙掕壊鏉冮檺鎺у埗锛堢粺涓€浣跨敤 canManageUserRole 鏁板€煎寲绾у埆妫€鏌ワ級
let validRole = 'USER'
- if (body.role && ['USER', 'TEACHER', 'ADMIN', 'SONG_ADMIN', 'SUPER_ADMIN'].includes(body.role)) {
- // 瓒呯骇绠$悊鍛樺彲浠ュ垱寤轰换浣曡鑹茬殑鐢ㄦ埛
- if (user.role === 'SUPER_ADMIN') {
- validRole = body.role
- }
- // 绠$悊鍛樺彧鑳藉垱寤虹鐞嗗憳浠ヤ笅鐨勮鑹诧紙USER, SONG_ADMIN锛?- else if (user.role === 'ADMIN') {
- if (['USER', 'TEACHER', 'SONG_ADMIN'].includes(body.role)) {
- validRole = body.role
- } else {
- throw createError({
- statusCode: 403,
- message: '绠$悊鍛樺彧鑳藉垱寤虹敤鎴枫€佹暀甯堝拰姝屾洸绠$悊鍛樿鑹?
- })
- }
- }
- // 鍏朵粬瑙掕壊涓嶈兘鍒涘缓鐢ㄦ埛
- else {
+ const requestedRole = body.role
+ if (requestedRole) {
+ if (!canManageUserRole(user, requestedRole)) {
+ const currentLevel = getRoleLevel(user.role)
+ const targetLevel = getRoleLevel(requestedRole)
throw createError({
statusCode: 403,
- message: '娌℃湁鏉冮檺鍒涘缓鐢ㄦ埛'
+ message: isRoleHigher(requestedRole, user.role) || currentLevel <= targetLevel
+ ? `鏉冮檺涓嶈冻锛?{user.role} 鏃犳硶鍒涘缓绾у埆楂樹簬鎴栫瓑浜庤嚜韬殑瑙掕壊锛?{requestedRole}锛塦
+ : `娌℃湁鏉冮檺鍒涘缓 ${requestedRole} 瑙掕壊鐨勭敤鎴穈
})
}
+ validRole = requestedRole
}
// 鐘舵€侀獙璇?diff --git a/server/api/auth/2fa/verify.post.ts b/server/api/auth/2fa/verify.post.ts
index 14f191c..7f5fd7e 100644
--- a/server/api/auth/2fa/verify.post.ts
+++ b/server/api/auth/2fa/verify.post.ts
@@ -3,6 +3,7 @@ const { authenticator } = otplib
import { db, userIdentities, eq, and, users } from '~/drizzle/db'
import { twoFactorCodes } from '~~/server/utils/twoFactorStore'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
+import { createAuthSession } from '~~/server/utils/create-auth-session'
import { getClientIP } from '~~/server/utils/ip-utils'
import { getBeijingTime } from '~/utils/timeUtils'
@@ -100,7 +101,12 @@ export default defineEventHandler(async (event) => {
.catch((err) => console.error('Error updating user login info:', err))
// 鐢熸垚Token
- const authToken = JWTEnhanced.generateToken(user.id, user.role)
+ const { token: authToken, jti } = JWTEnhanced.generateTokenWithJti(user.id, user.role)
+ await createAuthSession(event, {
+ userId: user.id,
+ jti,
+ loginMethod: type === 'totp' ? 'totp_2fa' : 'email_2fa'
+ })
const isSecure =
getRequestURL(event).protocol === 'https:' ||
diff --git a/server/api/auth/[provider]/callback.get.ts b/server/api/auth/[provider]/callback.get.ts
index fe196ca..dc24044 100644
--- a/server/api/auth/[provider]/callback.get.ts
+++ b/server/api/auth/[provider]/callback.get.ts
@@ -2,6 +2,7 @@ import { parseState, getRedirectUri } from '~~/server/utils/oauth'
import { generateBindingToken } from '~~/server/utils/oauth-token'
import { db, eq, userIdentities } from '~/drizzle/db'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
+import { createAuthSession } from '~~/server/utils/create-auth-session'
import { getOAuthStrategy } from '~~/server/utils/oauth-strategies'
import { isUserBlocked, getUserBlockRemainingTime } from '~~/server/services/securityService'
@@ -142,7 +143,13 @@ async function handleUserLoginOrBind(
}
// 鐧诲綍
- const token = JWTEnhanced.generateToken(existingIdentity.user.id, existingIdentity.user.role)
+ const { token, jti } = JWTEnhanced.generateTokenWithJti(existingIdentity.user.id, existingIdentity.user.role)
+ await createAuthSession(event, {
+ userId: existingIdentity.user.id,
+ jti,
+ loginMethod: 'oauth',
+ oauthProvider: provider
+ })
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
diff --git a/server/api/auth/bind.post.ts b/server/api/auth/bind.post.ts
index 1e31b27..43025d1 100644
--- a/server/api/auth/bind.post.ts
+++ b/server/api/auth/bind.post.ts
@@ -1,6 +1,7 @@
import bcrypt from 'bcryptjs'
import { db, eq, users, userIdentities } from '~/drizzle/db'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
+import { createAuthSession } from '~~/server/utils/create-auth-session'
import { verifyBindingToken } from '~~/server/utils/oauth-token'
import {
isAccountLocked,
@@ -86,7 +87,13 @@ export default defineEventHandler(async (event) => {
recordLoginSuccess(username, clientIp)
// 鐧诲綍
- const token = JWTEnhanced.generateToken(user.id, user.role)
+ const { token, jti } = JWTEnhanced.generateTokenWithJti(user.id, user.role)
+ await createAuthSession(event, {
+ userId: user.id,
+ jti,
+ loginMethod: 'oauth',
+ oauthProvider: payload.provider
+ })
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts
index 5d3f3f9..2113caf 100644
--- a/server/api/auth/login.post.ts
+++ b/server/api/auth/login.post.ts
@@ -1,6 +1,7 @@
import bcrypt from 'bcryptjs'
import { db, eq, users, userIdentities, and } from '~/drizzle/db'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
+import { createAuthSession } from '~~/server/utils/create-auth-session'
import {
getAccountLockRemainingTime,
getIPBlockRemainingTime,
@@ -187,7 +188,8 @@ export default defineEventHandler(async (event) => {
.catch((err) => console.error('Error updating user login info:', err))
// 鐢熸垚JWT
- const token = JWTEnhanced.generateToken(user.id, user.role)
+ const { token, jti } = JWTEnhanced.generateTokenWithJti(user.id, user.role)
+ await createAuthSession(event, { userId: user.id, jti, loginMethod: 'password' })
// 鑷姩鍒ゆ柇鏄惁闇€瑕乻ecure
const isSecure =
diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts
index 5e6ef73..7369928 100644
--- a/server/api/auth/register.post.ts
+++ b/server/api/auth/register.post.ts
@@ -1,6 +1,7 @@
import bcrypt from 'bcryptjs'
import { db, eq, users } from '~/drizzle/db'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
+import { createAuthSession } from '~~/server/utils/create-auth-session'
import { emailVerificationCodes } from '~~/server/api/user/email/send-code.post'
export default defineEventHandler(async (event) => {
@@ -109,7 +110,8 @@ export default defineEventHandler(async (event) => {
}
// 绛惧彂 JWT Token
- const token = JWTEnhanced.generateToken(newUser.id, newUser.role)
+ const { token, jti } = JWTEnhanced.generateTokenWithJti(newUser.id, newUser.role)
+ await createAuthSession(event, { userId: newUser.id, jti, loginMethod: 'password' })
const isSecure =
getRequestURL(event).protocol === 'https:' ||
diff --git a/server/api/auth/verify.get.ts b/server/api/auth/verify.get.ts
index d44065b..db98f63 100644
--- a/server/api/auth/verify.get.ts
+++ b/server/api/auth/verify.get.ts
@@ -3,6 +3,7 @@ import { db } from '~/drizzle/db'
import { users } from '~/drizzle/schema'
import { executeRedisCommand, isRedisReady } from '../../utils/redis'
import { eq } from 'drizzle-orm'
+import { isSessionRevokedByJti, touchAuthSession } from '~~/server/utils/create-auth-session'
// 鐢ㄦ埛璁よ瘉缂撳瓨锛堟案涔呯紦瀛橈紝鐧诲嚭鎴栨潈闄愬彉鏇存椂涓诲姩澶辨晥锛?
@@ -19,9 +20,17 @@ export default defineEventHandler(async (event) => {
}
// 楠岃瘉JWT浠ょ墝
- const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: number }
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: number; jti?: string }
const userId = decoded.userId
+ if (await isSessionRevokedByJti(decoded.jti)) {
+ throw createError({
+ statusCode: 401,
+ statusMessage: '浼氳瘽宸叉挙閿€锛岃閲嶆柊鐧诲綍'
+ })
+ }
+ touchAuthSession(decoded.jti).catch(() => undefined)
+
// 浼樺厛浠嶳edis缂撳瓨鑾峰彇鐢ㄦ埛璁よ瘉鐘舵€? if (isRedisReady()) {
const cachedUser = await executeRedisCommand(async () => {
diff --git a/server/api/auth/webauthn/login/verify.post.ts b/server/api/auth/webauthn/login/verify.post.ts
index 1367427..a05d8d4 100644
--- a/server/api/auth/webauthn/login/verify.post.ts
+++ b/server/api/auth/webauthn/login/verify.post.ts
@@ -4,6 +4,7 @@ import { getWebAuthnConfig } from '~~/server/utils/webauthn-config'
import { db, eq, and, userIdentities } from '~/drizzle/db'
import { createError, defineEventHandler, setCookie } from 'h3'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
+import { createAuthSession } from '~~/server/utils/create-auth-session'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
@@ -84,7 +85,8 @@ export default defineEventHandler(async (event) => {
.where(eq(userIdentities.id, identity.id))
// 绛惧彂鐧诲綍 Token
- const token = JWTEnhanced.generateToken(identity.user.id, identity.user.role)
+ const { token, jti } = JWTEnhanced.generateTokenWithJti(identity.user.id, identity.user.role)
+ await createAuthSession(event, { userId: identity.user.id, jti, loginMethod: 'webauthn' })
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
diff --git a/server/api/blacklist/check.post.ts b/server/api/blacklist/check.post.ts
index b0a2922..f666348 100644
--- a/server/api/blacklist/check.post.ts
+++ b/server/api/blacklist/check.post.ts
@@ -3,9 +3,39 @@ import { db } from '~/drizzle/db'
import { songBlacklists, systemSettings } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
+function isCJK(text: string): boolean {
+ return /[\u4e00-\u9fa5]/.test(text)
+}
+function isLatin(text: string): boolean {
+ return /[A-Za-z]/.test(text)
+}
+function isKana(text: string): boolean {
+ return /[\u3040-\u30ff]/.test(text)
+}
+function isHangul(text: string): boolean {
+ return /[\uac00-\ud7af]/.test(text)
+}
+function isCyrillic(text: string): boolean {
+ return /[\u0400-\u04ff]/.test(text)
+}
+function isArabic(text: string): boolean {
+ return /[\u0600-\u06ff]/.test(text)
+}
+function detectLanguages(text: string): string[] {
+ const result: string[] = []
+ if (!text) return result
+ if (isCJK(text)) result.push('zh', '涓枃', 'chinese', 'cjk')
+ if (isLatin(text)) result.push('en', '鑻辨枃', 'english', 'latin')
+ if (isKana(text)) result.push('ja', '鏃ユ枃', 'japanese')
+ if (isHangul(text)) result.push('ko', '闊╂枃', 'korean')
+ if (isCyrillic(text)) result.push('ru', '淇勬枃', 'russian', 'cyrillic')
+ if (isArabic(text)) result.push('ar', '闃挎媺浼枃', 'arabic')
+ return result
+}
+
export default defineEventHandler(async (event) => {
const body = await readBody(event)
- const { title, artist } = body
+ const { title, artist, genre } = body
if (!title) {
throw createError({
@@ -15,24 +45,31 @@ export default defineEventHandler(async (event) => {
}
try {
- // 鑾峰彇绯荤粺璁剧疆
const systemSettingsResult = await db.select().from(systemSettings).limit(1)
const systemSettingsData = systemSettingsResult[0]
const showBlacklistKeywords = systemSettingsData?.showBlacklistKeywords ?? false
- // 鑾峰彇鎵€鏈夋椿璺冪殑榛戝悕鍗曢」
const blacklistItems = await db
.select()
.from(songBlacklists)
.where(eq(songBlacklists.isActive, true))
const songFullName = `${title} - ${artist || ''}`.toLowerCase()
- const blocked = []
+ const genreLower = (genre || '').toLowerCase()
+ const combinedText = `${title} ${artist || ''} ${genre || ''}`
+ const textLanguages = new Set([
+ ...detectLanguages(title || ''),
+ ...detectLanguages(artist || ''),
+ ...detectLanguages(genre || '')
+ ])
+
+ const blocked: Array<{ type: string; value: string | null; reason: string }> = []
for (const item of blacklistItems) {
+ const valueLower = item.value.toLowerCase()
+
if (item.type === 'SONG') {
- // 妫€鏌ュ叿浣撴瓕鏇?- if (songFullName.includes(item.value.toLowerCase())) {
+ if (songFullName.includes(valueLower)) {
blocked.push({
type: 'song',
value: item.value,
@@ -40,15 +77,38 @@ export default defineEventHandler(async (event) => {
})
}
} else if (item.type === 'KEYWORD') {
- // 妫€鏌ュ叧閿瘝
- if (songFullName.includes(item.value.toLowerCase())) {
+ if (songFullName.includes(valueLower) || (genreLower && genreLower.includes(valueLower))) {
blocked.push({
type: 'keyword',
- value: showBlacklistKeywords ? item.value : null, // 鏍规嵁璁剧疆鍐冲畾鏄惁鏄剧ず鍏蜂綋鍏抽敭璇?+ value: showBlacklistKeywords ? item.value : null,
reason:
item.reason || (showBlacklistKeywords ? `鍖呭惈鍏抽敭璇嶏細${item.value}` : '鍖呭惈鍏抽敭璇?)
})
}
+ } else if (item.type === 'LANGUAGE') {
+ const tokens = [
+ valueLower,
+ ...valueLower.split(/[,锛孿/|\s]+/).map((s) => s.trim()).filter(Boolean)
+ ]
+ const hit = tokens.some((lang) => textLanguages.has(lang))
+ if (hit) {
+ blocked.push({
+ type: 'language',
+ value: showBlacklistKeywords ? item.value : null,
+ reason: item.reason || `璇ヨ绉嶆瓕鏇插凡琚檺鍒讹細${showBlacklistKeywords ? item.value : ''}`.trim()
+ })
+ }
+ } else if (item.type === 'GENRE') {
+ const combinedForGenre = `${genreLower} ${combinedText}`.toLowerCase()
+ const tokens = valueLower.split(/[,锛孿/|\s]+/).map((s) => s.trim()).filter(Boolean)
+ const hit = tokens.some((tok) => combinedForGenre.includes(tok))
+ if (hit) {
+ blocked.push({
+ type: 'genre',
+ value: showBlacklistKeywords ? item.value : null,
+ reason: item.reason || `璇ユ洸椋庢瓕鏇插凡琚檺鍒讹細${showBlacklistKeywords ? item.value : ''}`.trim()
+ })
+ }
}
}
@@ -57,7 +117,8 @@ export default defineEventHandler(async (event) => {
reasons: blocked,
song: {
title,
- artist
+ artist,
+ genre: genre || null
}
}
} catch (error) {
diff --git a/server/api/open/schedules.get.ts b/server/api/open/schedules.get.ts
index 4139815..9744bd4 100644
--- a/server/api/open/schedules.get.ts
+++ b/server/api/open/schedules.get.ts
@@ -1,12 +1,17 @@
import { createError, defineEventHandler, getQuery } from 'h3'
import { db } from '~/drizzle/db'
-import { playTimes, schedules, songCollaborators, songs, users } from '~/drizzle/schema'
+import { playTimes, schedules, songCollaborators, songs, systemSettings, users } from '~/drizzle/schema'
import { and, asc, desc, eq, gte, inArray, like, lt, or, sql } from 'drizzle-orm'
import { formatDateTime } from '~/utils/timeUtils'
import { openApiCache } from '~~/server/utils/open-api-cache'
import { CACHE_CONSTANTS } from '~~/server/config/constants'
import { cacheService } from '~~/server/services/cacheService'
import { executeRedisCommand, isRedisReady } from '~~/server/utils/redis'
+import {
+ resolveScheduleVisibilityRange,
+ isPlayDateInRange,
+ type VisibilitySettingsLike
+} from '~~/server/utils/schedule-visibility'
export default defineEventHandler(async (event) => {
try {
@@ -36,6 +41,27 @@ export default defineEventHandler(async (event) => {
const sortBy = (query.sortBy as string) || 'playDate'
const sortOrder = (query.sortOrder as string) || 'asc'
+ // 璇诲彇绯荤粺璁剧疆锛氭帓鏈熷彲瑙佽寖鍥?+ let visibilityRange = { startAt: null as Date | null, endAt: null as Date | null }
+ try {
+ const systemSettingsDataResult = await db.select().from(systemSettings).limit(1)
+ const systemSettingsData = systemSettingsDataResult[0] || null
+ const visibilitySettings: VisibilitySettingsLike = {
+ scheduleDaysBeforeEnabled: systemSettingsData?.scheduleDaysBeforeEnabled,
+ scheduleDaysBefore: systemSettingsData?.scheduleDaysBefore,
+ scheduleDaysAfterEnabled: systemSettingsData?.scheduleDaysAfterEnabled,
+ scheduleDaysAfter: systemSettingsData?.scheduleDaysAfter
+ }
+ visibilityRange = resolveScheduleVisibilityRange(visibilitySettings)
+ } catch (err) {
+ console.warn('[Schedules API] 璇诲彇鎺掓湡鍙鑼冨洿璁剧疆澶辫触锛屽皢灞曠ず鍏ㄩ儴:', err)
+ }
+
+ const applyVisibilityFilter = (items: any[]): any[] => {
+ if (!visibilityRange.startAt && !visibilityRange.endAt) return items
+ return items.filter((s) => isPlayDateInRange(s.playDate, visibilityRange))
+ }
+
// 浼樺厛浠庢櫘閫欰PI缂撳瓨鑾峰彇鏁版嵁
// 灏濊瘯浠嶤acheService鑾峰彇鎺掓湡鏁版嵁
@@ -49,6 +75,7 @@ export default defineEventHandler(async (event) => {
if (semester) {
filteredSchedules = cachedSchedules.filter((s) => s.song?.semester === semester)
}
+ filteredSchedules = applyVisibilityFilter(filteredSchedules)
// 杞崲涓哄紑鏀続PI鏍煎紡
const result = {
@@ -105,11 +132,12 @@ export default defineEventHandler(async (event) => {
})
if (redisData) {
+ const filteredRedisData = applyVisibilityFilter(redisData)
// 杞崲涓哄紑鏀続PI鏍煎紡
const result = {
success: true,
data: {
- schedules: redisData.map((schedule: any) => ({
+ schedules: filteredRedisData.map((schedule: any) => ({
id: schedule.id,
playDate: schedule.playDate,
sequence: schedule.sequence,
@@ -133,7 +161,7 @@ export default defineEventHandler(async (event) => {
}
: null
})),
- total: redisData.length,
+ total: filteredRedisData.length,
semester: semester || null
}
}
@@ -175,6 +203,13 @@ export default defineEventHandler(async (event) => {
conditions.push(or(like(songs.title, `%${search}%`)!, like(songs.artist, `%${search}%`)!)!)
}
+ if (visibilityRange.startAt) {
+ conditions.push(gte(schedules.playDate, visibilityRange.startAt)!)
+ }
+ if (visibilityRange.endAt) {
+ conditions.push(lt(schedules.playDate, visibilityRange.endAt)!)
+ }
+
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
// 璁$畻鍋忕Щ閲?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
diff --git a/server/api/site-config.get.ts b/server/api/site-config.get.ts
index 8ead517..0ad6af9 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 = [
@@ -26,17 +27,23 @@ const publicFields = [
'enableSubmissionRemarks',
'enableRequestTimeLimitation',
'forceBlockAllRequests',
- 'smtpEnabled'
+ 'smtpEnabled',
+ 'defaultTheme',
+ 'enabledThemes',
+ 'scheduleDaysBeforeEnabled',
+ 'scheduleDaysBefore',
+ 'scheduleDaysAfterEnabled',
+ 'scheduleDaysAfter',
+ 'statisticsCode',
+ 'statisticsCodeEnabled'
]
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
@@ -49,7 +56,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 })
}
}
@@ -70,7 +77,8 @@ export default defineEventHandler(async (event) => {
siteDescription: '鏍″洯骞挎挱绔欑偣姝岀郴缁?- 璁╀綘鐨勫0闊宠鍚',
siteVersion: process.env.NUXT_PUBLIC_SITE_VERSION || null,
submissionGuidelines: '璇烽伒瀹堟牎鍥瀹氾紝鎻愪氦鍋ュ悍鍚戜笂鐨勬瓕鏇层€?,
- icpNumber: null,
+ icpNumber: '绮CP澶?025450093鍙?,
+ gonganNumber: null,
enableSubmissionLimit: false,
dailySubmissionLimit: null,
weeklySubmissionLimit: null,
@@ -81,14 +89,20 @@ export default defineEventHandler(async (event) => {
enableRequestTimeLimitation: false,
forceBlockAllRequests: false,
enableReplayRequests: false,
- smtpEnabled: false
+ smtpEnabled: false,
+ defaultTheme: 'ClassicLight',
+ enabledThemes: '["ClassicLight","ModernLight"]',
+ scheduleDaysBeforeEnabled: false,
+ scheduleDaysBefore: 30,
+ scheduleDaysAfterEnabled: false,
+ scheduleDaysAfter: 14
})
.returning()
settings = newSettings[0]
}
- const publicSettings = filterPublicSettings(settings)
+ const publicSettings = filterPublicSettings({ ...SYSTEM_SETTINGS_DEFAULTS, ...(settings || {}) })
// 灏嗙粨鏋滅紦瀛樺埌Redis锛堝鏋滃彲鐢級- 姘镐箙缂撳瓨
if (settings && isRedisReady()) {
diff --git a/server/api/songs/[id]/update.put.ts b/server/api/songs/[id]/update.put.ts
index abe3c9c..3d5b70d 100644
--- a/server/api/songs/[id]/update.put.ts
+++ b/server/api/songs/[id]/update.put.ts
@@ -1,8 +1,16 @@
import { db } from '~/drizzle/db'
-import { songs, users, songCollaborators, collaborationLogs } from '~/drizzle/schema'
-import { eq, or } from 'drizzle-orm'
+import {
+ songs,
+ users,
+ songCollaborators,
+ collaborationLogs,
+ systemSettings,
+ songBlacklists
+} from '~/drizzle/schema'
+import { and, eq, inArray, or } from 'drizzle-orm'
import { cacheService } from '~~/server/services/cacheService'
import { createSubmissionNoteClearedNotification } from '~~/server/services/notificationService'
+import { moderateSubmissionNote } from '~~/server/utils/submissionNoteAutoModeration'
export default defineEventHandler(async (event) => {
try {
@@ -122,6 +130,38 @@ export default defineEventHandler(async (event) => {
? body.submissionNote.trim()
: null
}
+ if ('submissionNotePublic' in body) {
+ const wantPublic = updateData.submissionNote ? body.submissionNotePublic !== false : false
+ updateData.submissionNotePublic = wantPublic
+
+ // 鑷姩瀹℃煡锛堣嫢 systemSettings 鎵撳紑 submissionNoteRequiresApproval锛?+ try {
+ const sysSetRes = await db.select().from(systemSettings).limit(1)
+ const sysSet = sysSetRes[0]
+ if (sysSet?.submissionNoteRequiresApproval && wantPublic && updateData.submissionNote) {
+ const bl = await db
+ .select()
+ .from(songBlacklists)
+ .where(inArray(songBlacklists.type, ['KEYWORD', 'LANGUAGE', 'GENRE']))
+ const mod = moderateSubmissionNote(
+ updateData.submissionNote,
+ wantPublic,
+ true,
+ {
+ title: updateData.title || existingSong.title,
+ artist: updateData.artist || existingSong.artist
+ },
+ bl
+ )
+ updateData.submissionNotePublic = mod.notePublic
+ updateData.submissionNotePublicStatus = mod.publicStatus
+ } else {
+ updateData.submissionNotePublicStatus = null
+ }
+ } catch (err) {
+ console.warn('[Update API] 鎶曠鐣欒█鑷姩瀹℃煡澶辫触锛屼繚鎸佺敤鎴疯缃?', err)
+ }
+ }
const currentRequesterId = updateData.requesterId || existingSong.requesterId
diff --git a/server/api/songs/add.post.ts b/server/api/songs/add.post.ts
index ee7850c..b625176 100644
--- a/server/api/songs/add.post.ts
+++ b/server/api/songs/add.post.ts
@@ -2,6 +2,7 @@ import { db } from '~/drizzle/db'
import { songs, users } from '~/drizzle/schema'
import { and, eq, or } from 'drizzle-orm'
import { cacheService } from '~~/server/services/cacheService'
+import { normalizeCoverUrl } from '~~/server/utils/url'
export default defineEventHandler(async (event) => {
try {
@@ -87,6 +88,8 @@ export default defineEventHandler(async (event) => {
})
}
+ const normalizedCover = normalizeCoverUrl(cover, `add-song:${title || ''}`)
+
// 鍒涘缓姝屾洸
const newSongResult = await db
.insert(songs)
@@ -99,8 +102,8 @@ export default defineEventHandler(async (event) => {
musicPlatform: musicPlatform || null,
musicId: musicId || null,
kgHash: musicPlatform === 'kugou' ? musicId : null,
- cover: cover || null,
- coverUrl: cover || null,
+ cover: normalizedCover,
+ coverUrl: normalizedCover,
playUrl: playUrl || null
})
.returning()
diff --git a/server/api/songs/import.post.ts b/server/api/songs/import.post.ts
index fb48f03..6224ace 100644
--- a/server/api/songs/import.post.ts
+++ b/server/api/songs/import.post.ts
@@ -2,6 +2,7 @@ import { defineEventHandler, createError, readBody } from 'h3'
import { db } from '~/drizzle/db'
import { songs, semesters, songBlacklists } from '~/drizzle/schema'
import { eq, inArray, and, asc } from 'drizzle-orm'
+import { normalizeCoverUrl } from '~~/server/utils/url'
export default defineEventHandler(async (event) => {
const user = event.context.user
@@ -120,6 +121,8 @@ export default defineEventHandler(async (event) => {
continue
}
+ const normalizedCover = normalizeCoverUrl(song.cover, `import-song:${song.title || ''}`)
+
// 3. 鍑嗗鎻掑叆鏁版嵁
songsToInsert.push({
title: song.title,
@@ -129,7 +132,8 @@ export default defineEventHandler(async (event) => {
playedAt: null,
semester: currentSemesterName,
preferredPlayTimeId: null, // 閲嶇疆鍋忓ソ鏃堕棿
- cover: song.cover,
+ cover: normalizedCover,
+ coverUrl: normalizedCover,
playUrl: song.playUrl,
musicPlatform: song.musicPlatform,
musicId: song.musicId,
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
// 鏌ヨ姝屾洸鎬绘暟
diff --git a/server/api/songs/public.get.ts b/server/api/songs/public.get.ts
index 6cd730c..d342e44 100644
--- a/server/api/songs/public.get.ts
+++ b/server/api/songs/public.get.ts
@@ -11,7 +11,12 @@ import {
users,
votes
} from '~/drizzle/schema'
-import { and, count, desc, eq, inArray } from 'drizzle-orm'
+import { and, count, desc, eq, gte, inArray, lt, like, or } from 'drizzle-orm'
+import {
+ resolveScheduleVisibilityRange,
+ isPlayDateInRange,
+ type VisibilitySettingsLike
+} from '~~/server/utils/schedule-visibility'
import { cacheService } from '~~/server/services/cacheService'
import { executeRedisCommand, isRedisReady } from '../../utils/redis'
import { formatDateTime } from '~/utils/timeUtils'
@@ -38,6 +43,13 @@ export default defineEventHandler(async (event) => {
.limit(1)
const systemSettingsData = systemSettingsDataResult[0] || null
const shouldHideStudentInfo = systemSettingsData?.hideStudentInfo ?? true
+ const visibilitySettings: VisibilitySettingsLike = {
+ scheduleDaysBeforeEnabled: systemSettingsData?.scheduleDaysBeforeEnabled,
+ scheduleDaysBefore: systemSettingsData?.scheduleDaysBefore,
+ scheduleDaysAfterEnabled: systemSettingsData?.scheduleDaysAfterEnabled,
+ scheduleDaysAfter: systemSettingsData?.scheduleDaysAfter
+ }
+ const visibilityRange = resolveScheduleVisibilityRange(visibilitySettings)
// 鍒濆鍖栫紦瀛樻湇鍔?
@@ -73,7 +85,14 @@ export default defineEventHandler(async (event) => {
})
if (cachedData) {
- return cachedData
+ const filtered = visibilityRange.startAt || visibilityRange.endAt
+ ? cachedData.filter((s) => isPlayDateInRange(s.playDate, visibilityRange))
+ : cachedData
+ const resultData = JSON.parse(JSON.stringify(filtered)) as PublicScheduleItem[]
+ if (shouldHideStudentInfo && !isAdmin) {
+ maskPublicScheduleData(resultData)
+ }
+ return resultData
}
}
@@ -91,6 +110,12 @@ export default defineEventHandler(async (event) => {
filteredSchedules = cachedSchedules.filter((s) => s.song?.semester === semester)
console.log(`[Cache] 杩囨护瀛︽湡 ${semester} 鍚庣殑鏁伴噺: ${filteredSchedules.length}`)
}
+ // 搴旂敤鎺掓湡鍙鑼冨洿杩囨护
+ if (visibilityRange.startAt || visibilityRange.endAt) {
+ filteredSchedules = filteredSchedules.filter((s) =>
+ isPlayDateInRange(s.playDate, visibilityRange)
+ )
+ }
// 灏嗚繃婊ゅ悗鐨勬暟鎹紦瀛樺埌Redis锛堢紦瀛樺畬鏁存暟鎹紝涓嶈繘琛屾ā绯婂寲锛? if (isRedisReady()) {
@@ -116,6 +141,13 @@ export default defineEventHandler(async (event) => {
}
// 鑾峰彇鎺掓湡鐨勬瓕鏇诧紝鍖呭惈鎾斁鏃舵淇℃伅锛堟煡璇㈡墍鏈夊鏈熺殑鏁版嵁锛?+ const scheduleWhere: any[] = [eq(schedules.isDraft, false)]
+ if (visibilityRange.startAt) {
+ scheduleWhere.push(gte(schedules.playDate, visibilityRange.startAt)!)
+ }
+ if (visibilityRange.endAt) {
+ scheduleWhere.push(lt(schedules.playDate, visibilityRange.endAt)!)
+ }
const schedulesData = await db
.select({
id: schedules.id,
@@ -153,10 +185,9 @@ export default defineEventHandler(async (event) => {
.leftJoin(songs, eq(schedules.songId, songs.id))
.leftJoin(users, eq(songs.requesterId, users.id))
.leftJoin(playTimes, eq(schedules.playTimeId, playTimes.id))
- .where(eq(schedules.isDraft, false)) // 鍙煡璇㈠凡鍙戝竷鐨勬帓鏈?+ .where(and(...scheduleWhere)) // 鍙煡璇㈠凡鍙戝竷鐨勬帓鏈燂紙+鍙鑼冨洿锛? .orderBy(schedules.playDate)
- // 鑾峰彇姣忛姝岀殑鎶曠エ鏁? const voteCountsQuery = await db
.select({
songId: votes.songId,
diff --git a/server/api/songs/request.post.ts b/server/api/songs/request.post.ts
index 632859a..22f9761 100644
--- a/server/api/songs/request.post.ts
+++ b/server/api/songs/request.post.ts
@@ -4,14 +4,17 @@ import {
playTimes,
requestTimes,
semesters,
+ songBlacklists,
songCollaborators,
songs,
systemSettings
} from '~/drizzle/db'
-import { and, eq, gt, gte, lt, lte, sql } from 'drizzle-orm'
+import { and, eq, gt, gte, inArray, lt, lte, sql } from 'drizzle-orm'
import { createCollaborationInvitationNotification } from '~~/server/services/notificationService'
import { isLimitReached } from '~~/server/utils/submissionLimit'
import { getBeijingTimeISOString } from '~/utils/timeUtils'
+import { moderateSubmissionNote } from '~~/server/utils/submissionNoteAutoModeration'
+import { normalizeCoverUrl } from '~~/server/utils/url'
export default defineEventHandler(async (event) => {
// 妫€鏌ョ敤鎴疯璇?@@ -132,6 +135,18 @@ export default defineEventHandler(async (event) => {
const systemSettingsData = systemSettingsResult[0]
const isAdmin = user.role === 'SUPER_ADMIN' || user.role === 'ADMIN'
+ // 鍔犺浇鑷姩瀹℃煡闇€瑕佺殑榛戝悕鍗曪紙KEYWORD / LANGUAGE / GENRE锛?+ let moderationBlacklists: any[] = []
+ try {
+ moderationBlacklists = await db
+ .select()
+ .from(songBlacklists)
+ .where(inArray(songBlacklists.type, ['KEYWORD', 'LANGUAGE', 'GENRE']))
+ } catch (err) {
+ console.warn('[Request API] 鍔犺浇鑷姩瀹℃煡榛戝悕鍗曞け璐ワ紝灏嗚烦杩囪嚜鍔ㄥ鏌?', err)
+ moderationBlacklists = []
+ }
+
// 1. 妫€鏌ュ己鍒跺叧闂? if (systemSettingsData?.forceBlockAllRequests && !isAdmin) {
throw createError({
@@ -243,8 +258,34 @@ export default defineEventHandler(async (event) => {
}
const submissionNote =
systemSettingsData?.enableSubmissionRemarks && rawSubmissionNote ? rawSubmissionNote : null
- const submissionNotePublic =
- submissionNote !== null ? body.submissionNotePublic !== false : false
+ const rawSubmissionNotePublic = submissionNote !== null ? body.submissionNotePublic !== false : false
+ const requiresApproval = Boolean(systemSettingsData?.submissionNoteRequiresApproval)
+
+ let finalSubmissionNotePublic = rawSubmissionNotePublic
+ let submissionNotePublicStatus: string | null = null
+
+ if (submissionNote && rawSubmissionNotePublic && requiresApproval) {
+ const mod = moderateSubmissionNote(
+ submissionNote,
+ rawSubmissionNotePublic,
+ requiresApproval,
+ { title: body.title, artist: body.artist, genre: body.genre || null },
+ moderationBlacklists
+ )
+ finalSubmissionNotePublic = mod.notePublic
+ submissionNotePublicStatus = mod.publicStatus
+ if (mod.rejectedReasons.length > 0) {
+ console.log(
+ `[Request API] 鎶曠鐣欒█鑷姩瀹℃煡鏈€氳繃锛屾瓕鏇?${body.title} - ${body.artist}锛屽師鍥?`,
+ mod.rejectedReasons
+ )
+ }
+ } else if (submissionNote) {
+ // 娌″紑鑷姩瀹℃煡锛屼篃璁板綍涓€涓嬬姸鎬?+ submissionNotePublicStatus = null
+ }
+
+ const submissionNotePublic = finalSubmissionNotePublic
// 鍑嗗鍙戦€侀€氱煡鐨勫垪琛? const notificationsToSend: { userId: number; songId: number; songTitle: string }[] = []
@@ -350,6 +391,8 @@ export default defineEventHandler(async (event) => {
}
}
+ const normalizedCover = normalizeCoverUrl(body.cover, `request-song:${body.title || ''}`)
+
const songResult = await tx
.insert(songs)
.values({
@@ -359,14 +402,15 @@ export default defineEventHandler(async (event) => {
requesterId: user.id,
preferredPlayTimeId: preferredPlayTime?.id || null,
semester: currentSemester, // 浣跨敤澶栭儴鑾峰彇鐨勫鏈熷悕绉?- cover: body.cover || null,
- coverUrl: body.cover || null,
+ cover: normalizedCover,
+ coverUrl: normalizedCover,
musicPlatform: isBilibili ? 'bilibili' : (body.musicPlatform || null),
musicId: finalMusicId,
kgHash: body.musicPlatform === 'kugou' ? body.musicId : null,
playUrl: body.playUrl || null,
submissionNote,
submissionNotePublic,
+ submissionNotePublicStatus,
hitRequestId: hitRequestTime?.id || null,
expectedPlayDate: body.expectedPlayDate || null,
expectedPlaySequence: body.expectedPlaySequence !== undefined ? Number(body.expectedPlaySequence) : null
diff --git a/server/config/constants.ts b/server/config/constants.ts
index daa4c29..f708722 100644
--- a/server/config/constants.ts
+++ b/server/config/constants.ts
@@ -180,3 +180,13 @@ export const RISK_CONTROL_CONSTANTS = {
SONG_IP_DOMINANCE_PERCENT: 0.6,
SONG_IP_MIN_SAMPLE: 8
} as const
+
+export const SERVER_ERROR_CODES = {
+ AUTH_UNAUTHORIZED: 'AUTH_UNAUTHORIZED',
+ AUTH_FORBIDDEN: 'AUTH_FORBIDDEN',
+ SONG_NOT_FOUND: 'SONG_NOT_FOUND',
+ SONG_OPERATION_FAILED: 'SONG_OPERATION_FAILED',
+ SONG_BATCH_REJECT_IDS_REQUIRED: 'SONG_BATCH_REJECT_IDS_REQUIRED',
+ SONG_BATCH_REJECT_LIMIT_EXCEEDED: 'SONG_BATCH_REJECT_LIMIT_EXCEEDED',
+ SONG_BATCH_REJECT_REASON_REQUIRED: 'SONG_BATCH_REJECT_REASON_REQUIRED'
+} as const
diff --git a/server/plugins/sentry.ts b/server/plugins/sentry.ts
new file mode 100644
index 0000000..daec8c9
--- /dev/null
+++ b/server/plugins/sentry.ts
@@ -0,0 +1,24 @@
+import * as Sentry from '@sentry/node'
+
+export default defineNitroPlugin((nitroApp) => {
+ const config = useRuntimeConfig()
+ const dsn = config.sentry?.serverDsn || config.sentry?.dsn
+
+ if (!dsn) return
+
+ Sentry.init({
+ dsn,
+ environment: config.sentry?.environment || process.env.NODE_ENV || 'production',
+ release: config.sentry?.release || undefined,
+ tracesSampleRate: config.sentry?.errorSampleRate ?? 1.0,
+ sampleRate: config.sentry?.errorSampleRate ?? 1.0
+ })
+
+ nitroApp.hooks.hook('error', (error) => {
+ Sentry.captureException(error)
+ })
+
+ nitroApp.hooks.hook('request', (event) => {
+ event.context.sentry = Sentry
+ })
+})
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 // 鎻愬墠鍒锋柊鏃堕棿锛堢锛?diff --git a/server/services/cardCodeDeleteService.ts b/server/services/cardCodeDeleteService.ts
new file mode 100644
index 0000000..eecbfaf
--- /dev/null
+++ b/server/services/cardCodeDeleteService.ts
@@ -0,0 +1,20 @@
+import { inArray } from 'drizzle-orm'
+import { db } from '~/drizzle/db'
+import { cardCodes, cardCodeRedemptions, songs } from '~/drizzle/schema'
+
+export async function deleteCardCodesByIds(ids: number[]) {
+ return await db.transaction(async (tx) => {
+ await tx.update(songs)
+ .set({ cardCodeId: null })
+ .where(inArray(songs.cardCodeId, ids))
+
+ await tx.delete(cardCodeRedemptions)
+ .where(inArray(cardCodeRedemptions.cardCodeId, ids))
+
+ const deleted = await tx.delete(cardCodes)
+ .where(inArray(cardCodes.id, ids))
+ .returning()
+
+ return deleted
+ })
+}
diff --git a/server/utils/admin-password-policy.ts b/server/utils/admin-password-policy.ts
new file mode 100644
index 0000000..9755036
--- /dev/null
+++ b/server/utils/admin-password-policy.ts
@@ -0,0 +1,35 @@
+import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from '~/utils/password-policy'
+
+interface AdminPasswordViolation {
+ code: string
+ message: string
+}
+
+export function getAdminPasswordViolation(password: unknown): AdminPasswordViolation | null {
+ if (typeof password !== 'string' || !password) {
+ return { code: 'AUTH_NEW_PASSWORD_REQUIRED', message: '鏂板瘑鐮佷笉鑳戒负绌? }
+ }
+
+ if (password.length < PASSWORD_MIN_LENGTH) {
+ return {
+ code: 'AUTH_PASSWORD_TOO_SHORT',
+ message: `瀵嗙爜闀垮害涓嶈兘灏戜簬${PASSWORD_MIN_LENGTH}涓瓧绗
+ }
+ }
+
+ if (password.length > PASSWORD_MAX_LENGTH) {
+ return {
+ code: 'AUTH_PASSWORD_TOO_LONG',
+ message: `瀵嗙爜闀垮害涓嶈兘瓒呰繃${PASSWORD_MAX_LENGTH}浣峘
+ }
+ }
+
+ if (new TextEncoder().encode(password).length > 72) {
+ return {
+ code: 'AUTH_PASSWORD_TOO_MANY_BYTES',
+ message: '瀵嗙爜鏈夋晥闀垮害涓嶈兘瓒呰繃72瀛楄妭'
+ }
+ }
+
+ return null
+}
diff --git a/server/utils/apiError.ts b/server/utils/apiError.ts
new file mode 100644
index 0000000..cea7d95
--- /dev/null
+++ b/server/utils/apiError.ts
@@ -0,0 +1,15 @@
+import { createError } from 'h3'
+
+export function createApiError(
+ statusCode: number,
+ code: string,
+ message: string,
+ data?: Record<string, unknown>
+) {
+ return createError({
+ statusCode,
+ statusMessage: code,
+ message,
+ data: { code, ...(data || {}) }
+ })
+}
diff --git a/server/utils/auth-route-policy.ts b/server/utils/auth-route-policy.ts
new file mode 100644
index 0000000..b0ef35b
--- /dev/null
+++ b/server/utils/auth-route-policy.ts
@@ -0,0 +1,99 @@
+type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
+
+interface RoutePolicy {
+ path: string
+ methods: readonly HttpMethod[]
+}
+
+const GET_METHODS = ['GET', 'HEAD'] as const
+const WRITE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'] as const
+
+export const PASSWORD_CHANGE_ALLOWED_ROUTES: readonly RoutePolicy[] = [
+ { path: '/api/auth/verify', methods: GET_METHODS },
+ { path: '/api/auth/logout', methods: ['POST'] },
+ { path: '/api/auth/change-password', methods: ['POST'] },
+ { path: '/api/auth/set-initial-password', methods: ['POST'] },
+ { path: '/api/auth/2fa/verify', methods: ['POST'] },
+ { path: '/api/auth/2fa/send-email', methods: ['POST'] },
+ { path: '/api/site-config', methods: GET_METHODS }
+]
+
+export const PUBLIC_API_EXACT_ROUTES: readonly RoutePolicy[] = [
+ { path: '/api/auth/login', methods: ['POST'] },
+ { path: '/api/auth/register', methods: ['POST'] },
+ { path: '/api/auth/email-code', methods: ['POST'] },
+ { path: '/api/auth/grade-class-options', methods: GET_METHODS },
+ { path: '/api/auth/oauth-register-options', methods: GET_METHODS },
+ { path: '/api/auth/bind', methods: ['POST'] },
+ { path: '/api/auth/oauth-register', methods: ['POST'] },
+ { path: '/api/auth/2fa/verify', methods: ['POST'] },
+ { path: '/api/auth/2fa/send-email', methods: ['POST'] },
+ { path: '/api/auth/forgot-password', methods: ['POST'] },
+ { path: '/api/auth/reset-password', methods: ['POST'] },
+ { path: '/api/auth/captcha', methods: GET_METHODS },
+ { path: '/api/semesters/current', methods: GET_METHODS },
+ { path: '/api/play-times', methods: GET_METHODS },
+ { path: '/api/schedules/public', methods: GET_METHODS },
+ { path: '/api/songs/count', methods: GET_METHODS },
+ { path: '/api/songs/public', methods: GET_METHODS },
+ { path: '/api/site-config', methods: GET_METHODS },
+ { path: '/api/system/location', methods: GET_METHODS },
+ { path: '/api/auth/webauthn/login', methods: ['POST'] },
+ { path: '/api/music/resolve-url', methods: ['POST'] },
+ { path: '/api/music/state', methods: ['POST'] },
+ { path: '/api/music/websocket', methods: GET_METHODS },
+ { path: '/api/sys/time', methods: GET_METHODS }
+]
+
+export const PUBLIC_API_PREFIX_ROUTES: readonly RoutePolicy[] = [
+ { path: '/api/proxy/', methods: GET_METHODS },
+ { path: '/api/api-enhanced/', methods: [...GET_METHODS, ...WRITE_METHODS] },
+ { path: '/api/native-api/', methods: [...GET_METHODS, 'POST'] },
+ { path: '/api/open/', methods: [...GET_METHODS, ...WRITE_METHODS] },
+ { path: '/api/auth/webauthn/login/', methods: ['POST'] }
+]
+
+function matchesMethod(policy: RoutePolicy, method: string): boolean {
+ return policy.methods.includes(method.toUpperCase() as HttpMethod)
+}
+
+export function isPublicApiPath(pathname: string, method: string): boolean {
+ return (
+ PUBLIC_API_EXACT_ROUTES.some(
+ (policy) => pathname === policy.path && matchesMethod(policy, method)
+ ) ||
+ PUBLIC_API_PREFIX_ROUTES.some(
+ (policy) => pathname.startsWith(policy.path) && matchesMethod(policy, method)
+ )
+ )
+}
+
+export function isAllowedDuringPasswordChange(pathname: string, method: string): boolean {
+ return PASSWORD_CHANGE_ALLOWED_ROUTES.some(
+ (policy) => pathname === policy.path && matchesMethod(policy, method)
+ )
+}
+
+export function shouldBlockDuringPasswordChange(
+ pathname: string,
+ method: string,
+ requirePasswordChange: boolean
+): boolean {
+ return (
+ requirePasswordChange &&
+ !isPublicApiPath(pathname, method) &&
+ !isAllowedDuringPasswordChange(pathname, method)
+ )
+}
+
+export function shouldBypassPublicApiAuthentication(
+ pathname: string,
+ method: string,
+ hasToken: boolean
+): boolean {
+ return isPublicApiPath(pathname, method) && !hasToken
+}
+
+export function canBindOAuthIdentity(requirePasswordChange: boolean): boolean {
+ return !requirePasswordChange
+}
diff --git a/server/utils/card-code-delete-handler.ts b/server/utils/card-code-delete-handler.ts
new file mode 100644
index 0000000..682954d
--- /dev/null
+++ b/server/utils/card-code-delete-handler.ts
@@ -0,0 +1,68 @@
+import { createError, getQuery, readBody } from 'h3'
+import { z } from 'zod'
+import { deleteCardCodesByIds } from '~~/server/services/cardCodeDeleteService'
+
+const MAX_DELETE_COUNT = 500
+const idSchema = z.preprocess(
+ (value) => (typeof value === 'number' || typeof value === 'string' ? value : undefined),
+ z.coerce.number().int().positive()
+)
+
+const deleteCardCodesSchema = z.object({
+ id: idSchema.optional(),
+ ids: z.preprocess(
+ (value) => {
+ if (typeof value === 'string') {
+ return value.split(',').map((item) => item.trim()).filter(Boolean)
+ }
+ return value
+ },
+ z.array(idSchema).max(MAX_DELETE_COUNT, `鍗曟鏈€澶氬垹闄?${MAX_DELETE_COUNT} 涓偣姝屽埜`).optional()
+ )
+})
+
+export const handleOpenCardCodeDelete = async (event: any) => {
+ const apiKey = event.context.apiKey
+ if (!apiKey) {
+ throw createError({ statusCode: 401, message: 'API璁よ瘉澶辫触' })
+ }
+
+ try {
+ const query = getQuery(event)
+ const body = await readBody(event).catch(() => ({})) ?? {}
+ const validatedData = deleteCardCodesSchema.parse({ ...query, ...body })
+ const rawIds = [
+ ...(validatedData.id !== undefined ? [validatedData.id] : []),
+ ...(validatedData.ids || [])
+ ]
+ const ids = Array.from(new Set(rawIds))
+
+ if (!ids.length) {
+ throw createError({ statusCode: 400, message: '缂哄皯鏈夋晥鐐规瓕鍒窱D' })
+ }
+ if (ids.length > MAX_DELETE_COUNT) {
+ throw createError({ statusCode: 400, message: `鍗曟鏈€澶氬垹闄?${MAX_DELETE_COUNT} 涓偣姝屽埜` })
+ }
+
+ const deletedRows = await deleteCardCodesByIds(ids)
+ return {
+ success: true,
+ message: '鐐规瓕鍒稿垹闄ゆ垚鍔?,
+ data: {
+ cardCodes: deletedRows,
+ deleted: deletedRows.length,
+ requested: ids.length
+ }
+ }
+ } catch (err: any) {
+ if (err.statusCode) throw err
+ if (err.name === 'ZodError') {
+ throw createError({
+ statusCode: 400,
+ message: `璇锋眰鍙傛暟楠岃瘉澶辫触锛?{(err.issues || err.errors).map((issue: any) => issue.message).join(', ')}`
+ })
+ }
+ console.error('寮€鏀続PI鍒犻櫎鐐规瓕鍒稿け璐?, err)
+ throw createError({ statusCode: 500, message: '鍒犻櫎鐐规瓕鍒稿け璐? })
+ }
+}
diff --git a/server/utils/create-auth-session.ts b/server/utils/create-auth-session.ts
new file mode 100644
index 0000000..bd45df1
--- /dev/null
+++ b/server/utils/create-auth-session.ts
@@ -0,0 +1,83 @@
+import { getHeader } from 'h3'
+import { db, eq, and } from '~/drizzle/db'
+import { authSessions } from '~/drizzle/schema'
+import { parseUserAgent, type LoginMethod } from './user-agent-info'
+import { getClientIP } from './ip-utils'
+import { randomUUID } from 'node:crypto'
+
+export interface CreateAuthSessionOptions {
+ userId: number
+ jti: string
+ loginMethod: LoginMethod
+ expiresAt?: Date
+ oauthProvider?: string
+}
+
+const DEFAULT_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000
+
+export async function createAuthSession(
+ event: any,
+ options: CreateAuthSessionOptions
+): Promise<string> {
+ const { userId, jti, loginMethod, oauthProvider } = options
+ const ua = getHeader(event, 'user-agent')
+ const { browser, operatingSystem, device } = parseUserAgent(ua)
+ const ip = getClientIP(event)
+ const now = new Date()
+ const expiresAt = options.expiresAt ?? new Date(now.getTime() + DEFAULT_SESSION_TTL_MS)
+
+ try {
+ let method = loginMethod
+ if (oauthProvider && method === 'oauth') {
+ method = `oauth:${oauthProvider}` as any
+ }
+
+ await db.insert(authSessions).values({
+ id: randomUUID(),
+ userId,
+ sessionToken: jti,
+ browser,
+ operatingSystem,
+ device,
+ loginMethod: method,
+ ipAddress: ip,
+ userAgent: ua ?? null,
+ createdAt: now,
+ lastActiveAt: now,
+ expiresAt,
+ isRevoked: false
+ })
+ } catch (error) {
+ console.warn('[AuthSession] 鍒涘缓浼氳瘽澶辫触锛岀户缁棤浼氳瘽鐧诲綍:', error)
+ }
+
+ return jti
+}
+
+export async function isSessionRevokedByJti(jti: string | undefined): Promise<boolean> {
+ if (!jti) return false
+ try {
+ const row = await db.query.authSessions.findFirst({
+ where: and(eq(authSessions.sessionToken, jti)),
+ columns: { isRevoked: true, expiresAt: true }
+ })
+ if (!row) return false
+ if (row.expiresAt && row.expiresAt.getTime() < Date.now()) return true
+ return Boolean(row.isRevoked)
+ } catch (error) {
+ console.warn('[AuthSession] 浼氳瘽鐘舵€佹牎楠屽け璐ワ紝榛樿鏀捐:', error)
+ return false
+ }
+}
+
+export async function touchAuthSession(jti: string | undefined): Promise<void> {
+ if (!jti) return
+ try {
+ await db
+ .update(authSessions)
+ .set({ lastActiveAt: new Date() })
+ .where(eq(authSessions.sessionToken, jti))
+ } catch (_error) {
+ // ignore
+ }
+}
diff --git a/server/utils/email-verification.ts b/server/utils/email-verification.ts
new file mode 100644
index 0000000..d649217
--- /dev/null
+++ b/server/utils/email-verification.ts
@@ -0,0 +1,171 @@
+import { randomInt, timingSafeEqual } from 'node:crypto'
+import { getServerTimestamp } from '~~/server/utils/serverTime'
+import { getRedisClient, buildRedisKey } from '~~/server/utils/redis'
+
+const EMAIL_CODE_TTL_MS = 5 * 60 * 1000
+const EMAIL_CODE_COOLDOWN_MS = 60 * 1000
+const EMAIL_CODE_MAX_ATTEMPTS = 5
+const EMAIL_CODE_DAILY_LIMIT = 5
+const EMAIL_CODE_DAILY_WINDOW_MS = 24 * 60 * 60 * 1000
+
+type EmailCodeEntry = {
+ code: string
+ expiresAt: number
+ createdAt: number
+ attempts: number
+}
+
+const emailCodes = new Map<string, EmailCodeEntry>()
+const dailySendCounts = new Map<string, { count: number; dayKey: string }>()
+
+const normalizeEmail = (email: string) => email.trim().toLowerCase()
+
+const getDayKey = () => {
+ const now = new Date(getServerTimestamp())
+ return `${now.getUTCFullYear()}-${now.getUTCMonth() + 1}-${now.getUTCDate()}`
+}
+
+const codeRedisKey = (email: string) => buildRedisKey('email-code', 'code', normalizeEmail(email))
+const attemptsRedisKey = (email: string) => buildRedisKey('email-code', 'attempts', normalizeEmail(email))
+const cooldownRedisKey = (email: string) => buildRedisKey('email-code', 'cooldown', normalizeEmail(email))
+const dailyRedisKey = (email: string) => buildRedisKey('email-code', 'daily', normalizeEmail(email), getDayKey())
+
+const VERIFY_EMAIL_CODE_SCRIPT = `
+local stored = redis.call('GET', KEYS[1])
+if not stored then
+ return { 0, 0 }
+end
+local attempts = redis.call('INCR', KEYS[2])
+if attempts == 1 then
+ redis.call('EXPIRE', KEYS[2], 300)
+end
+if ARGV[1] == stored then
+ redis.call('DEL', KEYS[1], KEYS[2])
+ return { 1, attempts }
+end
+if attempts >= tonumber(ARGV[2]) then
+ redis.call('DEL', KEYS[1], KEYS[2])
+ return { 2, attempts }
+end
+return { 0, attempts }
+`
+
+const safeCodeEqual = (a: string, b: string): boolean => {
+ if (a.length !== b.length) return false
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b))
+}
+
+export function generateEmailCode(): string {
+ return String(randomInt(100000, 1000000))
+}
+
+export async function storeEmailCode(email: string, code: string): Promise<void> {
+ const normalized = normalizeEmail(email)
+ const now = getServerTimestamp()
+ emailCodes.set(normalized, {
+ code,
+ expiresAt: now + EMAIL_CODE_TTL_MS,
+ createdAt: now,
+ attempts: 0
+ })
+
+ const dayKey = getDayKey()
+ const record = dailySendCounts.get(normalized)
+ if (!record || record.dayKey !== dayKey) {
+ dailySendCounts.set(normalized, { count: 1, dayKey })
+ } else {
+ record.count++
+ }
+
+ let client: Awaited<ReturnType<typeof getRedisClient>> | null = null
+ try {
+ client = getRedisClient()
+ if (!client) return
+ const ttlSeconds = Math.ceil(EMAIL_CODE_TTL_MS / 1000)
+ await client.set(codeRedisKey(email), code, { EX: ttlSeconds })
+ await client.set(cooldownRedisKey(email), '1', {
+ EX: Math.ceil(EMAIL_CODE_COOLDOWN_MS / 1000)
+ })
+ const dailyKey = dailyRedisKey(email)
+ const dailyCount = await client.incr(dailyKey)
+ if (dailyCount === 1) {
+ await client.expire(dailyKey, Math.ceil(EMAIL_CODE_DAILY_WINDOW_MS / 1000))
+ }
+ } catch (error) {
+ console.error('[EmailCode] Redis 鍐欏叆澶辫触锛屽凡鍥為€€鍐呭瓨瀛樺偍:', error)
+ }
+}
+
+export async function isEmailCodeCooldownActive(email: string): Promise<boolean> {
+ let client: Awaited<ReturnType<typeof getRedisClient>> | null = null
+ try {
+ client = getRedisClient()
+ if (client) {
+ return Boolean(await client.exists(cooldownRedisKey(email)))
+ }
+ } catch (error) {
+ console.error('[EmailCode] Redis 璇诲彇澶辫触锛屽凡鍥為€€鍐呭瓨鍒ゆ柇:', error)
+ }
+ const entry = emailCodes.get(normalizeEmail(email))
+ return Boolean(entry && getServerTimestamp() - entry.createdAt < EMAIL_CODE_COOLDOWN_MS)
+}
+
+export async function isEmailCodeDailyLimitReached(email: string): Promise<boolean> {
+ let client: Awaited<ReturnType<typeof getRedisClient>> | null = null
+ try {
+ client = getRedisClient()
+ if (client) {
+ const count = await client.get(dailyRedisKey(email))
+ return Number(count || 0) >= EMAIL_CODE_DAILY_LIMIT
+ }
+ } catch (error) {
+ console.error('[EmailCode] Redis 璇诲彇澶辫触锛屽凡鍥為€€鍐呭瓨鍒ゆ柇:', error)
+ }
+ const normalized = normalizeEmail(email)
+ const dayKey = getDayKey()
+ const record = dailySendCounts.get(normalized)
+ return Boolean(record && record.dayKey === dayKey && record.count >= EMAIL_CODE_DAILY_LIMIT)
+}
+
+export async function verifyEmailCode(email: string, code: string): Promise<boolean> {
+ const trimmedCode = code.trim()
+ let client
+ try {
+ client = getRedisClient()
+ if (client) {
+ const result = await client.eval(VERIFY_EMAIL_CODE_SCRIPT, {
+ keys: [codeRedisKey(email), attemptsRedisKey(email)],
+ arguments: [trimmedCode, String(EMAIL_CODE_MAX_ATTEMPTS)]
+ })
+ if (Array.isArray(result)) {
+ return Number(result[0]) === 1
+ }
+ }
+ } catch (error) {
+ console.error('[EmailCode] Redis 鏍¢獙澶辫触锛屽凡鍥為€€鍐呭瓨鏍¢獙:', error)
+ }
+
+ const normalized = normalizeEmail(email)
+ const entry = emailCodes.get(normalized)
+ if (!entry) return false
+ if (getServerTimestamp() > entry.expiresAt) {
+ emailCodes.delete(normalized)
+ return false
+ }
+ if (safeCodeEqual(entry.code, trimmedCode)) {
+ emailCodes.delete(normalized)
+ return true
+ }
+ entry.attempts++
+ if (entry.attempts >= EMAIL_CODE_MAX_ATTEMPTS) {
+ emailCodes.delete(normalized)
+ }
+ return false
+}
+
+export function cleanupExpiredEmailCodes(): void {
+ const now = getServerTimestamp()
+ for (const [email, entry] of emailCodes) {
+ if (now > entry.expiresAt) emailCodes.delete(email)
+ }
+}
diff --git a/server/utils/grade-class-core.ts b/server/utils/grade-class-core.ts
new file mode 100644
index 0000000..39234a5
--- /dev/null
+++ b/server/utils/grade-class-core.ts
@@ -0,0 +1,36 @@
+export const smartSort = (a: string, b: string) => {
+ const gradeOrder: Record<string, number> = {
+ '鍒濅竴': 1, '鍒濅簩': 2, '鍒濅笁': 3,
+ '楂樹竴': 4, '楂樹簩': 5, '楂樹笁': 6,
+ '澶т竴': 7, '澶т簩': 8, '澶т笁': 9, '澶у洓': 10,
+ '鏁欏笀': 99, '鏁欒亴宸?: 99
+ }
+
+ const weightA = gradeOrder[a]
+ const weightB = gradeOrder[b]
+
+ if (weightA !== undefined && weightB !== undefined) return weightA - weightB
+ if (weightA !== undefined) return -1
+ if (weightB !== undefined) return 1
+
+ return a.localeCompare(b, 'zh-CN', { numeric: true })
+}
+
+export interface GradeClassOption {
+ grade: string
+ class: string
+}
+
+export const resolveGradeClassOptions = (
+ configItems: GradeClassOption[],
+ fallbackItems: GradeClassOption[]
+): GradeClassOption[] => {
+ const source = configItems.length > 0 ? configItems : fallbackItems
+
+ return source
+ .filter((item): item is GradeClassOption => Boolean(item.grade?.trim()) && Boolean(item.class?.trim()))
+ .sort((a, b) => {
+ const gradeResult = smartSort(a.grade, b.grade)
+ return gradeResult || smartSort(a.class, b.class)
+ })
+}
diff --git a/server/utils/grade-class-options.ts b/server/utils/grade-class-options.ts
new file mode 100644
index 0000000..b7fc19b
--- /dev/null
+++ b/server/utils/grade-class-options.ts
@@ -0,0 +1,45 @@
+import { and, isNotNull, eq, ne, sql } from 'drizzle-orm'
+import { db } from '~/drizzle/db'
+import { users, gradeClasses } from '~/drizzle/schema'
+import { resolveGradeClassOptions, type GradeClassOption } from './grade-class-core'
+
+export async function fetchGradeClassOptions(): Promise<GradeClassOption[]> {
+ const [configRows, userRows] = await Promise.all([
+ db.select({
+ grade: gradeClasses.grade,
+ class: gradeClasses.class
+ }).from(gradeClasses),
+ db
+ .selectDistinct({
+ grade: users.grade,
+ class: users.class
+ })
+ .from(users)
+ .where(and(eq(users.status, 'active'), isNotNull(users.grade), isNotNull(users.class)))
+ ])
+
+ return resolveGradeClassOptions(configRows, userRows)
+}
+
+export async function isGradeClassValid(grade: string, studentClass: string): Promise<boolean> {
+ const configHit = await db.query.gradeClasses.findFirst({
+ where: (t, { eq: eq_, and: and_ }) => and_(eq_(t.grade, grade), eq_(t.class, studentClass)),
+ columns: { id: true }
+ })
+
+ if (configHit) return true
+
+ const configCount = await db
+ .select({ count: () => sql<number>`count(*)::int` })
+ .from(gradeClasses)
+ .where(and(ne(gradeClasses.grade, ''), ne(gradeClasses.class, '')))
+ if ((configCount[0]?.count || 0) > 0) return false
+
+ const userHit = await db.query.users.findFirst({
+ where: (t, { eq: eq_, and: and_ }) =>
+ and_(eq_(t.status, 'active'), eq_(t.grade, grade), eq_(t.class, studentClass)),
+ columns: { id: true }
+ })
+
+ return Boolean(userHit)
+}
diff --git a/server/utils/important-notification-policy.ts b/server/utils/important-notification-policy.ts
new file mode 100644
index 0000000..2c8c273
--- /dev/null
+++ b/server/utils/important-notification-policy.ts
@@ -0,0 +1,72 @@
+export const NOTIFICATION_TITLE_MAX_LENGTH = 200
+export const NOTIFICATION_CONTENT_MAX_LENGTH = 20000
+export const NOTIFICATION_SOURCES = {
+ SYSTEM: 'SYSTEM',
+ ADMIN_MANUAL: 'ADMIN_MANUAL'
+} as const
+
+export type NotificationSenderInput = {
+ id: number
+ name?: string | null
+ username?: string | null
+}
+
+export type NotificationSenderSnapshot = {
+ senderId: number | null
+ senderName: string | null
+ senderUsername: string | null
+}
+
+export const resolveNotificationSource = (sender: NotificationSenderInput | null) =>
+ sender ? NOTIFICATION_SOURCES.ADMIN_MANUAL : NOTIFICATION_SOURCES.SYSTEM
+
+export const createNotificationSenderSnapshot = (
+ sender: NotificationSenderInput | null = null
+): NotificationSenderSnapshot => ({
+ senderId: sender?.id ?? null,
+ senderName: sender?.name?.trim() || null,
+ senderUsername: sender?.username?.trim() || null
+})
+
+export const serializeNotificationSender = (snapshot: NotificationSenderSnapshot) =>
+ snapshot.senderId
+ ? {
+ id: snapshot.senderId,
+ name: snapshot.senderName,
+ username: snapshot.senderUsername
+ }
+ : null
+
+export const shouldCheckImportantNotification = (
+ authenticated: boolean,
+ userId?: number | null,
+ requirePasswordChange = false
+): boolean =>
+ authenticated && Number.isInteger(userId) && Number(userId) > 0 && !requirePasswordChange
+
+export const canSendSystemNotification = (role?: string): boolean =>
+ role === 'ADMIN' || role === 'SUPER_ADMIN'
+
+export const resolveImportantFlag = (value: unknown): boolean | null => {
+ if (value === undefined) return false
+ return typeof value === 'boolean' ? value : null
+}
+
+export const shouldDeliverSystemNotification = (
+ important: boolean,
+ notificationsEnabled?: boolean
+): boolean => important || notificationsEnabled !== false
+
+export const shouldRetainNotificationHistory = (type: string, source: string): boolean =>
+ type === 'SYSTEM_NOTICE' && source === NOTIFICATION_SOURCES.ADMIN_MANUAL
+
+export const createNotificationReadUpdate = (updatedAt = new Date()) => ({
+ read: true as const,
+ updatedAt
+})
+
+export const createNotificationUserDeleteUpdate = (updatedAt = new Date()) => ({
+ userDeleted: true as const,
+ read: true as const,
+ updatedAt
+})
diff --git a/server/utils/initial-password-policy.ts b/server/utils/initial-password-policy.ts
new file mode 100644
index 0000000..c1e379d
--- /dev/null
+++ b/server/utils/initial-password-policy.ts
@@ -0,0 +1,33 @@
+interface InitialPasswordUserState {
+ requirePasswordChange?: boolean | null
+}
+
+interface StoredPasswordState {
+ password?: string | null
+ passwordChangedAt?: Date | string | null
+}
+
+export interface PasswordSetupState {
+ hasSetPassword: boolean
+ needsInitialPasswordSetup: boolean
+}
+
+export function getPasswordSetupState(
+ storedPassword: StoredPasswordState,
+ requirePasswordChange: boolean
+): PasswordSetupState {
+ const hasSetPassword = !!storedPassword.password
+ return {
+ hasSetPassword,
+ needsInitialPasswordSetup:
+ !storedPassword.passwordChangedAt && (requirePasswordChange || !hasSetPassword)
+ }
+}
+
+export function canSetInitialPassword(
+ user: InitialPasswordUserState,
+ storedPassword: StoredPasswordState
+): boolean {
+ return getPasswordSetupState(storedPassword, !!user.requirePasswordChange)
+ .needsInitialPasswordSetup
+}
diff --git a/server/utils/instance-id.ts b/server/utils/instance-id.ts
new file mode 100644
index 0000000..2d7ef39
--- /dev/null
+++ b/server/utils/instance-id.ts
@@ -0,0 +1,112 @@
+import { randomUUID } from 'node:crypto'
+import { asc, eq, sql } from 'drizzle-orm'
+import { db } from '~/drizzle/db'
+import { systemSettings } from '~/drizzle/schema'
+import { SYSTEM_SETTINGS_DEFAULTS } from './system-settings-defaults'
+import { getServerTimestamp } from './serverTime'
+
+export interface InstanceIdInfo {
+ instanceId: string
+ persisted: boolean
+}
+
+let cachedInstanceIdInfo: InstanceIdInfo | null = null
+let cachedInstanceIdExpiresAt = 0
+let pendingInstanceIdPromise: Promise<string> | null = null
+let pendingInstanceIdInfoPromise: Promise<InstanceIdInfo> | null = null
+
+const TEMPORARY_INSTANCE_ID_CACHE_TTL_MS = 5 * 60 * 1000
+
+const loadOrCreateInstanceId = async (): Promise<string> => {
+ const settingsResult = await db
+ .select({ id: systemSettings.id, instanceId: systemSettings.instanceId })
+ .from(systemSettings)
+ .orderBy(asc(systemSettings.id))
+ .limit(1)
+
+ const settings = settingsResult[0]
+
+ if (settings?.instanceId) {
+ return settings.instanceId
+ }
+
+ const instanceId = randomUUID()
+
+ if (!settings) {
+ await db
+ .insert(systemSettings)
+ .values({ id: 1, ...SYSTEM_SETTINGS_DEFAULTS, instanceId })
+ .onConflictDoUpdate({
+ target: systemSettings.id,
+ set: { instanceId, updatedAt: new Date() }
+ })
+
+ await db.execute(sql`
+ SELECT setval(pg_get_serial_sequence('"SystemSettings"', 'id'), (SELECT MAX(id) FROM "SystemSettings"))
+ `)
+ } else {
+ await db
+ .update(systemSettings)
+ .set({ instanceId, updatedAt: new Date() })
+ .where(eq(systemSettings.id, settings.id))
+ }
+
+ return instanceId
+}
+
+const getCachedInstanceIdInfo = (): InstanceIdInfo | null => {
+ if (!cachedInstanceIdInfo) {
+ return null
+ }
+
+ if (cachedInstanceIdInfo.persisted || cachedInstanceIdExpiresAt > getServerTimestamp()) {
+ return cachedInstanceIdInfo
+ }
+
+ return null
+}
+
+export const getInstanceIdInfo = async (): Promise<InstanceIdInfo> => {
+ const cached = getCachedInstanceIdInfo()
+ if (cached) {
+ return cached
+ }
+
+ if (!pendingInstanceIdInfoPromise) {
+ pendingInstanceIdInfoPromise = loadOrCreateInstanceId()
+ .then((instanceId) => {
+ const info = { instanceId, persisted: true }
+ cachedInstanceIdInfo = info
+ cachedInstanceIdExpiresAt = Number.POSITIVE_INFINITY
+ return info
+ })
+ .catch((error) => {
+ const fallbackInstanceId = cachedInstanceIdInfo?.persisted === false
+ ? cachedInstanceIdInfo.instanceId
+ : randomUUID()
+
+ const info = { instanceId: fallbackInstanceId, persisted: false }
+ cachedInstanceIdInfo = info
+ cachedInstanceIdExpiresAt = getServerTimestamp() + TEMPORARY_INSTANCE_ID_CACHE_TTL_MS
+ console.warn('[Instance ID] Failed to persist instance ID, using temporary fallback value:', error)
+ return info
+ })
+ .finally(() => {
+ pendingInstanceIdInfoPromise = null
+ })
+ }
+
+ return pendingInstanceIdInfoPromise
+}
+
+export const getInstanceId = async (): Promise<string> => {
+ if (!pendingInstanceIdPromise) {
+ pendingInstanceIdPromise = getInstanceIdInfo()
+ .then(({ instanceId }) => instanceId)
+ .finally(() => {
+ pendingInstanceIdPromise = null
+ })
+ }
+
+ return pendingInstanceIdPromise
+}
diff --git a/server/utils/jwt-enhanced.ts b/server/utils/jwt-enhanced.ts
index b57e83a..53aaa08 100644
--- a/server/utils/jwt-enhanced.ts
+++ b/server/utils/jwt-enhanced.ts
@@ -38,30 +38,34 @@ export class JWTEnhanced {
}
/**
- * 鐢熸垚JWT token
+ * 鐢熸垚JWT token锛堣繑鍥?token 瀛楃涓诧紝鍏煎鏃ц皟鐢級
*/
static generateToken(userId: number, role: string): string {
+ return this.generateTokenWithJti(userId, role).token
+ }
+
+ /**
+ * 鐢熸垚JWT token 骞惰繑鍥?jti锛堜細璇濊〃鍏宠仈闇€瑕侊級
+ */
+ static generateTokenWithJti(userId: number, role: string): { token: string; jti: string } {
if (!this.JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is not set')
}
- // 鐢熸垚鍞竴鐨凧WT ID
const jti = `${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
- // JWT杞借嵎
const payload: Omit<JWTPayload, 'iat' | 'exp'> = {
userId,
role,
jti
}
- // 鐢熸垚token
const token = jwt.sign(payload, this.JWT_SECRET, {
expiresIn: this.TOKEN_EXPIRES
})
console.log(`Generated JWT token for user ${userId}`)
- return token
+ return { token, jti }
}
/**
diff --git a/server/utils/notification-history-policy.ts b/server/utils/notification-history-policy.ts
new file mode 100644
index 0000000..9457fe1
--- /dev/null
+++ b/server/utils/notification-history-policy.ts
@@ -0,0 +1,108 @@
+export const NOTIFICATION_HISTORY_STATUSES = ['ALL', 'READ', 'UNREAD'] as const
+
+export type NotificationHistoryStatus = (typeof NOTIFICATION_HISTORY_STATUSES)[number]
+
+export const NOTIFICATION_HISTORY_TYPES = ['ALL', 'IMPORTANT', 'NORMAL'] as const
+export const NOTIFICATION_HISTORY_SORT_ORDERS = ['DESC', 'ASC'] as const
+export const NOTIFICATION_HISTORY_KEYWORD_MAX_LENGTH = 100
+
+export type NotificationHistoryType = (typeof NOTIFICATION_HISTORY_TYPES)[number]
+export type NotificationHistorySortOrder = (typeof NOTIFICATION_HISTORY_SORT_ORDERS)[number]
+
+export const resolveNotificationHistoryFilters = (values: {
+ keyword?: unknown
+ type?: unknown
+ sender?: unknown
+ sortOrder?: unknown
+}): {
+ keyword: string
+ type: NotificationHistoryType
+ senderId: number | null
+ sortOrder: NotificationHistorySortOrder
+} | null => {
+ if (values.keyword !== undefined && typeof values.keyword !== 'string') return null
+
+ const keyword = typeof values.keyword === 'string' ? values.keyword.trim() : ''
+ if (keyword.length > NOTIFICATION_HISTORY_KEYWORD_MAX_LENGTH) return null
+
+ if (values.type !== undefined && typeof values.type !== 'string') return null
+ const type = typeof values.type === 'string' && values.type.trim()
+ ? values.type.trim().toUpperCase()
+ : 'ALL'
+ if (!NOTIFICATION_HISTORY_TYPES.includes(type as NotificationHistoryType)) return null
+
+ let senderId: number | null = null
+ if (values.sender !== undefined && values.sender !== null && values.sender !== '') {
+ if (typeof values.sender !== 'string') return null
+ const parsedSenderId = Number(values.sender.trim())
+ if (!Number.isInteger(parsedSenderId) || parsedSenderId <= 0) return null
+ senderId = parsedSenderId
+ }
+
+ if (values.sortOrder !== undefined && typeof values.sortOrder !== 'string') return null
+ const sortOrder = typeof values.sortOrder === 'string' && values.sortOrder.trim()
+ ? values.sortOrder.trim().toUpperCase()
+ : 'DESC'
+ if (!NOTIFICATION_HISTORY_SORT_ORDERS.includes(sortOrder as NotificationHistorySortOrder)) {
+ return null
+ }
+
+ return {
+ keyword,
+ type: type as NotificationHistoryType,
+ senderId,
+ sortOrder: sortOrder as NotificationHistorySortOrder
+ }
+}
+
+export const resolveNotificationHistoryStatus = (
+ value: unknown
+): NotificationHistoryStatus | null => {
+ if (value === undefined || value === null || value === '') return 'ALL'
+ if (typeof value !== 'string') return null
+
+ const normalized = value.trim().toUpperCase()
+ return NOTIFICATION_HISTORY_STATUSES.includes(normalized as NotificationHistoryStatus)
+ ? (normalized as NotificationHistoryStatus)
+ : null
+}
+
+export const resolveNotificationHistoryPagination = (
+ pageValue: unknown,
+ limitValue: unknown
+) => {
+ const parsedPage = Number(pageValue)
+ const parsedLimit = Number(limitValue)
+ const page = Number.isInteger(parsedPage) && parsedPage > 0 ? parsedPage : 1
+ const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 100) : 20
+
+ return {
+ page,
+ limit,
+ offset: (page - 1) * limit
+ }
+}
+
+const UUID_PATTERN =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
+const LEGACY_BATCH_PATTERN = /^legacy-([1-9]\d*)$/
+
+export const resolveNotificationBatchReference = (value: unknown) => {
+ if (typeof value !== 'string') return null
+
+ const normalized = value.trim()
+ const legacyMatch = normalized.match(LEGACY_BATCH_PATTERN)
+ if (legacyMatch) {
+ return {
+ batchId: null,
+ notificationId: Number(legacyMatch[1])
+ }
+ }
+
+ if (!UUID_PATTERN.test(normalized)) return null
+
+ return {
+ batchId: normalized.toLowerCase(),
+ notificationId: null
+ }
+}
diff --git a/server/utils/oauth-identity.ts b/server/utils/oauth-identity.ts
new file mode 100644
index 0000000..1486f74
--- /dev/null
+++ b/server/utils/oauth-identity.ts
@@ -0,0 +1,85 @@
+import { db, eq, userIdentities, users } from '~/drizzle/db'
+import { getBeijingTime } from '~/utils/timeUtils'
+import { resolveAvatarSource } from '~~/server/utils/user-avatar'
+
+type DbTransaction = Parameters<Parameters<typeof db.transaction>[0]>[0]
+
+export interface OAuthIdentityPayload {
+ provider: string
+ providerUserId: string
+ providerUsername?: string | null
+ avatar?: string | null
+}
+
+export interface ExistingOAuthIdentity {
+ id: number
+ avatar: string | null
+}
+
+export async function syncOAuthIdentityAvatar(
+ tx: DbTransaction,
+ user: { id: number; avatarProvider?: string | null; avatarProviderUserId?: string | null },
+ existing: ExistingOAuthIdentity | null | undefined,
+ payload: OAuthIdentityPayload
+): Promise<{ created: boolean; avatarSourceSet: boolean }> {
+ const currentIdentities = await tx.query.userIdentities.findMany({
+ where: (t, { eq: eqUserId }) => eqUserId(t.userId, user.id),
+ columns: {
+ id: true,
+ provider: true,
+ providerUserId: true,
+ providerUsername: true,
+ avatar: true,
+ createdAt: true
+ }
+ })
+ const currentAvatarSource = resolveAvatarSource(user, currentIdentities)
+
+ let created = false
+ if (existing) {
+ if (payload.avatar && existing.avatar !== payload.avatar) {
+ await tx
+ .update(userIdentities)
+ .set({ avatar: payload.avatar })
+ .where(eq(userIdentities.id, existing.id))
+ }
+ } else {
+ await tx.insert(userIdentities).values({
+ userId: user.id,
+ provider: payload.provider,
+ providerUserId: payload.providerUserId,
+ providerUsername: payload.providerUsername,
+ avatar: payload.avatar || null,
+ createdAt: getBeijingTime()
+ })
+ created = true
+ }
+
+ let avatarSourceSet = false
+ if (!currentAvatarSource) {
+ const updatedIdentities = await tx.query.userIdentities.findMany({
+ where: (t, { eq: eqUserId }) => eqUserId(t.userId, user.id),
+ columns: {
+ id: true,
+ provider: true,
+ providerUserId: true,
+ providerUsername: true,
+ avatar: true,
+ createdAt: true
+ }
+ })
+ const nextAvatarSource = resolveAvatarSource(user, updatedIdentities)
+ if (nextAvatarSource) {
+ await tx
+ .update(users)
+ .set({
+ avatarProvider: nextAvatarSource.provider,
+ avatarProviderUserId: nextAvatarSource.providerUserId
+ })
+ .where(eq(users.id, user.id))
+ avatarSourceSet = true
+ }
+ }
+
+ return { created, avatarSourceSet }
+}
diff --git a/server/utils/oauth-providers.ts b/server/utils/oauth-providers.ts
new file mode 100644
index 0000000..594754c
--- /dev/null
+++ b/server/utils/oauth-providers.ts
@@ -0,0 +1,126 @@
+export type OAuthProvider = 'github' | 'casdoor' | 'google' | 'oauth2' | 'aggregate'
+
+export const AGGREGATE_OAUTH_LOGIN_TYPES = [
+ 'qq',
+ 'wx',
+ 'alipay',
+ 'sina',
+ 'baidu',
+ 'douyin',
+ 'huawei',
+ 'xiaomi',
+ 'gitee',
+ 'gitea',
+ 'bilibili',
+ 'kuaishou'
+] as const
+
+export type AggregateOAuthLoginType = (typeof AGGREGATE_OAUTH_LOGIN_TYPES)[number]
+
+const isPrivateIPv4 = (hostname: string): boolean => {
+ const parts = hostname.split('.').map(Number)
+ if (
+ parts.length !== 4 ||
+ parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
+ ) {
+ return false
+ }
+
+ const [first, second] = parts
+ return (
+ first === 10 ||
+ first === 127 ||
+ (first === 100 && second >= 64 && second <= 127) ||
+ (first === 169 && second === 254) ||
+ (first === 172 && second >= 16 && second <= 31) ||
+ (first === 192 && second === 168)
+ )
+}
+
+const isPrivateIPv6 = (hostname: string): boolean => {
+ const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase()
+ if (!normalized.includes(':')) return false
+ if (normalized.startsWith('::ffff:')) {
+ return isPrivateIPv4(normalized.slice('::ffff:'.length))
+ }
+ return (
+ normalized === '::1' ||
+ normalized.startsWith('fc') ||
+ normalized.startsWith('fd') ||
+ /^fe[89ab]/.test(normalized)
+ )
+}
+
+const isInternalHostname = (hostname: string): boolean => {
+ const normalized = hostname.toLowerCase()
+ if (normalized.includes(':')) return isPrivateIPv6(normalized)
+ if (isPrivateIPv4(normalized)) return true
+ return (
+ normalized === 'localhost' ||
+ !normalized.includes('.') ||
+ normalized.endsWith('.localhost') ||
+ normalized.endsWith('.local') ||
+ normalized.endsWith('.lan') ||
+ normalized.endsWith('.internal') ||
+ normalized.endsWith('.home.arpa')
+ )
+}
+
+export const isSafeAggregateOAuthUrl = (value: string): boolean => {
+ try {
+ const url = new URL(value)
+ if (url.username || url.password) return false
+ if (url.protocol === 'https:') return true
+ if (url.protocol !== 'http:') return false
+ return isInternalHostname(url.hostname)
+ } catch {
+ return false
+ }
+}
+
+export const normalizeAggregateOAuthLoginTypes = (value: unknown): AggregateOAuthLoginType[] => {
+ let values: unknown[] = []
+
+ if (Array.isArray(value)) {
+ values = value
+ } else if (typeof value === 'string' && value.trim()) {
+ const normalized = value.trim()
+ try {
+ const parsed = JSON.parse(normalized)
+ values = Array.isArray(parsed) ? parsed : [parsed]
+ } catch {
+ values = normalized.split(',')
+ }
+ }
+
+ return [
+ ...new Set(
+ values
+ .filter((item): item is string => typeof item === 'string')
+ .map((item) => item.trim().toLowerCase())
+ .filter((item): item is AggregateOAuthLoginType =>
+ AGGREGATE_OAUTH_LOGIN_TYPES.includes(item as AggregateOAuthLoginType)
+ )
+ )
+ ]
+}
+
+export const getAggregateOAuthLoginTypesOrDefault = (value: unknown): AggregateOAuthLoginType[] => {
+ const normalized = normalizeAggregateOAuthLoginTypes(value)
+ const hasConfiguredValue =
+ (Array.isArray(value) && value.length > 0) ||
+ (typeof value === 'string' && value.trim().length > 0)
+ return normalized.length > 0 || hasConfiguredValue ? normalized : ['qq']
+}
+
+export const SUPPORTED_OAUTH_PROVIDERS: OAuthProvider[] = [
+ 'github',
+ 'casdoor',
+ 'google',
+ 'oauth2',
+ 'aggregate'
+]
+
+export const isSupportedOAuthProvider = (provider: string): provider is OAuthProvider => {
+ return SUPPORTED_OAUTH_PROVIDERS.includes(provider as OAuthProvider)
+}
diff --git a/server/utils/oauth-strategies.ts b/server/utils/oauth-strategies.ts
index d317281..333c421 100644
--- a/server/utils/oauth-strategies.ts
+++ b/server/utils/oauth-strategies.ts
@@ -1,4 +1,12 @@
import { createError } from 'h3'
+import { createHmac, randomUUID } from 'node:crypto'
+import { db } from '~/drizzle/db'
+import { systemSettings } from '~/drizzle/schema'
+import { asc } from 'drizzle-orm'
+import {
+ isSafeAggregateOAuthUrl,
+ getAggregateOAuthLoginTypesOrDefault
+} from './oauth-providers'
export interface OAuthUserInfo {
id: string
@@ -250,6 +258,139 @@ const strategies: Record<string, OAuthStrategy> = {
google: googleStrategy
}
+async function loadAggregateSettings() {
+ const row = await db
+ .select({
+ aggregateOAuthEnabled: systemSettings.aggregateOAuthEnabled,
+ aggregateOAuthAppId: systemSettings.aggregateOAuthAppId,
+ aggregateOAuthAppKey: systemSettings.aggregateOAuthAppKey,
+ aggregateOAuthLoginType: systemSettings.aggregateOAuthLoginType,
+ aggregateOAuthEndpoint: systemSettings.aggregateOAuthEndpoint
+ })
+ .from(systemSettings)
+ .orderBy(asc(systemSettings.id))
+ .limit(1)
+ return row[0] ?? null
+}
+
+function normalizeEndpoint(endpoint: string): string {
+ return endpoint.replace(/\/+$/, '')
+}
+
+function aggregateSign(params: Record<string, string>, appKey: string): string {
+ const sorted = Object.keys(params)
+ .filter((k) => params[k] !== undefined && params[k] !== null && params[k] !== '')
+ .sort()
+ .map((k) => `${k}=${params[k]}`)
+ .join('&')
+ return createHmac('sha256', appKey).update(sorted).digest('hex')
+}
+
+const aggregateStrategy: OAuthStrategy = {
+ async getAuthorizeUrl(redirectUri: string, state: string) {
+ const settings = await loadAggregateSettings()
+ if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) {
+ throw createError({ statusCode: 500, message: '鑱氬悎鐧诲綍鏈厤缃? })
+ }
+ if (!isSafeAggregateOAuthUrl(settings.aggregateOAuthEndpoint)) {
+ throw createError({ statusCode: 500, message: '鑱氬悎鐧诲綍绔偣涓嶅畨鍏ㄦ垨鏍煎紡閿欒' })
+ }
+
+ const loginTypes = getAggregateOAuthLoginTypesOrDefault(settings.aggregateOAuthLoginType)
+ const loginType = loginTypes[0] || 'qq'
+ const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
+ const timestamp = String(Math.floor(Date.now() / 1000))
+ const nonce = randomUUID().replace(/-/g, '')
+ const baseParams: Record<string, string> = {
+ app_id: settings.aggregateOAuthAppId,
+ response_type: 'code',
+ redirect_uri: redirectUri,
+ state,
+ login_type: loginType,
+ timestamp,
+ nonce
+ }
+ const sign = aggregateSign(baseParams, settings.aggregateOAuthAppKey)
+ const query = new URLSearchParams({ ...baseParams, sign }).toString()
+ return `${endpoint}/oauth/authorize?${query}`
+ },
+
+ async exchangeToken(code: string, redirectUri: string) {
+ const settings = await loadAggregateSettings()
+ if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) {
+ throw createError({ statusCode: 500, message: '鑱氬悎鐧诲綍鏈厤缃? })
+ }
+ const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
+ const timestamp = String(Math.floor(Date.now() / 1000))
+ const nonce = randomUUID().replace(/-/g, '')
+ const baseParams: Record<string, string> = {
+ app_id: settings.aggregateOAuthAppId,
+ grant_type: 'authorization_code',
+ code,
+ redirect_uri: redirectUri,
+ timestamp,
+ nonce
+ }
+ const sign = aggregateSign(baseParams, settings.aggregateOAuthAppKey)
+
+ try {
+ const tokenResponse = await $fetch<any>(`${endpoint}/oauth/token`, {
+ method: 'POST',
+ body: { ...baseParams, sign },
+ headers: { Accept: 'application/json' }
+ })
+ if (tokenResponse.error) {
+ throw new Error(tokenResponse.error_description || tokenResponse.error || '浠ょ墝璇锋眰澶辫触')
+ }
+ const accessToken = tokenResponse.access_token || tokenResponse.accessToken
+ if (!accessToken) {
+ throw new Error('鏈兘鑾峰彇璁块棶浠ょ墝')
+ }
+ return accessToken
+ } catch (e: any) {
+ console.error('Aggregate OAuth token exchange failed', e)
+ throw new Error(e.message || '浠ょ墝璇锋眰澶辫触')
+ }
+ },
+
+ async getUserInfo(accessToken: string) {
+ const settings = await loadAggregateSettings()
+ if (!settings?.aggregateOAuthAppId || !settings?.aggregateOAuthAppKey || !settings?.aggregateOAuthEndpoint) {
+ throw createError({ statusCode: 500, message: '鑱氬悎鐧诲綍鏈厤缃? })
+ }
+ const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
+ const timestamp = String(Math.floor(Date.now() / 1000))
+ const nonce = randomUUID().replace(/-/g, '')
+ const baseParams: Record<string, string> = {
+ app_id: settings.aggregateOAuthAppId,
+ access_token: accessToken,
+ timestamp,
+ nonce
+ }
+ const sign = aggregateSign(baseParams, settings.aggregateOAuthAppKey)
+
+ try {
+ const userInfo = await $fetch<any>(`${endpoint}/oauth/userinfo`, {
+ method: 'POST',
+ body: { ...baseParams, sign },
+ headers: { Accept: 'application/json' }
+ })
+ return {
+ id: String(userInfo.id || userInfo.openid || userInfo.sub || userInfo.userId || ''),
+ username: userInfo.username || userInfo.nickname || userInfo.name || String(userInfo.id || 'user'),
+ name: userInfo.name || userInfo.nickname || undefined,
+ email: userInfo.email || undefined,
+ avatar: userInfo.avatar || userInfo.avatar_url || userInfo.headimgurl || undefined
+ }
+ } catch (e: any) {
+ console.error('Aggregate OAuth user info failed', e)
+ throw new Error('鑾峰彇鐢ㄦ埛淇℃伅澶辫触')
+ }
+ }
+}
+
+strategies.aggregate = aggregateStrategy
+
export const getOAuthStrategy = (provider: string): OAuthStrategy => {
const strategy = strategies[provider]
if (!strategy) {
diff --git a/server/utils/permissions.js b/server/utils/permissions.js
index a7830fd..2a3ac1e 100644
--- a/server/utils/permissions.js
+++ b/server/utils/permissions.js
@@ -85,18 +85,113 @@ function getRoleInfo(roleName) {
return ROLES[roleName] || null
}
-// 妫€鏌ョ敤鎴锋槸鍚﹀彲浠ョ鐞嗗叾浠栫敤鎴疯鑹?+const ROLE_LEVELS = {
+ SUPER_ADMIN: 100,
+ ADMIN: 80,
+ MINISTER: 80,
+ VICE_MINISTER: 70,
+ SONG_ADMIN: 60,
+ STAFF: 60,
+ RESIDENT: 50,
+ TEACHER: 40,
+ USER: 10
+}
+
+export function getRoleLevel(roleName) {
+ if (!roleName) return 0
+ const upperRole = String(roleName).toUpperCase()
+ return ROLE_LEVELS[upperRole] ?? 0
+}
+
+export function isRoleHigherOrEqual(currentRole, targetRole) {
+ return getRoleLevel(currentRole) >= getRoleLevel(targetRole)
+}
+
+export function isRoleHigher(currentRole, targetRole) {
+ return getRoleLevel(currentRole) > getRoleLevel(targetRole)
+}
+
+function canResetPasswordFor(currentUser, targetUser) {
+ if (!currentUser || !currentUser.role || !targetUser) return false
+
+ const currentLevel = getRoleLevel(currentUser.role)
+ const targetLevel = getRoleLevel(targetUser.role)
+
+ if (currentUser.role === 'SUPER_ADMIN') {
+ return true
+ }
+
+ if (currentUser.role === 'ADMIN' || currentUser.role === 'MINISTER') {
+ return targetLevel <= getRoleLevel('USER')
+ }
+
+ return false
+}
+
+function canChangeRoleOf(currentUser, targetUser, newRole) {
+ if (!currentUser || !currentUser.role || !targetUser) return false
+
+ const currentLevel = getRoleLevel(currentUser.role)
+ const targetLevel = getRoleLevel(targetUser.role)
+ const newRoleLevel = getRoleLevel(newRole)
+
+ if (String(currentUser.id) === String(targetUser.id)) {
+ return false
+ }
+
+ if (currentUser.role === 'SUPER_ADMIN') {
+ if (targetUser.id === 1) return false
+ return newRoleLevel <= currentLevel
+ }
+
+ if (currentLevel < targetLevel) {
+ return false
+ }
+
+ if (newRoleLevel >= currentLevel) {
+ return false
+ }
+
+ return true
+}
+
+function canDeleteUser(currentUser, targetUser) {
+ if (!currentUser || !currentUser.role || !targetUser) return false
+
+ if (String(currentUser.id) === String(targetUser.id)) {
+ return false
+ }
+
+ if (targetUser.id === 1) {
+ return false
+ }
+
+ const currentLevel = getRoleLevel(currentUser.role)
+ const targetLevel = getRoleLevel(targetUser.role)
+
+ return currentLevel > targetLevel
+}
+
function canManageUserRole(currentUser, targetRole) {
if (!currentUser || !currentUser.role) return false
- // 鍙湁瓒呯骇绠$悊鍛樺彲浠ュ垎閰嶈鑹? if (currentUser.role === 'SUPER_ADMIN') {
- // 瓒呯骇绠$悊鍛樺彲浠ュ垎閰嶉櫎鑷繁浠ュ鐨勬墍鏈夎鑹? return Object.keys(ROLES).includes(targetRole) && targetRole !== 'SUPER_ADMIN'
}
- return false
+ const currentLevel = getRoleLevel(currentUser.role)
+ const targetLevel = getRoleLevel(targetRole)
+ return targetLevel < currentLevel
}
// 瀵煎嚭鎵€鏈夊嚱鏁板拰甯搁噺
-export { canAccessAdmin, canAccessPage, getUserPages, getRoleInfo, canManageUserRole }
+export {
+ canAccessAdmin,
+ canAccessPage,
+ getUserPages,
+ getRoleInfo,
+ canManageUserRole,
+ canResetPasswordFor,
+ canChangeRoleOf,
+ canDeleteUser
+}
diff --git a/server/utils/redis.ts b/server/utils/redis.ts
index c8bb351..f7f6ce7 100644
--- a/server/utils/redis.ts
+++ b/server/utils/redis.ts
@@ -261,6 +261,12 @@ export const disconnectRedis = () => redisPool.disconnect()
export const getRedisClient = () => redisPool.getClient()
export const isRedisReady = () => redisPool.isReady()
export const getRedisStats = () => redisPool.getStats()
+
+export const REDIS_KEY_PREFIX = 'voicehub'
+
+export function buildRedisKey(...segments: string[]): string {
+ return [REDIS_KEY_PREFIX, ...segments].filter(Boolean).join(':')
+}
export const executeRedisCommand = <T>(command: () => Promise<T>, fallback?: () => Promise<T>) =>
redisPool.executeCommand(command, fallback)
diff --git a/server/utils/requireSongAdmin.ts b/server/utils/requireSongAdmin.ts
new file mode 100644
index 0000000..1a9eeba
--- /dev/null
+++ b/server/utils/requireSongAdmin.ts
@@ -0,0 +1,15 @@
+import { createApiError } from './apiError'
+import { SERVER_ERROR_CODES } from '../config/constants'
+
+const SONG_ADMIN_ROLES = ['SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN']
+
+export function requireSongAdmin(event: any) {
+ const user = event.context?.user
+ if (!user) {
+ throw createApiError(401, SERVER_ERROR_CODES.AUTH_UNAUTHORIZED, '璇峰厛鐧诲綍')
+ }
+ if (!SONG_ADMIN_ROLES.includes(user.role)) {
+ throw createApiError(403, SERVER_ERROR_CODES.AUTH_FORBIDDEN, '娌℃湁鏉冮檺鎵ц姝ゆ搷浣?)
+ }
+ return user
+}
diff --git a/server/utils/schedule-visibility.ts b/server/utils/schedule-visibility.ts
new file mode 100644
index 0000000..3412f33
--- /dev/null
+++ b/server/utils/schedule-visibility.ts
@@ -0,0 +1,52 @@
+export interface ScheduleVisibilityRange {
+ startAt: Date | null
+ endAt: Date | null
+}
+
+export interface VisibilitySettingsLike {
+ scheduleDaysBeforeEnabled?: boolean | null
+ scheduleDaysBefore?: number | null
+ scheduleDaysAfterEnabled?: boolean | null
+ scheduleDaysAfter?: number | null
+}
+
+export function resolveScheduleVisibilityRange(
+ settings: VisibilitySettingsLike | null | undefined,
+ referenceDate = new Date()
+): ScheduleVisibilityRange {
+ const today = new Date(
+ referenceDate.getFullYear(),
+ referenceDate.getMonth(),
+ referenceDate.getDate()
+ )
+
+ let startAt: Date | null = null
+ let endAt: Date | null = null
+
+ if (settings?.scheduleDaysBeforeEnabled) {
+ const days = Math.max(0, Number(settings.scheduleDaysBefore) || 0)
+ const d = new Date(today)
+ d.setDate(d.getDate() - days)
+ startAt = d
+ }
+
+ if (settings?.scheduleDaysAfterEnabled) {
+ const days = Math.max(0, Number(settings.scheduleDaysAfter) || 0)
+ const d = new Date(today)
+ d.setDate(d.getDate() + days + 1)
+ endAt = d
+ }
+
+ return { startAt, endAt }
+}
+
+export function isPlayDateInRange(
+ playDate: Date | string | null | undefined,
+ range: ScheduleVisibilityRange
+): boolean {
+ if (!playDate) return true
+ const d = new Date(playDate)
+ if (range.startAt && d.getTime() < range.startAt.getTime()) return false
+ if (range.endAt && d.getTime() >= range.endAt.getTime()) return false
+ return true
+}
diff --git a/server/utils/serverTime.ts b/server/utils/serverTime.ts
new file mode 100644
index 0000000..d8ac330
--- /dev/null
+++ b/server/utils/serverTime.ts
@@ -0,0 +1,3 @@
+export function getServerTimestamp(): number {
+ return Date.now()
+}
diff --git a/server/utils/submissionNoteAutoModeration.ts b/server/utils/submissionNoteAutoModeration.ts
new file mode 100644
index 0000000..ca3e29c
--- /dev/null
+++ b/server/utils/submissionNoteAutoModeration.ts
@@ -0,0 +1,150 @@
+import type { SongBlacklist } from '~/drizzle/schema'
+
+export type BlacklistType = 'KEYWORD' | 'LANGUAGE' | 'GENRE' | 'SONG'
+
+export interface ModerationContext {
+ title?: string | null
+ artist?: string | null
+ genre?: string | null
+}
+
+export interface ModerationResult {
+ notePublic: boolean
+ publicStatus: 'approved' | 'rejected' | null
+ rejectedReasons: string[]
+ matchedRules: { type: BlacklistType; value: string }[]
+}
+
+function escapeRegex(s: string): string {
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
+function isCJK(text: string): boolean {
+ return /[\u4e00-\u9fa5]/.test(text)
+}
+
+function isLatin(text: string): boolean {
+ return /[A-Za-z]/.test(text)
+}
+
+function isKana(text: string): boolean {
+ return /[\u3040-\u30ff]/.test(text)
+}
+
+function isHangul(text: string): boolean {
+ return /[\uac00-\ud7af]/.test(text)
+}
+
+function isCyrillic(text: string): boolean {
+ return /[\u0400-\u04ff]/.test(text)
+}
+
+function isArabic(text: string): boolean {
+ return /[\u0600-\u06ff]/.test(text)
+}
+
+function detectLanguages(text: string): string[] {
+ const result: string[] = []
+ if (!text) return result
+ const normalized = text.toLowerCase()
+ if (isCJK(text)) result.push('zh', '涓枃', 'chinese', 'cjk')
+ if (isLatin(text)) result.push('en', '鑻辨枃', 'english', 'latin')
+ if (isKana(text)) result.push('ja', '鏃ユ枃', 'japanese')
+ if (isHangul(text)) result.push('ko', '闊╂枃', 'korean')
+ if (isCyrillic(text)) result.push('ru', '淇勬枃', 'russian', 'cyrillic')
+ if (isArabic(text)) result.push('ar', '闃挎媺浼枃', 'arabic')
+ return result
+}
+
+export function moderateSubmissionNote(
+ note: string | null,
+ wantPublic: boolean,
+ requiresApproval: boolean,
+ song: ModerationContext,
+ blacklistRows: SongBlacklist[]
+): ModerationResult {
+ if (!note || !wantPublic) {
+ return {
+ notePublic: wantPublic && !!note,
+ publicStatus: null,
+ rejectedReasons: [],
+ matchedRules: []
+ }
+ }
+
+ const noteLower = note.toLowerCase()
+ const noteForKeyword = note.toLowerCase()
+ const title = (song.title || '').toLowerCase()
+ const artist = (song.artist || '').toLowerCase()
+ const genre = (song.genre || '').toLowerCase()
+
+ const rejectedReasons: string[] = []
+ const matchedRules: { type: BlacklistType; value: string }[] = []
+
+ const noteLanguages = detectLanguages(note)
+ const textLanguages = new Set([
+ ...noteLanguages,
+ ...detectLanguages(song.title || ''),
+ ...detectLanguages(song.artist || ''),
+ ...detectLanguages(song.genre || '')
+ ])
+
+ for (const row of blacklistRows) {
+ const t = row.type as BlacklistType
+ const rawValue = row.value
+ if (!rawValue) continue
+ const value = rawValue.trim()
+ if (!value) continue
+ const valueLower = value.toLowerCase()
+
+ if (t === 'KEYWORD') {
+ // 鍛戒腑 note / title / artist / genre 鐨勫叾涓竴涓兘绠?+ const haystacks = [noteForKeyword, title, artist, genre].filter(Boolean) as string[]
+ const exact = haystacks.some((h) => h.includes(valueLower))
+ const asRegex = (() => {
+ try {
+ const re = new RegExp(escapeRegex(value), 'i')
+ return haystacks.some((h) => re.test(h))
+ } catch (_) {
+ return false
+ }
+ })()
+ if (exact || asRegex) {
+ matchedRules.push({ type: t, value })
+ rejectedReasons.push(`鍛戒腑鏁忔劅璇嶏細${value}`)
+ }
+ } else if (t === 'LANGUAGE') {
+ const v = valueLower
+ const checkList = [
+ v,
+ ...v.split(/[,锛孿/|\s]+/).map((s) => s.trim()).filter(Boolean)
+ ]
+ const hit = checkList.some((lang) => textLanguages.has(lang))
+ if (hit) {
+ matchedRules.push({ type: t, value })
+ rejectedReasons.push(`鍛戒腑璇█闄愬埗锛?{value}`)
+ }
+ } else if (t === 'GENRE') {
+ // 鍖归厤娴佹淳锛歡enre 瀛楁鎴?note/title/artist 涓殑娴佹淳鍏抽敭璇?+ const combinedForGenre = `${genre} ${noteForKeyword} ${title} ${artist}`.trim()
+ if (!combinedForGenre) continue
+ const tokens = valueLower.split(/[,锛孿/|\s]+/).map((s) => s.trim()).filter(Boolean)
+ const hit = tokens.some((tok) => combinedForGenre.includes(tok))
+ if (hit) {
+ matchedRules.push({ type: t, value })
+ rejectedReasons.push(`鍛戒腑娴佹淳闄愬埗锛?{value}`)
+ }
+ } else {
+ // SONG 涓嶅弬涓?note 瀹℃煡
+ continue
+ }
+ }
+
+ const rejected = matchedRules.length > 0
+ return {
+ notePublic: rejected ? false : (requiresApproval ? false : wantPublic),
+ publicStatus: requiresApproval || matchedRules.length > 0 ? (rejected ? 'rejected' : 'approved') : null,
+ rejectedReasons,
+ matchedRules
+ }
+}
diff --git a/server/utils/system-settings-defaults.ts b/server/utils/system-settings-defaults.ts
new file mode 100644
index 0000000..73e2251
--- /dev/null
+++ b/server/utils/system-settings-defaults.ts
@@ -0,0 +1,73 @@
+export const SYSTEM_SETTINGS_DEFAULTS = {
+ siteTitle: '鏍″洯骞挎挱绔欑偣姝岀郴缁?,
+ siteVersion: '1.7.1.0',
+ icpNumber: '绮CP澶?025450093鍙?,
+ gonganNumber: '',
+ enableSubmissionLimit: false,
+ showBlacklistKeywords: false,
+ hideStudentInfo: true,
+ smtpEnabled: false,
+ smtpPort: 587,
+ smtpSecure: false,
+ smtpFromName: '鏍″洯骞挎挱绔?,
+ enableRequestTimeLimitation: false,
+ forceBlockAllRequests: false,
+ enableReplayRequests: false,
+ enableCollaborativeSubmission: true,
+ enableSubmissionRemarks: false,
+ 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,
+ statisticsCode: '',
+ statisticsCodeEnabled: false,
+ defaultTheme: 'ClassicLight',
+ enabledThemes: '["ClassicLight","ModernLight"]'
+} as const
diff --git a/server/utils/url.ts b/server/utils/url.ts
new file mode 100644
index 0000000..ad3c3c5
--- /dev/null
+++ b/server/utils/url.ts
@@ -0,0 +1,41 @@
+/**
+ * 瑙勮寖鍖?URL锛?+ * - trim()銆佸幓鎺夊浣欑┖鏍?+ * - 楠岃瘉蹇呴』鏄?http(s):// 寮€澶?+ * - 闈炴硶 URL 杩斿洖 null 骞惰褰?warning log
+ * - 閰风嫍杩斿洖鐨勭浉瀵硅矾寰?(//xxx) 琛ュ叏涓?https:// 鍗忚澶?+ *
+ * @param rawUrl 鍘熷 URL
+ * @param context 鏃ュ織涓婁笅鏂囷紙濡傛瓕鏇插悕銆丄PI鍚嶏級
+ * @returns 瑙勮寖鍖栧悗鐨?URL锛屾垨 null锛堥潪娉曟垨绌猴級
+ */
+export function normalizeCoverUrl(rawUrl: any, context: string = ''): string | null {
+ if (rawUrl === null || rawUrl === undefined) return null
+
+ let url = String(rawUrl)
+ .trim()
+ .replace(/[\u200B-\u200D\uFEFF]/g, '') // 鍘婚櫎闆跺瀛楃
+ .replace(/\s+/g, '') // 鍘婚櫎鎵€鏈夊唴閮ㄧ┖鏍?+
+ if (!url) return null
+
+ // 閰风嫍/閮ㄥ垎骞冲彴杩斿洖鐨勭浉瀵瑰崗璁矾寰勶細//xxx.xxx.com/... => https://xxx.xxx.com/...
+ if (url.startsWith('//')) {
+ url = 'https:' + url
+ console.warn(`[normalizeCoverUrl] ${context ? context + ' ' : ''}鐩稿鍗忚璺緞宸茶ˉ鍏ㄤ负 https: ${url}`)
+ }
+
+ // 鐩稿璺緞 /xxx 鎴?绾矾寰勪笉鍚堟硶锛岀洿鎺?null
+ if (!/^https?:\/\//i.test(url)) {
+ console.warn(`[normalizeCoverUrl] ${context ? context + ' ' : ''}闈炴硶灏侀潰URL (闈瀐ttp(s)寮€澶?锛屽凡缃负null: raw="${rawUrl}"`)
+ return null
+ }
+
+ try {
+ new URL(url)
+ return url
+ } catch (e: any) {
+ console.warn(`[normalizeCoverUrl] ${context ? context + ' ' : ''}URL瑙f瀽澶辫触锛屽凡缃负null: "${url}" error=${e?.message || e}`)
+ return null
+ }
+}
diff --git a/server/utils/user-agent-info.ts b/server/utils/user-agent-info.ts
new file mode 100644
index 0000000..e33f3e9
--- /dev/null
+++ b/server/utils/user-agent-info.ts
@@ -0,0 +1,71 @@
+export interface UserAgentInfo {
+ browser: string | null
+ operatingSystem: string | null
+ device: string | null
+}
+
+const BROWSER_PATTERNS: Array<[RegExp, string]> = [
+ [/Edg\//i, 'Microsoft Edge'],
+ [/OPR\//i, 'Opera'],
+ [/Chrome\//i, 'Chrome'],
+ [/Safari\//i, 'Safari'],
+ [/Firefox\//i, 'Firefox'],
+ [/MSIE |Trident\//i, 'Internet Explorer']
+]
+
+const OS_PATTERNS: Array<[RegExp, string]> = [
+ [/Windows NT 10\.0/i, 'Windows 10/11'],
+ [/Windows NT 6\.3/i, 'Windows 8.1'],
+ [/Windows NT 6\.2/i, 'Windows 8'],
+ [/Windows NT 6\.1/i, 'Windows 7'],
+ [/Windows/i, 'Windows'],
+ [/Mac OS X ([\d_.]+)/i, 'macOS'],
+ [/iPhone OS ([\d_]+)/i, 'iOS'],
+ [/iPad.*CPU OS ([\d_]+)/i, 'iPadOS'],
+ [/Android ([\d.]+)/i, 'Android'],
+ [/Linux/i, 'Linux']
+]
+
+const DEVICE_PATTERNS: Array<[RegExp, string]> = [
+ [/iPhone/i, 'iPhone'],
+ [/iPad/i, 'iPad'],
+ [/Android.*Mobile/i, 'Android Phone'],
+ [/Android/i, 'Android Tablet'],
+ [/Windows Phone/i, 'Windows Phone'],
+ [/Macintosh/i, 'Mac'],
+ [/Windows/i, 'Windows PC'],
+ [/Linux/i, 'Linux PC']
+]
+
+export function parseUserAgent(userAgent: string | null | undefined): UserAgentInfo {
+ const ua = (userAgent ?? '').trim()
+ if (!ua) return { browser: null, operatingSystem: null, device: null }
+
+ let browser: string | null = null
+ for (const [regex, name] of BROWSER_PATTERNS) {
+ if (regex.test(ua)) {
+ browser = name
+ break
+ }
+ }
+
+ let operatingSystem: string | null = null
+ for (const [regex, name] of OS_PATTERNS) {
+ if (regex.test(ua)) {
+ operatingSystem = name
+ break
+ }
+ }
+
+ let device: string | null = null
+ for (const [regex, name] of DEVICE_PATTERNS) {
+ if (regex.test(ua)) {
+ device = name
+ break
+ }
+ }
+
+ return { browser, operatingSystem, device }
+}
+
+export type LoginMethod = 'password' | 'oauth' | 'webauthn' | 'totp_2fa' | 'email_2fa'
diff --git a/server/utils/user-avatar.ts b/server/utils/user-avatar.ts
new file mode 100644
index 0000000..9cf6fc3
--- /dev/null
+++ b/server/utils/user-avatar.ts
@@ -0,0 +1,122 @@
+interface AvatarUserState {
+ avatarProvider?: string | null
+ avatarProviderUserId?: string | null
+ email?: string | null
+ username?: string | null
+ name?: string | null
+ id?: string | number | null
+}
+
+interface IdentityRow {
+ provider: string
+ providerUserId: string
+ avatar?: string | null
+ createdAt: Date | string
+ providerUsername?: string | null
+}
+
+export interface AvatarSource {
+ provider: string | null
+ providerUserId: string | null
+}
+
+export function resolveAvatarSource(
+ user: AvatarUserState,
+ identities: IdentityRow[]
+): AvatarSource | null {
+ if (user.avatarProvider) {
+ return {
+ provider: user.avatarProvider,
+ providerUserId: user.avatarProviderUserId ?? null
+ }
+ }
+
+ const sorted = [...identities].sort((a, b) => {
+ const at = new Date(a.createdAt).getTime()
+ const bt = new Date(b.createdAt).getTime()
+ return at - bt
+ })
+
+ for (const identity of sorted) {
+ if (identity.avatar) {
+ return {
+ provider: identity.provider,
+ providerUserId: identity.providerUserId
+ }
+ }
+ }
+
+ return null
+}
+
+function md5Hash(str: string): string {
+ let hash = 0
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i)
+ hash = ((hash << 5) - hash) + char
+ hash = hash & hash
+ }
+ return Math.abs(hash).toString(16).padStart(8, '0') + Math.abs(hash * 7).toString(16).padStart(8, '0') +
+ Math.abs(hash * 13).toString(16).padStart(8, '0') + Math.abs(hash * 17).toString(16).padStart(8, '0')
+}
+
+export function generateGravatarUrl(email?: string | null): string | null {
+ if (!email) return null
+ const normalizedEmail = email.trim().toLowerCase()
+ const hash = md5Hash(normalizedEmail)
+ return `https://www.gravatar.com/avatar/${hash}?d=404&s=128`
+}
+
+export function generateDiceBearUrl(seed: string): string {
+ const encodedSeed = encodeURIComponent(seed || 'user')
+ return `https://api.dicebear.com/7.x/initials/svg?seed=${encodedSeed}&backgroundType=gradientLinear&fontSize=40&fontWeight=700`
+}
+
+export function buildAvatarFromSource(source: AvatarSource | null, identities: IdentityRow[] = []): string | null {
+ if (!source || !source.provider) return null
+
+ const identity = identities.find(
+ (id) => id.provider === source.provider && id.providerUserId === source.providerUserId
+ )
+
+ if (identity?.avatar) {
+ return identity.avatar
+ }
+
+ switch (source.provider.toLowerCase()) {
+ case 'github':
+ if (identity?.providerUsername) {
+ return `https://github.com/${identity.providerUsername}.png?size=128`
+ }
+ if (source.providerUserId) {
+ return `https://avatars.githubusercontent.com/u/${source.providerUserId}?size=128`
+ }
+ return null
+ case 'google':
+ return null
+ case 'casdoor':
+ return null
+ default:
+ return null
+ }
+}
+
+export function getUserAvatarUrl(
+ user: AvatarUserState,
+ identities: IdentityRow[] = []
+): string {
+ try {
+ const source = resolveAvatarSource(user, identities)
+ const sourceAvatar = source ? buildAvatarFromSource(source, identities) : null
+ if (sourceAvatar) return sourceAvatar
+
+ const gravatar = generateGravatarUrl(user.email)
+ if (gravatar) return gravatar
+
+ const seed = (user.name || user.username || user.id?.toString() || 'User')
+ return generateDiceBearUrl(seed)
+ } catch {
+ const seed = (user?.name || user?.username || user?.id?.toString() || 'User')
+ return generateDiceBearUrl(seed)
+ }
+}
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)',