feat(新页面): 排行榜

This commit is contained in:
hzj 2025-09-28 15:27:42 +08:00
parent f37599be70
commit 998beb6a3f
17 changed files with 615 additions and 17 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@ -0,0 +1,164 @@
<template>
<div style="width: 100%">
<div style="position: relative">
<img
:src="BorderImgUrl"
alt=""
width="100%"
style="display: block; position: relative; z-index: 2"
/>
<!-- 头像 -->
<div
style="
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
"
:style="{ height: isTopOne ? '55%' : '60%' }"
>
<div style="width: 75%">
<img
:src="avatarUrl || ''"
alt=""
width="100%"
style="aspect-ratio: 1/1; object-fit: cover"
:style="{ borderRadius: isRoom ? '0' : '50%' }"
@error="defaultAvatarUrl"
/>
</div>
</div>
<!-- 用户名和贡献值 -->
<div
style="
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40%;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
"
>
<div
style="
height: 28%;
width: 60%;
display: flex;
justify-content: center;
align-items: center;
"
>
<div style="font-weight: 860" class="showText" :class="isTopOne ? 'top1Text' : 'text'">
{{ name }}
</div>
</div>
<div style="width: 60%; position: relative">
<img
src="/src/assets/images/Ranking/coinNumBg.png"
alt=""
width="100%"
style="display: block"
/>
<div
style="position: absolute; inset: 0; display: flex; align-items: center; padding: 3% 5%"
>
<img src="/src/assets/icon/coin.png" alt="" width="25%" style="display: block" />
<div
style="
font-weight: 700;
background: linear-gradient(180deg, #ffe656 0%, #fff 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
class="showText"
:class="isTopOne ? 'top1Text' : 'text'"
>
{{ distributionValue }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { watch } from 'vue'
const props = defineProps({
isRoom: {
type: Boolean,
default: false,
},
isTopOne: {
type: Boolean,
default: false,
},
avatarUrl: {
type: String,
default: '/src/assets/images/WeeklyStar/defaultAvatar.png',
},
BorderImgUrl: {
type: String,
required: true,
},
name: {
type: String,
default: '',
},
distributionValue: {
type: String,
default: '',
},
})
watch(
() => props.isRoom,
(newVal, oldVal) => {
console.log('isRoom 变化 =>', '新值:', newVal, '旧值:', oldVal)
}
)
const defaultAvatarUrl = (e) => {
console.log('头像资源出错')
e.target.onerror = null //
e.target.src = new URL('/src/assets/images/WeeklyStar/defaultAvatar.png', import.meta.url).href
}
</script>
<style lang="scss" scoped>
* {
font-family: 'SF Pro';
color: black;
}
.showText {
color: #fff;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.top1Text {
font-size: 1em;
}
.text {
font-size: 0.9em;
}
</style>

View File

@ -209,6 +209,12 @@ const router = createRouter({
component: () => import('../views/WeeklyStar/WeeklyStar.vue'),
meta: { requiresAuth: true },
},
{
path: '/ranking',
name: 'ranking',
component: () => import('../views/Ranking/Ranking.vue'),
meta: { requiresAuth: true },
},
],
})

View File

@ -112,6 +112,7 @@ export const ROLE_PERMISSIONS = {
'/recharge-freight-agent',
'/top-list', //排行榜
'/weekly-star', //每周明星
'/ranking', //总排行榜
],
// 加载页面

View File

