first commit
This commit is contained in:
599
src/pages/rank/rank.vue
Normal file
599
src/pages/rank/rank.vue
Normal file
@@ -0,0 +1,599 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user