存档点

This commit is contained in:
2026-01-14 18:17:16 +08:00
parent 39ea6f2fb1
commit cdc5dff129
8 changed files with 408 additions and 117 deletions

View File

@@ -16,4 +16,11 @@ export default {
page { page {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
background: transparent;
}
</style> </style>

View File

@@ -1,10 +1,10 @@
const config = { const config = {
baseUrl: getBaseUrl(), baseUrl: getBaseUrl(),
api: { api: {
login: '/gw/login', login: '/gw/open/login',
getQuestion: '/qgdzs/get_question', getQuestion: '/qgdzs/open/get_question',
answerQuestion: '/qgdzs/answer_question', answerQuestion: '/qgdzs/open/answer_question',
getAllCategory: '/qgdzs/get_all_category' getAllCategory: '/qgdzs/open/get_all_category'
}, },
timeout: 30000 timeout: 30000
} }

View File

@@ -2,8 +2,8 @@
"name" : "卷王大乱斗", "name" : "卷王大乱斗",
"appid" : "__UNI__3947B70", "appid" : "__UNI__3947B70",
"description" : "", "description" : "",
"versionName" : "1.0.7", "versionName" : "1.0.8",
"versionCode" : 107, "versionCode" : 108,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {

View File

@@ -1,74 +1,157 @@
<template> <template>
<view class="container"> <scroll-view
class="container"
scroll-y
show-scrollbar="false"
@scrolltolower="loadMore"
>
<view class="header"> <view class="header">
<view class="welcome"> <view class="welcome">
<text class="welcome-title">欢迎来到答题挑战</text> <text class="welcome-title">答题记录</text>
<text class="welcome-subtitle">多种玩法等你来挑战</text> <text class="welcome-subtitle">查看你的答题历史</text>
</view> </view>
</view> </view>
<view class="game-modes"> <view class="record-section">
<view class="mode-card" @click="goToRandom"> <view v-if="recordList.length > 0" class="record-list">
<view class="mode-icon random-icon">🎲</view> <view v-for="(record, index) in recordList" :key="index" class="record-item">
<view class="mode-info"> <view class="record-header">
<text class="mode-title">随机答题</text> <view class="record-category">{{ record.category }}</view>
<text class="mode-desc">随机抽取题目自由挑战</text> <view class="record-difficulty" :class="getDifficultyClass(record.difficulty)">
<text class="difficulty-text">{{ getDifficultyText(record.difficulty) }}</text>
</view> </view>
<text class="mode-arrow"></text> </view>
<view class="record-question">{{ record.question }}</view>
<view class="record-footer">
<view class="record-result" :class="record.is_correct ? 'correct' : 'wrong'">
<text class="result-icon">{{ record.is_correct ? '✓' : '✗' }}</text>
<text class="result-text">{{ record.is_correct ? '回答正确' : '回答错误' }}</text>
</view>
<text class="record-time">{{ formatTime(record.create_time) }}</text>
</view>
</view>
</view>
<view v-else class="empty-record">
<text class="empty-icon">📝</text>
<text class="empty-text">暂无答题记录</text>
<text class="empty-hint">快去答题吧</text>
</view> </view>
<view class="mode-card" @click="goToCategory"> <view v-if="isLoading" class="loading-more">
<view class="mode-icon category-icon">📚</view> <text class="loading-text">加载中...</text>
<view class="mode-info">
<text class="mode-title">类目答题</text>
<text class="mode-desc">按类目分类系统学习</text>
</view>
<text class="mode-arrow"></text>
</view> </view>
<view class="mode-card" @click="goToQuick"> <view v-if="!hasMore && recordList.length > 0" class="no-more">
<view class="mode-icon quick-icon"></view> <text class="no-more-text">没有更多了</text>
<view class="mode-info">
<text class="mode-title">快速答题</text>
<text class="mode-desc">限时挑战速度比拼</text>
</view>
<text class="mode-arrow"></text>
</view>
</view>
<view class="tips">
<view class="tip-item">
<text class="tip-icon">💡</text>
<text class="tip-text">每日坚持答题提升知识储备</text>
</view>
</view> </view>
</view> </view>
</scroll-view>
</template> </template>
<script> <script>
import api from '../../utils/api.js'
export default { export default {
data() { data() {
return { return {
recordList: [],
currentPage: 1,
pageSize: 10,
totalCount: 0,
hasMore: true,
isLoading: false
} }
}, },
onLoad() {
this.loadRecord()
},
onShow() {
this.loadRecord()
},
methods: { methods: {
goToRandom() { async loadRecord(isLoadMore = false) {
uni.switchTab({ if (this.isLoading) return
url: '/pages/random/random' if (!isLoadMore) {
}) this.currentPage = 1
this.hasMore = true
}
if (!this.hasMore) return
this.isLoading = true
try {
const res = await api.getRecord(this.currentPage, this.pageSize)
if (res.data) {
const newRecords = res.data.records || []
if (isLoadMore) {
this.recordList = [...this.recordList, ...newRecords]
} else {
this.recordList = newRecords
}
this.totalCount = res.data.count || 0
if (this.recordList.length >= this.totalCount) {
this.hasMore = false
} else {
this.currentPage++
}
}
} catch (error) {
console.error('获取答题记录失败:', error)
} finally {
this.isLoading = false
}
}, },
goToCategory() { loadMore() {
uni.switchTab({ this.loadRecord(true)
url: '/pages/category/category'
})
}, },
goToQuick() { getDifficultyText(difficulty) {
uni.switchTab({ if (!difficulty && difficulty !== 0) return '简单'
url: '/pages/quick/quick' if (difficulty <= 30) return '简单'
}) if (difficulty <= 60) return '中等'
if (difficulty <= 80) return '困难'
return '极难'
},
getDifficultyClass(difficulty) {
if (!difficulty && difficulty !== 0) return 'difficulty-easy'
if (difficulty <= 30) return 'difficulty-easy'
if (difficulty <= 60) return 'difficulty-medium'
if (difficulty <= 80) return 'difficulty-hard'
return 'difficulty-extreme'
},
formatTime(timestamp) {
if (!timestamp) return ''
const date = new Date(timestamp * 1000)
const now = new Date()
const diff = now - date
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
if (diff < minute) {
return '刚刚'
} else if (diff < hour) {
return Math.floor(diff / minute) + '分钟前'
} else if (diff < day) {
return Math.floor(diff / hour) + '小时前'
} else if (diff < 7 * day) {
return Math.floor(diff / day) + '天前'
} else {
const month = date.getMonth() + 1
const dayOfMonth = date.getDate()
return `${month}${dayOfMonth}`
}
} }
} }
} }
@@ -76,9 +159,25 @@ export default {
<style scoped> <style scoped>
.container { .container {
min-height: 100vh; height: 100vh;
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx; padding: 40rpx;
box-sizing: border-box;
}
.container::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
background: transparent;
}
/deep/ ::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
background: transparent;
} }
.header { .header {
@@ -103,93 +202,157 @@ export default {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
} }
.game-modes { .record-section {
display: flex; margin-bottom: 60rpx;
flex-direction: column;
gap: 30rpx;
} }
.mode-card { .record-list {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 20rpx;
}
.record-item {
background: #ffffff; background: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15);
transition: all 0.3s;
}
.mode-card:active {
transform: scale(0.98);
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
}
.mode-icon {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx; border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
width: 100%;
box-sizing: border-box;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.record-category {
font-size: 26rpx;
color: #666;
background: #f0f0f0;
padding: 6rpx 16rpx;
border-radius: 12rpx;
}
.record-difficulty {
padding: 6rpx 16rpx;
border-radius: 12rpx;
font-size: 24rpx;
}
.difficulty-easy {
background: #d4edda;
color: #155724;
}
.difficulty-medium {
background: #fff3cd;
color: #856404;
}
.difficulty-hard {
background: #fd7e14;
color: #ffffff;
}
.difficulty-extreme {
background: #dc3545;
color: #ffffff;
}
.difficulty-text {
font-weight: bold;
}
.record-question {
font-size: 30rpx;
color: #333;
line-height: 1.6;
margin-bottom: 20rpx;
}
.record-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.record-result {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
font-size: 50rpx;
margin-right: 30rpx;
}
.random-icon {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.category-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.quick-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.mode-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx; gap: 10rpx;
font-size: 26rpx;
} }
.mode-title { .record-result.correct {
color: #28a745;
}
.record-result.wrong {
color: #dc3545;
}
.result-icon {
font-size: 32rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
color: #333;
} }
.mode-desc { .result-text {
font-size: 26rpx; font-weight: bold;
}
.record-time {
font-size: 24rpx;
color: #999; color: #999;
} }
.mode-arrow { .empty-record {
font-size: 50rpx; background: rgba(255, 255, 255, 0.15);
color: #ccc; border-radius: 20rpx;
padding: 100rpx 40rpx;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.empty-icon {
font-size: 80rpx;
}
.empty-text {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.9);
font-weight: bold; font-weight: bold;
} }
.tips { .empty-hint {
margin-top: 60rpx; font-size: 26rpx;
color: rgba(255, 255, 255, 0.7);
} }
.tip-item { .loading-more {
display: flex; text-align: center;
align-items: center; padding: 40rpx 0;
background: rgba(255, 255, 255, 0.15);
border-radius: 16rpx;
padding: 30rpx;
} }
.tip-icon { .loading-text {
font-size: 36rpx;
margin-right: 20rpx;
}
.tip-text {
font-size: 28rpx; font-size: 28rpx;
color: #ffffff; color: rgba(255, 255, 255, 0.9);
}
.no-more {
text-align: center;
padding: 40rpx 0;
}
.no-more-text {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
} }
</style> </style>

View File

@@ -76,10 +76,11 @@ export default {
if (res.data && res.data.accessToken) { if (res.data && res.data.accessToken) {
storage.setToken(res.data.accessToken) storage.setToken(res.data.accessToken)
storage.setRefreshToken(res.data.refreshToken)
const userInfo = { const userInfo = {
...res.data, ...res.data,
nickName: res.data.nickName || '微信用户', nickName: res.data.name || '微信用户',
avatarUrl: res.data.avatarUrl || '' avatarUrl: res.data.avatarUrl || ''
} }
storage.setUserInfo(userInfo) storage.setUserInfo(userInfo)

View File

@@ -479,6 +479,7 @@ export default {
} }
.result-title { .result-title {
display: block;
font-size: 48rpx; font-size: 48rpx;
font-weight: bold; font-weight: bold;
color: #333; color: #333;

View File

@@ -1,4 +1,17 @@
import config from '../config.js' import config from '../config.js'
import storage from './storage.js'
let isRefreshing = false
let refreshSubscribers = []
const subscribeTokenRefresh = (callback) => {
refreshSubscribers.push(callback)
}
const onTokenRefreshed = (token) => {
refreshSubscribers.forEach(callback => callback(token))
refreshSubscribers = []
}
const request = (options) => { const request = (options) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -8,23 +21,105 @@ const request = (options) => {
console.log('Method:', options.method || 'GET') console.log('Method:', options.method || 'GET')
console.log('Data:', options.data) console.log('Data:', options.data)
const token = storage.getToken()
uni.request({ uni.request({
url: fullUrl, url: fullUrl,
method: options.method || 'GET', method: options.method || 'GET',
data: options.data || {}, data: options.data || {},
header: { header: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options.header ...options.header
}, },
timeout: config.timeout, timeout: config.timeout,
success: (res) => { success: async (res) => {
console.log('=== 请求成功 ===') console.log('=== 请求成功 ===')
console.log('StatusCode:', res.statusCode) console.log('StatusCode:', res.statusCode)
console.log('Response:', res.data) console.log('Response:', res.data)
const { statusCode, data } = res const { statusCode, data } = res
if (statusCode === 200) { if (statusCode === 401) {
console.log('=== Token 过期,尝试刷新 ===')
if (isRefreshing) {
return new Promise((resolve) => {
subscribeTokenRefresh((newToken) => {
options.header = options.header || {}
options.header.Authorization = `Bearer ${newToken}`
resolve(request(options))
})
}).then(resolve).catch(reject)
}
isRefreshing = true
try {
const refreshToken = storage.getRefreshToken()
if (!refreshToken) {
throw new Error('没有刷新令牌')
}
const refreshRes = await new Promise((resolve, reject) => {
uni.request({
url: config.baseUrl + '/gw/open/refresh_token',
method: 'POST',
data: {
refreshToken
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
if (res.statusCode === 200 && res.data.code === 0) {
resolve(res.data)
} else {
reject(res.data)
}
},
fail: reject
})
})
if (refreshRes.data && refreshRes.data.accessToken) {
storage.setToken(refreshRes.data.accessToken)
if (refreshRes.data.refreshToken) {
storage.setRefreshToken(refreshRes.data.refreshToken)
}
onTokenRefreshed(refreshRes.data.accessToken)
isRefreshing = false
options.header = options.header || {}
options.header.Authorization = `Bearer ${refreshRes.data.accessToken}`
const retryRes = await request(options)
resolve(retryRes)
} else {
throw new Error('刷新令牌失败')
}
} catch (refreshError) {
console.error('=== 刷新令牌失败 ===', refreshError)
isRefreshing = false
storage.removeToken()
storage.removeRefreshToken()
storage.removeUserInfo()
uni.showModal({
title: '登录过期',
content: '请重新登录',
showCancel: false,
success: () => {
uni.reLaunch({
url: '/pages/login/login'
})
}
})
reject(refreshError)
}
} else if (statusCode === 200) {
if (data.code === 0 || data.success === true) { if (data.code === 0 || data.success === true) {
resolve(data) resolve(data)
} else { } else {
@@ -114,6 +209,17 @@ const api = {
}) })
}, },
getRecord(page = 1, pageSize = 10) {
return request({
url: '/qgdzs/auth/get_record',
method: 'POST',
data: {
page,
page_size: pageSize
}
})
},
get(url, data = {}) { get(url, data = {}) {
return request({ return request({
url, url,

View File

@@ -1,4 +1,5 @@
const TOKEN_KEY = 'access_token' const TOKEN_KEY = 'access_token'
const REFRESH_TOKEN_KEY = 'refresh_token'
const USER_INFO_KEY = 'user_info' const USER_INFO_KEY = 'user_info'
const QUESTION_KEY = 'question' const QUESTION_KEY = 'question'
const ANSWERED_QUESTIONS_KEY = 'answered_questions' const ANSWERED_QUESTIONS_KEY = 'answered_questions'
@@ -16,6 +17,18 @@ const storage = {
uni.removeStorageSync(TOKEN_KEY) uni.removeStorageSync(TOKEN_KEY)
}, },
setRefreshToken(token) {
uni.setStorageSync(REFRESH_TOKEN_KEY, token)
},
getRefreshToken() {
return uni.getStorageSync(REFRESH_TOKEN_KEY) || ''
},
removeRefreshToken() {
uni.removeStorageSync(REFRESH_TOKEN_KEY)
},
setUserInfo(userInfo) { setUserInfo(userInfo) {
uni.setStorageSync(USER_INFO_KEY, userInfo) uni.setStorageSync(USER_INFO_KEY, userInfo)
}, },