appsnbcbweicheng 1.2.25 → 1.2.27

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,612 +0,0 @@
1
- <template>
2
- <div class="create-overlay">
3
- <div class="create-header">
4
- <div class="left">
5
- <el-button type="text" @click="$emit('close')">退出</el-button>
6
- </div>
7
- </div>
8
-
9
- <div class="create-content">
10
- <div class="top-section">
11
- <div class="left-table">
12
- <el-table :data="rows" border stripe size="small" class="edit-table" height="100%">
13
- <el-table-column type="index" label="序号" width="60" />
14
- <el-table-column prop="month" label="观点月份" min-width="120" />
15
- <el-table-column prop="assetsType" label="资产类别" min-width="120" />
16
- <el-table-column prop="assetName" label="研究对象" min-width="160" />
17
- <el-table-column prop="trackingIndex" label="跟踪指数" min-width="160" />
18
- <el-table-column prop="opinionEmotion" label="未来6个月观点" min-width="140" />
19
- <el-table-column prop="opinionDescription" label="论据摘要" min-width="240">
20
- <template slot-scope="{ row }">
21
- <div class="expandable-content">
22
- <div
23
- :class="{
24
- 'content-collapse': !row.expandOpinionDescription,
25
- 'content-expanded': row.expandOpinionDescription,
26
- }"
27
- >
28
- {{ row.opinionDescription }}
29
- </div>
30
- <el-button
31
- v-if="shouldShowExpand(row.opinionDescription)"
32
- type="text"
33
- size="mini"
34
- @click="toggleExpand(row, 'opinionDescription')"
35
- >
36
- {{ row.expandOpinionDescription ? '收起' : '展开' }}
37
- </el-button>
38
- </div>
39
- </template>
40
- </el-table-column>
41
- <el-table-column prop="argumentDetail" label="论据详情" min-width="240" />
42
- <el-table-column prop="opinion" label="机构" min-width="140" />
43
- <el-table-column prop="sourceChannel" label="来源渠道" min-width="120" />
44
- <el-table-column prop="dataSource" label="数据来源" min-width="120" />
45
- <el-table-column label="操作" width="140" fixed="right">
46
- <template slot-scope="{ row, $index }">
47
- <el-popconfirm title="是否确认删除?" @confirm="removeRow($index)">
48
- <el-button slot="reference" type="text" size="mini">删除</el-button>
49
- </el-popconfirm>
50
- <el-popconfirm title="是否确认编辑?" @confirm="startEditRow(row)">
51
- <el-button slot="reference" type="text" size="mini">编辑</el-button>
52
- </el-popconfirm>
53
- </template>
54
- </el-table-column>
55
- </el-table>
56
- </div>
57
-
58
- <div class="right-form">
59
- <div class="form-header-row">
60
- <div class="form-title">观点上传解析</div>
61
- <el-button type="primary" plain size="mini" @click="downloadTemplate"
62
- >提交模板下载</el-button
63
- >
64
- </div>
65
- <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" size="small">
66
- <el-form-item label="上传文件" prop="fileName">
67
- <el-upload
68
- action=""
69
- :show-file-list="false"
70
- :auto-upload="false"
71
- :on-change="handleFileChange"
72
- :on-remove="handleFileRemove"
73
- >
74
- <el-button size="small" type="primary">点击上传</el-button>
75
- <div slot="tip" class="el-upload__tip" style="margin-top: 5px">
76
- 请按照机构观点提报模板上传文件,否则可能导致解析失败
77
- </div>
78
- </el-upload>
79
- <div v-if="form.fileName" class="uploaded-file">
80
- 文件:{{ form.fileName }}
81
- <el-button type="text" size="mini" @click="clearFile">移除</el-button>
82
- </div>
83
- </el-form-item>
84
- <el-form-item label="数据创建时间" prop="createTime">
85
- <el-date-picker
86
- v-model="form.createTime"
87
- type="date"
88
- placeholder="请选择日期"
89
- value-format="yyyy-MM-dd"
90
- style="width: 100%"
91
- />
92
- </el-form-item>
93
- <el-form-item label="来源渠道" prop="sourceChannel">
94
- <el-select v-model="form.sourceChannel" placeholder="请选择" style="width: 100%">
95
- <el-option label="邮件" value="邮件" />
96
- <el-option label="平台" value="平台" />
97
- <el-option label="外部" value="外部" />
98
- </el-select>
99
- </el-form-item>
100
- <el-form-item label="机构" prop="opinionName">
101
- <el-input v-model="form.opinionName" placeholder="请输入机构名称" />
102
- </el-form-item>
103
- <div class="form-actions">
104
- <el-button @click="$emit('close')">取消</el-button>
105
- <el-popover
106
- placement="top"
107
- width="320"
108
- trigger="manual"
109
- v-model="submitErrorPopoverVisible"
110
- >
111
- <div class="error-popover">
112
- <div class="title">提交失败</div>
113
- <div class="summary">
114
- {{ errorSummary.missingCount }}条数据缺失,
115
- {{ errorSummary.invalidCount }}条数据未识别
116
- </div>
117
- <div class="details">
118
- <div v-for="(g, i) in groupedErrorTexts" :key="i">{{ g }}</div>
119
- </div>
120
- </div>
121
- <el-button slot="reference" type="primary" @click="submit">确认</el-button>
122
- </el-popover>
123
- </div>
124
- </el-form>
125
- </div>
126
- </div>
127
-
128
- <div class="bottom-section">
129
- <div class="row-1">
130
- <div class="left">
131
- <span class="label">异常检测({{ detectionCount }})</span>
132
- <el-button type="primary" size="mini" @click="runDetection">数据检测</el-button>
133
- </div>
134
- <div class="right">
135
- <el-button @click="$emit('close')">取消</el-button>
136
- <el-button type="primary" @click="submit">提交观点</el-button>
137
- </div>
138
- </div>
139
- <div class="row-2">
140
- <div class="error-list" v-if="detectionTriggered && errorList.length > 0">
141
- <div class="error-title">错误明细</div>
142
- <div class="error-group" v-for="(g, i) in groupedErrorTexts" :key="i">
143
- <span class="text">{{ g }}</span>
144
- </div>
145
- </div>
146
- <div class="no-error" v-else-if="detectionTriggered && errorList.length === 0">
147
- 无异常数据
148
- </div>
149
- </div>
150
- </div>
151
- </div>
152
- <el-dialog
153
- title="编辑观点"
154
- :visible.sync="editDialogVisible"
155
- width="600px"
156
- append-to-body
157
- :close-on-click-modal="false"
158
- >
159
- <el-form :model="editForm" label-width="120px" size="small">
160
- <el-form-item label="观点月份">
161
- <el-date-picker
162
- v-model="editForm.month"
163
- type="month"
164
- value-format="yyyy-MM"
165
- placeholder="选择月份"
166
- style="width: 100%"
167
- ></el-date-picker>
168
- </el-form-item>
169
- <el-form-item label="资产类别">
170
- <el-input v-model="editForm.assetsType"></el-input>
171
- </el-form-item>
172
- <el-form-item label="研究对象">
173
- <el-input v-model="editForm.assetName"></el-input>
174
- </el-form-item>
175
- <el-form-item label="跟踪指数">
176
- <el-input v-model="editForm.trackingIndex"></el-input>
177
- </el-form-item>
178
- <el-form-item label="未来6个月观点">
179
- <el-select v-model="editForm.opinionEmotion" style="width: 100%">
180
- <el-option label="看多" value="看多"></el-option>
181
- <el-option label="中性" value="中性"></el-option>
182
- <el-option label="谨慎" value="谨慎"></el-option>
183
- </el-select>
184
- </el-form-item>
185
- <el-form-item label="论据摘要">
186
- <el-input type="textarea" v-model="editForm.opinionDescription" :rows="3"></el-input>
187
- </el-form-item>
188
- <el-form-item label="论据详情">
189
- <el-input type="textarea" v-model="editForm.argumentDetail" :rows="3"></el-input>
190
- </el-form-item>
191
- <el-form-item label="机构">
192
- <el-input v-model="editForm.opinion"></el-input>
193
- </el-form-item>
194
- <el-form-item label="来源渠道">
195
- <el-input v-model="editForm.sourceChannel"></el-input>
196
- </el-form-item>
197
- <el-form-item label="数据来源">
198
- <el-input v-model="editForm.dataSource"></el-input>
199
- </el-form-item>
200
- </el-form>
201
- <span slot="footer" class="dialog-footer">
202
- <el-button @click="editDialogVisible = false" size="small">取 消</el-button>
203
- <el-button type="primary" @click="saveEdit" size="small">确 定</el-button>
204
- </span>
205
- </el-dialog>
206
- </div>
207
- </template>
208
-
209
- <script>
210
- export default {
211
- name: 'InstitutionViewpointCreate',
212
- props: {
213
- existingData: {
214
- type: Array,
215
- default: () => [],
216
- },
217
- },
218
- data() {
219
- return {
220
- editDialogVisible: false,
221
- editForm: {},
222
- editingIndex: -1,
223
- rows: [
224
- {
225
- month: '2025-12',
226
- assetsType: '股票',
227
- assetName: '沪深300',
228
- trackingIndex: '沪深300指数',
229
- opinionEmotion: '看多',
230
- opinionDescription: '盈利预期改善,政策环境友好,风险偏好提升。',
231
- argumentDetail: '宏观回升、估值较低、资金面宽松,存在上修弹性。',
232
- opinion: '机构A',
233
- sourceChannel: '平台',
234
- dataSource: '内部研究',
235
- expandOpinionDescription: false,
236
- },
237
- {
238
- month: '2025-12',
239
- assetsType: '债券',
240
- assetName: '国债指数',
241
- trackingIndex: '中证国债指数',
242
- opinionEmotion: '谨慎',
243
- opinionDescription: '收益率中枢或下移空间有限,配置为底仓。',
244
- argumentDetail: '政策中性略宽松,长端受供给与期限溢价约束。',
245
- opinion: '机构B',
246
- sourceChannel: '外部',
247
- dataSource: '外部研报',
248
- expandOpinionDescription: false,
249
- },
250
- {
251
- month: '2026-01',
252
- assetsType: '商品',
253
- assetName: '黄金',
254
- trackingIndex: 'COMEX黄金',
255
- opinionEmotion: '中性',
256
- opinionDescription: '避险情绪消退,美联储降息预期波动。',
257
- argumentDetail: '短期缺乏上行动力,但长期配置价值仍存。',
258
- opinion: '机构C',
259
- sourceChannel: '邮件',
260
- dataSource: '内部研究',
261
- expandOpinionDescription: false,
262
- },
263
- ],
264
- editingRowId: null,
265
- form: {
266
- fileName: '',
267
- createTime: '',
268
- sourceChannel: '',
269
- opinionName: '',
270
- },
271
- rules: {
272
- fileName: [{ required: true, message: '请上传文件', trigger: 'change' }],
273
- createTime: [{ required: true, message: '请选择数据创建时间', trigger: 'change' }],
274
- sourceChannel: [{ required: true, message: '请选择来源渠道', trigger: 'change' }],
275
- opinionName: [{ required: true, message: '请输入机构名称', trigger: 'blur' }],
276
- },
277
- detectionTriggered: false,
278
- detectionCount: 0,
279
- errorList: [],
280
- errorSummary: {
281
- missingCount: 0,
282
- invalidCount: 0,
283
- },
284
- submitErrorPopoverVisible: false,
285
- allowedOpinions: ['看多', '中性', '谨慎'],
286
- }
287
- },
288
- computed: {
289
- groupedErrorTexts() {
290
- const byType = {}
291
- this.errorList.forEach(e => {
292
- if (!byType[e.type]) byType[e.type] = []
293
- byType[e.type].push(e)
294
- })
295
- const texts = []
296
- Object.keys(byType).forEach(type => {
297
- const items = byType[type]
298
- const lines = items.map(it => `第${it.index}行${it.message}`)
299
- const joined = lines.join(',')
300
- texts.push(joined)
301
- })
302
- return texts
303
- },
304
- },
305
- methods: {
306
- downloadTemplate() {
307
- this.$message.info('模板下载功能开发中...')
308
- },
309
- shouldShowExpand(content) {
310
- if (!content) return false
311
- return content.length > 100
312
- },
313
- toggleExpand(row, field) {
314
- const expandField = `expand${field.charAt(0).toUpperCase() + field.slice(1)}`
315
- this.$set(row, expandField, !row[expandField])
316
- },
317
- handleFileChange(file) {
318
- const name = file.name || (file.raw && file.raw.name) || ''
319
- this.form.fileName = name
320
- if (!/\.(xlsx|xls)$/i.test(name)) {
321
- this.rows = []
322
- this.$message.error('解析失败:未按模板格式上传')
323
- return
324
- }
325
- // 模拟解析生成数据
326
- this.rows = [
327
- {
328
- month: '2025-12',
329
- assetsType: '股票',
330
- assetName: '沪深300',
331
- trackingIndex: '沪深300指数',
332
- opinionEmotion: '看多',
333
- opinionDescription: '盈利预期改善,政策环境友好,风险偏好提升。',
334
- argumentDetail: '宏观回升、估值较低、资金面宽松,存在上修弹性。',
335
- opinion: this.form.opinionName || '机构A',
336
- sourceChannel: this.form.sourceChannel || '平台',
337
- dataSource: '内部研究',
338
- expandOpinionDescription: false,
339
- },
340
- {
341
- month: '2025-12',
342
- assetsType: '债券',
343
- assetName: '国债指数',
344
- trackingIndex: '中证国债指数',
345
- opinionEmotion: '谨慎',
346
- opinionDescription: '收益率中枢或下移空间有限,配置为底仓。',
347
- argumentDetail: '政策中性略宽松,长端受供给与期限溢价约束。',
348
- opinion: this.form.opinionName || '机构A',
349
- sourceChannel: this.form.sourceChannel || '平台',
350
- dataSource: '外部研报',
351
- expandOpinionDescription: false,
352
- },
353
- ]
354
- this.$message.success('文件解析成功')
355
- },
356
- handleFileRemove() {
357
- this.clearFile()
358
- },
359
- clearFile() {
360
- this.form.fileName = ''
361
- this.rows = []
362
- },
363
- startEditRow(row) {
364
- this.editingIndex = this.rows.indexOf(row)
365
- this.editForm = { ...row }
366
- this.editDialogVisible = true
367
- },
368
- saveEdit() {
369
- if (this.editingIndex > -1) {
370
- this.$set(this.rows, this.editingIndex, { ...this.editForm })
371
- this.$message.success('修改成功')
372
- }
373
- this.editDialogVisible = false
374
- },
375
- removeRow(index) {
376
- this.rows.splice(index, 1)
377
- },
378
- runDetection() {
379
- this.detectionTriggered = true
380
- this.calculateErrors()
381
- this.detectionCount = this.errorList.length
382
- if (this.errorList.length === 0) {
383
- this.$message.success('数据检测通过:无异常数据')
384
- } else {
385
- this.$message.warning('数据检测发现异常,详见下方错误明细')
386
- }
387
- },
388
- calculateErrors() {
389
- const errors = []
390
- const requiredFields = [
391
- 'month',
392
- 'assetsType',
393
- 'assetName',
394
- 'trackingIndex',
395
- 'opinionEmotion',
396
- 'opinionDescription',
397
- 'argumentDetail',
398
- 'opinion',
399
- 'sourceChannel',
400
- 'dataSource',
401
- ]
402
- this.rows.forEach((r, idx) => {
403
- const index = idx + 1
404
- requiredFields.forEach(f => {
405
- if (r[f] === undefined || r[f] === null || String(r[f]).trim() === '') {
406
- errors.push({ type: '数据缺失', index, message: `${f}缺失` })
407
- }
408
- })
409
- if (r.opinionEmotion && !this.allowedOpinions.includes(r.opinionEmotion)) {
410
- errors.push({ type: '未识别', index, message: `未来6个月观点不在限制值范围` })
411
- }
412
- })
413
- this.errorList = errors
414
- this.errorSummary.missingCount = errors.filter(e => e.type === '数据缺失').length
415
- this.errorSummary.invalidCount = errors.filter(e => e.type === '未识别').length
416
- },
417
- hasDuplicate() {
418
- // 判重:同一机构来源(opinion),同一观点月份(month),同一资产名称(assetName)
419
- const setKey = new Set(
420
- (this.existingData || []).map(
421
- d => `${d.opinion}|${(d.gmtModified || '').slice(0, 7)}|${d.opinionTarget}`,
422
- ),
423
- )
424
- return this.rows.some(r => setKey.has(`${r.opinion}|${r.month}|${r.assetName}`))
425
- },
426
- submit() {
427
- this.$refs.formRef.validate(valid => {
428
- if (!valid) {
429
- this.$message.error('请完整填写右侧表单')
430
- return
431
- }
432
- this.calculateErrors()
433
- if (this.errorList.length > 0) {
434
- this.submitErrorPopoverVisible = true
435
- this.detectionTriggered = true
436
- this.detectionCount = this.errorList.length
437
- return
438
- }
439
- const proceed = () => {
440
- this.$message.success('提交成功')
441
- this.$emit('close')
442
- }
443
- if (this.hasDuplicate()) {
444
- this.$confirm('观点已存在,提交将进行覆盖,是否确认操作', '提示', {
445
- type: 'warning',
446
- })
447
- .then(() => proceed())
448
- .catch(() => {})
449
- } else {
450
- proceed()
451
- }
452
- })
453
- },
454
- },
455
- }
456
- </script>
457
-
458
- <style scoped lang="scss">
459
- .create-overlay {
460
- position: fixed;
461
- inset: 0;
462
- background: #fff;
463
- z-index: 1000;
464
- display: flex;
465
- flex-direction: column;
466
- }
467
- .create-header {
468
- display: flex;
469
- align-items: center;
470
- justify-content: space-between;
471
- padding: 12px 16px;
472
- border-bottom: 1px solid #ebeef5;
473
- flex-shrink: 0;
474
- .center {
475
- font-weight: 600;
476
- color: #303133;
477
- }
478
- }
479
- .create-content {
480
- flex: 1;
481
- overflow: hidden;
482
- padding: 16px;
483
- display: flex;
484
- flex-direction: column;
485
- }
486
- .top-section {
487
- flex: 1;
488
- display: flex;
489
- gap: 16px;
490
- min-height: 0; /* 关键:允许flex子项收缩到小于内容高度,触发内部滚动 */
491
- margin-bottom: 16px;
492
- }
493
- .left-table {
494
- flex: 8;
495
- min-width: 0; /* 防止表格把容器撑开 */
496
- min-height: 0; /* 关键:允许内部滚动 */
497
- display: flex;
498
- flex-direction: column;
499
- overflow: hidden; /* 自己不滚,交给内部 */
500
- }
501
- .edit-table {
502
- flex: 1;
503
- min-height: 0;
504
- overflow: auto;
505
- }
506
-
507
- .right-form {
508
- flex: 2;
509
- border-left: 1px dashed #ebeef5;
510
- padding-left: 16px;
511
- height: 100%;
512
- overflow-y: auto;
513
- }
514
- .form-header-row {
515
- display: flex;
516
- justify-content: space-between;
517
- align-items: center;
518
- margin-bottom: 16px;
519
- padding-bottom: 12px;
520
- border-bottom: 1px solid #ebeef5;
521
- }
522
- .form-title {
523
- font-weight: 600;
524
- color: #303133;
525
- font-size: 14px;
526
- }
527
- .uploaded-file {
528
- margin-top: 8px;
529
- color: #606266;
530
- }
531
- .form-actions {
532
- display: flex;
533
- justify-content: flex-end;
534
- gap: 8px;
535
- margin-top: 8px;
536
- }
537
- .expandable-content {
538
- width: 100%;
539
- .content-collapse {
540
- display: -webkit-box;
541
- -webkit-line-clamp: 3;
542
- line-clamp: 3;
543
- -webkit-box-orient: vertical;
544
- overflow: hidden;
545
- text-overflow: ellipsis;
546
- white-space: normal;
547
- word-wrap: break-word;
548
- }
549
- .content-expanded {
550
- white-space: normal;
551
- word-wrap: break-word;
552
- overflow: visible;
553
- }
554
- .el-button {
555
- margin-top: 5px;
556
- color: #409eff;
557
- }
558
- }
559
- .bottom-section {
560
- flex-shrink: 0;
561
- margin-top: 0; /* spacing handled by top-section margin-bottom */
562
- .row-1 {
563
- display: flex;
564
- justify-content: space-between;
565
- align-items: center;
566
- padding: 8px 0;
567
- border-top: 1px solid #ebeef5;
568
- .left {
569
- display: flex;
570
- align-items: center;
571
- gap: 12px;
572
- .label {
573
- font-weight: 600;
574
- color: #303133;
575
- }
576
- }
577
- .right {
578
- display: flex;
579
- gap: 8px;
580
- }
581
- }
582
- .row-2 {
583
- margin-top: 12px;
584
- max-height: 150px;
585
- overflow-y: auto;
586
- .error-title {
587
- color: #f56c6c;
588
- font-weight: 600;
589
- margin-bottom: 6px;
590
- }
591
- .error-group .text {
592
- color: #f56c6c;
593
- line-height: 22px;
594
- }
595
- .no-error {
596
- color: #67c23a;
597
- }
598
- }
599
- }
600
- .error-popover .title {
601
- font-weight: 600;
602
- color: #f56c6c;
603
- }
604
- .error-popover .summary {
605
- margin-top: 6px;
606
- color: #f56c6c;
607
- }
608
- .error-popover .details {
609
- margin-top: 6px;
610
- color: #f56c6c;
611
- }
612
- </style>