init: llpreheat 学法送演唱会门票 H5项目

This commit is contained in:
lcj
2026-09-03 10:21:11 +08:00
commit 305e22ddc5
223 changed files with 46261 additions and 0 deletions
+370
View File
@@ -0,0 +1,370 @@
<template>
<view class="content">
<view class="page-body">
<view class="loading-box" v-if="questions.length === 0 && !loadError">
<u-loading mode="circle" size="40"></u-loading>
<text class="loading-text">加载题目中...</text>
</view>
<view class="loading-box" v-if="loadError">
<text class="error-text">{{ loadError }}</text>
<button class="retry-btn" @tap="fetchQuestions">重新加载</button>
</view>
<template v-if="questions.length > 0">
<image src="https://aliyunoss.hn12301.net/learnlaw/learn_dtbj.jpg" mode="widthFix" style="width:750rpx;"></image>
<view style="position:absolute;top:200rpx;left:0;z-index:0;padding: 30rpx;" >
<view class="quiz-header">
<view class="timer-wrap">
<text class="timer-label">答题时间</text>
<text class="timer">{{ formatTime(usedSeconds) }}</text>
</view>
<text class="answered-count">已答 {{ answeredCount }} / {{ questions.length }}</text>
</view>
<view class="question-item" v-for="(q, qi) in questions" :key="q.question_id">
<view class="q-header">
<text class="q-index">{{ qi + 1 }}</text>
<text class="q-content">{{ q.question_content }}</text>
</view>
<view class="options">
<view
class="option"
v-for="(opt, oi) in q.answerList"
:key="opt.id"
:class="{ selected: answers[q.question_id] === oi }"
@tap="select(q.question_id, oi)"
>
<view class="option-radio" :class="{ checked: answers[q.question_id] === oi }">
<view v-if="answers[q.question_id] === oi" class="radio-dot"></view>
</view>
<text class="option-text">{{ opt.answer_content }}</text>
</view>
</view>
<view class="tips" v-if="answers[q.question_id] !== undefined">
<text class="tips-label">解析</text>
<text class="tips-text">{{ q.tips }}</text>
</view>
</view>
<button class="submit-btn" @tap="submit">提交</button>
</view>
</template>
</view>
</view>
</template>
<script>
export default {
data() {
return {
questions: [],
answers: {},
loadError: '',
startTime: 0,
usedSeconds: 0,
timer: null
}
},
computed: {
answeredCount() {
return Object.keys(this.answers).length
}
},
onLoad() {
this.fetchQuestions()
},
onUnload() {
this.stopTimer()
},
methods: {
fetchQuestions() {
this.loadError = ''
const userId = uni.getStorageSync('userId')
const userPhone = uni.getStorageSync('userPhone')
this.$u.api.queryWenjuanQA({
wenjuan_id: 1001,
user_id: userId,
user_phone: userPhone
}).then(res => {
if (res.code === 0 && res.Data?.wenjuanTmList) {
this.questions = res.Data.wenjuanTmList
this.startTimer()
} else {
this.loadError = res.msg || '数据异常'
}
}).catch(err => {
console.error('获取题目失败', err)
this.loadError = '网络请求失败'
})
},
startTimer() {
this.startTime = Date.now()
this.timer = setInterval(() => {
this.usedSeconds = Math.floor((Date.now() - this.startTime) / 1000)
}, 1000)
},
stopTimer() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
formatTime(sec) {
const m = Math.floor(sec / 60)
const s = sec % 60
return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s
},
select(qId, oi) {
this.$set(this.answers, qId, oi)
},
submit() {
if (this.answeredCount < this.questions.length) {
uni.showToast({ title: '还有题目未作答', icon: 'none' })
return
}
this.stopTimer()
const userId = uni.getStorageSync('userId')
const userPhone = uni.getStorageSync('userPhone')
const details = this.questions.map(q => ({
question_id: q.question_id,
user_answer: q.answerList[this.answers[q.question_id]]?.answer_content || ''
}))
this.$u.api.submitQA({
user_id: userId,
user_phone: userPhone,
wenjuan_id: 1001,
use_time: this.usedSeconds,
total_question_num: this.questions.length,
channel: 1,
user_answer_details: details
}).then(res => {
if (res.code === 0) {
uni.setStorageSync('resultData', {
...res.Data,
use_time: this.usedSeconds,
total_question_num: this.questions.length
})
uni.navigateTo({ url: '/pages/result/result' })
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
}).catch(() => {
uni.showToast({ title: '网络异常', icon: 'none' })
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #B3E9F3;
}
.content {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
}
.page-body {
flex: 1;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding: 16rpx 24rpx;
background: #71BCFB66;
border-radius: 12rpx;
flex-shrink: 0;
}
.quiz-scroll {
flex: 1;
overflow: hidden;
position: relative;
}
.timer-wrap {
display: flex;
align-items: center;
gap: 10rpx;
}
.timer-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.timer {
font-size: 28rpx;
color: #fff;
font-weight: 600;
}
.answered-count {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
}
.question-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 32rpx 28rpx;
margin-bottom: 24rpx;
}
.q-header {
display: flex;
margin-bottom: 20rpx;
}
.q-index {
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
background: #FD5401;
color: #fff;
border-radius: 50%;
font-size: 24rpx;
font-weight: 600;
flex-shrink: 0;
margin-right: 16rpx;
margin-top: 4rpx;
}
.q-content {
font-size: 28rpx;
color: #333;
line-height: 1.6;
flex: 1;
}
.options {
display: flex;
flex-direction: column;
gap: 14rpx;
}
.option {
padding: 22rpx 20rpx;
display: flex;
align-items: center;
border: 2rpx solid #eee;
border-radius: 12rpx;
}
.option.selected {
border-color: #FD5401;
background: rgba(253, 84, 1, 0.05);
}
.option-radio {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 3rpx solid #CCC;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
}
.option-radio.checked {
border-color: #FD5401;
}
.radio-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: #FD5401;
}
.option-text {
font-size: 27rpx;
color: #333;
line-height: 1.4;
}
.tips {
background: rgba(255, 247, 237, 0.95);
border-radius: 12rpx;
padding: 20rpx;
margin-top: 16rpx;
border-left: 6rpx solid #FD5401;
}
.tips-label {
font-size: 24rpx;
font-weight: 600;
color: #FD5401;
display: block;
margin-bottom: 6rpx;
}
.tips-text {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
color: #fff;
font-size: 32rpx;
font-weight: 600;
border: none;
border-radius: 44rpx;
margin: 10rpx 0 40rpx;
box-shadow: 0 4rpx 16rpx rgba(253, 84, 1, 0.3);
}
.loading-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 28rpx;
color: #fff;
margin-top: 20rpx;
}
.error-text {
font-size: 28rpx;
color: #fff;
text-align: center;
display: block;
}
.retry-btn {
margin-top: 30rpx;
width: 240rpx;
height: 72rpx;
line-height: 72rpx;
background: rgba(255, 255, 255, 0.9);
color: #FD5401;
border: none;
border-radius: 36rpx;
font-size: 28rpx;
}
</style>
+204
View File
@@ -0,0 +1,204 @@
<template>
<view>
<image src="https://aliyunoss.hn12301.net/learnlaw/xfsp_p_bj.jpg" style="width: 750rpx;" mode="widthFix"></image>
<view style="position: absolute;top: 1210rpx;width: 300rpx;left: 225rpx;height: 80rpx;" @click="goRegister"></view>
<view class="side-tab" style="top: 350rpx;" hover-class="side-tab-hover" @click="goRecords">答题记录</view>
<view class="side-tab" style="top: 430rpx;" hover-class="side-tab-hover" @click="goRules">活动规则</view>
<view class="blocked-mask" v-if="blocked">
<text class="blocked-tip">{{ blockTip }}</text>
<text class="blocked-sub">本活动仅支持手机微信访问</text>
</view>
</view>
</template>
<script>
const APP_ID = 'wx71c218bd9ff0e2da'
const SCOPE = 'snsapi_base'
export default {
data() {
return {
blocked: false,
blockTip: ''
}
},
onLoad() {
// if (new Date().getTime() < new Date('2026/09/02 10:00:00')) {
// uni.redirectTo({
// url:'/pages/jqqd/jqqd'
// })
// }
this.initLogin()
},
onShow() {
if (this.blocked) return
const openId = uni.getStorageSync('openId')
const isRegistered = uni.getStorageSync('isRegistered')
const userId = uni.getStorageSync('userId')
if (openId && (!isRegistered || !userId)) {
this.loginWithOpenId(openId)
}
},
methods: {
initLogin() {
if (!this.isWechatBrowser()) {
this.block('请在微信中打开')
return
}
if (this.isPcWechat()) {
this.block('请使用手机微信打开')
return
}
let openId = uni.getStorageSync('openId')
if (openId) {
this.loginWithOpenId(openId)
return
}
const code = this.getCodeFromUrl()
if (code) {
this.exchangeOpenId(code)
} else {
this.wechatAuth()
}
},
block(tip) {
this.blocked = true
this.blockTip = tip
},
isWechatBrowser() {
return /MicroMessenger/i.test(navigator.userAgent)
},
isPcWechat() {
return /MicroMessenger/i.test(navigator.userAgent) && /WindowsWechat|MacWechat/i.test(navigator.userAgent)
},
getCodeFromUrl() {
const match = window.location.href.match(/[?&]code=([^&#]+)/)
return match ? match[1] : null
},
cleanUrlCode() {
const cleanUrl = window.location.href.replace(/[?&]code=[^&#]*(&|#|$)/, (m, p1) => p1 === '&' ? '?' : (
p1 === '#' ? '#' : ''))
window.history.replaceState({}, '', cleanUrl)
},
wechatAuth() {
const redirectUri = encodeURIComponent(window.location.href.split('?')[0].split('#')[0])
const url =
`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${redirectUri}&response_type=code&scope=${SCOPE}&state=STATE#wechat_redirect`
window.location.href = url
},
loginWithOpenId(openId) {
this.$u.api.wxOpenidLogin({ wenjuan_id: 1001, open_id: openId }).then(loginRes => {
if (!loginRes.Data) {
uni.setStorageSync('isRegistered', false)
return
}
if (loginRes.code === 0 && loginRes.Data?.user_name && loginRes.Data?.user_phone) {
uni.setStorageSync('isRegistered', true)
uni.setStorageSync('userId', loginRes.Data.user_id)
uni.setStorageSync('userName', loginRes.Data.user_name)
uni.setStorageSync('userPhone', loginRes.Data.user_phone)
}
}).catch(err => {
console.error('登录请求失败', err)
})
},
goRegister() {
const isRegistered = uni.getStorageSync('isRegistered')
if (isRegistered) {
uni.navigateTo({
url: '/pages/answer/answer'
})
return
}
uni.navigateTo({
url: '/pages/register/register'
})
},
goRecords() {
uni.navigateTo({
url: '/pages/records/records'
})
},
goRules() {
uni.navigateTo({
url: '/pages/rules/rules'
})
},
exchangeOpenId(code) {
this.$u.api.getWxOpenId(code).then(res => {
if (res.code !== 0 || !res.Data || !res.Data.openid) return
const openId = res.Data.openid
uni.setStorageSync('openId', openId)
this.cleanUrlCode()
return this.$u.api.wxOpenidLogin({ wenjuan_id: 1001, open_id: openId })
}).then(loginRes => {
if (loginRes && loginRes.code === 0 && loginRes.Data && loginRes.Data.user_name && loginRes.Data.user_phone) {
uni.setStorageSync('isRegistered', true)
uni.setStorageSync('userId', loginRes.Data.user_id || loginRes.Data.uesr_id)
uni.setStorageSync('userName', loginRes.Data.user_name)
uni.setStorageSync('userPhone', loginRes.Data.user_phone)
} else if (loginRes && !loginRes.Data) {
uni.setStorageSync('isRegistered', false)
}
}).catch(err => {
console.error('请求失败', err)
})
}
}
}
</script>
<style>
page{
background-color: #7CC6EB;
}
.blocked-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
background-color: #7CC6EB;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.blocked-tip {
font-size: 36rpx;
font-weight: 600;
color: #fff;
}
.blocked-sub {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
margin-top: 16rpx;
}
.side-tab {
position: absolute;
right: 0;
width: 160rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 30rpx 0 0 30rpx;
background: linear-gradient(135deg, #FD5401, #FF8A3D);
color: #fff;
font-size: 24rpx;
font-weight: 600;
letter-spacing: 2rpx;
border-left: 6rpx solid rgba(255, 255, 255, 0.65);
box-shadow: 0 6rpx 16rpx rgba(253, 84, 1, 0.35);
}
.side-tab-hover {
opacity: 0.85;
}
</style>
+22
View File
@@ -0,0 +1,22 @@
<template>
<view>
<image src="https://aliyunoss.hn12301.net/learnlaw/xfspyr_jjqd.jpg" style="width: 750rpx;" mode="widthFix"></image>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>
+189
View File
@@ -0,0 +1,189 @@
<template>
<view class="content">
<view class="page-header">
<text class="header-back" @tap="goBack">‹</text>
<text class="header-title">用户隐私协议</text>
</view>
<scroll-view scroll-y class="doc-scroll">
<view class="doc-card">
<view class="doc-title">用户隐私协议</view>
<text class="doc-meta">主办方:湖南一二三零一文化旅游服务有限公司</text>
<text class="doc-meta">生效日期:自发布之日起</text>
<view class="sec">
<text class="sec-title">一、适用范围</text>
<text class="sec-text">本协议适用于"学法送演唱会门票"普法答题抽奖活动(以下简称"本活动")过程中我们对您个人信息的收集、使用与保护。您勾选同意本协议即表示您已阅读、理解并同意本协议全部内容。</text>
</view>
<view class="sec">
<text class="sec-title">二、我们收集的信息</text>
<text class="sec-text">1. 注册信息:姓名、手机号码,用于活动参与资格确认与实名核验,短信验证码(通过阿里云短信服务发送)用于验证手机归属;</text>
<text class="sec-text">2. 微信授权信息:本活动通过微信网页授权(snsapi_base静默授权)获取您的openid,用于关联您的身份与本活动答题、中奖记录,授权范围不包含您的微信昵称、头像等信息;</text>
<text class="sec-text">3. 答题与积分数据:答题记录、对错结果、用时及积分,用于成绩结算、排名与抽奖;</text>
<text class="sec-text">4. 中奖及兑奖信息:中奖记录;演唱会门票实行实名制管理,兑奖时需填写身份证号码,仅用于核验兑奖资格及门票发放。</text>
</view>
<view class="sec">
<text class="sec-title">三、信息的使用</text>
<text class="sec-text">1. 提供答题、积分计算、排名及开奖抽奖服务;</text>
<text class="sec-text">2. 验证用户身份及实名信息真实性,保障活动公平,防范刷量、冒领;</text>
<text class="sec-text">3. 完成兑奖核验,并在中奖时与您联系;</text>
<text class="sec-text">4. 满足法律法规及主管部门的监管要求。</text>
</view>
<view class="sec">
<text class="sec-title">四、信息的存储与保护</text>
<text class="sec-text">1. 您的个人信息存储于活动服务器,接口数据采用加密方式传输,并采取访问控制等安全措施;</text>
<text class="sec-text">2. 我们不会出售您的个人信息,亦不会向与本活动无关的第三方提供;除法律法规明确要求或司法、行政机关依法要求外,任何共享均经审慎评估;</text>
<text class="sec-text">3. 活动结束后,相关数据将依法律规定予以保留或删除。</text>
</view>
<view class="sec">
<text class="sec-title">五、信息的公示</text>
<text class="sec-text">为保障抽奖活动公开透明,中奖名单、积分排行等信息将依活动监管要求进行公示,公示内容将对姓名、手机号码等个人信息作脱敏处理。</text>
</view>
<view class="sec">
<text class="sec-title">六、您的权利</text>
<text class="sec-text">您可通过本协议所列联系方式向我们申请查询、更正、删除您的个人信息,或撤回您的同意。撤回同意不影响撤回前基于同意已进行的信息处理,但可能导致您无法继续参与本活动。</text>
</view>
<view class="sec">
<text class="sec-title">七、未成年人保护</text>
<text class="sec-text">本活动面向具备完全民事行为能力的成年人。未满十八周岁的未成年人请勿在本活动中提交个人信息,如需参与应取得监护人同意并在监护人陪同下进行。</text>
</view>
<view class="sec">
<text class="sec-title">八、协议更新</text>
<text class="sec-text">我们可能根据活动安排及法律法规变化适时更新本协议,更新后的协议将在活动页面公示后生效。若您继续参与活动,即表示您接受更新后的协议。</text>
</view>
<view class="sec">
<text class="sec-title">九、联系方式</text>
<text class="sec-text">如您对本协议或本活动个人信息保护事宜有任何疑问、意见或投诉,请通过以下方式联系我们:</text>
<text class="sec-text">主办方:湖南一二三零一文化旅游服务有限公司</text>
<text class="sec-text">联系电话:0731-88912301</text>
</view>
<view style="height: 40rpx;"></view>
</view>
</scroll-view>
<view class="page-footer">
<button class="agree-btn" @tap="agreeAndBack">已阅读并同意</button>
</view>
</view>
</template>
<script>
export default {
methods: {
goBack() {
uni.navigateBack()
},
agreeAndBack() {
const pages = getCurrentPages()
const prev = pages[pages.length - 2]
if (prev && prev.$vm) prev.$vm.agreed = true
uni.navigateBack()
}
}
}
</script>
<style lang="scss">
page {
background-color: #F5F5F5;
}
.content {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
}
.page-header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
flex-shrink: 0;
}
.header-back {
font-size: 48rpx;
color: #fff;
line-height: 1;
margin-right: 16rpx;
padding: 0 10rpx;
}
.header-title {
font-size: 30rpx;
font-weight: 600;
color: #fff;
flex: 1;
}
.doc-scroll {
flex: 1;
overflow: hidden;
padding-bottom: 20rpx;
}
.doc-card {
background: #fff;
margin: 24rpx;
border-radius: 20rpx;
padding: 36rpx 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.doc-title {
font-size: 36rpx;
font-weight: 700;
color: #333;
text-align: center;
margin-bottom: 16rpx;
}
.doc-meta {
display: block;
font-size: 22rpx;
color: #999;
text-align: center;
line-height: 1.8;
}
.sec {
margin-top: 30rpx;
}
.sec-title {
display: block;
font-size: 28rpx;
font-weight: 700;
color: #FD5401;
margin-bottom: 10rpx;
}
.sec-text {
display: block;
font-size: 26rpx;
color: #555;
line-height: 1.7;
margin-bottom: 6rpx;
}
.page-footer {
flex-shrink: 0;
padding: 20rpx 30rpx 40rpx;
background: #fff;
border-top: 1rpx solid #f0f0f0;
}
.agree-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
color: #fff;
font-size: 32rpx;
font-weight: 600;
border: none;
border-radius: 44rpx;
box-shadow: 0 4rpx 16rpx rgba(253, 84, 1, 0.3);
}
</style>
+224
View File
@@ -0,0 +1,224 @@
<template>
<view class="content">
<view class="page-body">
<view class="loading-box" v-if="detailList.length === 0 && !loadError">
<u-loading mode="circle" size="40"></u-loading>
<text class="loading-text">加载中...</text>
</view>
<view class="loading-box" v-if="loadError">
<text class="error-text">{{ loadError }}</text>
<button class="retry-btn" @tap="fetchDetail">重新加载</button>
</view>
<template v-if="detailList.length > 0">
<view class="detail-item" v-for="(item, i) in detailList" :key="item.id">
<view class="detail-index" :class="item.is_correct ? 'correct' : 'wrong'">
<text class="detail-index-text">{{ i + 1 }}</text>
</view>
<view class="detail-body">
<text class="detail-q">{{ item.tiku_question_content }}</text>
<view class="detail-answer-row">
<text class="detail-label">你的答案:</text>
<text class="detail-value" :class="item.is_correct ? 'text-correct' : 'text-wrong'">{{ item.user_answer }}</text>
</view>
<view class="detail-answer-row" v-if="!item.is_correct">
<text class="detail-label">正确答案:</text>
<text class="detail-value text-correct-answer">{{ item.tiku_correct_answer }}</text>
</view>
<view class="tips" v-if="item.tips">
<text class="tips-label">解析</text>
<text class="tips-text">{{ item.tips }}</text>
</view>
</view>
</view>
</template>
</view>
</view>
</template>
<script>
export default {
data() {
return {
resultId: 0,
detailList: [],
loadError: ''
}
},
onLoad(options) {
this.resultId = options.resultId
this.fetchDetail()
},
methods: {
fetchDetail() {
this.loadError = ''
this.$u.api.getUserHisResultDetail({
resultId: this.resultId
}).then(res => {
if (res.code === 0 && Array.isArray(res.Data) && res.Data.length) {
this.detailList = res.Data
} else {
this.loadError = res.msg || '数据异常'
}
}).catch(err => {
console.error('获取答题详情失败', err)
this.loadError = '网络请求失败'
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #7CC6EB;
}
.content {
position: relative;
height: 100vh;
height: 100dvh;
}
.page-body {
position: relative;
z-index: 1;
height: 100%;
padding: 40rpx 30rpx;
box-sizing: border-box;
overflow-y: auto;
}
.detail-item {
background: rgba(255,255,255,0.95);
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 20rpx;
display: flex;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.detail-index {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 16rpx;
margin-top: 2rpx;
}
.detail-index.correct {
background: linear-gradient(135deg, #52c41a, #73d13d);
box-shadow: 0 2rpx 8rpx rgba(82,196,26,0.3);
}
.detail-index.wrong {
background: linear-gradient(135deg, #f5222d, #ff4d4f);
box-shadow: 0 2rpx 8rpx rgba(245,34,45,0.3);
}
.detail-index-text {
font-size: 20rpx;
font-weight: 700;
color: #fff;
}
.detail-body {
flex: 1;
min-width: 0;
}
.detail-q {
font-size: 26rpx;
color: #333;
line-height: 1.5;
display: block;
margin-bottom: 10rpx;
}
.detail-answer-row {
display: flex;
align-items: baseline;
margin-top: 4rpx;
}
.detail-label {
font-size: 22rpx;
color: #999;
flex-shrink: 0;
}
.detail-value {
font-size: 24rpx;
font-weight: 500;
}
.text-correct {
color: #52c41a;
}
.text-wrong {
color: #f5222d;
}
.text-correct-answer {
color: #52c41a;
}
.tips {
background: rgba(255, 247, 237, 0.95);
border-radius: 12rpx;
padding: 20rpx;
margin-top: 16rpx;
border-left: 6rpx solid #FD5401;
}
.tips-label {
font-size: 24rpx;
font-weight: 600;
color: #FD5401;
display: block;
margin-bottom: 6rpx;
}
.tips-text {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
.loading-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 28rpx;
color: #fff;
margin-top: 20rpx;
}
.error-text {
font-size: 28rpx;
color: #fff;
text-align: center;
display: block;
}
.retry-btn {
margin-top: 30rpx;
width: 240rpx;
height: 72rpx;
line-height: 72rpx;
background: rgba(255, 255, 255, 0.9);
color: #FD5401;
border: none;
border-radius: 36rpx;
font-size: 28rpx;
}
</style>
+192
View File
@@ -0,0 +1,192 @@
<template>
<view class="content">
<view class="page-body">
<view class="loading-box" v-if="records.length === 0 && !loadError">
<u-loading mode="circle" size="40"></u-loading>
<text class="loading-text">加载中...</text>
</view>
<view class="loading-box" v-if="loadError">
<text class="error-text">{{ loadError }}</text>
<button class="retry-btn" @tap="fetchRecords">重新加载</button>
</view>
<template v-if="records.length > 0">
<view class="record-item" v-for="(item, i) in records" :key="item.id" @click="goDetail(item)">
<view class="record-index">
<text class="record-index-text">{{ i + 1 }}</text>
</view>
<view class="record-body">
<view class="record-row">
<text class="record-label">答题时间:</text>
<text class="record-value">{{ item.create_time }}</text>
</view>
<view class="record-row">
<text class="record-label">答对题数:</text>
<text class="record-value">{{ item.correct_question_num }} / {{ item.total_question_num }} 题</text>
</view>
<view class="record-row">
<text class="record-label">答题用时:</text>
<text class="record-value">{{ formatTime(item.use_time) }}</text>
</view>
</view>
</view>
</template>
</view>
</view>
</template>
<script>
export default {
data() {
return {
records: [],
loadError: ''
}
},
onLoad() {
this.fetchRecords()
},
methods: {
fetchRecords() {
this.loadError = ''
const userId = uni.getStorageSync('userId')
if (!userId) {
this.loadError = '暂未登录'
return
}
this.$u.api.getUserHisResultList({
wenjuanId: 1001,
userId: userId
}).then(res => {
if (res.code === 0 && res.Data?.result_list) {
this.records = res.Data.result_list
} else {
this.loadError = res.msg || '数据异常'
}
}).catch(err => {
console.error('获取答题记录失败', err)
this.loadError = '网络请求失败'
})
},
formatTime(sec) {
const m = Math.floor(sec / 60)
const s = sec % 60
return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s
},
goDetail(item) {
uni.navigateTo({
url: '/pages/record-detail/record-detail?resultId=' + item.id
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #7CC6EB;
}
.content {
position: relative;
height: 100vh;
height: 100dvh;
}
.page-body {
position: relative;
z-index: 1;
height: 100%;
padding: 40rpx 30rpx;
box-sizing: border-box;
overflow-y: auto;
}
.record-item {
background: rgba(255,255,255,0.95);
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 20rpx;
display: flex;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.record-index {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 16rpx;
margin-top: 2rpx;
box-shadow: 0 2rpx 8rpx rgba(253,84,1,0.3);
}
.record-index-text {
font-size: 20rpx;
font-weight: 700;
color: #fff;
}
.record-body {
flex: 1;
min-width: 0;
}
.record-row {
display: flex;
align-items: baseline;
margin-top: 6rpx;
}
.record-row:first-child {
margin-top: 0;
}
.record-label {
font-size: 24rpx;
color: #999;
flex-shrink: 0;
}
.record-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.loading-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 28rpx;
color: #fff;
margin-top: 20rpx;
}
.error-text {
font-size: 28rpx;
color: #fff;
text-align: center;
display: block;
}
.retry-btn {
margin-top: 30rpx;
width: 240rpx;
height: 72rpx;
line-height: 72rpx;
background: rgba(255, 255, 255, 0.9);
color: #FD5401;
border: none;
border-radius: 36rpx;
font-size: 28rpx;
}
</style>
+340
View File
@@ -0,0 +1,340 @@
<template>
<view class="register-page">
<view class="header">
<text class="header-title">注册信息</text>
<text class="header-desc">填写资料即可参与答题抽奖</text>
</view>
<view class="form">
<view class="field">
<text class="label">姓名</text>
<view class="input-wrap">
<text class="input-icon">👤</text>
<input class="input" v-model="userName" placeholder="请输入您的姓名" />
</view>
<text class="name-tip">本次演唱会门票实行实名制管理,参与抽奖活动须如实填报本人姓名与手机号码;中奖后须在24小时内完成兑奖操作,兑奖时需补充填报本人身份证号码。</text>
</view>
<view class="field">
<text class="label">手机号</text>
<view class="input-wrap">
<text class="input-icon">📱</text>
<input class="input" v-model="phone" placeholder="请输入手机号" type="number" maxlength="11" />
</view>
</view>
<view class="field">
<text class="label">验证码</text>
<view class="field-row">
<view class="input-wrap code-input-wrap">
<text class="input-icon">✉️</text>
<input class="input" v-model="smsCode" placeholder="请输入验证码" type="number" maxlength="6" />
</view>
<button class="code-btn" @tap="sendCode" :disabled="sending" :class="{ disabled: sending }">
{{ sending ? countdown + 's' : '获取验证码' }}
</button>
</view>
</view>
<view class="agree-row" @tap="agreed = !agreed">
<view class="agree-checkbox" :class="{ checked: agreed }">
<text v-if="agreed" class="agree-check-mark">✓</text>
</view>
<view class="agree-text">
<text>我已阅读并同意</text>
<text class="agree-link" @tap.stop="goPrivacy">《用户隐私协议》</text>
</view>
</view>
<button class="submit-btn" @tap="register" :disabled="submitting" :class="{ disabled: submitting }">
{{ submitting ? '提交中...' : '立即注册' }}
</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
userName: '',
phone: '',
smsCode: '',
sending: false,
countdown: 60,
timer: null,
submitting: false,
agreed: false
}
},
methods: {
goPrivacy() {
uni.navigateTo({ url: '/pages/privacy/privacy' })
},
sendCode() {
if (!/^1\d{10}$/.test(this.phone)) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
this.sending = true
this.$u.api.sendSms(this.phone).then(res => {
if (res.code === 0) {
uni.showToast({ title: '验证码已发送', icon: 'none' })
this.startCountdown()
} else {
uni.showToast({ title: res.msg || '发送失败', icon: 'none' })
this.sending = false
}
}).catch(() => {
uni.showToast({ title: '网络错误', icon: 'none' })
this.sending = false
})
},
startCountdown() {
this.countdown = 60
this.timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(this.timer)
this.timer = null
this.sending = false
}
}, 1000)
},
register() {
if (!this.userName || !this.phone || !this.smsCode) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
if (!this.agreed) {
uni.showToast({ title: '请阅读并同意《用户隐私协议》', icon: 'none' })
return
}
this.submitting = true
const openId = uni.getStorageSync('openId')
this.$u.api.userRegist({
wenjuan_id: 1001,
open_id: openId,
user_name: this.userName,
user_phone: this.phone,
sms_code: this.smsCode,
channel: 1
}).then(res => {
if (res.code === 0) {
uni.setStorageSync('isRegistered', true)
const data = res.Data || {}
const userId = data.user_id || data.uesr_id
if (userId) uni.setStorageSync('userId', userId)
if (data.user_name) uni.setStorageSync('userName', data.user_name)
if (data.user_phone) uni.setStorageSync('userPhone', data.user_phone)
this.syncUserInfo(openId)
uni.showToast({ title: '注册成功' })
setTimeout(() => uni.navigateBack(), 1500)
} else {
uni.showToast({ title: res.msg || '注册失败', icon: 'none' })
}
}).catch(() => {
uni.showToast({ title: '网络错误', icon: 'none' })
}).finally(() => {
this.submitting = false
})
},
syncUserInfo(openId) {
this.$u.api.wxOpenidLogin({ wenjuan_id: 1001, open_id: openId }).then(loginRes => {
if (loginRes.code === 0 && loginRes.Data) {
const data = loginRes.Data
if (data.user_id || data.uesr_id) uni.setStorageSync('userId', data.user_id || data.uesr_id)
if (data.user_name) uni.setStorageSync('userName', data.user_name)
if (data.user_phone) uni.setStorageSync('userPhone', data.user_phone)
}
}).catch(err => {
console.error('注册后同步用户信息失败', err)
})
},
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
}
</script>
<style>
page {
background: linear-gradient(135deg, #FFF8ED 0%, #FFE9C4 100%);
}
.register-page {
padding: 60rpx 40rpx;
min-height: 100vh;
box-sizing: border-box;
}
.header {
margin-bottom: 50rpx;
text-align: center;
}
.header-title {
font-size: 40rpx;
font-weight: bold;
color: #5D533D;
display: block;
margin-bottom: 12rpx;
}
.header-desc {
font-size: 26rpx;
color: #8B7355;
}
.form {
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
padding: 50rpx 40rpx;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.06);
}
.field {
margin-bottom: 36rpx;
}
.label {
font-size: 28rpx;
font-weight: 500;
color: #5D533D;
margin-bottom: 16rpx;
display: block;
padding-left: 8rpx;
}
.input-wrap {
display: flex;
align-items: center;
height: 88rpx;
border: 2rpx solid #F0E4D0;
border-radius: 12rpx;
background: #FEFCF8;
padding: 0 24rpx;
box-sizing: border-box;
}
.input-wrap:focus-within {
border-color: #FD5401;
box-shadow: 0 0 0 4rpx rgba(253, 84, 1, 0.08);
}
.input-icon {
font-size: 32rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.input {
height: 100%;
font-size: 28rpx;
color: #333;
flex: 1;
border: none;
background: transparent;
outline: none;
width: 100%;
box-sizing: border-box;
}
.name-tip {
display: block;
font-size: 22rpx;
color: #E4393C;
line-height: 1.5;
margin-top: 12rpx;
padding: 12rpx 16rpx;
background: #FFEFE9;
border-radius: 12rpx;
}
.field-row {
display: flex;
align-items: center;
}
.code-input-wrap {
flex: 1;
margin-right: 20rpx;
}
.code-btn {
flex-shrink: 0;
height: 88rpx;
line-height: 88rpx;
padding: 0 28rpx;
font-size: 26rpx;
color: #FD5401;
background: #FFF0E6;
border: none;
border-radius: 12rpx;
font-weight: 500;
}
.code-btn.disabled {
color: #B8A898;
background: #F0E8E0;
}
.submit-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
color: #fff;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
margin-top: 20rpx;
box-shadow: 0 6rpx 20rpx rgba(253, 84, 1, 0.3);
}
.submit-btn.disabled {
opacity: 0.5;
}
.agree-row {
display: flex;
align-items: center;
margin: -16rpx 8rpx 28rpx;
}
.agree-checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #CCC;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 12rpx;
box-sizing: border-box;
}
.agree-checkbox.checked {
border-color: #FD5401;
background: #FD5401;
}
.agree-check-mark {
font-size: 22rpx;
color: #fff;
font-weight: 700;
line-height: 1;
}
.agree-text {
display: flex;
align-items: center;
font-size: 24rpx;
color: #8B7355;
}
.agree-link {
color: #FD5401;
}
</style>
+237
View File
@@ -0,0 +1,237 @@
<template>
<view class="content">
<image src="https://aliyunoss.hn12301.net/learnlaw/learn_dtbj.jpg" mode="widthFix" style="width:750rpx;position:absolute;top:0;left:0;z-index:0;"></image>
<view class="page-body">
<view class="correct-card">
<text class="correct-label">答对题数</text>
<text class="correct-num">{{ correctCount }} <text class="correct-total">/ {{ detailList.length }}</text></text>
</view>
<view class="detail-section">
<view class="section-header">
<text class="section-title">答题详情</text>
<text class="section-subtitle">共 {{ detailList.length }} 题,{{ correctCount }} 对 {{ wrongCount }} 错</text>
</view>
<view class="detail-item" v-for="(item, i) in detailList" :key="i">
<view class="detail-index" :class="item.is_correct || item.status === 1 ? 'correct' : 'wrong'">
<text class="detail-index-text">{{ i + 1 }}</text>
</view>
<view class="detail-body">
<text class="detail-q">{{ item.tiku_question_content || '题目' + (i+1) }}</text>
<view class="detail-answer-row">
<text class="detail-label">你的答案:</text>
<text class="detail-value" :class="item.is_correct || item.status === 1 ? 'text-correct' : 'text-wrong'">{{ item.user_answer || '未作答' }}</text>
</view>
<view class="detail-answer-row" v-if="!(item.is_correct || item.status === 1)">
<text class="detail-label">正确答案:</text>
<text class="detail-value text-correct-answer">{{ item.tiku_correct_answer || '-' }}</text>
</view>
</view>
</view>
</view>
<button class="back-btn" @tap="goBack">返回首页</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
correctCount: 0,
wrongCount: 0,
detailList: []
}
},
onLoad() {
try {
const data = uni.getStorageSync('resultData') || {}
if (data.resultDetailList && data.resultDetailList.length) {
this.detailList = data.resultDetailList
this.correctCount = data.resultDetailList.filter(r => r.is_correct === true || r.is_correct === 1 || r.status === 1).length
this.wrongCount = data.resultDetailList.length - this.correctCount
}
} catch(e) {
console.error('解析结果数据失败', e)
}
},
methods: {
goBack() {
uni.reLaunch({ url: '/pages/index/index' })
}
}
}
</script>
<style lang="scss">
page {
background-color: #B3E9F3;
}
.content {
position: relative;
height: 100vh;
height: 100dvh;
}
.page-body {
position: relative;
z-index: 1;
height: 100%;
padding: 40rpx 30rpx;
box-sizing: border-box;
overflow-y: auto;
}
.correct-card {
background: linear-gradient(135deg, #FD5401, #FF7A3D);
border-radius: 20rpx;
padding: 28rpx 24rpx;
text-align: center;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(253, 84, 1, 0.2);
}
.correct-label {
font-size: 24rpx;
color: rgba(255,255,255,0.85);
display: block;
}
.correct-num {
font-size: 72rpx;
font-weight: 800;
color: #FFF;
display: block;
line-height: 1.2;
margin-top: 6rpx;
}
.correct-total {
font-size: 32rpx;
font-weight: 500;
color: rgba(255,255,255,0.8);
}
.detail-section {
background: rgba(255,255,255,0.95);
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #333;
}
.section-subtitle {
font-size: 24rpx;
color: #bbb;
}
.detail-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-index {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 16rpx;
margin-top: 2rpx;
}
.detail-index.correct {
background: linear-gradient(135deg, #52c41a, #73d13d);
box-shadow: 0 2rpx 8rpx rgba(82,196,26,0.3);
}
.detail-index.wrong {
background: linear-gradient(135deg, #f5222d, #ff4d4f);
box-shadow: 0 2rpx 8rpx rgba(245,34,45,0.3);
}
.detail-index-text {
font-size: 20rpx;
font-weight: 700;
color: #fff;
}
.detail-body {
flex: 1;
min-width: 0;
}
.detail-q {
font-size: 26rpx;
color: #333;
line-height: 1.5;
display: block;
margin-bottom: 10rpx;
}
.detail-answer-row {
display: flex;
align-items: baseline;
margin-top: 4rpx;
}
.detail-label {
font-size: 22rpx;
color: #999;
flex-shrink: 0;
}
.detail-value {
font-size: 24rpx;
font-weight: 500;
}
.text-correct {
color: #52c41a;
}
.text-wrong {
color: #f5222d;
}
.text-correct-answer {
color: #52c41a;
}
.back-btn {
width: 690rpx;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
color: #fff;
font-size: 32rpx;
font-weight: 600;
border: none;
border-radius: 44rpx;
box-shadow: 0 4rpx 16rpx rgba(253,84,1,0.3);
}
</style>
+220
View File
@@ -0,0 +1,220 @@
<template>
<view class="content">
<view class="page-header">
<text class="header-back" @tap="goBack">‹</text>
<text class="header-title">活动规则</text>
</view>
<view class="preview-wrap">
<view class="loading-box" v-if="loading">
<u-loading mode="circle" size="40"></u-loading>
<text class="loading-text">内容加载中...</text>
</view>
<scroll-view class="doc-scroll" scroll-y v-if="!loading && !error">
<rich-text class="doc-content" :nodes="htmlContent"></rich-text>
<view style="height:30rpx;"></view>
</scroll-view>
<view class="error-box" v-if="error">
<text class="error-text">{{ error }}</text>
<button class="preview-btn" @tap="loadDoc">重新加载</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
docUrl: 'https://goldnar.hntgov.net/img/llpreheat_rules.docx',
loading: false,
error: '',
htmlContent: ''
}
},
onReady() {
this.loadDoc()
},
methods: {
goBack() {
uni.navigateBack()
},
loadDoc() {
this.loading = true
this.error = ''
this.htmlContent = ''
this.ensureMammoth().then(() => {
return this.fetchDocx(this.docUrl)
}).then(arrayBuffer => {
return window.mammoth.convertToHtml({
arrayBuffer
})
}).then(result => {
this.htmlContent = result.value || ''
this.loading = false
}).catch(err => {
console.error('加载文档失败', err)
this.loading = false
this.error = '文档加载失败,请重试'
})
},
ensureMammoth() {
if (window.mammoth) return Promise.resolve()
return new Promise((resolve, reject) => {
const urls = [
'/static/mammoth.browser.min.js',
'https://unpkg.com/mammoth@1.6.0/mammoth.browser.min.js'
]
let i = 0
const loadNext = () => {
if (i >= urls.length) {
reject(new Error('解析组件加载失败'))
return
}
const s = document.createElement('script')
s.src = urls[i++]
s.onload = () => window.mammoth ? resolve() : loadNext()
s.onerror = loadNext
document.head.appendChild(s)
}
loadNext()
})
},
fetchDocx(url) {
return new Promise((resolve, reject) => {
if (typeof fetch !== 'undefined') {
fetch(url).then(res => {
if (!res.ok) throw new Error('HTTP ' + res.status)
return res.arrayBuffer()
}).then(resolve).catch(() => this.downloadDocx(url).then(resolve).catch(reject))
return
}
this.downloadDocx(url).then(resolve).catch(reject)
})
},
downloadDocx(url) {
return new Promise((resolve, reject) => {
uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error('下载失败'))
return
}
uni.getFileSystemManager().readFile({
filePath: res.tempFilePath,
success: (r) => resolve(r.data),
fail: () => {
if (res.tempFilePath && typeof fetch !== 'undefined') {
fetch(res.tempFilePath).then(r => r.arrayBuffer())
.then(resolve).catch(reject)
} else {
reject(new Error('读取文件失败'))
}
}
})
},
fail: reject
})
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #F8F8F8;
}
.content {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
}
.page-header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
flex-shrink: 0;
}
.header-back {
font-size: 48rpx;
color: #fff;
line-height: 1;
margin-right: 16rpx;
padding: 0 10rpx;
}
.header-title {
font-size: 30rpx;
font-weight: 600;
color: #fff;
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.preview-wrap {
flex: 1;
background: #fff;
overflow: hidden;
}
.loading-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.loading-text {
font-size: 26rpx;
color: #999;
margin-top: 20rpx;
}
.error-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.error-text {
font-size: 26rpx;
color: #999;
margin-bottom: 30rpx;
}
.preview-btn {
width: 300rpx;
height: 76rpx;
line-height: 76rpx;
background: linear-gradient(135deg, #FD5401, #FF7A3D);
color: #fff;
font-size: 28rpx;
font-weight: 600;
border: none;
border-radius: 38rpx;
}
.doc-scroll {
height: 100%;
padding: 20rpx 30rpx;
box-sizing: border-box;
}
.doc-content {
font-size: 28rpx;
color: #333;
line-height: 1.8;
}
</style>