ci: 添加 Epic UI 构建和部署工作流
- 新增 CI/CD 工作流文件,实现前端项目的自动构建和部署 - 支持 main、master 和 develop 分支的自动构建- 包含代码检出、环境安装、依赖管理、项目构建等步骤 - 实现构建产物的自动部署和 Docker 容器重启
This commit is contained in:
@@ -2,9 +2,9 @@ kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
#trigger:
|
||||
# event:
|
||||
# - manual
|
||||
trigger:
|
||||
event:
|
||||
- manual
|
||||
|
||||
steps:
|
||||
- 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 {useNavigate} from 'react-router-dom';
|
||||
import {Select} from 'antd';
|
||||
import {Input} from 'antd';
|
||||
import type {Hero} from '@/api/index';
|
||||
import * as EpicApi from '@/api/index';
|
||||
|
||||
@@ -14,60 +14,31 @@ type ExtendedHero = Hero & {
|
||||
};
|
||||
|
||||
// 自定义样式覆盖 Ant Design 默认样式
|
||||
const selectStyles = `
|
||||
.ant-select {
|
||||
background-color: #1A1412 !important;
|
||||
border-color: #C17F59 !important;
|
||||
color: #E6B17E !important;
|
||||
}
|
||||
|
||||
.ant-select .ant-select-selector {
|
||||
background-color: #1A1412 !important;
|
||||
border-color: #C17F59 !important;
|
||||
color: #E6B17E !important;
|
||||
}
|
||||
|
||||
.ant-select-focused .ant-select-selector {
|
||||
border-color: #C17F59 !important;
|
||||
box-shadow: 0 0 0 2px rgba(193, 127, 89, 0.2) !important;
|
||||
}
|
||||
|
||||
.ant-select-dropdown {
|
||||
background-color: #1A1412 !important;
|
||||
border-color: #C17F59 !important;
|
||||
}
|
||||
|
||||
.ant-select-item {
|
||||
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 inputStyles = `
|
||||
.ant-input, .ant-input-affix-wrapper, .ant-input-affix-wrapper input {
|
||||
background-color: #1A1412 !important;
|
||||
border-color: rgba(193, 127, 89, 0.3) !important;
|
||||
color: #E6B17E !important;
|
||||
border-radius: 0.5rem !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.ant-input:focus, .ant-input-affix-wrapper:focus, .ant-input-affix-wrapper-focused, .ant-input:active {
|
||||
background-color: #1A1412 !important;
|
||||
border-color: #C17F59 !important;
|
||||
color: #E6B17E !important;
|
||||
box-shadow: 0 0 0 2px rgba(193, 127, 89, 0.2) !important;
|
||||
}
|
||||
.ant-input::placeholder, .ant-input-affix-wrapper input::placeholder {
|
||||
color: #9B8579 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.ant-input-clear-icon {
|
||||
color: #E6B17E !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.ant-input-clear-icon:hover {
|
||||
color: #C17F59 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const ELEMENTS = [
|
||||
@@ -157,15 +128,15 @@ const Characters: React.FC = () => {
|
||||
return <img src={`/pic/role/${role}.png`} alt={role} className="w-7 h-7" />;
|
||||
};
|
||||
|
||||
// 处理英雄选择
|
||||
const handleHeroSelect = (value: string) => {
|
||||
setSearchTerm(value);
|
||||
// 处理搜索输入
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
@@ -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="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="选择英雄名称/昵称"
|
||||
value={searchTerm || undefined}
|
||||
onChange={handleHeroSelect}
|
||||
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>
|
||||
)
|
||||
}))}
|
||||
<Input
|
||||
placeholder="搜索英雄名称或昵称"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
allowClear
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -246,46 +202,57 @@ const Characters: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{filteredHeroes.map((hero) => (
|
||||
<div
|
||||
key={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">
|
||||
<img
|
||||
src={hero.headImgUrl}
|
||||
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>
|
||||
</div>
|
||||
{/* 右侧信息 */}
|
||||
<div className="flex flex-col justify-center flex-1 min-w-0">
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-[16px] font-bold text-[#E6B17E]">{hero.heroName}</span>
|
||||
</div>
|
||||
<div className="flex items-center mb-1">
|
||||
{renderStars(hero.stars)}
|
||||
</div>
|
||||
<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>
|
||||
<span className="align-middle">{ROLE_LABELS[hero.role] || hero.role}</span>
|
||||
</>
|
||||
)}
|
||||
{filteredHeroes.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{filteredHeroes.map((hero) => (
|
||||
<div
|
||||
key={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">
|
||||
<img
|
||||
src={hero.headImgUrl}
|
||||
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>
|
||||
</div>
|
||||
{/* 右侧信息 */}
|
||||
<div className="flex flex-col justify-center flex-1 min-w-0">
|
||||
<div className="flex items-center mb-1">
|
||||
<span className="text-[16px] font-bold text-[#E6B17E]">{hero.heroName}</span>
|
||||
</div>
|
||||
<div className="flex items-center mb-1">
|
||||
{renderStars(hero.stars)}
|
||||
</div>
|
||||
<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>
|
||||
<span className="align-middle">{ROLE_LABELS[hero.role] || hero.role}</span>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user