261 lines
10 KiB
Vue
261 lines
10 KiB
Vue
<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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''')
|
||
}
|
||
|
||
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>
|