feat: 实现天梯排行数字大屏页面并统一品牌名称为英飒俱乐部
- 新增无需登录的数字大屏页面,包含蓝色和橙色主题 - 在管理后台仪表盘添加大屏入口快速按钮 - 扩展天梯排名接口,支持大屏显示模式(绕过最低参赛场次限制) - 统一将项目品牌名称从“影杀/羽动俱乐部”更新为“英飒俱乐部” - 更新相关配置文件、文档和界面中的品牌名称 - 添加公开数据接口用于获取门店列表和天梯排名
This commit is contained in:
parent
90505cba5f
commit
e0713c3fd8
69
.trae/documents/实现管理后台数字大屏排行页面.md
Normal file
69
.trae/documents/实现管理后台数字大屏排行页面.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
## 任务目标
|
||||||
|
|
||||||
|
在管理后台项目中实现一个无需登录、效果炫酷的“天梯排行”数字大屏页面。支持切换不同门店(羽毛球馆)查看实时排名。
|
||||||
|
|
||||||
|
## 技术实现方案
|
||||||
|
|
||||||
|
### 1. 路由与权限配置
|
||||||
|
|
||||||
|
* **新建路由**: 在 [router/index.js](file:///e:/workspace/yingsha/admin/src/router/index.js) 中添加顶级路由 `/display/ranking`。
|
||||||
|
|
||||||
|
* **免登访问**: 设置 `meta: { public: true }`,使其绕过全局路由守卫的登录检查。
|
||||||
|
|
||||||
|
* **独立布局**: 不使用 `MainLayout`,采用全新的全屏容器。
|
||||||
|
|
||||||
|
### 2. 公共数据接口
|
||||||
|
|
||||||
|
* **新建接口定义**: 创建 `src/api/display.js`,封装后端公开的 API 接口:
|
||||||
|
|
||||||
|
* `getPublicStores()`: 调用 `/store/list` 获取门店列表。
|
||||||
|
|
||||||
|
* `getPublicRanking(params)`: 调用 `/ladder/ranking` 获取天梯实时排行。
|
||||||
|
|
||||||
|
### 3. 数字大屏 UI 设计
|
||||||
|
|
||||||
|
* **视觉风格**:
|
||||||
|
|
||||||
|
* 采用深色系背景(科技感深蓝/纯黑),搭配青色、橙色、紫色等荧光色点缀。
|
||||||
|
|
||||||
|
* 使用大尺寸字体,增强远距离阅读性。
|
||||||
|
|
||||||
|
* 添加背景粒子效果或动态渐变背景。
|
||||||
|
|
||||||
|
* **页面布局**:
|
||||||
|
|
||||||
|
* **顶部**: 动态时间显示 + 酷炫的标题装饰(英飒俱乐部天梯战力榜)。
|
||||||
|
|
||||||
|
* **侧边/浮动**: 门店切换菜单,采用科技感按钮或自动轮播逻辑。
|
||||||
|
|
||||||
|
* **主体**: 排行榜列表。
|
||||||
|
|
||||||
|
* **前三名**: 特殊动效和皇冠/奖牌图标。
|
||||||
|
|
||||||
|
* **列表项**: 包含排名、头像、昵称、战力值(大字显示)、胜率及等级标识。
|
||||||
|
|
||||||
|
* **交互动效**:
|
||||||
|
|
||||||
|
* 列表入场时的交替淡入效果。
|
||||||
|
|
||||||
|
* 排名数据变动时的数字滚动动画。
|
||||||
|
|
||||||
|
* 自动轮播排行榜(如果数据超过一屏)。
|
||||||
|
|
||||||
|
### 4. 关键功能实现
|
||||||
|
|
||||||
|
* **门店切换**: 顶部或侧边提供门店选择器,并且url也要能够接受门店参数,实现每个门店单独的url。
|
||||||
|
|
||||||
|
* **自适应适配**: 采用 `transform: scale()` 或 `vw/vh` 方案,确保在各种分辨率的大屏电视或显示器上都能完美展示。
|
||||||
|
|
||||||
|
* **自动刷新**: 页面每 5 分钟自动拉取一次最新数据,确保排名实时性。
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. **创建 API 模块**: 定义公共访问接口。
|
||||||
|
2. **编写大屏组件**: 实现 HTML/CSS 结构与动画效果。
|
||||||
|
3. **配置路由**: 注册页面并开启免登权限。
|
||||||
|
4. **集成门店切换**: 实现手动切换与数据联动逻辑。
|
||||||
|
5. **整体视觉优化**: 调整色彩、光效和动画细节。
|
||||||
|
|
||||||
|
确认此方案后,我将开始代码编写。
|
||||||
12
README.md
12
README.md
@ -1,6 +1,6 @@
|
|||||||
# 羽动俱乐部 - 羽毛球/网球俱乐部管理系统
|
# 英飒俱乐部 - 羽毛球/网球俱乐部管理系统
|
||||||
|
|
||||||
一个完整的羽毛球/网球俱乐部会员管理系统,包含微信小程序端和后台管理界面。
|
一个完整的英飒俱乐部会员管理系统,包含微信小程序端和后台管理界面。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ npm run dev
|
|||||||
| `VITE_API_URL` | 否 | `/api` | 后端 API 地址 |
|
| `VITE_API_URL` | 否 | `/api` | 后端 API 地址 |
|
||||||
| `VITE_AMAP_KEY` | 是\* | 无 | 高德地图 JS API Key |
|
| `VITE_AMAP_KEY` | 是\* | 无 | 高德地图 JS API Key |
|
||||||
| `VITE_AMAP_SECURITY_CODE` | 是\* | 无 | 高德地图安全密钥 |
|
| `VITE_AMAP_SECURITY_CODE` | 是\* | 无 | 高德地图安全密钥 |
|
||||||
| `VITE_APP_TITLE` | 否 | `羽动俱乐部管理后台` | 应用标题 |
|
| `VITE_APP_TITLE` | 否 | `英飒俱乐部管理后台` | 应用标题 |
|
||||||
|
|
||||||
> \*注:如不使用门店地址定位功能,可不配置高德地图相关参数
|
> \*注:如不使用门店地址定位功能,可不配置高德地图相关参数
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ VITE_AMAP_KEY=your_amap_js_api_key
|
|||||||
VITE_AMAP_SECURITY_CODE=your_security_code
|
VITE_AMAP_SECURITY_CODE=your_security_code
|
||||||
|
|
||||||
# 应用标题
|
# 应用标题
|
||||||
VITE_APP_TITLE=羽动俱乐部管理后台
|
VITE_APP_TITLE=英飒俱乐部管理后台
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 高德地图配置说明
|
#### 高德地图配置说明
|
||||||
@ -224,8 +224,8 @@ VITE_APP_TITLE=羽动俱乐部管理后台
|
|||||||
| Lv1 | 新锐 | 掌握基础动作,能进行多拍回合 |
|
| Lv1 | 新锐 | 掌握基础动作,能进行多拍回合 |
|
||||||
| Lv2 | 精锐 | 技术较全面,具备初步战术意识 |
|
| Lv2 | 精锐 | 技术较全面,具备初步战术意识 |
|
||||||
| Lv3 | 高手 | 技术稳定,战术意图清晰 |
|
| Lv3 | 高手 | 技术稳定,战术意图清晰 |
|
||||||
| Lv4 | 大师 | 俱乐部顶尖战力 |
|
| Lv4 | 大师 | 英飒俱乐部顶尖战力 |
|
||||||
| Lv5 | 宗师 | 技术全面,俱乐部标杆 |
|
| Lv5 | 宗师 | 技术全面,英飒俱乐部标杆 |
|
||||||
|
|
||||||
## 主题配色
|
## 主题配色
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 羽动俱乐部管理后台 - 环境配置模板
|
# 英飒俱乐部管理后台 - 环境配置模板
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 使用说明:
|
# 使用说明:
|
||||||
# 1. 复制此文件为 .env.local(开发环境)或 .env.production(生产环境)
|
1. 复制此文件为 .env.local(开发环境)或 .env.production(生产环境)
|
||||||
# 2. 根据实际情况修改各项配置
|
2. 根据实际情况修改各项配置
|
||||||
# 3. 环境文件不应提交到版本控制
|
3. 环境文件不应提交到版本控制
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
@ -24,10 +24,10 @@ VITE_API_URL=/api
|
|||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# 高德地图 JS API 配置
|
# 高德地图 JS API 配置
|
||||||
# 申请步骤:
|
# 申请步骤:
|
||||||
# 1. 访问 https://console.amap.com/ 注册账号
|
1. 访问 https://console.amap.com/ 注册账号
|
||||||
# 2. 进入「应用管理」->「我的应用」->「创建新应用」
|
2. 进入「应用管理」->「我的应用」->「创建新应用」
|
||||||
# 3. 添加 Key,选择服务平台为「Web端(JS API)」
|
3. 添加 Key,选择服务平台为「Web端(JS API)」
|
||||||
# 4. 复制生成的 Key 和安全密钥填入下方
|
4. 复制生成的 Key 和安全密钥填入下方
|
||||||
# 注意:高德地图提供免费配额,个人开发足够使用
|
# 注意:高德地图提供免费配额,个人开发足够使用
|
||||||
|
|
||||||
# 高德地图 Key
|
# 高德地图 Key
|
||||||
@ -41,7 +41,7 @@ VITE_AMAP_SECURITY_CODE=your_amap_security_code_here
|
|||||||
# 其他配置(可选)
|
# 其他配置(可选)
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# 应用标题
|
# 应用标题
|
||||||
VITE_APP_TITLE=羽动俱乐部管理后台
|
VITE_APP_TITLE=英飒俱乐部管理后台
|
||||||
|
|
||||||
# 是否开启 Mock 数据(开发调试用)
|
# 是否开启 Mock 数据(开发调试用)
|
||||||
# VITE_USE_MOCK=false
|
# VITE_USE_MOCK=false
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>羽毛球俱乐部管理系统</title>
|
<title>英飒俱乐部管理系统</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
7
admin/src/api/display.js
Normal file
7
admin/src/api/display.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
// 获取公开门店列表
|
||||||
|
export const getPublicStores = () => request.get('/store/list')
|
||||||
|
|
||||||
|
// 获取公开天梯排名
|
||||||
|
export const getPublicRanking = (params) => request.get('/ladder/ranking', { params })
|
||||||
@ -4,7 +4,7 @@
|
|||||||
<el-aside :width="isCollapse ? '64px' : '220px'" class="sidebar">
|
<el-aside :width="isCollapse ? '64px' : '220px'" class="sidebar">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/favicon.svg" alt="logo" class="logo-icon" />
|
<img src="/favicon.svg" alt="logo" class="logo-icon" />
|
||||||
<span v-show="!isCollapse" class="logo-text">羽动俱乐部</span>
|
<span v-show="!isCollapse" class="logo-text">英飒俱乐部</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-menu
|
<el-menu
|
||||||
@ -14,8 +14,7 @@
|
|||||||
background-color="#1A1A2E"
|
background-color="#1A1A2E"
|
||||||
text-color="#A0A0A0"
|
text-color="#A0A0A0"
|
||||||
active-text-color="#FF6B35"
|
active-text-color="#FF6B35"
|
||||||
router
|
router>
|
||||||
>
|
|
||||||
<template v-for="item in menuRoutes" :key="item.path">
|
<template v-for="item in menuRoutes" :key="item.path">
|
||||||
<el-menu-item :index="'/' + item.path">
|
<el-menu-item :index="'/' + item.path">
|
||||||
<el-icon><component :is="item.meta?.icon" /></el-icon>
|
<el-icon><component :is="item.meta?.icon" /></el-icon>
|
||||||
@ -29,16 +28,15 @@
|
|||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<el-header class="header">
|
<el-header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<el-icon
|
<el-icon class="collapse-btn" @click="isCollapse = !isCollapse">
|
||||||
class="collapse-btn"
|
|
||||||
@click="isCollapse = !isCollapse"
|
|
||||||
>
|
|
||||||
<Fold v-if="!isCollapse" />
|
<Fold v-if="!isCollapse" />
|
||||||
<Expand v-else />
|
<Expand v-else />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<el-breadcrumb separator="/">
|
<el-breadcrumb separator="/">
|
||||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||||
<el-breadcrumb-item v-if="currentTitle">{{ currentTitle }}</el-breadcrumb-item>
|
<el-breadcrumb-item v-if="currentTitle">{{
|
||||||
|
currentTitle
|
||||||
|
}}</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -51,16 +49,20 @@
|
|||||||
<el-dropdown trigger="click" @command="handleCommand">
|
<el-dropdown trigger="click" @command="handleCommand">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<el-avatar :size="32" :src="userStore.userInfo?.avatar">
|
<el-avatar :size="32" :src="userStore.userInfo?.avatar">
|
||||||
{{ userStore.userInfo?.realName?.[0] || 'A' }}
|
{{ userStore.userInfo?.realName?.[0] || "A" }}
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
<span class="user-name">{{ userStore.userInfo?.realName || userStore.userInfo?.username }}</span>
|
<span class="user-name">{{
|
||||||
|
userStore.userInfo?.realName || userStore.userInfo?.username
|
||||||
|
}}</span>
|
||||||
<el-icon><ArrowDown /></el-icon>
|
<el-icon><ArrowDown /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item command="profile">个人设置</el-dropdown-item>
|
<el-dropdown-item command="profile">个人设置</el-dropdown-item>
|
||||||
<el-dropdown-item command="password">修改密码</el-dropdown-item>
|
<el-dropdown-item command="password">修改密码</el-dropdown-item>
|
||||||
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
<el-dropdown-item divided command="logout"
|
||||||
|
>退出登录</el-dropdown-item
|
||||||
|
>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@ -80,15 +82,28 @@
|
|||||||
|
|
||||||
<!-- 修改密码弹窗 -->
|
<!-- 修改密码弹窗 -->
|
||||||
<el-dialog v-model="showPasswordDialog" title="修改密码" width="400px">
|
<el-dialog v-model="showPasswordDialog" title="修改密码" width="400px">
|
||||||
<el-form :model="passwordForm" :rules="passwordRules" ref="passwordFormRef" label-width="80px">
|
<el-form
|
||||||
|
:model="passwordForm"
|
||||||
|
:rules="passwordRules"
|
||||||
|
ref="passwordFormRef"
|
||||||
|
label-width="80px">
|
||||||
<el-form-item label="旧密码" prop="old_password">
|
<el-form-item label="旧密码" prop="old_password">
|
||||||
<el-input v-model="passwordForm.old_password" type="password" show-password />
|
<el-input
|
||||||
|
v-model="passwordForm.old_password"
|
||||||
|
type="password"
|
||||||
|
show-password />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="新密码" prop="new_password">
|
<el-form-item label="新密码" prop="new_password">
|
||||||
<el-input v-model="passwordForm.new_password" type="password" show-password />
|
<el-input
|
||||||
|
v-model="passwordForm.new_password"
|
||||||
|
type="password"
|
||||||
|
show-password />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="确认密码" prop="confirm_password">
|
<el-form-item label="确认密码" prop="confirm_password">
|
||||||
<el-input v-model="passwordForm.confirm_password" type="password" show-password />
|
<el-input
|
||||||
|
v-model="passwordForm.confirm_password"
|
||||||
|
type="password"
|
||||||
|
show-password />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -99,100 +114,107 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from "@/stores/user";
|
||||||
import { updatePassword } from '@/api/admin'
|
import { updatePassword } from "@/api/admin";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const isCollapse = ref(false)
|
const isCollapse = ref(false);
|
||||||
const showPasswordDialog = ref(false)
|
const showPasswordDialog = ref(false);
|
||||||
const passwordFormRef = ref()
|
const passwordFormRef = ref();
|
||||||
const passwordForm = ref({
|
const passwordForm = ref({
|
||||||
old_password: '',
|
old_password: "",
|
||||||
new_password: '',
|
new_password: "",
|
||||||
confirm_password: ''
|
confirm_password: "",
|
||||||
})
|
});
|
||||||
|
|
||||||
const passwordRules = {
|
const passwordRules = {
|
||||||
old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
|
old_password: [
|
||||||
|
{ required: true, message: "请输入旧密码", trigger: "blur" },
|
||||||
|
],
|
||||||
new_password: [
|
new_password: [
|
||||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
{ min: 6, message: "密码长度不能少于6位", trigger: "blur" },
|
||||||
],
|
],
|
||||||
confirm_password: [
|
confirm_password: [
|
||||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
{ required: true, message: "请确认新密码", trigger: "blur" },
|
||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value !== passwordForm.value.new_password) {
|
if (value !== passwordForm.value.new_password) {
|
||||||
callback(new Error('两次输入的密码不一致'))
|
callback(new Error("两次输入的密码不一致"));
|
||||||
} else {
|
} else {
|
||||||
callback()
|
callback();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trigger: 'blur'
|
trigger: "blur",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
const currentRoute = computed(() => route.path)
|
const currentRoute = computed(() => route.path);
|
||||||
const currentTitle = computed(() => route.meta?.title)
|
const currentTitle = computed(() => route.meta?.title);
|
||||||
|
|
||||||
const menuRoutes = computed(() => {
|
const menuRoutes = computed(() => {
|
||||||
const routes = router.options.routes[1]?.children || []
|
const mainRoute = router.options.routes.find((r) => r.path === "/");
|
||||||
return routes.filter(r => {
|
const routes = mainRoute?.children || [];
|
||||||
if (r.meta?.hidden) return false
|
return routes.filter((r) => {
|
||||||
if (r.meta?.superAdmin && !userStore.isSuperAdmin) return false
|
if (r.meta?.hidden) return false;
|
||||||
return true
|
if (r.meta?.superAdmin && !userStore.isSuperAdmin) return false;
|
||||||
})
|
return true;
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handleCommand = (command) => {
|
const handleCommand = (command) => {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'profile':
|
case "profile":
|
||||||
// 可扩展个人设置页面
|
// 可扩展个人设置页面
|
||||||
break
|
break;
|
||||||
case 'password':
|
case "password":
|
||||||
showPasswordDialog.value = true
|
showPasswordDialog.value = true;
|
||||||
passwordForm.value = { old_password: '', new_password: '', confirm_password: '' }
|
passwordForm.value = {
|
||||||
break
|
old_password: "",
|
||||||
case 'logout':
|
new_password: "",
|
||||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
confirm_password: "",
|
||||||
type: 'warning'
|
};
|
||||||
|
break;
|
||||||
|
case "logout":
|
||||||
|
ElMessageBox.confirm("确定要退出登录吗?", "提示", {
|
||||||
|
type: "warning",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
userStore.logout()
|
userStore.logout();
|
||||||
})
|
});
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleUpdatePassword = async () => {
|
const handleUpdatePassword = async () => {
|
||||||
await passwordFormRef.value?.validate()
|
await passwordFormRef.value?.validate();
|
||||||
await updatePassword({
|
await updatePassword({
|
||||||
old_password: passwordForm.value.old_password,
|
old_password: passwordForm.value.old_password,
|
||||||
new_password: passwordForm.value.new_password
|
new_password: passwordForm.value.new_password,
|
||||||
})
|
});
|
||||||
ElMessage.success('密码修改成功,请重新登录')
|
ElMessage.success("密码修改成功,请重新登录");
|
||||||
showPasswordDialog.value = false
|
showPasswordDialog.value = false;
|
||||||
userStore.logout()
|
userStore.logout();
|
||||||
}
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
userStore.fetchProfile()
|
userStore.fetchProfile();
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.main-layout {
|
.main-layout {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background: #1A1A2E;
|
background: #1a1a2e;
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -220,14 +242,14 @@ onMounted(() => {
|
|||||||
.el-menu {
|
.el-menu {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #F5F7FA;
|
background: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -277,20 +299,20 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,108 +1,120 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: "/login",
|
||||||
name: 'Login',
|
name: "Login",
|
||||||
component: () => import('@/views/login/index.vue'),
|
component: () => import("@/views/login/index.vue"),
|
||||||
meta: { title: '登录', public: true }
|
meta: { title: "登录", public: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
component: () => import('@/layouts/MainLayout.vue'),
|
component: () => import("@/layouts/MainLayout.vue"),
|
||||||
redirect: '/dashboard',
|
redirect: "/dashboard",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: "dashboard",
|
||||||
name: 'Dashboard',
|
name: "Dashboard",
|
||||||
component: () => import('@/views/dashboard/index.vue'),
|
component: () => import("@/views/dashboard/index.vue"),
|
||||||
meta: { title: '首页', icon: 'HomeFilled' }
|
meta: { title: "首页", icon: "HomeFilled" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: "users",
|
||||||
name: 'Users',
|
name: "Users",
|
||||||
component: () => import('@/views/user/index.vue'),
|
component: () => import("@/views/user/index.vue"),
|
||||||
meta: { title: '用户管理', icon: 'User', superAdmin: true }
|
meta: { title: "用户管理", icon: "User", superAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'stores',
|
path: "stores",
|
||||||
name: 'Stores',
|
name: "Stores",
|
||||||
component: () => import('@/views/store/index.vue'),
|
component: () => import("@/views/store/index.vue"),
|
||||||
meta: { title: '门店管理', icon: 'OfficeBuilding', superAdmin: true }
|
meta: { title: "门店管理", icon: "OfficeBuilding", superAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'ladder',
|
path: "ladder",
|
||||||
name: 'Ladder',
|
name: "Ladder",
|
||||||
component: () => import('@/views/ladder/index.vue'),
|
component: () => import("@/views/ladder/index.vue"),
|
||||||
meta: { title: '天梯用户', icon: 'TrendCharts' }
|
meta: { title: "天梯用户", icon: "TrendCharts" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'matches',
|
path: "matches",
|
||||||
name: 'Matches',
|
name: "Matches",
|
||||||
component: () => import('@/views/match/index.vue'),
|
component: () => import("@/views/match/index.vue"),
|
||||||
meta: { title: '比赛管理', icon: 'Trophy' }
|
meta: { title: "比赛管理", icon: "Trophy" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'matches/:id',
|
path: "matches/:id",
|
||||||
name: 'MatchDetail',
|
name: "MatchDetail",
|
||||||
component: () => import('@/views/match/detail.vue'),
|
component: () => import("@/views/match/detail.vue"),
|
||||||
meta: { title: '比赛详情', hidden: true }
|
meta: { title: "比赛详情", hidden: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'points/actions',
|
path: "points/actions",
|
||||||
name: 'PointActions',
|
name: "PointActions",
|
||||||
component: () => import('@/views/points/actions.vue'),
|
component: () => import("@/views/points/actions.vue"),
|
||||||
meta: { title: '积分行为', icon: 'StarFilled' }
|
meta: { title: "积分行为", icon: "StarFilled" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'points/products',
|
path: "points/products",
|
||||||
name: 'PointProducts',
|
name: "PointProducts",
|
||||||
component: () => import('@/views/points/products.vue'),
|
component: () => import("@/views/points/products.vue"),
|
||||||
meta: { title: '积分商品', icon: 'GoodsFilled' }
|
meta: { title: "积分商品", icon: "GoodsFilled" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'points/orders',
|
path: "points/orders",
|
||||||
name: 'PointOrders',
|
name: "PointOrders",
|
||||||
component: () => import('@/views/points/orders.vue'),
|
component: () => import("@/views/points/orders.vue"),
|
||||||
meta: { title: '兑换订单', icon: 'List' }
|
meta: { title: "兑换订单", icon: "List" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'system/users',
|
path: "system/users",
|
||||||
name: 'SystemUsers',
|
name: "SystemUsers",
|
||||||
component: () => import('@/views/system/users.vue'),
|
component: () => import("@/views/system/users.vue"),
|
||||||
meta: { title: '系统用户', icon: 'UserFilled', superAdmin: true }
|
meta: { title: "系统用户", icon: "UserFilled", superAdmin: true },
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
path: "/display/ranking",
|
||||||
|
name: "RankingBoard",
|
||||||
|
component: () => import("@/views/display/RankingBoard.vue"),
|
||||||
|
meta: { title: "天梯排行大屏", public: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/display/ranking-orange",
|
||||||
|
name: "RankingBoardOrange",
|
||||||
|
component: () => import("@/views/display/RankingBoardOrange.vue"),
|
||||||
|
meta: { title: "天梯排行大屏(橙色)", public: true },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
})
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
document.title = `${to.meta.title || ''} - 羽毛球俱乐部管理系统`
|
document.title = `${to.meta.title || ""} - 英飒俱乐部管理系统`;
|
||||||
|
|
||||||
if (to.meta.public) {
|
if (to.meta.public) {
|
||||||
next()
|
next();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore();
|
||||||
if (!userStore.token) {
|
if (!userStore.token) {
|
||||||
next('/login')
|
next("/login");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 超管权限检查
|
// 超管权限检查
|
||||||
if (to.meta.superAdmin && userStore.userInfo?.role !== 'super_admin') {
|
if (to.meta.superAdmin && userStore.userInfo?.role !== "super_admin") {
|
||||||
next('/dashboard')
|
next("/dashboard");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next();
|
||||||
})
|
});
|
||||||
|
|
||||||
export default router
|
export default router;
|
||||||
|
|||||||
@ -51,6 +51,14 @@
|
|||||||
<el-icon class="icon"><View /></el-icon>
|
<el-icon class="icon"><View /></el-icon>
|
||||||
<span class="label">扫码核销</span>
|
<span class="label">扫码核销</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="quick-action-btn" @click="$router.push('/display/ranking')">
|
||||||
|
<el-icon class="icon"><Monitor /></el-icon>
|
||||||
|
<span class="label">天梯大屏(蓝)</span>
|
||||||
|
</div>
|
||||||
|
<div class="quick-action-btn" @click="$router.push('/display/ranking-orange')">
|
||||||
|
<el-icon class="icon"><Monitor /></el-icon>
|
||||||
|
<span class="label">天梯大屏(橙)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -263,7 +271,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, nextTick, watch } from 'vue'
|
import { ref, onMounted, nextTick, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { View, Loading, InfoFilled, Search, User, Check } from '@element-plus/icons-vue'
|
import { View, Loading, InfoFilled, Search, User, Check, Monitor } from '@element-plus/icons-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { getDashboard, getPointActions, executePointAction, getMatches, getPointOrders, verifyOrder, verifyByCode, searchUsers } from '@/api/admin'
|
import { getDashboard, getPointActions, executePointAction, getMatches, getPointOrders, verifyOrder, verifyByCode, searchUsers } from '@/api/admin'
|
||||||
|
|
||||||
|
|||||||
989
admin/src/views/display/RankingBoard.vue
Normal file
989
admin/src/views/display/RankingBoard.vue
Normal file
@ -0,0 +1,989 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ranking-board-container">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
<div class="board-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="current-time">{{ currentTime }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-center">
|
||||||
|
<div class="title-bg">
|
||||||
|
<h1 class="main-title">英飒俱乐部 · 天梯战力榜</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div
|
||||||
|
class="style-switcher"
|
||||||
|
@click="
|
||||||
|
router.push({ path: '/display/ranking-orange', query: route.query })
|
||||||
|
">
|
||||||
|
<el-icon><Monitor /></el-icon>
|
||||||
|
<span>切换 Premium 风格</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-selector" @click="showStoreMenu = !showStoreMenu">
|
||||||
|
<span class="store-label">当前场馆:</span>
|
||||||
|
<span class="store-name">{{ currentStore?.name || "请选择" }}</span>
|
||||||
|
<el-icon class="arrow-icon" :class="{ rotate: showStoreMenu }"
|
||||||
|
><ArrowDown
|
||||||
|
/></el-icon>
|
||||||
|
|
||||||
|
<!-- 门店切换菜单 -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="showStoreMenu" class="store-menu" @click.stop>
|
||||||
|
<div
|
||||||
|
v-for="store in stores"
|
||||||
|
:key="store.id"
|
||||||
|
class="store-item"
|
||||||
|
:class="{ active: currentStore?.id === store.id }"
|
||||||
|
@click="selectStore(store)">
|
||||||
|
{{ store.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体内容区 -->
|
||||||
|
<div class="board-body">
|
||||||
|
<!-- 左右装饰线条 -->
|
||||||
|
<div class="side-decoration left"></div>
|
||||||
|
<div class="side-decoration right"></div>
|
||||||
|
|
||||||
|
<!-- 数据刷新提示 -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="isRefreshing" class="refresh-indicator">
|
||||||
|
<el-icon class="is-loading"><Refresh /></el-icon>
|
||||||
|
<span>同步最新战力数据...</span>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- 排行榜主体 -->
|
||||||
|
<div class="ranking-list-wrapper">
|
||||||
|
<div class="list-header">
|
||||||
|
<div class="col-rank">排名</div>
|
||||||
|
<div class="col-player">选手</div>
|
||||||
|
<div class="col-level">等级</div>
|
||||||
|
<div class="col-matches">场次/胜率</div>
|
||||||
|
<div class="col-score">战力值</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-body" ref="scrollContainer">
|
||||||
|
<div
|
||||||
|
class="scroll-content"
|
||||||
|
:style="{ transform: `translateY(${scrollY}px)` }">
|
||||||
|
<div
|
||||||
|
v-for="(player, index) in rankingList"
|
||||||
|
:key="'p1-' + player.id"
|
||||||
|
class="ranking-item"
|
||||||
|
:class="'rank-' + player.rank">
|
||||||
|
<div class="scan-line"></div>
|
||||||
|
<div class="col-rank">
|
||||||
|
<div v-if="player.rank <= 3" class="rank-badge">
|
||||||
|
<span class="rank-num">{{ player.rank }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="rank-num">{{ player.rank }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-player">
|
||||||
|
<div class="player-info">
|
||||||
|
<el-avatar
|
||||||
|
:size="50"
|
||||||
|
:src="player.avatar"
|
||||||
|
class="player-avatar">
|
||||||
|
{{ player.realName?.[0] || player.nickname?.[0] || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
<div class="player-names">
|
||||||
|
<span class="real-name">{{ player.realName }}</span>
|
||||||
|
<span class="nickname">{{ player.nickname }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-level">
|
||||||
|
<span :class="['level-tag', 'lv' + player.level]">{{
|
||||||
|
player.levelName
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-matches">
|
||||||
|
<div class="match-stats">
|
||||||
|
<span class="count">{{ player.matchCount }}场</span>
|
||||||
|
<span class="rate">{{ player.winRate }}% 胜率</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-score">
|
||||||
|
<div class="score-wrapper">
|
||||||
|
<span class="score-value">{{ player.powerScore }}</span>
|
||||||
|
<div class="score-bar">
|
||||||
|
<div
|
||||||
|
class="bar-fill"
|
||||||
|
:style="{
|
||||||
|
width: (player.powerScore / 3000) * 100 + '%',
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复制一份数据实现无缝循环 -->
|
||||||
|
<div
|
||||||
|
v-for="(player, index) in rankingList"
|
||||||
|
:key="'p2-' + player.id"
|
||||||
|
class="ranking-item"
|
||||||
|
:class="'rank-' + player.rank">
|
||||||
|
<div class="scan-line"></div>
|
||||||
|
<div class="col-rank">
|
||||||
|
<div v-if="player.rank <= 3" class="rank-badge">
|
||||||
|
<span class="rank-num">{{ player.rank }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="rank-num">{{ player.rank }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-player">
|
||||||
|
<div class="player-info">
|
||||||
|
<el-avatar
|
||||||
|
:size="50"
|
||||||
|
:src="player.avatar"
|
||||||
|
class="player-avatar">
|
||||||
|
{{ player.realName?.[0] || player.nickname?.[0] || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
<div class="player-names">
|
||||||
|
<span class="real-name">{{ player.realName }}</span>
|
||||||
|
<span class="nickname">{{ player.nickname }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-level">
|
||||||
|
<span :class="['level-tag', 'lv' + player.level]">{{
|
||||||
|
player.levelName
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-matches">
|
||||||
|
<div class="match-stats">
|
||||||
|
<span class="count">{{ player.matchCount }}场</span>
|
||||||
|
<span class="rate">{{ player.winRate }}% 胜率</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-score">
|
||||||
|
<div class="score-wrapper">
|
||||||
|
<span class="score-value">{{ player.powerScore }}</span>
|
||||||
|
<div class="score-bar">
|
||||||
|
<div
|
||||||
|
class="bar-fill"
|
||||||
|
:style="{
|
||||||
|
width: (player.powerScore / 3000) * 100 + '%',
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="!loading && rankingList.length === 0" class="empty-state">
|
||||||
|
暂无排行数据
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="loading" class="loading-state">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<span>数据加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部装饰 -->
|
||||||
|
<div class="board-footer">
|
||||||
|
<div class="footer-hint">数据实时同步 · 每30秒刷新一次</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { getPublicStores, getPublicRanking } from "@/api/display";
|
||||||
|
import { ArrowDown, Refresh, Monitor } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const currentTime = ref(dayjs().format("YYYY-MM-DD HH:mm:ss"));
|
||||||
|
const stores = ref([]);
|
||||||
|
const currentStore = ref(null);
|
||||||
|
const rankingList = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const showStoreMenu = ref(false);
|
||||||
|
const scrollContainer = ref(null);
|
||||||
|
const isRefreshing = ref(false);
|
||||||
|
let timer = null;
|
||||||
|
let refreshTimer = null;
|
||||||
|
const scrollReqId = ref(null);
|
||||||
|
const scrollY = ref(0); // 使用 transform 的偏移量
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
const updateTime = () => {
|
||||||
|
currentTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连续无缝滚动逻辑 (使用 Transform 性能更好)
|
||||||
|
const startContinuousScroll = () => {
|
||||||
|
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||||
|
|
||||||
|
const scroll = () => {
|
||||||
|
if (isRefreshing.value || rankingList.value.length === 0) {
|
||||||
|
scrollReqId.value = requestAnimationFrame(scroll);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每一帧移动的像素 (降低速度,使其更易阅读)
|
||||||
|
scrollY.value -= 0.6;
|
||||||
|
|
||||||
|
// 计算单份数据的高度:100条 * (90px行高 + 12px间距)
|
||||||
|
const singleSetHeight = rankingList.value.length * 102;
|
||||||
|
|
||||||
|
// 如果滚完了一整套数据,瞬间重置回 0
|
||||||
|
if (Math.abs(scrollY.value) >= singleSetHeight) {
|
||||||
|
scrollY.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollReqId.value = requestAnimationFrame(scroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollReqId.value = requestAnimationFrame(scroll);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取门店列表
|
||||||
|
const fetchStores = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getPublicStores();
|
||||||
|
stores.value = res.data?.list || [];
|
||||||
|
|
||||||
|
// 如果 URL 中有 store_id,则优先使用
|
||||||
|
const urlStoreId = route.query.store_id;
|
||||||
|
if (urlStoreId) {
|
||||||
|
const found = stores.value.find((s) => s.id === parseInt(urlStoreId));
|
||||||
|
if (found) {
|
||||||
|
currentStore.value = found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有选中的门店,默认选第一个
|
||||||
|
if (!currentStore.value && stores.value.length > 0) {
|
||||||
|
currentStore.value = stores.value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStore.value) {
|
||||||
|
fetchRanking();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取门店失败:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取排名数据
|
||||||
|
const fetchRanking = async () => {
|
||||||
|
if (!currentStore.value) return;
|
||||||
|
|
||||||
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getPublicRanking({
|
||||||
|
store_id: currentStore.value.id,
|
||||||
|
pageSize: 100, // 大屏展示前100名
|
||||||
|
is_display: 1, // 告知后端是大屏展示,可以忽略每月最低场次限制
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果数据没变,不重置滚动位置
|
||||||
|
const newList = res.data?.list || [];
|
||||||
|
rankingList.value = newList;
|
||||||
|
|
||||||
|
// 数据加载后开始滚动
|
||||||
|
if (rankingList.value.length > 0) {
|
||||||
|
nextTick(() => {
|
||||||
|
startContinuousScroll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取排名失败:", err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换门店
|
||||||
|
const selectStore = (store) => {
|
||||||
|
currentStore.value = store;
|
||||||
|
showStoreMenu.value = false;
|
||||||
|
// 更新 URL 方便分享/刷新
|
||||||
|
router.replace({ query: { ...route.query, store_id: store.id } });
|
||||||
|
fetchRanking();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStores();
|
||||||
|
timer = setInterval(updateTime, 1000);
|
||||||
|
refreshTimer = setInterval(fetchRanking, 30 * 1000); // 30秒刷新一次,避免过于频繁影响滚动感
|
||||||
|
|
||||||
|
// 点击外部关闭菜单
|
||||||
|
window.addEventListener("click", () => {
|
||||||
|
showStoreMenu.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
if (refreshTimer) clearInterval(refreshTimer);
|
||||||
|
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||||
|
window.removeEventListener("click", () => {
|
||||||
|
showStoreMenu.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听路由参数变化(比如用户直接修改 URL)
|
||||||
|
watch(
|
||||||
|
() => route.query.store_id,
|
||||||
|
(newId) => {
|
||||||
|
if (newId && stores.value.length > 0) {
|
||||||
|
const found = stores.value.find((s) => s.id === parseInt(newId));
|
||||||
|
if (found && found.id !== currentStore.value?.id) {
|
||||||
|
currentStore.value = found;
|
||||||
|
fetchRanking();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ranking-board-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #050a18;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(
|
||||||
|
circle at 50% 0%,
|
||||||
|
rgba(26, 63, 131, 0.4) 0%,
|
||||||
|
transparent 50%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 0% 100%,
|
||||||
|
rgba(18, 32, 64, 0.3) 0%,
|
||||||
|
transparent 40%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 100% 100%,
|
||||||
|
rgba(18, 32, 64, 0.3) 0%,
|
||||||
|
transparent 40%
|
||||||
|
);
|
||||||
|
color: #fff;
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M54.826 10.531c1.007 0 1.823.816 1.823 1.823 0 1.006-.816 1.822-1.823 1.822-1.006 0-1.822-.816-1.822-1.822 0-1.007.816-1.823 1.822-1.823zM4.785 4.785c1.007 0 1.823.816 1.823 1.823 0 1.006-.816 1.822-1.823 1.822-1.006 0-1.822-.816-1.822-1.822 0-1.007.816-1.823 1.822-1.823z' fill='%23ffffff' fill-opacity='0.03' fill-rule='evenodd'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
animation: pulse 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(0, 242, 255, 0.15);
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #00f2ff;
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
z-index: 20;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 242, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部样式 */
|
||||||
|
.board-header {
|
||||||
|
height: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 40px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
background: linear-gradient(to bottom, rgba(10, 25, 51, 0.8), transparent);
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
flex: 1;
|
||||||
|
.current-time {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #00f2ff;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 242, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-center {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.title-bg {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px 60px;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 100px;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(to right, transparent, #00f2ff);
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
left: -110px;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
right: -110px;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 48px;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 8px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(to bottom, #fff, #a5d8ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
text-shadow: 0 0 20px rgba(0, 242, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.style-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.2);
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #a5d8ff;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 242, 255, 0.1);
|
||||||
|
border-color: #00f2ff;
|
||||||
|
color: #00f2ff;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 242, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-selector {
|
||||||
|
background: rgba(0, 242, 255, 0.1);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 242, 255, 0.2);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 242, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #a5d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
&.rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 110%;
|
||||||
|
right: 0;
|
||||||
|
width: 200px;
|
||||||
|
background: #0a1933;
|
||||||
|
border: 1px solid #00f2ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 10px 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.store-item {
|
||||||
|
padding: 12px 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 242, 255, 0.1);
|
||||||
|
color: #00f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #00f2ff;
|
||||||
|
background: rgba(0, 242, 255, 0.15);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主体样式 */
|
||||||
|
.board-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 60px 20px 60px; /* 底部内边距调整为 50px */
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0; /* 确保子元素 flex 正确 */
|
||||||
|
|
||||||
|
.side-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 4px;
|
||||||
|
height: 60%;
|
||||||
|
background: linear-gradient(to bottom, transparent, #00f2ff, transparent);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
opacity: 0.3;
|
||||||
|
&.left {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-list-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(255, 255, 255, 0.01);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 0 40px rgba(0, 0, 0, 0.6),
|
||||||
|
inset 0 0 30px rgba(0, 242, 255, 0.03);
|
||||||
|
position: relative;
|
||||||
|
/* 移除 margin-bottom,由父级 padding 处理 */
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 150px; /* 加深底部遮挡,强化视觉渐变 */
|
||||||
|
background: linear-gradient(to top, #050a18 10%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(0, 242, 255, 0.12);
|
||||||
|
padding: 25px 20px;
|
||||||
|
font-size: 22px;
|
||||||
|
color: #00f2ff;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
border-bottom: 2px solid rgba(0, 242, 255, 0.3);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
div {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto; /* 允许滚动,但隐藏滚动条 */
|
||||||
|
padding: 10px 0;
|
||||||
|
scroll-behavior: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 90px; /* 稍微增加行高 */
|
||||||
|
padding: 0 30px;
|
||||||
|
margin: 0 20px 12px 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 242, 255, 0.1);
|
||||||
|
border-color: rgba(0, 242, 255, 0.4);
|
||||||
|
transform: scale(1.01) translateX(5px);
|
||||||
|
box-shadow: 0 0 25px rgba(0, 242, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-line {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent,
|
||||||
|
rgba(0, 242, 255, 0.1),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transform: skewX(-25deg);
|
||||||
|
animation: scanMove 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 前三名特殊样式 */
|
||||||
|
&.rank-1 {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 215, 0, 0.15),
|
||||||
|
rgba(255, 215, 0, 0.05)
|
||||||
|
);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
.rank-badge {
|
||||||
|
background: #ffd700;
|
||||||
|
color: #000;
|
||||||
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
.score-value {
|
||||||
|
color: #ffd700;
|
||||||
|
font-size: 32px;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.rank-2 {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(192, 192, 192, 0.15),
|
||||||
|
rgba(192, 192, 192, 0.05)
|
||||||
|
);
|
||||||
|
border-color: rgba(192, 192, 192, 0.3);
|
||||||
|
.rank-badge {
|
||||||
|
background: #c0c0c0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.score-value {
|
||||||
|
color: #c0c0c0;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.rank-3 {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(205, 127, 50, 0.15),
|
||||||
|
rgba(205, 127, 50, 0.05)
|
||||||
|
);
|
||||||
|
border-color: rgba(205, 127, 50, 0.3);
|
||||||
|
.rank-badge {
|
||||||
|
background: #cd7f32;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.score-value {
|
||||||
|
color: #cd7f32;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列宽度分配 */
|
||||||
|
.col-rank {
|
||||||
|
width: 100px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.col-player {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
.col-level {
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.col-matches {
|
||||||
|
width: 200px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.col-score {
|
||||||
|
flex: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-badge {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.player-avatar {
|
||||||
|
border: 2px solid rgba(0, 242, 255, 0.3);
|
||||||
|
background: #1a2f4d;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-names {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.real-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.nickname {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #00f2ff;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tag {
|
||||||
|
padding: 4px 16px;
|
||||||
|
border-radius: 4px; /* 赛博朋克风改用方角或微圆 */
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
&.lv1 {
|
||||||
|
background: rgba(46, 125, 50, 0.2);
|
||||||
|
color: #81c784;
|
||||||
|
border: 1px solid #2e7d32;
|
||||||
|
}
|
||||||
|
&.lv2 {
|
||||||
|
background: rgba(21, 101, 192, 0.2);
|
||||||
|
color: #64b5f6;
|
||||||
|
border: 1px solid #1565c0;
|
||||||
|
}
|
||||||
|
&.lv3 {
|
||||||
|
background: rgba(230, 81, 0, 0.2);
|
||||||
|
color: #ffb74d;
|
||||||
|
border: 1px solid #e65100;
|
||||||
|
}
|
||||||
|
&.lv4 {
|
||||||
|
background: rgba(194, 24, 91, 0.2);
|
||||||
|
color: #f06292;
|
||||||
|
border: 1px solid #c2185b;
|
||||||
|
}
|
||||||
|
&.lv5 {
|
||||||
|
background: rgba(123, 31, 162, 0.2);
|
||||||
|
color: #ba68c8;
|
||||||
|
border: 1px solid #7b1fa2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
.count {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rate {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #00f2ff;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #00f2ff;
|
||||||
|
text-align: right;
|
||||||
|
font-family: "Arial Black", sans-serif;
|
||||||
|
text-shadow: 0 0 15px rgba(0, 242, 255, 0.5);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
.bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to right, #00f2ff, #0072ff, #00f2ff);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: barGlow 2s linear infinite;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 242, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes barGlow {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部样式 */
|
||||||
|
.board-footer {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(5, 10, 24, 0.9); /* 加深底部背景,增强边界感 */
|
||||||
|
border-top: 1px solid rgba(0, 242, 255, 0.1);
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
.footer-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #5c78a7;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
.list-complete-enter-from,
|
||||||
|
.list-complete-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
.list-complete-item {
|
||||||
|
transition: all 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state,
|
||||||
|
.loading-state {
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #5c78a7;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 3px solid rgba(0, 242, 255, 0.1);
|
||||||
|
border-top-color: #00f2ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanMove {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 200%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
858
admin/src/views/display/RankingBoardOrange.vue
Normal file
858
admin/src/views/display/RankingBoardOrange.vue
Normal file
@ -0,0 +1,858 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ranking-board-orange-v2">
|
||||||
|
<!-- 背景特效层 -->
|
||||||
|
<div class="bg-glow"></div>
|
||||||
|
<div class="bg-grid"></div>
|
||||||
|
|
||||||
|
<!-- 顶部控制条 -->
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="time-box">{{ currentTime }}</div>
|
||||||
|
<div class="title-box">
|
||||||
|
<span class="title-icon">🏆</span>
|
||||||
|
<h1 class="main-title">英飒俱乐部 · 荣耀天梯</h1>
|
||||||
|
<span class="title-tag">PREMIUM</span>
|
||||||
|
</div>
|
||||||
|
<div class="right-controls">
|
||||||
|
<div
|
||||||
|
class="style-switcher"
|
||||||
|
@click="
|
||||||
|
router.push({ path: '/display/ranking', query: route.query })
|
||||||
|
">
|
||||||
|
<el-icon><Monitor /></el-icon>
|
||||||
|
<span>切换经典风格</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-box" @click="showStoreMenu = !showStoreMenu">
|
||||||
|
<span class="label">场馆:</span>
|
||||||
|
<span class="value">{{ currentStore?.name || "未选择" }}</span>
|
||||||
|
<el-icon :class="{ rotate: showStoreMenu }"><ArrowDown /></el-icon>
|
||||||
|
|
||||||
|
<transition name="slide-down">
|
||||||
|
<div v-if="showStoreMenu" class="store-dropdown" @click.stop>
|
||||||
|
<div
|
||||||
|
v-for="store in stores"
|
||||||
|
:key="store.id"
|
||||||
|
class="dropdown-item"
|
||||||
|
:class="{ active: currentStore?.id === store.id }"
|
||||||
|
@click="selectStore(store)">
|
||||||
|
{{ store.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 左侧:名人堂 (Top 3) -->
|
||||||
|
<div class="hall-of-fame">
|
||||||
|
<div class="panel-title">HALL OF FAME</div>
|
||||||
|
<div class="podium">
|
||||||
|
<!-- 第二名 -->
|
||||||
|
<div v-if="rankingList[1]" class="podium-card rank-2">
|
||||||
|
<div class="rank-label">NO.2</div>
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<el-avatar :size="70" :src="rankingList[1].avatar" class="avatar">
|
||||||
|
{{ rankingList[1].realName?.[0] || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
<div class="crown">🥈</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="name">{{ rankingList[1].realName }}</div>
|
||||||
|
<div class="score">{{ rankingList[1].powerScore }}</div>
|
||||||
|
<div class="stats-row">
|
||||||
|
<span class="level">{{ rankingList[1].levelName }}</span>
|
||||||
|
<span class="wins"
|
||||||
|
>{{
|
||||||
|
Math.round(
|
||||||
|
(rankingList[1].matchCount * rankingList[1].winRate) /
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
}}胜</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第一名 -->
|
||||||
|
<div v-if="rankingList[0]" class="podium-card rank-1">
|
||||||
|
<div class="rank-label">CHAMPION</div>
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<el-avatar
|
||||||
|
:size="100"
|
||||||
|
:src="rankingList[0].avatar"
|
||||||
|
class="avatar">
|
||||||
|
{{ rankingList[0].realName?.[0] || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
<div class="crown">👑</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="name">{{ rankingList[0].realName }}</div>
|
||||||
|
<div class="score">{{ rankingList[0].powerScore }}</div>
|
||||||
|
<div class="stats-row">
|
||||||
|
<span class="level">{{ rankingList[0].levelName }}</span>
|
||||||
|
<span class="wins"
|
||||||
|
>{{
|
||||||
|
Math.round(
|
||||||
|
(rankingList[0].matchCount * rankingList[0].winRate) /
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
}}胜</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fire-effect"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三名 -->
|
||||||
|
<div v-if="rankingList[2]" class="podium-card rank-3">
|
||||||
|
<div class="rank-label">NO.3</div>
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<el-avatar :size="70" :src="rankingList[2].avatar" class="avatar">
|
||||||
|
{{ rankingList[2].realName?.[0] || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
<div class="crown">🥉</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="name">{{ rankingList[2].realName }}</div>
|
||||||
|
<div class="score">{{ rankingList[2].powerScore }}</div>
|
||||||
|
<div class="stats-row">
|
||||||
|
<span class="level">{{ rankingList[2].levelName }}</span>
|
||||||
|
<span class="wins"
|
||||||
|
>{{
|
||||||
|
Math.round(
|
||||||
|
(rankingList[2].matchCount * rankingList[2].winRate) /
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
}}胜</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:争夺区 (Rank 4-100) -->
|
||||||
|
<div class="contenders-zone">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-title">CONTENDERS</div>
|
||||||
|
<div class="refresh-tag" v-if="isRefreshing">
|
||||||
|
<el-icon class="is-loading"><Refresh /></el-icon> 同步中
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scroll-container" ref="scrollContainer">
|
||||||
|
<div class="scroll-mask">
|
||||||
|
<div
|
||||||
|
class="scroll-wrapper"
|
||||||
|
:style="{ transform: `translateY(${scrollY}px)` }">
|
||||||
|
<!-- 第一组数据 -->
|
||||||
|
<div class="grid-layout" ref="group1">
|
||||||
|
<div
|
||||||
|
v-for="player in rankingList.slice(3)"
|
||||||
|
:key="'c1-' + player.id"
|
||||||
|
class="contender-card">
|
||||||
|
<div class="card-rank">{{ player.rank }}</div>
|
||||||
|
<div class="card-avatar">
|
||||||
|
<el-avatar :size="50" :src="player.avatar">
|
||||||
|
{{ player.realName?.[0] || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-name">{{ player.realName }}</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-level">{{ player.levelName }}</span>
|
||||||
|
<span class="card-wins"
|
||||||
|
>{{
|
||||||
|
Math.round(
|
||||||
|
(player.matchCount * player.winRate) / 100,
|
||||||
|
)
|
||||||
|
}}胜</span
|
||||||
|
>
|
||||||
|
<span class="card-winrate">{{ player.winRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-score">{{ player.powerScore }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 第二组数据 (复制一份实现无缝) -->
|
||||||
|
<div class="grid-layout">
|
||||||
|
<div
|
||||||
|
v-for="player in rankingList.slice(3)"
|
||||||
|
:key="'c2-' + player.id"
|
||||||
|
class="contender-card">
|
||||||
|
<div class="card-rank">{{ player.rank }}</div>
|
||||||
|
<div class="card-avatar">
|
||||||
|
<el-avatar :size="50" :src="player.avatar">
|
||||||
|
{{ player.realName?.[0] || "?" }}
|
||||||
|
</el-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-name">{{ player.realName }}</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-level">{{ player.levelName }}</span>
|
||||||
|
<span class="card-wins"
|
||||||
|
>{{
|
||||||
|
Math.round(
|
||||||
|
(player.matchCount * player.winRate) / 100,
|
||||||
|
)
|
||||||
|
}}胜</span
|
||||||
|
>
|
||||||
|
<span class="card-winrate">{{ player.winRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-score">{{ player.powerScore }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 独立的淡入淡出遮罩层 -->
|
||||||
|
<div class="bottom-fade-overlay"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部提示 -->
|
||||||
|
<div class="footer-bar">
|
||||||
|
<span>REAL-TIME DATA SYNC ACTIVE</span>
|
||||||
|
<div class="dot"></div>
|
||||||
|
<span>REFRESH EVERY 30 SECONDS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { getPublicStores, getPublicRanking } from "@/api/display";
|
||||||
|
import { ArrowDown, Refresh, Monitor } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const currentTime = ref(dayjs().format("HH:mm:ss"));
|
||||||
|
const stores = ref([]);
|
||||||
|
const currentStore = ref(null);
|
||||||
|
const rankingList = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const showStoreMenu = ref(false);
|
||||||
|
const scrollContainer = ref(null);
|
||||||
|
const isRefreshing = ref(false);
|
||||||
|
let timer = null;
|
||||||
|
let refreshTimer = null;
|
||||||
|
const group1 = ref(null);
|
||||||
|
const scrollReqId = ref(null);
|
||||||
|
const scrollY = ref(0);
|
||||||
|
|
||||||
|
const updateTime = () => {
|
||||||
|
currentTime.value = dayjs().format("HH:mm:ss");
|
||||||
|
};
|
||||||
|
|
||||||
|
const startContinuousScroll = () => {
|
||||||
|
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||||
|
|
||||||
|
const scroll = () => {
|
||||||
|
if (
|
||||||
|
isRefreshing.value ||
|
||||||
|
rankingList.value.length <= 3 ||
|
||||||
|
!group1.value
|
||||||
|
) {
|
||||||
|
scrollReqId.value = requestAnimationFrame(scroll);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollY.value -= 0.6;
|
||||||
|
|
||||||
|
// 直接获取第一组数据的高度,最准确
|
||||||
|
const singleSetHeight = group1.value.offsetHeight + 12; // 高度 + gap
|
||||||
|
|
||||||
|
if (Math.abs(scrollY.value) >= singleSetHeight) {
|
||||||
|
scrollY.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollReqId.value = requestAnimationFrame(scroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollReqId.value = requestAnimationFrame(scroll);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchStores = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getPublicStores();
|
||||||
|
stores.value = res.data?.list || [];
|
||||||
|
const urlStoreId = route.query.store_id;
|
||||||
|
if (urlStoreId) {
|
||||||
|
const found = stores.value.find((s) => s.id === parseInt(urlStoreId));
|
||||||
|
if (found) currentStore.value = found;
|
||||||
|
}
|
||||||
|
if (!currentStore.value && stores.value.length > 0) {
|
||||||
|
currentStore.value = stores.value[0];
|
||||||
|
}
|
||||||
|
if (currentStore.value) fetchRanking();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fetch stores error:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRanking = async () => {
|
||||||
|
if (!currentStore.value) return;
|
||||||
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getPublicRanking({
|
||||||
|
store_id: currentStore.value.id,
|
||||||
|
pageSize: 100,
|
||||||
|
is_display: 1,
|
||||||
|
});
|
||||||
|
rankingList.value = res.data?.list || [];
|
||||||
|
if (rankingList.value.length > 3) {
|
||||||
|
nextTick(() => startContinuousScroll());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fetch ranking error:", err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectStore = (store) => {
|
||||||
|
currentStore.value = store;
|
||||||
|
showStoreMenu.value = false;
|
||||||
|
router.replace({ query: { ...route.query, store_id: store.id } });
|
||||||
|
fetchRanking();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStores();
|
||||||
|
timer = setInterval(updateTime, 1000);
|
||||||
|
refreshTimer = setInterval(fetchRanking, 30 * 1000);
|
||||||
|
window.addEventListener("click", () => {
|
||||||
|
showStoreMenu.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
if (refreshTimer) clearInterval(refreshTimer);
|
||||||
|
if (scrollReqId.value) cancelAnimationFrame(scrollReqId.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ranking-board-orange-v2 {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0806;
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Oswald", "PingFang SC", sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.bg-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: -20%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 1000px;
|
||||||
|
height: 600px;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(255, 111, 0, 0.15) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-grid {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255, 152, 0, 0.02) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255, 152, 0, 0.02) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Bar */
|
||||||
|
.top-bar {
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid rgba(255, 152, 0, 0.2);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.time-box {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: 200;
|
||||||
|
font-family: monospace;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
.main-title {
|
||||||
|
font-size: 42px;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
background: linear-gradient(to bottom, #fff, #ffcc80);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.title-tag {
|
||||||
|
background: #ff9800;
|
||||||
|
color: #000;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
width: 400px; /* 增加宽度以容纳两个控件 */
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.style-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(255, 152, 0, 0.1);
|
||||||
|
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ffcc80;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 152, 0, 0.2);
|
||||||
|
border-color: #ff9800;
|
||||||
|
color: #ff9800;
|
||||||
|
box-shadow: 0 0 15px rgba(255, 152, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-box {
|
||||||
|
text-align: right;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
.label {
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
color: #ff9800;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: #1a140f;
|
||||||
|
border: 1px solid #ff9800;
|
||||||
|
padding: 10px 0;
|
||||||
|
min-width: 180px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8);
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: left;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 152, 0, 0.1);
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
padding: 30px 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hall of Fame */
|
||||||
|
.hall-of-fame {
|
||||||
|
width: 550px; /* 增加宽度以容纳横向布局 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #ff9800;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
font-weight: 900;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.podium-card {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 152, 0, 0.1) 0%,
|
||||||
|
rgba(255, 152, 0, 0.02) 100%
|
||||||
|
);
|
||||||
|
border: 1px solid rgba(255, 152, 0, 0.2);
|
||||||
|
padding: 0 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.rank-1 {
|
||||||
|
height: 180px;
|
||||||
|
border-color: #ffd700;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 215, 0, 0.2) 0%,
|
||||||
|
rgba(255, 152, 0, 0.05) 100%
|
||||||
|
);
|
||||||
|
.name {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
.score {
|
||||||
|
font-size: 54px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-2,
|
||||||
|
&.rank-3 {
|
||||||
|
height: 130px;
|
||||||
|
.name {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.score {
|
||||||
|
font-size: 42px;
|
||||||
|
color: #ffcc80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: rgba(255, 152, 0, 0.6);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
.avatar {
|
||||||
|
border: 3px solid #ff9800;
|
||||||
|
background: #33190a;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.crown {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px; /* 移动到右上角 */
|
||||||
|
font-size: 32px; /* 稍微放大 */
|
||||||
|
z-index: 5;
|
||||||
|
filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: 900;
|
||||||
|
min-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.score {
|
||||||
|
font-weight: 900;
|
||||||
|
font-family: "Arial Black";
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end; /* 内容靠右对齐 */
|
||||||
|
gap: 5px;
|
||||||
|
margin-left: auto; /* 容器靠右对齐 */
|
||||||
|
|
||||||
|
.level {
|
||||||
|
background: rgba(255, 152, 0, 0.2);
|
||||||
|
color: #ff9800;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.wins {
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 4px; /* 右侧微调对齐 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contenders Zone */
|
||||||
|
.contenders-zone {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
.panel-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #ff9800;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
font-weight: 900;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.refresh-tag {
|
||||||
|
color: #ff9800;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.scroll-mask {
|
||||||
|
position: absolute;
|
||||||
|
/* 四周扩展 20px 给 hover 缩放留出空间 */
|
||||||
|
top: -20px;
|
||||||
|
left: -20px;
|
||||||
|
right: -20px;
|
||||||
|
bottom: -20px;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-fade-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20px;
|
||||||
|
left: -20px;
|
||||||
|
right: -20px;
|
||||||
|
height: 120px;
|
||||||
|
/* 使用 rgba 确保颜色一致性,并调整渐变比例 */
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
#0a0806 0%,
|
||||||
|
#0a0806 15%,
|
||||||
|
rgba(10, 8, 6, 0) 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding-bottom: 20px; /* 移除超大 padding,由 reset 逻辑控制 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.contender-card {
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 152, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
gap: 15px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 152, 0, 0.15);
|
||||||
|
border-color: #ff9800;
|
||||||
|
transform: scale(1.03);
|
||||||
|
z-index: 10; /* 确保悬停卡片在最上层 */
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
.card-rank {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-rank {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: rgba(255, 152, 0, 0.3);
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
.card-avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
.el-avatar {
|
||||||
|
border: 2px solid #ff9800;
|
||||||
|
background: #33190a;
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.card-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.card-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap; /* 禁止换行 */
|
||||||
|
|
||||||
|
.card-level {
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-wins {
|
||||||
|
color: #aaa;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-winrate {
|
||||||
|
color: #666;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-score {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #ff9800;
|
||||||
|
font-family: "Arial Black";
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer-bar {
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #444;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
font-weight: 900;
|
||||||
|
.dot {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: #ff9800;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-enter-active,
|
||||||
|
.slide-down-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.slide-down-enter-from,
|
||||||
|
.slide-down-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.03;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.08;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<div class="logo-icon">🏸</div>
|
<div class="logo-icon">🏸</div>
|
||||||
<h1>羽动俱乐部</h1>
|
<h1>英飒俱乐部</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="subtitle">会员管理系统</p>
|
<p class="subtitle">会员管理系统</p>
|
||||||
</div>
|
</div>
|
||||||
@ -20,15 +20,13 @@
|
|||||||
:model="form"
|
:model="form"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
class="login-form"
|
class="login-form"
|
||||||
@submit.prevent="handleLogin"
|
@submit.prevent="handleLogin">
|
||||||
>
|
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
prefix-icon="User"
|
prefix-icon="User"
|
||||||
size="large"
|
size="large" />
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
@ -39,8 +37,7 @@
|
|||||||
prefix-icon="Lock"
|
prefix-icon="Lock"
|
||||||
size="large"
|
size="large"
|
||||||
show-password
|
show-password
|
||||||
@keyup.enter="handleLogin"
|
@keyup.enter="handleLogin" />
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -49,9 +46,8 @@
|
|||||||
size="large"
|
size="large"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
class="login-btn"
|
class="login-btn"
|
||||||
@click="handleLogin"
|
@click="handleLogin">
|
||||||
>
|
{{ loading ? "登录中..." : "登 录" }}
|
||||||
{{ loading ? '登录中...' : '登 录' }}
|
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -64,52 +60,52 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from "vue";
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from "element-plus";
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref();
|
||||||
const loading = ref(false)
|
const loading = ref(false);
|
||||||
const form = ref({
|
const form = ref({
|
||||||
username: '',
|
username: "",
|
||||||
password: ''
|
password: "",
|
||||||
})
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
await formRef.value?.validate()
|
await formRef.value?.validate();
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await userStore.login(form.value.username, form.value.password)
|
await userStore.login(form.value.username, form.value.password);
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success("登录成功");
|
||||||
router.push('/dashboard')
|
router.push("/dashboard");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.login-page {
|
.login-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: linear-gradient(135deg, #1A1A2E 0%, #16213E 100%);
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-bg {
|
.login-bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
||||||
@ -143,9 +139,9 @@ const handleLogin = async () => {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 30%;
|
left: 30%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
width: 420px;
|
width: 420px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@ -171,7 +167,11 @@ const handleLogin = async () => {
|
|||||||
h1 {
|
h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--primary-color),
|
||||||
|
var(--secondary-color)
|
||||||
|
);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@ -187,7 +187,7 @@ const handleLogin = async () => {
|
|||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
.el-input {
|
.el-input {
|
||||||
--el-input-bg-color: #F8F9FA;
|
--el-input-bg-color: #f8f9fa;
|
||||||
--el-input-border-color: transparent;
|
--el-input-border-color: transparent;
|
||||||
|
|
||||||
:deep(.el-input__wrapper) {
|
:deep(.el-input__wrapper) {
|
||||||
@ -202,11 +202,19 @@ const handleLogin = async () => {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--primary-color),
|
||||||
|
var(--primary-light)
|
||||||
|
);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: linear-gradient(135deg, var(--primary-light), var(--primary-color));
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--primary-light),
|
||||||
|
var(--primary-color)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,5 +225,5 @@ const handleLogin = async () => {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
"window": {
|
"window": {
|
||||||
"backgroundTextStyle": "dark",
|
"backgroundTextStyle": "dark",
|
||||||
"navigationBarBackgroundColor": "#FF6B35",
|
"navigationBarBackgroundColor": "#FF6B35",
|
||||||
"navigationBarTitleText": "羽动俱乐部",
|
"navigationBarTitleText": "英飒俱乐部",
|
||||||
"navigationBarTextStyle": "white"
|
"navigationBarTextStyle": "white"
|
||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
|
|||||||
@ -1,40 +1,49 @@
|
|||||||
/* ==========================================
|
/* ==========================================
|
||||||
影沙俱乐部 - 全局样式
|
英飒俱乐部 - 全局样式
|
||||||
设计理念:浅色高级感 · 橙色点缀 · 流畅动画
|
设计理念:浅色高级感 · 橙色点缀 · 流畅动画
|
||||||
========================================== */
|
========================================== */
|
||||||
|
|
||||||
page {
|
page {
|
||||||
/* 主色调:活力橙系 */
|
/* 主色调:活力橙系 */
|
||||||
--primary: #FF6B35;
|
--primary: #ff6b35;
|
||||||
--primary-dark: #E85A28;
|
--primary-dark: #e85a28;
|
||||||
--primary-light: #FF8C5A;
|
--primary-light: #ff8c5a;
|
||||||
--primary-soft: #FFF0EB;
|
--primary-soft: #fff0eb;
|
||||||
--primary-gradient: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 50%, #FFBA08 100%);
|
--primary-gradient: linear-gradient(
|
||||||
--primary-gradient-soft: linear-gradient(135deg, rgba(255,107,53,0.1) 0%, rgba(255,186,8,0.05) 100%);
|
135deg,
|
||||||
|
#ff6b35 0%,
|
||||||
|
#ff8c5a 50%,
|
||||||
|
#ffba08 100%
|
||||||
|
);
|
||||||
|
--primary-gradient-soft: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 107, 53, 0.1) 0%,
|
||||||
|
rgba(255, 186, 8, 0.05) 100%
|
||||||
|
);
|
||||||
|
|
||||||
/* 强调色 */
|
/* 强调色 */
|
||||||
--accent: #00C9A7;
|
--accent: #00c9a7;
|
||||||
--accent-light: #E6FBF7;
|
--accent-light: #e6fbf7;
|
||||||
--accent-soft: rgba(0, 201, 167, 0.1);
|
--accent-soft: rgba(0, 201, 167, 0.1);
|
||||||
|
|
||||||
/* 浅色背景系 */
|
/* 浅色背景系 */
|
||||||
--bg-page: #F7F8FA;
|
--bg-page: #f7f8fa;
|
||||||
--bg-white: #FFFFFF;
|
--bg-white: #ffffff;
|
||||||
--bg-card: #FFFFFF;
|
--bg-card: #ffffff;
|
||||||
--bg-card-hover: #FAFBFC;
|
--bg-card-hover: #fafbfc;
|
||||||
--bg-soft: #F2F3F5;
|
--bg-soft: #f2f3f5;
|
||||||
--bg-warm: #FFFBF8;
|
--bg-warm: #fffbf8;
|
||||||
|
|
||||||
/* 文字颜色 */
|
/* 文字颜色 */
|
||||||
--text-primary: #1A1A1A;
|
--text-primary: #1a1a1a;
|
||||||
--text-secondary: #5C5C5C;
|
--text-secondary: #5c5c5c;
|
||||||
--text-muted: #8C8C8C;
|
--text-muted: #8c8c8c;
|
||||||
--text-hint: #BFBFBF;
|
--text-hint: #bfbfbf;
|
||||||
--text-white: #FFFFFF;
|
--text-white: #ffffff;
|
||||||
|
|
||||||
/* 边框 */
|
/* 边框 */
|
||||||
--border-light: #EBEDF0;
|
--border-light: #ebedf0;
|
||||||
--border-soft: #F0F1F2;
|
--border-soft: #f0f1f2;
|
||||||
--border-primary: rgba(255, 107, 53, 0.2);
|
--border-primary: rgba(255, 107, 53, 0.2);
|
||||||
|
|
||||||
/* 阴影 */
|
/* 阴影 */
|
||||||
@ -51,7 +60,9 @@ page {
|
|||||||
--radius-xl: 32rpx;
|
--radius-xl: 32rpx;
|
||||||
--radius-full: 100rpx;
|
--radius-full: 100rpx;
|
||||||
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', sans-serif;
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC",
|
||||||
|
"Helvetica Neue", sans-serif;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
@ -68,11 +79,26 @@ page {
|
|||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex { display: flex; }
|
.flex {
|
||||||
.flex-center { display: flex; align-items: center; justify-content: center; }
|
display: flex;
|
||||||
.flex-between { display: flex; align-items: center; justify-content: space-between; }
|
}
|
||||||
.flex-column { display: flex; flex-direction: column; }
|
.flex-center {
|
||||||
.flex-1 { flex: 1; }
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
卡片组件 - 高级感设计
|
卡片组件 - 高级感设计
|
||||||
@ -96,11 +122,13 @@ page {
|
|||||||
.card-highlight {
|
.card-highlight {
|
||||||
background: var(--bg-white);
|
background: var(--bg-white);
|
||||||
border: 1rpx solid var(--border-primary);
|
border: 1rpx solid var(--border-primary);
|
||||||
box-shadow: var(--shadow-card), 0 0 0 1rpx rgba(255, 107, 53, 0.05);
|
box-shadow:
|
||||||
|
var(--shadow-card),
|
||||||
|
0 0 0 1rpx rgba(255, 107, 53, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-highlight::before {
|
.card-highlight::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -133,19 +161,29 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary::after {
|
.btn-primary::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.3),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
animation: btn-shimmer 2.5s ease-in-out infinite;
|
animation: btn-shimmer 2.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes btn-shimmer {
|
@keyframes btn-shimmer {
|
||||||
0% { left: -100%; }
|
0% {
|
||||||
50%, 100% { left: 100%; }
|
left: -100%;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@ -188,28 +226,28 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.level-tag.lv1 {
|
.level-tag.lv1 {
|
||||||
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
|
background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
|
||||||
color: #2E7D32;
|
color: #2e7d32;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-tag.lv2 {
|
.level-tag.lv2 {
|
||||||
background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
|
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||||||
color: #1565C0;
|
color: #1565c0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-tag.lv3 {
|
.level-tag.lv3 {
|
||||||
background: linear-gradient(135deg, #FFF3E0, #FFE0B2);
|
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
|
||||||
color: #E65100;
|
color: #e65100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-tag.lv4 {
|
.level-tag.lv4 {
|
||||||
background: linear-gradient(135deg, #FCE4EC, #F8BBD9);
|
background: linear-gradient(135deg, #fce4ec, #f8bbd9);
|
||||||
color: #C2185B;
|
color: #c2185b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-tag.lv5 {
|
.level-tag.lv5 {
|
||||||
background: linear-gradient(135deg, #F3E5F5, #E1BEE7);
|
background: linear-gradient(135deg, #f3e5f5, #e1bee7);
|
||||||
color: #7B1FA2;
|
color: #7b1fa2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
@ -228,20 +266,20 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rank-badge.top1 {
|
.rank-badge.top1 {
|
||||||
background: linear-gradient(135deg, #FFD700 0%, #FFAA00 100%);
|
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
|
||||||
color: #8B4513;
|
color: #8b4513;
|
||||||
box-shadow: 0 4rpx 16rpx rgba(255, 170, 0, 0.4);
|
box-shadow: 0 4rpx 16rpx rgba(255, 170, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-badge.top2 {
|
.rank-badge.top2 {
|
||||||
background: linear-gradient(135deg, #E8E8E8 0%, #C0C0C0 100%);
|
background: linear-gradient(135deg, #e8e8e8 0%, #c0c0c0 100%);
|
||||||
color: #4A4A4A;
|
color: #4a4a4a;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(192, 192, 192, 0.4);
|
box-shadow: 0 4rpx 12rpx rgba(192, 192, 192, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-badge.top3 {
|
.rank-badge.top3 {
|
||||||
background: linear-gradient(135deg, #DEB887 0%, #CD853F 100%);
|
background: linear-gradient(135deg, #deb887 0%, #cd853f 100%);
|
||||||
color: #5C4033;
|
color: #5c4033;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(205, 133, 63, 0.4);
|
box-shadow: 0 4rpx 12rpx rgba(205, 133, 63, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,13 +393,13 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-warning {
|
.tag-warning {
|
||||||
background: #FFF8E6;
|
background: #fff8e6;
|
||||||
color: #FA8C16;
|
color: #fa8c16;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-danger {
|
.tag-danger {
|
||||||
background: #FFF1F0;
|
background: #fff1f0;
|
||||||
color: #FF4D4F;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
@ -377,7 +415,7 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.power-change.negative {
|
.power-change.negative {
|
||||||
color: #FF4D4F;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
@ -474,7 +512,8 @@ page {
|
|||||||
|
|
||||||
/* 脉冲效果 */
|
/* 脉冲效果 */
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@ -486,7 +525,8 @@ page {
|
|||||||
|
|
||||||
/* 呼吸发光 */
|
/* 呼吸发光 */
|
||||||
@keyframes breathe {
|
@keyframes breathe {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.25);
|
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.25);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@ -496,28 +536,50 @@ page {
|
|||||||
|
|
||||||
/* 渐变流动 */
|
/* 渐变流动 */
|
||||||
@keyframes gradientFlow {
|
@keyframes gradientFlow {
|
||||||
0% { background-position: 0% 50%; }
|
0% {
|
||||||
50% { background-position: 100% 50%; }
|
background-position: 0% 50%;
|
||||||
100% { background-position: 0% 50%; }
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 旋转 */
|
/* 旋转 */
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg); }
|
from {
|
||||||
to { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 摇摆 */
|
/* 摇摆 */
|
||||||
@keyframes swing {
|
@keyframes swing {
|
||||||
0%, 100% { transform: rotate(0deg); }
|
0%,
|
||||||
25% { transform: rotate(3deg); }
|
100% {
|
||||||
75% { transform: rotate(-3deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(3deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(-3deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 闪烁 */
|
/* 闪烁 */
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.6; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 波纹扩散 */
|
/* 波纹扩散 */
|
||||||
@ -566,11 +628,21 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 延迟类 */
|
/* 延迟类 */
|
||||||
.delay-1 { animation-delay: 0.1s; }
|
.delay-1 {
|
||||||
.delay-2 { animation-delay: 0.2s; }
|
animation-delay: 0.1s;
|
||||||
.delay-3 { animation-delay: 0.3s; }
|
}
|
||||||
.delay-4 { animation-delay: 0.4s; }
|
.delay-2 {
|
||||||
.delay-5 { animation-delay: 0.5s; }
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
.delay-3 {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
.delay-4 {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.delay-5 {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
页面过渡效果
|
页面过渡效果
|
||||||
@ -580,16 +652,36 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 列表项依次入场 */
|
/* 列表项依次入场 */
|
||||||
.stagger-item:nth-child(1) { animation-delay: 0s; }
|
.stagger-item:nth-child(1) {
|
||||||
.stagger-item:nth-child(2) { animation-delay: 0.05s; }
|
animation-delay: 0s;
|
||||||
.stagger-item:nth-child(3) { animation-delay: 0.1s; }
|
}
|
||||||
.stagger-item:nth-child(4) { animation-delay: 0.15s; }
|
.stagger-item:nth-child(2) {
|
||||||
.stagger-item:nth-child(5) { animation-delay: 0.2s; }
|
animation-delay: 0.05s;
|
||||||
.stagger-item:nth-child(6) { animation-delay: 0.25s; }
|
}
|
||||||
.stagger-item:nth-child(7) { animation-delay: 0.3s; }
|
.stagger-item:nth-child(3) {
|
||||||
.stagger-item:nth-child(8) { animation-delay: 0.35s; }
|
animation-delay: 0.1s;
|
||||||
.stagger-item:nth-child(9) { animation-delay: 0.4s; }
|
}
|
||||||
.stagger-item:nth-child(10) { animation-delay: 0.45s; }
|
.stagger-item:nth-child(4) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
.stagger-item:nth-child(5) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
.stagger-item:nth-child(6) {
|
||||||
|
animation-delay: 0.25s;
|
||||||
|
}
|
||||||
|
.stagger-item:nth-child(7) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
.stagger-item:nth-child(8) {
|
||||||
|
animation-delay: 0.35s;
|
||||||
|
}
|
||||||
|
.stagger-item:nth-child(9) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.stagger-item:nth-child(10) {
|
||||||
|
animation-delay: 0.45s;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
装饰元素
|
装饰元素
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
================================================================================
|
================================================================================
|
||||||
影沙俱乐部小程序配置说明
|
英飒俱乐部小程序配置说明
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
【配置文件位置】
|
【配置文件位置】
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
<!--个人中心页面 - 运动感设计-->
|
<!-- 个人中心页面 - 运动感设计 -->
|
||||||
<view class="page-container">
|
<view class="page-container">
|
||||||
<!-- 顶部装饰背景 -->
|
<!-- 顶部装饰背景 -->
|
||||||
<view class="hero-bg">
|
<view class="hero-bg">
|
||||||
<view class="hero-pattern"></view>
|
<view class="hero-pattern"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 用户信息区域 -->
|
<!-- 用户信息区域 -->
|
||||||
<view class="user-section">
|
<view class="user-section">
|
||||||
<!-- 已登录状态 -->
|
<!-- 已登录状态 -->
|
||||||
@ -17,7 +16,6 @@
|
|||||||
<text class="edit-icon">✏️</text>
|
<text class="edit-icon">✏️</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="user-info">
|
<view class="user-info">
|
||||||
<text class="nickname">{{userInfo.nickname || '新用户'}}</text>
|
<text class="nickname">{{userInfo.nickname || '新用户'}}</text>
|
||||||
<view class="user-stats">
|
<view class="user-stats">
|
||||||
@ -38,7 +36,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 会员码卡片 -->
|
<!-- 会员码卡片 -->
|
||||||
<view class="member-card" bindtap="showMemberCode">
|
<view class="member-card" bindtap="showMemberCode">
|
||||||
<view class="member-card-bg"></view>
|
<view class="member-card-bg"></view>
|
||||||
@ -58,7 +55,6 @@
|
|||||||
<view class="scan-hint">出示给对方扫码挑战</view>
|
<view class="scan-hint">出示给对方扫码挑战</view>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
<!-- 未登录状态 -->
|
<!-- 未登录状态 -->
|
||||||
<block wx:else>
|
<block wx:else>
|
||||||
<view class="login-card animate-fadeInUp">
|
<view class="login-card animate-fadeInUp">
|
||||||
@ -66,31 +62,23 @@
|
|||||||
<image class="login-avatar" src="/images/avatar-default.svg" mode="aspectFill"></image>
|
<image class="login-avatar" src="/images/avatar-default.svg" mode="aspectFill"></image>
|
||||||
<view class="login-avatar-pulse"></view>
|
<view class="login-avatar-pulse"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="login-content">
|
<view class="login-content">
|
||||||
<text class="login-title">开启你的运动之旅</text>
|
<text class="login-title">开启你的运动之旅</text>
|
||||||
<text class="login-subtitle">授权手机号,加入影沙俱乐部</text>
|
<text class="login-subtitle">授权手机号,加入英飒俱乐部</text>
|
||||||
</view>
|
</view>
|
||||||
|
<button class="login-btn-primary" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
|
||||||
<button
|
|
||||||
class="login-btn-primary"
|
|
||||||
open-type="getPhoneNumber"
|
|
||||||
bindgetphonenumber="onGetPhoneNumber"
|
|
||||||
>
|
|
||||||
<text class="btn-icon">📱</text>
|
<text class="btn-icon">📱</text>
|
||||||
<text class="btn-text">手机号快捷登录</text>
|
<text class="btn-text">手机号快捷登录</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 天梯信息卡片 -->
|
<!-- 天梯信息卡片 -->
|
||||||
<view class="ladder-section animate-fadeInUp" style="animation-delay: 0.2s" wx:if="{{ladderUser}}">
|
<view class="ladder-section animate-fadeInUp" style="animation-delay: 0.2s" wx:if="{{ladderUser}}">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">天梯战绩</text>
|
<text class="section-title">天梯战绩</text>
|
||||||
<text class="section-badge">{{currentStore.storeName}}</text>
|
<text class="section-badge">{{currentStore.storeName}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="ladder-stats">
|
<view class="ladder-stats">
|
||||||
<view class="ladder-stat-item">
|
<view class="ladder-stat-item">
|
||||||
<view class="stat-icon lv{{ladderUser.level}}">
|
<view class="stat-icon lv{{ladderUser.level}}">
|
||||||
@ -98,7 +86,6 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="stat-name">{{ladderUser.realName}}</text>
|
<text class="stat-name">{{ladderUser.realName}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="ladder-progress">
|
<view class="ladder-progress">
|
||||||
<view class="progress-info">
|
<view class="progress-info">
|
||||||
<text class="progress-label">战力值</text>
|
<text class="progress-label">战力值</text>
|
||||||
@ -108,24 +95,26 @@
|
|||||||
<view class="progress-fill" style="width: {{ladderUser.powerScore / 30}}%;"></view>
|
<view class="progress-fill" style="width: {{ladderUser.powerScore / 30}}%;"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="ladder-record">
|
<view class="ladder-record">
|
||||||
<view class="record-item">
|
<view class="record-item">
|
||||||
<text class="record-value win">{{ladderUser.winCount || 0}}</text>
|
<text class="record-value win">{{ladderUser.winCount || 0}}</text>
|
||||||
<text class="record-label">胜场</text>
|
<text class="record-label">胜场</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-item">
|
<view class="record-item">
|
||||||
<text class="record-value">{{(ladderUser.matchCount || 0) - (ladderUser.winCount || 0)}}</text>
|
<text class="record-value">
|
||||||
|
{{(ladderUser.matchCount || 0) - (ladderUser.winCount || 0)}}
|
||||||
|
</text>
|
||||||
<text class="record-label">负场</text>
|
<text class="record-label">负场</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-item">
|
<view class="record-item">
|
||||||
<text class="record-value rate">{{(ladderUser.matchCount > 0 && ladderUser.winCount !== null && ladderUser.winCount !== undefined) ? Math.round((Number(ladderUser.winCount) || 0) / Number(ladderUser.matchCount) * 100) : 0}}%</text>
|
<text class="record-value rate">
|
||||||
|
{{(ladderUser.matchCount > 0 && ladderUser.winCount !== null && ladderUser.winCount !== undefined) ? Math.round((Number(ladderUser.winCount) || 0) / Number(ladderUser.matchCount) * 100) : 0}}%
|
||||||
|
</text>
|
||||||
<text class="record-label">胜率</text>
|
<text class="record-label">胜率</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 未加入天梯提示 -->
|
<!-- 未加入天梯提示 -->
|
||||||
<view class="notice-card animate-fadeInUp" style="animation-delay: 0.2s" wx:elif="{{userInfo && userInfo.phone}}">
|
<view class="notice-card animate-fadeInUp" style="animation-delay: 0.2s" wx:elif="{{userInfo && userInfo.phone}}">
|
||||||
<view class="notice-icon">🏸</view>
|
<view class="notice-icon">🏸</view>
|
||||||
@ -134,7 +123,6 @@
|
|||||||
<text class="notice-desc">请联系门店工作人员,开启你的天梯之旅</text>
|
<text class="notice-desc">请联系门店工作人员,开启你的天梯之旅</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 功能菜单 -->
|
<!-- 功能菜单 -->
|
||||||
<view class="menu-section animate-fadeInUp" style="animation-delay: 0.3s">
|
<view class="menu-section animate-fadeInUp" style="animation-delay: 0.3s">
|
||||||
<view class="menu-grid">
|
<view class="menu-grid">
|
||||||
@ -144,21 +132,18 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="menu-text">比赛记录</text>
|
<text class="menu-text">比赛记录</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="menu-item" bindtap="goTo" data-url="/pages/points/records/index">
|
<view class="menu-item" bindtap="goTo" data-url="/pages/points/records/index">
|
||||||
<view class="menu-icon points">
|
<view class="menu-icon points">
|
||||||
<image src="/images/icon-points.svg" mode="aspectFit"></image>
|
<image src="/images/icon-points.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-text">积分记录</text>
|
<text class="menu-text">积分记录</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="menu-item" bindtap="goTo" data-url="/pages/points/order/index">
|
<view class="menu-item" bindtap="goTo" data-url="/pages/points/order/index">
|
||||||
<view class="menu-icon order">
|
<view class="menu-icon order">
|
||||||
<image src="/images/icon-order.svg" mode="aspectFit"></image>
|
<image src="/images/icon-order.svg" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-text">兑换订单</text>
|
<text class="menu-text">兑换订单</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="menu-item" bindtap="goTo" data-url="/pages/store/index">
|
<view class="menu-item" bindtap="goTo" data-url="/pages/store/index">
|
||||||
<view class="menu-icon store">
|
<view class="menu-icon store">
|
||||||
<image src="/images/icon-store.svg" mode="aspectFit"></image>
|
<image src="/images/icon-store.svg" mode="aspectFit"></image>
|
||||||
@ -168,7 +153,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 会员码弹窗 -->
|
<!-- 会员码弹窗 -->
|
||||||
<view class="qrcode-overlay {{showQrcode ? 'show' : ''}}" bindtap="hideQrcode">
|
<view class="qrcode-overlay {{showQrcode ? 'show' : ''}}" bindtap="hideQrcode">
|
||||||
<view class="qrcode-modal {{showQrcode ? 'show' : ''}}" catchtap="preventBubble">
|
<view class="qrcode-modal {{showQrcode ? 'show' : ''}}" catchtap="preventBubble">
|
||||||
@ -176,7 +160,6 @@
|
|||||||
<text class="modal-title">我的会员码</text>
|
<text class="modal-title">我的会员码</text>
|
||||||
<view class="modal-close" bindtap="hideQrcode">×</view>
|
<view class="modal-close" bindtap="hideQrcode">×</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="qrcode-wrapper">
|
<view class="qrcode-wrapper">
|
||||||
<view class="qrcode-border">
|
<view class="qrcode-border">
|
||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
@ -185,13 +168,7 @@
|
|||||||
<text class="loading-text">生成中...</text>
|
<text class="loading-text">生成中...</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 二维码图片 -->
|
<!-- 二维码图片 -->
|
||||||
<image
|
<image wx:else class="qrcode-image" src="{{qrcodeImage}}" mode="aspectFit" show-menu-by-longpress="{{true}}"></image>
|
||||||
wx:else
|
|
||||||
class="qrcode-image"
|
|
||||||
src="{{qrcodeImage}}"
|
|
||||||
mode="aspectFit"
|
|
||||||
show-menu-by-longpress="{{true}}"
|
|
||||||
></image>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="qrcode-corners">
|
<view class="qrcode-corners">
|
||||||
<view class="corner tl"></view>
|
<view class="corner tl"></view>
|
||||||
@ -200,12 +177,10 @@
|
|||||||
<view class="corner br"></view>
|
<view class="corner br"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="qrcode-info">
|
<view class="qrcode-info">
|
||||||
<text class="code-label">会员码</text>
|
<text class="code-label">会员码</text>
|
||||||
<text class="code-value">{{userInfo.memberCode}}</text>
|
<text class="code-value">{{userInfo.memberCode}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="qrcode-tips">
|
<view class="qrcode-tips">
|
||||||
<view class="tip-item">
|
<view class="tip-item">
|
||||||
<text class="tip-icon">📱</text>
|
<text class="tip-icon">📱</text>
|
||||||
@ -214,7 +189,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 完善资料弹窗 -->
|
<!-- 完善资料弹窗 -->
|
||||||
<view class="profile-overlay {{showProfileModal ? 'show' : ''}}" bindtap="closeProfileModal">
|
<view class="profile-overlay {{showProfileModal ? 'show' : ''}}" bindtap="closeProfileModal">
|
||||||
<view class="profile-modal {{showProfileModal ? 'show' : ''}}" catchtap="preventBubble">
|
<view class="profile-modal {{showProfileModal ? 'show' : ''}}" catchtap="preventBubble">
|
||||||
@ -222,46 +196,33 @@
|
|||||||
<text class="profile-modal-title">{{isEditMode ? '修改资料' : '完善个人资料'}}</text>
|
<text class="profile-modal-title">{{isEditMode ? '修改资料' : '完善个人资料'}}</text>
|
||||||
<view class="profile-modal-close" bindtap="closeProfileModal">×</view>
|
<view class="profile-modal-close" bindtap="closeProfileModal">×</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="profile-modal-body">
|
<view class="profile-modal-body">
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<view class="profile-tips" wx:if="{{!isEditMode}}">
|
<view class="profile-tips" wx:if="{{!isEditMode}}">
|
||||||
<text class="tips-icon">💡</text>
|
<text class="tips-icon">💡</text>
|
||||||
<text class="tips-text">完善资料后,好友可以更容易找到你</text>
|
<text class="tips-text">完善资料后,好友可以更容易找到你</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 头像选择 -->
|
<!-- 头像选择 -->
|
||||||
<view class="profile-avatar-section">
|
<view class="profile-avatar-section">
|
||||||
<text class="profile-label">头像</text>
|
<text class="profile-label">头像</text>
|
||||||
<button class="avatar-choose-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatarNew">
|
<button class="avatar-choose-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatarNew">
|
||||||
<image
|
<image class="profile-avatar-preview" src="{{profileForm.avatar || '/images/avatar-default.svg'}}" mode="aspectFill"></image>
|
||||||
class="profile-avatar-preview"
|
|
||||||
src="{{profileForm.avatar || '/images/avatar-default.svg'}}"
|
|
||||||
mode="aspectFill"
|
|
||||||
></image>
|
|
||||||
<view class="avatar-choose-badge">
|
<view class="avatar-choose-badge">
|
||||||
<text class="choose-icon">📷</text>
|
<text class="choose-icon">📷</text>
|
||||||
</view>
|
</view>
|
||||||
</button>
|
</button>
|
||||||
<text class="avatar-tip">点击更换头像</text>
|
<text class="avatar-tip">点击更换头像</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 昵称输入 -->
|
<!-- 昵称输入 -->
|
||||||
<view class="profile-nickname-section">
|
<view class="profile-nickname-section">
|
||||||
<text class="profile-label">昵称</text>
|
<text class="profile-label">昵称</text>
|
||||||
<input
|
<input class="nickname-input" type="nickname" placeholder="请输入昵称" value="{{profileForm.nickname}}" bindinput="onNicknameInput" maxlength="20" />
|
||||||
class="nickname-input"
|
|
||||||
type="nickname"
|
|
||||||
placeholder="请输入昵称"
|
|
||||||
value="{{profileForm.nickname}}"
|
|
||||||
bindinput="onNicknameInput"
|
|
||||||
maxlength="20"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="profile-modal-footer">
|
<view class="profile-modal-footer">
|
||||||
<button class="profile-btn-cancel" bindtap="closeProfileModal">{{isEditMode ? '取消' : '跳过'}}</button>
|
<button class="profile-btn-cancel" bindtap="closeProfileModal">
|
||||||
|
{{isEditMode ? '取消' : '跳过'}}
|
||||||
|
</button>
|
||||||
<button class="profile-btn-confirm" bindtap="saveProfile">保存</button>
|
<button class="profile-btn-confirm" bindtap="saveProfile">保存</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"description": "羽毛球俱乐部小程序",
|
"description": "英飒俱乐部小程序",
|
||||||
"packOptions": {
|
"packOptions": {
|
||||||
"ignore": [],
|
"ignore": [],
|
||||||
"include": []
|
"include": []
|
||||||
|
|||||||
@ -1,73 +1,74 @@
|
|||||||
// 格式化日期
|
// 格式化日期
|
||||||
const formatDate = (date, format = 'YYYY-MM-DD HH:mm') => {
|
const formatDate = (date, format = "YYYY-MM-DD HH:mm") => {
|
||||||
if (!date) return ''
|
if (!date) return "";
|
||||||
const d = new Date(date)
|
const d = new Date(date);
|
||||||
const year = d.getFullYear()
|
const year = d.getFullYear();
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(d.getDate()).padStart(2, '0')
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
const hour = String(d.getHours()).padStart(2, '0')
|
const hour = String(d.getHours()).padStart(2, "0");
|
||||||
const minute = String(d.getMinutes()).padStart(2, '0')
|
const minute = String(d.getMinutes()).padStart(2, "0");
|
||||||
const second = String(d.getSeconds()).padStart(2, '0')
|
const second = String(d.getSeconds()).padStart(2, "0");
|
||||||
|
|
||||||
return format
|
return format
|
||||||
.replace('YYYY', year)
|
.replace("YYYY", year)
|
||||||
.replace('MM', month)
|
.replace("MM", month)
|
||||||
.replace('DD', day)
|
.replace("DD", day)
|
||||||
.replace('HH', hour)
|
.replace("HH", hour)
|
||||||
.replace('mm', minute)
|
.replace("mm", minute)
|
||||||
.replace('ss', second)
|
.replace("ss", second);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 等级名称
|
// 等级名称
|
||||||
const levelNames = {
|
const levelNames = {
|
||||||
1: '新锐',
|
1: "新锐",
|
||||||
2: '精锐',
|
2: "精锐",
|
||||||
3: '高手',
|
3: "高手",
|
||||||
4: '大师',
|
4: "大师",
|
||||||
5: '宗师'
|
5: "宗师",
|
||||||
}
|
};
|
||||||
|
|
||||||
const getLevelName = (level) => levelNames[level] || '未知'
|
const getLevelName = (level) => levelNames[level] || "未知";
|
||||||
|
|
||||||
// 等级描述
|
// 等级描述
|
||||||
const levelDescs = {
|
const levelDescs = {
|
||||||
1: '掌握基础动作,能进行多拍回合。',
|
1: "掌握基础动作,能进行多拍回合。",
|
||||||
2: '技术较全面,具备初步的战术意识。',
|
2: "技术较全面,具备初步的战术意识。",
|
||||||
3: '技术稳定,战术意图清晰,比赛经验丰富。',
|
3: "技术稳定,战术意图清晰,比赛经验丰富。",
|
||||||
4: '在俱乐部内属顶尖战力,具备较强掌控力。',
|
4: "在英飒俱乐部内属顶尖战力,具备较强掌控力。",
|
||||||
5: '技术全面,战术素养高,是俱乐部标杆。'
|
5: "技术全面,战术素养高,是英飒俱乐部标杆。",
|
||||||
}
|
};
|
||||||
|
|
||||||
const getLevelDesc = (level) => levelDescs[level] || ''
|
const getLevelDesc = (level) => levelDescs[level] || "";
|
||||||
|
|
||||||
// 比赛状态
|
// 比赛状态
|
||||||
const matchStatusTexts = {
|
const matchStatusTexts = {
|
||||||
0: '待开始',
|
0: "待开始",
|
||||||
1: '进行中',
|
1: "进行中",
|
||||||
2: '已结束',
|
2: "已结束",
|
||||||
3: '已取消'
|
3: "已取消",
|
||||||
}
|
};
|
||||||
|
|
||||||
const getMatchStatusText = (status) => matchStatusTexts[status] || '未知'
|
const getMatchStatusText = (status) => matchStatusTexts[status] || "未知";
|
||||||
|
|
||||||
// 订单状态
|
// 订单状态
|
||||||
const orderStatusTexts = {
|
const orderStatusTexts = {
|
||||||
0: '待核销',
|
0: "待核销",
|
||||||
1: '已完成',
|
1: "已完成",
|
||||||
2: '已取消'
|
2: "已取消",
|
||||||
}
|
};
|
||||||
|
|
||||||
const getOrderStatusText = (status) => orderStatusTexts[status] || '未知'
|
const getOrderStatusText = (status) => orderStatusTexts[status] || "未知";
|
||||||
|
|
||||||
// 生成随机字符串
|
// 生成随机字符串
|
||||||
const randomString = (len = 16) => {
|
const randomString = (len = 16) => {
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
const chars =
|
||||||
let result = ''
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let result = "";
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
formatDate,
|
formatDate,
|
||||||
@ -75,5 +76,5 @@ module.exports = {
|
|||||||
getLevelDesc,
|
getLevelDesc,
|
||||||
getMatchStatusText,
|
getMatchStatusText,
|
||||||
getOrderStatusText,
|
getOrderStatusText,
|
||||||
randomString
|
randomString,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 影杀俱乐部管理系统 - 环境配置模板
|
# 英飒俱乐部管理系统 - 环境配置模板
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 使用说明:
|
# 使用说明:
|
||||||
# 1. 复制此文件为 .env
|
# 1. 复制此文件为 .env
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "yingsha-server",
|
"name": "yingsha-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "羽毛球俱乐部管理系统后端服务",
|
"description": "英飒俱乐部管理系统后端服务",
|
||||||
"main": "src/app.js",
|
"main": "src/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/app.js",
|
"start": "node src/app.js",
|
||||||
|
|||||||
@ -4,29 +4,29 @@ const LADDER_LEVELS = {
|
|||||||
LV2_ELITE: 2, // 精锐
|
LV2_ELITE: 2, // 精锐
|
||||||
LV3_EXPERT: 3, // 高手
|
LV3_EXPERT: 3, // 高手
|
||||||
LV4_MASTER: 4, // 大师
|
LV4_MASTER: 4, // 大师
|
||||||
LV5_GRANDMASTER: 5 // 宗师
|
LV5_GRANDMASTER: 5, // 宗师
|
||||||
};
|
};
|
||||||
|
|
||||||
const LADDER_LEVEL_NAMES = {
|
const LADDER_LEVEL_NAMES = {
|
||||||
1: '新锐',
|
1: "新锐",
|
||||||
2: '精锐',
|
2: "精锐",
|
||||||
3: '高手',
|
3: "高手",
|
||||||
4: '大师',
|
4: "大师",
|
||||||
5: '宗师'
|
5: "宗师",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LADDER_LEVEL_DESC = {
|
const LADDER_LEVEL_DESC = {
|
||||||
1: '掌握基础动作,能进行多拍回合。',
|
1: "掌握基础动作,能进行多拍回合。",
|
||||||
2: '技术较全面,具备初步的战术意识。',
|
2: "技术较全面,具备初步的战术意识。",
|
||||||
3: '技术稳定,战术意图清晰,比赛经验丰富。',
|
3: "技术稳定,战术意图清晰,比赛经验丰富。",
|
||||||
4: '在俱乐部内属顶尖战力,具备较强掌控力。',
|
4: "在英飒俱乐部内属顶尖战力,具备较强掌控力。",
|
||||||
5: '技术全面,战术素养高,是俱乐部标杆。'
|
5: "技术全面,战术素养高,是英飒俱乐部标杆。",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 比赛类型
|
// 比赛类型
|
||||||
const MATCH_TYPES = {
|
const MATCH_TYPES = {
|
||||||
CHALLENGE: 1, // 挑战赛
|
CHALLENGE: 1, // 挑战赛
|
||||||
RANKING: 2 // 排位赛
|
RANKING: 2, // 排位赛
|
||||||
};
|
};
|
||||||
|
|
||||||
// 比赛状态
|
// 比赛状态
|
||||||
@ -34,7 +34,7 @@ const MATCH_STATUS = {
|
|||||||
PENDING: 0, // 待开始
|
PENDING: 0, // 待开始
|
||||||
ONGOING: 1, // 进行中
|
ONGOING: 1, // 进行中
|
||||||
FINISHED: 2, // 已结束
|
FINISHED: 2, // 已结束
|
||||||
CANCELLED: 3 // 已取消
|
CANCELLED: 3, // 已取消
|
||||||
};
|
};
|
||||||
|
|
||||||
// 排位赛阶段
|
// 排位赛阶段
|
||||||
@ -42,21 +42,21 @@ const RANKING_STAGE = {
|
|||||||
SIGNUP: 0, // 报名中
|
SIGNUP: 0, // 报名中
|
||||||
ROUND_ROBIN: 1, // 循环赛
|
ROUND_ROBIN: 1, // 循环赛
|
||||||
ELIMINATION: 2, // 淘汰赛
|
ELIMINATION: 2, // 淘汰赛
|
||||||
FINISHED: 3 // 已结束
|
FINISHED: 3, // 已结束
|
||||||
};
|
};
|
||||||
|
|
||||||
// 比赛确认状态
|
// 比赛确认状态
|
||||||
const CONFIRM_STATUS = {
|
const CONFIRM_STATUS = {
|
||||||
PENDING: 0, // 待确认
|
PENDING: 0, // 待确认
|
||||||
CONFIRMED: 1, // 已确认
|
CONFIRMED: 1, // 已确认
|
||||||
DISPUTED: 2 // 有争议
|
DISPUTED: 2, // 有争议
|
||||||
};
|
};
|
||||||
|
|
||||||
// 积分订单状态
|
// 积分订单状态
|
||||||
const ORDER_STATUS = {
|
const ORDER_STATUS = {
|
||||||
PENDING: 0, // 待核销
|
PENDING: 0, // 待核销
|
||||||
COMPLETED: 1, // 已完成
|
COMPLETED: 1, // 已完成
|
||||||
CANCELLED: 2 // 已取消
|
CANCELLED: 2, // 已取消
|
||||||
};
|
};
|
||||||
|
|
||||||
// 战力值计算参数
|
// 战力值计算参数
|
||||||
@ -69,7 +69,7 @@ const POWER_CALC = {
|
|||||||
NEWBIE_PROTECTION: 5, // 新手保护场次
|
NEWBIE_PROTECTION: 5, // 新手保护场次
|
||||||
NEWBIE_LOSE_RATE: 0.5, // 新手输分减半
|
NEWBIE_LOSE_RATE: 0.5, // 新手输分减半
|
||||||
MIN_MONTHLY_MATCHES: 3, // 每月最低参赛场次
|
MIN_MONTHLY_MATCHES: 3, // 每月最低参赛场次
|
||||||
CHALLENGE_COOLDOWN: 30 // 挑战赛同人冷却天数
|
CHALLENGE_COOLDOWN: 30, // 挑战赛同人冷却天数
|
||||||
};
|
};
|
||||||
|
|
||||||
// 比赛权重
|
// 比赛权重
|
||||||
@ -77,12 +77,12 @@ const MATCH_WEIGHTS = {
|
|||||||
DAILY: 1.0, // 日常畅打
|
DAILY: 1.0, // 日常畅打
|
||||||
CHALLENGE: 1.5, // 挑战赛
|
CHALLENGE: 1.5, // 挑战赛
|
||||||
MONTHLY: 1.5, // 月度排位赛
|
MONTHLY: 1.5, // 月度排位赛
|
||||||
SEASONAL: 2.0 // 季度/年度总决赛
|
SEASONAL: 2.0, // 季度/年度总决赛
|
||||||
};
|
};
|
||||||
|
|
||||||
// 积分计算
|
// 积分计算
|
||||||
const POINTS_CALC = {
|
const POINTS_CALC = {
|
||||||
CONSUME_RATE: 0.1 // 每10元1分
|
CONSUME_RATE: 0.1, // 每10元1分
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -96,5 +96,5 @@ module.exports = {
|
|||||||
ORDER_STATUS,
|
ORDER_STATUS,
|
||||||
POWER_CALC,
|
POWER_CALC,
|
||||||
MATCH_WEIGHTS,
|
MATCH_WEIGHTS,
|
||||||
POINTS_CALC
|
POINTS_CALC,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class LadderController {
|
|||||||
// 获取天梯排名
|
// 获取天梯排名
|
||||||
async getRanking(req, res) {
|
async getRanking(req, res) {
|
||||||
try {
|
try {
|
||||||
const { store_id, gender, level, page = 1, pageSize = 50 } = req.query;
|
const { store_id, gender, level, page = 1, pageSize = 50, is_display } = req.query;
|
||||||
const { limit, offset } = getPagination(page, pageSize);
|
const { limit, offset } = getPagination(page, pageSize);
|
||||||
|
|
||||||
if (!store_id) {
|
if (!store_id) {
|
||||||
@ -17,10 +17,14 @@ class LadderController {
|
|||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
store_id,
|
store_id,
|
||||||
status: 1,
|
status: 1
|
||||||
monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 如果不是大屏显示,则需要满足每月最低参赛场次限制
|
||||||
|
if (!is_display) {
|
||||||
|
where.monthly_match_count = { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES };
|
||||||
|
}
|
||||||
|
|
||||||
if (gender) {
|
if (gender) {
|
||||||
where.gender = gender;
|
where.gender = gender;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user