Files
playFun-uni/src/pages/rank/rank.vue
2026-10-12 11:22:54 +08:00

600 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { computed, ref, watch } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { levels, questions } from '../../mock/data';
import { claimWeeklyTask, getCompetitionProgress, getWeeklyTasks } from '../../mock/engine';
import { getMockState, updateMockState } from '../../mock/store';
// 本地自评结果和问卷步骤。
const assessment = ref(null);
// 段位积分依据已确认的拼场赛果和本周任务奖励计算。
const rankState = ref(getMockState());
const competition = computed(() => getCompetitionProgress(rankState.value.rankPoints));
const resultCount = computed(() => rankState.value.rankHistory.filter((entry) => entry.eventId.startsWith('match-')).length);
const weeklyTasks = computed(() => getWeeklyTasks(rankState.value));
// 展示的昵称来自本地演示档案。
const profileName = ref('球搭子阿豪');
const showQuiz = ref(false);
const questionIndex = ref(0);
const answers = ref([]);
// 匹配段位与下一次可重评时间。
const matchLevel = computed(() => assessment.value?.level || '中级');
const canRetake = computed(() => !assessment.value || Date.now() - assessment.value.completedAt >= 30 * 24 * 60 * 60 * 1000);
const retakeDate = computed(() => assessment.value ? new Date(assessment.value.completedAt + 30 * 24 * 60 * 60 * 1000).toLocaleDateString('zh-CN') : '');
// 切回页面时恢复本地自评结果。
onShow(() => {
const state = getMockState();
assessment.value = state.assessment;
profileName.value = state.profileName;
rankState.value = state;
});
// 自评弹层打开时隐藏系统 TabBar,避免选项被遮挡。
watch(showQuiz, (visible) => {
if (visible) uni.hideTabBar({ animation: false });
else uni.showTabBar({ animation: false });
});
// 从第一题开始一次完整的十五题自评。
function startQuiz() {
if (!canRetake.value) {
uni.showToast({ title: `${retakeDate.value} 后可重评`, icon: 'none' });
return;
}
questionIndex.value = 0;
answers.value = [];
showQuiz.value = true;
}
// 保存单题分值,完成时计算可用于报名的匹配段位。
function answer(score) {
answers.value.push(score);
if (questionIndex.value < questions.length - 1) {
questionIndex.value += 1;
return;
}
const total = answers.value.reduce((sum, value) => sum + value, 0);
const level = total >= 25 ? '高级' : total >= 17 ? '中级' : total >= 9 ? '初级' : '入门';
const result = { level, total, completedAt: Date.now() };
updateMockState((state) => ({ ...state, assessment: result }));
assessment.value = result;
showQuiz.value = false;
uni.showModal({ title: '自评完成', content: `当前匹配段位:${level}\n现在可用该段位加入相符的拼场。`, showCancel: false });
}
// 本周进度达标后领取积分,并将领取动作写入同一份本地流水。
function claimTask(taskId) {
try {
let points;
rankState.value = updateMockState((state) => {
points = claimWeeklyTask(state, taskId);
return state;
});
uni.showToast({ title: `已领取 ${points} 积分`, icon: 'none' });
} catch (error) {
uni.showToast({ title: error.message, icon: 'none' });
}
}
// 积分记录使用本地演示时钟对应的结算日期。
function formatDate(timestamp) {
return new Date(timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
// 从段位页进入可报名的拼场列表。
function findMatch() {
uni.switchTab({ url: '/pages/match/match' });
}
</script>
<template>
<view class="page rank-page">
<view class="profile-card card">
<view class="profile-top">
<view class="profile-avatar">{{ profileName.slice(-1) }}</view>
<view>
<view class="profile-name">{{ profileName }}</view>
<view class="profile-area">南京 · 雨花台</view>
</view>
<view class="rank-tag">{{ matchLevel }} · 匹配段位</view>
</view>
<view class="stat-grid">
<view><strong>{{ rankState.rankPoints.toLocaleString() }}</strong><text>段位积分</text></view>
<view><strong>{{ competition.current.name }}</strong><text>积分段位</text></view>
<view><strong>{{ resultCount }}</strong><text>已确认赛果</text></view>
</view>
</view>
<view class="progress-card card">
<view class="progress-ring"><text>{{ competition.percent }}%</text><small>晋级进度</small></view>
<view class="progress-copy">
<view class="progress-title">{{ competition.current.name }}<template v-if="competition.next"> → {{
competition.next.name }}</template><template v-else> · 已达最高段位</template></view>
<view class="progress-info">{{ competition.next ? `还需 ${competition.remaining} 分 · 下一档
${competition.next.points} 分` :
'继续约球可累积积分' }}</view>
<view class="progress-info">确认赛果和周任务计分 · 自评用于匹配</view>
<view class="progress-track">
<view :style="{ width: `${competition.percent}%` }"></view>
</view>
</view>
</view>
<view class="section-head"><text class="section-title">自评定级</text><text class="muted small">15 题 · 约 3 分钟</text>
</view>
<view class="assessment card">
<view>
<view class="assessment-title">当前匹配段位:{{ matchLevel }}</view>
<view class="assessment-sub">{{ assessment ? `自评得分 ${assessment.total}/30 · ${retakeDate} 后可重评` :
'完成自评后,结果直接用于拼场匹配'
}}</view>
</view><button class="outline-button assess-button" :disabled="!canRetake" @tap="startQuiz">{{ assessment ? '重新自评'
:
'开始自评' }}</button>
</view>
<view class="section-head"><text class="section-title">赛果计分</text><text class="muted small">双方确认后入账</text></view>
<view class="event-panel card">
<view class="event-meta">已签到并完赛的拼场,我方获胜 +160 分,落败 +80 分</view><button class="outline-button find-button"
@tap="findMatch">发现拼场</button>
</view>
<view class="section-head"><text class="section-title">本周晋级任务</text><text class="muted small">达标后领取积分</text></view>
<view class="task-list card">
<view v-for="task in weeklyTasks" :key="task.id" class="task"><text class="task-check"
:class="{ done: task.claimed }">{{ task.claimed ? '✓' : '' }}</text>
<view class="task-copy">
<view>{{ task.title }}</view><text class="muted small">本周 {{ task.progress }}/{{ task.target }} · 奖励 +{{
task.reward }} 分</text>
</view><button v-if="task.progress >= task.target" class="claim-button" :disabled="task.claimed"
@tap="claimTask(task.id)">{{ task.claimed ? '已领取' : '领取' }}</button>
</view>
</view>
<view class="section-head"><text class="section-title">积分记录</text><text class="muted small">共 {{
rankState.rankHistory.length }} 条</text></view>
<view class="history-list card">
<view v-if="!rankState.rankHistory.length" class="history-empty">暂无积分记录</view>
<view v-for="entry in rankState.rankHistory" :key="entry.eventId" class="history-item">
<view>
<view class="history-title">{{ entry.title }}</view><text class="muted small">{{ formatDate(entry.settledAt)
}} ·
结算后 {{ entry.total }} 分</text>
</view><text class="history-points">+{{ entry.points }}</text>
</view>
</view>
<view class="section-head"><text class="section-title">六段位晋级体系</text></view>
<view class="level-list card">
<view v-for="(level, index) in levels" :key="level.name" class="level-item"
:class="{ current: level.name === competition.current.name }"><text class="level-dot"
:class="`dot-${index}`"></text><text>{{ level.name }}</text><text class="level-points">{{ level.points }}
分起<template v-if="level.name === competition.current.name"> · 当前</template></text></view>
</view>
<view v-if="showQuiz" class="modal-mask">
<view class="sheet quiz-sheet">
<view class="sheet-grip"></view>
<view class="quiz-progress">自评定级 <text>{{ questionIndex + 1 }} / {{ questions.length }}</text></view>
<view class="quiz-track">
<view :style="{ width: `${(questionIndex + 1) / questions.length * 100}%` }"></view>
</view>
<view class="quiz-question">{{ questions[questionIndex] }}</view>
<view class="quiz-hint">请选择最符合你实际表现的选项</view><button class="quiz-option" @tap="answer(0)">还做不到</button><button
class="quiz-option" @tap="answer(1)">偶尔可以</button><button class="quiz-option"
@tap="answer(2)">能稳定做到</button><text class="quiz-exit" @tap="showQuiz = false">退出自评</text>
</view>
</view>
</view>
</template>
<style scoped>
.profile-card {
padding: 28rpx;
}
.profile-top {
display: flex;
align-items: center;
gap: 17rpx;
}
.profile-avatar {
width: 78rpx;
height: 78rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #d6ebd8;
color: #438c4b;
font-size: 36rpx;
font-weight: 750;
}
.profile-name {
font-weight: 700;
font-size: 29rpx;
}
.profile-area {
margin-top: 7rpx;
color: #98a39a;
font-size: 21rpx;
}
.rank-tag {
margin-left: auto;
color: #906925;
background: #fff0d4;
border-radius: 12rpx;
padding: 9rpx 12rpx;
font-size: 19rpx;
white-space: nowrap;
}
.stat-grid {
display: flex;
margin-top: 32rpx;
border-top: 1rpx solid #eef1ee;
padding-top: 24rpx;
}
.stat-grid view {
flex: 1;
text-align: center;
}
.stat-grid strong {
display: block;
color: #2c7539;
font-size: 35rpx;
}
.stat-grid text {
display: block;
color: #a0aaa1;
font-size: 20rpx;
margin-top: 5rpx;
}
.progress-card {
display: flex;
align-items: center;
gap: 25rpx;
padding: 27rpx;
margin-top: 20rpx;
}
.progress-ring {
width: 145rpx;
height: 145rpx;
flex: none;
border-radius: 50%;
border: 13rpx solid #f1ead8;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #b58227;
}
.progress-ring text {
font-weight: 800;
font-size: 34rpx;
}
.progress-ring small {
font-size: 17rpx;
}
.progress-copy {
flex: 1;
min-width: 0;
}
.progress-title {
font-size: 26rpx;
font-weight: 700;
}
.progress-info {
color: #8d968e;
font-size: 19rpx;
margin-top: 10rpx;
}
.progress-track {
height: 9rpx;
background: #f6f0e5;
border-radius: 9rpx;
margin-top: 17rpx;
}
.progress-track view {
width: 80%;
height: 100%;
background: #dba54a;
border-radius: inherit;
}
.assessment {
display: flex;
align-items: center;
gap: 15rpx;
padding: 24rpx;
}
.assessment>view {
flex: 1;
min-width: 0;
}
.assessment-title {
font-size: 25rpx;
font-weight: 650;
}
.assessment-sub {
color: #98a39a;
font-size: 20rpx;
margin-top: 9rpx;
line-height: 1.5;
}
.assess-button {
flex: none;
margin: 0;
padding: 0 17rpx;
height: 60rpx;
line-height: 60rpx;
font-size: 21rpx;
}
.assess-button[disabled] {
opacity: .55;
}
.event-panel {
padding: 24rpx;
display: flex;
align-items: center;
gap: 18rpx;
}
.event-meta {
flex: 1;
min-width: 0;
color: #7f8b81;
font-size: 23rpx;
line-height: 1.5;
}
.find-button {
flex: none;
margin: 0;
padding: 0 20rpx;
height: 60rpx;
line-height: 60rpx;
font-size: 21rpx;
}
.task-list {
padding: 0 24rpx;
}
.task {
min-height: 108rpx;
display: flex;
align-items: center;
gap: 17rpx;
border-bottom: 1rpx solid #f2f4f2;
}
.task:last-child {
border: 0;
}
.task-copy {
flex: 1;
min-width: 0;
font-size: 24rpx;
}
.task-copy .muted {
display: block;
margin-top: 8rpx;
}
.task-check {
flex: none;
width: 34rpx;
height: 34rpx;
border-radius: 50%;
background: #eef0ed;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
}
.task-check.done {
background: #e2ac48;
}
.claim-button {
flex: none;
margin: 0;
padding: 0 18rpx;
min-width: 96rpx;
height: 54rpx;
line-height: 54rpx;
border-radius: 9rpx;
background: #e8f4e9;
color: #398546;
font-size: 20rpx;
}
.claim-button[disabled] {
background: #f2f3f2;
color: #a1aaa1;
}
.history-list {
padding: 0 24rpx;
}
.history-item {
min-height: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
border-bottom: 1rpx solid #f2f4f2;
}
.history-item:last-child {
border: 0;
}
.history-title {
font-size: 24rpx;
margin-bottom: 6rpx;
}
.history-points {
flex: none;
color: #3b8d4a;
font-weight: 700;
font-size: 27rpx;
}
.history-empty {
padding: 32rpx 0;
color: #98a39a;
text-align: center;
font-size: 23rpx;
}
.level-list {
padding: 10rpx 28rpx;
}
.level-item {
display: flex;
align-items: center;
gap: 17rpx;
height: 76rpx;
font-size: 24rpx;
border-bottom: 1rpx solid #f3f4f3;
}
.level-item:last-child {
border: 0;
}
.level-item.current {
color: #378c42;
font-weight: 700;
}
.level-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: #bac8b8;
}
.dot-1 {
background: #7dbb8c;
}
.dot-2 {
background: #95acc8;
}
.dot-3 {
background: #dfab52;
}
.dot-4 {
background: #bd87a8;
}
.dot-5 {
background: #755fa4;
}
.level-points {
margin-left: auto;
color: #a1aaa1;
font-size: 21rpx;
}
.quiz-sheet {
min-height: 520rpx;
}
.quiz-progress {
display: flex;
justify-content: space-between;
font-weight: 700;
font-size: 30rpx;
}
.quiz-progress text {
color: #6aa270;
font-size: 24rpx;
}
.quiz-track {
height: 8rpx;
border-radius: 8rpx;
background: #eaf1e9;
margin-top: 22rpx;
}
.quiz-track view {
height: 100%;
border-radius: inherit;
background: #51a05a;
}
.quiz-question {
font-size: 31rpx;
font-weight: 650;
line-height: 1.5;
margin: 53rpx 0 12rpx;
}
.quiz-hint {
color: #9da79f;
font-size: 22rpx;
margin-bottom: 30rpx;
}
.quiz-option {
width: 100%;
background: #f5f8f5;
color: #3a5d40;
border-radius: 14rpx;
margin: 13rpx 0;
font-size: 25rpx;
}
.quiz-option:active {
background: #dceedd;
}
.quiz-exit {
display: block;
text-align: center;
color: #9ca49d;
font-size: 22rpx;
padding: 20rpx;
}
</style>