| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- <template>
- <view class="container">
- <view v-if="loading" class="loading-box">
- <text>加载报告中...</text>
- </view>
- <scroll-view v-else scroll-y class="scroll-body">
- <!-- 仪表盘区域 (Gauge Section) -->
- <view class="report-card gauge-card card-anim">
- <view class="gauge-container">
- <view class="gauge-main">
- <view class="gauge-base"></view>
- <!-- 仪表盘进度:总分 100 分对应 270 度 (从-135到135) -->
- <view class="gauge-progress-bar" :style="{ transform: 'rotate(' + (-135 + displayTotalScore * 2.7) + 'deg)' }"></view>
- <view class="gauge-ticks">
- <view v-for="i in 41" :key="i" class="tick" :style="{ transform: 'rotate(' + (-120 + (i-1)*6) + 'deg)' }"></view>
- </view>
- <view class="gauge-needle-box">
- <view class="needle" :style="{ transform: 'rotate(' + (-135 + displayTotalScore * 2.7) + 'deg)' }"></view>
- </view>
- <view class="score-display">
- <text class="num anim-num">{{ displayTotalScore }}</text>
- <text class="total">/100</text>
- </view>
- </view>
- </view>
- <view class="job-name">{{ evaluationName }}</view>
- <view class="report-meta">
- <text>测评报告分析</text>
- <text>{{ submitTime }}</text>
- </view>
- </view>
- <!-- 测评情况 (Radar Chart Section) -->
- <view class="report-card spider-card card-anim">
- <view class="section-header">
- <text class="title">维度分析</text>
- </view>
- <view class="spider-chart-box">
- <!-- 背景网格 -->
- <view class="pentagon-grids">
- <view v-for="i in [1,2,3,4,5]" :key="i" class="p-grid" :class="'pg-'+i" :style="{ clipPath: getPentagonPath(i * 20) }"></view>
- </view>
- <!-- 轴线 -->
- <view class="axes">
- <view v-for="(axis, index) in axesStyles" :key="index" class="axis" :style="axis"></view>
- </view>
- <!-- 动态数据区域 -->
- <view class="data-area">
- <view class="data-polygon" :style="{ clipPath: polygonClipPath }"></view>
- <view v-for="(node, index) in nodePositions" :key="index" class="node-point" :style="{ left: node.x + '%', top: node.y + '%' }">
- <text class="v-label" :style="node.labelStyle">{{ node.name }}</text>
- <text class="v-score" :style="node.scoreStyle">{{ node.score }}分</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 3. 维度详情列表 -->
- <view class="report-card detail-card card-anim">
- <view class="section-header">
- <text class="title">维度详情</text>
- <text class="subtitle">点击查看答题详情</text>
- </view>
- <view class="ability-detail-list">
- <view
- v-for="(item, index) in abilityResults"
- :key="index"
- class="ability-detail-item"
- @click="viewAbilityDetail(item)"
- >
- <view class="item-left">
- <text class="a-name">{{ item.name }}</text>
- <view class="status-tag" :class="item.isPass ? 'pass' : 'fail'">
- {{ item.isPass ? '已达标' : '未达标' }}
- </view>
- </view>
- <view class="item-right">
- <text class="a-score">{{ item.score }}</text>
- <text class="a-unit">分</text>
- <image src="/static/icons/arrow-right.svg" class="arrow-icon"></image>
- </view>
- </view>
- </view>
- </view>
- </scroll-view>
- </view>
- </template>
- <script setup>
- import { ref, computed } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import { getEvaluationResult } from '../../api/assessment.js'
- const loading = ref(true)
- const evaluationId = ref('')
- const displayTotalScore = ref(0)
- const evaluationName = ref('测评报告')
- const submitTime = ref('')
- const radarData = ref({ categories: [], series: [] })
- const abilityResults = ref([]) // 存储详细维度数据
- onLoad(async (options) => {
- evaluationId.value = options.id || options.assessmentId || ''
- if (!evaluationId.value) {
- uni.showToast({ title: '参数错误', icon: 'none' })
- loading.value = false
- return
- }
- await loadData()
- })
- const loadData = async () => {
- if (!evaluationId.value || evaluationId.value === 'undefined') {
- loading.value = false
- return
- }
- try {
- const userInfo = uni.getStorageSync('userInfo') || {}
- const studentId = userInfo.studentId || userInfo.id
- if (!studentId) {
- uni.showToast({ title: '请先登录', icon: 'none' })
- return
- }
- const res = await getEvaluationResult(evaluationId.value, studentId)
- if (res.code === 200 && res.data) {
- const data = res.data
- abilityResults.value = data.abilityResults || []
- radarData.value = data.radarChart || { categories: [], series: [] }
-
- // 如果没有总分,取平均分
- if (data.totalScore) {
- displayTotalScore.value = Math.round(Number(data.totalScore))
- } else if (radarData.value.series.length > 0) {
- const sum = radarData.value.series.reduce((s, val) => s + (Number(val) || 0), 0)
- displayTotalScore.value = Math.round(sum / radarData.value.series.length)
- }
-
- evaluationName.value = data.evaluationName || '测评报告'
- submitTime.value = new Date().toLocaleString()
- }
- } catch (e) {
- console.error('加载报告失败:', e)
- } finally {
- loading.value = false
- }
- }
- // 查看某个能力的答题详情
- const viewAbilityDetail = (item) => {
- if (item.inquireLink) {
- // 存入缓存防止长链接截断
- uni.setStorageSync('temp_report_url', item.inquireLink)
- uni.navigateTo({
- url: '/pages/assessment/quiz?from=report'
- })
- } else {
- uni.showToast({ title: '暂无详情', icon: 'none' })
- }
- }
- // 核心算法:计算多边形顶点坐标
- const getPoint = (angle, radius) => {
- const x = 50 + radius * Math.cos((angle - 90) * Math.PI / 180)
- const y = 50 + radius * Math.sin((angle - 90) * Math.PI / 180)
- return { x, y }
- }
- // 绘制背景五边形/多边形路径
- const getPentagonPath = (radius) => {
- const count = Math.max(radarData.value.categories.length, 3)
- const points = []
- for (let i = 0; i < count; i++) {
- const { x, y } = getPoint(i * (360 / count), radius)
- points.push(`${x}% ${y}%`)
- }
- return `polygon(${points.join(', ')})`
- }
- // 轴线样式
- const axesStyles = computed(() => {
- const count = Math.max(radarData.value.categories.length, 3)
- return Array.from({ length: count }).map((_, i) => ({
- transform: `rotate(${(i * (360 / count)) - 90}deg)`
- }))
- })
- // 动态数据路径
- const polygonClipPath = computed(() => {
- const series = radarData.value.series
- const count = series.length
- if (count < 3) return 'none'
-
- const points = series.map((score, i) => {
- // 假设满分 100,雷达图半径 50,最小保证 20% 半径让图形始终可见
- const minRadius = 20
- const rawRadius = (Math.min(Number(score) || 0, 100) / 100) * 50
- const radius = Math.max(rawRadius, minRadius)
- const { x, y } = getPoint(i * (360 / count), radius)
- return `${x}% ${y}%`
- })
- return `polygon(${points.join(', ')})`
- })
- // 节点和标签位置
- const nodePositions = computed(() => {
- const { categories, series } = radarData.value
- const count = categories.length
- return categories.map((name, i) => {
- const score = series[i]
- // 节点位置(为了美观,稍微往外推一点),同样保证最小半径
- const minRadius = 20
- const rawRadius = (Math.min(Number(score) || 0, 100) / 100) * 50
- const radius = Math.max(rawRadius, minRadius)
- const pos = getPoint(i * (360 / count), radius)
-
- // 标签偏移逻辑
- const angle = i * (360 / count)
- let labelStyle = { position: 'absolute' }
- if (angle === 0) labelStyle.bottom = '40rpx'
- else if (angle < 180) labelStyle.left = '40rpx'
- else labelStyle.right = '40rpx'
-
- return {
- x: pos.x,
- y: pos.y,
- name,
- score,
- labelStyle
- }
- })
- })
- </script>
- <style lang="scss" scoped>
- .loading-box { display: flex; justify-content: center; align-items: center; height: 100vh; color: #999; }
- .container { min-height: 100vh; background-color: #F8F9FB; display: flex; flex-direction: column; }
- .scroll-body { flex: 1; padding: 30rpx 40rpx; box-sizing: border-box; }
- .report-card { background: #FFF; border-radius: 32rpx; padding: 50rpx 40rpx; margin-bottom: 30rpx; box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.03); }
- /* 维度详情 */
- .detail-card {
- .section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 30rpx;
- .subtitle { font-size: 24rpx; color: #999; }
- }
- }
- .ability-detail-list {
- display: flex;
- flex-direction: column;
- gap: 24rpx;
- }
- .ability-detail-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 30rpx;
- background: #F8F9FB;
- border-radius: 20rpx;
- &:active { opacity: 0.8; }
-
- .item-left {
- display: flex;
- flex-direction: column;
- gap: 10rpx;
- .a-name { font-size: 30rpx; font-weight: bold; color: #333; }
- .status-tag {
- font-size: 20rpx;
- padding: 4rpx 12rpx;
- border-radius: 6rpx;
- width: fit-content;
- &.pass { background: rgba(82, 196, 26, 0.1); color: #52c41a; }
- &.fail { background: rgba(255, 77, 79, 0.1); color: #ff4d4f; }
- }
- }
-
- .item-right {
- display: flex;
- align-items: center;
- .a-score { font-size: 40rpx; font-weight: bold; color: #1F6CFF; }
- .a-unit { font-size: 24rpx; color: #999; margin-left: 4rpx; margin-right: 10rpx; }
- .arrow-icon { width: 32rpx; height: 32rpx; opacity: 0.3; }
- }
- }
- /* 仪表盘 */
- .gauge-container { height: 440rpx; display: flex; justify-content: center; align-items: center; }
- .gauge-main {
- width: 400rpx; height: 400rpx; position: relative;
- .gauge-base { position: absolute; inset: 0; border: 20rpx solid #F0F2F5; border-radius: 50%; clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%, 50% 50%); transform: rotate(135deg); }
- .gauge-progress-bar { position: absolute; inset: 0; border: 20rpx solid #1F6CFF; border-radius: 50%; clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%); transition: transform 1s cubic-bezier(0.4, 0, 0.2, 1); }
- .gauge-ticks { position: absolute; inset: 30rpx; .tick { position: absolute; top: 0; left: 50%; width: 2rpx; height: 12rpx; background: #EEE; transform-origin: center 130rpx; } }
- .gauge-needle-box { position: absolute; inset: 0; display: flex; justify-content: center; .needle { width: 16rpx; height: 110rpx; background: #1F6CFF; clip-path: polygon(50% 0, 100% 100%, 0 100%); margin-top: 50rpx; transform-origin: center 110rpx; transition: transform 1s cubic-bezier(0.4, 0, 0.2, 1); } }
- .score-display { position: absolute; bottom: 60rpx; left: 50%; transform: translateX(-50%); text-align: center; .num { font-size: 64rpx; font-weight: bold; color: #1F6CFF; display: block; line-height: 1; margin-bottom: 6rpx; } .total { font-size: 24rpx; color: #999; } }
- }
- .job-name { font-size: 40rpx; font-weight: bold; color: #1A1A1A; text-align: center; margin-top: 40rpx; margin-bottom: 30rpx; }
- .report-meta { display: flex; justify-content: space-between; font-size: 26rpx; color: #999; }
- /* 雷达图核心容器 */
- .spider-chart-box { height: 600rpx; width: 100%; position: relative; margin-top: 40rpx; display: flex; justify-content: center; align-items: center; }
- .pentagon-grids { position: absolute; inset: 0; display: flex; justify-content: center; align-items: center; .p-grid { position: absolute; width: 100%; height: 100%; background: none; border: 1.5rpx solid #F0F2F5; box-sizing: border-box; } }
- .axes { position: absolute; inset: 0; .axis { position: absolute; top: 50%; left: 50%; width: 50%; height: 1rpx; background: #F0F2F5; transform-origin: left center; } }
- .data-area {
- position: absolute; inset: 0;
- .data-polygon { position: absolute; inset: 0; background: rgba(31, 108, 255, 0.15); border: 3rpx solid #1F6CFF; transition: all 1s ease-in-out; }
- .node-point {
- position: absolute; width: 0; height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
- &::after { content: ''; position: absolute; width: 12rpx; height: 12rpx; background: #1F6CFF; border-radius: 50%; border: 4rpx solid #FFF; box-shadow: 0 0 10rpx rgba(31,108,255,0.3); transform: translate(-50%, -50%); }
- .v-label { font-size: 24rpx; color: #333; white-space: nowrap; }
- .v-score { font-size: 20rpx; color: #1F6CFF; white-space: nowrap; margin-top: 40rpx; }
- }
- }
- .card-anim { animation: slideUp 0.8s ease-out; }
- @keyframes slideUp { from { transform: translateY(30rpx); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
- </style>
|