Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e696e2f551 | |||
| bbe25b027f | |||
| 3b22c583b7 | |||
| f2d0bbf36b | |||
| 97c2832e89 | |||
| 9d43162830 | |||
| 530df9ce09 | |||
| 9f2264514d | |||
| 57d7b49aa9 | |||
| 8e7434f67c | |||
| e15366d5f7 | |||
| 517d07caaf | |||
| 3a8f171f19 | |||
| 1c53fc4fe4 | |||
| 1c0511d44e | |||
| df759063f2 | |||
| 8a0359fd7e | |||
| ae9f9063a9 | |||
| d8e29af1dc | |||
| 69c037bf51 | |||
| be99c7acfa | |||
| c5062278a3 | |||
| 8888382067 | |||
| d687e47fdc | |||
| 5082885ace | |||
| 929e405aab | |||
| 31c1ecb612 | |||
| e38904422d | |||
| acd0c94e60 | |||
| f9262b3754 | |||
| 4fc0b21c9c | |||
| ffbed8c72d | |||
| f6b306c823 | |||
| ed3b96158f | |||
| f9d55161e6 | |||
| ecd6cf1c94 | |||
| 107d950771 | |||
| 4d153b4149 | |||
| 2b0741fdb9 | |||
| 033dda7744 | |||
| 259d5a416c | |||
| f753721492 | |||
| ab5de5e0f5 | |||
| a9cbd2b921 | |||
| f7a28c1c86 | |||
| 8d64f66cca | |||
| 346ad0a4ec | |||
| c7bfc2729c | |||
| 4392dd4a3e | |||
| ced65527d5 | |||
| de6dfb5e20 | |||
| 11c05de565 |
@@ -0,0 +1,196 @@
|
|||||||
|
name: site-dist-deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
|
||||||
|
|
||||||
|
env:
|
||||||
|
VITE_APP_BASE_URL: https://h.hifast.biz
|
||||||
|
SSH_HOST: ${{ vars.PRO_SSH_HOST }}
|
||||||
|
SSH_PORT: ${{ vars.PRO_SSH_PORT }}
|
||||||
|
SSH_USER: ${{ vars.PRO_SSH_USER }}
|
||||||
|
SSH_PASSWORD: ${{ vars.PRO_SSH_PASSWORD }}
|
||||||
|
DEPLOY_PATH: /var/www/down
|
||||||
|
# TG通知
|
||||||
|
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
||||||
|
TG_CHAT_ID: "-4940243803"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: landing-hero-web01
|
||||||
|
steps:
|
||||||
|
- name: Manual checkout (no Node required)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
git fetch --all --tags
|
||||||
|
git checkout "${{ github.ref_name }}"
|
||||||
|
git reset --hard "origin/${{ github.ref_name }}"
|
||||||
|
else
|
||||||
|
REPO_URL="${{ github.server_url }}/${{ github.repository }}"
|
||||||
|
echo "Cloning $REPO_URL"
|
||||||
|
git clone --depth=1 --branch "${{ github.ref_name }}" "$REPO_URL" .
|
||||||
|
git fetch --tags
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build dist with Unified Script
|
||||||
|
env:
|
||||||
|
VITE_APP_BASE_URL: "https://h.hifast.biz"
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/ci-build.sh
|
||||||
|
./scripts/ci-build.sh
|
||||||
|
|
||||||
|
- name: Check Artifacts
|
||||||
|
run: |
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo "Listing all files in workspace:"
|
||||||
|
find . -maxdepth 2 -not -path '*/.*'
|
||||||
|
if [ -f "site_dist.tgz" ]; then
|
||||||
|
echo "✅ File exists: site_dist.tgz"
|
||||||
|
ls -lh site_dist.tgz
|
||||||
|
echo "File path: $(readlink -f site_dist.tgz)"
|
||||||
|
else
|
||||||
|
echo "❌ File NOT found: site_dist.tgz"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy to Host (Native SSH/SCP)
|
||||||
|
run: |
|
||||||
|
echo "Installing SSH tools..."
|
||||||
|
if command -v apk &> /dev/null; then
|
||||||
|
echo "Detected Alpine Linux. Installing sshpass openssh-client via apk..."
|
||||||
|
apk add --no-cache sshpass openssh-client
|
||||||
|
elif command -v apt-get &> /dev/null; then
|
||||||
|
echo "Detected Debian/Ubuntu. Installing sshpass openssh-client via apt..."
|
||||||
|
apt-get update -y && apt-get install -y sshpass openssh-client
|
||||||
|
elif command -v yum &> /dev/null; then
|
||||||
|
echo "Detected RHEL/CentOS. Installing sshpass openssh-clients via yum..."
|
||||||
|
yum install -y sshpass openssh-clients
|
||||||
|
elif command -v dnf &> /dev/null; then
|
||||||
|
echo "Detected Fedora/RHEL8+. Installing sshpass openssh-clients via dnf..."
|
||||||
|
dnf install -y sshpass openssh-clients
|
||||||
|
elif command -v zypper &> /dev/null; then
|
||||||
|
echo "Detected OpenSUSE. Installing sshpass openssh via zypper..."
|
||||||
|
zypper install -y sshpass openssh
|
||||||
|
else
|
||||||
|
echo "Error: No known package manager found. Cannot install sshpass."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Uploading artifact..."
|
||||||
|
# 使用 sshpass 传递密码 (更安全的方式是使用 key,但此处沿用 password)
|
||||||
|
export SSHPASS="${{ env.SSH_PASSWORD }}"
|
||||||
|
|
||||||
|
# 1. 检查连接并创建目录 (包含 /tmp/ci-upload 和 目标目录的准备)
|
||||||
|
sshpass -e ssh -o StrictHostKeyChecking=no -p ${{ env.SSH_PORT }} ${{ env.SSH_USER }}@${{ env.SSH_HOST }} "mkdir -p /tmp/ci-upload"
|
||||||
|
|
||||||
|
# 2. SCP 上传 (直接使用当前目录下的 site_dist.tgz,规避跨容器挂载问题)
|
||||||
|
if [ ! -f "site_dist.tgz" ]; then
|
||||||
|
echo "❌ Error: site_dist.tgz not found in current directory!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sshpass -e scp -o StrictHostKeyChecking=no -P ${{ env.SSH_PORT }} site_dist.tgz ${{ env.SSH_USER }}@${{ env.SSH_HOST }}:/tmp/ci-upload/site_dist.tgz
|
||||||
|
|
||||||
|
# 3. 解压并重启 Nginx
|
||||||
|
echo "Deploying on remote host..."
|
||||||
|
sshpass -e ssh -o StrictHostKeyChecking=no -p ${{ env.SSH_PORT }} ${{ env.SSH_USER }}@${{ env.SSH_HOST }} "
|
||||||
|
echo 'Preparing target directory ${{ env.DEPLOY_PATH }}...'
|
||||||
|
mkdir -p ${{ env.DEPLOY_PATH }}
|
||||||
|
|
||||||
|
# 切换到目录,确保操作安全
|
||||||
|
cd ${{ env.DEPLOY_PATH }} || exit 1
|
||||||
|
|
||||||
|
echo 'Cleaning up old files (preserving download/downsload)...'
|
||||||
|
|
||||||
|
# 使用更安全的策略:
|
||||||
|
# 1. 创建临时备份目录
|
||||||
|
mkdir -p /tmp/site_backup_safe
|
||||||
|
rm -rf /tmp/site_backup_safe/*
|
||||||
|
|
||||||
|
# 2. 将需要保留的文件夹移到备份目录 (如果存在)
|
||||||
|
if [ -d 'download' ]; then
|
||||||
|
echo 'Backing up download folder...'
|
||||||
|
mv download /tmp/site_backup_safe/
|
||||||
|
fi
|
||||||
|
if [ -d 'downsload' ]; then
|
||||||
|
echo 'Backing up downsload folder...'
|
||||||
|
mv downsload /tmp/site_backup_safe/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 清空当前目录 (此时 download/downsload 已经移走,安全删除所有)
|
||||||
|
# 注意:不删除当前目录本身,只删除内容
|
||||||
|
find . -mindepth 1 -delete
|
||||||
|
|
||||||
|
# 4. 移回备份的文件夹
|
||||||
|
if [ -d '/tmp/site_backup_safe/download' ]; then
|
||||||
|
echo 'Restoring download folder...'
|
||||||
|
mv /tmp/site_backup_safe/download .
|
||||||
|
fi
|
||||||
|
if [ -d '/tmp/site_backup_safe/downsload' ]; then
|
||||||
|
echo 'Restoring downsload folder...'
|
||||||
|
mv /tmp/site_backup_safe/downsload .
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. 清理备份目录
|
||||||
|
rm -rf /tmp/site_backup_safe
|
||||||
|
|
||||||
|
echo 'Extracting to ${{ env.DEPLOY_PATH }}...'
|
||||||
|
# 解压覆盖
|
||||||
|
tar -xzf /tmp/ci-upload/site_dist.tgz -C ${{ env.DEPLOY_PATH }}
|
||||||
|
|
||||||
|
echo 'Reloading Nginx...'
|
||||||
|
# 尝试多种 reload 方式
|
||||||
|
nginx -s reload || systemctl reload nginx || echo 'Warning: Nginx reload returned non-zero'
|
||||||
|
|
||||||
|
echo 'Cleanup...'
|
||||||
|
rm -f /tmp/ci-upload/site_dist.tgz
|
||||||
|
"
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
|
||||||
|
|
||||||
|
# 步骤6: TG通知 (成功)
|
||||||
|
- name: 📱 发送成功通知到Telegram
|
||||||
|
if: success()
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
token: ${{ env.TG_BOT_TOKEN }}
|
||||||
|
to: ${{ env.TG_CHAT_ID }}
|
||||||
|
message: |
|
||||||
|
✅ 部署成功!
|
||||||
|
|
||||||
|
📦 项目: ${{ github.repository }}
|
||||||
|
🌿 分支: ${{ github.ref_name }}
|
||||||
|
📝 提交: ${{ github.sha }}
|
||||||
|
👤 提交者: ${{ github.actor }}
|
||||||
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
|
🚀 服务已成功部署到生产环境
|
||||||
|
parse_mode: Markdown
|
||||||
|
|
||||||
|
# 步骤5: TG通知 (失败)
|
||||||
|
- name: 📱 发送失败通知到Telegram
|
||||||
|
if: failure()
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
token: ${{ env.TG_BOT_TOKEN }}
|
||||||
|
to: ${{ env.TG_CHAT_ID }}
|
||||||
|
message: |
|
||||||
|
❌ 部署失败!
|
||||||
|
|
||||||
|
📦 项目: ${{ github.repository }}
|
||||||
|
🌿 分支: ${{ github.ref_name }}
|
||||||
|
📝 提交: ${{ github.sha }}
|
||||||
|
👤 提交者: ${{ github.actor }}
|
||||||
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
|
⚠️ 请检查构建日志获取详细信息
|
||||||
|
parse_mode: Markdown
|
||||||
|
|
||||||
@@ -0,0 +1,790 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
- cicd
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
- cicd
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOMAIN_URL: git.kxsw.us
|
||||||
|
REPO: ${{ vars.REPO }}
|
||||||
|
TELEGRAM_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
||||||
|
TELEGRAM_CHAT_ID: "-4940243803"
|
||||||
|
DOCKER_REGISTRY: registry.kxsw.us
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
DOCKER_API_VERSION: "1.44"
|
||||||
|
# Host SSH - 根据分支动态选择
|
||||||
|
SSH_HOST: ${{ github.ref_name == 'main' && vars.PRO_SSH_HOST || (github.ref_name == 'develop' && vars.DEV_SSH_HOST || vars.PRO_SSH_HOST) }}
|
||||||
|
SSH_PORT: ${{ github.ref_name == 'main' && vars.PRO_SSH_PORT || (github.ref_name == 'develop' && vars.DEV_SSH_PORT || vars.PRO_SSH_PORT) }}
|
||||||
|
SSH_USER: ${{ github.ref_name == 'main' && vars.PRO_SSH_USER || (github.ref_name == 'dedevelopv' && vars.DEV_SSH_USER || vars.PRO_SSH_USER) }}
|
||||||
|
SSH_PASSWORD: ${{ github.ref_name == 'main' && vars.PRO_SSH_PASSWORD || (github.ref_name == 'dedevelopv' && vars.DEV_SSH_PASSWORD || vars.PRO_SSH_PASSWORD) }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: fastvpn-admin01
|
||||||
|
container:
|
||||||
|
image: node:20
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# 只有node支持版本号别名
|
||||||
|
node: ['20.15.1']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: 缓存服务健康检查
|
||||||
|
id: cache-health
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
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: 缓存降级提示
|
||||||
|
if: env.CACHE_AVAILABLE == 'false'
|
||||||
|
run: |
|
||||||
|
echo "🔄 缓存服务不可用,构建将在无缓存模式下进行"
|
||||||
|
echo "⏱️ 这可能会增加构建时间,但不会影响构建结果"
|
||||||
|
echo "📦 所有依赖将重新下载和安装"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
# 基础工具和GPG
|
||||||
|
apt-get install -y -o Dpkg::Lock::Timeout=600 jq curl ca-certificates gnupg
|
||||||
|
# 配置Docker官方源,安装新版CLI与Buildx插件(支持 API 1.44+)
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
|
||||||
|
apt-get update -y -o Dpkg::Lock::Timeout=600
|
||||||
|
apt-get install -y -o Dpkg::Lock::Timeout=600 docker-ce-cli docker-buildx-plugin
|
||||||
|
docker --version
|
||||||
|
jq --version
|
||||||
|
curl --version
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
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: 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: Install dependencies cache (Bun)
|
||||||
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: ~/.bun
|
||||||
|
key: bun-${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
bun-${{ runner.os }}-${{ matrix.node }}-
|
||||||
|
bun-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install dependencies cache (node_modules)
|
||||||
|
if: env.CACHE_AVAILABLE == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
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: |
|
||||||
|
if [ "$BUILD_TARGET" = "admin" ]; then
|
||||||
|
VERSION=$(jq -r .version apps/admin/package.json)
|
||||||
|
echo "使用 admin 应用版本: $VERSION"
|
||||||
|
elif [ "$BUILD_TARGET" = "user" ]; then
|
||||||
|
VERSION=$(jq -r .version apps/user/package.json)
|
||||||
|
echo "使用 user 应用版本: $VERSION"
|
||||||
|
else
|
||||||
|
# both 或其他情况使用根目录版本
|
||||||
|
VERSION=$(jq -r .version package.json)
|
||||||
|
echo "使用根目录版本: $VERSION"
|
||||||
|
fi
|
||||||
|
if [ "$VERSION" = "null" ] || [ -z "$VERSION" ] || [ "$VERSION" = "undefined" ]; then
|
||||||
|
echo "检测到版本为空,回退到根目录版本"
|
||||||
|
VERSION=$(jq -r .version package.json)
|
||||||
|
fi
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 根据分支动态设置API地址
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||||
|
echo "NEXT_PUBLIC_API_URL=https://api.hifast.biz" >> $GITHUB_ENV
|
||||||
|
echo "为main分支设置生产环境API地址"
|
||||||
|
elif [ "${{ github.ref_name }}" = "develop" ]; then
|
||||||
|
echo "NEXT_PUBLIC_API_URL=https://api.hifast.biz" >> $GITHUB_ENV
|
||||||
|
echo "为 develop 分支设置开发环境API地址"
|
||||||
|
else
|
||||||
|
echo "NEXT_PUBLIC_API_URL=https://api.hifast.biz" >> $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/fastvpn-admin-web:cache \
|
||||||
|
--cache-to type=registry,ref=${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-admin-web:cache,mode=max \
|
||||||
|
-f ./docker/ppanel-admin-web/Dockerfile \
|
||||||
|
-t ${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-admin-web:${{ env.VERSION }} \
|
||||||
|
--push .
|
||||||
|
else
|
||||||
|
echo "使用常规docker构建"
|
||||||
|
docker build -f ./docker/ppanel-admin-web/Dockerfile -t ${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-admin-web:${{ env.VERSION }} .
|
||||||
|
docker push ${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-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/fastvpn-user-web:cache \
|
||||||
|
--cache-to type=registry,ref=${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-user-web:cache,mode=max \
|
||||||
|
-f ./docker/ppanel-user-web/Dockerfile \
|
||||||
|
-t ${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-user-web:${{ env.VERSION }} \
|
||||||
|
--push .
|
||||||
|
else
|
||||||
|
echo "使用常规docker构建"
|
||||||
|
docker build -f ./docker/ppanel-user-web/Dockerfile -t ${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-user-web:${{ env.VERSION }} .
|
||||||
|
docker push ${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-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/fastvpn-admin-web:${{ env.VERSION }}"
|
||||||
|
if docker pull ${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-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 \
|
||||||
|
--add-host api.airoport.co:103.150.215.40 \
|
||||||
|
--name fastvpn-admin-web \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3001:3000 \
|
||||||
|
-e NEXT_PUBLIC_API_URL=${{ env.NEXT_PUBLIC_API_URL }} \
|
||||||
|
${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-admin-web:${{ env.VERSION }}
|
||||||
|
|
||||||
|
# 验证容器启动
|
||||||
|
echo "验证容器启动状态..."
|
||||||
|
for i in {1..10}; do
|
||||||
|
if docker ps -q -f name=fastvpn-admin-web | grep -q .; then
|
||||||
|
echo "✅ 管理面板部署成功"
|
||||||
|
docker ps -f name=fastvpn-admin-web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "等待容器启动... $i/10"
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ 管理面板部署失败 - 容器未能正常启动"
|
||||||
|
docker logs fastvpn-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 \
|
||||||
|
--add-host api.airoport.co:103.150.215.40 \
|
||||||
|
--name fastvpn-user-web \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3002:3000 \
|
||||||
|
-e NEXT_PUBLIC_API_URL=${{ env.NEXT_PUBLIC_API_URL }} \
|
||||||
|
${{ env.DOCKER_REGISTRY }}/ppanel/fastvpn-user-web:${{ env.VERSION }}
|
||||||
|
|
||||||
|
# 验证容器启动
|
||||||
|
echo "验证容器启动状态..."
|
||||||
|
for i in {1..10}; do
|
||||||
|
if docker ps -q -f name=fastvpn-user-web | grep -q .; then
|
||||||
|
echo "✅ 用户面板部署成功"
|
||||||
|
docker ps -f name=fastvpn-user-web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "等待容器启动... $i/10"
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ 用户面板部署失败 - 容器未能正常启动"
|
||||||
|
docker logs fastvpn-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 }}
|
||||||
|
|
||||||
|
⚠️ 请检查构建日志获取详细信息
|
||||||
@@ -18,6 +18,21 @@ export default defineConfigWithVueTs(
|
|||||||
|
|
||||||
...pluginVue.configs['flat/essential'],
|
...pluginVue.configs['flat/essential'],
|
||||||
vueTsConfigs.recommended,
|
vueTsConfigs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// 1. 强制 Vue 文件内标签块的顺序
|
||||||
|
'vue/block-order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
order: ['template', 'script', 'style'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 2. 自动关闭单单词组件名检查(根据需要可选)
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
// 3. 可以在这里添加更多 TypeScript 或 Vue 的自定义校验
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
skipFormatting,
|
skipFormatting,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vite App</title>
|
<title>HiFast VPN</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --mode dev",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build:test": "vite build --mode test",
|
||||||
|
"build:prod": "vite build --mode pord",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
|
||||||
"type-check": "vue-tsc --build",
|
"type-check": "vue-tsc --build",
|
||||||
"lint": "eslint . --fix --cache",
|
"lint": "eslint . --fix --cache",
|
||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write --experimental-cli src/"
|
||||||
@@ -18,8 +18,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"crisp-sdk-web": "^1.0.27",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"lucide-vue-next": "^0.562.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
"reka-ui": "^2.7.0",
|
"reka-ui": "^2.7.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
@@ -45,6 +48,7 @@
|
|||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-vue-devtools": "^8.0.5",
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue-tsc": "^3.2.1"
|
"vue-tsc": "^3.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,21 @@ importers:
|
|||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
|
axios:
|
||||||
|
specifier: ^1.13.2
|
||||||
|
version: 1.13.2
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
crisp-sdk-web:
|
||||||
|
specifier: ^1.0.27
|
||||||
|
version: 1.0.27
|
||||||
|
crypto-js:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.0
|
||||||
lucide-vue-next:
|
lucide-vue-next:
|
||||||
specifier: ^0.562.0
|
specifier: ^0.562.0
|
||||||
version: 0.562.0(vue@3.5.26(typescript@5.9.3))
|
version: 0.562.0(vue@3.5.26(typescript@5.9.3))
|
||||||
@@ -90,6 +99,9 @@ importers:
|
|||||||
vite-plugin-vue-devtools:
|
vite-plugin-vue-devtools:
|
||||||
specifier: ^8.0.5
|
specifier: ^8.0.5
|
||||||
version: 8.0.5(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))
|
version: 8.0.5(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))
|
||||||
|
vite-svg-loader:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^3.2.1
|
specifier: ^3.2.1
|
||||||
version: 3.2.1(typescript@5.9.3)
|
version: 3.2.1(typescript@5.9.3)
|
||||||
@@ -713,6 +725,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^2.7.0 || ^3.0.0
|
vue: ^2.7.0 || ^3.0.0
|
||||||
|
|
||||||
|
'@trysound/sax@0.2.0':
|
||||||
|
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
'@tsconfig/node24@24.0.3':
|
'@tsconfig/node24@24.0.3':
|
||||||
resolution: {integrity: sha512-vcERKtKQKHgzt/vfS3Gjasd8SUI2a0WZXpgJURdJsMySpS5+ctgbPfuLj2z/W+w4lAfTWxoN4upKfu2WzIRYnw==}
|
resolution: {integrity: sha512-vcERKtKQKHgzt/vfS3Gjasd8SUI2a0WZXpgJURdJsMySpS5+ctgbPfuLj2z/W+w4lAfTWxoN4upKfu2WzIRYnw==}
|
||||||
|
|
||||||
@@ -950,6 +966,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
asynckit@0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
|
axios@1.13.2:
|
||||||
|
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
@@ -982,6 +1004,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
callsites@3.1.0:
|
callsites@3.1.0:
|
||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1007,6 +1033,14 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
commander@7.2.0:
|
||||||
|
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
@@ -1017,15 +1051,40 @@ packages:
|
|||||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
crisp-sdk-web@1.0.27:
|
||||||
|
resolution: {integrity: sha512-aNWR3te65YiaVFu/iwdqOo3cyUBZHUheE4d6EtgQu/T18jh/9SpoYXjXF/OzUD3Cqy0pGryoqtuy5gxD8tqX9Q==}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
crypto-js@4.2.0:
|
||||||
|
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||||
|
|
||||||
|
css-select@5.2.2:
|
||||||
|
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
|
||||||
|
|
||||||
|
css-tree@2.2.1:
|
||||||
|
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
|
||||||
|
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
||||||
|
|
||||||
|
css-tree@2.3.1:
|
||||||
|
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
|
||||||
|
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||||
|
|
||||||
|
css-what@6.2.2:
|
||||||
|
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
cssesc@3.0.0:
|
cssesc@3.0.0:
|
||||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
csso@5.0.5:
|
||||||
|
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
|
||||||
|
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@@ -1056,10 +1115,31 @@ packages:
|
|||||||
defu@6.1.4:
|
defu@6.1.4:
|
||||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
|
||||||
|
domelementtype@2.3.0:
|
||||||
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
electron-to-chromium@1.5.267:
|
electron-to-chromium@1.5.267:
|
||||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||||
|
|
||||||
@@ -1067,6 +1147,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
|
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
entities@7.0.0:
|
entities@7.0.0:
|
||||||
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
|
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@@ -1074,6 +1158,22 @@ packages:
|
|||||||
error-stack-parser-es@1.0.5:
|
error-stack-parser-es@1.0.5:
|
||||||
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
||||||
|
|
||||||
|
es-define-property@1.0.1:
|
||||||
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-errors@1.3.0:
|
||||||
|
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-set-tostringtag@2.1.0:
|
||||||
|
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
esbuild@0.27.2:
|
esbuild@0.27.2:
|
||||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1213,15 +1313,39 @@ packages:
|
|||||||
flatted@3.3.3:
|
flatted@3.3.3:
|
||||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||||
|
|
||||||
|
follow-redirects@1.15.11:
|
||||||
|
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
function-bind@1.1.2:
|
||||||
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2:
|
gensync@1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1234,6 +1358,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
gopd@1.2.0:
|
||||||
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
@@ -1241,6 +1369,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
has-symbols@1.1.0:
|
||||||
|
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
has-tostringtag@1.0.2:
|
||||||
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
@@ -1429,6 +1569,16 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0:
|
||||||
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
mdn-data@2.0.28:
|
||||||
|
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
|
||||||
|
|
||||||
|
mdn-data@2.0.30:
|
||||||
|
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||||
|
|
||||||
memorystream@0.3.1:
|
memorystream@0.3.1:
|
||||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
@@ -1441,6 +1591,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
|
||||||
@@ -1621,6 +1779,9 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0:
|
||||||
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1705,6 +1866,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
svgo@3.3.2:
|
||||||
|
resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
synckit@0.11.11:
|
synckit@0.11.11:
|
||||||
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
@@ -1809,6 +1975,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
|
vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
|
||||||
|
|
||||||
|
vite-svg-loader@5.1.0:
|
||||||
|
resolution: {integrity: sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.2.13'
|
||||||
|
|
||||||
vite@7.3.0:
|
vite@7.3.0:
|
||||||
resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
|
resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -2469,6 +2640,8 @@ snapshots:
|
|||||||
'@tanstack/virtual-core': 3.13.13
|
'@tanstack/virtual-core': 3.13.13
|
||||||
vue: 3.5.26(typescript@5.9.3)
|
vue: 3.5.26(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@trysound/sax@0.2.0': {}
|
||||||
|
|
||||||
'@tsconfig/node24@24.0.3': {}
|
'@tsconfig/node24@24.0.3': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
@@ -2797,6 +2970,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
|
axios@1.13.2:
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.11
|
||||||
|
form-data: 4.0.5
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.9.11: {}
|
baseline-browser-mapping@2.9.11: {}
|
||||||
@@ -2830,6 +3013,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
run-applescript: 7.1.0
|
run-applescript: 7.1.0
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
callsites@3.1.0: {}
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001761: {}
|
caniuse-lite@1.0.30001761: {}
|
||||||
@@ -2851,6 +3039,12 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
|
commander@7.2.0: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
@@ -2859,14 +3053,42 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-what: 5.5.0
|
is-what: 5.5.0
|
||||||
|
|
||||||
|
crisp-sdk-web@1.0.27: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
crypto-js@4.2.0: {}
|
||||||
|
|
||||||
|
css-select@5.2.2:
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
css-what: 6.2.2
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.2.2
|
||||||
|
nth-check: 2.1.1
|
||||||
|
|
||||||
|
css-tree@2.2.1:
|
||||||
|
dependencies:
|
||||||
|
mdn-data: 2.0.28
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
css-tree@2.3.1:
|
||||||
|
dependencies:
|
||||||
|
mdn-data: 2.0.30
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
css-what@6.2.2: {}
|
||||||
|
|
||||||
cssesc@3.0.0: {}
|
cssesc@3.0.0: {}
|
||||||
|
|
||||||
|
csso@5.0.5:
|
||||||
|
dependencies:
|
||||||
|
css-tree: 2.2.1
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
@@ -2886,8 +3108,34 @@ snapshots:
|
|||||||
|
|
||||||
defu@6.1.4: {}
|
defu@6.1.4: {}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
|
domelementtype@2.3.0: {}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-errors: 1.3.0
|
||||||
|
gopd: 1.2.0
|
||||||
|
|
||||||
electron-to-chromium@1.5.267: {}
|
electron-to-chromium@1.5.267: {}
|
||||||
|
|
||||||
enhanced-resolve@5.18.4:
|
enhanced-resolve@5.18.4:
|
||||||
@@ -2895,10 +3143,27 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.3.0
|
tapable: 2.3.0
|
||||||
|
|
||||||
|
entities@4.5.0: {}
|
||||||
|
|
||||||
entities@7.0.0: {}
|
entities@7.0.0: {}
|
||||||
|
|
||||||
error-stack-parser-es@1.0.5: {}
|
error-stack-parser-es@1.0.5: {}
|
||||||
|
|
||||||
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
|
es-errors@1.3.0: {}
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
es-set-tostringtag@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
hasown: 2.0.2
|
||||||
|
|
||||||
esbuild@0.27.2:
|
esbuild@0.27.2:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.27.2
|
'@esbuild/aix-ppc64': 0.27.2
|
||||||
@@ -3072,11 +3337,41 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.3.3: {}
|
flatted@3.3.3: {}
|
||||||
|
|
||||||
|
follow-redirects@1.15.11: {}
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
es-set-tostringtag: 2.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
es-errors: 1.3.0
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-proto: 1.0.1
|
||||||
|
gopd: 1.2.0
|
||||||
|
has-symbols: 1.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
math-intrinsics: 1.1.0
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
dunder-proto: 1.0.1
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -3087,10 +3382,22 @@ snapshots:
|
|||||||
|
|
||||||
globals@14.0.0: {}
|
globals@14.0.0: {}
|
||||||
|
|
||||||
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
|
has-symbols@1.1.0: {}
|
||||||
|
|
||||||
|
has-tostringtag@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
has-symbols: 1.1.0
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
@@ -3226,6 +3533,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
|
mdn-data@2.0.28: {}
|
||||||
|
|
||||||
|
mdn-data@2.0.30: {}
|
||||||
|
|
||||||
memorystream@0.3.1: {}
|
memorystream@0.3.1: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
@@ -3235,6 +3548,12 @@ snapshots:
|
|||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
mime-db@1.52.0: {}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
@@ -3347,6 +3666,8 @@ snapshots:
|
|||||||
|
|
||||||
prettier@3.7.4: {}
|
prettier@3.7.4: {}
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
@@ -3445,6 +3766,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
svgo@3.3.2:
|
||||||
|
dependencies:
|
||||||
|
'@trysound/sax': 0.2.0
|
||||||
|
commander: 7.2.0
|
||||||
|
css-select: 5.2.2
|
||||||
|
css-tree: 2.3.1
|
||||||
|
css-what: 6.2.2
|
||||||
|
csso: 5.0.5
|
||||||
|
picocolors: 1.1.1
|
||||||
|
|
||||||
synckit@0.11.11:
|
synckit@0.11.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@pkgr/core': 0.2.9
|
'@pkgr/core': 0.2.9
|
||||||
@@ -3564,6 +3895,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
vite-svg-loader@5.1.0(vue@3.5.26(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
svgo: 3.3.2
|
||||||
|
vue: 3.5.26(typescript@5.9.3)
|
||||||
|
|
||||||
vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2):
|
vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.2
|
esbuild: 0.27.2
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 统一构建脚本 (CI Build Script)
|
||||||
|
# 功能:自动检测环境、安装 Node.js (不依赖系统预装)、构建项目、打包产物
|
||||||
|
# 解决:Runner 环境缺失 Node/Docker/Python 等工具导致的问题
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 配置节点版本
|
||||||
|
NODE_VERSION="20.10.0"
|
||||||
|
DIST_FILE="site_dist.tgz"
|
||||||
|
|
||||||
|
echo ">>> [Init] Starting CI Build Script..."
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# 1. 基础工具检测与安装 (curl, tar, xz)
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
ensure_tools() {
|
||||||
|
echo ">>> [Tools] Checking basic tools..."
|
||||||
|
local missing_tools=()
|
||||||
|
|
||||||
|
for tool in curl tar xz; do
|
||||||
|
if ! command -v "$tool" &> /dev/null; then
|
||||||
|
missing_tools+=("$tool")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing_tools[@]} -eq 0 ]; then
|
||||||
|
echo " All tools present."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Missing tools: ${missing_tools[*]}. Attempting installation..."
|
||||||
|
|
||||||
|
if command -v apk &> /dev/null; then
|
||||||
|
apk add --no-cache curl tar xz
|
||||||
|
elif command -v apt-get &> /dev/null; then
|
||||||
|
apt-get update && apt-get install -y curl tar xz-utils
|
||||||
|
elif command -v yum &> /dev/null; then
|
||||||
|
yum install -y curl tar xz
|
||||||
|
elif command -v dnf &> /dev/null; then
|
||||||
|
dnf install -y curl tar xz
|
||||||
|
elif command -v zypper &> /dev/null; then
|
||||||
|
zypper install -y curl tar xz
|
||||||
|
else
|
||||||
|
echo "!!! Error: Cannot install missing tools. No known package manager found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# 2. 环境安装 (System Node.js - Most Reliable on Alpine)
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
install_env() {
|
||||||
|
echo ">>> [Env] Setting up System Node.js environment..."
|
||||||
|
|
||||||
|
# 尝试使用系统包管理器安装 Node.js 和 npm
|
||||||
|
if command -v apk &> /dev/null; then
|
||||||
|
echo " Detected Alpine Linux. Installing nodejs and npm via apk..."
|
||||||
|
apk add --no-cache nodejs npm
|
||||||
|
elif command -v apt-get &> /dev/null; then
|
||||||
|
echo " Detected Debian/Ubuntu. Installing nodejs and npm via apt..."
|
||||||
|
apt-get update && apt-get install -y nodejs npm
|
||||||
|
elif command -v yum &> /dev/null; then
|
||||||
|
yum install -y nodejs npm
|
||||||
|
elif command -v dnf &> /dev/null; then
|
||||||
|
dnf install -y nodejs npm
|
||||||
|
elif command -v zypper &> /dev/null; then
|
||||||
|
zypper install -y nodejs npm
|
||||||
|
else
|
||||||
|
echo "!!! Warning: No package manager found. Checking if Node is pre-installed..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 验证安装
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
echo "!!! Error: Node.js not found and could not be installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
echo "!!! Error: npm not found and could not be installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Node version: $(node -v)"
|
||||||
|
echo " npm version: $(npm -v)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# 3. 项目构建与打包
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
build_project() {
|
||||||
|
echo ">>> [Build] Starting project build..."
|
||||||
|
|
||||||
|
# 注入环境变量
|
||||||
|
if [ -n "$VITE_APP_BASE_URL" ]; then
|
||||||
|
echo " Setting VITE_APP_BASE_URL=${VITE_APP_BASE_URL}"
|
||||||
|
# 兼容 package.json 中的 mode: pord (Typo in original project)
|
||||||
|
echo "VITE_APP_BASE_URL=${VITE_APP_BASE_URL}" > .env.pord
|
||||||
|
# 同时写入 .env 以防万一
|
||||||
|
echo "VITE_APP_BASE_URL=${VITE_APP_BASE_URL}" >> .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Installing dependencies..."
|
||||||
|
# 使用 npm install 而不是 npm ci,以避免因 lockfile 版本不匹配或 engines 检查导致的失败
|
||||||
|
npm install --no-audit --progress=false
|
||||||
|
|
||||||
|
echo " Building..."
|
||||||
|
# 使用 package.json 中定义的 build:prod 命令
|
||||||
|
npm run build:prod
|
||||||
|
|
||||||
|
echo ">>> [Package] Compressing artifacts..."
|
||||||
|
if [ ! -d "dist" ]; then
|
||||||
|
echo "!!! Error: 'dist' directory not found after build."
|
||||||
|
# 列出当前目录以便调试
|
||||||
|
ls -la
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 直接打包 dist 目录下的内容
|
||||||
|
tar -C dist -czf "$DIST_FILE" .
|
||||||
|
|
||||||
|
echo " Success! Artifact created: $DIST_FILE"
|
||||||
|
ls -lh "$DIST_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Main Execution Flow
|
||||||
|
# ==========================================
|
||||||
|
ensure_tools
|
||||||
|
install_env
|
||||||
|
build_project
|
||||||
@@ -1,9 +1,48 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { RouterView } from 'vue-router'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<div>
|
||||||
|
<main>
|
||||||
|
<RouterView />
|
||||||
|
</main>
|
||||||
|
<Toaster
|
||||||
|
position="top-center"
|
||||||
|
:toast-options="{
|
||||||
|
style: {
|
||||||
|
background: '#000000',
|
||||||
|
color: '#ffffff',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import 'vue-sonner/style.css'
|
||||||
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
|
import { Crisp } from 'crisp-sdk-web'
|
||||||
|
|
||||||
|
const WEBSITE_ID = '47fcc1ac-9674-4ab1-9e3c-6b5666f59a38'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 定义加载逻辑
|
||||||
|
const loadCrisp = () => {
|
||||||
|
console.log('页面资源已就绪,开始初始化 Crisp...')
|
||||||
|
Crisp.configure(WEBSITE_ID)
|
||||||
|
|
||||||
|
// 可选:初始化后自动隐藏或执行其他逻辑
|
||||||
|
// Crisp.chat.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果页面已经加载完成(或者是从其他路由跳转过来的)
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
loadCrisp()
|
||||||
|
} else {
|
||||||
|
// 否则等待 window load 事件,确保图片、CSS等资源全部加载完毕
|
||||||
|
window.addEventListener('load', loadCrisp, { once: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
defaultValue?: string | number
|
||||||
|
modelValue?: string | number
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: "update:modelValue", payload: string | number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelValue = useVModel(props, "modelValue", emits, {
|
||||||
|
passive: true,
|
||||||
|
defaultValue: props.defaultValue,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
v-model="modelValue"
|
||||||
|
data-slot="input"
|
||||||
|
:class="cn(
|
||||||
|
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Input } from "./Input.vue"
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div class="device-list-container">
|
||||||
|
<div v-if="!devices?.length" class="w-full text-center">暂无绑定设备</div>
|
||||||
|
<div v-else class="grid min-h-[76px] grid-cols-2 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="device in devices"
|
||||||
|
:key="device.id"
|
||||||
|
@click="delDevice(device)"
|
||||||
|
class="relative h-[76px] rounded-[25px] border-2 p-4 text-[10px]"
|
||||||
|
>
|
||||||
|
<div class="flex h-full items-center justify-center gap-3">
|
||||||
|
<component
|
||||||
|
:is="getDeviceTypeInfo(device.user_agent)?.iconComponent"
|
||||||
|
class="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<span class="text-white">设备: {{ getDeviceTypeInfo(device.user_agent)?.type }}</span>
|
||||||
|
<span class="font-mono text-[11px] tracking-tighter uppercase"
|
||||||
|
>SN: {{ device?.identifier?.slice(0, 4) }}{{ device.id }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<X :size="16" class="absolute top-[5px] right-[10px] text-[12px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Laptop, Smartphone, Tablet, Monitor, Cpu, HelpCircle, X } from 'lucide-vue-next'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
|
interface Device {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
user_agent: string
|
||||||
|
identifier: string
|
||||||
|
type: 'mobile' | 'desktop'
|
||||||
|
deviceId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
devices: Device[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
function delDevice(device: Device) {
|
||||||
|
if (confirm('请确认是否移除此设备?')) {
|
||||||
|
request.put('/api/v1/public/user/unbind_device', { id: device.id }).then(() => {
|
||||||
|
toast.success('设备已移除')
|
||||||
|
emit('refresh')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceTypeInfo {
|
||||||
|
type: string
|
||||||
|
iconComponent: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备类型和图标
|
||||||
|
* @param userAgent 浏览器代理字符串
|
||||||
|
*/
|
||||||
|
const getDeviceTypeInfo = (userAgent: string): DeviceTypeInfo => {
|
||||||
|
// 转换为小写以便进行不区分大小写的匹配
|
||||||
|
const ua = userAgent.toLowerCase()
|
||||||
|
|
||||||
|
if (ua.includes('ipad')) {
|
||||||
|
return { type: 'iPad', iconComponent: Tablet }
|
||||||
|
}
|
||||||
|
if (ua.includes('iphone') || ua.includes('ios')) {
|
||||||
|
return { type: 'iPhone', iconComponent: Smartphone }
|
||||||
|
}
|
||||||
|
if (ua.includes('android')) {
|
||||||
|
return { type: 'Android', iconComponent: Smartphone }
|
||||||
|
}
|
||||||
|
if (ua.includes('mac') || ua.includes('macos')) {
|
||||||
|
return { type: 'Mac', iconComponent: Laptop }
|
||||||
|
}
|
||||||
|
if (ua.includes('windows')) {
|
||||||
|
return { type: 'Windows', iconComponent: Monitor }
|
||||||
|
}
|
||||||
|
if (ua.includes('linux')) {
|
||||||
|
return { type: 'Linux', iconComponent: Cpu }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'Unknown', iconComponent: HelpCircle }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :open="open" @update:open="handleClose">
|
||||||
|
<DialogContent
|
||||||
|
:show-close-button="false"
|
||||||
|
@pointer-down-outside="(event) => event.preventDefault()"
|
||||||
|
@focus-outside="(event) => event.preventDefault()"
|
||||||
|
class="max-w-[345px] rounded-[32px] border-none bg-[#E5E5E5] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center py-6">
|
||||||
|
<h2 class="mb-1 text-xl font-semibold text-black">订单状态</h2>
|
||||||
|
<div class="mb-10 text-[24px] font-bold text-black">
|
||||||
|
{{ statusInfo.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="statusInfo.description"
|
||||||
|
class="mb-8 text-center text-sm whitespace-pre-line text-gray-600"
|
||||||
|
>
|
||||||
|
{{ statusInfo.description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex w-full gap-3">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
class="h-[50px] flex-1 rounded-[25px] bg-[#D1D1D1] text-lg font-bold text-gray-600 hover:bg-gray-300"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="h-[50px] flex-1 rounded-[25px] bg-[#ADFF5B] text-lg font-bold text-black hover:bg-[#9EE64F]"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
|
返回首页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'refresh'])
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
const orderNo = ref<string | null>(null)
|
||||||
|
const status = ref<number>(1) // 1: Pending, 2: Paid, 3: Finished, 4: Close, 5: Failed, -1: Error
|
||||||
|
const countdown = ref('')
|
||||||
|
let timer: any = null
|
||||||
|
let countdownTimer: any = null
|
||||||
|
let createdAt: Date | null = null
|
||||||
|
|
||||||
|
const statusInfo = computed(() => {
|
||||||
|
switch (status.value) {
|
||||||
|
case -1:
|
||||||
|
return {
|
||||||
|
title: '检查失败',
|
||||||
|
description: '检查支付状态失败,请稍后刷新页面或联系客服',
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
return {
|
||||||
|
title: '待支付',
|
||||||
|
description: `订单正在处理中\n剩余时间: ${countdown.value}`,
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
return {
|
||||||
|
title: '支付确认中',
|
||||||
|
description: '订单已支付,系统正在确认,请稍候...',
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
return {
|
||||||
|
title: '支付成功',
|
||||||
|
description: '您的订单已支付完成',
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
return {
|
||||||
|
title: '订单已关闭',
|
||||||
|
description: '该订单已超时或被手动关闭',
|
||||||
|
}
|
||||||
|
case 5:
|
||||||
|
return {
|
||||||
|
title: '支付失败',
|
||||||
|
description: '订单支付失败,请检查支付信息并重试',
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
title: '待支付',
|
||||||
|
description: `订单正在处理中\n剩余时间: ${countdown.value}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
stopPolling()
|
||||||
|
open.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkStatus() {
|
||||||
|
if (!orderNo.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await request.get('/api/v1/public/order/detail', { order_no: orderNo.value })
|
||||||
|
// 1. 保存订单创建时间(用于倒计时)
|
||||||
|
if (!createdAt && res.created_at) {
|
||||||
|
const timestamp = res.created_at
|
||||||
|
const isMilliseconds = timestamp > 10000000000
|
||||||
|
createdAt = new Date(isMilliseconds ? timestamp : timestamp * 1000)
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
status.value = res.status
|
||||||
|
|
||||||
|
// 2. 根据状态处理业务逻辑
|
||||||
|
if (status.value === 3) {
|
||||||
|
// Finished
|
||||||
|
stopPolling()
|
||||||
|
emit('refresh')
|
||||||
|
} else if (status.value === 4 || status.value === 5) {
|
||||||
|
// Closed or Failed
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 查询失败:', error)
|
||||||
|
status.value = -1
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
stopPolling()
|
||||||
|
checkStatus()
|
||||||
|
timer = setInterval(checkStatus, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
if (countdownTimer) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
if (!createdAt) return
|
||||||
|
|
||||||
|
const updateCountdown = () => {
|
||||||
|
const now = new Date()
|
||||||
|
// 假设订单有效期为 15 分钟 (900 秒)
|
||||||
|
const expiryTime = new Date(createdAt!.getTime() + 900 * 1000)
|
||||||
|
const diff = expiryTime.getTime() - now.getTime()
|
||||||
|
console.log('diff', diff)
|
||||||
|
if (diff <= 0) {
|
||||||
|
countdown.value = '00:00'
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / 1000 / 60)
|
||||||
|
const seconds = Math.floor((diff / 1000) % 60)
|
||||||
|
countdown.value = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCountdown()
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer)
|
||||||
|
countdownTimer = setInterval(updateCountdown, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(no: string) {
|
||||||
|
orderNo.value = no
|
||||||
|
status.value = 1
|
||||||
|
createdAt = null
|
||||||
|
countdown.value = '15:00'
|
||||||
|
open.value = true
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="md:flex md:h-full md:flex-col md:justify-between">
|
||||||
|
<div class="bg-[#A8FF53] px-6 font-sans text-black">
|
||||||
|
<h2 class="mb-1 text-center text-2xl font-bold">选择付款方式</h2>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 pt-[43px]">
|
||||||
|
<div
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.value"
|
||||||
|
@click="handleSelect(item.value)"
|
||||||
|
:class="[
|
||||||
|
'flex h-[66px] cursor-pointer items-center justify-between border-[5px] border-black px-6 transition-all active:scale-[0.98]',
|
||||||
|
'rounded-[32px]', // 超大圆角
|
||||||
|
selectedValue === item.value ? 'bg-[#A8FF53]' : 'bg-[#A8FF53]',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="text-xl font-semibold">{{ item.label }}</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex h-7 w-7 items-center justify-center rounded-full border-[1px] border-black bg-transparent"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="selectedValue === item.value"
|
||||||
|
class="h-3.5 w-3.5 rounded-full bg-black"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 pt-[55px]">
|
||||||
|
<h2 class="mb-1 text-center text-2xl font-bold">订单信息</h2>
|
||||||
|
<div class="ml-[11px] pt-[22px] text-[16px] font-semibold">
|
||||||
|
{{ selectedPlan?.days }}天套餐
|
||||||
|
</div>
|
||||||
|
<div class="ml-[11px] flex items-center justify-between pt-[22px] text-[16px] font-semibold">
|
||||||
|
<div>订单金额</div>
|
||||||
|
<div>USD {{ selectedPlan?.price }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 pt-[85px] pb-[23px]">
|
||||||
|
<Button
|
||||||
|
:disabled="isPaying"
|
||||||
|
@click="$emit('pay', selectedValue)"
|
||||||
|
class="h-[50px] w-full rounded-[32px] border-none bg-black text-[16px] font-black text-white transition-all hover:bg-black/90 active:scale-[0.98] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="isPaying" class="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
{{ isPaying ? '正在支付...' : '立即支付' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Loader2 } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface PaymentOption {
|
||||||
|
label: string
|
||||||
|
value: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
methods: any[]
|
||||||
|
selectedPlan: any
|
||||||
|
isPaying?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const list = computed(() =>
|
||||||
|
props.methods.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 当前选中的值,默认选中第一个
|
||||||
|
const selectedValue = ref<number | string>(props.methods[0]?.id || 1)
|
||||||
|
|
||||||
|
const handleSelect = (value: number | string) => {
|
||||||
|
selectedValue.value = value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 如果需要特殊的斜体感,可以在这里微调 */
|
||||||
|
span {
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-6 font-sans">
|
||||||
|
<h2 class="mb-1 text-center text-2xl font-bold">套餐选择</h2>
|
||||||
|
<p class="mb-4 text-center text-sm font-[100] text-gray-600">*所有套餐均不限流量不限速度</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="(plan, index) in plans"
|
||||||
|
:key="plan.id"
|
||||||
|
@click="$emit('select', plan.id)"
|
||||||
|
:class="[
|
||||||
|
'relative h-[126px] cursor-pointer overflow-hidden rounded-[40px] border-4 border-black py-4 pl-[45px] transition-all',
|
||||||
|
currentPlanIndex === index ? 'bg-black text-white' : 'bg-[#A8FF53] text-black',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="text-2xl font-bold">{{ plan.days }}天</div>
|
||||||
|
<div class="text-[40px] leading-none font-semibold">${{ plan.price }}</div>
|
||||||
|
<div :class="'text-sm'">约${{ plan.daily }}/天</div>
|
||||||
|
<div
|
||||||
|
v-if="plan.discount"
|
||||||
|
:class="[
|
||||||
|
index > 1 ? 'font-semibold' : '',
|
||||||
|
currentPlanIndex === index ? 'bg-[#A8FF53]! text-black' : ' ',
|
||||||
|
]"
|
||||||
|
class="absolute top-[20px] -right-[40px] h-[16px] w-[126px] origin-center rotate-45 bg-black text-center text-[14px] leading-[16px] font-[200] text-[#ADFF5B]"
|
||||||
|
>
|
||||||
|
{{ plan.discount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
plans: Array,
|
||||||
|
currentPlanIndex: Number,
|
||||||
|
})
|
||||||
|
defineEmits(['select'])
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-center-skeleton custom-pulse" :class="[layout === 'desktop' ? 'w-full' : '']">
|
||||||
|
<!-- --- Header Part --- -->
|
||||||
|
<template v-if="type === 'header'">
|
||||||
|
<div v-if="layout === 'mobile'" class="mb-3 ml-[31px] flex h-[60px] items-center gap-3">
|
||||||
|
<div class="size-[60px] rounded-full bg-[#A8FF53]/60"></div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="h-5 w-40 rounded bg-[#A8FF53]/60"></div>
|
||||||
|
<div class="h-3 w-24 rounded bg-[#A8FF53]/40"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mb-3 flex flex-col items-center">
|
||||||
|
<div class="size-[60px] rounded-full bg-[#A8FF53]/60"></div>
|
||||||
|
<div class="mt-2 h-6 w-32 rounded bg-[#A8FF53]/60"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- --- Devices Part --- -->
|
||||||
|
<template v-if="type === 'devices'">
|
||||||
|
<div v-if="layout === 'mobile'" class="h-[76px] w-full rounded-2xl bg-[#A8FF53]/15"></div>
|
||||||
|
<div v-else class="h-[160px] w-full rounded-2xl bg-[#A8FF53]/15"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- --- Plans Part --- -->
|
||||||
|
<template v-if="type === 'plans'">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="h-[120px] w-full rounded-2xl bg-black/15"></div>
|
||||||
|
<div class="h-[120px] w-full rounded-2xl bg-black/15"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- --- Payments Part --- -->
|
||||||
|
<template v-if="type === 'payments'">
|
||||||
|
<div class="h-[300px] w-full rounded-2xl bg-black/15"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- --- Full Layouts (Backward Compatibility) --- -->
|
||||||
|
<template v-if="!type">
|
||||||
|
<!-- Old Full Layout implementation if needed, but we'll use type-based now -->
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
type?: 'header' | 'devices' | 'plans' | 'payments'
|
||||||
|
layout?: 'mobile' | 'desktop'
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-pulse {
|
||||||
|
animation: fast-pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fast-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<div class="font-sans text-white">
|
||||||
|
<div class="flex flex-col gap-[10px]">
|
||||||
|
<div
|
||||||
|
v-for="item in downloadMethods"
|
||||||
|
:key="item.id"
|
||||||
|
@click="toggle(item.id)"
|
||||||
|
class="group relative flex cursor-pointer flex-col rounded-[30px] border-[4px] border-white bg-black pt-4 pb-2 transition-all md:pb-6"
|
||||||
|
>
|
||||||
|
<div class="px-4 md:pl-[42px]">
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center gap-x-2 text-sm leading-tight font-bold md:text-2xl"
|
||||||
|
>
|
||||||
|
<StartIcon v-if="item.isHot" class="size-[20px] md:size-[30px]" />
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
|
||||||
|
<span v-if="item.subtitle" class="font-medium">——{{ item.subtitle }}</span>
|
||||||
|
|
||||||
|
<span v-if="item.highlight" class="font-black text-[#FF00FF]"
|
||||||
|
>——{{ item.highlight }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 打开按钮 -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-end gap-1.5 self-end text-sm font-bold group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="activeId !== item.id"
|
||||||
|
class="mt-[16px] flex items-center gap-[10px] md:mt-[60px] md:text-xl"
|
||||||
|
>
|
||||||
|
<span class="tracking-tight">点击展开详情</span>
|
||||||
|
<ArrowIcon class="size-[12px] md:size-[18px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeId === item.id" class="px-2 md:pt-8">
|
||||||
|
<div
|
||||||
|
class="animate-in fade-in zoom-in-95 border-t-2 border-white pt-[10px] text-[15px] leading-relaxed duration-200 md:pt-[20px]"
|
||||||
|
>
|
||||||
|
<!-- 内容区域 (点击图片不收起,只有特定的关闭按钮收起) -->
|
||||||
|
<div class="relative px-2" @click.stop>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-end gap-1.5 self-end text-sm font-bold group-hover:opacity-100 md:text-xl"
|
||||||
|
>
|
||||||
|
<div class="relative z-10 flex items-center gap-[10px]" @click="toggle(item.id)">
|
||||||
|
<span class="tracking-tight">点击收起</span>
|
||||||
|
<ArrowIcon class="size-[12px] rotate-180 md:size-[22px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Image Stack -->
|
||||||
|
<div class="relative -mt-10 flex flex-col md:hidden">
|
||||||
|
<img
|
||||||
|
v-for="(img, idx) in item.mobileImages"
|
||||||
|
:key="idx"
|
||||||
|
:src="img"
|
||||||
|
class="w-full"
|
||||||
|
alt="mobile guide"
|
||||||
|
/>
|
||||||
|
<!-- Mobile Hot Zones -->
|
||||||
|
<div
|
||||||
|
v-for="(hz, hzIdx) in item.mobileHotzones"
|
||||||
|
:key="hzIdx"
|
||||||
|
class="absolute z-20 cursor-pointer"
|
||||||
|
:style="{
|
||||||
|
left: hz.x + '%',
|
||||||
|
top: hz.y + '%',
|
||||||
|
width: hz.w + '%',
|
||||||
|
height: hz.h + '%',
|
||||||
|
}"
|
||||||
|
@click="handleHotzone(hz)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PC Image Stack -->
|
||||||
|
<div class="relative mt-4 hidden flex-col md:flex">
|
||||||
|
<img
|
||||||
|
v-for="(img, idx) in item.pcImages"
|
||||||
|
:key="idx"
|
||||||
|
:src="img"
|
||||||
|
class="w-full"
|
||||||
|
alt="pc guide"
|
||||||
|
/>
|
||||||
|
<!-- PC Hot Zones -->
|
||||||
|
<div
|
||||||
|
v-for="(hz, hzIdx) in item.pcHotzones"
|
||||||
|
:key="hzIdx"
|
||||||
|
class="absolute z-20 cursor-pointer"
|
||||||
|
:style="{
|
||||||
|
left: hz.x + '%',
|
||||||
|
top: hz.y + '%',
|
||||||
|
width: hz.w + '%',
|
||||||
|
height: hz.h + '%',
|
||||||
|
}"
|
||||||
|
@click="handleHotzone(hz)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部收起按钮 -->
|
||||||
|
<div class="absolute right-4 bottom-4 z-10">
|
||||||
|
<div @click="toggle(item.id)" class="h-[30px] w-[200px]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import ArrowIcon from './arrow-icon.svg?component'
|
||||||
|
import StartIcon from './Star-1.svg?component'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
|
// Sliced WebP Images
|
||||||
|
// Method 1
|
||||||
|
import m1_1 from './mobile1/row-1-column-1.webp'
|
||||||
|
import m1_2 from './mobile1/row-2-column-1.webp'
|
||||||
|
import m1_3 from './mobile1/row-3-column-1.webp'
|
||||||
|
import pc1_1 from './pc1/row-1-column-1.webp'
|
||||||
|
import pc1_2 from './pc1/row-2-column-1.webp'
|
||||||
|
import pc1_3 from './pc1/row-3-column-1.webp'
|
||||||
|
|
||||||
|
// Method 2
|
||||||
|
import m2_1 from './mobile2/row-1-column-1.webp'
|
||||||
|
import m2_2 from './mobile2/row-2-column-1.webp'
|
||||||
|
import m2_3 from './mobile2/row-3-column-1.webp'
|
||||||
|
import pc2_1 from './pc2/row-1-column-1.webp'
|
||||||
|
import pc2_2 from './pc2/row-2-column-1.webp'
|
||||||
|
import pc2_3 from './pc2/row-3-column-1.webp'
|
||||||
|
|
||||||
|
// Method 3
|
||||||
|
import m3_1 from './mobile3/row-1-column-1.webp'
|
||||||
|
import m3_2 from './mobile3/row-2-column-1.webp'
|
||||||
|
import m3_3 from './mobile3/row-3-column-1.webp'
|
||||||
|
import pc3_1 from './pc3/row-1-column-1.webp'
|
||||||
|
import pc3_2 from './pc3/row-2-column-1.webp'
|
||||||
|
import pc3_3 from './pc3/row-3-column-1.webp'
|
||||||
|
|
||||||
|
interface Hotzone {
|
||||||
|
x: number // 百分比
|
||||||
|
y: number // 百分比
|
||||||
|
w: number // 百分比
|
||||||
|
h: number // 百分比
|
||||||
|
type: 'copy' | 'link'
|
||||||
|
payload: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接在内部定义数据
|
||||||
|
const downloadMethods = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '创建新“香港Apple ID”',
|
||||||
|
subtitle: '首推',
|
||||||
|
isHot: true,
|
||||||
|
highlight: '',
|
||||||
|
mobileImages: [m1_1, m1_2, m1_3],
|
||||||
|
pcImages: [pc1_1, pc1_2, pc1_3],
|
||||||
|
mobileHotzones: [] as Hotzone[],
|
||||||
|
pcHotzones: [] as Hotzone[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '修改已有 Apple ID 至 <香港>',
|
||||||
|
subtitle: '',
|
||||||
|
isHot: false,
|
||||||
|
highlight: '',
|
||||||
|
mobileImages: [m2_1, m2_2, m2_3],
|
||||||
|
pcImages: [pc2_1, pc2_2, pc2_3],
|
||||||
|
mobileHotzones: [] as Hotzone[],
|
||||||
|
pcHotzones: [] as Hotzone[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '使用Hi快提供的公用账号',
|
||||||
|
subtitle: '',
|
||||||
|
isHot: false,
|
||||||
|
highlight: '谨慎选择',
|
||||||
|
mobileImages: [m3_1, m3_2, m3_3],
|
||||||
|
pcImages: [pc3_1, pc3_2, pc3_3],
|
||||||
|
mobileHotzones: [] as Hotzone[],
|
||||||
|
pcHotzones: [] as Hotzone[],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const activeId = ref<number | null>(null)
|
||||||
|
|
||||||
|
const toggle = (id: number) => {
|
||||||
|
activeId.value = activeId.value === id ? null : id
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHotzone = (hz: Hotzone) => {
|
||||||
|
if (hz.type === 'copy') {
|
||||||
|
navigator.clipboard.writeText(hz.payload)
|
||||||
|
toast.success(`${hz.label || '内容'}已复制`)
|
||||||
|
} else if (hz.type === 'link') {
|
||||||
|
window.open(hz.payload, '_blank')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 针对整体风格进行微调 */
|
||||||
|
.font-bold {
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除移动端点击的高亮蓝色框 */
|
||||||
|
div {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.5105 0L11.7556 6.90983H19.0211L13.1432 11.1803L15.3884 18.0902L9.5105 13.8197L3.63265 18.0902L5.87779 11.1803L-6.67572e-05 6.90983H7.26536L9.5105 0Z" fill="#ADFF5B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.35098 0.101416L4.35098 7.62314C4.35079 7.64339 4.34446 7.66314 4.33279 7.6799C4.32112 7.69666 4.30463 7.70968 4.28539 7.71733C4.26615 7.72497 4.24502 7.72689 4.22467 7.72286C4.20431 7.71882 4.18563 7.70901 4.17097 7.69465L0.178818 3.7225C0.164119 3.70767 0.145131 3.69756 0.124387 3.69352C0.103641 3.68948 0.0821265 3.69171 0.0627125 3.6999C0.0432995 3.7081 0.0269087 3.72187 0.0157297 3.73938C0.00455077 3.75689 -0.000885175 3.77731 0.000145746 3.79792L0.105484 7.51912C0.105567 7.53222 0.10832 7.54517 0.113585 7.55723C0.11885 7.56928 0.126521 7.58019 0.136151 7.58933L5.52169 12.9683C5.53164 12.9783 5.54355 12.9863 5.55672 12.9917C5.5699 12.9972 5.58405 13 5.59836 13C5.61267 13 5.62683 12.9972 5.64 12.9917C5.65317 12.9863 5.66508 12.9783 5.67503 12.9683L11.1966 7.61793C11.2067 7.60781 11.2146 7.5958 11.2199 7.58262C11.2252 7.56944 11.2277 7.55536 11.2272 7.54122L11.1672 4.14767C11.1668 4.12768 11.1604 4.10825 11.1488 4.09176C11.1372 4.07527 11.1209 4.06244 11.1019 4.05485C11.083 4.04726 11.0621 4.04523 11.042 4.04901C11.0219 4.0528 11.0033 4.06223 10.9886 4.07616L7.19242 7.67124C7.17788 7.68535 7.15939 7.69494 7.13926 7.69882C7.11914 7.7027 7.0983 7.70069 7.07934 7.69305C7.06039 7.68541 7.04418 7.67247 7.03274 7.65587C7.02131 7.63926 7.01516 7.61973 7.01508 7.59973L7.01508 0.221036C7.01576 0.185813 7.01843 0.150654 7.02308 0.115719C7.02518 0.101361 7.0241 0.0867316 7.0199 0.0728175C7.01571 0.0589035 7.0085 0.0460284 6.99877 0.0350608C6.98904 0.0240932 6.97701 0.0152882 6.96349 0.00923998C6.94998 0.00319171 6.93529 4.07951e-05 6.92041 -1.8826e-07L4.45499 -2.96027e-07C4.4274 -2.97233e-07 4.40095 0.0106846 4.38144 0.0297039C4.36194 0.0487232 4.35098 0.0745188 4.35098 0.101416Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 192 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="11" viewBox="0 0 20 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.664368 0.747314L9.66437 8.74731L18.6644 0.747314" stroke="white" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 199 B |
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-black font-sans text-white">
|
||||||
|
<div class="rounded-[30px]">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div v-for="(item, index) in faqList" :key="index" class="flex flex-col">
|
||||||
|
<div
|
||||||
|
@click="toggle(index)"
|
||||||
|
class="flex cursor-pointer items-center justify-between py-4 transition-opacity active:opacity-70 md:py-8"
|
||||||
|
>
|
||||||
|
<h3 class="pr-5 text-[17px] leading-snug font-bold tracking-tight md:text-xl">
|
||||||
|
{{ item.question }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ArrowIcon
|
||||||
|
:class="[
|
||||||
|
'mt-1 h-5 w-5 shrink-0 transition-transform duration-300',
|
||||||
|
activeIds.has(index) ? 'rotate-180' : '',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
enter-from-class="max-h-0 opacity-0"
|
||||||
|
enter-to-class="max-h-[500px] opacity-100"
|
||||||
|
leave-from-class="max-h-[500px] opacity-100"
|
||||||
|
leave-to-class="max-h-0 opacity-0"
|
||||||
|
>
|
||||||
|
<div v-if="activeIds.has(index)" class="overflow-hidden">
|
||||||
|
<div class="pb-6 text-base leading-relaxed text-white/60">
|
||||||
|
<p
|
||||||
|
class="mb-6 whitespace-pre-line last:mb-0"
|
||||||
|
v-for="(answer, answerIndex) in item.answer"
|
||||||
|
:key="answerIndex"
|
||||||
|
>
|
||||||
|
{{ answer }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<div class="h-[1px] w-full bg-[#757575]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import ArrowIcon from './arrow-icon2.svg?component'
|
||||||
|
|
||||||
|
const faqList = ref([
|
||||||
|
{
|
||||||
|
question: '登陆公共 Apple ID 时,出现“双重验证”提示,且要求输入验证码怎么办?',
|
||||||
|
answer: [
|
||||||
|
'这是由于有其他用户没有按照教程操作,将公共 Apple ID 绑定手机号码,开启了双重验证所导致。',
|
||||||
|
'解决办法:请您先清除浏览器缓存,再刷新本页面,获取新的公共 Apple ID ,接着请按照教程一步一步操作。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: '手机显示“丢失的 iPhone”,要求联系非官方的私人账号怎么办?',
|
||||||
|
answer: [
|
||||||
|
'这是因为您在手机设置中登陆了公开账号,导致设备被其他用户设置为“丢失模式”。',
|
||||||
|
'解决方法:输入正确的手机锁屏密码即可解除“丢失模式”,随后请从手机设置中 <立即退出公开账号>。切勿相信或添加任何非Hi快官方的私人账号!如有疑问,请通过右下角的官方客服获取帮助。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: '下载Hi快 App 时提示出现 “App 不可用” 怎么办?',
|
||||||
|
answer: [
|
||||||
|
'遇到这种情况请首先检查您登录的 Apple ID 地区是否处于 <中国大陆>;或者是否刚切换过地区,Apple 可能还没完成信息同步。',
|
||||||
|
'解决办法:重新退出登录 <香港 Apple ID>,再进行下载。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: '登入 App Store 却提示“验证失败”怎么办?',
|
||||||
|
answer: [
|
||||||
|
'这种错误场景会出现在您使用 <Apple ID A> 下载Hi快 VPN,然后使用 <Apple ID B> 进行版本更新。',
|
||||||
|
'解决办法:登录<Apple ID A>更新Hi快,或者删除旧版本,重新安装新版本。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: '使用Hi快 App 时却提示 “不能购买此项目” 怎么办?',
|
||||||
|
answer: [
|
||||||
|
'遇到此情况可能说明您的手机中还存在Hi快 VPN 的 <历史旧版本>,它会与 App Store 版本产生冲突。',
|
||||||
|
'解决办法:删除旧版本,重新安装新版本。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: '下载Hi快 App 时提示出现 “此项目已不再提供” 怎么办?',
|
||||||
|
answer: [
|
||||||
|
'遇到此情况可能说明您的手机中还存在Hi快 VPN 的 <历史旧版本>,它会与 App Store 版本产生冲突。',
|
||||||
|
'解决办法:删除旧版本,重新安装新版本。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 1. 使用 reactive 配合 Set 记录所有展开的 ID
|
||||||
|
const activeIds = reactive(new Set<number>())
|
||||||
|
|
||||||
|
// 2. 修改 toggle 逻辑:如果已存在则删除(收起),不存在则添加(展开)
|
||||||
|
const toggle = (index: number) => {
|
||||||
|
if (activeIds.has(index)) {
|
||||||
|
activeIds.delete(index)
|
||||||
|
} else {
|
||||||
|
activeIds.add(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 61 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M30.5 0C13.6572 0 0 10.2105 0 22.8242C0 28.0805 2.38578 32.8937 6.38467 36.7806C6.44567 36.9215 6.51344 37.0491 6.56767 37.1968C7.00057 38.4969 7.06851 39.8891 6.76422 41.2246C6.40891 42.8122 5.6666 44.2895 4.60211 45.5276C3.34634 46.966 1.77109 48.0965 0 48.8304C2.0481 49.6896 4.26434 50.0841 6.48633 49.9851C9.01057 49.8736 11.4803 49.2209 13.725 48.0718C15.7601 47.0254 17.595 45.6351 19.1472 43.9635C22.8299 45.0581 26.6551 45.61 30.5 45.6015C47.3428 45.6015 61 35.391 61 22.7772C61 10.1635 47.3428 0 30.5 0ZM32.4859 34.7599H28.5141V30.8261H32.4859V34.7599ZM33.9499 14.8089L32.4588 28.7652H28.5412L27.0501 14.8089V11.882H33.9499V14.8089Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg width="49" height="73" viewBox="0 0 49 73" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.175484 64L2.94748 55.06H5.51548L8.28748 64H6.03148L4.91548 59.536C4.79548 59.088 4.67548 58.612 4.55548 58.108C4.44348 57.604 4.32748 57.124 4.20748 56.668H4.15948C4.05548 57.132 3.94348 57.616 3.82348 58.12C3.70348 58.616 3.58748 59.088 3.47548 59.536L2.35948 64H0.175484ZM2.05948 61.948V60.292H6.37948V61.948H2.05948ZM9.05736 64V57.172H10.7974L10.9534 58.024H10.9894C11.2694 57.744 11.5854 57.504 11.9374 57.304C12.2974 57.104 12.7094 57.004 13.1734 57.004C13.9174 57.004 14.4574 57.252 14.7934 57.748C15.1294 58.244 15.2974 58.92 15.2974 59.776V64H13.1614V60.04C13.1614 59.568 13.0974 59.248 12.9694 59.08C12.8414 58.912 12.6414 58.828 12.3694 58.828C12.1294 58.828 11.9254 58.88 11.7574 58.984C11.5894 59.088 11.4014 59.24 11.1934 59.44V64H9.05736ZM19.4038 64.168C18.8358 64.168 18.3398 64.024 17.9158 63.736C17.4998 63.448 17.1758 63.036 16.9438 62.5C16.7198 61.964 16.6078 61.324 16.6078 60.58C16.6078 59.836 16.7438 59.2 17.0158 58.672C17.2878 58.136 17.6398 57.724 18.0718 57.436C18.5038 57.148 18.9558 57.004 19.4278 57.004C19.8038 57.004 20.1158 57.068 20.3638 57.196C20.6118 57.324 20.8478 57.496 21.0718 57.712L20.9998 56.692V54.412H23.1358V64H21.3958L21.2398 63.34H21.1918C20.9518 63.58 20.6758 63.78 20.3638 63.94C20.0518 64.092 19.7318 64.168 19.4038 64.168ZM19.9678 62.416C20.1758 62.416 20.3598 62.376 20.5198 62.296C20.6878 62.216 20.8478 62.064 20.9998 61.84V59.152C20.8398 59 20.6678 58.896 20.4838 58.84C20.2998 58.776 20.1158 58.744 19.9318 58.744C19.7398 58.744 19.5558 58.808 19.3798 58.936C19.2118 59.064 19.0718 59.26 18.9598 59.524C18.8558 59.788 18.8038 60.132 18.8038 60.556C18.8038 60.988 18.8478 61.344 18.9358 61.624C19.0318 61.896 19.1678 62.096 19.3438 62.224C19.5198 62.352 19.7278 62.416 19.9678 62.416ZM24.8542 64V57.172H26.5942L26.7502 58.36H26.7862C27.0422 57.904 27.3422 57.564 27.6862 57.34C28.0382 57.116 28.3862 57.004 28.7302 57.004C28.9382 57.004 29.1022 57.02 29.2222 57.052C29.3502 57.076 29.4622 57.108 29.5582 57.148L29.2102 58.984C29.0822 58.952 28.9622 58.928 28.8502 58.912C28.7382 58.888 28.6062 58.876 28.4542 58.876C28.2062 58.876 27.9422 58.968 27.6622 59.152C27.3902 59.336 27.1662 59.652 26.9902 60.1V64H24.8542ZM33.162 64.168C32.586 64.168 32.042 64.028 31.53 63.748C31.018 63.46 30.602 63.048 30.282 62.512C29.97 61.976 29.814 61.332 29.814 60.58C29.814 59.828 29.97 59.188 30.282 58.66C30.602 58.124 31.018 57.716 31.53 57.436C32.042 57.148 32.586 57.004 33.162 57.004C33.594 57.004 34.01 57.084 34.41 57.244C34.81 57.404 35.166 57.64 35.478 57.952C35.79 58.256 36.038 58.628 36.222 59.068C36.406 59.508 36.498 60.012 36.498 60.58C36.498 61.332 36.338 61.976 36.018 62.512C35.706 63.048 35.294 63.46 34.782 63.748C34.278 64.028 33.738 64.168 33.162 64.168ZM33.162 62.44C33.426 62.44 33.642 62.364 33.81 62.212C33.986 62.06 34.114 61.844 34.194 61.564C34.274 61.284 34.314 60.956 34.314 60.58C34.314 60.204 34.274 59.88 34.194 59.608C34.114 59.328 33.986 59.112 33.81 58.96C33.642 58.808 33.426 58.732 33.162 58.732C32.898 58.732 32.678 58.808 32.502 58.96C32.334 59.112 32.206 59.328 32.118 59.608C32.038 59.88 31.998 60.204 31.998 60.58C31.998 60.956 32.038 61.284 32.118 61.564C32.206 61.844 32.334 62.06 32.502 62.212C32.678 62.364 32.898 62.44 33.162 62.44ZM37.8386 64V57.172H39.9746V64H37.8386ZM38.9066 56.224C38.5466 56.224 38.2586 56.124 38.0426 55.924C37.8266 55.724 37.7186 55.456 37.7186 55.12C37.7186 54.792 37.8266 54.528 38.0426 54.328C38.2586 54.128 38.5466 54.028 38.9066 54.028C39.2586 54.028 39.5426 54.128 39.7586 54.328C39.9826 54.528 40.0946 54.792 40.0946 55.12C40.0946 55.456 39.9826 55.724 39.7586 55.924C39.5426 56.124 39.2586 56.224 38.9066 56.224ZM44.1421 64.168C43.5741 64.168 43.0781 64.024 42.6541 63.736C42.2381 63.448 41.9141 63.036 41.6821 62.5C41.4581 61.964 41.3461 61.324 41.3461 60.58C41.3461 59.836 41.4821 59.2 41.7541 58.672C42.0261 58.136 42.3781 57.724 42.8101 57.436C43.2421 57.148 43.6941 57.004 44.1661 57.004C44.5421 57.004 44.8541 57.068 45.1021 57.196C45.3501 57.324 45.5861 57.496 45.8101 57.712L45.7381 56.692V54.412H47.8741V64H46.1341L45.9781 63.34H45.9301C45.6901 63.58 45.4141 63.78 45.1021 63.94C44.7901 64.092 44.4701 64.168 44.1421 64.168ZM44.7061 62.416C44.9141 62.416 45.0981 62.376 45.2581 62.296C45.4261 62.216 45.5861 62.064 45.7381 61.84V59.152C45.5781 59 45.4061 58.896 45.2221 58.84C45.0381 58.776 44.8541 58.744 44.6701 58.744C44.4781 58.744 44.2941 58.808 44.1181 58.936C43.9501 59.064 43.8101 59.26 43.6981 59.524C43.5941 59.788 43.5421 60.132 43.5421 60.556C43.5421 60.988 43.5861 61.344 43.6741 61.624C43.7701 61.896 43.9061 62.096 44.0821 62.224C44.2581 62.352 44.4661 62.416 44.7061 62.416Z" fill="white"/>
|
||||||
|
<g clip-path="url(#clip0_9522_1052)">
|
||||||
|
<path d="M28.3624 37.0001C29.5995 37.0001 30.6784 35.9213 30.6784 34.6842V29.2754H32.2248C33.1527 29.2754 33.7712 28.6567 33.7712 27.729V12.2808H15.229V27.7291C15.229 28.657 15.8476 29.2755 16.7754 29.2755H18.3219V34.6843C18.3219 35.9214 19.4005 37.0003 20.6379 37.0003C21.8749 37.0003 22.9536 35.9214 22.9536 34.6843V29.2755H26.0466V34.6843C26.0464 35.9213 27.1253 37.0001 28.3624 37.0001Z" fill="white"/>
|
||||||
|
<path d="M37.6333 27.7291C38.8705 27.7291 39.9491 26.6504 39.9491 25.4132V14.5957C39.9491 13.3652 38.8705 12.2808 37.6333 12.2808C36.3961 12.2808 35.3174 13.3652 35.3174 14.5957V25.4132C35.3174 26.6503 36.396 27.7291 37.6333 27.7291Z" fill="white"/>
|
||||||
|
<path d="M11.3665 27.7291C12.6037 27.7291 13.6825 26.6504 13.6825 25.4132V14.5957C13.6825 13.3652 12.6039 12.2808 11.3665 12.2808C10.1294 12.2808 9.05078 13.3652 9.05078 14.5957V25.4132C9.05078 26.6503 10.1294 27.7291 11.3665 27.7291Z" fill="white"/>
|
||||||
|
<path d="M31.9154 0.23335C31.6062 -0.0777832 31.1459 -0.0777832 30.8368 0.23335L28.7627 2.30053L28.667 2.39609C27.4371 1.78025 26.0583 1.4727 24.5227 1.46973C24.5152 1.46973 24.5078 1.46949 24.5003 1.46949H24.5C24.4922 1.46949 24.4851 1.46973 24.4773 1.46973C22.9419 1.4727 21.563 1.78025 20.3333 2.39609L20.2374 2.30053L18.1634 0.23335C17.854 -0.0777832 17.394 -0.0777832 17.0848 0.23335C16.7754 0.542754 16.7754 1.00192 17.0848 1.31108L19.0911 3.31776C18.4447 3.7494 17.8584 4.27758 17.3487 4.8817C16.1282 6.32835 15.3498 8.21107 15.2427 10.251C15.2417 10.2722 15.2396 10.2932 15.2386 10.3144C15.2321 10.4533 15.229 10.593 15.229 10.7331H33.7712C33.7712 10.593 33.7678 10.4533 33.7615 10.3144C33.7605 10.2932 33.7584 10.2722 33.7572 10.251C33.6504 8.21107 32.8717 6.32822 31.6513 4.88182C31.1417 4.27771 30.5553 3.74952 29.9088 3.31789L31.9154 1.3112C32.2248 1.00192 32.2248 0.542754 31.9154 0.23335ZM20.6349 8.03303C19.9951 8.03303 19.4765 7.51436 19.4765 6.87456C19.4765 6.23476 19.9951 5.71608 20.6349 5.71608C21.2747 5.71608 21.7934 6.23476 21.7934 6.87456C21.7934 7.51436 21.2747 8.03303 20.6349 8.03303ZM28.3652 8.03303C27.7254 8.03303 27.2068 7.51436 27.2068 6.87456C27.2068 6.23476 27.7254 5.71608 28.3652 5.71608C29.005 5.71608 29.5237 6.23476 29.5237 6.87456C29.5237 7.51436 29.005 8.03303 28.3652 8.03303Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_9522_1052">
|
||||||
|
<rect width="37" height="37" fill="white" transform="translate(6)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -1,10 +1,155 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto p-8">
|
<div class="min-h-screen bg-black text-white">
|
||||||
<h1 class="text-3xl font-bold mb-4">Help Page</h1>
|
<!-- Full Width Header -->
|
||||||
<p class="text-gray-600">This is the help page content.</p>
|
<div class="h-[88px] md:h-[94px]">
|
||||||
|
<div class="fixed z-50 w-full bg-black pt-[20px] md:pt-[34px]">
|
||||||
|
<div class="container">
|
||||||
|
<header
|
||||||
|
class="flex h-[68px] items-center justify-between rounded-full bg-[#ADFF5B] pr-[30px] pl-6 md:h-[60px] md:pr-[58px] md:pl-[41px]"
|
||||||
|
>
|
||||||
|
<router-link to="/" class="flex items-center gap-2">
|
||||||
|
<!-- Desktop Logo -->
|
||||||
|
<!-- <Logo :src="Logo" alt="Hi快VPN" class="hidden h-10 w-auto text-black md:block" />-->
|
||||||
|
<!-- Mobile Logo -->
|
||||||
|
<MobileLogo alt="Hi快VPN" class="block h-[28px] text-black md:h-[36px]" />
|
||||||
|
</router-link>
|
||||||
|
<RightText class="block h-[15px] text-black md:h-[24px]" />
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-[125px] md:h-[180px]">
|
||||||
|
<div class="fixed z-50 w-full bg-black">
|
||||||
|
<div class="m-auto w-[340px] pb-[10px] md:w-[760px]">
|
||||||
|
<div class="pt-[30px] pb-[15px] text-center text-xl font-[900] md:pb-[30px] md:text-2xl">
|
||||||
|
请选择适用您情形的选项
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="w-1/2 rounded-full border-5 p-[6px] md:border-[10px] md:p-[10px]"
|
||||||
|
:class="[activeIndex === 0 ? 'border-white' : 'border-black']"
|
||||||
|
@click="activeIndex = 0"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="h-[30px] w-full cursor-pointer rounded-full bg-[#ADFF5B] text-xs text-black hover:bg-[#ADFF5B]/90 md:h-[40px] md:text-xl"
|
||||||
|
>
|
||||||
|
我只有中国大陆Apple ID
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-1/2 rounded-full border-5 p-[6px] md:border-[10px] md:p-[10px]"
|
||||||
|
:class="[activeIndex === 1 ? 'border-white' : 'border-black']"
|
||||||
|
@click="activeIndex = 1"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="h-[30px] w-full cursor-pointer rounded-full bg-[#ADFF5B] text-xs text-black hover:bg-[#ADFF5B]/90 md:h-[40px] md:text-xl"
|
||||||
|
>
|
||||||
|
我有海外Apple ID
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-auto w-[340px] md:w-[760px]">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<!-- tab1 -->
|
||||||
|
<div v-if="activeIndex === 0" key="tab1">
|
||||||
|
<div class="pt-[15px] pb-[15px] text-center text-xl font-[900] md:pb-[39px] md:text-2xl">
|
||||||
|
Hi快提供三种下载方式
|
||||||
|
</div>
|
||||||
|
<DownloadMethodList />
|
||||||
|
</div>
|
||||||
|
<!-- tab2 -->
|
||||||
|
<div v-else-if="activeIndex === 1" key="tab2">
|
||||||
|
<div
|
||||||
|
class="mx-auto flex w-[237px] items-center justify-center pt-[34px] text-center text-xl font-[900] md:w-full md:gap-[18px] md:text-2xl"
|
||||||
|
>
|
||||||
|
<ChatIcon class="h-[60px] w-[50px]" />登录海外 Apple ID 后再点击下方下载按钮
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto h-[101px] w-[247px] md:h-[202px] md:w-[494px]">
|
||||||
|
<a
|
||||||
|
href="https://apps.apple.com/us/app/hi%E5%BF%ABvpn/id6755683167"
|
||||||
|
target="_blank"
|
||||||
|
class="block h-full w-full"
|
||||||
|
>
|
||||||
|
<img src="./Group%20100.png" class="h-full w-full" alt="下载" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container md:px-[92px]">
|
||||||
|
<div class="pt-[56px] pb-[15px] text-center text-xl font-[900] md:text-2xl">
|
||||||
|
常见问题与解答
|
||||||
|
</div>
|
||||||
|
<div class="px-[24px]">
|
||||||
|
<FAQAccordion />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container px-[92px] pt-[34px]">
|
||||||
|
<div class="mx-auto w-[240px] text-center font-black text-[#757575] md:w-full md:text-xl">
|
||||||
|
若您在下载过程中遇到任何问题,
|
||||||
|
<br class="md:hidden" />
|
||||||
|
点击右下角在线客服按钮,
|
||||||
|
<br class="md:hidden" />
|
||||||
|
会有专业客服帮您解决问题
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[18px] pt-[34px] pb-[calc(50px+env(safe-area-inset-bottom))]">
|
||||||
|
<div class="pb-[15px] text-center text-xl font-[900] md:pb-[30px] md:text-3xl">
|
||||||
|
其他客户端下载
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center gap-4">
|
||||||
|
<template v-for="client in otherClients" :key="client.link">
|
||||||
|
<a :href="client.link" target="_blank">
|
||||||
|
<MacosIcon v-if="client.type === 'mac'" />
|
||||||
|
<WindowsIcon v-if="client.type === 'window'" />
|
||||||
|
<AndroidIcon v-if="client.type === 'android'" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
// Help page logic
|
// Help page logic
|
||||||
|
import Logo from '@/pages/Home/logo.svg?component'
|
||||||
|
import MobileLogo from '@/pages/Home/mobile-logo.svg?component'
|
||||||
|
import RightText from './right-text.svg?component'
|
||||||
|
import ChatIcon from './Vector.svg?component'
|
||||||
|
import MacosIcon from './macos.svg?component'
|
||||||
|
import WindowsIcon from './windows.svg?component'
|
||||||
|
import AndroidIcon from './android.svg?component'
|
||||||
|
|
||||||
|
import DownloadMethodList from './DownloadMethodList/DownloadMethodList.vue'
|
||||||
|
import FAQAccordion from './FAQAccordion/index.vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { downLoadAndroid, downLoadWin, downLoadMac } from '@/utils/constant.ts'
|
||||||
|
|
||||||
|
const activeIndex = ref(0)
|
||||||
|
|
||||||
|
const otherClients = [
|
||||||
|
{ icon: WindowsIcon, link: downLoadWin, type: 'window' },
|
||||||
|
{ icon: MacosIcon, link: downLoadMac, type: 'mac' },
|
||||||
|
{ icon: AndroidIcon, link: downLoadAndroid, type: 'android' },
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 382 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="!pointer-events-auto fixed inset-0 z-[100] flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="backdrop-blur-[2px z-[110]] absolute inset-0 bg-black/40"
|
||||||
|
@click.stop="hide"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative z-10 w-full max-w-[280px] rounded-[40px] border border-white/10 bg-[#999999] p-8 shadow-2xl"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="space-y-2 text-black">
|
||||||
|
<h3 class="flex items-start text-lg font-black"><span class="mr-1">*</span>重要提示</h3>
|
||||||
|
<p class="text-[15px] leading-relaxed font-bold tracking-tight">
|
||||||
|
验证邮件已发送至邮箱,如无法找到,请检查垃圾邮件箱或营销邮件箱。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const timer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
console.log('hide')
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载前清除计时器防止内存泄漏
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,17 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<div class="mx-auto grid grid-cols-2 gap-4 md:flex md:flex-wrap">
|
||||||
class="flex h-[40px] w-[140px] items-center space-x-2 bg-[url('@/pages/Home/liquid-button-bg.png')]"
|
<template v-for="(item, index) in downloadLinks" :key="index">
|
||||||
>
|
<router-link
|
||||||
<component :is="icon" class="h-6 w-6" />
|
v-if="item.isInternal"
|
||||||
<span class="text-sm font-medium">{{ label }}</span>
|
:to="item.link"
|
||||||
</button>
|
class="flex h-[40px] w-[140px] shrink-0 items-center space-x-2 rounded-full transition-transform hover:brightness-110 active:scale-95 md:h-[50px] md:w-[180px]"
|
||||||
|
style="
|
||||||
|
backdrop-filter: blur(36px);
|
||||||
|
box-shadow:
|
||||||
|
0px 0px 33px 0px #f2f2f280 inset,
|
||||||
|
-1px -1.5px 1.5px -3px #b3b3b3 inset,
|
||||||
|
3px 4.5px 1.5px -3px #b3b3b333 inset,
|
||||||
|
4.5px 4.5px 1.5px -5.25px #ffffff80 inset,
|
||||||
|
-4.5px -4.5px 1.5px -5.25px #ffffff80 inset;
|
||||||
|
border-image-source: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.4) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 30%
|
||||||
|
);
|
||||||
|
border-image-slice: 1;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<component :is="item.icon" class="h-[40px] w-[140px] md:h-[50px] md:w-[180px]" />
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="item.link"
|
||||||
|
target="_blank"
|
||||||
|
class="flex h-[40px] w-[140px] shrink-0 items-center space-x-2 rounded-full transition-transform hover:brightness-110 active:scale-95 md:h-[50px] md:w-[180px]"
|
||||||
|
style="
|
||||||
|
backdrop-filter: blur(36px);
|
||||||
|
box-shadow:
|
||||||
|
0px 0px 33px 0px #f2f2f280 inset,
|
||||||
|
-1px -1.5px 1.5px -3px #b3b3b3 inset,
|
||||||
|
3px 4.5px 1.5px -3px #b3b3b333 inset,
|
||||||
|
4.5px 4.5px 1.5px -5.25px #ffffff80 inset,
|
||||||
|
-4.5px -4.5px 1.5px -5.25px #ffffff80 inset;
|
||||||
|
border-image-source: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.4) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 30%
|
||||||
|
);
|
||||||
|
border-image-slice: 1;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<component :is="item.icon" class="h-[40px] w-[140px] md:h-[50px] md:w-[180px]" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Component } from 'vue'
|
import Icon1 from './Group 105.svg?component'
|
||||||
|
import Icon2 from './Group 106.svg?component'
|
||||||
defineProps<{
|
import Icon3 from './Group 107.svg?component'
|
||||||
icon: Component
|
import Icon4 from './Group 108.svg?component'
|
||||||
label: string
|
import { downLoadAndroid, downLoadWin, downLoadMac } from '@/utils/constant.ts'
|
||||||
}>()
|
// 定义下载链接数据
|
||||||
|
const downloadLinks = [
|
||||||
|
{ icon: Icon2, link: '/help', isInternal: true },
|
||||||
|
{ icon: Icon1, link: downLoadWin },
|
||||||
|
{ icon: Icon3, link: downLoadMac },
|
||||||
|
{ icon: Icon4, link: downLoadAndroid },
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
@@ -1,26 +1,116 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grid gap-4 py-4">
|
<div class="flex flex-col gap-6 text-base text-black md:text-2xl">
|
||||||
<div class="grid gap-2">
|
<div class="overflow-hidden rounded-[20px] bg-[#78788029] px-4">
|
||||||
<label for="email" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Email</label>
|
<div class="relative">
|
||||||
<input
|
<Input
|
||||||
id="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="m@example.com"
|
placeholder="Email"
|
||||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
class="h-[50px] border-none bg-transparent text-base focus-visible:ring-0 md:text-2xl"
|
||||||
/>
|
/>
|
||||||
|
<div class="absolute bottom-0 left-0 h-[1px] w-full bg-gray-400/30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<Input
|
||||||
|
v-model="code"
|
||||||
|
type="text"
|
||||||
|
placeholder="验证码"
|
||||||
|
class="h-[50px] border-none bg-transparent text-base focus-visible:ring-0 md:text-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
@click="handleGetCode"
|
||||||
|
:disabled="countdown > 0"
|
||||||
|
class="h-10 rounded-full bg-[#4B94E6] px-[32px] text-base font-bold text-white hover:bg-[#4B94E6]/90 md:px-[45px] md:text-[18px]"
|
||||||
|
>
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : '获取' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
|
||||||
<label for="password" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Password</label>
|
<div class="flex gap-4 pt-2">
|
||||||
<input
|
<Button
|
||||||
id="password"
|
variant="secondary"
|
||||||
type="password"
|
@click="emit('close')"
|
||||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
class="h-[48px] flex-1 cursor-pointer rounded-[25px] bg-[#D1D1D1] text-lg font-medium text-[#757575] hover:bg-[#C1C1C1]"
|
||||||
/>
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
@click="handleLogin"
|
||||||
|
class="h-[48px] flex-1 cursor-pointer rounded-[25px] bg-[#A8FF53] text-lg font-medium text-black hover:bg-[#96E64A]"
|
||||||
|
>
|
||||||
|
登录/注册
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" class="w-full">Sign In</Button>
|
<CodeSentTip ref="CodeSentTipRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import CodeSentTip from '@/pages/Home/components/CodeSentTip.vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const CodeSentTipRef = ref(null)
|
||||||
|
const email = ref('')
|
||||||
|
const code = ref('')
|
||||||
|
const countdown = ref(0)
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
const handleGetCode = () => {
|
||||||
|
if (!email.value) {
|
||||||
|
toast('请输入邮箱')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CodeSentTipRef.value.show()
|
||||||
|
|
||||||
|
request
|
||||||
|
.get('/api/v1/auth/check', {
|
||||||
|
email: email.value,
|
||||||
|
})
|
||||||
|
.then(({ exist }) => {
|
||||||
|
console.log(exist)
|
||||||
|
request
|
||||||
|
.post('/api/v1/common/send_code', {
|
||||||
|
// 1=登录, 2=注册,
|
||||||
|
email: email.value,
|
||||||
|
type: exist ? 2 : 1,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
countdown.value = 60
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) clearInterval(timer)
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const router = useRouter()
|
||||||
|
const handleLogin = () => {
|
||||||
|
request
|
||||||
|
.post('/api/v1/auth/login/email', {
|
||||||
|
email: email.value,
|
||||||
|
code: code.value,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
localStorage.setItem('Authorization', res.token)
|
||||||
|
localStorage.setItem('UserEmail', email.value)
|
||||||
|
router.push({ path: '/user-center' })
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 移除 Shadcn Input 的默认边框阴影,保持纯净感 */
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :open="isOpen" @update:open="setOpen">
|
<Dialog :open="isOpen" @update:open="setOpen">
|
||||||
<DialogContent class="sm:max-w-[425px]">
|
<DialogContent
|
||||||
<DialogHeader>
|
@pointer-down-outside="(event) => event.preventDefault()"
|
||||||
<DialogTitle>Login</DialogTitle>
|
@focus-outside="(event) => event.preventDefault()"
|
||||||
<DialogDescription>
|
class="max-w-[300px] rounded-4xl bg-[#DDDDDD] p-[14px] md:max-w-[390px]"
|
||||||
Enter your credentials to access your account.
|
:showCloseButton="false"
|
||||||
|
>
|
||||||
|
<DialogHeader class="py-2 pl-2">
|
||||||
|
<DialogTitle class="text-left text-base md:text-2xl">登录Hi快帐户</DialogTitle>
|
||||||
|
<DialogDescription class="text-left text-base text-black md:text-2xl">
|
||||||
|
如使用新邮箱地址,将自动为您创建Hi快帐户
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<LoginForm />
|
<LoginForm @close="isOpen = false" />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,6 +44,6 @@ const hide = () => {
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
hide
|
hide,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@@ -1,100 +1,161 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="relative min-h-screen overflow-hidden bg-[#24963e] pb-[calc(1rem+env(safe-area-inset-bottom))] font-sans text-white md:pb-0"
|
class="relative min-h-screen overflow-hidden bg-[#24963e] bg-[url('@/pages/Home/bg-mobile.jpg')] bg-cover bg-center bg-no-repeat pb-[calc(1rem+env(safe-area-inset-bottom))] font-sans text-white md:flex md:flex-col md:bg-[url('@/pages/Home/bg-desktop.webp')] md:pb-0"
|
||||||
>
|
>
|
||||||
<!-- Background Gradient Effects -->
|
|
||||||
<div
|
|
||||||
class="absolute top-[-10%] left-[-10%] h-[500px] w-[500px] rounded-full bg-[#34d399] opacity-20 blur-[100px]"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Full Width Header -->
|
<!-- Full Width Header -->
|
||||||
<div class="h-[88px]">
|
<div class="h-[88px] md:h-[125px]">
|
||||||
<header
|
<div class="fixed top-[20px] z-50 w-full md:top-[45px]">
|
||||||
class="fixed top-[20px] right-[18px] left-[18px] z-50 flex h-[68px] items-center justify-between rounded-[90px] pr-[10px] pl-6 transition-all duration-300"
|
<div class="container">
|
||||||
style="
|
<header
|
||||||
backdrop-filter: blur(36px);
|
class="flex h-[68px] items-center justify-between rounded-[90px] pr-[10px] transition-all duration-300 md:h-[80px]"
|
||||||
box-shadow:
|
style="
|
||||||
0px 0px 33px 0px #f2f2f280 inset,
|
backdrop-filter: blur(36px);
|
||||||
-3px -4.5px 1.5px -3px #b3b3b3 inset,
|
box-shadow:
|
||||||
3px 4.5px 1.5px -3px #b3b3b333 inset,
|
0px 0px 33px 0px #f2f2f280 inset,
|
||||||
4.5px 4.5px 1.5px -5.25px #ffffff80 inset,
|
-1px -1.5px 1.5px -3px #b3b3b3 inset,
|
||||||
-4.5px -4.5px 1.5px -5.25px #ffffff80 inset;
|
3px 4.5px 1.5px -3px #b3b3b333 inset,
|
||||||
border-image-source: linear-gradient(
|
4.5px 4.5px 1.5px -5.25px #ffffff80 inset,
|
||||||
135deg,
|
-4.5px -4.5px 1.5px -5.25px #ffffff80 inset;
|
||||||
rgba(255, 255, 255, 0.4) 0%,
|
border-image-source: linear-gradient(
|
||||||
rgba(255, 255, 255, 0) 30%
|
135deg,
|
||||||
);
|
rgba(255, 255, 255, 0.4) 0%,
|
||||||
border-image-slice: 1;
|
rgba(255, 255, 255, 0) 30%
|
||||||
"
|
);
|
||||||
>
|
border-image-slice: 1;
|
||||||
<div class="flex items-center gap-2">
|
"
|
||||||
<!-- Desktop Logo -->
|
>
|
||||||
<img :src="Logo" alt="Hi快VPN" class="hidden h-10 w-auto md:block" />
|
<router-link to="/" class="flex items-center gap-2">
|
||||||
<!-- Mobile Logo -->
|
<!-- Desktop Logo -->
|
||||||
<img :src="MobileLogo" alt="Hi快VPN" class="block h-[28px] w-[67px] w-auto md:hidden" />
|
<Logo alt="Hi快VPN" class="hidden h-[29px] w-auto md:ml-[62px] md:block" />
|
||||||
|
<!-- Mobile Logo -->
|
||||||
|
<MobileLogo alt="Hi快VPN" class="ml-6 block h-[28px] w-[67px] md:hidden" />
|
||||||
|
</router-link>
|
||||||
|
<div v-if="isLoggedIn" class="flex items-center">
|
||||||
|
<router-link
|
||||||
|
to="/user-center"
|
||||||
|
class="mr-2 flex size-[48px] items-center justify-center rounded-full bg-[#A8FF53] text-xl font-bold text-black shadow-lg transition hover:scale-105 md:size-[60px] md:text-3xl"
|
||||||
|
>
|
||||||
|
{{ userLetter }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
@click="openLoginModal"
|
||||||
|
class="flex h-[48px] cursor-pointer items-center justify-center rounded-full bg-[#78788029] px-6 text-sm font-bold backdrop-blur-md transition hover:brightness-110 md:h-[60px] md:w-[220px] md:text-2xl"
|
||||||
|
>
|
||||||
|
登录 / 注册
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</div>
|
||||||
@click="openLoginModal"
|
|
||||||
class="flex h-[48px] items-center rounded-full bg-[#70C877] px-6 text-sm font-bold backdrop-blur-md transition hover:bg-white/30"
|
|
||||||
>
|
|
||||||
登陆 / 注册
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content Container -->
|
<!-- Main Content Container -->
|
||||||
<div class="container mx-auto flex flex-1 flex-col md:px-6">
|
<div class="container mx-auto flex flex-1 flex-col">
|
||||||
<main class="f pt-10">
|
<main class="pt-10 md:grid md:flex-1 md:grid-cols-2 md:pt-0">
|
||||||
<!-- Left Column: Text & Downloads -->
|
<!-- Left Column: Text & Downloads -->
|
||||||
<div class="md:w-1/2">
|
<div class="md:flex md:w-[432px] md:flex-col md:justify-center">
|
||||||
<div class="mb-[20px] ml-[42px]">
|
<div class="mb-[20px] ml-[42px] md:ml-[17px]">
|
||||||
<h2 class="mb-2 text-5xl font-black italic md:text-7xl">
|
<h2 class="mb-2 text-2xl font-black md:text-8xl">
|
||||||
<img :src="Logo" alt="Hi快VPN" class="h-[34px] w-auto" />
|
<Logo class="h-[66px]" />
|
||||||
</h2>
|
</h2>
|
||||||
<p class="font-600 text-3xl md:text-4xl">网在我在, 网快我快</p>
|
<p class="font-600 text-3xl md:text-[48px]">网在我在, 网快我快</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Screenshot Image -->
|
<!-- Screenshot Image -->
|
||||||
<div class="relative z-10 mx-auto mb-9 w-full max-w-[182px]">
|
<div class="relative z-10 mx-auto mb-9 w-full max-w-[182px] md:hidden">
|
||||||
<img
|
<img
|
||||||
:src="ScreenshotMobile"
|
:src="ScreenshotMobile"
|
||||||
alt="App Screenshot"
|
alt="App Screenshot"
|
||||||
class="h-auto w-full drop-shadow-2xl"
|
class="h-auto w-full drop-shadow-2xl"
|
||||||
/>
|
/>
|
||||||
|
<div class="absolute right-[8%] bottom-[97px] z-50 aspect-square w-[58%]">
|
||||||
|
<!-- Ripple Animation (Background) -->
|
||||||
|
<div class="ripple-container absolute inset-0 top-[7px]">
|
||||||
|
<div class="ripple ripple-outer-3"></div>
|
||||||
|
<div class="ripple ripple-outer-2"></div>
|
||||||
|
<div class="ripple ripple-outer-1"></div>
|
||||||
|
<div class="ripple ripple-core"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Center Image (Foreground/Top Layer) -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bottom-0 z-100 bg-[url('@/pages/Home/connected-mobile-bg.png')] bg-cover bg-center bg-no-repeat"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Download Buttons Grid -->
|
<!-- Download Buttons Grid -->
|
||||||
<div class="mb-9 grid grid-cols-2 gap-4 px-[42px]">
|
<div
|
||||||
<DownloadButton :icon="AppleIcon" label="iPho版本下载" />
|
class="mg:mb-[65px] mb-9 flex grid-cols-2 flex-wrap gap-[10px] px-[24px] md:mt-[124px] md:ml-[17px] md:px-0"
|
||||||
<DownloadButton :icon="MonitorIcon" label="Wind下载" />
|
>
|
||||||
<DownloadButton :icon="SmartphoneIcon" label="Andro本下载" />
|
<DownloadButton />
|
||||||
<DownloadButton :icon="MonitorIcon" label="Mac电脑下载" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Features / Footer Info -->
|
<!-- Features / Footer Info -->
|
||||||
<div class="mb-5 w-full text-center text-xs leading-5 font-[300]">
|
<div
|
||||||
<p>最先进加密协议 - 安全有保障</p>
|
class="mb-5 w-full text-center text-xs leading-5 font-[300] md:ml-[17px] md:text-left md:text-sm"
|
||||||
<p>全真实IEPL专线 - IP纯净且稳定</p>
|
>
|
||||||
<p>4K高清 / 不限流量 - 网速有多快, Hi快有多快</p>
|
<p>最新加密协议-安全有保障</p>
|
||||||
<p>极速闪连功能 - 连接永远快人一步</p>
|
<p>IEPL专线-纯净、稳定</p>
|
||||||
|
<p>不限速/不限流-网速多快,Hi快多快</p>
|
||||||
|
<p>极速闪连-永远快人一步</p>
|
||||||
|
<div class="flex justify-center md:justify-start">
|
||||||
|
<a
|
||||||
|
href="https://x.com/hifasttech"
|
||||||
|
target="_blank"
|
||||||
|
class="mt-[12px] flex h-[30px] w-[100px] shrink-0 items-center justify-center space-x-2 rounded-full transition-transform hover:brightness-110 md:mt-[16px]"
|
||||||
|
style="
|
||||||
|
backdrop-filter: blur(36px);
|
||||||
|
box-shadow:
|
||||||
|
0px 0px 33px 0px #f2f2f280 inset,
|
||||||
|
-1px -1.5px 1.5px -3px #b3b3b3 inset,
|
||||||
|
3px 4.5px 1.5px -3px #b3b3b333 inset,
|
||||||
|
4.5px 4.5px 1.5px -5.25px #ffffff80 inset,
|
||||||
|
-4.5px -4.5px 1.5px -5.25px #ffffff80 inset;
|
||||||
|
border-image-source: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.4) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 30%
|
||||||
|
);
|
||||||
|
border-image-slice: 1;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<img src="./x-logo.png" class="h-[16px] w-[77px]" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-auto mb-1 h-[20px] w-[352px]">
|
<div class="mx-auto mb-1 h-[20px] w-[352px] md:ml-[17px]">
|
||||||
<img src="./image-1.png" alt="image" />
|
<img src="./image-1.png" alt="image" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-[10px] leading-[14px] font-[300]">
|
<div class="text-center text-[10px] leading-[14px] font-[300] md:ml-[17px] md:text-left">
|
||||||
<span class="font-[600]">Hi快VPN™</span> © All rights reserved.<br />
|
<span class="font-[600]">Hi快VPN™</span> © All rights reserved.<br />
|
||||||
<a href="#" class="underline">Terms of Service</a>
|
<router-link to="/terms-of-service" class="underline">Terms of Service</router-link>
|
||||||
<a href="#" class="ml-2 underline">Privacy Policy</a>
|
<router-link to="/privacy-policy" class="ml-2 underline">Privacy Policy</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: Phone Screenshot -->
|
<!-- Right Column: Phone Screenshot -->
|
||||||
<div class="relative mt-12 flex hidden w-full justify-center md:mt-0 md:w-1/2">
|
<div class="relative hidden w-full max-w-[632px] md:mt-0 md:block">
|
||||||
<!-- Decorative elements behind phone -->
|
<!-- Screenshot with Ripple Effect -->
|
||||||
<div
|
<div class="absolute right-[9%] bottom-0 z-50 aspect-square w-[58%] overflow-hidden">
|
||||||
class="absolute top-1/2 left-1/2 -z-0 h-[60%] w-[60%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#a3e635] opacity-20 blur-[60px]"
|
<!-- Ripple Animation (Background) -->
|
||||||
></div>
|
<div class="ripple-container absolute inset-0">
|
||||||
|
<div class="ripple ripple-outer-3"></div>
|
||||||
|
<div class="ripple ripple-outer-2"></div>
|
||||||
|
<div class="ripple ripple-outer-1"></div>
|
||||||
|
<div class="ripple ripple-core"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Center Image (Foreground/Top Layer) -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bottom-[36px] z-100 bg-[url('@/pages/Home/connected-bg.png')] bg-cover bg-center bg-no-repeat"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute right-[2vw] bottom-0 max-w-[632px]">
|
||||||
|
<img :src="ScreenshotDesktop" alt="App Screenshot" class="relative z-10 w-full" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,13 +165,64 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
|
import type { LocationQueryValue } from 'vue-router'
|
||||||
import LoginFormModal from './components/LoginFormModal.vue'
|
import LoginFormModal from './components/LoginFormModal.vue'
|
||||||
import DownloadButton from './components/DownloadButton.vue'
|
import DownloadButton from './components/DownloadButton.vue'
|
||||||
import Logo from './logo.svg'
|
import Logo from './logo.svg?component'
|
||||||
import MobileLogo from './mobile-logo.svg'
|
import MobileLogo from './mobile-logo.svg?component'
|
||||||
import ScreenshotMobile from './screenshot-mobile.png'
|
import ScreenshotMobile from './screenshot-mobile.png'
|
||||||
import { AppleIcon, MonitorIcon, SmartphoneIcon, LocateFixedIcon } from 'lucide-vue-next'
|
import ScreenshotDesktop from './screenshot-desktop.webp'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const token = useLocalStorage('Authorization', '')
|
||||||
|
const userEmail = useLocalStorage('UserEmail', '')
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
const userLetter = computed(() => {
|
||||||
|
if (!userEmail.value) return '?'
|
||||||
|
return userEmail.value.charAt(0).toUpperCase()
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
if (!token.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = (await request.get('/api/v1/public/user/info')) as any
|
||||||
|
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
|
||||||
|
if (emailInfo) {
|
||||||
|
userEmail.value = emailInfo.auth_identifier
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to fetch user info:', error)
|
||||||
|
if (error?.code === 401 || error?.status === 401) {
|
||||||
|
token.value = ''
|
||||||
|
userEmail.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUserInfo()
|
||||||
|
if (route.query.login === 'true') {
|
||||||
|
openLoginModal()
|
||||||
|
router.replace({ query: { ...route.query, login: undefined } })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.login,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal === 'true') {
|
||||||
|
openLoginModal()
|
||||||
|
router.replace({ query: { ...route.query, login: undefined } })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const loginModalRef = ref<InstanceType<typeof LoginFormModal> | null>(null)
|
const loginModalRef = ref<InstanceType<typeof LoginFormModal> | null>(null)
|
||||||
|
|
||||||
@@ -118,3 +230,63 @@ const openLoginModal = () => {
|
|||||||
loginModalRef.value?.show()
|
loginModalRef.value?.show()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ripple-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #a8ff53; /* Brand neon green */
|
||||||
|
will-change: width, height, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Core: Fixed minimum diameter (~57% of max) */
|
||||||
|
.ripple-core {
|
||||||
|
width: 57%;
|
||||||
|
height: 57%;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outer Layer 1: Opacity 0.8, 5.4s */
|
||||||
|
.ripple-outer-1 {
|
||||||
|
animation: breathe 5.4s ease-in-out infinite alternate;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outer Layer 2: Opacity 0.6, 4.2s */
|
||||||
|
.ripple-outer-2 {
|
||||||
|
animation: breathe 4.2s ease-in-out infinite alternate;
|
||||||
|
opacity: 0.6;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outer Layer 3 (Outer-most): Opacity 0.4, 3s */
|
||||||
|
.ripple-outer-3 {
|
||||||
|
animation: breathe 3s ease-in-out infinite alternate;
|
||||||
|
opacity: 0.4;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes breathe {
|
||||||
|
0% {
|
||||||
|
width: 57%;
|
||||||
|
height: 57%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<svg width="103" height="29" viewBox="0 0 103 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 103 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10.4016 11.4828H17.0339L19.3193 0H26.26L20.5763 28.7559H13.6144L15.8796 17.2933H9.24829L6.96193 28.7559H0L4.88123 4.20849L2.71884 0H12.6668L10.4016 11.4828Z" fill="white"/>
|
<path d="M10.4016 11.4828H17.0339L19.3193 0H26.26L20.5763 28.7559H13.6144L15.8796 17.2933H9.24829L6.96193 28.7559H0L4.88123 4.20849L2.71884 0H12.6668L10.4016 11.4828Z" fill="currentColor"/>
|
||||||
<path d="M29.5968 28.7559H22.6561L26.7156 8.2363H33.6708L29.5968 28.7559Z" fill="white"/>
|
<path d="M29.5968 28.7559H22.6561L26.7156 8.2363H33.6708L29.5968 28.7559Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.8974 5.31659L47.9128 6.28534L48.4606 3.90191H52.8411L53.7012 0H60.0375L59.3081 3.90191H67.3656L65.775 13.4087H67.7568L66.9216 18.8301H58.1346L65.775 24.4081L61.4983 28.7559L53.3629 22.3658L44.1319 28.7559L41.9109 26.556L41.4977 28.7559H35.4228L37.8303 16.29L33.6237 15.7134L36.7327 5.46556H39.9206L40.9768 0H46.896L45.8974 5.31659ZM42.6336 22.706L48.9036 18.8301H43.3612L42.6336 22.706ZM46.896 12.7283L44.7086 11.6538L44.3789 13.4087H51.042L52.0069 9.11375H49.4774L46.896 12.7283ZM57.4561 13.4087H59.5945L60.3768 9.11375H58.3172L57.4561 13.4087Z" fill="white"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.8974 5.31659L47.9128 6.28534L48.4606 3.90191H52.8411L53.7012 0H60.0375L59.3081 3.90191H67.3656L65.775 13.4087H67.7568L66.9216 18.8301H58.1346L65.775 24.4081L61.4983 28.7559L53.3629 22.3658L44.1319 28.7559L41.9109 26.556L41.4977 28.7559H35.4228L37.8303 16.29L33.6237 15.7134L36.7327 5.46556H39.9206L40.9768 0H46.896L45.8974 5.31659ZM42.6336 22.706L48.9036 18.8301H43.3612L42.6336 22.706ZM46.896 12.7283L44.7086 11.6538L44.3789 13.4087H51.042L52.0069 9.11375H49.4774L46.896 12.7283ZM57.4561 13.4087H59.5945L60.3768 9.11375H58.3172L57.4561 13.4087Z" fill="currentColor"/>
|
||||||
<path d="M75.7576 22.3514L78.5379 15.6634H81.5884L76.1459 28.7559H73.0955L72.3103 15.6634H75.3607L75.7576 22.3514Z" fill="white"/>
|
<path d="M75.7576 22.3514L78.5379 15.6634H81.5884L76.1459 28.7559H73.0955L72.3103 15.6634H75.3607L75.7576 22.3514Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M85.5412 15.6634C86.4527 15.6725 87.5357 15.7459 88.5464 16.0651C89.0518 16.2202 89.5124 16.4575 89.9275 16.7676C90.7578 17.3971 91.2998 18.3916 91.2999 19.9334C91.2999 21.1832 90.8303 22.4242 89.7564 23.3913C88.6734 24.3584 86.9764 24.9789 84.5215 24.9789H83.7632L83.0501 28.7559H79.9997L82.4994 15.6634H85.5412ZM84.2688 22.3331H85.027C86.7057 22.3331 88.4474 21.6942 88.4474 20.0247C88.4473 18.3279 86.6334 18.3092 85.3067 18.3092H85.0357L84.2688 22.3331Z" fill="white"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M85.5412 15.6634C86.4527 15.6725 87.5357 15.7459 88.5464 16.0651C89.0518 16.2202 89.5124 16.4575 89.9275 16.7676C90.7578 17.3971 91.2998 18.3916 91.2999 19.9334C91.2999 21.1832 90.8303 22.4242 89.7564 23.3913C88.6734 24.3584 86.9764 24.9789 84.5215 24.9789H83.7632L83.0501 28.7559H79.9997L82.4994 15.6634H85.5412ZM84.2688 22.3331H85.027C86.7057 22.3331 88.4474 21.6942 88.4474 20.0247C88.4473 18.3279 86.6334 18.3092 85.3067 18.3092H85.0357L84.2688 22.3331Z" fill="currentColor"/>
|
||||||
<path d="M98.0504 22.3149L99.3228 15.6634H102.373L99.8735 28.7559H96.8231L94.9462 22.114L93.6824 28.7559H90.6319L93.1317 15.6634H96.1821L98.0504 22.3149Z" fill="white"/>
|
<path d="M98.0504 22.3149L99.3228 15.6634H102.373L99.8735 28.7559H96.8231L94.9462 22.114L93.6824 28.7559H90.6319L93.1317 15.6634H96.1821L98.0504 22.3149Z" fill="currentColor"/>
|
||||||
<path d="M34.011 6.52561H27.0539L28.3407 0.0201823H35.3027L34.011 6.52561Z" fill="white"/>
|
<path d="M34.011 6.52561H27.0539L28.3407 0.0201823H35.3027L34.011 6.52561Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,18 +1,18 @@
|
|||||||
<svg width="67" height="28" viewBox="0 0 67 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 67 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6.64761 6.43747H10.3658L11.647 0H15.5382L12.3518 16.1211H8.44879L9.71872 9.69499H6.00106L4.71928 16.1211H0.816284L3.5528 2.35936L2.34052 0H7.91754L6.64761 6.43747Z" fill="white"/>
|
<path d="M6.64761 6.43747H10.3658L11.647 0H15.5382L12.3518 16.1211H8.44879L9.71872 9.69499H6.00106L4.71928 16.1211H0.816284L3.5528 2.35936L2.34052 0H7.91754L6.64761 6.43747Z" fill="currentColor"/>
|
||||||
<path d="M17.4089 16.1211H13.5177L15.7936 4.61743H19.6928L17.4089 16.1211Z" fill="white"/>
|
<path d="M17.4089 16.1211H13.5177L15.7936 4.61743H19.6928L17.4089 16.1211Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.5473 2.98059L27.6771 3.52369L27.9842 2.18749H30.4401L30.9223 0H34.4745L34.0656 2.18749H38.5828L37.6911 7.5172H38.8021L38.3339 10.5565H33.4077L37.6911 13.6837L35.2935 16.1211L30.7326 12.5387L25.5575 16.1211L24.3124 14.8879L24.0807 16.1211H20.675L22.0247 9.13249L19.6664 8.80922L21.4094 3.0641H23.1966L23.7887 0H27.1071L26.5473 2.98059ZM24.7176 12.7295L28.2326 10.5565H25.1254L24.7176 12.7295ZM27.1071 7.13574L25.8808 6.53337L25.696 7.5172H29.4314L29.9724 5.10935H28.5543L27.1071 7.13574ZM33.0273 7.5172H34.2261L34.6647 5.10935H33.5101L33.0273 7.5172Z" fill="white"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.5473 2.98059L27.6771 3.52369L27.9842 2.18749H30.4401L30.9223 0H34.4745L34.0656 2.18749H38.5828L37.6911 7.5172H38.8021L38.3339 10.5565H33.4077L37.6911 13.6837L35.2935 16.1211L30.7326 12.5387L25.5575 16.1211L24.3124 14.8879L24.0807 16.1211H20.675L22.0247 9.13249L19.6664 8.80922L21.4094 3.0641H23.1966L23.7887 0H27.1071L26.5473 2.98059ZM24.7176 12.7295L28.2326 10.5565H25.1254L24.7176 12.7295ZM27.1071 7.13574L25.8808 6.53337L25.696 7.5172H29.4314L29.9724 5.10935H28.5543L27.1071 7.13574ZM33.0273 7.5172H34.2261L34.6647 5.10935H33.5101L33.0273 7.5172Z" fill="currentColor"/>
|
||||||
<path d="M43.2875 12.5306L44.8462 8.7812H46.5563L43.5052 16.1211H41.7951L41.3549 8.7812H43.065L43.2875 12.5306Z" fill="white"/>
|
<path d="M43.2875 12.5306L44.8462 8.7812H46.5563L43.5052 16.1211H41.7951L41.3549 8.7812H43.065L43.2875 12.5306Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.7724 8.7812C49.2834 8.78632 49.8905 8.82745 50.4572 9.00642C50.7405 9.09336 50.9987 9.2264 51.2314 9.40027C51.6969 9.75318 52.0008 10.3107 52.0008 11.175C52.0008 11.8757 51.7376 12.5715 51.1355 13.1136C50.5284 13.6558 49.577 14.0037 48.2007 14.0037H47.7756L47.3758 16.1211H45.6657L47.0671 8.7812H48.7724ZM48.059 12.5204H48.4841C49.4252 12.5204 50.4017 12.1622 50.4017 11.2262C50.4016 10.275 49.3847 10.2645 48.6409 10.2645H48.489L48.059 12.5204Z" fill="white"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.7724 8.7812C49.2834 8.78632 49.8905 8.82745 50.4572 9.00642C50.7405 9.09336 50.9987 9.2264 51.2314 9.40027C51.6969 9.75318 52.0008 10.3107 52.0008 11.175C52.0008 11.8757 51.7376 12.5715 51.1355 13.1136C50.5284 13.6558 49.577 14.0037 48.2007 14.0037H47.7756L47.3758 16.1211H45.6657L47.0671 8.7812H48.7724ZM48.059 12.5204H48.4841C49.4252 12.5204 50.4017 12.1622 50.4017 11.2262C50.4016 10.275 49.3847 10.2645 48.6409 10.2645H48.489L48.059 12.5204Z" fill="currentColor"/>
|
||||||
<path d="M55.7853 12.5102L56.4986 8.7812H58.2088L56.8074 16.1211H55.0972L54.045 12.3976L53.3365 16.1211H51.6264L53.0278 8.7812H54.7379L55.7853 12.5102Z" fill="white"/>
|
<path d="M55.7853 12.5102L56.4986 8.7812H58.2088L56.8074 16.1211H55.0972L54.045 12.3976L53.3365 16.1211H51.6264L53.0278 8.7812H54.7379L55.7853 12.5102Z" fill="currentColor"/>
|
||||||
<path d="M19.8835 3.65839H15.9832L16.7047 0.0113146H20.6077L19.8835 3.65839Z" fill="white"/>
|
<path d="M19.8835 3.65839H15.9832L16.7047 0.0113146H20.6077L19.8835 3.65839Z" fill="currentColor"/>
|
||||||
<path d="M2.77626 25.5216C2.5952 25.2235 2.41414 24.9333 2.23308 24.651C1.98412 25.3412 1.6899 25.9373 1.35041 26.4392L0.852494 25.7961C1.19953 25.2471 1.48621 24.6039 1.71253 23.8824C1.41076 23.4588 1.09391 23.0431 0.777052 22.6431V27.349H0V20.3373H6.20133V26.4706C6.20133 27.0196 5.93728 27.2941 5.41673 27.2941H4.66231L4.46616 26.502L5.16023 26.5412C5.33375 26.5412 5.42428 26.4549 5.42428 26.2824V25.3961L4.9339 25.9059C4.73775 25.5451 4.54161 25.1922 4.33037 24.8471C4.05878 25.5373 3.74192 26.1255 3.3798 26.6275L2.88188 25.9843C3.25909 25.4196 3.57595 24.7765 3.83245 24.0392C3.53068 23.5765 3.21383 23.1294 2.88943 22.6902L3.37226 22.2039C3.59104 22.4706 3.83245 22.7843 4.08895 23.1608C4.21721 22.651 4.32282 22.1098 4.40581 21.5373L5.14514 21.6706C5.01689 22.5176 4.84337 23.2941 4.63214 23.9843C4.88864 24.3843 5.15269 24.8235 5.42428 25.2941V21.1216H0.777052V22.5569L1.22216 22.1098C1.44849 22.3608 1.6899 22.6588 1.9464 23.0039C2.05202 22.5333 2.14255 22.0314 2.21045 21.5137L2.93469 21.6471C2.82153 22.4314 2.67819 23.1373 2.50467 23.7804C2.76118 24.1412 3.02522 24.5412 3.29681 24.9804L2.77626 25.5216Z" fill="white"/>
|
<path d="M2.77626 25.5216C2.5952 25.2235 2.41414 24.9333 2.23308 24.651C1.98412 25.3412 1.6899 25.9373 1.35041 26.4392L0.852494 25.7961C1.19953 25.2471 1.48621 24.6039 1.71253 23.8824C1.41076 23.4588 1.09391 23.0431 0.777052 22.6431V27.349H0V20.3373H6.20133V26.4706C6.20133 27.0196 5.93728 27.2941 5.41673 27.2941H4.66231L4.46616 26.502L5.16023 26.5412C5.33375 26.5412 5.42428 26.4549 5.42428 26.2824V25.3961L4.9339 25.9059C4.73775 25.5451 4.54161 25.1922 4.33037 24.8471C4.05878 25.5373 3.74192 26.1255 3.3798 26.6275L2.88188 25.9843C3.25909 25.4196 3.57595 24.7765 3.83245 24.0392C3.53068 23.5765 3.21383 23.1294 2.88943 22.6902L3.37226 22.2039C3.59104 22.4706 3.83245 22.7843 4.08895 23.1608C4.21721 22.651 4.32282 22.1098 4.40581 21.5373L5.14514 21.6706C5.01689 22.5176 4.84337 23.2941 4.63214 23.9843C4.88864 24.3843 5.15269 24.8235 5.42428 25.2941V21.1216H0.777052V22.5569L1.22216 22.1098C1.44849 22.3608 1.6899 22.6588 1.9464 23.0039C2.05202 22.5333 2.14255 22.0314 2.21045 21.5137L2.93469 21.6471C2.82153 22.4314 2.67819 23.1373 2.50467 23.7804C2.76118 24.1412 3.02522 24.5412 3.29681 24.9804L2.77626 25.5216Z" fill="currentColor"/>
|
||||||
<path d="M9.55095 23.6471H11.173V22.298H11.9726V23.6471H13.7003V24.4471H11.9726V26.2824H14.0247V27.0824H9.27936V26.2824H11.173V24.4471H9.55095V23.6471ZM8.23072 24.4314C8.02702 24.6353 7.81579 24.8314 7.597 25.0275L7.09154 24.3451C7.93649 23.6157 8.61547 22.7843 9.12093 21.8588H7.36313V21.0588H9.49814C9.64148 20.7137 9.76219 20.3608 9.86781 20L10.6826 20.102C10.5921 20.4314 10.4864 20.7451 10.3733 21.0588H14.0247V21.8588H10.0262C9.73956 22.4549 9.40007 23.0118 9.00777 23.5294V27.3647H8.23072V24.4314Z" fill="white"/>
|
<path d="M9.55095 23.6471H11.173V22.298H11.9726V23.6471H13.7003V24.4471H11.9726V26.2824H14.0247V27.0824H9.27936V26.2824H11.173V24.4471H9.55095V23.6471ZM8.23072 24.4314C8.02702 24.6353 7.81579 24.8314 7.597 25.0275L7.09154 24.3451C7.93649 23.6157 8.61547 22.7843 9.12093 21.8588H7.36313V21.0588H9.49814C9.64148 20.7137 9.76219 20.3608 9.86781 20L10.6826 20.102C10.5921 20.4314 10.4864 20.7451 10.3733 21.0588H14.0247V21.8588H10.0262C9.73956 22.4549 9.40007 23.0118 9.00777 23.5294V27.3647H8.23072V24.4314Z" fill="currentColor"/>
|
||||||
<path d="M18.272 23.6471V24.4784C17.8722 24.6196 17.4573 24.7451 17.0197 24.8627V26.4549C17.0197 26.9725 16.7557 27.2392 16.2276 27.2392H15.3751L15.2016 26.4627C15.4656 26.4941 15.7146 26.5098 15.956 26.5098C16.1295 26.5098 16.22 26.4157 16.22 26.2431V25.0588C15.7825 25.1529 15.3373 25.2392 14.8696 25.3176L14.7564 24.4941C15.2695 24.4157 15.7598 24.3216 16.22 24.2196V22.8706H14.7489V22.0863H16.22V21.3569C15.8579 21.4353 15.4882 21.498 15.0959 21.5529L14.93 20.7765C15.9711 20.6588 16.9065 20.4314 17.7213 20.0863L17.9929 20.8549C17.6836 20.9725 17.3592 21.0745 17.0197 21.1686V22.0863H18.5663C18.521 21.4824 18.5059 20.8157 18.5059 20.0941H19.2981C19.3056 20.8471 19.3282 21.5059 19.3735 22.0863H21.6292V22.8706H19.4338C19.4791 23.3804 19.547 23.7961 19.63 24.1255C19.6602 24.251 19.6904 24.3608 19.7205 24.4706C20.0298 24.0706 20.3014 23.6235 20.5278 23.1373L21.2067 23.5137C20.8748 24.2196 20.4749 24.8392 20.0148 25.3569C20.0902 25.5451 20.1732 25.7098 20.2562 25.8353C20.4674 26.1804 20.6334 26.3529 20.739 26.3529C20.822 26.3529 20.905 25.9529 20.9955 25.1686L21.6971 25.5529C21.501 26.6745 21.2218 27.2392 20.8522 27.2392C20.5202 27.2392 20.1732 27.0039 19.8186 26.5412C19.6828 26.3608 19.5546 26.1569 19.4414 25.9216C18.9284 26.3608 18.355 26.7059 17.7289 26.9569L17.2913 26.2588C17.9854 25.9922 18.5964 25.6157 19.117 25.1294C19.034 24.8784 18.9586 24.6196 18.8907 24.3529C18.7775 23.9294 18.6945 23.4353 18.6342 22.8706H17.0197V24.0314C17.4573 23.9137 17.8797 23.7882 18.272 23.6471ZM20.1279 20.1647C20.656 20.5098 21.1087 20.8549 21.4859 21.2L20.9201 21.7882C20.6107 21.4431 20.1732 21.0824 19.5998 20.6902L20.1279 20.1647Z" fill="white"/>
|
<path d="M18.272 23.6471V24.4784C17.8722 24.6196 17.4573 24.7451 17.0197 24.8627V26.4549C17.0197 26.9725 16.7557 27.2392 16.2276 27.2392H15.3751L15.2016 26.4627C15.4656 26.4941 15.7146 26.5098 15.956 26.5098C16.1295 26.5098 16.22 26.4157 16.22 26.2431V25.0588C15.7825 25.1529 15.3373 25.2392 14.8696 25.3176L14.7564 24.4941C15.2695 24.4157 15.7598 24.3216 16.22 24.2196V22.8706H14.7489V22.0863H16.22V21.3569C15.8579 21.4353 15.4882 21.498 15.0959 21.5529L14.93 20.7765C15.9711 20.6588 16.9065 20.4314 17.7213 20.0863L17.9929 20.8549C17.6836 20.9725 17.3592 21.0745 17.0197 21.1686V22.0863H18.5663C18.521 21.4824 18.5059 20.8157 18.5059 20.0941H19.2981C19.3056 20.8471 19.3282 21.5059 19.3735 22.0863H21.6292V22.8706H19.4338C19.4791 23.3804 19.547 23.7961 19.63 24.1255C19.6602 24.251 19.6904 24.3608 19.7205 24.4706C20.0298 24.0706 20.3014 23.6235 20.5278 23.1373L21.2067 23.5137C20.8748 24.2196 20.4749 24.8392 20.0148 25.3569C20.0902 25.5451 20.1732 25.7098 20.2562 25.8353C20.4674 26.1804 20.6334 26.3529 20.739 26.3529C20.822 26.3529 20.905 25.9529 20.9955 25.1686L21.6971 25.5529C21.501 26.6745 21.2218 27.2392 20.8522 27.2392C20.5202 27.2392 20.1732 27.0039 19.8186 26.5412C19.6828 26.3608 19.5546 26.1569 19.4414 25.9216C18.9284 26.3608 18.355 26.7059 17.7289 26.9569L17.2913 26.2588C17.9854 25.9922 18.5964 25.6157 19.117 25.1294C19.034 24.8784 18.9586 24.6196 18.8907 24.3529C18.7775 23.9294 18.6945 23.4353 18.6342 22.8706H17.0197V24.0314C17.4573 23.9137 17.8797 23.7882 18.272 23.6471ZM20.1279 20.1647C20.656 20.5098 21.1087 20.8549 21.4859 21.2L20.9201 21.7882C20.6107 21.4431 20.1732 21.0824 19.5998 20.6902L20.1279 20.1647Z" fill="currentColor"/>
|
||||||
<path d="M24.6393 23.6471H26.2613V22.298H27.061V23.6471H28.7887V24.4471H27.061V26.2824H29.1131V27.0824H24.3678V26.2824H26.2613V24.4471H24.6393V23.6471ZM23.3191 24.4314C23.1154 24.6353 22.9042 24.8314 22.6854 25.0275L22.1799 24.3451C23.0249 23.6157 23.7039 22.7843 24.2093 21.8588H22.4515V21.0588H24.5865C24.7299 20.7137 24.8506 20.3608 24.9562 20L25.771 20.102C25.6804 20.4314 25.5748 20.7451 25.4617 21.0588H29.1131V21.8588H25.1146C24.8279 22.4549 24.4885 23.0118 24.0962 23.5294V27.3647H23.3191V24.4314Z" fill="white"/>
|
<path d="M24.6393 23.6471H26.2613V22.298H27.061V23.6471H28.7887V24.4471H27.061V26.2824H29.1131V27.0824H24.3678V26.2824H26.2613V24.4471H24.6393V23.6471ZM23.3191 24.4314C23.1154 24.6353 22.9042 24.8314 22.6854 25.0275L22.1799 24.3451C23.0249 23.6157 23.7039 22.7843 24.2093 21.8588H22.4515V21.0588H24.5865C24.7299 20.7137 24.8506 20.3608 24.9562 20L25.771 20.102C25.6804 20.4314 25.5748 20.7451 25.4617 21.0588H29.1131V21.8588H25.1146C24.8279 22.4549 24.4885 23.0118 24.0962 23.5294V27.3647H23.3191V24.4314Z" fill="currentColor"/>
|
||||||
<path d="M30.9689 25.3412C31.1726 25.3412 31.3461 25.4275 31.4819 25.6C31.6102 25.7725 31.6781 26 31.6781 26.2745C31.6781 26.6902 31.5574 27.051 31.331 27.3647C31.0972 27.6706 30.7803 27.8824 30.388 28V27.4824C30.5917 27.4039 30.7652 27.2784 30.9086 27.1059C31.0368 26.9176 31.1047 26.7373 31.1047 26.549C31.0595 26.5647 30.9991 26.5804 30.9161 26.5804C30.7501 26.5804 30.6143 26.5176 30.5012 26.4C30.388 26.2824 30.3352 26.1333 30.3352 25.9608C30.3352 25.7725 30.3956 25.6235 30.5163 25.5137C30.6294 25.3961 30.7803 25.3412 30.9689 25.3412Z" fill="white"/>
|
<path d="M30.9689 25.3412C31.1726 25.3412 31.3461 25.4275 31.4819 25.6C31.6102 25.7725 31.6781 26 31.6781 26.2745C31.6781 26.6902 31.5574 27.051 31.331 27.3647C31.0972 27.6706 30.7803 27.8824 30.388 28V27.4824C30.5917 27.4039 30.7652 27.2784 30.9086 27.1059C31.0368 26.9176 31.1047 26.7373 31.1047 26.549C31.0595 26.5647 30.9991 26.5804 30.9161 26.5804C30.7501 26.5804 30.6143 26.5176 30.5012 26.4C30.388 26.2824 30.3352 26.1333 30.3352 25.9608C30.3352 25.7725 30.3956 25.6235 30.5163 25.5137C30.6294 25.3961 30.7803 25.3412 30.9689 25.3412Z" fill="currentColor"/>
|
||||||
<path d="M40.4972 25.5216C40.3162 25.2235 40.1351 24.9333 39.9541 24.651C39.7051 25.3412 39.4109 25.9373 39.0714 26.4392L38.5735 25.7961C38.9205 25.2471 39.2072 24.6039 39.4335 23.8824C39.1317 23.4588 38.8149 23.0431 38.498 22.6431V27.349H37.721V20.3373H43.9223V26.4706C43.9223 27.0196 43.6583 27.2941 43.1377 27.2941H42.3833L42.1871 26.502L42.8812 26.5412C43.0547 26.5412 43.1453 26.4549 43.1453 26.2824V25.3961L42.6549 25.9059C42.4587 25.5451 42.2626 25.1922 42.0513 24.8471C41.7798 25.5373 41.4629 26.1255 41.1008 26.6275L40.6029 25.9843C40.9801 25.4196 41.2969 24.7765 41.5534 24.0392C41.2517 23.5765 40.9348 23.1294 40.6104 22.6902L41.0932 22.2039C41.312 22.4706 41.5534 22.7843 41.8099 23.1608C41.9382 22.651 42.0438 22.1098 42.1268 21.5373L42.8661 21.6706C42.7379 22.5176 42.5644 23.2941 42.3531 23.9843C42.6096 24.3843 42.8737 24.8235 43.1453 25.2941V21.1216H38.498V22.5569L38.9431 22.1098C39.1695 22.3608 39.4109 22.6588 39.6674 23.0039C39.773 22.5333 39.8635 22.0314 39.9314 21.5137L40.6557 21.6471C40.5425 22.4314 40.3992 23.1373 40.2257 23.7804C40.4822 24.1412 40.7462 24.5412 41.0178 24.9804L40.4972 25.5216Z" fill="white"/>
|
<path d="M40.4972 25.5216C40.3162 25.2235 40.1351 24.9333 39.9541 24.651C39.7051 25.3412 39.4109 25.9373 39.0714 26.4392L38.5735 25.7961C38.9205 25.2471 39.2072 24.6039 39.4335 23.8824C39.1317 23.4588 38.8149 23.0431 38.498 22.6431V27.349H37.721V20.3373H43.9223V26.4706C43.9223 27.0196 43.6583 27.2941 43.1377 27.2941H42.3833L42.1871 26.502L42.8812 26.5412C43.0547 26.5412 43.1453 26.4549 43.1453 26.2824V25.3961L42.6549 25.9059C42.4587 25.5451 42.2626 25.1922 42.0513 24.8471C41.7798 25.5373 41.4629 26.1255 41.1008 26.6275L40.6029 25.9843C40.9801 25.4196 41.2969 24.7765 41.5534 24.0392C41.2517 23.5765 40.9348 23.1294 40.6104 22.6902L41.0932 22.2039C41.312 22.4706 41.5534 22.7843 41.8099 23.1608C41.9382 22.651 42.0438 22.1098 42.1268 21.5373L42.8661 21.6706C42.7379 22.5176 42.5644 23.2941 42.3531 23.9843C42.6096 24.3843 42.8737 24.8235 43.1453 25.2941V21.1216H38.498V22.5569L38.9431 22.1098C39.1695 22.3608 39.4109 22.6588 39.6674 23.0039C39.773 22.5333 39.8635 22.0314 39.9314 21.5137L40.6557 21.6471C40.5425 22.4314 40.3992 23.1373 40.2257 23.7804C40.4822 24.1412 40.7462 24.5412 41.0178 24.9804L40.4972 25.5216Z" fill="currentColor"/>
|
||||||
<path d="M47.5963 21.2H48.8487V20.0941H49.6182V21.2H51.2175V23.5451H51.7079V24.3451H49.8596C50.3047 25.2863 50.9912 26.0627 51.9116 26.6588L51.376 27.3098C50.4556 26.6118 49.7615 25.7569 49.2938 24.7373C48.9317 25.8824 48.2829 26.7373 47.3323 27.302L46.8495 26.6275C47.7397 26.149 48.3281 25.3882 48.6223 24.3451H47.2946V23.5451H48.7808C48.826 23.2314 48.8487 22.902 48.8487 22.5569V21.9608H47.5963V21.2ZM49.6182 21.9608V22.3765C49.6182 22.7843 49.588 23.1765 49.5427 23.5451H50.4631V21.9608H49.6182ZM45.069 21.6314L45.6726 21.6784C45.6575 22.4627 45.5745 23.2078 45.4387 23.9137L44.805 23.7255C44.9559 23.0667 45.0464 22.3686 45.069 21.6314ZM47.1663 21.4275C47.3625 21.9294 47.5435 22.5098 47.6944 23.1608L47.0682 23.3255C46.9551 22.7765 46.8117 22.2431 46.6307 21.7333V27.3176H45.8536V20.102H46.6307V21.6078L47.1663 21.4275Z" fill="white"/>
|
<path d="M47.5963 21.2H48.8487V20.0941H49.6182V21.2H51.2175V23.5451H51.7079V24.3451H49.8596C50.3047 25.2863 50.9912 26.0627 51.9116 26.6588L51.376 27.3098C50.4556 26.6118 49.7615 25.7569 49.2938 24.7373C48.9317 25.8824 48.2829 26.7373 47.3323 27.302L46.8495 26.6275C47.7397 26.149 48.3281 25.3882 48.6223 24.3451H47.2946V23.5451H48.7808C48.826 23.2314 48.8487 22.902 48.8487 22.5569V21.9608H47.5963V21.2ZM49.6182 21.9608V22.3765C49.6182 22.7843 49.588 23.1765 49.5427 23.5451H50.4631V21.9608H49.6182ZM45.069 21.6314L45.6726 21.6784C45.6575 22.4627 45.5745 23.2078 45.4387 23.9137L44.805 23.7255C44.9559 23.0667 45.0464 22.3686 45.069 21.6314ZM47.1663 21.4275C47.3625 21.9294 47.5435 22.5098 47.6944 23.1608L47.0682 23.3255C46.9551 22.7765 46.8117 22.2431 46.6307 21.7333V27.3176H45.8536V20.102H46.6307V21.6078L47.1663 21.4275Z" fill="currentColor"/>
|
||||||
<path d="M55.993 23.6471V24.4784C55.5932 24.6196 55.1782 24.7451 54.7407 24.8627V26.4549C54.7407 26.9725 54.4766 27.2392 53.9485 27.2392H53.0961L52.9225 26.4627C53.1866 26.4941 53.4355 26.5098 53.6769 26.5098C53.8505 26.5098 53.941 26.4157 53.941 26.2431V25.0588C53.5034 25.1529 53.0583 25.2392 52.5906 25.3176L52.4774 24.4941C52.9904 24.4157 53.4808 24.3216 53.941 24.2196V22.8706H52.4699V22.0863H53.941V21.3569C53.5789 21.4353 53.2092 21.498 52.8169 21.5529L52.6509 20.7765C53.692 20.6588 54.6275 20.4314 55.4423 20.0863L55.7139 20.8549C55.4046 20.9725 55.0802 21.0745 54.7407 21.1686V22.0863H56.2872C56.242 21.4824 56.2269 20.8157 56.2269 20.0941H57.019C57.0266 20.8471 57.0492 21.5059 57.0945 22.0863H59.3502V22.8706H57.1548C57.2001 23.3804 57.268 23.7961 57.351 24.1255C57.3812 24.251 57.4113 24.3608 57.4415 24.4706C57.7508 24.0706 58.0224 23.6235 58.2487 23.1373L58.9277 23.5137C58.5958 24.2196 58.1959 24.8392 57.7357 25.3569C57.8112 25.5451 57.8942 25.7098 57.9771 25.8353C58.1884 26.1804 58.3544 26.3529 58.46 26.3529C58.543 26.3529 58.6259 25.9529 58.7165 25.1686L59.4181 25.5529C59.2219 26.6745 58.9428 27.2392 58.5731 27.2392C58.2412 27.2392 57.8942 27.0039 57.5396 26.5412C57.4038 26.3608 57.2755 26.1569 57.1624 25.9216C56.6494 26.3608 56.076 26.7059 55.4498 26.9569L55.0123 26.2588C55.7063 25.9922 56.3174 25.6157 56.838 25.1294C56.755 24.8784 56.6795 24.6196 56.6116 24.3529C56.4985 23.9294 56.4155 23.4353 56.3551 22.8706H54.7407V24.0314C55.1782 23.9137 55.6007 23.7882 55.993 23.6471ZM57.8489 20.1647C58.377 20.5098 58.8296 20.8549 59.2068 21.2L58.641 21.7882C58.3317 21.4431 57.8942 21.0824 57.3208 20.6902L57.8489 20.1647Z" fill="white"/>
|
<path d="M55.993 23.6471V24.4784C55.5932 24.6196 55.1782 24.7451 54.7407 24.8627V26.4549C54.7407 26.9725 54.4766 27.2392 53.9485 27.2392H53.0961L52.9225 26.4627C53.1866 26.4941 53.4355 26.5098 53.6769 26.5098C53.8505 26.5098 53.941 26.4157 53.941 26.2431V25.0588C53.5034 25.1529 53.0583 25.2392 52.5906 25.3176L52.4774 24.4941C52.9904 24.4157 53.4808 24.3216 53.941 24.2196V22.8706H52.4699V22.0863H53.941V21.3569C53.5789 21.4353 53.2092 21.498 52.8169 21.5529L52.6509 20.7765C53.692 20.6588 54.6275 20.4314 55.4423 20.0863L55.7139 20.8549C55.4046 20.9725 55.0802 21.0745 54.7407 21.1686V22.0863H56.2872C56.242 21.4824 56.2269 20.8157 56.2269 20.0941H57.019C57.0266 20.8471 57.0492 21.5059 57.0945 22.0863H59.3502V22.8706H57.1548C57.2001 23.3804 57.268 23.7961 57.351 24.1255C57.3812 24.251 57.4113 24.3608 57.4415 24.4706C57.7508 24.0706 58.0224 23.6235 58.2487 23.1373L58.9277 23.5137C58.5958 24.2196 58.1959 24.8392 57.7357 25.3569C57.8112 25.5451 57.8942 25.7098 57.9771 25.8353C58.1884 26.1804 58.3544 26.3529 58.46 26.3529C58.543 26.3529 58.6259 25.9529 58.7165 25.1686L59.4181 25.5529C59.2219 26.6745 58.9428 27.2392 58.5731 27.2392C58.2412 27.2392 57.8942 27.0039 57.5396 26.5412C57.4038 26.3608 57.2755 26.1569 57.1624 25.9216C56.6494 26.3608 56.076 26.7059 55.4498 26.9569L55.0123 26.2588C55.7063 25.9922 56.3174 25.6157 56.838 25.1294C56.755 24.8784 56.6795 24.6196 56.6116 24.3529C56.4985 23.9294 56.4155 23.4353 56.3551 22.8706H54.7407V24.0314C55.1782 23.9137 55.6007 23.7882 55.993 23.6471ZM57.8489 20.1647C58.377 20.5098 58.8296 20.8549 59.2068 21.2L58.641 21.7882C58.3317 21.4431 57.8942 21.0824 57.3208 20.6902L57.8489 20.1647Z" fill="currentColor"/>
|
||||||
<path d="M62.6847 21.2H63.9371V20.0941H64.7066V21.2H66.3059V23.5451H66.7963V24.3451H64.948C65.3931 25.2863 66.0796 26.0627 67 26.6588L66.4644 27.3098C65.544 26.6118 64.8499 25.7569 64.3822 24.7373C64.02 25.8824 63.3712 26.7373 62.4207 27.302L61.9378 26.6275C62.8281 26.149 63.4165 25.3882 63.7107 24.3451H62.383V23.5451H63.8692C63.9144 23.2314 63.9371 22.902 63.9371 22.5569V21.9608H62.6847V21.2ZM64.7066 21.9608V22.3765C64.7066 22.7843 64.6764 23.1765 64.6311 23.5451H65.5515V21.9608H64.7066ZM60.1574 21.6314L60.761 21.6784C60.7459 22.4627 60.6629 23.2078 60.5271 23.9137L59.8934 23.7255C60.0443 23.0667 60.1348 22.3686 60.1574 21.6314ZM62.2547 21.4275C62.4509 21.9294 62.6319 22.5098 62.7828 23.1608L62.1566 23.3255C62.0435 22.7765 61.9001 22.2431 61.7191 21.7333V27.3176H60.942V20.102H61.7191V21.6078L62.2547 21.4275Z" fill="white"/>
|
<path d="M62.6847 21.2H63.9371V20.0941H64.7066V21.2H66.3059V23.5451H66.7963V24.3451H64.948C65.3931 25.2863 66.0796 26.0627 67 26.6588L66.4644 27.3098C65.544 26.6118 64.8499 25.7569 64.3822 24.7373C64.02 25.8824 63.3712 26.7373 62.4207 27.302L61.9378 26.6275C62.8281 26.149 63.4165 25.3882 63.7107 24.3451H62.383V23.5451H63.8692C63.9144 23.2314 63.9371 22.902 63.9371 22.5569V21.9608H62.6847V21.2ZM64.7066 21.9608V22.3765C64.7066 22.7843 64.6764 23.1765 64.6311 23.5451H65.5515V21.9608H64.7066ZM60.1574 21.6314L60.761 21.6784C60.7459 22.4627 60.6629 23.2078 60.5271 23.9137L59.8934 23.7255C60.0443 23.0667 60.1348 22.3686 60.1574 21.6314ZM62.2547 21.4275C62.4509 21.9294 62.6319 22.5098 62.7828 23.1608L62.1566 23.3255C62.0435 22.7765 61.9001 22.2431 61.7191 21.7333V27.3176H60.942V20.102H61.7191V21.6078L62.2547 21.4275Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-black text-white" style="font-family: 'Roboto', sans-serif">
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="mx-auto max-w-[1000px] px-6 md:px-2 md:pt-16 md:pb-[50px]">
|
||||||
|
<!-- Mobile Images -->
|
||||||
|
<div class="md:hidden">
|
||||||
|
<img
|
||||||
|
src="./mobile/row-1-column-1.webp"
|
||||||
|
alt="Terms of Service - Part 1"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./mobile/row-2-column-1.webp"
|
||||||
|
alt="Terms of Service - Part 2"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./mobile/row-3-column-1.webp"
|
||||||
|
alt="Terms of Service - Part 3"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Images (2x) -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<img
|
||||||
|
src="./pc/row1.png"
|
||||||
|
alt="Terms of Service - Part 1"
|
||||||
|
class="mb-10 origin-left scale-50"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./pc/row2.png"
|
||||||
|
alt="Terms of Service - Part 2"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
style="width: 100%; height: auto"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./pc/row3.png"
|
||||||
|
alt="Terms of Service - Part 3"
|
||||||
|
class="m-auto mt-[60px] h-[28px] w-[594px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No additional logic needed for this static page
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* No additional styles needed */
|
||||||
|
</style>
|
||||||
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-black text-white" style="font-family: 'Roboto', sans-serif">
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="mx-auto max-w-[1000px] px-6 md:px-2 md:pt-16 md:pb-[50px]">
|
||||||
|
<!-- Mobile Images -->
|
||||||
|
<div class="md:hidden">
|
||||||
|
<img
|
||||||
|
src="./mobile/row-1-column-1.webp"
|
||||||
|
alt="Terms of Service - Part 1"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./mobile/row-2-column-1.webp"
|
||||||
|
alt="Terms of Service - Part 2"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./mobile/row-3-column-1.webp"
|
||||||
|
alt="Terms of Service - Part 3"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Images (2x) -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<img src="./pc/row1.png" alt="Terms of Service - Part 1" class="mb-10 h-[18px] w-[206px]" />
|
||||||
|
<img
|
||||||
|
src="./pc/row2.png"
|
||||||
|
alt="Terms of Service - Part 2"
|
||||||
|
class="mb-0 w-full"
|
||||||
|
style="width: 100%; height: auto"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./pc/row3.png"
|
||||||
|
alt="Terms of Service - Part 3"
|
||||||
|
class="m-auto mt-[60px] h-[70px] w-[693px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No additional logic needed for this static page
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* No additional styles needed */
|
||||||
|
</style>
|
||||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 335 B |
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Main Neon Green Card -->
|
||||||
|
<div class="flex h-[678px] justify-center">
|
||||||
|
<div
|
||||||
|
class="flex w-[345px] flex-col justify-between rounded-4xl border-5 border-white py-[32px] pb-[22px]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 flex min-h-[88px] flex-col items-center">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<UserCenterSkeleton v-if="isUserLoading" type="header" layout="desktop" />
|
||||||
|
<div v-else class="flex flex-col items-center">
|
||||||
|
<img src="../avatar.png" class="size-[60px]" alt="" />
|
||||||
|
<div class="flex flex-col justify-center text-white">
|
||||||
|
<div class="text-xl font-semibold">{{ userInfo.email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5 px-[20px] text-white">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<UserCenterSkeleton v-if="isUserLoading" type="devices" layout="desktop" />
|
||||||
|
<DeviceList v-else :devices="devices" @refresh="emit('refresh')" />
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="px-6 pt-8 pb-9">
|
||||||
|
<!-- <Button
|
||||||
|
variant="outline"
|
||||||
|
class="mb-[10px] h-[50px] w-full rounded-[32px] border-2 border-[#FF00B7] bg-transparent text-xl font-bold text-[#FF00B7] transition-all hover:bg-[#FF00FF]/90 active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
注销账户
|
||||||
|
</Button>-->
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
@click="logout"
|
||||||
|
class="h-[50px] w-full rounded-[32px] border-2 border-white bg-transparent text-xl font-bold text-white transition-all hover:bg-white/90 active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center text-xs" :class="{ 'text-[#FF00B7]': expireDateInfo.highlight }">
|
||||||
|
{{ expireDateInfo.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="ml-2.5 flex items-center overflow-hidden rounded-4xl bg-[#A8FF53] pt-[32px] pb-[22px]"
|
||||||
|
>
|
||||||
|
<div class="h-full w-[345px]">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<div v-if="isPlansLoading" class="p-8">
|
||||||
|
<UserCenterSkeleton type="plans" layout="desktop" />
|
||||||
|
</div>
|
||||||
|
<PlanCard
|
||||||
|
v-else
|
||||||
|
:plans="plans"
|
||||||
|
:currentPlanIndex="currentPlanIndex"
|
||||||
|
@select="handlePlanSelect"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx-[5px] h-[624px] w-[1px] bg-[url(@/pages/UserCenter/DesktopLayout/Line-8.png)]"
|
||||||
|
></div>
|
||||||
|
<div class="h-full w-[345px]">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<div v-if="isPaymentsLoading" class="p-8">
|
||||||
|
<UserCenterSkeleton type="payments" layout="desktop" />
|
||||||
|
</div>
|
||||||
|
<PaymentMethod
|
||||||
|
v-else
|
||||||
|
:methods="payments"
|
||||||
|
:selectedPlan="selectedPlan"
|
||||||
|
:is-paying="isPaying"
|
||||||
|
@pay="(id: number | string) => $emit('pay', id)"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import PlanCard from '@/components/user-center/PlanCard.vue'
|
||||||
|
import DeviceList from '@/components/user-center/DeviceList.vue'
|
||||||
|
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
|
||||||
|
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const props = defineProps<{
|
||||||
|
devices: any[]
|
||||||
|
plans: any[]
|
||||||
|
payments: any[]
|
||||||
|
alreadySubscribed: any[]
|
||||||
|
userInfo: { email: string }
|
||||||
|
selectedPlanId: string
|
||||||
|
selectedPlan: any
|
||||||
|
isPaying: boolean
|
||||||
|
isUserLoading: boolean
|
||||||
|
isPlansLoading: boolean
|
||||||
|
isPaymentsLoading: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
const handlePlanSelect = (id: string) => {
|
||||||
|
emit('select-plan', id)
|
||||||
|
}
|
||||||
|
const currentPlanIndex = computed(() => {
|
||||||
|
return props.plans.findIndex((p) => p.id === props.selectedPlanId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const expireDateInfo = computed(() => {
|
||||||
|
const first = props.alreadySubscribed[0]
|
||||||
|
let text = ''
|
||||||
|
let highlight = false
|
||||||
|
|
||||||
|
if (!first || !first.expireDate) {
|
||||||
|
text = '尚未购买套餐'
|
||||||
|
highlight = true
|
||||||
|
} else {
|
||||||
|
// 尝试解析日期,兼容多种格式
|
||||||
|
const dateStr = first.expireDate.replace(/ /g, 'T')
|
||||||
|
const expireDateTime = new Date(dateStr)
|
||||||
|
|
||||||
|
if (isNaN(expireDateTime.getTime())) {
|
||||||
|
text = '套餐信息无效'
|
||||||
|
} else if (expireDateTime < new Date()) {
|
||||||
|
const year = expireDateTime.getFullYear()
|
||||||
|
const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(expireDateTime.getDate()).padStart(2, '0')
|
||||||
|
text = `已于 ${year}/${month}/${day} 到期`
|
||||||
|
highlight = true
|
||||||
|
} else {
|
||||||
|
const year = expireDateTime.getFullYear()
|
||||||
|
const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(expireDateTime.getDate()).padStart(2, '0')
|
||||||
|
const hour = String(expireDateTime.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(expireDateTime.getMinutes()).padStart(2, '0')
|
||||||
|
const second = String(expireDateTime.getSeconds()).padStart(2, '0')
|
||||||
|
text = `到期时间:${year}/${month}/${day} ${hour}:${minute}:${second}`
|
||||||
|
highlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text, highlight }
|
||||||
|
})
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
router.push('/')
|
||||||
|
localStorage.removeItem('Authorization')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Simplified layout font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
|
||||||
|
|
||||||
|
.tracking-tight {
|
||||||
|
font-family: 'Inter', 'PingFang SC', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Main Neon Green Card -->
|
||||||
|
<div class="pt-[35px]">
|
||||||
|
<div class="mb-3 ml-[31px] flex h-[60px] items-center gap-3">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<UserCenterSkeleton v-if="isUserLoading" type="header" layout="mobile" />
|
||||||
|
<div v-else class="flex items-center gap-3 text-white">
|
||||||
|
<img src="../avatar.png" class="size-[60px]" alt="" />
|
||||||
|
<div class="flex h-full flex-col justify-center text-white">
|
||||||
|
<div class="text-xl font-semibold">{{ userInfo.email }}</div>
|
||||||
|
<div class="text-xs" :class="{ 'text-[#FF00B7]': expireDateInfo.highlight }">
|
||||||
|
{{ expireDateInfo.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5 pr-[25px] pl-[21px] text-white">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<UserCenterSkeleton v-if="isUserLoading" type="devices" layout="mobile" />
|
||||||
|
<DeviceList v-else :devices="devices" @refresh="emit('refresh')" />
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden rounded-4xl bg-[#A8FF53]">
|
||||||
|
<div class="pt-7">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<div v-if="isPlansLoading" class="px-6">
|
||||||
|
<UserCenterSkeleton type="plans" layout="mobile" />
|
||||||
|
</div>
|
||||||
|
<PlanCard
|
||||||
|
v-else
|
||||||
|
:plans="plans"
|
||||||
|
:currentPlanIndex="currentPlanIndex"
|
||||||
|
@select="handlePlanSelect"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
<div class="pt-7">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<div v-if="isPaymentsLoading" class="px-6">
|
||||||
|
<UserCenterSkeleton type="payments" layout="mobile" />
|
||||||
|
</div>
|
||||||
|
<PaymentMethod
|
||||||
|
v-else
|
||||||
|
:methods="payments"
|
||||||
|
:selectedPlan="selectedPlan"
|
||||||
|
:is-paying="isPaying"
|
||||||
|
@pay="(id: number | string) => $emit('pay', id)"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-6 pt-8 pb-[calc(50px+env(safe-area-inset-bottom))]">
|
||||||
|
<!-- <Button
|
||||||
|
variant="outline"
|
||||||
|
class="mb-[10px] h-[50px] w-full rounded-[32px] border-2 border-[#FF00B7] bg-transparent text-xl font-bold text-[#FF00B7] transition-all hover:bg-[#FF00FF]/90 active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
注销账户
|
||||||
|
</Button>-->
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
@click="logout"
|
||||||
|
class="h-[50px] w-full rounded-[32px] border-2 border-white bg-transparent text-xl font-bold text-white transition-all hover:bg-white/90 active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import PlanCard from '@/components/user-center/PlanCard.vue'
|
||||||
|
import DeviceList from '@/components/user-center/DeviceList.vue'
|
||||||
|
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
|
||||||
|
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const props = defineProps<{
|
||||||
|
devices: any[]
|
||||||
|
plans: any[]
|
||||||
|
payments: any[]
|
||||||
|
alreadySubscribed: any[]
|
||||||
|
userInfo: { email: string }
|
||||||
|
selectedPlanId: string
|
||||||
|
selectedPlan: any
|
||||||
|
isPaying: boolean
|
||||||
|
isUserLoading: boolean
|
||||||
|
isPlansLoading: boolean
|
||||||
|
isPaymentsLoading: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
const handlePlanSelect = (id: string) => {
|
||||||
|
emit('select-plan', id)
|
||||||
|
}
|
||||||
|
const currentPlanIndex = computed(() => {
|
||||||
|
return props.plans.findIndex((p) => p.id === props.selectedPlanId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const expireDateInfo = computed(() => {
|
||||||
|
const first = props.alreadySubscribed[0]
|
||||||
|
let text = ''
|
||||||
|
let highlight = false
|
||||||
|
|
||||||
|
if (!first || !first.expireDate) {
|
||||||
|
text = '尚未购买套餐'
|
||||||
|
highlight = true
|
||||||
|
} else {
|
||||||
|
// 尝试解析日期,兼容多种格式
|
||||||
|
const dateStr = first.expireDate.replace(/ /g, 'T')
|
||||||
|
const expireDateTime = new Date(dateStr)
|
||||||
|
|
||||||
|
if (isNaN(expireDateTime.getTime())) {
|
||||||
|
text = '套餐信息无效'
|
||||||
|
} else if (expireDateTime < new Date()) {
|
||||||
|
const year = expireDateTime.getFullYear()
|
||||||
|
const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(expireDateTime.getDate()).padStart(2, '0')
|
||||||
|
text = `已于 ${year}/${month}/${day} 到期`
|
||||||
|
highlight = true
|
||||||
|
} else {
|
||||||
|
const year = expireDateTime.getFullYear()
|
||||||
|
const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(expireDateTime.getDate()).padStart(2, '0')
|
||||||
|
const hour = String(expireDateTime.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(expireDateTime.getMinutes()).padStart(2, '0')
|
||||||
|
const second = String(expireDateTime.getSeconds()).padStart(2, '0')
|
||||||
|
text = `到期时间:${year}/${month}/${day} ${hour}:${minute}:${second}`
|
||||||
|
highlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text, highlight }
|
||||||
|
})
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
router.push('/')
|
||||||
|
localStorage.removeItem('Authorization')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Simplified layout font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
|
||||||
|
|
||||||
|
.tracking-tight {
|
||||||
|
font-family: 'Inter', 'PingFang SC', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,10 +1,278 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-min-vh bg-black">
|
<div class="flex min-h-screen flex-col bg-black text-black">
|
||||||
<h1 class="mb-4 text-3xl font-bold">User Center</h1>
|
<!-- Full Width Header -->
|
||||||
<p class="text-gray-600">This is the user profile page.</p>
|
<div class="h-[88px] md:h-[125px]">
|
||||||
|
<div class="fixed z-50 w-full bg-black pt-[20px] md:pt-[45px]">
|
||||||
|
<div class="container">
|
||||||
|
<header
|
||||||
|
class="flex min-h-[66px] items-center justify-between rounded-full bg-[#ADFF5B] pr-[30px] pl-6 md:h-[80px] md:pr-[58px] md:pl-[41px]"
|
||||||
|
>
|
||||||
|
<router-link to="/" class="flex items-center gap-2">
|
||||||
|
<!-- Desktop Logo -->
|
||||||
|
<!-- <Logo :src="Logo" alt="Hi快VPN" class="hidden h-10 w-auto text-black md:block" />-->
|
||||||
|
<!-- Mobile Logo -->
|
||||||
|
<MobileLogo
|
||||||
|
:src="MobileLogo"
|
||||||
|
alt="Hi快VPN"
|
||||||
|
class="block h-[28px] text-black md:h-[50px]"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<div class="text-xl font-[600] text-black md:text-3xl">个人账户</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<!-- Main Neon Green Card -->
|
||||||
|
<div class="container md:hidden">
|
||||||
|
<MobileLayout
|
||||||
|
:already-subscribed="alreadySubscribed"
|
||||||
|
:devices="devices"
|
||||||
|
:plans="plans"
|
||||||
|
:payments="payments"
|
||||||
|
:user-info="userSubInfo"
|
||||||
|
:selected-plan-id="selectedPlanId"
|
||||||
|
:selected-plan="activePlan"
|
||||||
|
:is-paying="isPaying"
|
||||||
|
:is-user-loading="isUserLoading"
|
||||||
|
:is-plans-loading="isPlansLoading"
|
||||||
|
:is-payments-loading="isPaymentsLoading"
|
||||||
|
@select-plan="handlePlanSelect"
|
||||||
|
@pay="handlePay"
|
||||||
|
@refresh="init"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hidden flex-1 items-center justify-center md:flex md:pb-[50px]">
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<DesktopLayout
|
||||||
|
:already-subscribed="alreadySubscribed"
|
||||||
|
:devices="devices"
|
||||||
|
:plans="plans"
|
||||||
|
:payments="payments"
|
||||||
|
:user-info="userSubInfo"
|
||||||
|
:selected-plan-id="selectedPlanId"
|
||||||
|
:selected-plan="activePlan"
|
||||||
|
:is-paying="isPaying"
|
||||||
|
:is-user-loading="isUserLoading"
|
||||||
|
:is-plans-loading="isPlansLoading"
|
||||||
|
:is-payments-loading="isPaymentsLoading"
|
||||||
|
@select-plan="handlePlanSelect"
|
||||||
|
@pay="handlePay"
|
||||||
|
@refresh="init"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OrderStatusDialog ref="orderStatusDialogRef" @close="handleStatusClose" @refresh="init" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// User center logic
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import MobileLayout from './MobileLayout/index.vue'
|
||||||
|
import DesktopLayout from './DesktopLayout/index.vue'
|
||||||
|
import OrderStatusDialog from '@/components/user-center/OrderStatusDialog.vue'
|
||||||
|
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
||||||
|
import Logo from '@/pages/Home/logo.svg?component'
|
||||||
|
import MobileLogo from '@/pages/Home/mobile-logo.svg?component'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
// --- State ---
|
||||||
|
const selectedPlanId = ref('p2')
|
||||||
|
const isPaying = ref(false)
|
||||||
|
const isUserLoading = ref(true)
|
||||||
|
const isPlansLoading = ref(true)
|
||||||
|
const isPaymentsLoading = ref(true)
|
||||||
|
const orderStatusDialogRef = ref<InstanceType<typeof OrderStatusDialog> | null>(null)
|
||||||
|
const selectedPayment = ref('alipay')
|
||||||
|
|
||||||
|
const devices = ref<any[]>([])
|
||||||
|
const plans = ref<any[]>([])
|
||||||
|
const payments = ref<any[]>([])
|
||||||
|
const alreadySubscribed = ref<any[]>([])
|
||||||
|
const userSubInfo = ref({
|
||||||
|
email: '',
|
||||||
|
expireDate: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Data Mapping ---
|
||||||
|
const mapPlans = (apiList: any[]) => {
|
||||||
|
const flattened: any[] = []
|
||||||
|
let globalIndex = 0
|
||||||
|
apiList.forEach((item: any) => {
|
||||||
|
const unitDays = item.unit_time === 'Day' ? 1 : 365
|
||||||
|
if (!item.discount || item.discount.length === 0) {
|
||||||
|
flattened.push({
|
||||||
|
id: item.id.toString(),
|
||||||
|
planId: item.id,
|
||||||
|
quantity: 1,
|
||||||
|
days: unitDays,
|
||||||
|
price: (item.unit_price / 100).toFixed(2),
|
||||||
|
daily: (item.unit_price / 100 / unitDays).toFixed(2),
|
||||||
|
discount: '',
|
||||||
|
})
|
||||||
|
globalIndex++
|
||||||
|
} else {
|
||||||
|
item.discount.forEach((opt: any) => {
|
||||||
|
const totalDays = opt.quantity * unitDays
|
||||||
|
const ratioToPay = opt.discount === 0 || opt.discount === undefined ? 100 : opt.discount
|
||||||
|
const totalPrice = (item.unit_price / 100) * opt.quantity * (ratioToPay / 100)
|
||||||
|
|
||||||
|
flattened.push({
|
||||||
|
id: `${item.id}-${opt.quantity}`,
|
||||||
|
planId: item.id,
|
||||||
|
quantity: opt.quantity,
|
||||||
|
days: totalDays,
|
||||||
|
price: totalPrice.toFixed(2),
|
||||||
|
daily: (totalPrice / totalDays).toFixed(2),
|
||||||
|
discount:
|
||||||
|
globalIndex > 0 && ratioToPay < 100 ? `${Math.ceil(100 - ratioToPay)}% off` : '',
|
||||||
|
})
|
||||||
|
globalIndex++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return flattened
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
const activePlan = computed(() => {
|
||||||
|
return plans.value.find((p) => p.id === selectedPlanId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handlePlanSelect = (id: string) => {
|
||||||
|
selectedPlanId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePay = (methodId: number | string) => {
|
||||||
|
if (isPaying.value) return
|
||||||
|
const plan = plans.value.find((p) => p.id === selectedPlanId.value)
|
||||||
|
if (!plan) return
|
||||||
|
|
||||||
|
const already = alreadySubscribed.value.find((s: any) => s.subscribe_id === plan.planId)
|
||||||
|
const isRenewal = !!already
|
||||||
|
const api = isRenewal ? '/api/v1/public/order/renewal' : '/api/v1/public/order/purchase'
|
||||||
|
const params: any = {
|
||||||
|
subscribe_id: plan.planId,
|
||||||
|
quantity: plan.quantity,
|
||||||
|
payment: methodId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRenewal) {
|
||||||
|
params.user_subscribe_id = already.id
|
||||||
|
}
|
||||||
|
isPaying.value = true
|
||||||
|
request
|
||||||
|
.post(api, params)
|
||||||
|
.then((res: any) => {
|
||||||
|
request
|
||||||
|
.post('/api/v1/public/portal/order/checkout', {
|
||||||
|
orderNo: res.order_no,
|
||||||
|
returnUrl: `${window.location.origin}/user-center?order_no=${res.order_no}`,
|
||||||
|
})
|
||||||
|
.then((checkoutRes: any) => {
|
||||||
|
if (checkoutRes.type === 'url' && checkoutRes.checkout_url) {
|
||||||
|
localStorage.setItem('pending_order_no', res.order_no)
|
||||||
|
console.log('pending_order_no', res.order_no)
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = checkoutRes.checkout_url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isPaying.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
isPaying.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
// 1. 用户信息 & 设备列表
|
||||||
|
isUserLoading.value = true
|
||||||
|
request
|
||||||
|
.get('/api/v1/public/user/info')
|
||||||
|
.then((res: any) => {
|
||||||
|
devices.value = res.user_devices
|
||||||
|
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
|
||||||
|
if (emailInfo) {
|
||||||
|
userSubInfo.value.email = emailInfo.auth_identifier
|
||||||
|
localStorage.setItem('UserEmail', emailInfo.auth_identifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isUserLoading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 已订阅列表 (非阻塞主要展示)
|
||||||
|
request.get('/api/v1/public/user/subscribe').then((res: any) => {
|
||||||
|
alreadySubscribed.value = res.list || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 订阅套餐列表
|
||||||
|
isPlansLoading.value = true
|
||||||
|
request
|
||||||
|
.get('/api/v1/public/subscribe/list')
|
||||||
|
.then((res: any) => {
|
||||||
|
plans.value = mapPlans(res.list || [])
|
||||||
|
if (plans.value.length > 0) {
|
||||||
|
selectedPlanId.value = plans.value[0].id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isPlansLoading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 获取支付方式
|
||||||
|
isPaymentsLoading.value = true
|
||||||
|
request
|
||||||
|
.get('/api/v1/public/payment/methods')
|
||||||
|
.then((res: any) => {
|
||||||
|
payments.value =
|
||||||
|
res.list?.filter((p: any) => p.platform !== 'apple_iap' && p.platform !== 'Stripe') || []
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isPaymentsLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
const orderNo = (route.query.order_no as string) || localStorage.getItem('pending_order_no')
|
||||||
|
if (orderNo) {
|
||||||
|
orderStatusDialogRef.value?.show(orderNo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleStatusClose() {
|
||||||
|
localStorage.removeItem('pending_order_no')
|
||||||
|
// 清除 URL 中的 order_no,防止刷新再次弹出
|
||||||
|
router.replace({ query: { ...route.query, order_no: undefined } })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Simplified layout font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
|
||||||
|
|
||||||
|
.tracking-tight {
|
||||||
|
font-family: 'Inter', 'PingFang SC', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -18,7 +18,25 @@ const router = createRouter({
|
|||||||
name: 'user-center',
|
name: 'user-center',
|
||||||
component: () => import('../pages/UserCenter/index.vue'),
|
component: () => import('../pages/UserCenter/index.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/terms-of-service',
|
||||||
|
name: 'terms-of-service',
|
||||||
|
component: () => import('../pages/TermsOfService/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/privacy-policy',
|
||||||
|
name: 'privacy-policy',
|
||||||
|
component: () => import('../pages/PrivacyPolicy/index.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
if (to.path === '/user-center' && !localStorage.getItem('Authorization')) {
|
||||||
|
next({ path: '/', query: { login: 'true' } })
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('/src/styles/roboto.woff2') format('woff2');
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
@@ -28,7 +36,30 @@
|
|||||||
0 0 33px 0 rgba(242, 242, 242, 0.50) inset;
|
0 0 33px 0 rgba(242, 242, 242, 0.50) inset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@layer components {
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
/* 1. 移动端默认:左右 18px 边距 */
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-right: 18px;
|
||||||
|
|
||||||
|
/* 2. 桌面端逻辑:当屏幕达到 1440px 及以上 */
|
||||||
|
@media (width >= 1440px) {
|
||||||
|
/* 计算公式:
|
||||||
|
内容区固定为 1268px
|
||||||
|
侧边留白 = (1440 - 1268) / 2 = 86px
|
||||||
|
*/
|
||||||
|
max-width: 1268px;
|
||||||
|
|
||||||
|
/* 此时 padding 可以设为 0,因为 max-width 配合 margin: auto 已经产生了 86px 的留白 */
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const downLoadAndroid = 'https://h.hifastapp.com/download/app-arm64-v8a-release.apk'
|
||||||
|
export const downLoadMac = 'https://h.hifastapp.com/download/HiFastVPN-1.0.0+100-macos.dmg'
|
||||||
|
export const downLoadWin = 'https://h.hifastapp.com/download/HiFastVPN-0.0.2-windows-setup.exe'
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import CryptoJS from 'crypto-js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HiAesUtil - AES-256-CBC 加密工具类
|
||||||
|
* 逻辑与 Dart 版本保持 100% 同步
|
||||||
|
*/
|
||||||
|
export class HiAesUtil {
|
||||||
|
/**
|
||||||
|
* 生成 32 字节密钥 (SHA-256)
|
||||||
|
*/
|
||||||
|
static _generateKey(keyStr) {
|
||||||
|
return CryptoJS.SHA256(keyStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 16 字节 IV
|
||||||
|
* 逻辑: SHA-256(MD5(nonce) + keyStr) -> 取前16字节
|
||||||
|
*/
|
||||||
|
static _generateIv(nonce, keyStr) {
|
||||||
|
// 1. MD5 处理 nonce
|
||||||
|
const md5Hash = CryptoJS.MD5(nonce)
|
||||||
|
// 2. 转为 16 进制字符串
|
||||||
|
const md5Hex = md5Hash.toString(CryptoJS.enc.Hex)
|
||||||
|
// 3. 拼接 md5Hex + keyStr 并做 SHA-256
|
||||||
|
const finalHash = CryptoJS.SHA256(md5Hex + keyStr)
|
||||||
|
|
||||||
|
// 4. 重要:截取前 16 字节 (128位)
|
||||||
|
// CryptoJS 的 WordArray 由 32 位整数组成,16 字节即前 4 个 words
|
||||||
|
const iv = CryptoJS.lib.WordArray.create(finalHash.words.slice(0, 4), 16)
|
||||||
|
return iv
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密方法
|
||||||
|
* @param {string} plainText - 明文
|
||||||
|
* @param {string} keyStr - 密钥
|
||||||
|
*/
|
||||||
|
static encryptData(plainText, keyStr) {
|
||||||
|
// 生成 ISO8601 时间戳 (与 Dart DateTime.now().toIso8601String() 对应)
|
||||||
|
// Dart: 2025-12-30T21:05:06.075850 (no Z, 6 decimal places)
|
||||||
|
const now = new Date()
|
||||||
|
const nonce = now.toISOString().replace('Z', '') + '000'
|
||||||
|
|
||||||
|
const key = this._generateKey(keyStr)
|
||||||
|
const iv = this._generateIv(nonce, keyStr)
|
||||||
|
|
||||||
|
const encrypted = CryptoJS.AES.encrypt(plainText, key, {
|
||||||
|
iv: iv,
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
padding: CryptoJS.pad.Pkcs7,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: encrypted.toString(), // Base64 字符串
|
||||||
|
time: nonce,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密方法
|
||||||
|
* @param {string} encryptedData - Base64 加密字符串
|
||||||
|
* @param {string} nonce - 时间戳
|
||||||
|
* @param {string} keyStr - 密钥
|
||||||
|
*/
|
||||||
|
static decryptData(encryptedData, nonce, keyStr) {
|
||||||
|
const key = this._generateKey(keyStr)
|
||||||
|
const iv = this._generateIv(nonce, keyStr)
|
||||||
|
|
||||||
|
const decrypted = CryptoJS.AES.decrypt(encryptedData, key, {
|
||||||
|
iv: iv,
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
padding: CryptoJS.pad.Pkcs7,
|
||||||
|
})
|
||||||
|
|
||||||
|
return decrypted.toString(CryptoJS.enc.Utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { AxiosRequestConfig, Canceler } from 'axios'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// Used to store the identification and cancellation function of each request
|
||||||
|
let pendingMap = new Map<string, Canceler>()
|
||||||
|
|
||||||
|
const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&')
|
||||||
|
|
||||||
|
export class AxiosCanceler {
|
||||||
|
/**
|
||||||
|
* Add request
|
||||||
|
* @param {Object} config
|
||||||
|
*/
|
||||||
|
addPending(config: AxiosRequestConfig): void {
|
||||||
|
this.removePending(config)
|
||||||
|
const url = getPendingUrl(config)
|
||||||
|
config.cancelToken = config.cancelToken
|
||||||
|
|| new axios.CancelToken((cancel) => {
|
||||||
|
if (!pendingMap.has(url)) {
|
||||||
|
// If there is no current request in pending, add it
|
||||||
|
pendingMap.set(url, cancel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Clear all pending
|
||||||
|
*/
|
||||||
|
removeAllPending(): void {
|
||||||
|
pendingMap.forEach((cancel) => {
|
||||||
|
cancel?.()
|
||||||
|
})
|
||||||
|
pendingMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removal request
|
||||||
|
* @param {Object} config
|
||||||
|
*/
|
||||||
|
removePending(config: AxiosRequestConfig): void {
|
||||||
|
const url = getPendingUrl(config)
|
||||||
|
|
||||||
|
if (pendingMap.has(url)) {
|
||||||
|
// If there is a current request identifier in pending,
|
||||||
|
// the current request needs to be cancelled and removed
|
||||||
|
const cancel = pendingMap.get(url)
|
||||||
|
cancel && cancel(url)
|
||||||
|
pendingMap.delete(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: reset
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
pendingMap = new Map<string, Canceler>()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { AxiosCanceler } from './cancel'
|
||||||
|
import router from '@/router'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { HiAesUtil } from './HiAesUtil.ts'
|
||||||
|
const encryptionKey = 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx'
|
||||||
|
function redirectLogin() {
|
||||||
|
localStorage.removeItem('Authorization')
|
||||||
|
router.push({ path: '/', query: { login: 'true' } })
|
||||||
|
}
|
||||||
|
export interface ExtraConfig {
|
||||||
|
/**
|
||||||
|
* 默认值 1 错误等级 0-忽略,1-warning,2-error
|
||||||
|
*/
|
||||||
|
errorLevel?: 0 | 1 | 2
|
||||||
|
/**
|
||||||
|
* 默认true 是否携带token
|
||||||
|
*/
|
||||||
|
withToken?: boolean
|
||||||
|
/**
|
||||||
|
* 默认true 是否处理响应
|
||||||
|
*/
|
||||||
|
handleResponse?: boolean
|
||||||
|
/**
|
||||||
|
* 默认false 是否取消重复请求
|
||||||
|
*/
|
||||||
|
cancelRepetition?: boolean
|
||||||
|
/**
|
||||||
|
* 默认false 是否返回axios完整响应
|
||||||
|
*/
|
||||||
|
originResponseData?: boolean
|
||||||
|
/**
|
||||||
|
* 默认token 存储key
|
||||||
|
*/
|
||||||
|
tokenKey?: string
|
||||||
|
/**
|
||||||
|
* 获取token的方法
|
||||||
|
*/
|
||||||
|
getToken?: () => string
|
||||||
|
/**
|
||||||
|
* 自定义header方法,此方法返回的header会覆盖默认的header
|
||||||
|
*/
|
||||||
|
formatHeader?: (header: Record<string, string>) => Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestConfig extends AxiosRequestConfig {
|
||||||
|
extraConfig?: ExtraConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResponseType extends AxiosResponse {
|
||||||
|
config: RequestConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_MESSAGES: Record<number | string, string> = {
|
||||||
|
'200': '成功',
|
||||||
|
'500': '内部服务器错误',
|
||||||
|
'10001': '数据库查询错误',
|
||||||
|
'10002': '数据库更新错误',
|
||||||
|
'10003': '数据库插入错误',
|
||||||
|
'10004': '数据库删除错误',
|
||||||
|
'20001': '用户已存在',
|
||||||
|
'20002': '用户不存在',
|
||||||
|
'20003': '用户密码错误',
|
||||||
|
'20004': '用户已禁用',
|
||||||
|
'20005': '余额不足',
|
||||||
|
'20006': '停止注册',
|
||||||
|
'20007': '未绑定Telegram',
|
||||||
|
'20008': '用户未绑定OAuth方式',
|
||||||
|
'20009': '邀请码错误',
|
||||||
|
'30001': '节点已存在',
|
||||||
|
'30002': '节点不存在',
|
||||||
|
'30003': '节点组已存在',
|
||||||
|
'30004': '节点组不存在',
|
||||||
|
'30005': '节点组不为空',
|
||||||
|
'400': '参数错误',
|
||||||
|
'40002': '用户令牌为空',
|
||||||
|
'40003': '用户令牌无效',
|
||||||
|
'40004': '用户令牌已过期',
|
||||||
|
'40005': '您还没有登录',
|
||||||
|
'401': '请求过多',
|
||||||
|
'50001': '优惠券不存在',
|
||||||
|
'50002': '优惠券已被使用',
|
||||||
|
'50003': '优惠券不匹配',
|
||||||
|
'60001': '订阅已过期',
|
||||||
|
'60002': '订阅不可用',
|
||||||
|
'60003': '用户已有订阅',
|
||||||
|
'60004': '订阅已被使用',
|
||||||
|
'60005': '单一订阅模式超出限制',
|
||||||
|
'60006': '订阅配额限制',
|
||||||
|
'70001': '验证码错误',
|
||||||
|
'80001': '队列入队错误',
|
||||||
|
'90001': '调试模式已启用',
|
||||||
|
'90002': '发送短信错误',
|
||||||
|
'90003': '短信功能未启用',
|
||||||
|
'90004': '电子邮件功能未启用',
|
||||||
|
'90005': '不支持的登录方式',
|
||||||
|
'90006': '身份验证器不支持此方式',
|
||||||
|
'90007': '电话区号为空',
|
||||||
|
'90008': '密码为空',
|
||||||
|
'90009': '区号为空',
|
||||||
|
'90010': '需要密码或验证码',
|
||||||
|
'90011': '电子邮件已存在',
|
||||||
|
'90012': '电话号码已存在',
|
||||||
|
'90013': '设备已存在',
|
||||||
|
'90014': '电话号码错误',
|
||||||
|
'90015': '此账户今日已达到发送次数限制',
|
||||||
|
'90017': '设备不存在',
|
||||||
|
'90018': '用户 ID 不匹配',
|
||||||
|
'61001': '订单不存在',
|
||||||
|
'61002': '支付方式未找到',
|
||||||
|
'61003': '订单状态错误',
|
||||||
|
'61004': '重置周期不足',
|
||||||
|
'61005': '存在没用完的流量',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Request {
|
||||||
|
public axiosInstance: AxiosInstance
|
||||||
|
|
||||||
|
private config: RequestConfig
|
||||||
|
|
||||||
|
constructor(config: RequestConfig) {
|
||||||
|
this.config = config
|
||||||
|
this.axiosInstance = axios.create(config)
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultConfig: Required<ExtraConfig> = {
|
||||||
|
errorLevel: 2,
|
||||||
|
withToken: true,
|
||||||
|
handleResponse: true,
|
||||||
|
cancelRepetition: false,
|
||||||
|
originResponseData: false,
|
||||||
|
tokenKey: 'token',
|
||||||
|
formatHeader: (headers) => {
|
||||||
|
return headers
|
||||||
|
},
|
||||||
|
getToken: () => '',
|
||||||
|
}
|
||||||
|
|
||||||
|
private errorReport(lv: number, message: string) {
|
||||||
|
toast(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
const axiosCanceler = new AxiosCanceler()
|
||||||
|
|
||||||
|
this.axiosInstance.interceptors.request.use(
|
||||||
|
(config: RequestConfig) => {
|
||||||
|
const mergeExtraConfig = {
|
||||||
|
...Request.defaultConfig,
|
||||||
|
...this.config.extraConfig,
|
||||||
|
...config.extraConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (config.data && !(config.data instanceof FormData)) {
|
||||||
|
// const plainText = JSON.stringify(config.data)
|
||||||
|
// config.data = HiAesUtil.encryptData(plainText, encryptionKey)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (config.method?.toLowerCase() === 'get' || config.params) {
|
||||||
|
// const paramsToEncrypt = config.params || {}
|
||||||
|
// const plainParamsText = JSON.stringify(paramsToEncrypt)
|
||||||
|
// const encryptedParams = HiAesUtil.encryptData(plainParamsText, encryptionKey)
|
||||||
|
//
|
||||||
|
// config.params = {
|
||||||
|
// data: encryptedParams.data,
|
||||||
|
// time: encryptedParams.time,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
config.headers = mergeExtraConfig.formatHeader({
|
||||||
|
...this.config.headers,
|
||||||
|
...config.headers,
|
||||||
|
lang: 'zh_CN',
|
||||||
|
// 'login-type': 'device',
|
||||||
|
...(mergeExtraConfig.withToken && {
|
||||||
|
[mergeExtraConfig.tokenKey]: mergeExtraConfig.getToken(),
|
||||||
|
}),
|
||||||
|
} as any)
|
||||||
|
config.extraConfig = mergeExtraConfig
|
||||||
|
if (mergeExtraConfig.cancelRepetition) {
|
||||||
|
axiosCanceler.addPending(config)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.errorReport(error?.config.extraConfig.errorLevel, error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
this.axiosInstance.interceptors.response.use(
|
||||||
|
(response: ResponseType) => {
|
||||||
|
const { data, config } = response
|
||||||
|
const responseData = response.data.data
|
||||||
|
|
||||||
|
// if (responseData && responseData.data && responseData.time) {
|
||||||
|
// try {
|
||||||
|
// const decryptedStr = HiAesUtil.decryptData(
|
||||||
|
// responseData.data,
|
||||||
|
// responseData.time,
|
||||||
|
// encryptionKey,
|
||||||
|
// )
|
||||||
|
// responseData = JSON.parse(decryptedStr)
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error('解密失败:', e)
|
||||||
|
// return Promise.reject({ message: '数据解密异常' })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
axiosCanceler.removePending(config)
|
||||||
|
|
||||||
|
if (data.code !== 200) {
|
||||||
|
const msg = ERROR_MESSAGES[data.code] || response.data?.msg || data?.error || '未知错误'
|
||||||
|
|
||||||
|
if (data.code == 40004 || data.code == 40003 || data.code == 40005) {
|
||||||
|
toast.error(msg)
|
||||||
|
redirectLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.extraConfig?.handleResponse) {
|
||||||
|
this.errorReport(config.extraConfig.errorLevel ?? 2, msg)
|
||||||
|
return Promise.reject({
|
||||||
|
...data,
|
||||||
|
message: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.extraConfig?.originResponseData ? response : responseData
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
const status = error?.response?.status
|
||||||
|
const code = error?.code
|
||||||
|
let message = error?.message
|
||||||
|
if (status === 401) {
|
||||||
|
redirectLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (code === 'ECONNABORTED') {
|
||||||
|
message = '网络环境太差,请求超时'
|
||||||
|
} else if (code === 'Network Error' || message === 'Network Error') {
|
||||||
|
if (error.response) {
|
||||||
|
message = `${error.response.status}:network连接失败,请求中断`
|
||||||
|
} else {
|
||||||
|
message = '网络好像出现问题了'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error.__CANCEL__) {
|
||||||
|
console.warn('request canceled', error?.message)
|
||||||
|
} else {
|
||||||
|
this.errorReport(error?.config?.extraConfig?.errorLevel, message)
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public get<T>(url: string, params?: Record<string, unknown>, config?: RequestConfig): Promise<T> {
|
||||||
|
return this.axiosInstance.get(url, { ...config, params })
|
||||||
|
}
|
||||||
|
|
||||||
|
public post<D, T>(url: string, data?: D, config?: RequestConfig): Promise<T> {
|
||||||
|
return this.axiosInstance.post(url, data, { ...config })
|
||||||
|
}
|
||||||
|
|
||||||
|
public put<D, T>(url: string, data?: D, config?: RequestConfig): Promise<T> {
|
||||||
|
return this.axiosInstance.put(url, data, { ...config })
|
||||||
|
}
|
||||||
|
|
||||||
|
public patch<D, T>(url: string, data?: D, config?: RequestConfig): Promise<T> {
|
||||||
|
return this.axiosInstance.patch(url, data, { ...config })
|
||||||
|
}
|
||||||
|
|
||||||
|
public delete<D, T>(url: string, params?: D, config?: RequestConfig): Promise<T> {
|
||||||
|
return this.axiosInstance.delete(url, { ...config, params })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Request from './core'
|
||||||
|
export * from './core'
|
||||||
|
const baseUrl = import.meta.env.VITE_APP_BASE_URL
|
||||||
|
|
||||||
|
const request = new Request({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
timeout: 6000,
|
||||||
|
headers: {},
|
||||||
|
extraConfig: {
|
||||||
|
/** 这里是核心配置,一般不需要再去修改request/core.ts */
|
||||||
|
tokenKey: 'Authorization',
|
||||||
|
getToken: () => localStorage.getItem('Authorization') || '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default request
|
||||||
@@ -4,18 +4,33 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import svgLoader from 'vite-svg-loader'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [vue(), tailwindcss(), vueDevTools(), svgLoader()],
|
||||||
vue(),
|
|
||||||
tailwindcss(),
|
|
||||||
vueDevTools(),
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// 将所有以 /api 开头的请求转发到目标服务器
|
||||||
|
// 1. 匹配所有以 /public 开头的请求
|
||||||
|
'/api/v1': {
|
||||||
|
target: 'https://api.hifast.biz',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
autoRewrite: true,
|
||||||
|
// 3. 关键:将路径重写,在前面补上 /api/v1
|
||||||
|
// 验证请求是否进入代理,可以在终端看到打印
|
||||||
|
configure: (proxy, _options) => {
|
||||||
|
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||||
|
console.log('代理请求:', req.method, req.url, ' -> ', proxyReq.path)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||