|
|
@@ -7,7 +7,7 @@
|
|
|
* 纸面:标题(品牌 · 报告标题)· 副题(窗 + 台数,对外时带标记)· 章节数 · 各块 · 事实附注 · 页脚。
|
|
|
* 默认章节库为空 ⇒ 走"预览为空"分支(与旧页一致)。各块正文由 `rptBlock` 渲染,见 `blockHtml`。
|
|
|
*/
|
|
|
-import { computed, reactive } from 'vue'
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
import { t } from '../i18n/zh'
|
|
|
import { winName } from '../lib/render'
|
|
|
import { esc } from '../lib/vib'
|
|
|
@@ -49,6 +49,15 @@ const opts = computed(() => {
|
|
|
: `<option value="">${localDown.value ? t('ask.local_down') : t('ask.no_models')}</option>`
|
|
|
})
|
|
|
|
|
|
+/** ★P82 供 v-for 渲染的章节库数据(与旧页 `libraryHtml` 的 DOM 逐项对应)。 */
|
|
|
+const libGroups = computed(() => RPT_GROUPS.map((g) => ({
|
|
|
+ g,
|
|
|
+ label: t('rpt.g.' + g),
|
|
|
+ items: Object.keys(RPT_BLOCKS).filter((k) => RPT_BLOCKS[k] === g).map((k) => ({
|
|
|
+ k, icon: RPT_ICON[k] || '', on: on(k), name: t('rpt.b.' + k + '.nm'),
|
|
|
+ })),
|
|
|
+})))
|
|
|
+
|
|
|
/** 旧 `RPT_GROUPS.map(...)`:每组一行"组名 + 该组块按钮" */
|
|
|
const libraryHtml = computed(() => RPT_GROUPS.map((g) =>
|
|
|
`<div class="small muted" style="text-align:right;white-space:nowrap">${t('rpt.g.' + g)}</div>`
|
|
|
@@ -84,6 +93,89 @@ const outText = computed(() => {
|
|
|
|
|
|
/** 旧:数据时点只认 `YYYY-MM-DD`(该字段可能是一句说明) */
|
|
|
const asof = computed(() => (String(props.d?.fus?.数据时点 || '').match(/\d{4}-\d{2}-\d{2}/) || [''])[0])
|
|
|
+/** ★P82 照旧页 `bindReport()`(app.js:599-643)逐条接线 —— 此前该页模板没有任何 @click,按钮自然全不可用。 */
|
|
|
+function applyPreset(k: string): void {
|
|
|
+ RPT.canvas = (RPT_PRESETS[k] || []).slice()
|
|
|
+ void ensureComposeProbe()
|
|
|
+}
|
|
|
+function toggleBlock(k: string): void {
|
|
|
+ const i = RPT.canvas.indexOf(k)
|
|
|
+ if (i >= 0) RPT.canvas.splice(i, 1)
|
|
|
+ else RPT.canvas.push(k)
|
|
|
+ hintText.value = t('rpt.hint_idle')
|
|
|
+ void ensureComposeProbe()
|
|
|
+}
|
|
|
+function hoverBlock(k: string): void {
|
|
|
+ hintText.value = t('rpt.b.' + k + '.nm') + ' — ' + t('rpt.b.' + k + '.d')
|
|
|
+}
|
|
|
+function clearHover(): void { hintText.value = t('rpt.hint_idle') }
|
|
|
+function mv(k: string, act: string): void {
|
|
|
+ const i = RPT.canvas.indexOf(k)
|
|
|
+ if (i < 0) return
|
|
|
+ if (act === 'del') { RPT.canvas.splice(i, 1); return }
|
|
|
+ const j = i + (act === 'up' ? -1 : 1)
|
|
|
+ if (j < 0 || j >= RPT.canvas.length) return
|
|
|
+ const tmp = RPT.canvas[i]
|
|
|
+ RPT.canvas[i] = RPT.canvas[j]
|
|
|
+ RPT.canvas[j] = tmp
|
|
|
+}
|
|
|
+function onAud(ev: Event): void { RPT.aud = (ev.target as HTMLSelectElement).value }
|
|
|
+function printPaper(): void { try { window.print() } catch { /* ignore */ } }
|
|
|
+function exportAs(fmtx: string): void {
|
|
|
+ if (!RPT.canvas.length) { try { window.alert(t('rpt.empty_canvas')) } catch { /* ignore */ } ; return }
|
|
|
+ const narr = RPT.narr && RPT.narr.text ? RPT.narr.text : ''
|
|
|
+ const url = '/api/rpt_export?fmt=' + fmtx + '&blocks=' + encodeURIComponent(RPT.canvas.join(','))
|
|
|
+ + '&aud=' + RPT.aud + '&win=' + encodeURIComponent(String((props.d && props.d.win) || ''))
|
|
|
+ + '&narr=' + encodeURIComponent(narr)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = ''
|
|
|
+ document.body.appendChild(a)
|
|
|
+ a.click()
|
|
|
+ a.remove()
|
|
|
+}
|
|
|
+let composeProbed = false
|
|
|
+async function ensureComposeProbe(): Promise<void> {
|
|
|
+ if (composeProbed) return
|
|
|
+ composeProbed = true
|
|
|
+ try {
|
|
|
+ const r = await fetch('/api/rpt_compose?probe=1')
|
|
|
+ RPT.compose = !!(r && r.ok)
|
|
|
+ } catch { RPT.compose = false }
|
|
|
+}
|
|
|
+async function compose(): Promise<void> {
|
|
|
+ const need = String(RPT.need || '')
|
|
|
+ if (!need.trim()) { composeOut.value = t('rpt.need_empty'); return }
|
|
|
+ composing.value = true
|
|
|
+ composeOut.value = t('rpt.composing')
|
|
|
+ let d: any
|
|
|
+ try {
|
|
|
+ const mdl = RPT.model || ''
|
|
|
+ d = await (await fetch('/api/rpt_compose?need=' + encodeURIComponent(need)
|
|
|
+ + '&canvas=' + encodeURIComponent(RPT.canvas.join(',')) + '&model=' + encodeURIComponent(mdl))).json()
|
|
|
+ } catch (e) {
|
|
|
+ composeOut.value = t('rpt.compose_fail', { e: (e as any)?.message || String(e) })
|
|
|
+ composing.value = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ composing.value = false
|
|
|
+ if (d.err) { composeOut.value = String(d.err); return }
|
|
|
+ RPT.narr = { text: d.narrative, model: d.model, ungrounded: d.ungrounded || [] }
|
|
|
+ const bl = (d.blocks || []).filter((b: string) => RPT_BLOCKS[b])
|
|
|
+ if (bl.length) RPT.canvas = (d.narrative ? ['narr'] : []).concat(bl)
|
|
|
+ else if (d.narrative && !RPT.canvas.includes('narr')) RPT.canvas.unshift('narr')
|
|
|
+ composeOut.value = t('rpt.composed', { n: bl.length, narr: d.narrative ? t('rpt.narr_plus') : '', m: d.model || '', s: d.secs })
|
|
|
+}
|
|
|
+function onModelChange(ev: Event): void { RPT.model = (ev.target as HTMLSelectElement).value }
|
|
|
+function onNeedInput(ev: Event): void { RPT.need = (ev.target as HTMLInputElement).value }
|
|
|
+const hintText = ref<string>(t('rpt.hint_idle'))
|
|
|
+const composing = ref<boolean>(false)
|
|
|
+const composeOut = ref<string>('')
|
|
|
+onMounted(() => {
|
|
|
+ void ensureComposeProbe()
|
|
|
+ // 旧页还会取 /api/facts 与 /api/ask_models(模型清单经 props.apis 已可用)
|
|
|
+})
|
|
|
+
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
@@ -92,26 +184,36 @@ const asof = computed(() => (String(props.d?.fus?.数据时点 || '').match(/\d{
|
|
|
<div class="panel mb noprint">
|
|
|
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
|
|
|
<span class="small" style="font-weight:600">{{ t('rpt.presets') }}</span>
|
|
|
- <button v-for="k in Object.keys(RPT_PRESETS)" :key="k" class="pill" :data-preset="k">{{ t('rpt.preset.' + k) }}</button>
|
|
|
+ <button v-for="k in Object.keys(RPT_PRESETS)" :key="k" class="pill" :data-preset="k" @click="applyPreset(k)">{{ t('rpt.preset.' + k) }}</button>
|
|
|
<span style="flex:1"></span>
|
|
|
- <select id="rptaud">
|
|
|
+ <select id="rptaud" @change="onAud">
|
|
|
<option value="in" :selected="RPT.aud === 'in'">{{ t('rpt.aud_in') }}</option>
|
|
|
<option value="up" :selected="RPT.aud === 'up'">{{ t('rpt.aud_up') }}</option>
|
|
|
</select>
|
|
|
- <button class="btn quiet" id="rptprint">{{ t('rpt.print') }}</button><button class="btn quiet" data-export="docx">{{ t('rpt.export_docx') }}</button><button class="btn quiet" data-export="xlsx">{{ t('rpt.export_xlsx') }}</button>
|
|
|
+ <button class="btn quiet" id="rptprint" @click="printPaper">{{ t('rpt.print') }}</button><button class="btn quiet" data-export="docx" @click="exportAs('docx')">{{ t('rpt.export_docx') }}</button><button class="btn quiet" data-export="xlsx" @click="exportAs('xlsx')">{{ t('rpt.export_xlsx') }}</button>
|
|
|
<span v-if="RPT.aud === 'up'" class="small" style="color:var(--warning);flex-basis:100%">{{ t('rpt.up_note') }}</span>
|
|
|
</div>
|
|
|
<div class="small" style="font-weight:600;margin-bottom:7px">{{ t('rpt.library') }} <span style="font-weight:400;color:var(--muted)">{{ t('rpt.library_note', { n: RPT.canvas.length }) }}</span></div>
|
|
|
- <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 12px;align-items:center" v-html="libraryHtml"></div>
|
|
|
- <div id="rpthint" class="small muted" style="margin-top:9px;padding-top:8px;border-top:1px solid var(--line);min-height:19px">{{ t('rpt.hint_idle') }}</div>
|
|
|
+ <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 12px;align-items:center">
|
|
|
+ <template v-for="grp in libGroups" :key="grp.g">
|
|
|
+ <div class="small muted" style="text-align:right;padding-top:3px">{{ grp.label }}</div>
|
|
|
+ <div style="display:flex;flex-wrap:wrap;gap:6px">
|
|
|
+ <button v-for="it in grp.items" :key="it.k" class="pill" :class="{ on: it.on }" :data-block="it.k"
|
|
|
+ :style="{ borderRadius: '7px', padding: '5px 11px', fontSize: '12.5px', opacity: it.on ? 1 : 0.8 }"
|
|
|
+ @click="toggleBlock(it.k)" @mouseenter="hoverBlock(it.k)" @mouseleave="clearHover">
|
|
|
+ {{ it.icon }} {{ it.name }}</button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <div id="rpthint" class="small muted" style="margin-top:9px;padding-top:8px;border-top:1px solid var(--line);min-height:19px">{{ hintText }}</div>
|
|
|
<div style="margin-top:11px;padding-top:10px;border-top:1px solid var(--line)">
|
|
|
<div class="small" style="font-weight:600;margin-bottom:6px">{{ t('rpt.need_head') }} <span id="rptmdlnote" style="font-weight:400;color:var(--muted)" v-html="needHint"></span></div>
|
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
|
|
- <input id="rptneed" :placeholder="esc(t('rpt.need_ph'))" :value="RPT.need" style="flex:1 1 300px;min-width:220px">
|
|
|
- <select id="rptmdl" v-html="opts"></select>
|
|
|
- <button class="btn" id="rptgo" :disabled="!canCompose">{{ t('rpt.compose') }}</button>
|
|
|
+ <input id="rptneed" :placeholder="esc(t('rpt.need_ph'))" :value="RPT.need" style="flex:1 1 300px;min-width:220px" @input="onNeedInput" @keydown.enter="compose">
|
|
|
+ <select id="rptmdl" v-html="opts" @change="onModelChange"></select>
|
|
|
+ <button class="btn" id="rptgo" :disabled="!canCompose" @click="compose">{{ t('rpt.compose') }}</button>
|
|
|
</div>
|
|
|
- <div id="rptout" class="small muted" style="margin-top:7px">{{ outText }}</div>
|
|
|
+ <div id="rptout" class="small muted" style="margin-top:7px">{{ composeOut || outText }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-if="!RPT.canvas.length" class="paper" style="text-align:center;padding:56px 40px">
|
|
|
@@ -127,9 +229,9 @@ const asof = computed(() => (String(props.d?.fus?.数据时点 || '').match(/\d{
|
|
|
</div>
|
|
|
<template v-for="(k, i) in RPT.canvas" :key="k">
|
|
|
<div v-if="RPT_BLOCKS[k]" class="noprint" style="display:flex;gap:3px;justify-content:flex-end;margin-bottom:-6px;position:relative;z-index:2">
|
|
|
- <button class="pill" data-mv="up" :data-k="k" :disabled="i === 0" style="padding:1px 8px;font-size:11px;opacity:.7">↑</button>
|
|
|
- <button class="pill" data-mv="down" :data-k="k" :disabled="i === RPT.canvas.length - 1" style="padding:1px 8px;font-size:11px;opacity:.7">↓</button>
|
|
|
- <button class="pill" data-mv="del" :data-k="k" style="padding:1px 8px;font-size:11px;opacity:.7">×</button>
|
|
|
+ <button class="pill" data-mv="up" :data-k="k" :disabled="i === 0" style="padding:1px 8px;font-size:11px;opacity:.7" @click="mv(k, 'up')">↑</button>
|
|
|
+ <button class="pill" data-mv="down" :data-k="k" :disabled="i === RPT.canvas.length - 1" style="padding:1px 8px;font-size:11px;opacity:.7" @click="mv(k, 'down')">↓</button>
|
|
|
+ <button class="pill" data-mv="del" :data-k="k" style="padding:1px 8px;font-size:11px;opacity:.7" @click="mv(k, 'del')">×</button>
|
|
|
</div>
|
|
|
<div v-if="RPT_BLOCKS[k]" v-html="blockHtml(k)"></div>
|
|
|
</template>
|