Compare commits

100 Commits

Author SHA1 Message Date
shanshanzhong147 bd4091524e in1
CI / build (20.15.1) (push) Failing after 24m12s
2025-09-26 18:50:38 -07:00
shanshanzhong147 5b7f778551 in1
CI / build (20.15.1) (push) Successful in 30m33s
2025-09-26 17:33:59 -07:00
shanshanzhong147 38f64f770d in1
CI / build (20.15.1) (push) Failing after 23m2s
2025-09-26 09:48:00 -07:00
shanshanzhong147 2ee249f6f7 in1
CI / build (20.15.1) (push) Failing after 24m30s
2025-09-26 07:22:05 -07:00
shanshanzhong147 e3698a8a60 in1 2025-09-26 07:21:51 -07:00
shanshanzhong147 618ac0bf8c in1
CI / build (20.15.1) (push) Failing after 1m6s
2025-09-26 07:14:26 -07:00
shanshanzhong147 0f58c4ef7c in1
CI / build (20.15.1) (push) Failing after 23m22s
2025-09-26 06:47:11 -07:00
shanshanzhong147 92e298ee4e in1
CI / build (20.15.1) (push) Failing after 23m4s
2025-09-26 05:45:09 -07:00
shanshanzhong147 3df33ae4ce inini
CI / build (20.15.1) (push) Failing after 4m31s
2025-09-26 05:03:18 -07:00
shanshanzhong147 fd711a22c8 tg
CI / build (20.15.1) (push) Has been cancelled
2025-09-26 04:58:24 -07:00
shanshanzhong147 d81cdfe789 tg
CI / build (20.15.1) (push) Failing after 23m29s
2025-09-26 03:33:53 -07:00
shanshanzhong147 fd8b07b0d8 tg
CI / build (20.15.1) (push) Failing after 1m42s
2025-09-26 02:37:06 -07:00
shanshanzhong147 394727ccdb tg
CI / build (20.15.1) (push) Failing after 21m21s
2025-09-26 02:14:40 -07:00
shanshanzhong147 993c6d8870 tg 2025-09-26 02:14:33 -07:00
shanshanzhong147 f35eb48480 ci11
CI / build (20.15.1) (push) Failing after 21m52s
2025-09-26 01:49:44 -07:00
shanshanzhong147 8b25de05dc ci11
CI / build (20.15.1) (push) Failing after 33s
2025-09-26 01:38:57 -07:00
shanshanzhong147 afba56c177 i9
CI / build (20.15.1) (push) Failing after 1m56s
2025-09-25 11:44:51 -07:00
shanshanzhong147 d37e2abf0d i9 2025-09-25 11:43:59 -07:00
shanshanzhong147 71dbb921ff i9 2025-09-25 11:28:51 -07:00
shanshanzhong147 a38bb5d38a i8
CI / build (20.15.1) (push) Failing after 18m20s
2025-09-25 11:24:18 -07:00
shanshanzhong147 94c515b2de i7
CI / build (20.15.1) (push) Has been cancelled
2025-09-25 11:17:05 -07:00
shanshanzhong147 61b299ed15 i7
CI / build (20.15.1) (push) Has been cancelled
2025-09-25 11:15:12 -07:00
shanshanzhong147 94004069cb i5
CI / build (20.15.1) (push) Has been cancelled
2025-09-25 11:01:23 -07:00
shanshanzhong147 1c85a4abef i5
CI / build (20.15.1) (push) Failing after 18m47s
2025-09-25 10:19:47 -07:00
shanshanzhong147 7d39166ed5 i4
CI / build (20.15.1) (push) Has been cancelled
2025-09-25 10:18:59 -07:00
shanshanzhong147 ff9401c24e i3
CI / build (20.15.1) (push) Failing after 17m29s
2025-09-25 10:01:17 -07:00
shanshanzhong147 4181e4bd71 i2
CI / build (20.15.1) (push) Failing after 2m17s
2025-09-25 09:54:43 -07:00
shanshanzhong147 5be2e91d0a i1
CI / build (20.15.1) (push) Has been cancelled
2025-09-25 09:53:01 -07:00
shanshanzhong147 fcac29b7b0 oo2 2025-09-25 09:28:58 -07:00
shanshanzhong147 2b24a66dbc oo1 2025-09-25 09:27:18 -07:00
shanshanzhong147 92487eac92 11 2025-09-25 09:16:23 -07:00
shanshanzhong147 c1ab737894 c 2025-09-25 09:13:54 -07:00
shanshanzhong147 3fdd504d01 i1 2025-09-25 09:11:44 -07:00
shanshanzhong147 98a9fc041f init02 2025-09-25 09:10:04 -07:00
shanshanzhong147 006886751f ci(build:admin) 2025-09-25 09:06:10 -07:00
shanshanzhong147 832e55363b ci(build:admin) 2025-09-25 09:02:08 -07:00
shanshanzhong147 ea16dbb91d tg 2025-09-25 08:58:54 -07:00
shanshanzhong147 fcfd2a9a9f init01 2025-09-25 08:55:12 -07:00
shanshanzhong147 02fc54df21 init01 2025-09-25 08:36:23 -07:00
shanshanzhong147 f3113d787d init01 2025-09-25 08:31:59 -07:00
shanshanzhong147 a607748df0 init01 2025-09-25 08:21:38 -07:00
shanshanzhong147 731d64b26d init01 2025-09-25 08:16:56 -07:00
shanshanzhong147 b6a2c13f72 init01 2025-09-25 08:14:59 -07:00
shanshanzhong147 3dcd4b0581 init01 2025-09-25 08:07:36 -07:00
shanshanzhong147 8ec2f9f097 init 2025-09-25 06:04:01 -07:00
shanshanzhong147 bbf27538b2 init 2025-09-25 05:53:33 -07:00
shanshanzhong147 56cf0e1de0 ini 2025-09-25 05:13:29 -07:00
shanshanzhong147 83476efe63 ini 2025-09-25 05:11:03 -07:00
shanshanzhong147 49359845c0 main 2025-09-25 04:57:54 -07:00
shanshanzhong147 170c42cb52 main 2025-09-25 04:47:45 -07:00
shanshanzhong147 67a427af49 main 2025-09-25 04:43:02 -07:00
shanshanzhong147 e65a2ded46 main 2025-09-25 04:36:19 -07:00
shanshanzhong147 de8761846a main 2025-09-25 04:24:47 -07:00
shanshanzhong147 b0ecc0a514 main 2025-09-25 04:20:43 -07:00
shanshanzhong147 3d404dc6ab main 2025-09-25 04:17:57 -07:00
shanshanzhong147 f22be971a5 init 2025-09-25 04:12:01 -07:00
shanshanzhong147 e5da524504 init 2025-09-25 04:05:52 -07:00
shanshanzhong147 2bc3fd541a init 2025-09-25 04:05:09 -07:00
shanshanzhong147 82aec50e7a init 2025-09-25 04:03:29 -07:00
shanshanzhong147 314406cd41 ci 2025-09-25 03:47:46 -07:00
shanshanzhong147 13635b2d9d ci: 移除pnpm action setup的注释链接 2025-09-25 03:35:44 -07:00
shanshanzhong147 3eb4d5ff80 ci 2025-09-25 03:34:55 -07:00
shanshanzhong147 601425c783 ci 2025-09-25 03:34:09 -07:00
shanshanzhong147 cb705c0150 ci 2025-09-25 03:24:17 -07:00
shanshanzhong147 419b3c8625 ci 2025-09-25 03:18:40 -07:00
shanshanzhong147 b5a460e440 ci 2025-09-25 03:16:43 -07:00
shanshanzhong147 109e5ec119 ci 2025-09-25 03:12:23 -07:00
shanshanzhong147 22f5197417 ci 2025-09-25 03:03:46 -07:00
shanshanzhong147 854273f6a4 ci 2025-09-25 02:59:51 -07:00
shanshanzhong147 fad0450fd1 ci 2025-09-25 02:58:43 -07:00
shanshanzhong147 3ce7e7cee0 ci 2025-09-25 02:54:28 -07:00
shanshanzhong147 5637b002a5 ci 2025-09-25 02:50:29 -07:00
shanshanzhong147 eed624e491 ci 2025-09-25 02:49:29 -07:00
shanshanzhong147 ed9cbde7be ci 2025-09-25 02:43:52 -07:00
shanshanzhong147 0db0f40ce3 ci 2025-09-25 02:37:59 -07:00
shanshanzhong147 dabe5fbb2c ci 2025-09-25 02:29:14 -07:00
shanshanzhong147 61fa7be47d ci 2025-09-25 02:19:18 -07:00
shanshanzhong147 849a7219df ci 2025-09-25 02:17:57 -07:00
shanshanzhong147 c6f47e0e10 ci 2025-09-25 02:12:56 -07:00
shanshanzhong147 dbaee3d858 ci 2025-09-25 01:59:37 -07:00
shanshanzhong147 8ed24ed44b ci 2025-09-25 01:52:12 -07:00
shanshanzhong147 3d7e13cbc2 ci 2025-09-25 01:44:24 -07:00
shanshanzhong147 db0cd8167d ci 2025-09-25 01:42:01 -07:00
shanshanzhong147 611602cfef ci 2025-09-24 23:42:11 -07:00
shanshanzhong147 67eec9ebb4 ci 2025-09-24 21:32:19 -07:00
shanshanzhong147 6c1953a831 ci 2025-09-24 21:28:07 -07:00
shanshanzhong147 df01c08734 ci 2025-09-24 21:18:09 -07:00
shanshanzhong147 23385ce3ac ci 2025-09-24 19:44:46 -07:00
shanshanzhong147 a65c46cb46 ci 2025-09-24 19:32:34 -07:00
shanshanzhong147 9f716728fa ci 2025-09-24 19:30:52 -07:00
shanshanzhong147 0fba9eac06 ci 2025-09-24 19:20:14 -07:00
shanshanzhong147 b0b58333a4 ci 2025-09-24 19:16:26 -07:00
shanshanzhong147 df79d6f74e ci 2025-09-24 19:13:51 -07:00
shanshanzhong147 cf66053337 ci 2025-09-24 19:09:07 -07:00
shanshanzhong147 7cedf4fba3 ci 2025-09-24 19:08:19 -07:00
sepakeloudest 09084eef21 feat: 新版样式 2025-09-24 07:26:08 -07:00
sepakeloudest 8c7d415ae7 feat: 弹窗兼容修改 2025-09-23 06:19:12 -07:00
sepakeloudest b6c82e9bc9 feat: 修改图标 2025-09-19 03:28:59 -07:00
sepakeloudest 6c40fde269 feat: 佣金提现和备注功能 2025-09-16 04:51:27 -07:00
sepakeloudest 5db1e8b15a feat: 佣金提现和备注功能 2025-09-15 20:31:10 -07:00
17 changed files with 1044 additions and 188 deletions
+736 -117
View File
@@ -1,146 +1,765 @@
name: Publish Release Assets
run-name: Build and publish web assets and Docker images
name: CI
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "apps/**"
- "packages/**"
- "package.json"
- "turbo.json"
- ".gitea/workflows/*.yml"
- dev
- cicd
pull_request:
types: [opened, synchronize, reopened]
paths:
- "apps/**"
- "packages/**"
- "package.json"
- "turbo.json"
- ".gitea/workflows/*.yml"
release:
types: [published]
branches:
- main
- dev
- cicd
env:
# Gitea
GIT_USERNAME: ${{ vars.GIT_USERNAME }}
GIT_PASSWORD: ${{ vars.GIT_PASSWORD }}
# Docker Hub
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
# Host SSH (用于上传构建产物)
DOMAIN_URL: git.kxsw.us #*修改为你12
REPO: ${{ vars.REPO }}
TELEGRAM_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
TELEGRAM_CHAT_ID: "-4940243803"
DOCKER_REGISTRY: registry.kxsw.us
DOCKER_BUILDKIT: 1
# Host SSH
SSH_HOST: ${{ vars.SSH_HOST }}
SSH_PORT: ${{ vars.SSH_PORT }}
SSH_USER: ${{ vars.SSH_USER }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_PASSWORD: ${{ vars.SSH_PASSWORD }}
jobs:
publish:
name: Publish Release Assets
runs-on: ubuntu-latest
build:
runs-on: ppanel-web02
container:
image: node:20
strategy:
matrix:
# 只有node支持版本号别名
node: ['20.15.1']
steps:
- name: Checkout codebase
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 'latest'
fetch-depth: 0
- name: Cache Bun dependencies
uses: actions/cache@v3
with:
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
- name: 缓存服务健康检查
id: cache-health
continue-on-error: true
run: |
chmod +x scripts/publish.sh
./scripts/publish.sh
echo "检查缓存服务可用性..."
# 设置缓存可用性标志
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
if: ${{ gitea.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: |
out/ppanel-admin-web.tar.gz
out/ppanel-user-web.tar.gz
token: ${{ secrets.GITEA_TOKEN }}
- name: 缓存降级提示
if: env.CACHE_AVAILABLE == 'false'
run: |
echo "🔄 缓存服务不可用,构建将在无缓存模式下进行"
echo "⏱️ 这可能会增加构建时间,但不会影响构建结果"
echo "📦 所有依赖将重新下载和安装"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Install system tools (jq, docker, curl)
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
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
run: sudo apt-get install -y jq
- name: Configure npm registry (npmmirror) and canvas mirror
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
id: version
run: echo "PPANEL_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_ENV
- name: Build and push Docker image for ppanel-admin-web
uses: docker/build-push-action@v6
- name: Install dependencies cache (Bun)
if: env.CACHE_AVAILABLE == 'true'
uses: actions/cache@v4
continue-on-error: true
with:
context: .
file: ./docker/ppanel-admin-web/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.DOCKER_USERNAME }}/ppanel-admin-web:latest
${{ env.DOCKER_USERNAME }}/ppanel-admin-web:${{ env.PPANEL_VERSION }}
path: ~/.bun
key: bun-${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('bun.lock') }}
restore-keys: |
bun-${{ runner.os }}-${{ matrix.node }}-
bun-${{ runner.os }}-
- name: Build and push Docker image for ppanel-user-web
uses: docker/build-push-action@v6
- name: Install dependencies cache (node_modules)
if: env.CACHE_AVAILABLE == 'true'
uses: actions/cache@v4
continue-on-error: true
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: |
out/ppanel-admin-web.tar.gz
out/ppanel-user-web.tar.gz
node_modules
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: chapvic/telegram-notify@master
with:
bot_token: ${{ env.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ 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 }}
🚀 **服务已成功部署到生产环境**
parse_mode: Markdown
# 步骤5: TG通知 (失败)
- name: 📱 发送失败通知到Telegram
if: failure()
uses: chapvic/telegram-notify@master
with:
bot_token: ${{ env.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ env.TELEGRAM_CHAT_ID }}
message: |
❌ **部署失败!**
📦 **项目**: ${{ github.repository }}
🌿 **分支**: ${{ github.ref_name }}
🔖 **版本**: ${{ env.VERSION }}
🎯 **构建目标**: ${{ env.BUILD_TARGET }}
📝 **提交**: ${{ github.sha }}
👤 **提交者**: ${{ github.actor }}
🕐 **时间**: ${{ github.event.head_commit.timestamp }}
⚠️ **请检查构建日志获取详细信息**
parse_mode: Markdown
+91
View File
@@ -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}"
+1 -1
View File
@@ -15,7 +15,7 @@ unset APP_NAME # 显式销毁变量
1. 执行 `./scripts/publish-dev.sh` 构建产物在 out-dev文件夹
2. 上传out/zip.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
# 第一次启动
+1 -1
View File
@@ -3,7 +3,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=zh-CN
# Site URL and API URL
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
NEXT_PUBLIC_DEFAULT_USER_EMAIL=
+12 -5
View File
@@ -20,16 +20,23 @@ import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { toast } from 'sonner';
import { UserDetail } from './user-detail';
import UserForm from './user-form';
// 新的子组件,现在管理它自己的备注状态
const RemarkForm = ({ onSave, initialRemark, CloseComponent }) => {
const [remark, setRemark] = useState(initialRemark);
// 为 RemarkForm 组件定义 props 类型
interface RemarkFormProps {
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);
};
@@ -59,11 +59,6 @@ export default function Page() {
accessorKey: 'title',
header: t('title'),
},
{
accessorKey: 'user_id',
header: t('user'),
cell: ({ row }) => <UserDetail id={row.original.user_id} />,
},
{
accessorKey: 'status',
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>
),
},
{
accessorKey: 'user_id',
header: t('user'),
cell: ({ row }) => <UserDetail id={row.original.user_id} />,
},
{
accessorKey: 'updated_at',
header: t('updatedAt'),
cell: ({ row }) => formatDate(row.getValue('updated_at')),
},
]}
params={[
/*params={[
{
key: 'status',
placeholder: t('status.0'),
@@ -100,11 +100,12 @@ export default function Page() {
},
],
},
]}
]}*/
request={async (pagination, filters) => {
const { data } = await getTicketList({
...pagination,
...filters,
issue_type: 1,
});
return {
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> {ticket?.title?.split('-')[1]}</div>
<div> {ticket?.description?.split('-')[0]}</div>
{ticket?.description?.split('-')[1].includes('data:image') ? (
{ticket?.description?.split('-')[1]?.includes('data:image') ? (
<div>
<div></div>
<NextImage
src={ticket?.description?.split('-')[1]}
src={ticket?.description?.split('-')[1]!}
width={300}
height={300}
className='!size-auto object-cover'
+3 -1
View File
@@ -10,9 +10,11 @@ import { getLocale, getMessages } from 'next-intl/server';
import { PublicEnvScript } from 'next-runtime-env';
import { unstable_noStore as noStore } from 'next/cache';
// import { Geist, Geist_Mono } from 'next/font/google';
import { Inter } from 'next/font/google';
import { cookies } from 'next/headers';
import NextTopLoader from 'nextjs-toploader';
import React from 'react';
const inter = Inter({ subsets: ['latin'] });
// const fontSans = Geist({
// subsets: ['latin'],
@@ -112,7 +114,7 @@ export default async function RootLayout({
<body
suppressHydrationWarning
// ${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}>
<NextTopLoader showSpinner={false} />
+1 -1
View File
@@ -107,7 +107,7 @@ export const navs = [
{
title: 'Withdraw Ticket Management',
url: '/dashboard/withdraw-ticket',
icon: 'flat-color-icons:collaboration',
icon: 'flat-color-icons:debt',
},
{
title: 'Document Management',
+1
View File
@@ -864,6 +864,7 @@ declare namespace API {
user_id?: number;
status?: number;
search?: string;
issue_type?: number;
};
type GetTicketListRequest = {
+1 -1
View File
@@ -3,7 +3,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=zh-CN
# Site URL and API URL
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
# Home Page Settings
+1 -1
View File
@@ -3,7 +3,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US
# Site URL and API URL
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
# Home Page Settings
@@ -7,6 +7,7 @@ import useGlobalStore from '@/config/use-global';
import { getStat } from '@/services/common/common';
import { queryUserSubscribe } from '@/services/user/user';
import { useQuery } from '@tanstack/react-query';
import { AiroButton } from '@workspace/airo-ui/components/AiroButton';
import { Button } from '@workspace/airo-ui/components/button';
import { Card } from '@workspace/airo-ui/components/card';
import { useTranslations } from 'next-intl';
@@ -23,6 +24,7 @@ import {
PopupRef,
} from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Popup';
import { Empty } from '@/components/empty';
import SvgIcon from '@/components/SvgIcon';
import { queryAnnouncement } from '@/services/user/announcement';
import { queryOrderList } from '@/services/user/order';
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 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 (
<>
<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 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'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
{t('accountOverview')}
@@ -120,7 +231,7 @@ export default function Content() {
<span className='text-sm font-light text-[#666666]'>{t('accountBalance')}</span>
<Recharge
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>
@@ -131,7 +242,7 @@ export default function Content() {
</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=''>
<h3 className='flex items-center justify-between text-[#666666]'>
<div className={'flex items-center justify-between'}>
@@ -224,8 +335,9 @@ export default function Content() {
<Airo_Empty className={'py-0'} description={t('noPlanAvailable')} />
)}
</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'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
{t('siteAnnouncements')}
@@ -282,31 +394,6 @@ export default function Content() {
<Popup ref={popupRef} />
<AnnouncementDialog ref={dialogRef} />
</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>
</>
);
@@ -55,7 +55,7 @@ const WalletDialog: WalletDialogProps = (props) => {
const { currency } = common;
// 定义支持的账户类型
const ACCOUNT_TYPE = ['USDT', '微信', '支付宝'] as const;
const ACCOUNT_TYPE = ['USDT(TRC20)', '微信', '支付宝'] as const;
// 根据账户类型定义 Zod 验证模式
const formSchema = z
@@ -76,11 +76,11 @@ const WalletDialog: WalletDialogProps = (props) => {
})
.superRefine((data, ctx) => {
// 根据提现方式进行条件验证
if (data.type === 'USDT') {
if (data.type === 'USDT(TRC20)') {
if (!data.account || data.account.trim().length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'USDT 账号不能为空',
message: 'USDT(TRC20) 账号不能为空',
path: ['account'],
});
}
@@ -111,7 +111,7 @@ const WalletDialog: WalletDialogProps = (props) => {
// 构建 title 和 description
const title = `提现金额-${data.money}`;
let description = '';
if (data.type === 'USDT') {
if (data.type === 'USDT(TRC20)') {
description = `${data.type}-${data.account || ''}`;
} else if (data.type === '微信' || data.type === '支付宝') {
description = `${data.type}-${data.avatar || ''}`;
@@ -151,7 +151,7 @@ const WalletDialog: WalletDialogProps = (props) => {
size: 1,
issue_type: 1,
});
if (ticketData?.list?.length > 1) {
if (ticketData?.list?.length > 0) {
toast.info('已经存在待处理提现,请耐心等待');
return;
}
@@ -181,7 +181,7 @@ const WalletDialog: WalletDialogProps = (props) => {
// 根据提现方式动态生成弹窗描述
const getModalDescription = () => {
if (currentType === 'USDT') {
if (currentType === 'USDT(TRC20)') {
const accountInfo = account || '未知地址';
return `请确认您的提现地址及金额无误,您将提现${money}RMB至${accountInfo}地址账号。`;
} else {
@@ -214,7 +214,7 @@ const WalletDialog: WalletDialogProps = (props) => {
</DialogHeader>
<div className={'pt-4'}>
<div className={'pb-2 text-sm font-semibold text-[#7A7A7A]'}>
{currentType === 'USDT'
{currentType === 'USDT(TRC20)'
? '将佣金提现至您的个人数字钱包,无手续费'
: '该提现方式需10%手续费,该费率由支付平台收取'}
</div>
@@ -250,8 +250,8 @@ const WalletDialog: WalletDialogProps = (props) => {
)}
/>
{/* 提现账号输入(仅当选择USDT时显示) */}
{currentType === 'USDT' && (
{/* 提现账号输入(仅当选择USDT(TRC20)时显示) */}
{currentType === 'USDT(TRC20)' && (
<FormField
control={form.control}
name='account'
+25
View File
@@ -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 { useMemo, useRef } from 'react';
import { useRef } from 'react';
const LoadingState = () => {
const t = useTranslations('components.offerDialog');
@@ -338,16 +338,17 @@ const TabContent: React.FC<TabContentProps> = ({
}
};
// 使用 useMemo 优化数据处理性能
const yearlyPlans: PlanProps[] = useMemo(
() => subscribeData?.map((item) => processPlanData(item, true)),
[subscribeData],
);
console.log('subscribeData:', subscribeData);
const monthlyPlans: PlanProps[] = useMemo(
() => subscribeData?.map((item) => processPlanData(item, false)),
[subscribeData],
);
let yearlyPlans: PlanProps[] = [];
if (Array.isArray(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<{
show: (subscribe: API.Subscribe, tabValue: string) => void;
@@ -28,7 +28,7 @@ export function DatePicker({
};
return (
<Popover>
<Popover modal>
<PopoverTrigger asChild>
<Button
variant='outline'