lambo-design-bi 0.0.248 → 0.0.251
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
<!-- eslint-disable -->
|
|
2
2
|
<template>
|
|
3
3
|
<div class="bulletin-widget">
|
|
4
|
-
<div class="bulletin-header" v-if="hasTitle">
|
|
5
|
-
<div class="bulletin-title"
|
|
6
|
-
|
|
4
|
+
<div class="bulletin-header" v-if="hasTitle || previewUrl">
|
|
5
|
+
<div class="bulletin-title-wrap" v-if="hasTitle">
|
|
6
|
+
<div class="bulletin-title">{{ cardTitle }}</div>
|
|
7
|
+
<div class="bulletin-subtitle" v-if="subTitle">{{ subTitle }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
<div
|
|
10
|
+
v-if="previewUrl"
|
|
11
|
+
class="bulletin-download-btn"
|
|
12
|
+
title="下载简报"
|
|
13
|
+
@click="doDownload">
|
|
14
|
+
<Icon type="md-download" class="btn-icon" />
|
|
15
|
+
<span class="btn-text">下载</span>
|
|
16
|
+
</div>
|
|
7
17
|
</div>
|
|
8
18
|
|
|
9
19
|
<div
|
|
@@ -21,25 +31,19 @@
|
|
|
21
31
|
<p>尚未发布简报</p>
|
|
22
32
|
<p class="hint">该期间/组织暂无已发布的简报成品</p>
|
|
23
33
|
</div>
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</div>
|
|
31
|
-
<lambo-doc-word
|
|
32
|
-
:url="previewUrl"
|
|
33
|
-
:key="previewUrl"
|
|
34
|
-
:report="summary" />
|
|
35
|
-
</template>
|
|
34
|
+
<lambo-doc-word
|
|
35
|
+
v-else
|
|
36
|
+
:url="previewUrl"
|
|
37
|
+
:key="previewUrl"
|
|
38
|
+
:report="summary"
|
|
39
|
+
:publish-time="publishTime" />
|
|
36
40
|
</div>
|
|
37
41
|
</div>
|
|
38
42
|
</template>
|
|
39
43
|
<script>
|
|
40
44
|
import LamboDocWord from '../../../pub/doc/word/index.vue'
|
|
41
45
|
import { parseStyleStr } from '../util/css-util'
|
|
42
|
-
import {
|
|
46
|
+
import { listBulletinPublishDoc } from './api/bulletin'
|
|
43
47
|
import { fileGetUrl } from '../api/config'
|
|
44
48
|
|
|
45
49
|
// 统一日志前缀,便于在控制台按关键字过滤简报组件的关键过程
|
|
@@ -67,6 +71,24 @@ const PERIOD_KEY_CANDIDATES = {
|
|
|
67
71
|
const COM_ID_CANDIDATES = ['comId', 'com_id']
|
|
68
72
|
const DIST_CT_ID_CANDIDATES = ['distCtId', 'dist_ct_id']
|
|
69
73
|
|
|
74
|
+
// 规范化 publishTime 字符串:兼容后端可能返回的多种格式
|
|
75
|
+
// - "2026-07-30 17:14:22" / "2026-07-30T17:14:22" → 原样
|
|
76
|
+
// - "20260730171422" 14 位紧凑 → "2026-07-30 17:14:22"
|
|
77
|
+
// - 其他 → 原样
|
|
78
|
+
function normalizePublishTime(v) {
|
|
79
|
+
if (v == null) return ""
|
|
80
|
+
const s = String(v).trim()
|
|
81
|
+
if (!s) return ""
|
|
82
|
+
// 已经是带分隔符的格式
|
|
83
|
+
if (/[-/]/.test(s)) return s
|
|
84
|
+
// 14 位紧凑:YYYYMMDDHHmmss
|
|
85
|
+
const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(s)
|
|
86
|
+
if (m) {
|
|
87
|
+
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`
|
|
88
|
+
}
|
|
89
|
+
return s
|
|
90
|
+
}
|
|
91
|
+
|
|
70
92
|
// 从 params 里按候选 key 取首个非空项,返回命中的 { key, value };均未命中返回 null
|
|
71
93
|
function firstNonBlankEntry(obj, keys) {
|
|
72
94
|
if (!obj || !Array.isArray(keys)) return null
|
|
@@ -101,10 +123,11 @@ export default {
|
|
|
101
123
|
previewUrl: '',
|
|
102
124
|
lastDocId: '',
|
|
103
125
|
lastFileName: '',
|
|
126
|
+
// 简报生成时间(plat_bulletin_publish_doc.publish_time)
|
|
127
|
+
publishTime: '',
|
|
104
128
|
// 空态原因:'' 无 / 'invalid' 查询条件不足(校验未过)/ 'empty' 已查询但无成品
|
|
105
129
|
emptyReason: '',
|
|
106
|
-
//
|
|
107
|
-
summaryLoading: false,
|
|
130
|
+
// 摘要:取自已发布成品行的 sum_json(plat_bulletin_publish_doc.sum_json),缺省为 null
|
|
108
131
|
summaryData: null
|
|
109
132
|
}
|
|
110
133
|
},
|
|
@@ -121,8 +144,8 @@ export default {
|
|
|
121
144
|
// 右侧摘要数据源:data 阶段拉取的 summaryData,缺省用空骨架
|
|
122
145
|
summary() {
|
|
123
146
|
const s = this.summaryData
|
|
124
|
-
if (!s || typeof s !== 'object') return { ...EMPTY_SUMMARY, title:
|
|
125
|
-
return Object.assign({}, EMPTY_SUMMARY, { title:
|
|
147
|
+
if (!s || typeof s !== 'object') return { ...EMPTY_SUMMARY, title: '摘要' }
|
|
148
|
+
return Object.assign({}, EMPTY_SUMMARY, s, { title: '摘要' })
|
|
126
149
|
}
|
|
127
150
|
},
|
|
128
151
|
watch: {
|
|
@@ -160,6 +183,7 @@ export default {
|
|
|
160
183
|
this.lastDocId = ''
|
|
161
184
|
this.lastFileName = ''
|
|
162
185
|
this.previewUrl = ''
|
|
186
|
+
this.publishTime = ''
|
|
163
187
|
this.emptyReason = reason || ''
|
|
164
188
|
},
|
|
165
189
|
loadDoc(params) {
|
|
@@ -225,11 +249,13 @@ export default {
|
|
|
225
249
|
console.log(LOG, 'loadDoc 命中已发布成品,直接渲染', { docId: doc.docId, docName: doc.docName })
|
|
226
250
|
this.lastDocId = doc.docId
|
|
227
251
|
this.lastFileName = doc.docName || (this.cardTitle + '.docx')
|
|
252
|
+
// 与右侧摘要 meta 保持统一格式(左侧语义为"简报发布时间":数据源 plat_bulletin_publish_doc.publish_time)
|
|
253
|
+
this.publishTime = doc.publishTime ? `简报发布时间:${normalizePublishTime(doc.publishTime)}` : ''
|
|
228
254
|
this.previewUrl = fileGetUrl + doc.docId
|
|
229
255
|
this.emptyReason = ''
|
|
230
256
|
this.$Message.success({ content: '查询到已发布的简报', duration: 1.8 })
|
|
231
|
-
//
|
|
232
|
-
this.
|
|
257
|
+
// 命中成品:直接用该行的 sum_json 作为右侧摘要,不再另发请求
|
|
258
|
+
this.applySummaryFromDoc(doc)
|
|
233
259
|
return
|
|
234
260
|
}
|
|
235
261
|
// 未命中已发布成品 → 空态,同时清空摘要(不再查摘要)
|
|
@@ -264,42 +290,37 @@ export default {
|
|
|
264
290
|
console.log(LOG, 'resolvePeriodEntry', { periodType, candidates, entry })
|
|
265
291
|
return entry
|
|
266
292
|
},
|
|
267
|
-
// data
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
293
|
+
// data 阶段之二:从已发布成品行读取右侧摘要(plat_bulletin_publish_doc.sum_json)
|
|
294
|
+
// sum_json 是发布时缓存的摘要快照;为空/解析失败则摘要置空(右侧面板自动隐藏)
|
|
295
|
+
applySummaryFromDoc(doc) {
|
|
296
|
+
const raw = doc && doc.sumJson
|
|
297
|
+
if (!raw || typeof raw !== 'string') {
|
|
298
|
+
console.log(LOG, 'applySummaryFromDoc 摘要为空,置空', { docId: doc && doc.docId })
|
|
272
299
|
this.summaryData = null
|
|
273
300
|
return
|
|
274
301
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.then(res => {
|
|
279
|
-
const body = res && res.data
|
|
280
|
-
if (!body || body.code !== 1) {
|
|
281
|
-
console.warn(LOG, 'loadSummary 失败:', body)
|
|
282
|
-
this.summaryData = null
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
this.summaryData = (body.data && typeof body.data === 'object') ? body.data : null
|
|
286
|
-
const s = this.summaryData || {}
|
|
287
|
-
console.log(LOG, 'loadSummary 成功', {
|
|
288
|
-
conclusions: (s.conclusions || []).length,
|
|
289
|
-
metrics: (s.metrics || []).length,
|
|
290
|
-
highlights: (s.highlights || []).length,
|
|
291
|
-
risks: (s.risks || []).length,
|
|
292
|
-
advices: (s.advices || []).length,
|
|
293
|
-
warnings: (s.warnings || []).length
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
.catch(err => {
|
|
297
|
-
console.error(LOG, 'loadSummary 异常:', err)
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(raw)
|
|
304
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
298
305
|
this.summaryData = null
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
// 库内 meta 是生成时的"统计时间",发布后以发布人为准(发布时间已在左侧目录下方展示)
|
|
309
|
+
const publisher = doc.publishUserName ? String(doc.publishUserName).trim() : ''
|
|
310
|
+
parsed.meta = publisher ? ('发布人:' + publisher) : ''
|
|
311
|
+
this.summaryData = parsed
|
|
312
|
+
console.log(LOG, 'applySummaryFromDoc 摘要已加载', {
|
|
313
|
+
docId: doc.docId,
|
|
314
|
+
conclusions: (parsed.conclusions || []).length,
|
|
315
|
+
metrics: (parsed.metrics || []).length,
|
|
316
|
+
highlights: (parsed.highlights || []).length,
|
|
317
|
+
risks: (parsed.risks || []).length,
|
|
318
|
+
advices: (parsed.advices || []).length
|
|
299
319
|
})
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
} catch (e) {
|
|
321
|
+
console.warn(LOG, 'applySummaryFromDoc sum_json 解析失败,置空:', e)
|
|
322
|
+
this.summaryData = null
|
|
323
|
+
}
|
|
303
324
|
},
|
|
304
325
|
// 下载:用当前展示的已发布成品 docId
|
|
305
326
|
doDownload() {
|
|
@@ -349,9 +370,16 @@ export default {
|
|
|
349
370
|
.bulletin-widget {
|
|
350
371
|
width: 100%;
|
|
351
372
|
}
|
|
352
|
-
/*
|
|
373
|
+
/* 自绘标题栏:左标题右按钮,无边框,仅与下方内容留间距 */
|
|
353
374
|
.bulletin-header {
|
|
354
|
-
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
justify-content: space-between;
|
|
378
|
+
gap: 12px;
|
|
379
|
+
padding: 0 0 3px;
|
|
380
|
+
}
|
|
381
|
+
.bulletin-title-wrap {
|
|
382
|
+
min-width: 0;
|
|
355
383
|
}
|
|
356
384
|
.bulletin-title {
|
|
357
385
|
font-size: 16px;
|
|
@@ -367,44 +395,30 @@ export default {
|
|
|
367
395
|
}
|
|
368
396
|
.bulletin-body {
|
|
369
397
|
width: 100%;
|
|
370
|
-
/*
|
|
371
|
-
|
|
372
|
-
/* 有文档预览时给 lambo-doc-word 一个明确的高度基线,其内部 word-preview 使用 height:100% */
|
|
373
|
-
height: 720px;
|
|
398
|
+
/* 视口自适应高度:吃满屏幕剩余空间,大屏不留白;减去的值覆盖页头/查询栏/简报标题栏/边距 */
|
|
399
|
+
height: calc(100vh - 210px);
|
|
374
400
|
min-height: 480px;
|
|
375
401
|
}
|
|
376
|
-
/*
|
|
377
|
-
.bulletin-download-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
right: 16px;
|
|
381
|
-
z-index: 10;
|
|
382
|
-
width: 40px;
|
|
383
|
-
height: 40px;
|
|
384
|
-
display: flex;
|
|
402
|
+
/* 下载按钮:标题栏右侧,文字链接式,主题蓝可点,hover 加深 */
|
|
403
|
+
.bulletin-download-btn {
|
|
404
|
+
flex-shrink: 0;
|
|
405
|
+
display: inline-flex;
|
|
385
406
|
align-items: center;
|
|
386
|
-
|
|
387
|
-
font-size:
|
|
407
|
+
gap: 4px;
|
|
408
|
+
font-size: 13px;
|
|
409
|
+
font-weight: 500;
|
|
388
410
|
color: #2d8cf0;
|
|
389
411
|
cursor: pointer;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
-
|
|
394
|
-
border: 1px solid rgba(45, 140, 240, 0.15);
|
|
395
|
-
box-shadow: 0 4px 14px rgba(31, 45, 61, 0.12);
|
|
396
|
-
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
412
|
+
transition: color 0.18s ease;
|
|
413
|
+
}
|
|
414
|
+
.bulletin-download-btn .btn-icon {
|
|
415
|
+
font-size: 16px;
|
|
397
416
|
}
|
|
398
|
-
.bulletin-download-
|
|
399
|
-
color: #
|
|
400
|
-
background: linear-gradient(135deg, #2d8cf0, #57a3f3);
|
|
401
|
-
border-color: transparent;
|
|
402
|
-
transform: translateY(-2px);
|
|
403
|
-
box-shadow: 0 8px 20px rgba(45, 140, 240, 0.4);
|
|
417
|
+
.bulletin-download-btn:hover {
|
|
418
|
+
color: #57a3f3;
|
|
404
419
|
}
|
|
405
|
-
.bulletin-download-
|
|
406
|
-
|
|
407
|
-
box-shadow: 0 3px 10px rgba(45, 140, 240, 0.35);
|
|
420
|
+
.bulletin-download-btn:active {
|
|
421
|
+
color: #2372c8;
|
|
408
422
|
}
|
|
409
423
|
/* 空态/加载态:无文档时高度自适应内容,避免撑出大片空白 */
|
|
410
424
|
.bulletin-body.is-empty {
|
package/package.json
CHANGED
package/pub/doc/word/index.vue
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="word-preview">
|
|
3
|
-
<!--
|
|
4
|
-
<aside class="word-nav">
|
|
5
|
-
<div class="word-nav-
|
|
3
|
+
<!-- 左侧:目录(可折叠) -->
|
|
4
|
+
<aside class="word-nav" v-if="!navCollapsed">
|
|
5
|
+
<div class="word-nav-head">
|
|
6
|
+
<div class="word-nav-title">文档目录</div>
|
|
7
|
+
<span class="panel-collapse-btn" title="收起目录" @click="toggleNav">
|
|
8
|
+
<Icon type="ios-arrow-back" />
|
|
9
|
+
</span>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="word-nav-publish" v-if="publishTime">{{ publishTime }}</div>
|
|
6
12
|
<div v-if="!ready" class="word-nav-empty">解析中…</div>
|
|
7
13
|
<div v-else-if="!toc.length" class="word-nav-empty">未识别到章节目录</div>
|
|
8
14
|
<a
|
|
@@ -13,6 +19,11 @@
|
|
|
13
19
|
@click="scrollTo(item)"
|
|
14
20
|
>{{ item.text }}</a>
|
|
15
21
|
</aside>
|
|
22
|
+
<!-- 左侧折叠窄条 -->
|
|
23
|
+
<div class="word-rail word-rail-left" v-else title="展开目录" @click="toggleNav">
|
|
24
|
+
<Icon type="ios-arrow-forward" class="rail-arrow" />
|
|
25
|
+
<span class="rail-text">文档目录</span>
|
|
26
|
+
</div>
|
|
16
27
|
|
|
17
28
|
<!-- 中间:真实 Word 预览 -->
|
|
18
29
|
<div class="word-doc">
|
|
@@ -23,8 +34,17 @@
|
|
|
23
34
|
></js-preview-docx>
|
|
24
35
|
</div>
|
|
25
36
|
|
|
26
|
-
<!--
|
|
27
|
-
<
|
|
37
|
+
<!-- 右侧折叠窄条(摘要非空且已折叠时显示) -->
|
|
38
|
+
<div class="word-rail word-rail-right" v-if="!summaryEmpty && asideCollapsed" title="展开摘要" @click="toggleAside">
|
|
39
|
+
<Icon type="ios-arrow-back" class="rail-arrow" />
|
|
40
|
+
<span class="rail-text">文档摘要</span>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- 右侧:摘要(四部分全空时隐藏,中间预览区自动扩展;可折叠) -->
|
|
44
|
+
<aside class="word-aside" v-if="!summaryEmpty && !asideCollapsed">
|
|
45
|
+
<span class="panel-collapse-btn panel-collapse-btn-float" title="收起摘要" @click="toggleAside">
|
|
46
|
+
<Icon type="ios-arrow-forward" />
|
|
47
|
+
</span>
|
|
28
48
|
<div class="paper-title" v-if="internalReport.title">{{ internalReport.title }}</div>
|
|
29
49
|
<div class="paper-meta" v-if="internalReport.meta">{{ internalReport.meta }}</div>
|
|
30
50
|
|
|
@@ -81,6 +101,10 @@
|
|
|
81
101
|
import JsPreviewDocx from "../preview/components/JsPreviewDocx.vue"
|
|
82
102
|
import JSZip from "jszip"
|
|
83
103
|
|
|
104
|
+
// 折叠状态在 localStorage 的存储 key
|
|
105
|
+
const NAV_COLLAPSE_KEY = "lambo-doc-word-nav-collapsed"
|
|
106
|
+
const ASIDE_COLLAPSE_KEY = "lambo-doc-word-aside-collapsed"
|
|
107
|
+
|
|
84
108
|
export default {
|
|
85
109
|
name: "LamboDocWord",
|
|
86
110
|
components: {
|
|
@@ -96,6 +120,11 @@ export default {
|
|
|
96
120
|
report: {
|
|
97
121
|
type: Object,
|
|
98
122
|
default: null
|
|
123
|
+
},
|
|
124
|
+
// 简报生成时间(plat_bulletin_publish_doc.publish_time),展示在目录下方
|
|
125
|
+
publishTime: {
|
|
126
|
+
type: String,
|
|
127
|
+
default: ""
|
|
99
128
|
}
|
|
100
129
|
},
|
|
101
130
|
data() {
|
|
@@ -144,7 +173,10 @@ export default {
|
|
|
144
173
|
scrollEl: null,
|
|
145
174
|
// 右侧摘要(优先使用外部 report prop,缺失时回退到内置模拟数据)
|
|
146
175
|
internalReport: this.report || defaultReport,
|
|
147
|
-
defaultReport: defaultReport
|
|
176
|
+
defaultReport: defaultReport,
|
|
177
|
+
// 折叠状态(从 localStorage 恢复,容错降级为默认展开)
|
|
178
|
+
navCollapsed: this.readCollapsed(NAV_COLLAPSE_KEY),
|
|
179
|
+
asideCollapsed: this.readCollapsed(ASIDE_COLLAPSE_KEY)
|
|
148
180
|
};
|
|
149
181
|
},
|
|
150
182
|
watch: {
|
|
@@ -183,6 +215,34 @@ export default {
|
|
|
183
215
|
}
|
|
184
216
|
},
|
|
185
217
|
methods: {
|
|
218
|
+
// 读取 localStorage 中的折叠状态;不可用或无值时默认展开(false)
|
|
219
|
+
readCollapsed(key) {
|
|
220
|
+
try {
|
|
221
|
+
return typeof window !== "undefined" && window.localStorage
|
|
222
|
+
? window.localStorage.getItem(key) === "1"
|
|
223
|
+
: false;
|
|
224
|
+
} catch (e) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
// 写入折叠状态到 localStorage(容错,不阻断切换)
|
|
229
|
+
writeCollapsed(key, val) {
|
|
230
|
+
try {
|
|
231
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
232
|
+
window.localStorage.setItem(key, val ? "1" : "0");
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// ignore
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
toggleNav() {
|
|
239
|
+
this.navCollapsed = !this.navCollapsed;
|
|
240
|
+
this.writeCollapsed(NAV_COLLAPSE_KEY, this.navCollapsed);
|
|
241
|
+
},
|
|
242
|
+
toggleAside() {
|
|
243
|
+
this.asideCollapsed = !this.asideCollapsed;
|
|
244
|
+
this.writeCollapsed(ASIDE_COLLAPSE_KEY, this.asideCollapsed);
|
|
245
|
+
},
|
|
186
246
|
// JsPreviewDocx 渲染完成,root 为其容器 DOM(.vue-office-docx,即滚动容器)
|
|
187
247
|
async onRendered(root) {
|
|
188
248
|
this.docRoot = root;
|
|
@@ -400,7 +460,7 @@ export default {
|
|
|
400
460
|
width: 100%;
|
|
401
461
|
height: 100%;
|
|
402
462
|
box-sizing: border-box;
|
|
403
|
-
padding:
|
|
463
|
+
padding: 5px;
|
|
404
464
|
background: #f3f4f6;
|
|
405
465
|
color: #1e293b;
|
|
406
466
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
|
@@ -417,9 +477,77 @@ export default {
|
|
|
417
477
|
background: #fff;
|
|
418
478
|
border: 1px solid #e2e8f0;
|
|
419
479
|
border-radius: 6px;
|
|
420
|
-
padding:
|
|
480
|
+
padding: 7px 8px;
|
|
421
481
|
box-sizing: border-box;
|
|
422
482
|
overflow-y: auto;
|
|
483
|
+
transition: flex-basis 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
484
|
+
width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
485
|
+
}
|
|
486
|
+
/* 目录标题行:标题 + 收起按钮 */
|
|
487
|
+
.word-nav-head {
|
|
488
|
+
display: flex;
|
|
489
|
+
align-items: center;
|
|
490
|
+
justify-content: space-between;
|
|
491
|
+
margin-bottom: 6px;
|
|
492
|
+
}
|
|
493
|
+
.word-nav-head .word-nav-title {
|
|
494
|
+
margin-bottom: 0;
|
|
495
|
+
}
|
|
496
|
+
/* 摘要收起按钮:悬浮右上角,不占文档流,避免挤压标题到第二行 */
|
|
497
|
+
.panel-collapse-btn-float {
|
|
498
|
+
position: absolute;
|
|
499
|
+
top: 7px;
|
|
500
|
+
right: 8px;
|
|
501
|
+
z-index: 2;
|
|
502
|
+
}
|
|
503
|
+
/* 面板内收起按钮:纯图标,克制 */
|
|
504
|
+
.panel-collapse-btn {
|
|
505
|
+
display: inline-flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
justify-content: center;
|
|
508
|
+
width: 22px;
|
|
509
|
+
height: 22px;
|
|
510
|
+
border-radius: 6px;
|
|
511
|
+
color: #94a3b8;
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
font-size: 16px;
|
|
514
|
+
transition: background 0.15s, color 0.15s;
|
|
515
|
+
}
|
|
516
|
+
.panel-collapse-btn:hover {
|
|
517
|
+
background: #f0f7ff;
|
|
518
|
+
color: #2d8cf0;
|
|
519
|
+
}
|
|
520
|
+
/* 折叠窄条:竖排标题 + 箭头,点击整条展开 */
|
|
521
|
+
.word-rail {
|
|
522
|
+
flex: 0 0 32px;
|
|
523
|
+
width: 32px;
|
|
524
|
+
align-self: stretch;
|
|
525
|
+
display: flex;
|
|
526
|
+
flex-direction: column;
|
|
527
|
+
align-items: center;
|
|
528
|
+
gap: 8px;
|
|
529
|
+
padding: 10px 0;
|
|
530
|
+
background: #fff;
|
|
531
|
+
border: 1px solid #e2e8f0;
|
|
532
|
+
border-radius: 6px;
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
color: #64748b;
|
|
535
|
+
transition: background 0.15s, color 0.15s;
|
|
536
|
+
}
|
|
537
|
+
.word-rail:hover {
|
|
538
|
+
background: #f0f7ff;
|
|
539
|
+
color: #2d8cf0;
|
|
540
|
+
}
|
|
541
|
+
.word-rail .rail-arrow {
|
|
542
|
+
font-size: 16px;
|
|
543
|
+
flex-shrink: 0;
|
|
544
|
+
}
|
|
545
|
+
.word-rail .rail-text {
|
|
546
|
+
writing-mode: vertical-rl;
|
|
547
|
+
letter-spacing: 2px;
|
|
548
|
+
font-size: 13px;
|
|
549
|
+
font-weight: 600;
|
|
550
|
+
user-select: none;
|
|
423
551
|
}
|
|
424
552
|
.word-nav-title {
|
|
425
553
|
font-size: 14px;
|
|
@@ -467,6 +595,13 @@ export default {
|
|
|
467
595
|
color: #1e40af;
|
|
468
596
|
font-weight: 700;
|
|
469
597
|
}
|
|
598
|
+
/* 文档目录标题下方:简报发布时间,与右侧 .paper-meta 同一套样式 */
|
|
599
|
+
.word-nav-publish {
|
|
600
|
+
margin: 4px 0 6px;
|
|
601
|
+
font-size: 11px;
|
|
602
|
+
color: #64748b;
|
|
603
|
+
line-height: 1.6;
|
|
604
|
+
}
|
|
470
605
|
|
|
471
606
|
/* 中间 Word 预览 */
|
|
472
607
|
.word-doc {
|
|
@@ -475,7 +610,6 @@ export default {
|
|
|
475
610
|
display: flex;
|
|
476
611
|
flex-direction: column;
|
|
477
612
|
background: #fff;
|
|
478
|
-
border: 1px solid #e2e8f0;
|
|
479
613
|
border-radius: 6px;
|
|
480
614
|
overflow: hidden;
|
|
481
615
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
|
|
@@ -493,26 +627,31 @@ export default {
|
|
|
493
627
|
|
|
494
628
|
/* 右侧摘要 */
|
|
495
629
|
.word-aside {
|
|
496
|
-
|
|
497
|
-
|
|
630
|
+
position: relative;
|
|
631
|
+
flex: 0 0 290px;
|
|
632
|
+
width: 290px;
|
|
498
633
|
align-self: stretch;
|
|
499
634
|
background: #fff;
|
|
500
635
|
border: 1px solid #e2e8f0;
|
|
501
636
|
border-radius: 6px;
|
|
502
|
-
padding:
|
|
637
|
+
padding: 7px 8px;
|
|
503
638
|
box-sizing: border-box;
|
|
504
639
|
overflow-y: auto;
|
|
640
|
+
transition: flex-basis 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
641
|
+
width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
505
642
|
}
|
|
506
643
|
.paper-title {
|
|
507
644
|
font-size: 14px;
|
|
508
645
|
font-weight: 700;
|
|
509
646
|
color: #0f172a;
|
|
510
|
-
margin-bottom:
|
|
647
|
+
margin-bottom: 6px;
|
|
648
|
+
/* 给右上角悬浮收起按钮让位 */
|
|
649
|
+
padding-right: 26px;
|
|
511
650
|
}
|
|
512
651
|
.paper-meta {
|
|
513
|
-
font-size:
|
|
652
|
+
font-size: 11px;
|
|
514
653
|
color: #64748b;
|
|
515
|
-
margin-bottom:
|
|
654
|
+
margin-bottom: 6px;
|
|
516
655
|
}
|
|
517
656
|
/* 摘要分区 */
|
|
518
657
|
.summary-section {
|
|
@@ -277,10 +277,10 @@ export default {
|
|
|
277
277
|
}
|
|
278
278
|
},
|
|
279
279
|
handleSubmit() {
|
|
280
|
-
if (!this.applyDescription.trim()) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
280
|
+
// if (!this.applyDescription.trim()) {
|
|
281
|
+
// this.$Message.warning({ content: '请输入申请描述', duration: 2.5 })
|
|
282
|
+
// return
|
|
283
|
+
// }
|
|
284
284
|
this.submitting = true
|
|
285
285
|
this.$emit('submit', {
|
|
286
286
|
applyDescription: this.applyDescription,
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
LamboTableStatusFlowCol 表格状态流转列组件
|
|
3
|
+
|
|
4
|
+
用途:
|
|
5
|
+
在表格单元格中以「节点1 › 节点2 › 节点3 ...」的横向流转条形式展示业务单据的当前状态,
|
|
6
|
+
并支持点击打开抽屉查看完整的状态流转时间线(含每一步的时间、操作人、备注等历史信息)。
|
|
7
|
+
|
|
8
|
+
展示规则:
|
|
9
|
+
- 总状态数 <= 3:全部节点展开显示。
|
|
10
|
+
- 总状态数 > 3:当前节点之前仅保留一个直接前置(多个折叠为 +N),
|
|
11
|
+
当前节点之后最多显示一个直接后置(剩余 1 个显示名称,多个折叠为 +N)。
|
|
12
|
+
- 已完成节点使用成功色(绿色),待处理节点使用表格常规文字色,当前节点高亮。
|
|
13
|
+
|
|
14
|
+
Props:
|
|
15
|
+
- statuses {Array<{ K: string|number, V: string }>} 状态节点列表,按流转顺序排列,
|
|
16
|
+
K 为状态值(与业务数据中的状态字段对应),V 为状态显示名称,K 需唯一。
|
|
17
|
+
示例:
|
|
18
|
+
[
|
|
19
|
+
{ K: '10', V: '新建' },
|
|
20
|
+
{ K: '20', V: '审批中' },
|
|
21
|
+
{ K: '30', V: '已通过' },
|
|
22
|
+
{ K: '40', V: '已完成' }
|
|
23
|
+
]
|
|
24
|
+
- current {string|number} 当前单据所处的状态值,需与 statuses 中某项的 K 相等
|
|
25
|
+
(比较时会做 String() 转换,数字与字符串形式可互通)。示例:'20' 或 20。
|
|
26
|
+
- history {Array<{ K: string|number, time?: string, operator?: string, comment?: string }>}
|
|
27
|
+
可选,各状态节点的历史记录,用于在抽屉时间线中展示时间/操作人/备注;
|
|
28
|
+
K 需与 statuses 中的 K 对应,相同 K 默认只取一条。示例:
|
|
29
|
+
[
|
|
30
|
+
{ K: '10', time: '2026-08-01 09:00', operator: '张三', comment: '提交申请' },
|
|
31
|
+
{ K: '20', time: '2026-08-01 10:30', operator: '李四', comment: '审批中' }
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
用法示例:
|
|
35
|
+
<LamboTableStatusFlowCol
|
|
36
|
+
:statuses="[
|
|
37
|
+
{ K: '10', V: '新建' },
|
|
38
|
+
{ K: '20', V: '审批中' },
|
|
39
|
+
{ K: '30', V: '已通过' }
|
|
40
|
+
]"
|
|
41
|
+
current="20"
|
|
42
|
+
:history="[
|
|
43
|
+
{ K: '10', time: '2026-08-01 09:00', operator: '张三', comment: '提交申请' }
|
|
44
|
+
]"
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
事件:无。
|
|
48
|
+
-->
|
|
49
|
+
<template>
|
|
50
|
+
<div class="status-flow-wrapper">
|
|
51
|
+
<div class="status-flow" title="点击查看完整流转" @click="drawerVisible = true">
|
|
52
|
+
<template v-if="currentIndex >= 0">
|
|
53
|
+
<!-- 总状态数 <=3:全部展开,不折叠 -->
|
|
54
|
+
<template v-if="expandAll">
|
|
55
|
+
<span
|
|
56
|
+
v-for="(node, i) in statuses"
|
|
57
|
+
:key="node.K"
|
|
58
|
+
:class="i < currentIndex ? 'collapsed-prev' : (i === currentIndex ? 'current' : 'normal')"
|
|
59
|
+
>{{ node.V }}<span v-if="i < statuses.length - 1" class="arrow">›</span></span>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<!-- 总状态数 >3:前置/后置按规则折叠 -->
|
|
63
|
+
<template v-else>
|
|
64
|
+
<!-- 前置节点:1 个直接显示名称,多个折叠为 +N(成功色,已完成) -->
|
|
65
|
+
<template v-if="currentIndex > 0">
|
|
66
|
+
<span v-if="currentIndex === 1" class="collapsed-prev">{{ prevName }}</span>
|
|
67
|
+
<span v-else class="collapsed-prev">+{{ currentIndex }}</span>
|
|
68
|
+
<span class="arrow">›</span>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<!-- 当前状态 - 成功色 -->
|
|
72
|
+
<span class="current">{{ currentName }}</span>
|
|
73
|
+
|
|
74
|
+
<!-- 后置:紧挨的下一个直接显示;剩余 1 个显示名称,多个折叠为 +N -->
|
|
75
|
+
<template v-if="nextIndex < statuses.length">
|
|
76
|
+
<span class="arrow">›</span>
|
|
77
|
+
<span class="normal">{{ nextName }}</span>
|
|
78
|
+
|
|
79
|
+
<template v-if="remainingCount === 1">
|
|
80
|
+
<span class="arrow">›</span>
|
|
81
|
+
<span class="normal">{{ remainingName }}</span>
|
|
82
|
+
</template>
|
|
83
|
+
<template v-else-if="remainingCount > 1">
|
|
84
|
+
<span class="arrow">›</span>
|
|
85
|
+
<span class="collapsed-next">+{{ remainingCount }}</span>
|
|
86
|
+
</template>
|
|
87
|
+
</template>
|
|
88
|
+
</template>
|
|
89
|
+
</template>
|
|
90
|
+
<span v-else>-</span>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<Drawer
|
|
94
|
+
v-model="drawerVisible"
|
|
95
|
+
title="完整状态流转"
|
|
96
|
+
:width="400"
|
|
97
|
+
:mask-closable="true"
|
|
98
|
+
>
|
|
99
|
+
<div class="timeline">
|
|
100
|
+
<div v-for="(node, i) in statuses" :key="node.K" class="timeline-item">
|
|
101
|
+
<div
|
|
102
|
+
class="timeline-dot"
|
|
103
|
+
:class="{ completed: i < currentIndex, current: i === currentIndex }"
|
|
104
|
+
></div>
|
|
105
|
+
<div class="timeline-label">
|
|
106
|
+
{{ node.V }}
|
|
107
|
+
<span
|
|
108
|
+
class="badge"
|
|
109
|
+
:class="badgeClass(i)"
|
|
110
|
+
>{{ badgeText(i) }}</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div v-if="historyMap[node.K]" class="timeline-meta">
|
|
113
|
+
<div v-if="historyMap[node.K].time" class="meta-line">
|
|
114
|
+
时间:{{ historyMap[node.K].time }}
|
|
115
|
+
</div>
|
|
116
|
+
<div v-if="historyMap[node.K].operator" class="meta-line">
|
|
117
|
+
操作人:{{ historyMap[node.K].operator }}
|
|
118
|
+
</div>
|
|
119
|
+
<div v-if="historyMap[node.K].comment" class="meta-line">
|
|
120
|
+
备注:{{ historyMap[node.K].comment }}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</Drawer>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<script>
|
|
130
|
+
export default {
|
|
131
|
+
name: 'LamboTableStatusFlowCol',
|
|
132
|
+
props: {
|
|
133
|
+
statuses: {
|
|
134
|
+
type: Array,
|
|
135
|
+
required: true
|
|
136
|
+
// [{ K: '10', V: '新建' }, ...]
|
|
137
|
+
},
|
|
138
|
+
current: {
|
|
139
|
+
type: [String, Number],
|
|
140
|
+
required: true
|
|
141
|
+
},
|
|
142
|
+
history: {
|
|
143
|
+
type: Array,
|
|
144
|
+
default: () => []
|
|
145
|
+
// [{ K: '20', time: '...', operator: '...', comment: '...' }]
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
data() {
|
|
149
|
+
return {
|
|
150
|
+
drawerVisible: false
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
computed: {
|
|
154
|
+
currentIndex() {
|
|
155
|
+
return this.statuses.findIndex(s => String(s.K) === String(this.current))
|
|
156
|
+
},
|
|
157
|
+
expandAll() {
|
|
158
|
+
return this.statuses.length <= 3
|
|
159
|
+
},
|
|
160
|
+
nextIndex() {
|
|
161
|
+
return this.currentIndex + 1
|
|
162
|
+
},
|
|
163
|
+
remainingCount() {
|
|
164
|
+
return Math.max(0, this.statuses.length - this.currentIndex - 2)
|
|
165
|
+
},
|
|
166
|
+
currentName() {
|
|
167
|
+
const node = this.statuses[this.currentIndex]
|
|
168
|
+
return node ? node.V : ''
|
|
169
|
+
},
|
|
170
|
+
nextName() {
|
|
171
|
+
const node = this.statuses[this.nextIndex]
|
|
172
|
+
return node ? node.V : ''
|
|
173
|
+
},
|
|
174
|
+
prevName() {
|
|
175
|
+
const node = this.statuses[this.currentIndex - 1]
|
|
176
|
+
return node ? node.V : ''
|
|
177
|
+
},
|
|
178
|
+
remainingName() {
|
|
179
|
+
const node = this.statuses[this.nextIndex + 1]
|
|
180
|
+
return node ? node.V : ''
|
|
181
|
+
},
|
|
182
|
+
historyMap() {
|
|
183
|
+
const map = {}
|
|
184
|
+
this.history.forEach(h => {
|
|
185
|
+
map[h.K] = h
|
|
186
|
+
})
|
|
187
|
+
return map
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
methods: {
|
|
191
|
+
badgeClass(i) {
|
|
192
|
+
if (i < this.currentIndex) return 'badge-completed'
|
|
193
|
+
if (i === this.currentIndex) return 'badge-processing'
|
|
194
|
+
return 'badge-pending'
|
|
195
|
+
},
|
|
196
|
+
badgeText(i) {
|
|
197
|
+
if (i < this.currentIndex) return '已完成'
|
|
198
|
+
if (i === this.currentIndex) return '进行中'
|
|
199
|
+
return '待处理'
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
</script>
|
|
204
|
+
|
|
205
|
+
<style scoped>
|
|
206
|
+
.status-flow {
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
display: inline-flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 4px;
|
|
211
|
+
padding: 4px 0;
|
|
212
|
+
transition: opacity 0.3s;
|
|
213
|
+
font-size: 14px;
|
|
214
|
+
color: #515a6e;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.status-flow:hover {
|
|
218
|
+
opacity: 0.7;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* 前置折叠 - 成功色(已完成) */
|
|
222
|
+
.collapsed-prev {
|
|
223
|
+
color: #19be6b;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* 后置折叠 - 与表格文字一致(待处理) */
|
|
227
|
+
.collapsed-next {
|
|
228
|
+
color: #515a6e;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* 箭头 */
|
|
232
|
+
.arrow {
|
|
233
|
+
color: #c5c8ce;
|
|
234
|
+
margin: 0 2px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* 当前状态 - 成功色 */
|
|
238
|
+
.current {
|
|
239
|
+
color: #19be6b;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* 下一节点 - 与表格文字一致 */
|
|
243
|
+
.normal {
|
|
244
|
+
color: #515a6e;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* 抽屉内时间线 */
|
|
248
|
+
.timeline {
|
|
249
|
+
position: relative;
|
|
250
|
+
padding-left: 30px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.timeline::before {
|
|
254
|
+
content: '';
|
|
255
|
+
position: absolute;
|
|
256
|
+
left: 10px;
|
|
257
|
+
top: 10px;
|
|
258
|
+
bottom: 10px;
|
|
259
|
+
width: 2px;
|
|
260
|
+
background: #e8eaec;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.timeline-item {
|
|
264
|
+
position: relative;
|
|
265
|
+
margin-bottom: 25px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.timeline-item:last-child {
|
|
269
|
+
margin-bottom: 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.timeline-dot {
|
|
273
|
+
position: absolute;
|
|
274
|
+
left: -25px;
|
|
275
|
+
top: 5px;
|
|
276
|
+
width: 12px;
|
|
277
|
+
height: 12px;
|
|
278
|
+
border-radius: 50%;
|
|
279
|
+
background: #c5c8ce;
|
|
280
|
+
border: 2px solid white;
|
|
281
|
+
box-shadow: 0 0 0 2px #c5c8ce;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.timeline-dot.completed,
|
|
285
|
+
.timeline-dot.current {
|
|
286
|
+
background: #19be6b;
|
|
287
|
+
box-shadow: 0 0 0 2px #19be6b;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.timeline-label {
|
|
291
|
+
font-size: 14px;
|
|
292
|
+
color: #333;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.badge {
|
|
296
|
+
display: inline-block;
|
|
297
|
+
padding: 2px 8px;
|
|
298
|
+
border-radius: 12px;
|
|
299
|
+
font-size: 12px;
|
|
300
|
+
margin-left: 8px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.badge-processing,
|
|
304
|
+
.badge-completed {
|
|
305
|
+
background: #e8f8ef;
|
|
306
|
+
color: #19be6b;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.badge-pending {
|
|
310
|
+
background: #f5f5f5;
|
|
311
|
+
color: #999;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.timeline-meta {
|
|
315
|
+
margin-top: 6px;
|
|
316
|
+
font-size: 12px;
|
|
317
|
+
color: #999;
|
|
318
|
+
line-height: 1.8;
|
|
319
|
+
}
|
|
320
|
+
</style>
|