xinli/xinlidsj/pages/report/detail.vue
2026-02-24 16:49:05 +08:00

261 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page" :class="{ big: isH5 }">
<view class="panel">
<view class="title">报告详情</view>
<view class="subtitle">{{ metaTitle }}</view>
</view>
<view v-if="loading" class="placeholder">加载中...</view>
<view v-else>
<view v-if="errorMsg" class="panel">
<view class="error">{{ errorMsg }}</view>
</view>
<view v-else class="panel">
<view class="kv">
<view class="kv-item"><text class="k">来源</text><text class="v">{{ sourceTypeLabel }}</text></view>
<view class="kv-item"><text class="k">报告ID</text><text class="v">{{ reportId || '—' }}</text></view>
<view class="kv-item"><text class="k">生成时间</text><text class="v">{{ formatTime(report.generateTime) }}</text></view>
</view>
<view class="section-title">内容</view>
<view class="content">
<rich-text :nodes="htmlNodes"></rich-text>
<view v-if="htmlFallback" class="hint">内容不是标准HTML已自动按文本格式展示。</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getReport } from '../../api/psychology/report'
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function normalizeHtml(raw) {
let html = raw == null ? '' : String(raw)
const hasHtmlDoc = /<\s*html[\s\S]*?>/i.test(html)
if (hasHtmlDoc) {
const bodyMatch = html.match(/<\s*body[\s\S]*?>([\s\S]*?)<\s*\/\s*body\s*>/i)
if (bodyMatch && bodyMatch[1] != null) {
html = bodyMatch[1]
}
}
// Some miniapp rich-text runtimes ignore <style> tags; inject critical styles inline.
const applyInlineHeadingStyle = (input) => {
let out = input
const rules = {
1: 'font-size:22px;margin:14px 0 10px;line-height:1.55;font-weight:800;',
2: 'font-size:18px;margin:14px 0 10px;line-height:1.6;font-weight:800;',
3: 'font-size:16px;margin:12px 0 8px;line-height:1.65;font-weight:800;',
4: 'font-size:15px;margin:10px 0 8px;line-height:1.7;font-weight:800;'
}
out = out.replace(/<h([1-4])([^>]*)>/gi, (m, level, attrs) => {
const base = 'display:block;white-space:normal;word-break:break-word;overflow-wrap:anywhere;'
const styleRule = (rules[level] || '') + base
const a = attrs || ''
if (/\sstyle\s*=\s*['"][^'"]*['"]/i.test(a)) {
return `<h${level}${a.replace(/\sstyle\s*=\s*(['"])([^'"]*)\1/i, (sm, q, s) => ` style=${q}${s};${styleRule}${q}`)}>`
}
return `<h${level}${a} style="${styleRule}">`
})
return out
}
const baseCss = `
.report-wrap{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;line-height:1.75;color:#111827;word-break:break-word;font-size:15px;}
.report-wrap h1,.report-wrap h2,.report-wrap h3,.report-wrap h4{display:block;white-space:normal;word-break:break-word;overflow-wrap:anywhere;}
.report-wrap h1{font-size:22px;margin:14px 0 10px;line-height:1.45;font-weight:800;}
.report-wrap h2{font-size:18px;margin:14px 0 10px;line-height:1.5;font-weight:800;}
.report-wrap h3{font-size:16px;margin:12px 0 8px;line-height:1.55;font-weight:800;}
.report-wrap h4{font-size:15px;margin:10px 0 8px;line-height:1.6;font-weight:800;}
.report-wrap p{margin:10px 0;}
.report-wrap ul,.report-wrap ol{padding-left:20px;margin:10px 0;}
.report-wrap li{margin:6px 0;}
.report-wrap img{max-width:100%;height:auto;border-radius:10px;}
.report-wrap hr{border:0;border-top:1px solid rgba(15,23,42,0.08);margin:14px 0;}
.report-wrap blockquote{margin:12px 0;padding:10px 12px;border-left:4px solid rgba(22,119,255,0.5);background:rgba(22,119,255,0.06);border-radius:10px;}
.report-wrap pre{white-space:pre-wrap;word-break:break-word;background:rgba(15,23,42,0.04);padding:12px;border-radius:10px;}
.report-wrap table{width:100%;border-collapse:collapse;}
.report-wrap .table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;border-radius:12px;border:1px solid rgba(15,23,42,0.06);}
.report-wrap .table-scroll table{min-width:520px;}
.report-wrap th,.report-wrap td{border:1px solid rgba(15,23,42,0.08);padding:10px;vertical-align:top;word-break:break-word;}
.report-wrap th{background:rgba(15,23,42,0.04);font-weight:800;}
`;
// wrap tables for horizontal scrolling on mobile
html = html.replace(/<table([\s\S]*?)>([\s\S]*?)<\/table>/gi, (m) => {
return `<div class="table-scroll">${m}</div>`
})
html = applyInlineHeadingStyle(html)
return `<div class="report-wrap"><style>${baseCss}</style>${html}</div>`
}
export default {
data() {
return {
isH5: false,
loading: false,
errorMsg: '',
reportId: '',
sourceType: '',
report: {},
htmlNodes: '',
htmlFallback: false
}
},
computed: {
metaTitle() {
return (this.report && (this.report.reportTitle || this.report.reportType))
? `${this.report.reportTitle || ''}${this.report.reportType ? ' · ' + this.report.reportType : ''}`
: '—'
},
sourceTypeLabel() {
if (this.sourceType === 'questionnaire') return '问卷'
if (this.sourceType === 'assessment') return '测评'
return this.sourceType || '—'
}
},
onLoad(options) {
try {
const info = uni.getSystemInfoSync()
const p = info ? (info.uniPlatform || info.platform) : ''
this.isH5 = p === 'web' || p === 'h5'
} catch (e) {
this.isH5 = false
}
this.reportId = options && options.reportId ? options.reportId : ''
this.sourceType = options && options.sourceType ? options.sourceType : ''
if (!this.reportId) {
this.errorMsg = '缺少 reportId'
return
}
this.fetchDetail()
},
methods: {
formatTime(val) {
if (!val) return '—'
try {
const d = new Date(val)
if (isNaN(d.getTime())) return '—'
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${hh}:${mm}`
} catch (e) {
return '—'
}
},
buildRender(content) {
const raw = content == null ? '' : String(content)
// HTML如果像HTML就直接渲染否则用 <pre> 包一层,避免 rich-text 解析异常
const looksLikeHtml = /<\s*\w+[\s\S]*>/.test(raw)
if (looksLikeHtml) {
this.htmlNodes = normalizeHtml(raw)
this.htmlFallback = false
return
}
this.htmlNodes = normalizeHtml(`<pre>${escapeHtml(raw)}</pre>`)
this.htmlFallback = true
},
fetchDetail() {
this.loading = true
this.errorMsg = ''
const tryTypes = this.sourceType ? [this.sourceType] : ['', 'assessment', 'questionnaire']
const tryFetch = (idx) => {
if (idx >= tryTypes.length) {
this.loading = false
if (!this.errorMsg) this.errorMsg = '加载失败'
return Promise.resolve()
}
const st = tryTypes[idx]
return getReport(this.reportId, st)
.then((res) => {
const data = res && res.data ? res.data : null
if (!data || data.code !== 200) {
this.errorMsg = (data && data.msg) ? data.msg : '加载失败'
return tryFetch(idx + 1)
}
const report = data.data || {}
const content = report && report.reportContent != null ? String(report.reportContent) : ''
if (!content && idx + 1 < tryTypes.length) {
this.errorMsg = ''
return tryFetch(idx + 1)
}
this.loading = false
this.report = report
this.sourceType = st || (report.sourceType || this.sourceType)
this.buildRender(this.report.reportContent)
})
.catch((e) => {
this.errorMsg = e && e.message ? e.message : '网络错误'
return tryFetch(idx + 1)
})
}
return tryFetch(0)
}
}
}
</script>
<style>
.page { min-height: 100vh; padding: 32rpx; box-sizing: border-box; background: #f6f7fb; }
.page.big {
padding: 14rpx 14rpx 120rpx;
background-image:
radial-gradient(1100rpx 520rpx at 50% 14%, rgba(43, 107, 255, 0.30) 0%, rgba(6, 16, 40, 0.0) 65%),
linear-gradient(180deg, rgba(5, 11, 24, 0.90) 0%, rgba(8, 20, 45, 0.85) 42%, rgba(6, 16, 40, 0.92) 100%),
url('/static/bg.png');
background-size: auto, auto, cover;
background-position: center, center, center;
background-repeat: no-repeat, no-repeat, no-repeat;
}
.panel { background: #fff; border-radius: 20rpx; padding: 24rpx; border: 1px solid rgba(0,0,0,0.05); margin-bottom: 24rpx; }
.title { font-size: 36rpx; font-weight: 700; color: #1f2329; }
.subtitle { margin-top: 10rpx; font-size: 24rpx; color: #646a73; line-height: 36rpx; }
.section-title { margin-top: 18rpx; font-size: 28rpx; font-weight: 700; color: #1f2329; }
.kv-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1px solid rgba(0,0,0,0.06); }
.kv-item:last-child { border-bottom: 0; }
.k { color: #8f959e; font-size: 24rpx; }
.v { color: #1f2329; font-size: 24rpx; font-weight: 700; }
.mode-switch { display: flex; margin-top: 14rpx; }
.chip { padding: 10rpx 18rpx; border-radius: 999rpx; font-size: 24rpx; color: #646a73; background: #f7f8fa; margin-right: 14rpx; }
.chip.active { color: #1677ff; background: rgba(22,119,255,0.12); }
.content { margin-top: 14rpx; font-size: 26rpx; color: #1f2329; line-height: 44rpx; }
.placeholder { height: 240rpx; border-radius: 20rpx; background: #fff; border: 1px dashed rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; color: #8f959e; font-size: 24rpx; }
.error { font-size: 24rpx; color: #ff4d4f; line-height: 36rpx; }
.hint { margin-top: 10rpx; font-size: 22rpx; color: #8f959e; }
.page.big .panel,
.page.big .placeholder {
border: 1px solid rgba(116, 216, 255, 0.22);
background: linear-gradient(180deg, rgba(10, 18, 38, 0.75) 0%, rgba(5, 10, 22, 0.55) 100%);
box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.35);
}
.page.big .title,
.page.big .section-title,
.page.big .v { color: rgba(235, 248, 255, 0.92); }
.page.big .subtitle,
.page.big .k,
.page.big .hint,
.page.big .placeholder { color: rgba(201, 242, 255, 0.65); }
.page.big .placeholder { border-style: solid; }
.page.big .kv-item { border-bottom-color: rgba(116, 216, 255, 0.12); }
.page.big .chip { color: rgba(201, 242, 255, 0.70); background: rgba(10, 18, 38, 0.65); border: 1px solid rgba(116, 216, 255, 0.20); }
.page.big .chip.active { color: #0b1226; background: linear-gradient(90deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.90) 100%); border-color: rgba(116, 216, 255, 0.5); }
.page.big .content { color: rgba(235, 248, 255, 0.88); }
</style>