@ -19,7 +19,7 @@ import { appConnectionManager, isAppConnected } from './appConnectionManager.js'
* APP连接检查器
*/
class AppConnectionChecker {
constructor () {
constructor() {
this.isConnecting = false
}
@ -27,7 +27,7 @@ class AppConnectionChecker {
* 确保APP连接
* @returns {Promise<boolean>} 连接是否成功
*/
async ensureConnection () {
async ensureConnection() {
console.debug('🔗 Checking APP connection...')
// 如果不在APP环境中直接返回成功
@ -66,7 +66,7 @@ class AppConnectionChecker {
* 执行APP连接
* @returns {Promise<boolean>}
*/
async _performConnection () {
async _performConnection() {
try {
console.debug('📱 Starting new APP connection...')
@ -120,7 +120,7 @@ class AppConnectionChecker {
* APP连接成功后获取用户信息
* @private
*/
async _fetchUserInfoAfterConnection () {
async _fetchUserInfoAfterConnection() {
try {
console.debug('👤 Fetching user info after connection...')
@ -143,7 +143,7 @@ class AppConnectionChecker {
/**
* 重置连接状态
*/
reset () {
reset() {
appConnectionManager.reset()
}
}
@ -152,7 +152,7 @@ class AppConnectionChecker {
* 身份检查器
*/
class IdentityChecker {
constructor () {
constructor() {
this.isChecking = false
this.checkPromise = null
}
@ -162,7 +162,7 @@ class IdentityChecker {
* @param {string} userId - 用户ID
* @returns {Promise<string>} 返回主要身份
*/
async checkUserIdentity (userId) {
async checkUserIdentity(userId) {
if (!userId) {
console.warn('⚠️ No user ID provided')
permissionManager.setUserIdentity(null)
@ -192,7 +192,7 @@ class IdentityChecker {
* @param {string} userId - 用户ID
* @returns {Promise<string>} 主要身份
*/
async _performIdentityCheck (userId) {
async _performIdentityCheck(userId) {
try {
console.debug('🔍 Checking user identity for:', userId)
@ -221,7 +221,7 @@ class IdentityChecker {
/**
* 重置检查状态
*/
reset () {
reset() {
this.isChecking = false
this.checkPromise = null
}
@ -238,7 +238,7 @@ class PageRedirector {
* @param {string} currentPath - 当前页面路径
* @returns {Promise<boolean>} 是否进行了重定向
*/
async redirectToCorrectPage (router, targetPath, currentPath) {
async redirectToCorrectPage(router, targetPath, currentPath) {
const primaryRole = permissionManager.getPrimaryRole()
// 检查目标页面权限
@ -277,7 +277,7 @@ class PageRedirector {
* @param {Object} router - Vue Router 实例
* @param {string} attemptedPage - 尝试访问的页面
*/
async handleUnauthorizedAccess (router, attemptedPage) {
async handleUnauthorizedAccess(router, attemptedPage) {
const primaryRole = permissionManager.getPrimaryRole()
const defaultPage = permissionManager.getDefaultPage()
@ -296,7 +296,7 @@ class PageRedirector {
* 路由守卫类
*/
class RouteGuard {
constructor () {
constructor() {
this.appConnectionChecker = new AppConnectionChecker()
this.identityChecker = new IdentityChecker()
this.pageRedirector = new PageRedirector()
@ -307,7 +307,7 @@ class RouteGuard {
* 安装路由守卫
* @param {Object} router - Vue Router 实例
*/
install (router) {
install(router) {
router.beforeEach(async (to, from, next) => {
await this.handleRouteChange(to, from, next)
})
@ -322,7 +322,7 @@ class RouteGuard {
* @param {Object} from - 来源路由
* @param {Function} next - 路由继续函数
*/
async handleRouteChange (to, from, next) {
async handleRouteChange(to, from, next) {
console.group('🛡️ Route Guard Check')
console.debug('📍 Route change:', from.path, '→', to.path)
@ -394,7 +394,7 @@ class RouteGuard {
* @param {string} path - 页面路径
* @returns {boolean}
*/
isPublicPage (path) {
isPublicPage(path) {
const publicPages = [
PAGES.APPLY, // 申请页面 - 重要:申请页面不需要权限检查
PAGES.NOT_APP, // 错误页面 - APP连接失败时的页面
@ -406,6 +406,7 @@ class RouteGuard {
'/pay-result',
'/top-list', //排行榜
'/weekly-star', //每周明星
'/ranking', //总排行榜
]
return publicPages.includes(path)
}
@ -415,7 +416,7 @@ class RouteGuard {
* @param {Object} router - Vue Router 实例
* @param {string} userId - 用户ID
*/
async checkAndRedirect (router, userId) {
async checkAndRedirect(router, userId) {
try {
console.debug('🔄 Manual identity check and redirect')
@ -443,7 +444,7 @@ class RouteGuard {
/**
* 重置守卫状态
*/
reset () {
reset() {
this.appConnectionChecker.reset()
this.identityChecker.reset()
permissionManager.reset()

View File

@ -0,0 +1,426 @@
<template>
<div class="fullPage" :style="{ backgroundColor, backgroundImage }">
<div ref="sentinel"></div>
<!-- 哨兵元素 -->
<!-- 粘性布局贴顶部 -->
<div
ref="stickyHeader"
style="
position: sticky;
top: 0;
z-index: 999;
padding-bottom: 10px;
border-radius: 0 0 12px 12px;
transition: all 0.3s;
"
>
<!-- 状态栏占位区域仅在APP中显示 -->
<div v-if="isInAppEnvironment" style="height: 30px"></div>
<!-- 顶部导航栏 -->
<div style="padding: 4px 8%">
<div style="display: flex; justify-content: space-around; position: relative">
<div
class="tab"
@click="rankingType = 'Wealth'"
style=""
:style="{ color: rankingType == 'Wealth' ? '#FFB760' : '#fff' }"
>
Wealth
</div>
<div
class="tab"
@click="rankingType = 'Charm'"
style=""
:style="{ color: rankingType == 'Charm' ? '#60FF83' : '#fff' }"
>
Charm
</div>
<div
class="tab"
@click="rankingType = 'Room'"
style=""
:style="{ color: rankingType == 'Room' ? '#9B60FF' : '#fff' }"
>
Room
</div>
<!-- 推出键 -->
<img
src="../../assets/icon/arrowBack.png"
alt=""
style="position: absolute; left: 0; top: 50%; transform: translateY(-50%)"
width="6%"
@click="closePage"
/>
</div>
</div>
<!-- 排行榜时间选择 -->
<div
style="
margin: 10px 4% 0;
padding: 4px 16px;
border-radius: 32px;
backdrop-filter: blur(32px);
display: flex;
justify-content: space-between;
"
:style="{ border, background }"
>
<div
class="tag"
:class="{ tagActive: selectedTimeTab == 'Hourly' }"
@click="selectedTimeTab = 'Hourly'"
>
Hourly
</div>
<div
class="tag"
:class="{ tagActive: selectedTimeTab == 'Daily' }"
@click="selectedTimeTab = 'Daily'"
>
Daily
</div>
<div
class="tag"
:class="{ tagActive: selectedTimeTab == 'Weekly' }"
@click="selectedTimeTab = 'Weekly'"
>
Weekly
</div>
<div
class="tag"
:class="{ tagActive: selectedTimeTab == 'Monthly' }"
@click="selectedTimeTab = 'Monthly'"
>
Monthly
</div>
</div>
</div>
<!-- 排行榜模块 -->
<div>
<!-- 前3名 -->
<div style="margin: 3% 10px 0">
<!-- 第一 -->
<div style="display: flex; justify-content: center">
<TopUser
:isRoom="rankingType === 'Room'"
:isTopOne="true"
:BorderImgUrl="topBorderBg"
avatarUrl=""
name="aaaa"
distributionValue="11111"
style="width: 40%"
/>
</div>
<!-- 第二三 -->
<div style="display: flex; justify-content: space-between; margin-top: -20%">
<TopUser
:isRoom="rankingType === 'Room'"
:BorderImgUrl="top2BorderBg"
avatarUrl=""
name="aaaa"
distributionValue="11111"
style="width: 30%"
/>
<TopUser
:isRoom="rankingType === 'Room'"
:BorderImgUrl="top3BorderBg"
avatarUrl=""
name="aaaa"
distributionValue="11111"
style="width: 30%"
/>
</div>
</div>
<!-- 第四名开始 -->
<div style="margin: 3% 10px 0">
<div
v-for="value in 9"
style="
border-radius: 12px;
backdrop-filter: blur(32px);
margin: 5px 0;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
"
:style="{ background: rankItemBackground, border: '1px ' + rankItemBorder }"
>
<div style="display: flex; align-items: center; width: 70%">
<div style="font-weight: 700">999</div>
<img
src=""
alt=""
width="20%"
style="display: block; aspect-ratio: 1/1; margin: 0 4px"
:style="{ borderRadius: rankingType === 'Room' ? '0' : '50%' }"
@error="defaultAvatarUrl"
/>
<div
style="
font-weight: 700;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
aaaaaaaaaaaa
</div>
</div>
<div style="display: flex; align-items: center; width: 30%">
<img
src="/src/assets/icon/coin.png"
alt=""
width="20%"
style="display: block; aspect-ratio: 1/1"
/>
<div
style="
font-weight: 500;
color: rgba(248, 182, 45, 1);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
123456789
</div>
</div>
</div>
</div>
</div>
<!-- 我的排名 -->
<div style="position: sticky; bottom: 0; z-index: 999; padding: 0 1px; width: 100%">
<div
style="
border-radius: 12px 12px 0 0;
border-top: 2px solid #ffa166;
border-right: 1px solid #ffa166;
border-left: 1px solid #ffa166;
backdrop-filter: blur(32px);
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
"
:style="{
background: rankItemBackground,
borderTop: '2px ' + rankItemBorder,
borderLeft: '1px ' + rankItemBorder,
borderRight: '1px ' + rankItemBorder,
}"
>
<div style="display: flex; align-items: center; width: 70%">
<div style="font-weight: 700">999</div>
<img
src=""
alt=""
width="20%"
style="display: block; aspect-ratio: 1/1; margin: 0 4px"
@error="defaultAvatarUrl"
/>
<div
style="font-weight: 700; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
>
aaaaaaaaaaaa
</div>
</div>
<div style="display: flex; align-items: center; width: 30%">
<img
src="/src/assets/icon/coin.png"
alt=""
width="20%"
style="display: block; aspect-ratio: 1/1"
/>
<div
style="
font-weight: 500;
color: rgba(248, 182, 45, 1);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
"
>
123456789
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { isInApp, closePage } from '@/utils/appBridge.js'
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
import TopUser from '@/components/Ranking/topUser.vue'
//vite
const imageModules = import.meta.glob('@/assets/images/Ranking/*.{png,jpg,svg}', { eager: true })
//
const images = Object.fromEntries(
Object.entries(imageModules).map(([path, module]) => {
const fileName = path.split('/').pop().split('.')[0]
return [fileName, module.default]
})
)
const isInAppEnvironment = ref(false) // APP
const rankingType = ref('Wealth') //
const selectedTimeTab = ref('Hourly') //
//
const backgroundColor = ref('') //
const backgroundImage = ref('') //
const topBorderBg = ref('') //top1
const top2BorderBg = ref('') //top2
const top3BorderBg = ref('') //top3
const border = ref('') //
const background = ref('') //
const rankItemBackground = ref('') //
const rankItemBorder = ref('') //
const sentinel = ref(null)
const stickyHeader = ref(null)
watchEffect(() => {
if (rankingType.value == 'Wealth') {
backgroundColor.value = '#230F0A'
backgroundImage.value = `url(${images.bgWealth})`
topBorderBg.value = images['top1-wealth']
top2BorderBg.value = images['top2']
top3BorderBg.value = images['top3']
border.value = '1px solid rgba(221, 114, 0, 0.39)'
background.value = 'rgba(252, 194, 147, 0.20)'
rankItemBackground.value =
'linear-gradient(97deg, rgba(50, 34, 21, 0.60) 12.03%, rgba(99, 52, 33, 0.60) 100.37%)'
rankItemBorder.value = 'solid #FFA166'
}
if (rankingType.value == 'Charm') {
backgroundColor.value = '#112C1E'
backgroundImage.value = `url(${images.bgCharm})`
topBorderBg.value = images['top1-charm']
top2BorderBg.value = images['top2']
top3BorderBg.value = images['top3']
border.value = '1px solid rgba(0, 221, 74, 0.39)'
background.value = 'rgba(147, 252, 166, 0.20)'
rankItemBackground.value =
'linear-gradient(97deg, rgba(21, 50, 22, 0.60) 12.03%, rgba(33, 99, 40, 0.60) 100.37%)'
rankItemBorder.value = 'solid #66FF82'
}
if (rankingType.value == 'Room') {
backgroundColor.value = '#211031'
backgroundImage.value = `url(${images.bgRoom})`
topBorderBg.value = images['top1-room']
top2BorderBg.value = images['top2-room']
top3BorderBg.value = images['top3-room']
border.value = '1px solid rgba(155, 0, 221, 0.39)'
background.value = 'rgba(219, 147, 252, 0.20)'
rankItemBackground.value =
'linear-gradient(97deg, rgba(33, 21, 50, 0.60) 12.03%, rgba(60, 33, 99, 0.60) 100.37%)'
rankItemBorder.value = 'solid #A366FF'
}
})
const defaultAvatarUrl = (e) => {
console.log('头像资源出错')
e.target.onerror = null //
e.target.src = new URL('/src/assets/images/WeeklyStar/defaultAvatar.png', import.meta.url).href
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
//
if (!entry.isIntersecting) {
console.log('哨兵元素离开视口')
if (stickyHeader.value) {
stickyHeader.value.classList.add('stuck')
console.log('添加stuck类')
} else {
console.log('元素未找到!')
}
} else {
stickyHeader.value.classList.remove('stuck')
}
})
})
onMounted(() => {
isInAppEnvironment.value = isInApp()
observer.observe(sentinel.value)
})
</script>
<style lang="scss" scoped>
* {
color: #fff;
font-family: 'SF Pro';
}
.fullPage {
width: 100vw;
min-height: 100vh;
background-size: 100% auto;
background-repeat: no-repeat;
position: relative;
}
.tab {
font-weight: 700;
font-size: 1em;
}
.tag {
font-weight: 510;
padding: 4px 4%;
}
.tagActive {
color: black;
border-radius: 32px;
border: 1px solid #f5f54c;
box-sizing: border-box;
background: #fcfc93;
box-shadow: 0 0 8.6px 3px #fc8b02 inset;
display: flex;
justify-content: center;
align-items: center;
}
.stuck {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(32px);
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 16px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>