feat: 高考志愿与定价分销链路更新(管理端/API/小程序)
- 新增高考志愿核心服务、模型、控制器与迁移脚本 - 同步管理端定价/分销/设置与小程序入口、历史、测试选择和支付逻辑 - 补充相关开发文档 Made-with: Cursor
This commit is contained in:
387
miniprogram/pages/gaokao/form.js
Normal file
387
miniprogram/pages/gaokao/form.js
Normal file
@@ -0,0 +1,387 @@
|
||||
const gaokaoApi = require('../../utils/gaokao')
|
||||
|
||||
/** 科类/选科备选项:首项为占位,不可作为有效保存值 */
|
||||
const SUBJECT_PLACEHOLDER = '请选择科类/选科'
|
||||
const SUBJECT_CHOICES = [
|
||||
SUBJECT_PLACEHOLDER,
|
||||
'文科',
|
||||
'理科',
|
||||
'物化生',
|
||||
'物化地',
|
||||
'物化政',
|
||||
'物生地',
|
||||
'物生政',
|
||||
'物政地',
|
||||
'化生地',
|
||||
'化政地',
|
||||
'生政地',
|
||||
'史政地',
|
||||
'史化政',
|
||||
'史化生',
|
||||
'物化技',
|
||||
'物生技',
|
||||
'史地技',
|
||||
'艺术类(物理向)',
|
||||
'艺术类(历史向)',
|
||||
'体育类(物理向)',
|
||||
'体育类(历史向)',
|
||||
'中职/对口/单招'
|
||||
]
|
||||
|
||||
/** 在意向专业中展示/保存的选项:首项表示不填 */
|
||||
const MAJOR_PLACEHOLDER = '(可选)不填'
|
||||
const MAJOR_CHOICES = [
|
||||
MAJOR_PLACEHOLDER,
|
||||
'哲学',
|
||||
'经济学 / 金融',
|
||||
'法学',
|
||||
'教育学 / 师范',
|
||||
'文学',
|
||||
'外语 / 新传',
|
||||
'理学',
|
||||
'工学 / 工程',
|
||||
'计算机 / 软件 / 人工智能',
|
||||
'电子 / 通信 / 信息',
|
||||
'医学 / 临床 / 公卫 / 中医',
|
||||
'农学 / 林学 / 生科',
|
||||
'历史学',
|
||||
'管理学 / 商学',
|
||||
'艺术学',
|
||||
'交叉学科 / 暂未确定'
|
||||
]
|
||||
|
||||
/** 将微信 region 结果格式化为只到「市」的文案(不含区) */
|
||||
function regionToCityText(v) {
|
||||
if (!v || !v.length) return ''
|
||||
const p = (v[0] || '').trim()
|
||||
const c = (v[1] || '').trim()
|
||||
if (p && c) return p + ' ' + c
|
||||
return p || c
|
||||
}
|
||||
|
||||
/** 展示用:只取省+市,直辖市 p===c 时只显示一个 */
|
||||
function formatRegionLine(v) {
|
||||
if (!v || !v.length) return ''
|
||||
const p = (v[0] || '').trim()
|
||||
const c = (v[1] || '').trim()
|
||||
if (!p && !c) return ''
|
||||
if (c && c !== p) return p + ' · ' + c
|
||||
return p || c
|
||||
}
|
||||
|
||||
/** 从 bindchange 取 value(部分环境 detail 结构异常时兜底) */
|
||||
function regionValueFromEvent(e) {
|
||||
const d = (e && e.detail) || {}
|
||||
let v = d.value
|
||||
if (Array.isArray(v) && v.length) return v
|
||||
return []
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
form: {
|
||||
name: '',
|
||||
province: '',
|
||||
/** 高考生源地所在市(与省同一套 region 选择结果,用于展示/扩展) */
|
||||
city: '',
|
||||
streamOrSubjects: '',
|
||||
/** 与微信 region picker 联动;level=city 时为 [省,市];老数据或省级可能为 3 项 */
|
||||
region: [],
|
||||
/**
|
||||
* 意向地区:仅省为 [p];老数据 可能 为 [p,c];与 preferredRegions 对应
|
||||
*/
|
||||
preferredRegion: [],
|
||||
estimatedScore: '',
|
||||
scoreText: '',
|
||||
wishListText: '',
|
||||
preferredRegions: '',
|
||||
preferredFields: ''
|
||||
},
|
||||
/** 高考生源:与 level=city 一致,为 [省,市] */
|
||||
regionPickerValue: [],
|
||||
/** 意向:只省 */
|
||||
intendedProvPicker: [],
|
||||
/** 意向:省+市 */
|
||||
intendedCityPicker: [],
|
||||
/** 0=只到省份 1=到省+市 */
|
||||
intendedModeOptions: ['只到省份', '到省+市'],
|
||||
intendedModeIndex: 1,
|
||||
intendedModeLine: '到省+市',
|
||||
/** 地区展示文案(不依赖 wxml 里对 length 的比较,避免真机/模拟器不渲染) */
|
||||
regionLine: '',
|
||||
intendedRegionLine: '',
|
||||
subjectOptions: SUBJECT_CHOICES,
|
||||
streamOrSubjectsIndex: 0,
|
||||
majorOptions: MAJOR_CHOICES,
|
||||
preferredFieldsIndex: 0,
|
||||
saving: false
|
||||
},
|
||||
|
||||
/** 每次页面展示拉取(含从上级页返回),避免栈内页面不触发 onLoad 时看不到已保存内容 */
|
||||
onShow() {
|
||||
this.loadFormFromServer()
|
||||
},
|
||||
|
||||
loadFormFromServer() {
|
||||
gaokaoApi
|
||||
.getForm()
|
||||
.then((res) => {
|
||||
const form = res.form || {}
|
||||
const pr = (() => {
|
||||
const a = form.preferredRegion
|
||||
if (!Array.isArray(a) || a.length < 1) return []
|
||||
return a
|
||||
})()
|
||||
const baseForm = {
|
||||
...this.data.form,
|
||||
...form,
|
||||
city: (form.city != null && form.city !== '') ? String(form.city) : (this.data.form.city || ''),
|
||||
region: (() => {
|
||||
const a = form.region
|
||||
if (!Array.isArray(a) || a.length < 2) return []
|
||||
return a
|
||||
})(),
|
||||
preferredRegion: pr,
|
||||
preferredRegions: (() => {
|
||||
if (pr && pr.length >= 2) {
|
||||
return regionToCityText([pr[0], pr[1]])
|
||||
}
|
||||
if (pr && pr.length === 1) {
|
||||
return (pr[0] || '').trim()
|
||||
}
|
||||
return form.preferredRegions != null ? String(form.preferredRegions) : ''
|
||||
})(),
|
||||
estimatedScore: form.estimatedScore != null ? String(form.estimatedScore) : ''
|
||||
}
|
||||
const rpv = baseForm.region
|
||||
if (rpv && rpv.length >= 2) {
|
||||
baseForm.province = rpv[0] || baseForm.province
|
||||
baseForm.city = rpv[1] || baseForm.city
|
||||
}
|
||||
const rForPicker = (() => {
|
||||
if (Array.isArray(baseForm.region) && baseForm.region.length >= 2) {
|
||||
const a0 = (baseForm.region[0] || '').trim()
|
||||
const a1 = (baseForm.region[1] || a0).trim()
|
||||
return [a0, a1]
|
||||
}
|
||||
if (baseForm.province) {
|
||||
const a0 = String(baseForm.province).trim()
|
||||
const a1 = (baseForm.city && String(baseForm.city).trim()) || a0
|
||||
return [a0, a1]
|
||||
}
|
||||
return []
|
||||
})()
|
||||
const regionLine = rForPicker.length
|
||||
? formatRegionLine(rForPicker)
|
||||
: ''
|
||||
const intendedModeOptions = this.data.intendedModeOptions
|
||||
const modeIdx = (() => {
|
||||
if (pr && pr.length >= 2) return 1
|
||||
if (pr && pr.length === 1) return 0
|
||||
return 1
|
||||
})()
|
||||
const intendedModeLine = intendedModeOptions[modeIdx] || '到省+市'
|
||||
const intendedProvPicker = (() => {
|
||||
if (pr && pr.length >= 1) return [(String(pr[0] || '')).trim()]
|
||||
return []
|
||||
})()
|
||||
const intendedCityPicker = (() => {
|
||||
if (pr && pr.length >= 2) {
|
||||
return [
|
||||
(String(pr[0] || '')).trim(),
|
||||
(String(pr[1] || '')).trim()
|
||||
]
|
||||
}
|
||||
if (pr && pr.length === 1 && modeIdx === 1) {
|
||||
const p0 = (String(pr[0] || '')).trim()
|
||||
return p0 ? [p0, p0] : []
|
||||
}
|
||||
return []
|
||||
})()
|
||||
let intendedRegionLine = (() => {
|
||||
if (pr && pr.length >= 2) {
|
||||
return formatRegionLine([(pr[0] || '').trim(), (pr[1] || '').trim()])
|
||||
}
|
||||
if (pr && pr.length === 1) {
|
||||
return (pr[0] || '').trim()
|
||||
}
|
||||
if (form.preferredRegions) return String(form.preferredRegions)
|
||||
return ''
|
||||
})()
|
||||
const stream = baseForm.streamOrSubjects || ''
|
||||
const opts = (() => {
|
||||
if (stream && SUBJECT_CHOICES.indexOf(stream) < 0) {
|
||||
return [SUBJECT_CHOICES[0], stream, ...SUBJECT_CHOICES.slice(1)]
|
||||
}
|
||||
return SUBJECT_CHOICES
|
||||
})()
|
||||
let sIdx = opts.indexOf(stream)
|
||||
if (sIdx < 0) sIdx = 0
|
||||
const pField = baseForm.preferredFields || ''
|
||||
const mOpts = (() => {
|
||||
if (pField && MAJOR_CHOICES.indexOf(pField) < 0) {
|
||||
return [MAJOR_CHOICES[0], pField, ...MAJOR_CHOICES.slice(1)]
|
||||
}
|
||||
return MAJOR_CHOICES
|
||||
})()
|
||||
let mIdx = mOpts.indexOf(pField)
|
||||
if (mIdx < 0) mIdx = 0
|
||||
this.setData({
|
||||
form: baseForm,
|
||||
regionPickerValue: rForPicker,
|
||||
regionLine,
|
||||
intendedModeIndex: modeIdx,
|
||||
intendedModeLine,
|
||||
intendedProvPicker,
|
||||
intendedCityPicker,
|
||||
intendedRegionLine,
|
||||
subjectOptions: opts,
|
||||
streamOrSubjectsIndex: sIdx,
|
||||
majorOptions: mOpts,
|
||||
preferredFieldsIndex: mIdx
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
wx.showToast({ title: '加载表单失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onInput(e) {
|
||||
const key = e.currentTarget.dataset.key
|
||||
this.setData({ [`form.${key}`]: e.detail.value })
|
||||
},
|
||||
|
||||
onRegionChange(e) {
|
||||
const v = regionValueFromEvent(e)
|
||||
if (!v || !v.length) return
|
||||
const p = (v[0] || '').trim()
|
||||
if (!p) return
|
||||
const c2 = v[1] != null && v[1] !== '' ? String(v[1]).trim() : ''
|
||||
const pair = [p, c2 || p]
|
||||
this.setData({
|
||||
regionPickerValue: pair,
|
||||
regionLine: formatRegionLine(pair),
|
||||
'form.province': p,
|
||||
'form.city': c2 || p,
|
||||
'form.region': pair
|
||||
})
|
||||
},
|
||||
|
||||
onSubjectChange(e) {
|
||||
const idx = parseInt(e.detail.value, 10) || 0
|
||||
const opts = this.data.subjectOptions
|
||||
const raw = opts[idx] || ''
|
||||
const val =
|
||||
raw && raw !== SUBJECT_PLACEHOLDER
|
||||
? raw
|
||||
: ''
|
||||
this.setData({
|
||||
streamOrSubjectsIndex: idx,
|
||||
'form.streamOrSubjects': val
|
||||
})
|
||||
},
|
||||
|
||||
onIntendedModeChange(e) {
|
||||
const idx = parseInt(e.detail.value, 10) || 0
|
||||
const opts = this.data.intendedModeOptions
|
||||
const pr = this.data.form.preferredRegion
|
||||
const arr = Array.isArray(pr) ? pr : []
|
||||
if (idx === 0) {
|
||||
const p = arr[0] ? String(arr[0]).trim() : ''
|
||||
const next = p ? [p] : []
|
||||
this.setData({
|
||||
intendedModeIndex: idx,
|
||||
intendedModeLine: opts[idx],
|
||||
intendedProvPicker: next,
|
||||
intendedRegionLine: p,
|
||||
'form.preferredRegion': next,
|
||||
'form.preferredRegions': p
|
||||
})
|
||||
return
|
||||
}
|
||||
let cityPick = []
|
||||
if (arr.length >= 2) {
|
||||
cityPick = [String(arr[0] || '').trim(), String(arr[1] || '').trim()]
|
||||
} else if (arr.length === 1) {
|
||||
const p0 = String(arr[0] || '').trim()
|
||||
cityPick = p0 ? [p0, p0] : []
|
||||
}
|
||||
const hasPair = arr.length >= 2
|
||||
this.setData({
|
||||
intendedModeIndex: idx,
|
||||
intendedModeLine: opts[idx],
|
||||
intendedCityPicker: cityPick
|
||||
})
|
||||
if (hasPair) {
|
||||
const line = formatRegionLine(cityPick)
|
||||
this.setData({
|
||||
intendedRegionLine: line,
|
||||
'form.preferredRegion': [cityPick[0], cityPick[1]],
|
||||
'form.preferredRegions': regionToCityText(cityPick)
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
intendedRegionLine: arr[0] ? String(arr[0]).trim() : '',
|
||||
'form.preferredRegion': arr
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onIntendedRegionProv(e) {
|
||||
const v = regionValueFromEvent(e)
|
||||
if (!v || !v.length) return
|
||||
const p = (v[0] || '').trim()
|
||||
if (!p) return
|
||||
this.setData({
|
||||
intendedProvPicker: [p],
|
||||
intendedRegionLine: p,
|
||||
'form.preferredRegion': [p],
|
||||
'form.preferredRegions': p
|
||||
})
|
||||
},
|
||||
|
||||
onIntendedRegionCity(e) {
|
||||
const v = regionValueFromEvent(e)
|
||||
if (!v || !v.length) return
|
||||
const p = (v[0] || '').trim()
|
||||
if (!p) return
|
||||
const c2 = v[1] != null && v[1] !== '' ? String(v[1]).trim() : ''
|
||||
const pair = [p, c2 || p]
|
||||
this.setData({
|
||||
intendedCityPicker: pair,
|
||||
intendedRegionLine: formatRegionLine(pair),
|
||||
'form.preferredRegion': pair,
|
||||
'form.preferredRegions': regionToCityText(pair)
|
||||
})
|
||||
},
|
||||
|
||||
onPreferredFieldChange(e) {
|
||||
const idx = parseInt(e.detail.value, 10) || 0
|
||||
const opts = this.data.majorOptions
|
||||
const raw = opts[idx] || ''
|
||||
const val = raw && raw !== MAJOR_PLACEHOLDER ? raw : ''
|
||||
this.setData({
|
||||
preferredFieldsIndex: idx,
|
||||
'form.preferredFields': val
|
||||
})
|
||||
},
|
||||
|
||||
onSave() {
|
||||
const f = this.data.form
|
||||
if (!f.name || !f.province || !f.streamOrSubjects) {
|
||||
wx.showToast({ title: '请选择姓名、所在地区与科类/选科', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ saving: true })
|
||||
gaokaoApi.saveForm({
|
||||
...f,
|
||||
estimatedScore: f.estimatedScore ? Number(f.estimatedScore) : null
|
||||
}).then(() => {
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
}).catch((e) => {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}).finally(() => this.setData({ saving: false }))
|
||||
}
|
||||
})
|
||||
|
||||
4
miniprogram/pages/gaokao/form.json
Normal file
4
miniprogram/pages/gaokao/form.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "高考信息表单"
|
||||
}
|
||||
|
||||
72
miniprogram/pages/gaokao/form.wxml
Normal file
72
miniprogram/pages/gaokao/form.wxml
Normal file
@@ -0,0 +1,72 @@
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="item">
|
||||
<view class="label">姓名</view>
|
||||
<input class="input" data-key="name" value="{{form.name}}" bindinput="onInput" placeholder="请输入姓名" />
|
||||
</view>
|
||||
<view class="item">
|
||||
<view class="label">所在地区</view>
|
||||
<picker mode="region" level="city" value="{{regionPickerValue}}" bindchange="onRegionChange">
|
||||
<view class="input input-picker {{!regionLine ? 'input-picker-empty' : ''}}">
|
||||
<text>{{regionLine ? regionLine : '点选高考生源省、市'}}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="item">
|
||||
<view class="label">科类/选科</view>
|
||||
<picker mode="selector" range="{{subjectOptions}}" value="{{streamOrSubjectsIndex}}" bindchange="onSubjectChange">
|
||||
<view class="input input-picker">
|
||||
<text wx:if="{{form.streamOrSubjects}}">{{form.streamOrSubjects}}</text>
|
||||
<text wx:else class="ph">请点选科类或选科组合</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="item">
|
||||
<view class="label">估分</view>
|
||||
<input class="input" type="number" data-key="estimatedScore" value="{{form.estimatedScore}}" bindinput="onInput" placeholder="选填" />
|
||||
</view>
|
||||
<view class="item">
|
||||
<view class="label">成绩说明</view>
|
||||
<textarea class="textarea" data-key="scoreText" value="{{form.scoreText}}" bindinput="onInput" placeholder="可选,填写模考成绩说明"></textarea>
|
||||
</view>
|
||||
<view class="item">
|
||||
<view class="label">志愿草表</view>
|
||||
<textarea class="textarea" data-key="wishListText" value="{{form.wishListText}}" bindinput="onInput" placeholder="可选,填写当前志愿方案"></textarea>
|
||||
</view>
|
||||
<view class="item item-intended">
|
||||
<view class="label">意向地区</view>
|
||||
<picker class="intended-mode-picker" mode="selector" range="{{intendedModeOptions}}" value="{{intendedModeIndex}}" bindchange="onIntendedModeChange">
|
||||
<view class="input input-picker input-sub">
|
||||
<text>粒度:{{intendedModeLine}}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="intended-region-pickers">
|
||||
<block wx:if="{{intendedModeIndex == 0}}">
|
||||
<picker mode="region" level="province" value="{{intendedProvPicker}}" bindchange="onIntendedRegionProv">
|
||||
<view class="input input-picker {{!intendedRegionLine ? 'input-picker-empty' : ''}}">
|
||||
<text>{{intendedRegionLine ? intendedRegionLine : '点选省份(可只到省)'}}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<picker mode="region" level="city" value="{{intendedCityPicker}}" bindchange="onIntendedRegionCity">
|
||||
<view class="input input-picker {{!intendedRegionLine ? 'input-picker-empty' : ''}}">
|
||||
<text>{{intendedRegionLine ? intendedRegionLine : '点选省、市(含直辖市)'}}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item">
|
||||
<view class="label">意向专业</view>
|
||||
<picker mode="selector" range="{{majorOptions}}" value="{{preferredFieldsIndex}}" bindchange="onPreferredFieldChange">
|
||||
<view class="input input-picker">
|
||||
<text wx:if="{{form.preferredFields}}">{{form.preferredFields}}</text>
|
||||
<text wx:else class="ph">可选,点选专业方向</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<button class="save-btn" loading="{{saving}}" bindtap="onSave">保存表单</button>
|
||||
</view>
|
||||
|
||||
86
miniprogram/pages/gaokao/form.wxss
Normal file
86
miniprogram/pages/gaokao/form.wxss
Normal file
@@ -0,0 +1,86 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fc;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 74rpx;
|
||||
border: 1rpx solid #e6e8ee;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.input-picker {
|
||||
line-height: 72rpx;
|
||||
box-sizing: border-box;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* 避免内联 picker 与下一行表单项左右串版(如市名跑到「科类」旁) */
|
||||
.item > picker,
|
||||
.item picker,
|
||||
.intended-region-pickers picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-picker-empty text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.input-picker .ph {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
min-height: 140rpx;
|
||||
border: 1rpx solid #e6e8ee;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
font-size: 26rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 意向地区:先选粒度,再选省或省+市;第二行与第一行间留白 */
|
||||
.intended-mode-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-intended .intended-region-pickers {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.input-sub text {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
margin-top: 24rpx;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
border-radius: 999rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
107
miniprogram/pages/gaokao/index.js
Normal file
107
miniprogram/pages/gaokao/index.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const gaokaoApi = require('../../utils/gaokao')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
tasks: {
|
||||
mbti: { code: 'mbti', status: 'todo', resultText: '', testResultId: 0, id: 0, testType: 'mbti', typeName: 'MBTI性格', emoji: '🧠', testTime: '' },
|
||||
pdp: { code: 'pdp', status: 'todo', resultText: '', testResultId: 0, id: 0, testType: 'pdp', typeName: 'PDP行为', emoji: '🦁', testTime: '' },
|
||||
disc: { code: 'disc', status: 'todo', resultText: '', testResultId: 0, id: 0, testType: 'disc', typeName: 'DISC测评', emoji: '📊', testTime: '' },
|
||||
face: { code: 'face', status: 'todo', resultText: '', testResultId: 0, id: 0, testType: 'ai', typeName: '拍照面相', emoji: '📷', testTime: '', recordTestType: 'ai' },
|
||||
form: { code: 'form', status: 'todo', resultText: '', testResultId: 0, id: 0, testType: 'form', typeName: '高考信息表单', emoji: '📝', testTime: '' }
|
||||
},
|
||||
canAnalyze: false,
|
||||
missingItems: [],
|
||||
analyzing: false
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.refreshStatus()
|
||||
},
|
||||
|
||||
refreshStatus() {
|
||||
this.setData({ loading: true })
|
||||
gaokaoApi
|
||||
.getTaskStatus({ scene: 'gaokao_hub' })
|
||||
.then((data) => {
|
||||
this.setData({
|
||||
tasks: data.tasks || this.data.tasks,
|
||||
canAnalyze: !!data.canAnalyze,
|
||||
missingItems: data.missingItems || []
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
wx.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => this.setData({ loading: false }))
|
||||
},
|
||||
|
||||
goTask(e) {
|
||||
const code = e.currentTarget.dataset.code
|
||||
const resultId = Number(e.currentTarget.dataset.resultId || 0)
|
||||
const done = (e.currentTarget.dataset.status || '') === 'done'
|
||||
if (code === 'mbti') {
|
||||
if (done && resultId > 0) {
|
||||
wx.navigateTo({ url: `/pages/result/mbti?id=${resultId}&type=mbti` })
|
||||
} else {
|
||||
wx.navigateTo({ url: '/pages/test/mbti' })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (code === 'pdp') {
|
||||
if (done && resultId > 0) {
|
||||
wx.navigateTo({ url: `/pages/result/pdp?id=${resultId}&type=pdp` })
|
||||
} else {
|
||||
wx.navigateTo({ url: '/pages/test/pdp' })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (code === 'disc') {
|
||||
if (done && resultId > 0) {
|
||||
wx.navigateTo({ url: `/pages/result/disc?id=${resultId}&type=disc` })
|
||||
} else {
|
||||
wx.navigateTo({ url: '/pages/test/disc' })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (code === 'face') {
|
||||
const recType = String(e.currentTarget.dataset.recordType || 'ai').toLowerCase()
|
||||
const typeParam = recType === 'face' ? 'face' : 'ai'
|
||||
if (done && resultId > 0) {
|
||||
wx.navigateTo({ url: `/pages/index/result?id=${resultId}&type=${typeParam}` })
|
||||
} else {
|
||||
wx.switchTab({ url: '/pages/index/camera' })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (code === 'form') {
|
||||
wx.navigateTo({ url: '/pages/gaokao/form' })
|
||||
}
|
||||
},
|
||||
|
||||
onAnalyzeTap() {
|
||||
if (!this.data.canAnalyze) {
|
||||
const nameMap = {
|
||||
mbti: 'MBTI测试',
|
||||
pdp: 'PDP测试',
|
||||
disc: 'DISC测试',
|
||||
face: '拍照面相',
|
||||
form: '高考信息表单'
|
||||
}
|
||||
const msg = (this.data.missingItems || []).map((k) => nameMap[k] || k).join('、')
|
||||
wx.showToast({
|
||||
title: msg ? `请先完成:${msg}` : '请先完成全部任务',
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setData({ analyzing: true })
|
||||
wx.navigateTo({
|
||||
url: '/pages/gaokao/report?pendingAnalyze=1',
|
||||
complete: () => {
|
||||
this.setData({ analyzing: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/gaokao/index.json
Normal file
4
miniprogram/pages/gaokao/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "高考志愿"
|
||||
}
|
||||
|
||||
90
miniprogram/pages/gaokao/index.wxml
Normal file
90
miniprogram/pages/gaokao/index.wxml
Normal file
@@ -0,0 +1,90 @@
|
||||
<view class="page">
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<view class="title">高考志愿任务中心</view>
|
||||
<view class="desc">完成 MBTI、PDP、DISC、拍照面相与高考信息表单后,即可生成综合分析;若定价非 0,请在报告页解锁全文(与问卷类测评一致)。</view>
|
||||
</view>
|
||||
|
||||
<view class="task-list">
|
||||
<view class="task-card" data-code="mbti" data-status="{{tasks.mbti.status}}" data-result-id="{{tasks.mbti.testResultId}}" bindtap="goTask">
|
||||
<view class="left">
|
||||
<view class="task-title-row">
|
||||
<text class="task-emoji">{{tasks.mbti.emoji}}</text>
|
||||
<text class="name">{{tasks.mbti.typeName}}</text>
|
||||
</view>
|
||||
<view class="result result--value" wx:if="{{tasks.mbti.status==='done'}}">{{tasks.mbti.resultText || '已完成'}}</view>
|
||||
<view class="result todo" wx:else>未完成,请去测试</view>
|
||||
<view class="task-time" wx:if="{{tasks.mbti.status==='done' && tasks.mbti.testTime}}">{{tasks.mbti.testTime}}</view>
|
||||
</view>
|
||||
<view class="status {{tasks.mbti.status==='done'?'done':''}}">{{tasks.mbti.status==='done'?'查看结果':'去完成'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="task-card" data-code="pdp" data-status="{{tasks.pdp.status}}" data-result-id="{{tasks.pdp.testResultId}}" bindtap="goTask">
|
||||
<view class="left">
|
||||
<view class="task-title-row">
|
||||
<text class="task-emoji">{{tasks.pdp.emoji}}</text>
|
||||
<text class="name">{{tasks.pdp.typeName}}</text>
|
||||
</view>
|
||||
<view class="result result--value" wx:if="{{tasks.pdp.status==='done'}}">{{tasks.pdp.resultText || '已完成'}}</view>
|
||||
<view class="result todo" wx:else>未完成,请去测试</view>
|
||||
<view class="task-time" wx:if="{{tasks.pdp.status==='done' && tasks.pdp.testTime}}">{{tasks.pdp.testTime}}</view>
|
||||
</view>
|
||||
<view class="status {{tasks.pdp.status==='done'?'done':''}}">{{tasks.pdp.status==='done'?'查看结果':'去完成'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="task-card" data-code="disc" data-status="{{tasks.disc.status}}" data-result-id="{{tasks.disc.testResultId}}" bindtap="goTask">
|
||||
<view class="left">
|
||||
<view class="task-title-row">
|
||||
<text class="task-emoji">{{tasks.disc.emoji}}</text>
|
||||
<text class="name">{{tasks.disc.typeName}}</text>
|
||||
</view>
|
||||
<view class="result result--value" wx:if="{{tasks.disc.status==='done'}}">{{tasks.disc.resultText || '已完成'}}</view>
|
||||
<view class="result todo" wx:else>未完成,请去测试</view>
|
||||
<view class="task-time" wx:if="{{tasks.disc.status==='done' && tasks.disc.testTime}}">{{tasks.disc.testTime}}</view>
|
||||
</view>
|
||||
<view class="status {{tasks.disc.status==='done'?'done':''}}">{{tasks.disc.status==='done'?'查看结果':'去完成'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="task-card" data-code="face" data-status="{{tasks.face.status}}" data-result-id="{{tasks.face.testResultId}}" data-record-type="{{tasks.face.recordTestType}}" bindtap="goTask">
|
||||
<view class="left">
|
||||
<view class="task-title-row">
|
||||
<text class="task-emoji">{{tasks.face.emoji}}</text>
|
||||
<text class="name">{{tasks.face.typeName}}</text>
|
||||
</view>
|
||||
<view class="result result--value" wx:if="{{tasks.face.status==='done'}}">{{tasks.face.resultText || '已完成'}}</view>
|
||||
<view class="result todo" wx:else>未完成,请去拍摄</view>
|
||||
<view class="task-time" wx:if="{{tasks.face.status==='done' && tasks.face.testTime}}">{{tasks.face.testTime}}</view>
|
||||
</view>
|
||||
<view class="status {{tasks.face.status==='done'?'done':''}}">{{tasks.face.status==='done'?'查看结果':'去完成'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="task-card" data-code="form" data-status="{{tasks.form.status}}" data-result-id="{{tasks.form.testResultId}}" bindtap="goTask">
|
||||
<view class="left">
|
||||
<view class="task-title-row">
|
||||
<text class="task-emoji">{{tasks.form.emoji}}</text>
|
||||
<text class="name">{{tasks.form.typeName}}</text>
|
||||
</view>
|
||||
<view class="result result--value" wx:if="{{tasks.form.status==='done'}}">
|
||||
<block wx:if="{{tasks.form.resultText}}">已填写:{{tasks.form.resultText}}</block>
|
||||
<block wx:else>已完成</block>
|
||||
</view>
|
||||
<view class="result todo" wx:else>未完成,请先填写</view>
|
||||
<view class="task-time" wx:if="{{tasks.form.status==='done' && tasks.form.testTime}}">{{tasks.form.testTime}}</view>
|
||||
</view>
|
||||
<view class="status {{tasks.form.status==='done'?'done':''}}">{{tasks.form.status==='done'?'查看结果':'去完成'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-actions">
|
||||
<button
|
||||
class="action-btn {{canAnalyze ? 'action-btn--primary' : 'action-btn--disabled'}}"
|
||||
loading="{{analyzing}}"
|
||||
bindtap="onAnalyzeTap"
|
||||
disabled="{{analyzing}}"
|
||||
>
|
||||
{{canAnalyze ? '开始综合分析' : '综合分析(未完成)'}}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
142
miniprogram/pages/gaokao/index.wxss
Normal file
142
miniprogram/pages/gaokao/index.wxss
Normal file
@@ -0,0 +1,142 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100%;
|
||||
background: #f7f8fc;
|
||||
padding: 32rpx 24rpx;
|
||||
/* 底部固定操作条占位(单主按钮 + 安全区) */
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: #fff;
|
||||
border-radius: 14rpx;
|
||||
padding: 22rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.task-emoji {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-time {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.result--value {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.result.todo {
|
||||
color: #d04848;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 24rpx;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.status.done {
|
||||
color: #1f9d55;
|
||||
}
|
||||
|
||||
/* 底部悬浮:全宽主操作 */
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 200;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16rpx 24rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 -8rpx 32rpx rgba(15, 23, 42, 0.08);
|
||||
border-top: 1rpx solid #eef0f4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 100% !important;
|
||||
margin: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
line-height: 1.35;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn--primary {
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn--disabled {
|
||||
background: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
690
miniprogram/pages/gaokao/report.js
Normal file
690
miniprogram/pages/gaokao/report.js
Normal file
@@ -0,0 +1,690 @@
|
||||
const app = getApp()
|
||||
const gaokaoApi = require('../../utils/gaokao')
|
||||
const { requestPromise } = require('../../utils/request')
|
||||
const payment = require('../../utils/payment')
|
||||
const {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded
|
||||
} = require('../../utils/phoneAuth.js')
|
||||
const unlockGate = require('../../utils/unlockGate.js')
|
||||
const inviteCodeGate = require('../../utils/inviteCodeGate.js')
|
||||
const { openTimelineShareHint } = require('../../utils/resultProfileGate.js')
|
||||
const { computeJourney, markShared } = require('../../utils/gaokaoJourneyState.js')
|
||||
const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js')
|
||||
|
||||
/** GET /api/test/detail | share-detail 返回体 -> mergeApiReport 入参 */
|
||||
function mapTestDetailToReportPayload(detail) {
|
||||
if (!detail || typeof detail !== 'object') {
|
||||
return null
|
||||
}
|
||||
if (String(detail.testType || '').toLowerCase() !== 'gaokao') {
|
||||
return null
|
||||
}
|
||||
const d = detail.data
|
||||
if (!d || typeof d !== 'object') {
|
||||
return null
|
||||
}
|
||||
const locked = !!d.locked
|
||||
let report = d.report
|
||||
if (typeof report === 'string') {
|
||||
try {
|
||||
report = JSON.parse(report)
|
||||
} catch (e) {
|
||||
report = null
|
||||
}
|
||||
}
|
||||
const inputSnap =
|
||||
d.inputSnapshot && typeof d.inputSnapshot === 'object' && !Array.isArray(d.inputSnapshot)
|
||||
? d.inputSnapshot
|
||||
: {}
|
||||
|
||||
if (locked) {
|
||||
const ov =
|
||||
typeof d.overview === 'string' && d.overview !== ''
|
||||
? d.overview
|
||||
: String(d.overview || '')
|
||||
report = {
|
||||
overview: ov,
|
||||
personalityReason: '',
|
||||
disclaimers: '',
|
||||
majorRecommend: [],
|
||||
schoolRecommend: {},
|
||||
inputEcho: {
|
||||
name: String(inputSnap.name || ''),
|
||||
province: String(inputSnap.province || ''),
|
||||
streamSubjects: String(inputSnap.streamSubjects || ''),
|
||||
estimatedScore: inputSnap.estimatedScore != null ? Number(inputSnap.estimatedScore) : 0,
|
||||
mbti: String(inputSnap.mbti || ''),
|
||||
pdp: String(inputSnap.pdp || ''),
|
||||
disc: String(inputSnap.disc || '')
|
||||
},
|
||||
locked: true
|
||||
}
|
||||
} else if (!report || typeof report !== 'object' || Array.isArray(report)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const topOv = typeof d.overview === 'string' ? d.overview : ''
|
||||
return {
|
||||
id: detail.id,
|
||||
createdAt: detail.createdAt,
|
||||
overview: topOv,
|
||||
report
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口返回: { id, createdAt, overview, report: reportJson }
|
||||
* 展示用合并为一层,便于 wxml 绑定
|
||||
*/
|
||||
function mergeApiReport(payload) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null
|
||||
}
|
||||
let br = payload.report
|
||||
if (typeof br === 'string') {
|
||||
try {
|
||||
br = JSON.parse(br)
|
||||
} catch (e) {
|
||||
br = null
|
||||
}
|
||||
}
|
||||
const block = br && typeof br === 'object' && !Array.isArray(br) ? br : {}
|
||||
const hasBlock = Object.keys(block).length > 0
|
||||
const hasOverview = typeof payload.overview === 'string' && payload.overview !== ''
|
||||
if (!hasBlock && !hasOverview && !(payload.id > 0)) {
|
||||
return null
|
||||
}
|
||||
const overview = hasOverview ? payload.overview : block.overview || ''
|
||||
return Object.assign({}, block, { overview })
|
||||
}
|
||||
|
||||
function normalizeSchoolRow(x, band) {
|
||||
if (!x || typeof x !== 'object') {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
band: band || '',
|
||||
schoolName: String(x.schoolName || x.name || '').trim() || '未命名院校',
|
||||
city: String(x.city || '').trim(),
|
||||
level: String(x.level || '').trim(),
|
||||
reason: String(x.reason || x.desc || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
function buildSchoolListFlat(rawSr) {
|
||||
if (Array.isArray(rawSr)) {
|
||||
return rawSr.map((x) => normalizeSchoolRow(x, '')).filter(Boolean)
|
||||
}
|
||||
if (rawSr && typeof rawSr === 'object' && !Array.isArray(rawSr)) {
|
||||
const chong = Array.isArray(rawSr.chong) ? rawSr.chong : []
|
||||
const wen = Array.isArray(rawSr.wen) ? rawSr.wen : []
|
||||
const bao = Array.isArray(rawSr.bao) ? rawSr.bao : []
|
||||
return [
|
||||
...chong.map((x) => normalizeSchoolRow(x, '冲')),
|
||||
...wen.map((x) => normalizeSchoolRow(x, '稳')),
|
||||
...bao.map((x) => normalizeSchoolRow(x, '保'))
|
||||
].filter(Boolean)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function buildViewModel(payload) {
|
||||
const report = mergeApiReport(payload)
|
||||
if (!report) {
|
||||
return {
|
||||
report: null,
|
||||
inputEcho: {},
|
||||
majorList: [],
|
||||
schoolListFlat: [],
|
||||
schoolChongCount: 0,
|
||||
schoolWenCount: 0,
|
||||
schoolBaoCount: 0,
|
||||
hasSchoolFlat: false,
|
||||
hasNoMajors: true
|
||||
}
|
||||
}
|
||||
const rawSr = report.schoolRecommend
|
||||
const schoolListFlat = buildSchoolListFlat(rawSr)
|
||||
|
||||
let schoolChongCount = 0
|
||||
let schoolWenCount = 0
|
||||
let schoolBaoCount = 0
|
||||
if (rawSr && typeof rawSr === 'object' && !Array.isArray(rawSr)) {
|
||||
schoolChongCount = Array.isArray(rawSr.chong) ? rawSr.chong.length : 0
|
||||
schoolWenCount = Array.isArray(rawSr.wen) ? rawSr.wen.length : 0
|
||||
schoolBaoCount = Array.isArray(rawSr.bao) ? rawSr.bao.length : 0
|
||||
}
|
||||
|
||||
const majors = Array.isArray(report.majorRecommend) ? report.majorRecommend : []
|
||||
const inputEcho = report.inputEcho || {}
|
||||
|
||||
return {
|
||||
report,
|
||||
inputEcho,
|
||||
majorList: majors.map((m) => {
|
||||
const rawName =
|
||||
m &&
|
||||
(m.majorName ||
|
||||
m.name ||
|
||||
m.title ||
|
||||
m.major ||
|
||||
m.major_name ||
|
||||
m.majorChinese ||
|
||||
m['专业'] ||
|
||||
m['专业名称'])
|
||||
const name = rawName != null && rawName !== '' ? String(rawName).trim() : ''
|
||||
const displayName = name || '未命名专业'
|
||||
const score = m && (m.fitScore != null ? m.fitScore : m.matchScore)
|
||||
const fitLabel = score != null && score !== '' ? '(' + String(score) + ')' : ''
|
||||
return { name: displayName, fitLabel }
|
||||
}),
|
||||
schoolListFlat,
|
||||
schoolChongCount,
|
||||
schoolWenCount,
|
||||
schoolBaoCount,
|
||||
hasSchoolFlat: schoolListFlat.length > 0,
|
||||
hasNoMajors: majors.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
function payInfoFromDetail(detail) {
|
||||
const isPaid = !!(detail && (detail.isPaid === 1 || detail.isPaid === true))
|
||||
const paidAmount = detail && detail.paidAmount != null ? Number(detail.paidAmount) : 0
|
||||
const amountYuan =
|
||||
detail && detail.amountYuan != null
|
||||
? Number(detail.amountYuan)
|
||||
: paidAmount > 0
|
||||
? paidAmount / 100
|
||||
: 0
|
||||
const needPaymentToUnlock =
|
||||
detail &&
|
||||
(detail.needPaymentToUnlock === true ||
|
||||
(!!detail.requiresPayment && !isPaid && paidAmount > 0))
|
||||
return {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
amountYuan: needPaymentToUnlock ? amountYuan : 0
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
report: null,
|
||||
inputEcho: {},
|
||||
majorList: [],
|
||||
schoolListFlat: [],
|
||||
schoolChongCount: 0,
|
||||
schoolWenCount: 0,
|
||||
schoolBaoCount: 0,
|
||||
hasSchoolFlat: false,
|
||||
hasNoMajors: true,
|
||||
journey: { step1Unlocked: false, step2Unlocked: false, activeStep: 1 },
|
||||
payInfo: {
|
||||
requiresPayment: false,
|
||||
isPaid: false,
|
||||
amountYuan: 0
|
||||
},
|
||||
testResultId: '',
|
||||
shareToken: '',
|
||||
hasReloadedAfterPay: false,
|
||||
hasPhone: false,
|
||||
fromShare: false,
|
||||
profileGate: false,
|
||||
showInviteCodeDialog: false,
|
||||
isPendingAnalyze: false,
|
||||
analyzingTitle: '正在生成高考志愿分析报告',
|
||||
analyzingTip: '',
|
||||
analyzeProgress: 0
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
try {
|
||||
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
} catch (e) {}
|
||||
|
||||
const fromShareFs = options && (String(options.fs) === '1' || options.from === 'share')
|
||||
const sid = options && options.id != null && options.id !== '' ? String(options.id) : ''
|
||||
const st = options && options.st ? String(options.st).trim() : ''
|
||||
|
||||
if (sid && st) {
|
||||
this.setData({ fromShare: true })
|
||||
this.loadShareDetail(sid, st)
|
||||
return
|
||||
}
|
||||
|
||||
const ec =
|
||||
typeof this.getOpenerEventChannel === 'function' ? this.getOpenerEventChannel() : null
|
||||
if (ec && typeof ec.once === 'function') {
|
||||
ec.once('gaokaoAnalyzeReport', (payload) => {
|
||||
if (payload && payload.report) {
|
||||
this.applyPayloadOnly({
|
||||
id: payload.id,
|
||||
createdAt: payload.createdAt || 0,
|
||||
overview: payload.overview || '',
|
||||
report: payload.report
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const pendingAnalyze = options && String(options.pendingAnalyze) === '1'
|
||||
if (pendingAnalyze) {
|
||||
this._pendingAnalyze = true
|
||||
return
|
||||
}
|
||||
|
||||
const rid = options && options.id != null ? parseInt(String(options.id), 10) : 0
|
||||
this._detailReportId = rid > 0 && !Number.isNaN(rid) ? rid : 0
|
||||
if (fromShareFs) {
|
||||
this.setData({ fromShare: true })
|
||||
}
|
||||
|
||||
const delay = options && options.fromAnalyze === '1' ? 400 : 0
|
||||
setTimeout(() => this.load(), delay)
|
||||
},
|
||||
|
||||
onReady() {
|
||||
if (this._pendingAnalyze) {
|
||||
this._pendingAnalyze = false
|
||||
this.beginAnalyzeFlow()
|
||||
}
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this._clearAnalyzeTimers()
|
||||
},
|
||||
|
||||
_clearAnalyzeTimers() {
|
||||
if (this._analyzeProgressTimer) {
|
||||
clearInterval(this._analyzeProgressTimer)
|
||||
this._analyzeProgressTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
beginAnalyzeFlow() {
|
||||
this._clearAnalyzeTimers()
|
||||
const tips = [
|
||||
'正在读取您的 MBTI 与测评数据…',
|
||||
'正在匹配专业维度与性格倾向…',
|
||||
'正在根据分数与省份生成志愿建议…',
|
||||
'正在润色报告与安全合规校验…',
|
||||
'生成综合报告…'
|
||||
]
|
||||
let progress = 0
|
||||
let tipIndex = 0
|
||||
this.setData({
|
||||
isPendingAnalyze: true,
|
||||
analyzingTitle: '正在生成高考志愿分析报告',
|
||||
analyzingTip: tips[0],
|
||||
analyzeProgress: 0
|
||||
})
|
||||
this._analyzeProgressTimer = setInterval(() => {
|
||||
progress += 3
|
||||
if (progress > 95) progress = 95
|
||||
if (progress > (tipIndex + 1) * 18 && tipIndex < tips.length - 1) tipIndex++
|
||||
this.setData({
|
||||
analyzeProgress: Math.floor(progress),
|
||||
analyzingTip: tips[tipIndex]
|
||||
})
|
||||
}, 200)
|
||||
|
||||
gaokaoApi
|
||||
.analyze()
|
||||
.then((res) => {
|
||||
this._clearAnalyzeTimers()
|
||||
const rawId = res && (res.reportId != null ? res.reportId : res.id)
|
||||
const numId = parseInt(String(rawId), 10)
|
||||
if (!rawId || Number.isNaN(numId) || numId <= 0) {
|
||||
throw new Error('未返回报告')
|
||||
}
|
||||
this._detailReportId = numId
|
||||
this.setData({
|
||||
analyzeProgress: 100,
|
||||
analyzingTip: '分析完成!'
|
||||
})
|
||||
return new Promise((r) => setTimeout(r, 400)).then(() =>
|
||||
this.loadDetail(numId, { silent: true }).catch(() => {
|
||||
const rep = res && res.report
|
||||
if (rep && typeof rep === 'object') {
|
||||
this.applyPayloadOnly({
|
||||
id: numId,
|
||||
createdAt: res.createdAt || 0,
|
||||
overview:
|
||||
(typeof res.overview === 'string' && res.overview) ||
|
||||
(rep.overview && String(rep.overview)) ||
|
||||
'',
|
||||
report: rep
|
||||
})
|
||||
return
|
||||
}
|
||||
return Promise.reject(new Error('报告已生成,但加载详情失败'))
|
||||
})
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
this.setData({ isPendingAnalyze: false })
|
||||
})
|
||||
.catch((e) => {
|
||||
this._clearAnalyzeTimers()
|
||||
this.setData({ isPendingAnalyze: false, analyzeProgress: 0 })
|
||||
wx.showToast({ title: (e && e.message) || '分析失败', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.navigateBack({ delta: 1 })
|
||||
}, 1600)
|
||||
})
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
if (this.data.report && !this.data.fromShare) {
|
||||
const profileGate = needsResultProfileGate(!!this.data.fromShare)
|
||||
this.setData({ profileGate })
|
||||
this._syncJourney()
|
||||
}
|
||||
// 切换个人/企业 Tab 后回到报告页:静默重拉详情以同步 paidAmount(与当前 Tab 定价一致)
|
||||
if (!this.data.fromShare && !this.data.isPendingAnalyze) {
|
||||
const rid = parseInt(String(this.data.testResultId || ''), 10)
|
||||
if (rid > 0) {
|
||||
this.loadDetail(rid, { silent: true }).catch(() => {})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const id = this.data.testResultId
|
||||
const st = this.data.shareToken
|
||||
if (!id || !st) {
|
||||
return { title: '高考志愿分析报告', path: '/pages/gaokao/index' }
|
||||
}
|
||||
return {
|
||||
title: '高考志愿分析报告',
|
||||
path: `/pages/gaokao/report?id=${encodeURIComponent(id)}&st=${encodeURIComponent(st)}&fs=1`
|
||||
}
|
||||
},
|
||||
|
||||
_syncJourney() {
|
||||
const j = computeJourney(
|
||||
{
|
||||
profileGate: !!this.data.profileGate,
|
||||
payRequired: !!(this.data.payInfo && this.data.payInfo.requiresPayment),
|
||||
isPaid: !!(this.data.payInfo && this.data.payInfo.isPaid)
|
||||
},
|
||||
this.data.testResultId || '0'
|
||||
)
|
||||
this.setData({ journey: j })
|
||||
},
|
||||
|
||||
_reportPaywallOnce(payInfo) {
|
||||
if (!payInfo || !payInfo.requiresPayment || payInfo.isPaid) return
|
||||
if (this._paywallReported) return
|
||||
this._paywallReported = true
|
||||
try {
|
||||
require('../../utils/analytics').track('paywall_view', {
|
||||
type: 'gaokao',
|
||||
amountYuan: payInfo.amountYuan
|
||||
})
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
applyPayloadOnly(payload) {
|
||||
const vm = buildViewModel(payload)
|
||||
const profileGate = needsResultProfileGate(!!this.data.fromShare)
|
||||
const patch = Object.assign(vm, { profileGate })
|
||||
if (payload && payload.id != null) {
|
||||
patch.testResultId = String(payload.id)
|
||||
}
|
||||
this.setData(patch)
|
||||
this._syncJourney()
|
||||
},
|
||||
|
||||
applyDetailPayload(detail) {
|
||||
const mapped = mapTestDetailToReportPayload(detail)
|
||||
if (!mapped) {
|
||||
wx.showToast({ title: '报告数据无效', icon: 'none' })
|
||||
this.setData(buildViewModel(null))
|
||||
return
|
||||
}
|
||||
const payInfo = payInfoFromDetail(detail)
|
||||
const profileGate = needsResultProfileGate(!!this.data.fromShare)
|
||||
const vm = buildViewModel(mapped)
|
||||
const patch = Object.assign(vm, {
|
||||
payInfo,
|
||||
profileGate,
|
||||
shareToken: (detail && detail.shareToken) || '',
|
||||
testResultId: detail.id != null ? String(detail.id) : ''
|
||||
})
|
||||
this.setData(patch)
|
||||
this._reportPaywallOnce(payInfo)
|
||||
this._syncJourney()
|
||||
},
|
||||
|
||||
loadShareDetail(id, st) {
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
requestPromise({
|
||||
url: `/api/test/share-detail?id=${encodeURIComponent(id)}&st=${encodeURIComponent(st)}`,
|
||||
method: 'GET'
|
||||
})
|
||||
.then((res) => {
|
||||
const body = res.data || {}
|
||||
if (body.code !== 200) {
|
||||
throw new Error(body.message || '加载失败')
|
||||
}
|
||||
this.applyDetailPayload(body.data || {})
|
||||
})
|
||||
.catch((e) => {
|
||||
wx.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
|
||||
this.setData(buildViewModel(null))
|
||||
})
|
||||
.finally(() => wx.hideLoading())
|
||||
},
|
||||
|
||||
loadDetail(id, opts) {
|
||||
const silent = !!(opts && opts.silent)
|
||||
const numId = parseInt(String(id), 10)
|
||||
if (!numId || Number.isNaN(numId)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!silent) wx.showLoading({ title: '加载中...' })
|
||||
const gd = app.globalData || {}
|
||||
const pricingScope = gd.appScope === 'enterprise' ? 'enterprise' : 'personal'
|
||||
let detailUrl = `/api/test/detail?id=${encodeURIComponent(numId)}&pricingScope=${encodeURIComponent(pricingScope)}`
|
||||
try {
|
||||
const eid = getEnterpriseIdForApiPayload()
|
||||
if (eid != null && Number(eid) > 0) {
|
||||
detailUrl += `&enterpriseId=${encodeURIComponent(String(eid))}`
|
||||
}
|
||||
} catch (e) {}
|
||||
return requestPromise({
|
||||
url: detailUrl,
|
||||
method: 'GET'
|
||||
})
|
||||
.then((res) => {
|
||||
const body = res.data || {}
|
||||
if (body.code !== 200) {
|
||||
throw new Error(body.message || '加载失败')
|
||||
}
|
||||
this.applyDetailPayload(body.data || {})
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!silent) {
|
||||
wx.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
|
||||
this.setData(buildViewModel(null))
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!silent) wx.hideLoading()
|
||||
})
|
||||
},
|
||||
|
||||
load() {
|
||||
const rid = this._detailReportId || 0
|
||||
if (rid > 0) {
|
||||
this.loadDetail(rid)
|
||||
return
|
||||
}
|
||||
gaokaoApi
|
||||
.latestReport()
|
||||
.then((payload) => {
|
||||
const id = payload && payload.id
|
||||
if (!id) {
|
||||
throw new Error('暂无报告')
|
||||
}
|
||||
return this.loadDetail(id)
|
||||
})
|
||||
.catch((e) => {
|
||||
wx.showToast({ title: e.message || '暂无报告', icon: 'none' })
|
||||
this.setData(buildViewModel(null))
|
||||
})
|
||||
},
|
||||
|
||||
goCompleteProfile() {
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_complete_profile', { from: 'gaokao_report' })
|
||||
} catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/user-profile/index' })
|
||||
},
|
||||
|
||||
goWantTest() {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
},
|
||||
|
||||
goReadFullFromShare() {
|
||||
wx.switchTab({ url: '/pages/profile/index' })
|
||||
},
|
||||
|
||||
onTapReadFull() {
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_read_full', { type: 'gaokao' })
|
||||
} catch (e) {}
|
||||
if (this.data.profileGate) {
|
||||
unlockGate.scrollToUnlockAnchor(this)
|
||||
wx.showToast({
|
||||
title: this.data.hasPhone ? '请先完善头像与昵称' : '请在上滑区域内完成手机号授权',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
|
||||
this.unlockFullReport()
|
||||
return
|
||||
}
|
||||
wx.showToast({ title: '当前已是完整报告', icon: 'none' })
|
||||
},
|
||||
|
||||
onTapShareMoment() {
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_share_moment', { type: 'gaokao' })
|
||||
} catch (e) {}
|
||||
if (!this.data.journey.step1Unlocked) {
|
||||
wx.showToast({ title: '请先解锁全文', icon: 'none' })
|
||||
this.onTapReadFull()
|
||||
return
|
||||
}
|
||||
markShared(this.data.testResultId || '0')
|
||||
this._syncJourney()
|
||||
openTimelineShareHint()
|
||||
},
|
||||
|
||||
unlockFullReport() {
|
||||
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
|
||||
if (!payInfo.requiresPayment || payInfo.isPaid) return
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_unlock_full', {
|
||||
type: 'gaokao',
|
||||
amountYuan: payInfo.amountYuan
|
||||
})
|
||||
} catch (e) {}
|
||||
const run =
|
||||
typeof app.ensureLogin === 'function'
|
||||
? app.ensureLogin()
|
||||
: Promise.resolve(!!(app.globalData && app.globalData.token) || !!wx.getStorageSync('token'))
|
||||
run.then((logged) => {
|
||||
if (!logged) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
unlockGate.ensureUnlockPrerequisitesBeforePay(this).then((ok) => {
|
||||
if (!ok) return
|
||||
inviteCodeGate.ensureInviteCodeGate(this).then((go) => {
|
||||
if (!go) return
|
||||
payment.purchaseGaokaoReport({
|
||||
testResultId: testResultId ? parseInt(String(testResultId), 10) || undefined : undefined,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
|
||||
this.setData({ 'payInfo.isPaid': true })
|
||||
this._syncJourney()
|
||||
if (testResultId && !hasReloadedAfterPay) {
|
||||
this.setData({ hasReloadedAfterPay: true })
|
||||
setTimeout(() => this.loadDetail(testResultId), 500)
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onGetPhoneNumberForGaokaoPay(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.unlockFullReport()
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
if (hasPhone()) {
|
||||
this.unlockFullReport()
|
||||
} else {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
const profileGate = needsResultProfileGate(!!this.data.fromShare)
|
||||
this.setData({ profileGate })
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
this._syncJourney()
|
||||
this.unlockFullReport()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
onPhoneLoginForResultGate(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能查看完整报告', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
const profileGate = needsResultProfileGate(!!this.data.fromShare)
|
||||
this.setData({ profileGate })
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
this._syncJourney()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
onInviteCodeSkip() {
|
||||
inviteCodeGate.finishInviteCodeGate(this, true)
|
||||
},
|
||||
|
||||
onInviteCodeSuccess() {
|
||||
inviteCodeGate.finishInviteCodeGate(this, true)
|
||||
}
|
||||
})
|
||||
6
miniprogram/pages/gaokao/report.json
Normal file
6
miniprogram/pages/gaokao/report.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "高考分析报告",
|
||||
"usingComponents": {
|
||||
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog"
|
||||
}
|
||||
}
|
||||
262
miniprogram/pages/gaokao/report.wxml
Normal file
262
miniprogram/pages/gaokao/report.wxml
Normal file
@@ -0,0 +1,262 @@
|
||||
<view class="gaokao-report-page">
|
||||
<!-- 综合分析:与人脸结果页类似的加载态 -->
|
||||
<view class="analyzing-modal" wx:if="{{isPendingAnalyze}}">
|
||||
<view class="analyzing-content">
|
||||
<view class="analyzing-icon">
|
||||
<view class="analyzing-spinner"></view>
|
||||
</view>
|
||||
<text class="analyzing-title">{{analyzingTitle}}</text>
|
||||
<text class="analyzing-desc">{{analyzingTip}}</text>
|
||||
<view class="analyzing-bar">
|
||||
<view class="analyzing-bar-fill" style="width: {{analyzeProgress}}%"></view>
|
||||
</view>
|
||||
<text class="analyzing-hint">AI 生成可能需要 30 秒~1 分钟,请稍候…</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="container {{report && !fromShare ? 'container--with-bottom-tools' : ''}} {{report && fromShare ? 'container--with-share-footer' : ''}}"
|
||||
wx:if="{{report}}"
|
||||
>
|
||||
<view class="header-card">
|
||||
<view class="header-hero">
|
||||
<view class="header-top">
|
||||
<view class="user-info">
|
||||
<view class="user-name">{{inputEcho.name || '同学'}}</view>
|
||||
<view class="user-tags">
|
||||
<text class="tag tag-province" wx:if="{{inputEcho.province}}">{{inputEcho.province}}</text>
|
||||
<text class="tag tag-stream" wx:if="{{inputEcho.streamSubjects}}">{{inputEcho.streamSubjects}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="score-box">
|
||||
<text class="score-num">{{inputEcho.estimatedScore || 0}}</text>
|
||||
<text class="score-unit">分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-body" wx:if="{{inputEcho.mbti || inputEcho.pdp || inputEcho.disc}}">
|
||||
<view class="personality-tags">
|
||||
<view class="p-tag" wx:if="{{inputEcho.mbti}}">
|
||||
<text class="p-label">MBTI</text>
|
||||
<text class="p-value">{{inputEcho.mbti}}</text>
|
||||
</view>
|
||||
<view class="p-tag" wx:if="{{inputEcho.pdp}}">
|
||||
<text class="p-label">PDP</text>
|
||||
<text class="p-value">{{inputEcho.pdp}}</text>
|
||||
</view>
|
||||
<view class="p-tag" wx:if="{{inputEcho.disc}}">
|
||||
<text class="p-label">DISC</text>
|
||||
<text class="p-value">{{inputEcho.disc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card paywall-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid && !fromShare}}">
|
||||
<view class="paywall-content paywall-content--preview40">
|
||||
<view class="paywall-blur">
|
||||
<text class="paywall-fake-title">完整高考志愿分析报告</text>
|
||||
<text class="paywall-fake-line paywall-fake-line--compact">解锁后可查看冲稳保院校、专业推荐与性格解读全文。</text>
|
||||
<view class="paywall-preview-wrap" wx:if="{{report.overview}}">
|
||||
<text class="paywall-preview-label">综合总评(预览)</text>
|
||||
<view class="paywall-preview-inner">
|
||||
<text class="paywall-preview-text">{{report.overview}}</text>
|
||||
<view class="paywall-preview-fade"></view>
|
||||
</view>
|
||||
</view>
|
||||
<block wx:else>
|
||||
<text class="paywall-fake-line">• 冲稳保院校与推荐理由</text>
|
||||
<text class="paywall-fake-line">• 专业推荐与匹配说明</text>
|
||||
<text class="paywall-fake-line">• 性格与志愿方向解读</text>
|
||||
</block>
|
||||
</view>
|
||||
<view class="paywall-mask paywall-mask--from40"></view>
|
||||
<view id="unlock-gate-anchor" class="unlock-gate-anchor-btn-wrap">
|
||||
<button
|
||||
class="paywall-btn"
|
||||
wx:if="{{!hasPhone}}"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumberForGaokaoPay"
|
||||
>
|
||||
<text class="paywall-btn-main">解锁完整报告</text>
|
||||
<text class="paywall-btn-price">¥{{payInfo.amountYuan}} / 次</text>
|
||||
</button>
|
||||
<button class="paywall-btn" wx:elif="{{hasPhone}}" bindtap="unlockFullReport">
|
||||
<text class="paywall-btn-main">解锁完整报告</text>
|
||||
<text class="paywall-btn-price">¥{{payInfo.amountYuan}} / 次</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card paywall-card" wx:elif="{{report.locked && !fromShare && !profileGate}}">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告暂不可查看</text>
|
||||
<text class="paywall-fake-line">请稍后在「我的」重试或联系客服;若刚授权手机,可下拉刷新本页。</text>
|
||||
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去个人资料</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
wx:if="{{profileGate && report && (!payInfo.requiresPayment || payInfo.isPaid) && !fromShare}}"
|
||||
id="unlock-gate-anchor"
|
||||
class="unlock-gate-profile-block"
|
||||
>
|
||||
<view class="preview-teaser-card" wx:if="{{report.overview}}">
|
||||
<text class="preview-teaser-title">综合总评(预览)</text>
|
||||
<text class="preview-teaser-desc">{{report.overview}}</text>
|
||||
<text class="preview-teaser-hint">授权手机号并设置头像、昵称后即可查看全文。</text>
|
||||
</view>
|
||||
<button
|
||||
wx:if="{{!hasPhone}}"
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onPhoneLoginForResultGate"
|
||||
>
|
||||
<text class="paywall-btn-main">登录解锁全文</text>
|
||||
</button>
|
||||
<button wx:else class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">完善资料 · 查看全文</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view id="unlock-scroll-tail"></view>
|
||||
|
||||
<view class="card" wx:if="{{!report.locked}}">
|
||||
<view class="title-wrap">
|
||||
<view class="title-icon">📊</view>
|
||||
<view class="title">综合总评</view>
|
||||
</view>
|
||||
<view class="content">{{report.overview}}</view>
|
||||
</view>
|
||||
|
||||
<view class="card" wx:if="{{!report.locked}}">
|
||||
<view class="title-wrap">
|
||||
<view class="title-icon">🎯</view>
|
||||
<view class="title">冲稳保建议</view>
|
||||
</view>
|
||||
<block wx:if="{{hasSchoolFlat}}">
|
||||
<view class="sub-counts" wx:if="{{schoolChongCount || schoolWenCount || schoolBaoCount}}">
|
||||
<view class="count-tag count-chong">冲 {{schoolChongCount}}所</view>
|
||||
<view class="count-tag count-wen">稳 {{schoolWenCount}}所</view>
|
||||
<view class="count-tag count-bao">保 {{schoolBaoCount}}所</view>
|
||||
</view>
|
||||
<view class="sub-counts" wx:else>
|
||||
<view class="count-tag count-total">院校参考(共 {{schoolListFlat.length}} 所)</view>
|
||||
</view>
|
||||
|
||||
<view class="school-list">
|
||||
<block wx:for="{{schoolListFlat}}" wx:for-item="sch" wx:for-index="sidx" wx:key="sidx">
|
||||
<view class="school-item">
|
||||
<view class="school-header">
|
||||
<view
|
||||
class="school-band-tag band-{{sch.band === '冲' ? 'chong' : (sch.band === '稳' ? 'wen' : (sch.band === '保' ? 'bao' : 'none'))}}"
|
||||
wx:if="{{sch.band}}"
|
||||
>{{sch.band}}</view>
|
||||
<view class="school-name">{{sch.schoolName}}</view>
|
||||
</view>
|
||||
<view class="school-meta-row" wx:if="{{sch.city || sch.level}}">
|
||||
<text class="meta-item" wx:if="{{sch.city}}"><text class="meta-icon">📍</text>{{sch.city}}</text>
|
||||
<text class="meta-item" wx:if="{{sch.level}}"><text class="meta-icon">🎓</text>{{sch.level}}</text>
|
||||
</view>
|
||||
<view class="school-reason" wx:if="{{sch.reason}}">
|
||||
<text class="quote-mark">“</text>
|
||||
{{sch.reason}}
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
<view wx:else class="sub-counts">
|
||||
<view class="count-tag count-chong">冲 {{schoolChongCount}}所</view>
|
||||
<view class="count-tag count-wen">稳 {{schoolWenCount}}所</view>
|
||||
<view class="count-tag count-bao">保 {{schoolBaoCount}}所</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card" wx:if="{{!report.locked}}">
|
||||
<view class="title-wrap">
|
||||
<view class="title-icon">💼</view>
|
||||
<view class="title">专业建议</view>
|
||||
</view>
|
||||
<view class="major-list" wx:if="{{!hasNoMajors}}">
|
||||
<block wx:for="{{majorList}}" wx:for-item="row" wx:for-index="idx" wx:key="idx">
|
||||
<view class="major-item">
|
||||
<view class="major-rank rank-{{idx < 3 ? idx + 1 : 'other'}}">{{idx + 1}}</view>
|
||||
<view class="major-name">{{row.name}}</view>
|
||||
<view class="major-score" wx:if="{{row.fitLabel}}">{{row.fitLabel}}</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view wx:if="{{hasNoMajors}}" class="sub">暂无专业推荐</view>
|
||||
</view>
|
||||
|
||||
<view class="card" wx:if="{{!report.locked}}">
|
||||
<view class="title-wrap">
|
||||
<view class="title-icon">🧠</view>
|
||||
<view class="title">性格匹配说明</view>
|
||||
</view>
|
||||
<view class="content">{{report.personalityReason}}</view>
|
||||
</view>
|
||||
|
||||
<view class="card disclaimer-card" wx:if="{{!report.locked}}">
|
||||
<view class="title-wrap">
|
||||
<view class="title-icon">⚠️</view>
|
||||
<view class="title">免责声明</view>
|
||||
</view>
|
||||
<view class="content disclaimer-text">{{report.disclaimers}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:elif="{{!isPendingAnalyze}}">
|
||||
<view class="empty-icon">📄</view>
|
||||
<view class="empty-text">暂无报告,请先完成综合分析</view>
|
||||
</view>
|
||||
|
||||
<view class="result-bottom-tools" wx:if="{{report && !fromShare}}">
|
||||
<view class="journey-stepper journey-stepper--two">
|
||||
<view class="journey-stepper__item">
|
||||
<view
|
||||
class="journey-stepper__dot {{journey.step1Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===1 ? 'journey-stepper__dot--active' : '')}}"
|
||||
>1</view>
|
||||
<text class="journey-stepper__label {{journey.activeStep===1 ? 'journey-stepper__label--active' : ''}}">看全文</text>
|
||||
</view>
|
||||
<view class="journey-stepper__bar {{journey.step1Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
|
||||
<view class="journey-stepper__item">
|
||||
<view
|
||||
class="journey-stepper__dot {{journey.step2Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===2 ? 'journey-stepper__dot--active' : '')}}"
|
||||
>2</view>
|
||||
<text class="journey-stepper__label {{journey.activeStep===2 ? 'journey-stepper__label--active' : ''}}">分享朋友圈</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="result-bottom-tools__row result-bottom-tools__row--two">
|
||||
<view class="result-tool-btn result-tool-btn--primary" bindtap="onTapReadFull">
|
||||
<text>{{journey.step1Unlocked ? '查看全文' : '① 解锁全文'}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? (hasPhone ? '完善资料后解锁' : '登录并完善资料') : '点击解锁')}}</text>
|
||||
</view>
|
||||
<view
|
||||
class="result-tool-btn {{journey.step1Unlocked ? 'result-tool-btn--rose' : 'result-tool-btn--locked'}}"
|
||||
bindtap="onTapShareMoment"
|
||||
>
|
||||
<text><text wx:if="{{!journey.step1Unlocked}}" class="result-tool-btn__lock">🔒</text>朋友圈</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? (journey.step2Unlocked ? '已分享' : '点击分享') : '先解锁全文'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="result-share-footer result-share-footer--stack" wx:if="{{report && fromShare}}">
|
||||
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
|
||||
<view class="result-share-footer-row2">
|
||||
<button class="result-share-footer-btn result-share-footer-btn--ghost" bindtap="goReadFullFromShare">看全文</button>
|
||||
<button class="result-share-footer-btn result-share-footer-btn--share" open-type="share">分享给好友</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<invite-code-dialog
|
||||
visible="{{showInviteCodeDialog}}"
|
||||
bind:skip="onInviteCodeSkip"
|
||||
bind:success="onInviteCodeSuccess"
|
||||
/>
|
||||
</view>
|
||||
698
miniprogram/pages/gaokao/report.wxss
Normal file
698
miniprogram/pages/gaokao/report.wxss
Normal file
@@ -0,0 +1,698 @@
|
||||
@import '../../styles/result-page-dashboard.wxss';
|
||||
|
||||
.gaokao-report-page {
|
||||
min-height: 100vh;
|
||||
background: #f4f6f9;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f4f6f9;
|
||||
padding: 24rpx;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container.container--with-bottom-tools {
|
||||
padding-bottom: calc(120rpx + 400rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.container.container--with-share-footer {
|
||||
padding-bottom: calc(220rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.journey-stepper--two {
|
||||
max-width: 520rpx;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.result-bottom-tools__row--two .result-tool-btn:nth-child(1)::before {
|
||||
content: '01';
|
||||
}
|
||||
.result-bottom-tools__row--two .result-tool-btn:nth-child(2)::before {
|
||||
content: '02';
|
||||
}
|
||||
|
||||
/* 付费墙(与 MBTI 结果页同款布局,配色贴近高考页主色) */
|
||||
.paywall-card {
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.paywall-content {
|
||||
position: relative;
|
||||
min-height: 360rpx;
|
||||
}
|
||||
/* 有总评预览时拉高容器,便于展示约 40vh 正文 */
|
||||
.paywall-content--preview40 {
|
||||
min-height: 48vh;
|
||||
}
|
||||
.paywall-blur {
|
||||
padding: 32rpx 24rpx 200rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
backdrop-filter: blur(6rpx);
|
||||
}
|
||||
.paywall-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 24rpx;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.28) 38%,
|
||||
rgba(255, 255, 255, 0.58) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
/* 顶部约 40% 区域不压暗,以下渐强引导至解锁按钮 */
|
||||
.paywall-mask--from40 {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0) 40%,
|
||||
rgba(255, 255, 255, 0.35) 58%,
|
||||
rgba(255, 255, 255, 0.72) 78%,
|
||||
rgba(255, 255, 255, 0.94) 100%
|
||||
);
|
||||
}
|
||||
.paywall-fake-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.paywall-fake-line {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #888;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.paywall-fake-line--compact {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
.paywall-preview-wrap {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.paywall-preview-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #6366f1;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.paywall-preview-inner {
|
||||
position: relative;
|
||||
max-height: 40vh;
|
||||
overflow: hidden;
|
||||
border-radius: 16rpx;
|
||||
background: #f8fafc;
|
||||
padding: 20rpx 20rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.paywall-preview-text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #334155;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.paywall-preview-fade {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(to bottom, rgba(248, 250, 252, 0), rgba(248, 250, 252, 1));
|
||||
pointer-events: none;
|
||||
}
|
||||
.paywall-btn {
|
||||
position: absolute;
|
||||
left: 5%;
|
||||
right: 5%;
|
||||
width: 90%;
|
||||
bottom: 132rpx;
|
||||
z-index: 3;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.35);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.paywall-btn-main {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.paywall-btn-price {
|
||||
font-size: 24rpx;
|
||||
color: #e0e7ff;
|
||||
}
|
||||
.paywall-btn--inline-profile {
|
||||
position: relative !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
width: 100% !important;
|
||||
margin-top: 28rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.result-share-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 200;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #f5f5f5;
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.result-share-footer-btn {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border: none;
|
||||
}
|
||||
.result-share-footer-btn::after {
|
||||
border: none;
|
||||
}
|
||||
.result-share-footer-row2 .result-share-footer-btn {
|
||||
flex: 1;
|
||||
min-height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 26rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Header Card:渐变区随内容增高,避免省/科类标签落到白底上被「裁切」看不见 */
|
||||
.header-card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.header-hero {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
padding: 32rpx 32rpx 36rpx;
|
||||
}
|
||||
|
||||
.header-body {
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.35;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 22rpx;
|
||||
padding: 8rpx 18rpx;
|
||||
line-height: 1.3;
|
||||
border-radius: 100rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-province {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.tag-stream {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.score-box {
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 16rpx rgba(99, 102, 241, 0.15);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.score-num {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #6366f1;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.score-unit {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-left: 4rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.personality-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
padding-top: 4rpx;
|
||||
border-top: 1rpx dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.p-tag {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
background: #f8fafc;
|
||||
border-radius: 12rpx;
|
||||
overflow: visible;
|
||||
border: 1rpx solid #e2e8f0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.p-label {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 20rpx;
|
||||
font-weight: bold;
|
||||
padding: 6rpx 12rpx;
|
||||
}
|
||||
|
||||
.p-value {
|
||||
color: #334155;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
padding: 6rpx 16rpx;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
max-width: 460rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Common Card */
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28rpx;
|
||||
color: #475569;
|
||||
line-height: 1.7;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* Sub Counts (冲稳保) */
|
||||
.sub-counts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.count-tag {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.count-chong {
|
||||
background: #fff1f2;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.count-wen {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.count-bao {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.count-total {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
/* School List */
|
||||
.school-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.school-item {
|
||||
background: #f8fafc;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
border: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
.school-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.school-band-tag {
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.band-chong {
|
||||
background: #ffe4e6;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.band-wen {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.band-bao {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.band-none {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.school-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #1e293b;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.school-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
margin-right: 6rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.school-reason {
|
||||
font-size: 26rpx;
|
||||
color: #475569;
|
||||
line-height: 1.6;
|
||||
background: #fff;
|
||||
padding: 16rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quote-mark {
|
||||
color: #cbd5e1;
|
||||
font-size: 40rpx;
|
||||
font-family: serif;
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
left: 12rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.school-reason {
|
||||
padding-left: 40rpx;
|
||||
}
|
||||
|
||||
/* Major List */
|
||||
.major-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.major-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.major-rank {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rank-1 {
|
||||
background: #fef08a;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.rank-2 {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.rank-3 {
|
||||
background: #ffedd5;
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
.rank-other {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.major-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.major-score {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
background: #fff;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
/* Disclaimer */
|
||||
.disclaimer-card {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.disclaimer-card .title {
|
||||
color: #9f1239;
|
||||
}
|
||||
|
||||
.disclaimer-text {
|
||||
color: #be123c;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 32rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 30rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* 综合分析加载(与人脸结果页同款结构,配色贴近高考页) */
|
||||
.analyzing-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(15, 23, 42, 0.72);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.analyzing-content {
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 80rpx 56rpx;
|
||||
width: 560rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 16rpx 48rpx rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.analyzing-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.analyzing-spinner {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border: 8rpx solid #e2e8f0;
|
||||
border-top-color: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: gaokao-analyzing-spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gaokao-analyzing-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.analyzing-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.analyzing-desc {
|
||||
font-size: 28rpx;
|
||||
color: #6366f1;
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.analyzing-bar {
|
||||
width: 400rpx;
|
||||
height: 12rpx;
|
||||
background: #e2e8f0;
|
||||
border-radius: 6rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.analyzing-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #6366f1, #a855f7);
|
||||
border-radius: 6rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.analyzing-hint {
|
||||
font-size: 24rpx;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
Reference in New Issue
Block a user