ci: 添加 Epic UI 构建和部署工作流
- 新增 CI/CD 工作流文件,实现前端项目的自动构建和部署 - 支持 main、master 和 develop 分支的自动构建- 包含代码检出、环境安装、依赖管理、项目构建等步骤 - 实现构建产物的自动部署和 Docker 容器重启
This commit is contained in:
@@ -2,9 +2,9 @@ kind: pipeline
|
|||||||
type: docker
|
type: docker
|
||||||
name: default
|
name: default
|
||||||
|
|
||||||
#trigger:
|
trigger:
|
||||||
# event:
|
event:
|
||||||
# - manual
|
- manual
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: build
|
||||||
|
|||||||
@@ -1,314 +0,0 @@
|
|||||||
name: Epic UI Build & Deploy
|
|
||||||
run-name: ${{ gitea.actor }} 正在构建 Epic UI 前端项目 🚀
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, master, develop ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main, master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: gitea_labels
|
|
||||||
container:
|
|
||||||
image: gitea-ci-bash:latest
|
|
||||||
env:
|
|
||||||
# 指定容器将工具缓存路径存放到 /opt/hostedtoolcache,该目录是Gitea Runner的标准工具缓存目录
|
|
||||||
RUNNER_TOOL_CACHE: /opt/hostedtoolcache
|
|
||||||
volumes:
|
|
||||||
# 直接挂载到指定的宿主机路径
|
|
||||||
# 挂载生产环境目录
|
|
||||||
- /opt/1panel/apps/openresty/openresty/www/sites/epic7/index:/opt/1panel/apps/openresty/openresty/www/sites/epic7/index
|
|
||||||
steps:
|
|
||||||
- name: 检出代码
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "📥 检出代码到工作目录..."
|
|
||||||
|
|
||||||
# 解析分支名
|
|
||||||
BRANCH_NAME=$(echo "${{ gitea.ref }}" | sed 's#refs/heads/##')
|
|
||||||
|
|
||||||
# 拉取代码(使用token鉴权)
|
|
||||||
git clone --depth=1 -b "$BRANCH_NAME" "http://1c18ee1ab9a9cb291506d0c5c016a33be7d59e8c:x-oauth-basic@gitea.htoop.cn/${{ gitea.repository }}.git" .
|
|
||||||
|
|
||||||
echo "✅ 代码检出成功"
|
|
||||||
|
|
||||||
- name: 安装Node.js环境
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "🔧 安装Node.js环境..."
|
|
||||||
if command -v node &> /dev/null; then
|
|
||||||
echo "✅ Node.js已安装: $(node --version)"
|
|
||||||
else
|
|
||||||
echo "📥 下载并安装Node.js..."
|
|
||||||
NODE_VERSION="18.19.0"
|
|
||||||
NODE_ARCH="linux-x64"
|
|
||||||
MIRRORS=(
|
|
||||||
"https://mirrors.aliyun.com/nodejs-release/v${NODE_VERSION}/node-v${NODE_VERSION}-${NODE_ARCH}.tar.xz"
|
|
||||||
"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-${NODE_ARCH}.tar.xz"
|
|
||||||
)
|
|
||||||
DOWNLOAD_SUCCESS=false
|
|
||||||
for mirror in "${MIRRORS[@]}"; do
|
|
||||||
echo "尝试从镜像下载: $mirror"
|
|
||||||
if wget -q --timeout=30 --tries=3 "$mirror" -O node.tar.xz; then
|
|
||||||
echo "✅ 下载成功: $mirror"
|
|
||||||
DOWNLOAD_SUCCESS=true
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "❌ 下载失败: $mirror"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$DOWNLOAD_SUCCESS" = false ]; then
|
|
||||||
echo "❌ 所有镜像源下载失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "解压Node.js到/usr/local..."
|
|
||||||
tar -C /usr/local -xJf node.tar.xz --strip-components=1
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
rm node.tar.xz
|
|
||||||
echo "✅ Node.js安装完成"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 安装pnpm
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "📦 安装pnpm包管理器..."
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
|
|
||||||
if command -v pnpm &> /dev/null; then
|
|
||||||
echo "✅ pnpm已安装: $(pnpm --version)"
|
|
||||||
else
|
|
||||||
echo "📥 安装pnpm..."
|
|
||||||
npm install -g pnpm
|
|
||||||
echo "✅ pnpm安装完成: $(pnpm --version)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 配置pnpm缓存
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "📦 配置pnpm缓存..."
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
|
|
||||||
# 配置pnpm缓存目录
|
|
||||||
PNPM_STORE_DIR="/opt/hostedtoolcache/pnpm-store"
|
|
||||||
mkdir -p "$PNPM_STORE_DIR"
|
|
||||||
|
|
||||||
# 设置pnpm使用缓存目录
|
|
||||||
pnpm config set store-dir "$PNPM_STORE_DIR"
|
|
||||||
pnpm config set cache-dir "/opt/hostedtoolcache/pnpm-cache"
|
|
||||||
|
|
||||||
echo "✅ pnpm缓存配置完成"
|
|
||||||
|
|
||||||
- name: 恢复依赖缓存
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "📦 检查并恢复依赖缓存..."
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
|
|
||||||
# 生成缓存键
|
|
||||||
CACHE_KEY=$(md5sum pnpm-lock.yaml | cut -d' ' -f1)
|
|
||||||
echo "缓存键: $CACHE_KEY"
|
|
||||||
|
|
||||||
# 使用 /opt/hostedtoolcache 目录
|
|
||||||
CACHE_FILE="/opt/hostedtoolcache/node_modules_${CACHE_KEY}.tar.gz"
|
|
||||||
echo "📁 检查缓存文件: $CACHE_FILE"
|
|
||||||
|
|
||||||
if [ -f "$CACHE_FILE" ]; then
|
|
||||||
echo "✅ 找到缓存文件: $CACHE_FILE"
|
|
||||||
echo "📦 缓存文件大小: $(du -sh "$CACHE_FILE" | cut -f1)"
|
|
||||||
echo "正在恢复缓存..."
|
|
||||||
|
|
||||||
if tar -xzf "$CACHE_FILE"; then
|
|
||||||
echo "✅ 缓存恢复成功"
|
|
||||||
echo "📦 恢复的 node_modules 大小:"
|
|
||||||
du -sh node_modules 2>/dev/null || echo "node_modules 目录不存在"
|
|
||||||
else
|
|
||||||
echo "❌ 缓存恢复失败"
|
|
||||||
echo "📥 将重新安装依赖"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "📥 未找到缓存文件,将重新安装依赖"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 安装项目依赖
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "📦 安装项目依赖..."
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
|
|
||||||
# 检查是否已有node_modules
|
|
||||||
if [ -d "node_modules" ] && [ -f "node_modules/.pnpm-debug.log" ]; then
|
|
||||||
echo "✅ 检测到已存在的依赖,跳过安装"
|
|
||||||
else
|
|
||||||
echo "📥 安装项目依赖..."
|
|
||||||
pnpm install --frozen-lockfile --prefer-offline
|
|
||||||
echo "✅ 依赖安装完成"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 保存依赖缓存
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "💾 保存依赖缓存..."
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
|
|
||||||
# 生成缓存键
|
|
||||||
CACHE_KEY=$(md5sum pnpm-lock.yaml | cut -d' ' -f1)
|
|
||||||
echo "缓存键: $CACHE_KEY"
|
|
||||||
|
|
||||||
# 检查node_modules是否存在
|
|
||||||
if [ ! -d "node_modules" ]; then
|
|
||||||
echo "❌ node_modules 目录不存在,跳过缓存保存"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查缓存文件是否已存在且是最新的
|
|
||||||
CACHE_FILE="/opt/hostedtoolcache/node_modules_${CACHE_KEY}.tar.gz"
|
|
||||||
if [ -f "$CACHE_FILE" ]; then
|
|
||||||
echo "✅ 缓存文件已存在且是最新的,跳过创建: $CACHE_FILE"
|
|
||||||
echo "📦 现有缓存文件大小: $(du -sh "$CACHE_FILE" | cut -f1)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 缓存文件不存在,需要创建
|
|
||||||
echo "📦 正在创建缓存文件: $CACHE_FILE"
|
|
||||||
|
|
||||||
if tar -czf "$CACHE_FILE" node_modules; then
|
|
||||||
echo "✅ 缓存已保存到: $CACHE_FILE"
|
|
||||||
echo "📦 缓存文件大小: $(du -sh "$CACHE_FILE" | cut -f1)"
|
|
||||||
else
|
|
||||||
echo "❌ 缓存保存失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: 构建项目
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "🔨 构建 Epic UI 前端项目..."
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
|
|
||||||
# 执行构建
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
echo "✅ 构建完成"
|
|
||||||
|
|
||||||
- name: 检查构建产物
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "📦 构建产物信息:"
|
|
||||||
if [ -d "dist" ]; then
|
|
||||||
echo "✅ 找到dist目录"
|
|
||||||
ls -la dist/
|
|
||||||
echo "dist目录大小: $(du -sh dist | cut -f1)"
|
|
||||||
else
|
|
||||||
echo "❌ 未找到dist目录"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 显示项目信息
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "📋 项目信息:"
|
|
||||||
export PATH=$PATH:/usr/local/bin
|
|
||||||
echo "Node.js 版本: $(node --version)"
|
|
||||||
echo "npm 版本: $(npm --version)"
|
|
||||||
echo "pnpm 版本: $(pnpm --version)"
|
|
||||||
echo "构建时间: $(date)"
|
|
||||||
echo "分支: ${{ gitea.ref }}"
|
|
||||||
echo "提交: ${{ gitea.sha }}"
|
|
||||||
|
|
||||||
- name: 部署到生产环境
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "🚀 部署到生产环境..."
|
|
||||||
|
|
||||||
# 生产环境目录(容器内路径,直接映射到nginx静态文件目录)
|
|
||||||
PROD_DIR="/opt/1panel/apps/openresty/openresty/www/sites/epic7/index"
|
|
||||||
|
|
||||||
# 检查构建产物
|
|
||||||
if [ ! -d "dist" ]; then
|
|
||||||
echo "❌ 构建产物不存在"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📦 构建产物内容:"
|
|
||||||
ls -la dist/
|
|
||||||
|
|
||||||
# 检查生产目录挂载
|
|
||||||
echo "📁 检查生产目录挂载状态..."
|
|
||||||
if [ -d "$PROD_DIR" ]; then
|
|
||||||
echo "✅ 生产目录已存在: $PROD_DIR"
|
|
||||||
echo "📁 当前生产目录内容:"
|
|
||||||
ls -la "$PROD_DIR" 2>/dev/null || echo "目录为空或无法访问"
|
|
||||||
else
|
|
||||||
echo "📁 生产目录不存在,将创建: $PROD_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 备份当前生产环境
|
|
||||||
if [ -d "$PROD_DIR" ] && [ "$(ls -A "$PROD_DIR" 2>/dev/null)" ]; then
|
|
||||||
BACKUP_DIR="/opt/prod_backup_$(date +%Y%m%d_%H%M%S)"
|
|
||||||
echo "📦 备份当前生产环境到: $BACKUP_DIR"
|
|
||||||
cp -r "$PROD_DIR" "$BACKUP_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 确保生产目录存在并清空
|
|
||||||
echo "📤 部署构建产物到nginx静态文件目录..."
|
|
||||||
mkdir -p "$PROD_DIR"
|
|
||||||
rm -rf "$PROD_DIR"/*
|
|
||||||
|
|
||||||
# 复制构建产物到生产环境
|
|
||||||
echo "📦 复制构建产物..."
|
|
||||||
if cp -r dist/* "$PROD_DIR/"; then
|
|
||||||
echo "✅ 构建产物复制成功"
|
|
||||||
else
|
|
||||||
echo "❌ 构建产物复制失败"
|
|
||||||
echo "📁 检查目标目录权限和空间..."
|
|
||||||
df -h "$PROD_DIR" 2>/dev/null || echo "无法检查磁盘空间"
|
|
||||||
ls -ld "$PROD_DIR" 2>/dev/null || echo "无法检查目录权限"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 设置权限
|
|
||||||
chmod -R 755 "$PROD_DIR"
|
|
||||||
|
|
||||||
# 强制同步文件系统 - 确保文件写入到宿主机
|
|
||||||
echo "🔄 同步文件系统..."
|
|
||||||
sync
|
|
||||||
|
|
||||||
# 等待文件系统同步完成
|
|
||||||
echo "⏳ 等待文件系统同步..."
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
# 再次强制同步
|
|
||||||
sync
|
|
||||||
|
|
||||||
# 验证文件是否真的写入到宿主机
|
|
||||||
echo "🔍 验证文件同步状态..."
|
|
||||||
if [ -f "$PROD_DIR/index.html" ]; then
|
|
||||||
echo "✅ 确认index.html已同步到宿主机"
|
|
||||||
echo "📋 文件大小: $(ls -lh "$PROD_DIR/index.html" | awk '{print $5}')"
|
|
||||||
echo "📋 文件时间: $(ls -l "$PROD_DIR/index.html" | awk '{print $6, $7, $8}')"
|
|
||||||
else
|
|
||||||
echo "❌ index.html未同步到宿主机"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ 部署完成"
|
|
||||||
echo "📁 生产环境目录: $PROD_DIR (直接映射到宿主机)"
|
|
||||||
echo "📦 部署的文件:"
|
|
||||||
ls -la "$PROD_DIR"
|
|
||||||
|
|
||||||
# 验证部署结果
|
|
||||||
echo "🔍 验证部署结果..."
|
|
||||||
echo "📋 文件数量: $(find "$PROD_DIR" -type f | wc -l)"
|
|
||||||
echo "📋 目录数量: $(find "$PROD_DIR" -type d | wc -l)"
|
|
||||||
echo "📋 总大小: $(du -sh "$PROD_DIR" | cut -f1)"
|
|
||||||
|
|
||||||
# 检查关键文件
|
|
||||||
if [ -f "$PROD_DIR/index.html" ]; then
|
|
||||||
echo "✅ index.html 存在"
|
|
||||||
echo "📋 index.html 大小: $(ls -lh "$PROD_DIR/index.html" | awk '{print $5}')"
|
|
||||||
else
|
|
||||||
echo "❌ index.html 不存在"
|
|
||||||
fi
|
|
||||||
5
.npmrc
5
.npmrc
@@ -1,5 +0,0 @@
|
|||||||
# pnpm 性能优化配置
|
|
||||||
registry=https://registry.npmmirror.com/
|
|
||||||
prefer-offline=true
|
|
||||||
auto-install-peers=true
|
|
||||||
shamefully-hoist=true
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {useNavigate} from 'react-router-dom';
|
import {useNavigate} from 'react-router-dom';
|
||||||
import {Select} from 'antd';
|
import {Input} from 'antd';
|
||||||
import type {Hero} from '@/api/index';
|
import type {Hero} from '@/api/index';
|
||||||
import * as EpicApi from '@/api/index';
|
import * as EpicApi from '@/api/index';
|
||||||
|
|
||||||
@@ -14,60 +14,31 @@ type ExtendedHero = Hero & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 自定义样式覆盖 Ant Design 默认样式
|
// 自定义样式覆盖 Ant Design 默认样式
|
||||||
const selectStyles = `
|
const inputStyles = `
|
||||||
.ant-select {
|
.ant-input, .ant-input-affix-wrapper, .ant-input-affix-wrapper input {
|
||||||
background-color: #1A1412 !important;
|
background-color: #1A1412 !important;
|
||||||
border-color: #C17F59 !important;
|
border-color: rgba(193, 127, 89, 0.3) !important;
|
||||||
color: #E6B17E !important;
|
color: #E6B17E !important;
|
||||||
}
|
border-radius: 0.5rem !important;
|
||||||
|
box-shadow: none !important;
|
||||||
.ant-select .ant-select-selector {
|
}
|
||||||
background-color: #1A1412 !important;
|
.ant-input:focus, .ant-input-affix-wrapper:focus, .ant-input-affix-wrapper-focused, .ant-input:active {
|
||||||
border-color: #C17F59 !important;
|
background-color: #1A1412 !important;
|
||||||
color: #E6B17E !important;
|
border-color: #C17F59 !important;
|
||||||
}
|
color: #E6B17E !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(193, 127, 89, 0.2) !important;
|
||||||
.ant-select-focused .ant-select-selector {
|
}
|
||||||
border-color: #C17F59 !important;
|
.ant-input::placeholder, .ant-input-affix-wrapper input::placeholder {
|
||||||
box-shadow: 0 0 0 2px rgba(193, 127, 89, 0.2) !important;
|
color: #9B8579 !important;
|
||||||
}
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
.ant-select-dropdown {
|
.ant-input-clear-icon {
|
||||||
background-color: #1A1412 !important;
|
color: #E6B17E !important;
|
||||||
border-color: #C17F59 !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
.ant-input-clear-icon:hover {
|
||||||
.ant-select-item {
|
color: #C17F59 !important;
|
||||||
background-color: #1A1412 !important;
|
}
|
||||||
color: #E6B17E !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-item-option-selected {
|
|
||||||
background-color: #2A211E !important;
|
|
||||||
color: #E6B17E !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-item-option-active {
|
|
||||||
background-color: #2A211E !important;
|
|
||||||
color: #E6B17E !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selection-placeholder {
|
|
||||||
color: #9B8579 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selection-item {
|
|
||||||
color: #E6B17E !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-arrow {
|
|
||||||
color: #E6B17E !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-clear {
|
|
||||||
background-color: #1A1412 !important;
|
|
||||||
color: #E6B17E !important;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ELEMENTS = [
|
const ELEMENTS = [
|
||||||
@@ -157,15 +128,15 @@ const Characters: React.FC = () => {
|
|||||||
return <img src={`/pic/role/${role}.png`} alt={role} className="w-7 h-7" />;
|
return <img src={`/pic/role/${role}.png`} alt={role} className="w-7 h-7" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理英雄选择
|
// 处理搜索输入
|
||||||
const handleHeroSelect = (value: string) => {
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchTerm(value);
|
setSearchTerm(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#1A1412] text-white font-sans relative">
|
<div className="min-h-screen bg-[#1A1412] text-white font-sans relative">
|
||||||
{/* 注入自定义样式 */}
|
{/* 注入自定义样式 */}
|
||||||
<style dangerouslySetInnerHTML={{ __html: selectStyles }} />
|
<style dangerouslySetInnerHTML={{ __html: inputStyles }} />
|
||||||
{/* 背景装饰 */}
|
{/* 背景装饰 */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-[#1A1412] via-[#2A211E] to-[#1A1412]"></div>
|
<div className="absolute inset-0 bg-gradient-to-b from-[#1A1412] via-[#2A211E] to-[#1A1412]"></div>
|
||||||
|
|
||||||
@@ -179,26 +150,11 @@ const Characters: React.FC = () => {
|
|||||||
{/* 搜索组件 */}
|
{/* 搜索组件 */}
|
||||||
<div className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] p-4 rounded-lg mb-8 border border-[#C17F59]/30 backdrop-blur-sm">
|
<div className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] p-4 rounded-lg mb-8 border border-[#C17F59]/30 backdrop-blur-sm">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||||
<Select
|
<Input
|
||||||
showSearch
|
placeholder="搜索英雄名称或昵称"
|
||||||
placeholder="选择英雄名称/昵称"
|
value={searchTerm}
|
||||||
value={searchTerm || undefined}
|
onChange={handleSearchChange}
|
||||||
onChange={handleHeroSelect}
|
allowClear
|
||||||
onSearch={setSearchTerm}
|
|
||||||
filterOption={(input, option) => {
|
|
||||||
const heroName = option?.value as string;
|
|
||||||
return heroName.toLowerCase().includes(input.toLowerCase());
|
|
||||||
}}
|
|
||||||
options={allHeroes.map(hero => ({
|
|
||||||
value: hero.heroName,
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<img src={hero.headImgUrl} alt={hero.heroName} className="w-6 h-6 rounded-full" />
|
|
||||||
<span>{hero.heroName}</span>
|
|
||||||
{hero.nickName && <span className="text-[#9B8579] text-sm">({hero.nickName})</span>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}))}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,46 +202,57 @@ const Characters: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
{filteredHeroes.length > 0 ? (
|
||||||
{filteredHeroes.map((hero) => (
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div
|
{filteredHeroes.map((hero) => (
|
||||||
key={hero.heroCode}
|
<div
|
||||||
className="flex items-center bg-[#23201B] rounded-xl border border-[#C17F59]/30 hover:border-[#C17F59] transition-all duration-300 shadow-md px-4 py-3 min-h-[96px] max-h-[96px] cursor-pointer backdrop-blur-sm"
|
key={hero.heroCode}
|
||||||
onClick={() => navigate(`/character/${hero.heroCode}`)}
|
className="flex items-center bg-[#23201B] rounded-xl border border-[#C17F59]/30 hover:border-[#C17F59] transition-all duration-300 shadow-md px-4 py-3 min-h-[96px] max-h-[96px] cursor-pointer backdrop-blur-sm"
|
||||||
>
|
onClick={() => navigate(`/character/${hero.heroCode}`)}
|
||||||
{/* 头像+attr */}
|
>
|
||||||
<div className="relative mr-4 flex-shrink-0">
|
{/* 头像+attr */}
|
||||||
<img
|
<div className="relative mr-4 flex-shrink-0">
|
||||||
src={hero.headImgUrl}
|
<img
|
||||||
alt={hero.heroName}
|
src={hero.headImgUrl}
|
||||||
className="w-16 h-16 rounded-full border-2 border-[#C17F59] object-cover object-center"
|
alt={hero.heroName}
|
||||||
/>
|
className="w-16 h-16 rounded-full border-2 border-[#C17F59] object-cover object-center"
|
||||||
<div className="absolute -top-2 -right-2">
|
/>
|
||||||
{getElementIcon(hero.attribute)}
|
<div className="absolute -top-2 -right-2">
|
||||||
</div>
|
{getElementIcon(hero.attribute)}
|
||||||
</div>
|
</div>
|
||||||
{/* 右侧信息 */}
|
</div>
|
||||||
<div className="flex flex-col justify-center flex-1 min-w-0">
|
{/* 右侧信息 */}
|
||||||
<div className="flex items-center mb-1">
|
<div className="flex flex-col justify-center flex-1 min-w-0">
|
||||||
<span className="text-[16px] font-bold text-[#E6B17E]">{hero.heroName}</span>
|
<div className="flex items-center mb-1">
|
||||||
</div>
|
<span className="text-[16px] font-bold text-[#E6B17E]">{hero.heroName}</span>
|
||||||
<div className="flex items-center mb-1">
|
</div>
|
||||||
{renderStars(hero.stars)}
|
<div className="flex items-center mb-1">
|
||||||
</div>
|
{renderStars(hero.stars)}
|
||||||
<div className="flex items-center text-[#C17F59] text-base font-medium">
|
</div>
|
||||||
{hero.role && (
|
<div className="flex items-center text-[#C17F59] text-base font-medium">
|
||||||
<>
|
{hero.role && (
|
||||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[#E6B17E] mr-2">
|
<>
|
||||||
<img src={`/pic/role/${hero.role}.png`} alt={hero.role} className="w-6 h-6" />
|
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[#E6B17E] mr-2">
|
||||||
</span>
|
<img src={`/pic/role/${hero.role}.png`} alt={hero.role} className="w-6 h-6" />
|
||||||
<span className="align-middle">{ROLE_LABELS[hero.role] || hero.role}</span>
|
</span>
|
||||||
</>
|
<span className="align-middle">{ROLE_LABELS[hero.role] || hero.role}</span>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16">
|
||||||
|
<div className="text-[#9B8579] text-xl font-medium mb-4">
|
||||||
|
暂无匹配的角色数据
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="text-[#7A6B5F] text-sm">
|
||||||
</div>
|
请尝试调整搜索条件或筛选条件
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user