Compare commits
105 Commits
8241e95508
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 71d768bd9e | |||
| 3b7c8bbcb2 | |||
| ccfd5be59f | |||
| 96aa507f77 | |||
| 95f614a51e | |||
| bd4091524e | |||
| 5b7f778551 | |||
| 38f64f770d | |||
| 2ee249f6f7 | |||
| e3698a8a60 | |||
| 618ac0bf8c | |||
| 0f58c4ef7c | |||
| 92e298ee4e | |||
| 3df33ae4ce | |||
| fd711a22c8 | |||
| d81cdfe789 | |||
| fd8b07b0d8 | |||
| 394727ccdb | |||
| 993c6d8870 | |||
| f35eb48480 | |||
| 8b25de05dc | |||
| afba56c177 | |||
| d37e2abf0d | |||
| 71dbb921ff | |||
| a38bb5d38a | |||
| 94c515b2de | |||
| 61b299ed15 | |||
| 94004069cb | |||
| 1c85a4abef | |||
| 7d39166ed5 | |||
| ff9401c24e | |||
| 4181e4bd71 | |||
| 5be2e91d0a | |||
| fcac29b7b0 | |||
| 2b24a66dbc | |||
| 92487eac92 | |||
| c1ab737894 | |||
| 3fdd504d01 | |||
| 98a9fc041f | |||
| 006886751f | |||
| 832e55363b | |||
| ea16dbb91d | |||
| fcfd2a9a9f | |||
| 02fc54df21 | |||
| f3113d787d | |||
| a607748df0 | |||
| 731d64b26d | |||
| b6a2c13f72 | |||
| 3dcd4b0581 | |||
| 8ec2f9f097 | |||
| bbf27538b2 | |||
| 56cf0e1de0 | |||
| 83476efe63 | |||
| 49359845c0 | |||
| 170c42cb52 | |||
| 67a427af49 | |||
| e65a2ded46 | |||
| de8761846a | |||
| b0ecc0a514 | |||
| 3d404dc6ab | |||
| f22be971a5 | |||
| e5da524504 | |||
| 2bc3fd541a | |||
| 82aec50e7a | |||
| 314406cd41 | |||
| 13635b2d9d | |||
| 3eb4d5ff80 | |||
| 601425c783 | |||
| cb705c0150 | |||
| 419b3c8625 | |||
| b5a460e440 | |||
| 109e5ec119 | |||
| 22f5197417 | |||
| 854273f6a4 | |||
| fad0450fd1 | |||
| 3ce7e7cee0 | |||
| 5637b002a5 | |||
| eed624e491 | |||
| ed9cbde7be | |||
| 0db0f40ce3 | |||
| dabe5fbb2c | |||
| 61fa7be47d | |||
| 849a7219df | |||
| c6f47e0e10 | |||
| dbaee3d858 | |||
| 8ed24ed44b | |||
| 3d7e13cbc2 | |||
| db0cd8167d | |||
| 611602cfef | |||
| 67eec9ebb4 | |||
| 6c1953a831 | |||
| df01c08734 | |||
| 23385ce3ac | |||
| a65c46cb46 | |||
| 9f716728fa | |||
| 0fba9eac06 | |||
| b0b58333a4 | |||
| df79d6f74e | |||
| cf66053337 | |||
| 7cedf4fba3 | |||
| 09084eef21 | |||
| 8c7d415ae7 | |||
| b6c82e9bc9 | |||
| 6c40fde269 | |||
| 5db1e8b15a |
+734
-117
@@ -1,146 +1,763 @@
|
|||||||
name: Publish Release Assets
|
name: CI
|
||||||
run-name: Build and publish web assets and Docker images
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
- dev
|
||||||
- "apps/**"
|
- cicd
|
||||||
- "packages/**"
|
|
||||||
- "package.json"
|
|
||||||
- "turbo.json"
|
|
||||||
- ".gitea/workflows/*.yml"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
branches:
|
||||||
paths:
|
- main
|
||||||
- "apps/**"
|
- dev
|
||||||
- "packages/**"
|
- cicd
|
||||||
- "package.json"
|
|
||||||
- "turbo.json"
|
|
||||||
- ".gitea/workflows/*.yml"
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Gitea
|
DOMAIN_URL: git.kxsw.us
|
||||||
GIT_USERNAME: ${{ vars.GIT_USERNAME }}
|
REPO: ${{ vars.REPO }}
|
||||||
GIT_PASSWORD: ${{ vars.GIT_PASSWORD }}
|
TELEGRAM_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
||||||
# Docker Hub
|
TELEGRAM_CHAT_ID: "-4940243803"
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_REGISTRY: registry.kxsw.us
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_BUILDKIT: 1
|
||||||
# Host SSH (用于上传构建产物)
|
# Host SSH
|
||||||
SSH_HOST: ${{ vars.SSH_HOST }}
|
SSH_HOST: ${{ vars.SSH_HOST }}
|
||||||
SSH_PORT: ${{ vars.SSH_PORT }}
|
SSH_PORT: ${{ vars.SSH_PORT }}
|
||||||
SSH_USER: ${{ vars.SSH_USER }}
|
SSH_USER: ${{ vars.SSH_USER }}
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
SSH_PASSWORD: ${{ vars.SSH_PASSWORD }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
build:
|
||||||
name: Publish Release Assets
|
runs-on: ppanel-web02
|
||||||
runs-on: ubuntu-latest
|
container:
|
||||||
|
image: node:20
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# 只有node支持版本号别名
|
||||||
|
node: ['20.15.1']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout codebase
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v1
|
|
||||||
with:
|
with:
|
||||||
bun-version: 'latest'
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Cache Bun dependencies
|
- name: 缓存服务健康检查
|
||||||
uses: actions/cache@v3
|
id: cache-health
|
||||||
with:
|
continue-on-error: true
|
||||||
path: |
|
|
||||||
~/.bun
|
|
||||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('**/bun.lockb') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-cache-
|
|
||||||
|
|
||||||
- name: Install deps
|
|
||||||
run: bun install --cache
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: bun run build
|
|
||||||
|
|
||||||
- name: Run publish script
|
|
||||||
run: |
|
run: |
|
||||||
chmod +x scripts/publish.sh
|
echo "检查缓存服务可用性..."
|
||||||
./scripts/publish.sh
|
|
||||||
|
# 设置缓存可用性标志
|
||||||
|
CACHE_AVAILABLE=true
|
||||||
|
|
||||||
|
# 测试GitHub Actions缓存API
|
||||||
|
if ! curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${{ github.repository }}/actions/caches" > /dev/null 2>&1; then
|
||||||
|
echo "⚠️ GitHub Actions缓存服务不可用,将跳过缓存步骤"
|
||||||
|
CACHE_AVAILABLE=false
|
||||||
|
else
|
||||||
|
echo "✅ 缓存服务可用"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "CACHE_AVAILABLE=$CACHE_AVAILABLE" >> $GITHUB_ENV
|
||||||
|
echo "cache-available=$CACHE_AVAILABLE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload tar.gz file to release
|
- name: 缓存降级提示
|
||||||
if: ${{ gitea.event_name == 'release' }}
|
if: env.CACHE_AVAILABLE == 'false'
|
||||||
uses: softprops/action-gh-release@v2
|
run: |
|
||||||
with:
|
echo "🔄 缓存服务不可用,构建将在无缓存模式下进行"
|
||||||
files: |
|
echo "⏱️ 这可能会增加构建时间,但不会影响构建结果"
|
||||||
out/ppanel-admin-web.tar.gz
|
echo "📦 所有依赖将重新下载和安装"
|
||||||
out/ppanel-user-web.tar.gz
|
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Install system tools (jq, docker, curl)
|
||||||
uses: docker/setup-qemu-action@v3
|
run: |
|
||||||
|
set -e
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
echo "Waiting for apt/dpkg locks (unattended-upgrades) to release..."
|
||||||
|
# Wait up to 300s for unattended-upgrades/apt/dpkg locks
|
||||||
|
end=$((SECONDS+300))
|
||||||
|
while true; do
|
||||||
|
LOCKS_BUSY=0
|
||||||
|
# If unattended-upgrades is running, mark busy
|
||||||
|
if pgrep -x unattended-upgrades >/dev/null 2>&1; then LOCKS_BUSY=1; fi
|
||||||
|
# If fuser exists, check common lock files
|
||||||
|
if command -v fuser >/dev/null 2>&1; then
|
||||||
|
if fuser /var/lib/dpkg/lock >/dev/null 2>&1 \
|
||||||
|
|| fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|
||||||
|
|| fuser /var/lib/apt/lists/lock >/dev/null 2>&1; then
|
||||||
|
LOCKS_BUSY=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Break if not busy
|
||||||
|
if [ "$LOCKS_BUSY" -eq 0 ]; then break; fi
|
||||||
|
# Timeout after ~5 minutes
|
||||||
|
if [ $SECONDS -ge $end ]; then
|
||||||
|
echo "Timeout waiting for apt/dpkg locks, proceeding with Dpkg::Lock::Timeout..."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Still waiting for locks..."; sleep 5
|
||||||
|
done
|
||||||
|
apt-get update -y -o Dpkg::Lock::Timeout=600
|
||||||
|
apt-get install -y -o Dpkg::Lock::Timeout=600 jq curl ca-certificates docker.io
|
||||||
|
docker --version
|
||||||
|
jq --version
|
||||||
|
curl --version
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
run: |
|
||||||
|
# Check if buildx is available
|
||||||
|
if docker buildx version >/dev/null 2>&1; then
|
||||||
|
echo "Docker Buildx is available"
|
||||||
|
# Create builder if it doesn't exist
|
||||||
|
if ! docker buildx ls | grep -q "builder"; then
|
||||||
|
docker buildx create --name builder --driver docker-container
|
||||||
|
fi
|
||||||
|
# Use the builder
|
||||||
|
docker buildx use builder
|
||||||
|
docker buildx inspect --bootstrap
|
||||||
|
else
|
||||||
|
echo "Docker Buildx not available, using regular docker build"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Bun
|
||||||
|
run: |
|
||||||
|
echo "=== Installing Bun ==="
|
||||||
|
echo "Current working directory: $(pwd)"
|
||||||
|
echo "Current user: $(whoami)"
|
||||||
|
echo "Home directory: $HOME"
|
||||||
|
|
||||||
|
# 设置Bun安装路径
|
||||||
|
export BUN_INSTALL="$HOME/.bun"
|
||||||
|
echo "BUN_INSTALL=$BUN_INSTALL" >> $GITHUB_ENV
|
||||||
|
echo "PATH=$BUN_INSTALL/bin:${PATH}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 检查缓存是否存在
|
||||||
|
if [ -d "$BUN_INSTALL" ]; then
|
||||||
|
echo "✅ Bun cache found at $BUN_INSTALL"
|
||||||
|
ls -la "$BUN_INSTALL" || true
|
||||||
|
else
|
||||||
|
echo "❌ No Bun cache found, will install fresh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 安装Bun
|
||||||
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
|
# 验证安装
|
||||||
|
"$BUN_INSTALL/bin/bun" --version
|
||||||
|
echo "✅ Bun installed successfully"
|
||||||
|
|
||||||
- name: Install jq
|
- name: Configure npm registry (npmmirror) and canvas mirror
|
||||||
run: sudo apt-get install -y jq
|
run: |
|
||||||
|
echo "registry=https://registry.npmmirror.com" >> .npmrc
|
||||||
|
echo "canvas_binary_host_mirror=https://registry.npmmirror.com/-/binary/canvas" >> .npmrc
|
||||||
|
|
||||||
- name: Extract version from package.json
|
- name: Install dependencies cache (Bun)
|
||||||
id: version
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
run: echo "PPANEL_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_ENV
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
- name: Build and push Docker image for ppanel-admin-web
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
with:
|
||||||
context: .
|
path: ~/.bun
|
||||||
file: ./docker/ppanel-admin-web/Dockerfile
|
key: bun-${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('bun.lock') }}
|
||||||
platforms: linux/amd64,linux/arm64
|
restore-keys: |
|
||||||
push: true
|
bun-${{ runner.os }}-${{ matrix.node }}-
|
||||||
tags: |
|
bun-${{ runner.os }}-
|
||||||
${{ env.DOCKER_USERNAME }}/ppanel-admin-web:latest
|
|
||||||
${{ env.DOCKER_USERNAME }}/ppanel-admin-web:${{ env.PPANEL_VERSION }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image for ppanel-user-web
|
- name: Install dependencies cache (node_modules)
|
||||||
uses: docker/build-push-action@v6
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
context: .
|
|
||||||
file: ./docker/ppanel-user-web/Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_USERNAME }}/ppanel-user-web:latest
|
|
||||||
${{ env.DOCKER_USERNAME }}/ppanel-user-web:${{ env.PPANEL_VERSION }}
|
|
||||||
|
|
||||||
- name: Upload to SSH server (if configured)
|
|
||||||
if: ${{ env.SSH_HOST != '' }}
|
|
||||||
uses: appleboy/ssh-action@v1
|
|
||||||
with:
|
|
||||||
host: ${{ env.SSH_HOST }}
|
|
||||||
username: ${{ env.SSH_USER }}
|
|
||||||
key: ${{ env.SSH_PRIVATE_KEY }}
|
|
||||||
port: ${{ env.SSH_PORT }}
|
|
||||||
script: |
|
|
||||||
mkdir -p ~/releases/${{ gitea.ref_name || 'latest' }}
|
|
||||||
|
|
||||||
- name: Upload files via SCP (if SSH configured)
|
|
||||||
if: ${{ env.SSH_HOST != '' }}
|
|
||||||
uses: appleboy/scp-action@v1
|
|
||||||
with:
|
|
||||||
host: ${{ env.SSH_HOST }}
|
|
||||||
username: ${{ env.SSH_USER }}
|
|
||||||
key: ${{ env.SSH_PRIVATE_KEY }}
|
|
||||||
port: ${{ env.SSH_PORT }}
|
|
||||||
source: "out/*.tar.gz"
|
|
||||||
target: "~/releases/${{ gitea.ref_name || 'latest' }}/"
|
|
||||||
|
|
||||||
- name: Upload artifacts to Gitea
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: ppanel-web-assets
|
|
||||||
path: |
|
path: |
|
||||||
out/ppanel-admin-web.tar.gz
|
node_modules
|
||||||
out/ppanel-user-web.tar.gz
|
apps/*/node_modules
|
||||||
|
packages/*/node_modules
|
||||||
|
key: node-modules-${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('bun.lock', 'package.json', 'apps/*/package.json', 'packages/*/package.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
node-modules-${{ runner.os }}-${{ matrix.node }}-
|
||||||
|
node-modules-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: 缓存状态检查和设置
|
||||||
|
run: |
|
||||||
|
echo "=== 缓存状态检查 ==="
|
||||||
|
echo "检查缓存恢复状态..."
|
||||||
|
|
||||||
|
# 检查各种缓存目录
|
||||||
|
echo "Bun缓存: $([ -d ~/.bun ] && echo '✅ 已发现' || echo '❌ 缺失')"
|
||||||
|
echo "node_modules: $([ -d node_modules ] && echo '✅ 已发现' || echo '❌ 缺失')"
|
||||||
|
echo "Turbo缓存: $([ -d .turbo ] && echo '✅ 已发现' || echo '❌ 缺失')"
|
||||||
|
|
||||||
|
# 显示缓存大小
|
||||||
|
if [ -d ~/.bun ]; then
|
||||||
|
echo "Bun缓存大小: $(du -sh ~/.bun 2>/dev/null || echo '未知')"
|
||||||
|
fi
|
||||||
|
if [ -d node_modules ]; then
|
||||||
|
echo "node_modules大小: $(du -sh node_modules 2>/dev/null || echo '未知')"
|
||||||
|
fi
|
||||||
|
if [ -d .turbo ]; then
|
||||||
|
echo "Turbo缓存大小: $(du -sh .turbo 2>/dev/null || echo '未知')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== 缓存设置 ==="
|
||||||
|
# 确保缓存目录存在且权限正确
|
||||||
|
mkdir -p ~/.bun ~/.cache .turbo
|
||||||
|
chmod -R 755 ~/.bun ~/.cache .turbo 2>/dev/null || true
|
||||||
|
|
||||||
|
# 设置Bun环境变量
|
||||||
|
echo "BUN_INSTALL_CACHE_DIR=$HOME/.cache/bun" >> $GITHUB_ENV
|
||||||
|
echo "BUN_INSTALL_BIN_DIR=$HOME/.bun/bin" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "✅ 缓存目录已准备完成"
|
||||||
|
|
||||||
|
- name: Turborepo cache (.turbo)
|
||||||
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: .turbo
|
||||||
|
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json') }}-${{ hashFiles('apps//package.json') }}-${{ hashFiles('packages//package.json') }}-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
turbo-${{ runner.os }}-${{ hashFiles('turbo.json') }}-${{ hashFiles('apps//package.json') }}-${{ hashFiles('packages//package.json') }}-
|
||||||
|
turbo-${{ runner.os }}-${{ hashFiles('turbo.json') }}-
|
||||||
|
turbo-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: 安装依赖 (bun)
|
||||||
|
run: |
|
||||||
|
echo "=== 依赖安装调试信息 ==="
|
||||||
|
echo "当前目录: $(pwd)"
|
||||||
|
echo "Bun版本: $(bun --version)"
|
||||||
|
|
||||||
|
# 检查node_modules缓存状态
|
||||||
|
if [ -d "node_modules" ]; then
|
||||||
|
echo "✅ 发现node_modules缓存"
|
||||||
|
echo "node_modules大小: $(du -sh node_modules 2>/dev/null || echo '未知')"
|
||||||
|
else
|
||||||
|
echo "❌ 未发现node_modules缓存"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查bun.lock文件
|
||||||
|
if [ -f "bun.lock" ]; then
|
||||||
|
echo "✅ 发现bun.lock文件"
|
||||||
|
else
|
||||||
|
echo "❌ 未发现bun.lock文件"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== 开始安装依赖 ==="
|
||||||
|
echo "安装开始时间: $(date)"
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
echo "安装完成时间: $(date)"
|
||||||
|
|
||||||
|
echo "=== 依赖安装完成 ==="
|
||||||
|
echo "最终node_modules大小: $(du -sh node_modules 2>/dev/null || echo '未知')"
|
||||||
|
|
||||||
|
# 验证缓存效果
|
||||||
|
echo "=== 缓存效果验证 ==="
|
||||||
|
if [ -d "node_modules" ]; then
|
||||||
|
echo "✅ 依赖安装成功"
|
||||||
|
echo "包数量: $(ls node_modules | wc -l 2>/dev/null || echo '未知')"
|
||||||
|
else
|
||||||
|
echo "⚠️ 依赖可能未完全安装"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
- name: Decide build target (admin/user/both)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
||||||
|
BUILD_TARGET="both"
|
||||||
|
if echo "$COMMIT_MSG" | grep -qi "\[admin-only\]"; then
|
||||||
|
BUILD_TARGET="admin"
|
||||||
|
elif echo "$COMMIT_MSG" | grep -qi "\[user-only\]"; then
|
||||||
|
BUILD_TARGET="user"
|
||||||
|
else
|
||||||
|
if git rev-parse HEAD^ >/dev/null 2>&1; then
|
||||||
|
RANGE="HEAD^..HEAD"
|
||||||
|
else
|
||||||
|
RANGE="$(git rev-list --max-parents=0 HEAD)..HEAD"
|
||||||
|
fi
|
||||||
|
CHANGED=$(git diff --name-only $RANGE || true)
|
||||||
|
ADMIN_MATCH=$(echo "$CHANGED" | grep -E '^(apps/admin/|docker/ppanel-admin-web/)' || true)
|
||||||
|
USER_MATCH=$(echo "$CHANGED" | grep -E '^(apps/user/|docker/ppanel-user-web/)' || true)
|
||||||
|
PACKAGE_MATCH=$(echo "$CHANGED" | grep -E '^(packages/|turbo.json|package.json|bun.lock)' || true)
|
||||||
|
if [ -n "$PACKAGE_MATCH" ]; then
|
||||||
|
BUILD_TARGET="both"
|
||||||
|
else
|
||||||
|
if [ -n "$ADMIN_MATCH" ] && [ -z "$USER_MATCH" ]; then BUILD_TARGET="admin"; fi
|
||||||
|
if [ -n "$USER_MATCH" ] && [ -z "$ADMIN_MATCH" ]; then BUILD_TARGET="user"; fi
|
||||||
|
if [ -n "$ADMIN_MATCH" ] && [ -n "$USER_MATCH" ]; then BUILD_TARGET="both"; fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "BUILD_TARGET=$BUILD_TARGET" >> $GITHUB_ENV
|
||||||
|
echo "Decided BUILD_TARGET=$BUILD_TARGET"
|
||||||
|
|
||||||
|
- name: Read version from package.json
|
||||||
|
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 根据分支动态设置API地址
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||||
|
echo "NEXT_PUBLIC_API_URL=https://api.airoport.co" >> $GITHUB_ENV
|
||||||
|
echo "为main分支设置生产环境API地址"
|
||||||
|
elif [ "${{ github.ref_name }}" = "dev" ]; then
|
||||||
|
echo "NEXT_PUBLIC_API_URL=https://api.kxsw.us" >> $GITHUB_ENV
|
||||||
|
echo "为dev分支设置开发环境API地址"
|
||||||
|
else
|
||||||
|
echo "NEXT_PUBLIC_API_URL=https://api.airoport.co" >> $GITHUB_ENV
|
||||||
|
echo "为其他分支设置默认API地址"
|
||||||
|
fi
|
||||||
|
echo "BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache Next.js build artifacts (.next/cache)
|
||||||
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
apps/admin/.next/cache
|
||||||
|
apps/user/.next/cache
|
||||||
|
key: nextcache-${{ runner.os }}-${{ hashFiles('apps//package.json') }}-${{ hashFiles('packages//package.json') }}-${{ hashFiles('turbo.json') }}-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
nextcache-${{ runner.os }}-${{ hashFiles('apps//package.json') }}-${{ hashFiles('packages//package.json') }}-
|
||||||
|
nextcache-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Cache build outputs
|
||||||
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
apps/admin/.next
|
||||||
|
apps/user/.next
|
||||||
|
apps/admin/dist
|
||||||
|
apps/user/dist
|
||||||
|
key: build-${{ runner.os }}-${{ hashFiles('apps//*.ts', 'apps//*.tsx', 'apps//*.js', 'apps//*.jsx') }}-${{ hashFiles('packages//*.ts', 'packages//*.tsx') }}-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
build-${{ runner.os }}-${{ hashFiles('apps//*.ts', 'apps//*.tsx', 'apps//*.js', 'apps//*.jsx') }}-
|
||||||
|
build-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Cache ESLint
|
||||||
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.eslintcache
|
||||||
|
apps/admin/.eslintcache
|
||||||
|
apps/user/.eslintcache
|
||||||
|
key: eslint-${{ runner.os }}-${{ hashFiles('.eslintrc*', 'apps//.eslintrc*', 'packages//.eslintrc*') }}-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
eslint-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Cache TypeScript
|
||||||
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.tsbuildinfo
|
||||||
|
apps/admin/.tsbuildinfo
|
||||||
|
apps/user/.tsbuildinfo
|
||||||
|
packages//.tsbuildinfo
|
||||||
|
key: typescript-${{ runner.os }}-${{ hashFiles('tsconfig*.json', 'apps//tsconfig*.json', 'packages//tsconfig*.json') }}-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
typescript-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: 构建管理面板
|
||||||
|
if: env.BUILD_TARGET == 'admin' || env.BUILD_TARGET == 'both'
|
||||||
|
run: bun run build --filter=ppanel-admin-web
|
||||||
|
|
||||||
|
- name: 构建用户面板
|
||||||
|
if: env.BUILD_TARGET == 'user' || env.BUILD_TARGET == 'both'
|
||||||
|
run: bun run build --filter=ppanel-user-web
|
||||||
|
|
||||||
|
- name: 构建并推送管理面板Docker镜像
|
||||||
|
if: env.BUILD_TARGET == 'admin' || env.BUILD_TARGET == 'both'
|
||||||
|
run: |
|
||||||
|
if docker buildx version >/dev/null 2>&1; then
|
||||||
|
echo "使用docker buildx进行优化构建"
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--cache-from type=registry,ref=${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:cache \
|
||||||
|
--cache-to type=registry,ref=${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:cache,mode=max \
|
||||||
|
-f ./docker/ppanel-admin-web/Dockerfile \
|
||||||
|
-t ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:${{ env.VERSION }} \
|
||||||
|
--push .
|
||||||
|
else
|
||||||
|
echo "使用常规docker构建"
|
||||||
|
docker build -f ./docker/ppanel-admin-web/Dockerfile -t ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:${{ env.VERSION }} .
|
||||||
|
docker push ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:${{ env.VERSION }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 构建并推送用户面板Docker镜像
|
||||||
|
if: env.BUILD_TARGET == 'user' || env.BUILD_TARGET == 'both'
|
||||||
|
run: |
|
||||||
|
if docker buildx version >/dev/null 2>&1; then
|
||||||
|
echo "使用docker buildx进行优化构建"
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--cache-from type=registry,ref=${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:cache \
|
||||||
|
--cache-to type=registry,ref=${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:cache,mode=max \
|
||||||
|
-f ./docker/ppanel-user-web/Dockerfile \
|
||||||
|
-t ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:${{ env.VERSION }} \
|
||||||
|
--push .
|
||||||
|
else
|
||||||
|
echo "使用常规docker构建"
|
||||||
|
docker build -f ./docker/ppanel-user-web/Dockerfile -t ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:${{ env.VERSION }} .
|
||||||
|
docker push ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:${{ env.VERSION }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SSH连接预检查
|
||||||
|
if: env.BUILD_TARGET == 'admin' || env.BUILD_TARGET == 'user' || env.BUILD_TARGET == 'both'
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ env.SSH_HOST }}
|
||||||
|
username: ${{ env.SSH_USER }}
|
||||||
|
password: ${{ env.SSH_PASSWORD }}
|
||||||
|
port: ${{ env.SSH_PORT }}
|
||||||
|
timeout: 300s
|
||||||
|
command_timeout: 600s
|
||||||
|
debug: true
|
||||||
|
script: |
|
||||||
|
echo "=== SSH连接测试 ==="
|
||||||
|
echo "连接时间: $(date)"
|
||||||
|
echo "服务器主机名: $(hostname)"
|
||||||
|
echo "当前用户: $(whoami)"
|
||||||
|
echo "系统信息: $(uname -a)"
|
||||||
|
echo "Docker版本: $(docker --version 2>/dev/null || echo 'Docker未安装')"
|
||||||
|
echo "✅ SSH连接成功"
|
||||||
|
|
||||||
|
- name: 部署管理面板到服务器
|
||||||
|
if: env.BUILD_TARGET == 'admin' || env.BUILD_TARGET == 'both'
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ env.SSH_HOST }}
|
||||||
|
username: ${{ env.SSH_USER }}
|
||||||
|
password: ${{ env.SSH_PASSWORD }}
|
||||||
|
port: ${{ env.SSH_PORT }}
|
||||||
|
timeout: 300s
|
||||||
|
command_timeout: 600s
|
||||||
|
script: |
|
||||||
|
echo "=== SSH变量调试信息 ==="
|
||||||
|
echo "DOCKER_REGISTRY: ${{ env.DOCKER_REGISTRY }}"
|
||||||
|
echo "VERSION: ${{ env.VERSION }}"
|
||||||
|
echo "NEXT_PUBLIC_API_URL: ${{ env.NEXT_PUBLIC_API_URL }}"
|
||||||
|
echo "BRANCH: ${{ env.BRANCH }}"
|
||||||
|
|
||||||
|
echo "=== 部署管理面板 ==="
|
||||||
|
|
||||||
|
# 网络连通性检查
|
||||||
|
echo "检查镜像服务器连通性..."
|
||||||
|
REGISTRY_HOST=$(echo "${{ env.DOCKER_REGISTRY }}" | sed 's|https\?://||' | cut -d'/' -f1)
|
||||||
|
echo "镜像仓库地址: $REGISTRY_HOST"
|
||||||
|
|
||||||
|
if ping -c 3 "$REGISTRY_HOST" > /dev/null 2>&1; then
|
||||||
|
echo "✅ 镜像服务器连通性正常"
|
||||||
|
else
|
||||||
|
echo "⚠️ 镜像服务器ping失败,但继续尝试拉取镜像"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查Docker登录状态
|
||||||
|
echo "检查Docker登录状态..."
|
||||||
|
if docker info > /dev/null 2>&1; then
|
||||||
|
echo "✅ Docker服务正常"
|
||||||
|
else
|
||||||
|
echo "❌ Docker服务异常"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 拉取镜像(带重试)
|
||||||
|
echo "拉取Docker镜像..."
|
||||||
|
for i in {1..3}; do
|
||||||
|
echo "尝试拉取镜像 ($i/3): ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:${{ env.VERSION }}"
|
||||||
|
if docker pull ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:${{ env.VERSION }}; then
|
||||||
|
echo "✅ 镜像拉取成功"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "❌ 镜像拉取失败,重试 $i/3"
|
||||||
|
echo "检查网络和镜像仓库状态..."
|
||||||
|
|
||||||
|
# 显示详细错误信息
|
||||||
|
echo "--- 网络诊断信息 ---"
|
||||||
|
echo "DNS解析测试:"
|
||||||
|
nslookup "$REGISTRY_HOST" || echo "DNS解析失败"
|
||||||
|
echo "网络连通性测试:"
|
||||||
|
ping -c 2 "$REGISTRY_HOST" || echo "ping失败"
|
||||||
|
echo "Docker镜像仓库连接测试:"
|
||||||
|
curl -I "https://$REGISTRY_HOST/v2/" 2>/dev/null || echo "仓库API访问失败"
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
if [ $i -eq 3 ]; then
|
||||||
|
echo "❌ 镜像拉取失败,部署终止"
|
||||||
|
echo "请检查:"
|
||||||
|
echo "1. 网络连接是否正常"
|
||||||
|
echo "2. 镜像仓库是否可访问"
|
||||||
|
echo "3. 镜像标签是否存在"
|
||||||
|
echo "4. Docker登录凭据是否正确"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 安全停止和移除容器
|
||||||
|
echo "检查现有容器状态..."
|
||||||
|
CONTAINER_NAME="ppanel-admin-web"
|
||||||
|
|
||||||
|
# 检查容器是否存在
|
||||||
|
if docker ps -aq -f name=$CONTAINER_NAME | grep -q .; then
|
||||||
|
echo "发现现有容器,开始清理..."
|
||||||
|
|
||||||
|
# 检查容器是否正在运行
|
||||||
|
if docker ps -q -f name=$CONTAINER_NAME | grep -q .; then
|
||||||
|
echo "停止运行中的容器..."
|
||||||
|
docker stop $CONTAINER_NAME --time=15 || true
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查容器是否正在被移除
|
||||||
|
echo "检查容器移除状态..."
|
||||||
|
for i in {1..15}; do
|
||||||
|
# 尝试获取容器状态
|
||||||
|
CONTAINER_STATUS=$(docker inspect $CONTAINER_NAME --format='{{.State.Status}}' 2>/dev/null || echo "not_found")
|
||||||
|
|
||||||
|
if [ "$CONTAINER_STATUS" = "not_found" ]; then
|
||||||
|
echo "✅ 容器已不存在"
|
||||||
|
break
|
||||||
|
elif [ "$CONTAINER_STATUS" = "removing" ]; then
|
||||||
|
echo "⏳ 容器正在移除中,等待完成... $i/15"
|
||||||
|
sleep 3
|
||||||
|
else
|
||||||
|
echo "尝试移除容器... $i/15"
|
||||||
|
if docker rm -f $CONTAINER_NAME 2>/dev/null; then
|
||||||
|
echo "✅ 容器移除成功"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "⚠️ 容器移除失败,重试..."
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 最后一次尝试强制清理
|
||||||
|
if [ $i -eq 15 ]; then
|
||||||
|
echo "🔧 执行强制清理..."
|
||||||
|
docker kill $CONTAINER_NAME 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
docker rm -f $CONTAINER_NAME 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "✅ 未发现现有容器"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "启动新容器..."
|
||||||
|
docker run -d \
|
||||||
|
--name ppanel-admin-web \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3001:3000 \
|
||||||
|
-e NEXT_PUBLIC_API_URL=${{ env.NEXT_PUBLIC_API_URL }} \
|
||||||
|
${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-admin-web:${{ env.VERSION }}
|
||||||
|
|
||||||
|
# 验证容器启动
|
||||||
|
echo "验证容器启动状态..."
|
||||||
|
for i in {1..10}; do
|
||||||
|
if docker ps -q -f name=ppanel-admin-web | grep -q .; then
|
||||||
|
echo "✅ 管理面板部署成功"
|
||||||
|
docker ps -f name=ppanel-admin-web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "等待容器启动... $i/10"
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ 管理面板部署失败 - 容器未能正常启动"
|
||||||
|
docker logs ppanel-admin-web || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: 部署用户面板到服务器
|
||||||
|
if: env.BUILD_TARGET == 'user' || env.BUILD_TARGET == 'both'
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ env.SSH_HOST }}
|
||||||
|
username: ${{ env.SSH_USER }}
|
||||||
|
password: ${{ env.SSH_PASSWORD }}
|
||||||
|
port: ${{ env.SSH_PORT }}
|
||||||
|
timeout: 300s
|
||||||
|
command_timeout: 600s
|
||||||
|
debug: true
|
||||||
|
script: |
|
||||||
|
echo "=== SSH变量调试信息 ==="
|
||||||
|
echo "DOCKER_REGISTRY: ${{ env.DOCKER_REGISTRY }}"
|
||||||
|
echo "VERSION: ${{ env.VERSION }}"
|
||||||
|
echo "NEXT_PUBLIC_API_URL: ${{ env.NEXT_PUBLIC_API_URL }}"
|
||||||
|
echo "BRANCH: ${{ env.BRANCH }}"
|
||||||
|
|
||||||
|
echo "=== 部署用户面板 ==="
|
||||||
|
|
||||||
|
# 网络连通性检查
|
||||||
|
echo "检查镜像服务器连通性..."
|
||||||
|
REGISTRY_HOST=$(echo "${{ env.DOCKER_REGISTRY }}" | sed 's|https\?://||' | cut -d'/' -f1)
|
||||||
|
echo "镜像仓库地址: $REGISTRY_HOST"
|
||||||
|
|
||||||
|
if ping -c 3 "$REGISTRY_HOST" > /dev/null 2>&1; then
|
||||||
|
echo "✅ 镜像服务器连通性正常"
|
||||||
|
else
|
||||||
|
echo "⚠️ 镜像服务器ping失败,但继续尝试拉取镜像"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查Docker登录状态
|
||||||
|
echo "检查Docker登录状态..."
|
||||||
|
if docker info > /dev/null 2>&1; then
|
||||||
|
echo "✅ Docker服务正常"
|
||||||
|
else
|
||||||
|
echo "❌ Docker服务异常"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 拉取镜像(带重试)
|
||||||
|
echo "拉取Docker镜像..."
|
||||||
|
for i in {1..3}; do
|
||||||
|
echo "尝试拉取镜像 ($i/3): ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:${{ env.VERSION }}"
|
||||||
|
if docker pull ${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:${{ env.VERSION }}; then
|
||||||
|
echo "✅ 镜像拉取成功"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "❌ 镜像拉取失败,重试 $i/3"
|
||||||
|
echo "检查网络和镜像仓库状态..."
|
||||||
|
|
||||||
|
# 显示详细错误信息
|
||||||
|
echo "--- 网络诊断信息 ---"
|
||||||
|
echo "DNS解析测试:"
|
||||||
|
nslookup "$REGISTRY_HOST" || echo "DNS解析失败"
|
||||||
|
echo "网络连通性测试:"
|
||||||
|
ping -c 2 "$REGISTRY_HOST" || echo "ping失败"
|
||||||
|
echo "Docker镜像仓库连接测试:"
|
||||||
|
curl -I "https://$REGISTRY_HOST/v2/" 2>/dev/null || echo "仓库API访问失败"
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
if [ $i -eq 3 ]; then
|
||||||
|
echo "❌ 镜像拉取失败,部署终止"
|
||||||
|
echo "请检查:"
|
||||||
|
echo "1. 网络连接是否正常"
|
||||||
|
echo "2. 镜像仓库是否可访问"
|
||||||
|
echo "3. 镜像标签是否存在"
|
||||||
|
echo "4. Docker登录凭据是否正确"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 安全停止和移除容器
|
||||||
|
echo "检查现有容器状态..."
|
||||||
|
CONTAINER_NAME="ppanel-user-web"
|
||||||
|
|
||||||
|
# 检查容器是否存在
|
||||||
|
if docker ps -aq -f name=$CONTAINER_NAME | grep -q .; then
|
||||||
|
echo "发现现有容器,开始清理..."
|
||||||
|
|
||||||
|
# 检查容器是否正在运行
|
||||||
|
if docker ps -q -f name=$CONTAINER_NAME | grep -q .; then
|
||||||
|
echo "停止运行中的容器..."
|
||||||
|
docker stop $CONTAINER_NAME --time=15 || true
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查容器是否正在被移除
|
||||||
|
echo "检查容器移除状态..."
|
||||||
|
for i in {1..15}; do
|
||||||
|
# 尝试获取容器状态
|
||||||
|
CONTAINER_STATUS=$(docker inspect $CONTAINER_NAME --format='{{.State.Status}}' 2>/dev/null || echo "not_found")
|
||||||
|
|
||||||
|
if [ "$CONTAINER_STATUS" = "not_found" ]; then
|
||||||
|
echo "✅ 容器已不存在"
|
||||||
|
break
|
||||||
|
elif [ "$CONTAINER_STATUS" = "removing" ]; then
|
||||||
|
echo "⏳ 容器正在移除中,等待完成... $i/15"
|
||||||
|
sleep 3
|
||||||
|
else
|
||||||
|
echo "尝试移除容器... $i/15"
|
||||||
|
if docker rm -f $CONTAINER_NAME 2>/dev/null; then
|
||||||
|
echo "✅ 容器移除成功"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "⚠️ 容器移除失败,重试..."
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 最后一次尝试强制清理
|
||||||
|
if [ $i -eq 15 ]; then
|
||||||
|
echo "🔧 执行强制清理..."
|
||||||
|
docker kill $CONTAINER_NAME 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
docker rm -f $CONTAINER_NAME 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "✅ 未发现现有容器"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "启动新容器..."
|
||||||
|
docker run -d \
|
||||||
|
--name ppanel-user-web \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3002:3000 \
|
||||||
|
-e NEXT_PUBLIC_API_URL=${{ env.NEXT_PUBLIC_API_URL }} \
|
||||||
|
${{ env.DOCKER_REGISTRY }}/ppanel/ppanel-user-web:${{ env.VERSION }}
|
||||||
|
|
||||||
|
# 验证容器启动
|
||||||
|
echo "验证容器启动状态..."
|
||||||
|
for i in {1..10}; do
|
||||||
|
if docker ps -q -f name=ppanel-user-web | grep -q .; then
|
||||||
|
echo "✅ 用户面板部署成功"
|
||||||
|
docker ps -f name=ppanel-user-web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "等待容器启动... $i/10"
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ 用户面板部署失败 - 容器未能正常启动"
|
||||||
|
docker logs ppanel-user-web || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# 步骤5: TG通知 (成功)
|
||||||
|
- name: 📱 发送成功通知到Telegram
|
||||||
|
if: success()
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
token: ${{ env.TELEGRAM_BOT_TOKEN }}
|
||||||
|
to: ${{ env.TELEGRAM_CHAT_ID }}
|
||||||
|
message: |
|
||||||
|
✅ 部署成功!
|
||||||
|
|
||||||
|
📦 项目: ${{ github.repository }}
|
||||||
|
🌿 分支: ${{ github.ref_name }}
|
||||||
|
🔖 版本: ${{ env.VERSION }}
|
||||||
|
🎯 构建目标: ${{ env.BUILD_TARGET }}
|
||||||
|
🔗 API地址: ${{ env.NEXT_PUBLIC_API_URL }}
|
||||||
|
📝 提交: ${{ github.sha }}
|
||||||
|
👤 提交者: ${{ github.actor }}
|
||||||
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
|
🚀 服务已成功部署到生产环境
|
||||||
|
|
||||||
|
# 步骤5: TG通知 (失败)
|
||||||
|
- name: 📱 发送失败通知到Telegram
|
||||||
|
if: failure()
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
token: ${{ env.TELEGRAM_BOT_TOKEN }}
|
||||||
|
to: ${{ env.TELEGRAM_CHAT_ID }}
|
||||||
|
message: |
|
||||||
|
❌ 部署失败!
|
||||||
|
|
||||||
|
📦 项目: ${{ github.repository }}
|
||||||
|
🌿 分支: ${{ github.ref_name }}
|
||||||
|
🔖 版本: ${{ env.VERSION }}
|
||||||
|
🎯 构建目标: ${{ env.BUILD_TARGET }}
|
||||||
|
📝 提交: ${{ github.sha }}
|
||||||
|
👤 提交者: ${{ github.actor }}
|
||||||
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
|
⚠️ 请检查构建日志获取详细信息
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Makefile for panel-web
|
||||||
|
|
||||||
|
# Docker Registry
|
||||||
|
REGISTRY_URL=registry.kxsw.us
|
||||||
|
|
||||||
|
# Image names
|
||||||
|
ADMIN_IMAGE_NAME=ppanel/ppanel-admin-web
|
||||||
|
USER_IMAGE_NAME=ppanel/ppanel-user-web
|
||||||
|
|
||||||
|
# Full image names with registry
|
||||||
|
ADMIN_IMAGE=${REGISTRY_URL}/${ADMIN_IMAGE_NAME}
|
||||||
|
USER_IMAGE=${REGISTRY_URL}/${USER_IMAGE_NAME}
|
||||||
|
|
||||||
|
# Version from package.json. Requires jq to be installed.
|
||||||
|
VERSION=$(shell jq -r .version package.json)
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: build ## Default target
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Build Targets
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: build-admin build-user ## Build both admin and user docker images
|
||||||
|
|
||||||
|
.PHONY: build-admin
|
||||||
|
build-admin: ## Build the admin docker image
|
||||||
|
@echo "Building admin image with tags ${ADMIN_IMAGE}:${VERSION} and ${ADMIN_IMAGE}:latest..."
|
||||||
|
docker build -f ./docker/ppanel-admin-web/Dockerfile -t ${ADMIN_IMAGE}:${VERSION} -t ${ADMIN_IMAGE}:latest .
|
||||||
|
@echo "Successfully built admin image."
|
||||||
|
|
||||||
|
.PHONY: build-user
|
||||||
|
build-user: ## Build the user docker image
|
||||||
|
@echo "Building user image with tags ${USER_IMAGE}:${VERSION} and ${USER_IMAGE}:latest..."
|
||||||
|
docker build -f ./docker/ppanel-user-web/Dockerfile -t ${USER_IMAGE}:${VERSION} -t ${USER_IMAGE}:latest .
|
||||||
|
@echo "Successfully built user image."
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Publish Targets
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
.PHONY: push
|
||||||
|
push: push-admin push-user ## Push both admin and user images to the registry
|
||||||
|
|
||||||
|
.PHONY: push-admin
|
||||||
|
push-admin: ## Push the admin image to the registry
|
||||||
|
@echo "Pushing admin image ${ADMIN_IMAGE}:${VERSION} and ${ADMIN_IMAGE}:latest..."
|
||||||
|
docker push ${ADMIN_IMAGE}:${VERSION}
|
||||||
|
docker push ${ADMIN_IMAGE}:latest
|
||||||
|
@echo "Successfully pushed admin image."
|
||||||
|
|
||||||
|
.PHONY: push-user
|
||||||
|
push-user: ## Push the user image to the registry
|
||||||
|
@echo "Pushing user image ${USER_IMAGE}:${VERSION} and ${USER_IMAGE}:latest..."
|
||||||
|
docker push ${USER_IMAGE}:${VERSION}
|
||||||
|
docker push ${USER_IMAGE}:latest
|
||||||
|
@echo "Successfully pushed user image."
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Run Targets
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
.PHONY: ensure-version
|
||||||
|
ensure-version:
|
||||||
|
@if [ -z "${VERSION}" ] || [ "${VERSION}" = "null" ]; then \
|
||||||
|
echo "ERROR: 未检测到版本号。请安装 jq 并在 panel-web/package.json 中设置有效的 version 字段。"; \
|
||||||
|
echo "提示:可以运行 'make version' 查看当前版本值。"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Other Targets
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
.PHONY: login
|
||||||
|
login: ## Login to the Docker registry
|
||||||
|
docker login ${REGISTRY_URL}
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show this help message
|
||||||
|
@echo "Usage: make [target]"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
.PHONY: version
|
||||||
|
version: ## Print project version from package.json
|
||||||
|
@echo "${VERSION}"
|
||||||
@@ -15,7 +15,7 @@ unset APP_NAME # 显式销毁变量
|
|||||||
1. 执行 `./scripts/publish-dev.sh` 构建产物在 out-dev文件夹
|
1. 执行 `./scripts/publish-dev.sh` 构建产物在 out-dev文件夹
|
||||||
2. 上传out/zip.gz到服务器
|
2. 上传out/zip.gz到服务器
|
||||||
3. tar -xzvf ppanel-user-web-dev.tar.gz 解压
|
3. tar -xzvf ppanel-user-web-dev.tar.gz 解压
|
||||||
4. tar -xzvf ppanel-user-web-dev.tar.gz
|
4. tar -xzvf ppanel-admin-web-dev.tar.gz
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 第一次启动
|
# 第一次启动
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=zh-CN
|
|||||||
|
|
||||||
# Site URL and API URL
|
# Site URL and API URL
|
||||||
NEXT_PUBLIC_SITE_URL=https://admin.ppanel.dev
|
NEXT_PUBLIC_SITE_URL=https://admin.ppanel.dev
|
||||||
NEXT_PUBLIC_API_URL=https://api.kxsw.us
|
NEXT_PUBLIC_API_URL=https://api.airoport.co
|
||||||
|
|
||||||
# Default Login User
|
# Default Login User
|
||||||
NEXT_PUBLIC_DEFAULT_USER_EMAIL=
|
NEXT_PUBLIC_DEFAULT_USER_EMAIL=
|
||||||
|
|||||||
@@ -20,16 +20,23 @@ import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
|||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { UserDetail } from './user-detail';
|
import { UserDetail } from './user-detail';
|
||||||
import UserForm from './user-form';
|
import UserForm from './user-form';
|
||||||
|
|
||||||
// 新的子组件,现在管理它自己的备注状态
|
// 为 RemarkForm 组件定义 props 类型
|
||||||
const RemarkForm = ({ onSave, initialRemark, CloseComponent }) => {
|
interface RemarkFormProps {
|
||||||
const [remark, setRemark] = useState(initialRemark);
|
initialRemark?: string | null;
|
||||||
|
onSave: (remark: string) => void;
|
||||||
|
CloseComponent: React.ComponentType<{ asChild?: boolean; children: React.ReactNode }>;
|
||||||
|
}
|
||||||
|
|
||||||
const handleInputChange = (event) => {
|
// 新的子组件,在管理它自己的备注状态
|
||||||
|
const RemarkForm: React.FC<RemarkFormProps> = ({ onSave, initialRemark, CloseComponent }) => {
|
||||||
|
const [remark, setRemark] = useState<string>(initialRemark ?? '');
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setRemark(event.target.value);
|
setRemark(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -59,11 +59,6 @@ export default function Page() {
|
|||||||
accessorKey: 'title',
|
accessorKey: 'title',
|
||||||
header: t('title'),
|
header: t('title'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'user_id',
|
|
||||||
header: t('user'),
|
|
||||||
cell: ({ row }) => <UserDetail id={row.original.user_id} />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
header: t('status.0'),
|
header: t('status.0'),
|
||||||
@@ -79,17 +74,22 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t(`status.${row.original.status}`)}
|
{row.original.status === 4 ? t(`status.${row.original.status}`) : t(`status.1`)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'user_id',
|
||||||
|
header: t('user'),
|
||||||
|
cell: ({ row }) => <UserDetail id={row.original.user_id} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'updated_at',
|
accessorKey: 'updated_at',
|
||||||
header: t('updatedAt'),
|
header: t('updatedAt'),
|
||||||
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
params={[
|
/*params={[
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
placeholder: t('status.0'),
|
placeholder: t('status.0'),
|
||||||
@@ -100,11 +100,12 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}
|
]}*/
|
||||||
request={async (pagination, filters) => {
|
request={async (pagination, filters) => {
|
||||||
const { data } = await getTicketList({
|
const { data } = await getTicketList({
|
||||||
...pagination,
|
...pagination,
|
||||||
...filters,
|
...filters,
|
||||||
|
issue_type: 1,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
list: data.data?.list || [],
|
list: data.data?.list || [],
|
||||||
@@ -166,11 +167,11 @@ export default function Page() {
|
|||||||
<div className={cn('bg-accent w-fit rounded-lg p-2 font-medium', {})}>
|
<div className={cn('bg-accent w-fit rounded-lg p-2 font-medium', {})}>
|
||||||
<div> 提现金额:{ticket?.title?.split('-')[1]}</div>
|
<div> 提现金额:{ticket?.title?.split('-')[1]}</div>
|
||||||
<div> 提现方式:{ticket?.description?.split('-')[0]}</div>
|
<div> 提现方式:{ticket?.description?.split('-')[0]}</div>
|
||||||
{ticket?.description?.split('-')[1].includes('data:image') ? (
|
{ticket?.description?.split('-')[1]?.includes('data:image') ? (
|
||||||
<div>
|
<div>
|
||||||
<div>收款码:</div>
|
<div>收款码:</div>
|
||||||
<NextImage
|
<NextImage
|
||||||
src={ticket?.description?.split('-')[1]}
|
src={ticket?.description?.split('-')[1]!}
|
||||||
width={300}
|
width={300}
|
||||||
height={300}
|
height={300}
|
||||||
className='!size-auto object-cover'
|
className='!size-auto object-cover'
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import { getLocale, getMessages } from 'next-intl/server';
|
|||||||
import { PublicEnvScript } from 'next-runtime-env';
|
import { PublicEnvScript } from 'next-runtime-env';
|
||||||
import { unstable_noStore as noStore } from 'next/cache';
|
import { unstable_noStore as noStore } from 'next/cache';
|
||||||
// import { Geist, Geist_Mono } from 'next/font/google';
|
// import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import NextTopLoader from 'nextjs-toploader';
|
import NextTopLoader from 'nextjs-toploader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
// const fontSans = Geist({
|
// const fontSans = Geist({
|
||||||
// subsets: ['latin'],
|
// subsets: ['latin'],
|
||||||
@@ -112,7 +114,7 @@ export default async function RootLayout({
|
|||||||
<body
|
<body
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
// ${fontSans.variable} ${fontMono.variable}
|
// ${fontSans.variable} ${fontMono.variable}
|
||||||
className={`size-full min-h-[calc(100dvh-env(safe-area-inset-top))] font-sans antialiased`}
|
className={`size-full min-h-[calc(100dvh-env(safe-area-inset-top))] ${inter.className} antialiased`}
|
||||||
>
|
>
|
||||||
<NextIntlClientProvider messages={messages}>
|
<NextIntlClientProvider messages={messages}>
|
||||||
<NextTopLoader showSpinner={false} />
|
<NextTopLoader showSpinner={false} />
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const navs = [
|
|||||||
{
|
{
|
||||||
title: 'Withdraw Ticket Management',
|
title: 'Withdraw Ticket Management',
|
||||||
url: '/dashboard/withdraw-ticket',
|
url: '/dashboard/withdraw-ticket',
|
||||||
icon: 'flat-color-icons:collaboration',
|
icon: 'flat-color-icons:debt',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Document Management',
|
title: 'Document Management',
|
||||||
|
|||||||
+1
@@ -864,6 +864,7 @@ declare namespace API {
|
|||||||
user_id?: number;
|
user_id?: number;
|
||||||
status?: number;
|
status?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
issue_type?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetTicketListRequest = {
|
type GetTicketListRequest = {
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=zh-CN
|
|||||||
|
|
||||||
# Site URL and API URL
|
# Site URL and API URL
|
||||||
NEXT_PUBLIC_SITE_URL=https://user.ppanel.dev
|
NEXT_PUBLIC_SITE_URL=https://user.ppanel.dev
|
||||||
NEXT_PUBLIC_API_URL=https://api.kxsw.us
|
NEXT_PUBLIC_API_URL=https://api.airoport.co
|
||||||
NEXT_PUBLIC_CDN_URL=https://cdn.jsdelivr.net
|
NEXT_PUBLIC_CDN_URL=https://cdn.jsdelivr.net
|
||||||
|
|
||||||
# Home Page Settings
|
# Home Page Settings
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US
|
|||||||
|
|
||||||
# Site URL and API URL
|
# Site URL and API URL
|
||||||
NEXT_PUBLIC_SITE_URL=https://user.ppanel.dev
|
NEXT_PUBLIC_SITE_URL=https://user.ppanel.dev
|
||||||
NEXT_PUBLIC_API_URL=https://api.kxsw.us
|
NEXT_PUBLIC_API_URL=https://airoport.co
|
||||||
NEXT_PUBLIC_CDN_URL=https://cdn.jsdelivr.net
|
NEXT_PUBLIC_CDN_URL=https://cdn.jsdelivr.net
|
||||||
|
|
||||||
# Home Page Settings
|
# Home Page Settings
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import useGlobalStore from '@/config/use-global';
|
|||||||
import { getStat } from '@/services/common/common';
|
import { getStat } from '@/services/common/common';
|
||||||
import { queryUserSubscribe } from '@/services/user/user';
|
import { queryUserSubscribe } from '@/services/user/user';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { AiroButton } from '@workspace/airo-ui/components/AiroButton';
|
||||||
import { Button } from '@workspace/airo-ui/components/button';
|
import { Button } from '@workspace/airo-ui/components/button';
|
||||||
import { Card } from '@workspace/airo-ui/components/card';
|
import { Card } from '@workspace/airo-ui/components/card';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
PopupRef,
|
PopupRef,
|
||||||
} from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Popup';
|
} from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Popup';
|
||||||
import { Empty } from '@/components/empty';
|
import { Empty } from '@/components/empty';
|
||||||
|
import SvgIcon from '@/components/SvgIcon';
|
||||||
import { queryAnnouncement } from '@/services/user/announcement';
|
import { queryAnnouncement } from '@/services/user/announcement';
|
||||||
import { queryOrderList } from '@/services/user/order';
|
import { queryOrderList } from '@/services/user/order';
|
||||||
import { default as Airo_Empty } from '@workspace/airo-ui/custom-components/empty';
|
import { default as Airo_Empty } from '@workspace/airo-ui/custom-components/empty';
|
||||||
@@ -91,11 +93,120 @@ export default function Content() {
|
|||||||
|
|
||||||
const popupRef = useRef<PopupRef>(null);
|
const popupRef = useRef<PopupRef>(null);
|
||||||
const dialogRef = useRef<DialogRef>(null);
|
const dialogRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
|
function openPopupWindow(item) {
|
||||||
|
// features 字符串用于控制窗口的特性
|
||||||
|
const pageWidth = 600; // 弹出窗口的宽度
|
||||||
|
const pageHeight = 550; // 弹出窗口的高度
|
||||||
|
const {
|
||||||
|
availLeft, // 返回浏览器可用空间左边距离屏幕(系统桌面)左边界的距离。
|
||||||
|
availHeight, // 浏览器在显示屏上的可用高度,即当前屏幕高度
|
||||||
|
availWidth, // 浏览器在显示屏上的可用宽度,即当前屏幕宽度
|
||||||
|
} = window.screen;
|
||||||
|
const pageTop = (availHeight - pageHeight) / 2; // 窗口的垂直位置
|
||||||
|
let pageLeft = (availWidth - pageWidth) / 2; // 窗口的水平位置;
|
||||||
|
|
||||||
|
pageLeft += availLeft; // 加上屏幕偏移值
|
||||||
|
|
||||||
|
const features = `width=${pageWidth},height=${pageHeight},left=${pageLeft},top=${pageTop},toolbar=no,location=no,menubar=no`;
|
||||||
|
console.log(features);
|
||||||
|
window.open(item.download, item.title, features);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'grid grid-cols-1 gap-[10px] sm:gap-6 lg:grid-cols-2'}>
|
<div className={'grid grid-cols-1 gap-[10px] sm:gap-5 2xl:grid-cols-4'}>
|
||||||
|
{/* 快捷下载 Card */}
|
||||||
|
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)] 2xl:order-2 2xl:col-span-1'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>快捷下载</h3>
|
||||||
|
</div>
|
||||||
|
<div className={'text-xs font-normal leading-[1.5] text-[#666]'}>
|
||||||
|
选择对应操作系统下载客户端
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mt-2.5 flex items-center justify-around rounded-2xl bg-[#EAEAEA] px-4 pb-3 pt-[18px] text-xs text-[#0F2C53]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
label: 'iOS',
|
||||||
|
icon: 'Group 77',
|
||||||
|
href: 'https://apps.apple.com/us/app/shadowrocket/id932747118?l=zh-Hans-CN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mac',
|
||||||
|
icon: 'Group 77',
|
||||||
|
href: 'https://apps.apple.com/us/app/shadowrocket/id932747118?l=zh-Hans-CN',
|
||||||
|
},
|
||||||
|
{ label: 'Win', icon: 'Group 75', href: '' },
|
||||||
|
{ label: 'Android', icon: 'Group 75', href: '' },
|
||||||
|
].map((v) => {
|
||||||
|
return (
|
||||||
|
<a href={v.href} target={'_blank'} className={'cursor-pointer text-center'}>
|
||||||
|
<div className={''}>
|
||||||
|
<SvgIcon name={v.icon}></SvgIcon>
|
||||||
|
</div>
|
||||||
|
{v.label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mt-2.5 flex h-[37px] items-center justify-between rounded-full bg-[#EAEAEA] pl-4 text-[#666]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={'text-sm font-medium'}>免费美区Apple ID</span>
|
||||||
|
<AiroButton
|
||||||
|
className={'m-1'}
|
||||||
|
variant={'primary'}
|
||||||
|
onClick={() => {
|
||||||
|
openPopupWindow({
|
||||||
|
download: 'https://aunlock.laomaos.com/share/DEXVzMSP',
|
||||||
|
title: 'Shadowrocket',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
获取
|
||||||
|
</AiroButton>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 我的订阅 Card */}
|
||||||
|
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)] 2xl:order-3 2xl:col-span-2'>
|
||||||
|
<div className=''>
|
||||||
|
<div className={'flex items-center justify-between'}>
|
||||||
|
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
|
||||||
|
{t('mySubscription')}
|
||||||
|
</h3>
|
||||||
|
<Link
|
||||||
|
href={'/document'}
|
||||||
|
className='border-0 bg-transparent p-0 text-sm font-semibold text-[#225BA9] shadow-none outline-0 hover:bg-transparent sm:font-normal'
|
||||||
|
>
|
||||||
|
{t('beginnerTutorial')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className={'text-xs font-light leading-[1.5] text-[#666]'}>
|
||||||
|
复制订阅链接或点击二维码按钮扫码
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{userSubscribe?.[0] && data?.protocol ? (
|
||||||
|
<SubScribeCard
|
||||||
|
userSubscribeData={userSubscribe?.[0]}
|
||||||
|
protocol={data.protocol}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 账户概况 Card */}
|
{/* 账户概况 Card */}
|
||||||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)] 2xl:order-1 2xl:col-span-1'>
|
||||||
<div className='mb-1'>
|
<div className='mb-1'>
|
||||||
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
|
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
|
||||||
{t('accountOverview')}
|
{t('accountOverview')}
|
||||||
@@ -120,7 +231,7 @@ export default function Content() {
|
|||||||
<span className='text-sm font-light text-[#666666]'>{t('accountBalance')}</span>
|
<span className='text-sm font-light text-[#666666]'>{t('accountBalance')}</span>
|
||||||
<Recharge
|
<Recharge
|
||||||
className={
|
className={
|
||||||
'min-w-[50px] border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
|
'min-w-[50px] border-0 bg-transparent p-0 text-sm font-semibold text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +242,7 @@ export default function Content() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 套餐状态 Card */}
|
{/* 套餐状态 Card */}
|
||||||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 text-[#666666] shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
<Card className='2xl:order-0 rounded-[20px] border border-[#D9D9D9] p-6 text-[#666666] shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)] 2xl:col-span-2'>
|
||||||
<div className=''>
|
<div className=''>
|
||||||
<h3 className='flex items-center justify-between text-[#666666]'>
|
<h3 className='flex items-center justify-between text-[#666666]'>
|
||||||
<div className={'flex items-center justify-between'}>
|
<div className={'flex items-center justify-between'}>
|
||||||
@@ -224,8 +335,9 @@ export default function Content() {
|
|||||||
<Airo_Empty className={'py-0'} description={t('noPlanAvailable')} />
|
<Airo_Empty className={'py-0'} description={t('noPlanAvailable')} />
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 网站公告 Card */}
|
{/* 网站公告 Card */}
|
||||||
<Card className='relative order-4 rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 pb-0 sm:order-none'>
|
<Card className='relative rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 pb-0 2xl:order-4 2xl:col-span-2'>
|
||||||
<div className='mb-3 flex items-center justify-between sm:mb-1'>
|
<div className='mb-3 flex items-center justify-between sm:mb-1'>
|
||||||
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
|
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
|
||||||
{t('siteAnnouncements')}
|
{t('siteAnnouncements')}
|
||||||
@@ -282,31 +394,6 @@ export default function Content() {
|
|||||||
<Popup ref={popupRef} />
|
<Popup ref={popupRef} />
|
||||||
<AnnouncementDialog ref={dialogRef} />
|
<AnnouncementDialog ref={dialogRef} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 我的订阅 Card */}
|
|
||||||
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
|
|
||||||
{t('mySubscription')}
|
|
||||||
</h3>
|
|
||||||
<Link
|
|
||||||
href={'/document'}
|
|
||||||
className='border-0 bg-transparent p-0 text-sm font-semibold text-[#225BA9] shadow-none outline-0 hover:bg-transparent sm:font-normal'
|
|
||||||
>
|
|
||||||
{t('beginnerTutorial')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{userSubscribe?.[0] && data?.protocol ? (
|
|
||||||
<SubScribeCard
|
|
||||||
userSubscribeData={userSubscribe?.[0]}
|
|
||||||
protocol={data.protocol}
|
|
||||||
refetch={refetch}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Empty />
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const WalletDialog: WalletDialogProps = (props) => {
|
|||||||
const { currency } = common;
|
const { currency } = common;
|
||||||
|
|
||||||
// 定义支持的账户类型
|
// 定义支持的账户类型
|
||||||
const ACCOUNT_TYPE = ['USDT', '微信', '支付宝'] as const;
|
const ACCOUNT_TYPE = ['USDT(TRC20)', '微信', '支付宝'] as const;
|
||||||
|
|
||||||
// 根据账户类型定义 Zod 验证模式
|
// 根据账户类型定义 Zod 验证模式
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
@@ -76,11 +76,11 @@ const WalletDialog: WalletDialogProps = (props) => {
|
|||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
// 根据提现方式进行条件验证
|
// 根据提现方式进行条件验证
|
||||||
if (data.type === 'USDT') {
|
if (data.type === 'USDT(TRC20)') {
|
||||||
if (!data.account || data.account.trim().length === 0) {
|
if (!data.account || data.account.trim().length === 0) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: 'USDT 账号不能为空',
|
message: 'USDT(TRC20) 账号不能为空',
|
||||||
path: ['account'],
|
path: ['account'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ const WalletDialog: WalletDialogProps = (props) => {
|
|||||||
// 构建 title 和 description
|
// 构建 title 和 description
|
||||||
const title = `提现金额-${data.money}`;
|
const title = `提现金额-${data.money}`;
|
||||||
let description = '';
|
let description = '';
|
||||||
if (data.type === 'USDT') {
|
if (data.type === 'USDT(TRC20)') {
|
||||||
description = `${data.type}-${data.account || ''}`;
|
description = `${data.type}-${data.account || ''}`;
|
||||||
} else if (data.type === '微信' || data.type === '支付宝') {
|
} else if (data.type === '微信' || data.type === '支付宝') {
|
||||||
description = `${data.type}-${data.avatar || ''}`;
|
description = `${data.type}-${data.avatar || ''}`;
|
||||||
@@ -151,7 +151,7 @@ const WalletDialog: WalletDialogProps = (props) => {
|
|||||||
size: 1,
|
size: 1,
|
||||||
issue_type: 1,
|
issue_type: 1,
|
||||||
});
|
});
|
||||||
if (ticketData?.list?.length > 1) {
|
if (ticketData?.list?.length > 0) {
|
||||||
toast.info('已经存在待处理提现,请耐心等待');
|
toast.info('已经存在待处理提现,请耐心等待');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ const WalletDialog: WalletDialogProps = (props) => {
|
|||||||
|
|
||||||
// 根据提现方式动态生成弹窗描述
|
// 根据提现方式动态生成弹窗描述
|
||||||
const getModalDescription = () => {
|
const getModalDescription = () => {
|
||||||
if (currentType === 'USDT') {
|
if (currentType === 'USDT(TRC20)') {
|
||||||
const accountInfo = account || '未知地址';
|
const accountInfo = account || '未知地址';
|
||||||
return `请确认您的提现地址及金额无误,您将提现${money}RMB至${accountInfo}地址账号。`;
|
return `请确认您的提现地址及金额无误,您将提现${money}RMB至${accountInfo}地址账号。`;
|
||||||
} else {
|
} else {
|
||||||
@@ -214,7 +214,7 @@ const WalletDialog: WalletDialogProps = (props) => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className={'pt-4'}>
|
<div className={'pt-4'}>
|
||||||
<div className={'pb-2 text-sm font-semibold text-[#7A7A7A]'}>
|
<div className={'pb-2 text-sm font-semibold text-[#7A7A7A]'}>
|
||||||
{currentType === 'USDT'
|
{currentType === 'USDT(TRC20)'
|
||||||
? '将佣金提现至您的个人数字钱包,无手续费'
|
? '将佣金提现至您的个人数字钱包,无手续费'
|
||||||
: '该提现方式需10%手续费,该费率由支付平台收取'}
|
: '该提现方式需10%手续费,该费率由支付平台收取'}
|
||||||
</div>
|
</div>
|
||||||
@@ -250,8 +250,8 @@ const WalletDialog: WalletDialogProps = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 提现账号输入(仅当选择USDT时显示) */}
|
{/* 提现账号输入(仅当选择USDT(TRC20)时显示) */}
|
||||||
{currentType === 'USDT' && (
|
{currentType === 'USDT(TRC20)' && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='account'
|
name='account'
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<svg width="58" height="58" viewBox="0 0 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g filter="url(#filter0_d_2526_1784)">
|
||||||
|
<rect x="2.11401" y="2.06323" width="53.6962" height="53.6962" rx="10" fill="white"/>
|
||||||
|
<g clip-path="url(#clip0_2526_1784)">
|
||||||
|
<path d="M35.2874 19.2087C35.2874 18.9251 35.4299 18.6623 35.663 18.5164L41.8535 14.64C42.3693 14.3171 43.0255 14.7047 43.0255 15.3324V21.9808C43.0255 22.4267 42.679 22.7882 42.2517 22.7882H36.0612C35.6339 22.7882 35.2874 22.4267 35.2874 21.9808V19.2087Z" fill="#455FE9"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.8295 24.9768C24.5964 25.1227 24.4538 25.3855 24.4538 25.6691V33.2867V34.0936V34.0941V35.3041C24.4538 35.75 24.1074 36.0712 23.68 36.0712H22.1323C21.705 36.0712 21.3585 35.75 21.3585 35.3041V34.0936V29.06C21.3585 28.4323 20.7023 28.0447 20.1866 28.3676L13.996 32.2439C13.763 32.3899 13.6204 32.6526 13.6204 32.9364V41.3613C13.6204 41.8072 13.9668 42.1687 14.3942 42.1687H20.5847C21.0121 42.1687 21.3585 41.8072 21.3585 41.3613V40.5533V39.5381C21.3585 39.0921 21.705 38.8219 22.1323 38.8219H23.68C24.1074 38.8219 24.4538 39.0921 24.4538 39.5381V40.5533V41.3613C24.4538 41.8072 24.8003 42.1687 25.2276 42.1687H31.4181C31.8455 42.1687 32.192 41.8072 32.192 41.3613V34.0941V33.2867V21.7929C32.192 21.1652 31.5358 20.7775 31.0199 21.1005L24.8295 24.9768Z" fill="#455FE9"/>
|
||||||
|
<path d="M35.2878 26.0989V41.4406C35.2878 41.8643 35.6343 42.2077 36.0617 42.2077H42.2522C42.6795 42.2077 43.026 41.8643 43.026 41.4406V26.0989C43.026 25.6752 42.6795 25.3318 42.2522 25.3318H36.0617C35.6343 25.3318 35.2878 25.6752 35.2878 26.0989Z" fill="#455FE9"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_2526_1784" x="0.114014" y="0.0632324" width="57.6963" height="57.6963" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2526_1784"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2526_1784" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="clip0_2526_1784">
|
||||||
|
<rect width="29.4051" height="30.6835" fill="white" transform="translate(13.6204 12.2913)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.0 KiB |
@@ -1,6 +1,6 @@
|
|||||||
// 加载状态组件
|
// 加载状态组件
|
||||||
import { unitConversion } from '@workspace/airo-ui/utils';
|
import { unitConversion } from '@workspace/airo-ui/utils';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
const LoadingState = () => {
|
const LoadingState = () => {
|
||||||
const t = useTranslations('components.offerDialog');
|
const t = useTranslations('components.offerDialog');
|
||||||
@@ -338,16 +338,17 @@ const TabContent: React.FC<TabContentProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用 useMemo 优化数据处理性能
|
console.log('subscribeData:', subscribeData);
|
||||||
const yearlyPlans: PlanProps[] = useMemo(
|
|
||||||
() => subscribeData?.map((item) => processPlanData(item, true)),
|
|
||||||
[subscribeData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const monthlyPlans: PlanProps[] = useMemo(
|
let yearlyPlans: PlanProps[] = [];
|
||||||
() => subscribeData?.map((item) => processPlanData(item, false)),
|
if (Array.isArray(subscribeData)) {
|
||||||
[subscribeData],
|
yearlyPlans = subscribeData.map((item) => processPlanData(item, true));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
let monthlyPlans: PlanProps[] = [];
|
||||||
|
if (Array.isArray(subscribeData)) {
|
||||||
|
monthlyPlans = subscribeData.map((item) => processPlanData(item, false));
|
||||||
|
}
|
||||||
|
|
||||||
const PurchaseRef = useRef<{
|
const PurchaseRef = useRef<{
|
||||||
show: (subscribe: API.Subscribe, tabValue: string) => void;
|
show: (subscribe: API.Subscribe, tabValue: string) => void;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function DatePicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover modal>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
|
|||||||
Reference in New Issue
Block a user