ai-read-over-pro 0.0.18 → 0.0.19

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.
@@ -77,6 +77,14 @@ export const loopPoll = (data) => {
77
77
  });
78
78
  }
79
79
 
80
+ export const analysisSave = (data) => {
81
+ return request({
82
+ url: prefix + '/criticism/analysis/save',
83
+ method: 'post',
84
+ data,
85
+ });
86
+ }
87
+
80
88
  // export const registerSse = (sseSessionId, fn, successFn) => {
81
89
  // const source = new EventSourcePolyfill(baseURL + prefix + '/sse?id=' + sseSessionId, {
82
90
  // headers: {
@@ -0,0 +1,301 @@
1
+ <template>
2
+ <div class="ri-wrap">
3
+ <!-- 公式弹窗 -->
4
+ <el-popover
5
+ v-model="formulaVisible"
6
+ popper-class="ri-formula-popover"
7
+ placement="top-start"
8
+ trigger="manual"
9
+ >
10
+ <div class="ri-formula-container">
11
+ <iframe
12
+ :src="`https://zjyw.icve.com.cn/kityformula-editor/kityFormula.html?c=${formulaValue}`"
13
+ ref="editorFrame"
14
+ class="ri-formula-iframe"
15
+ ></iframe>
16
+ <el-button type="primary" size="small" class="ri-formula-save" @click="saveFormula">保存</el-button>
17
+ </div>
18
+ <!-- 触发锚点(隐藏,通过 v-model 手动控制) -->
19
+ <span slot="reference"></span>
20
+ </el-popover>
21
+
22
+ <!-- 编辑器容器 -->
23
+ <div
24
+ class="ri-editor-wrap"
25
+ :style="{ minHeight: height + 'px' }"
26
+ :class="{ 'ri-editor-focus': isFocused }"
27
+ >
28
+ <Editor
29
+ class="ri-editor"
30
+ :style="{ minHeight: height + 'px' }"
31
+ :defaultConfig="editorConfig"
32
+ :mode="'simple'"
33
+ @onCreated="onCreated"
34
+ @onChange="onChange"
35
+ @onFocus="isFocused = true"
36
+ @onBlur="isFocused = false"
37
+ @customPaste="customPaste"
38
+ />
39
+ </div>
40
+
41
+ <!-- 插入公式按钮 -->
42
+ <div class="ri-toolbar">
43
+ <span class="ri-formula-btn" @click="openFormula">
44
+ <img src="../static/formula.png" class="ri-fx-img" alt="fx" />
45
+ 插入公式
46
+ </span>
47
+ </div>
48
+ </div>
49
+ </template>
50
+
51
+ <script>
52
+ import { Boot } from '@wangeditor/editor';
53
+ import formulaModule from '@wangeditor/plugin-formula';
54
+ import { Editor } from '@wangeditor/editor-for-vue';
55
+
56
+ let _booted = false;
57
+ function ensureBoot() {
58
+ if (!_booted) {
59
+ try {
60
+ Boot.registerModule(formulaModule);
61
+ } catch (e) {}
62
+ _booted = true;
63
+ }
64
+ }
65
+ ensureBoot();
66
+
67
+ export default {
68
+ name: 'RichInput',
69
+ components: { Editor },
70
+ props: {
71
+ value: {
72
+ type: String,
73
+ default: '',
74
+ },
75
+ placeholder: {
76
+ type: String,
77
+ default: '请输入内容,支持公式编辑',
78
+ },
79
+ height: {
80
+ type: Number,
81
+ default: 90,
82
+ },
83
+ },
84
+ data() {
85
+ return {
86
+ editor: null,
87
+ isFocused: false,
88
+ formulaVisible: false,
89
+ formulaValue: '',
90
+ editorConfig: {
91
+ placeholder: this.placeholder,
92
+ hoverbarKeys: {
93
+ formula: {
94
+ menuKeys: [],
95
+ },
96
+ },
97
+ },
98
+ };
99
+ },
100
+ watch: {
101
+ value(val) {
102
+ if (!this.editor) return;
103
+ try {
104
+ const current = this.editor.getHtml();
105
+ if (val !== current) {
106
+ // 直接 nextTick 就够,因为此时编辑器已经初始化完成
107
+ this.$nextTick(() => {
108
+ try {
109
+ this.editor.setHtml(val || '<p><br></p>');
110
+ } catch (e) {}
111
+ });
112
+ }
113
+ } catch (e) {}
114
+ },
115
+ },
116
+ mounted() {
117
+ window.addEventListener('message', this.latexMessage);
118
+ },
119
+ beforeDestroy() {
120
+ window.removeEventListener('message', this.latexMessage);
121
+ if (this.editor) {
122
+ this.editor.destroy();
123
+ }
124
+ },
125
+ methods: {
126
+ onCreated(editor) {
127
+ this.editor = Object.seal(editor);
128
+ },
129
+
130
+ onChange(editor) {
131
+ this.$emit('input', editor.getHtml());
132
+ },
133
+
134
+ openFormula() {
135
+ if (!this.editor) return;
136
+ this.editor.focus();
137
+ this.formulaValue = '';
138
+ this.formulaVisible = true;
139
+ },
140
+
141
+ saveFormula() {
142
+ if (this.$refs.editorFrame) {
143
+ this.$refs.editorFrame.contentWindow.postMessage('message', '*');
144
+ }
145
+ setTimeout(() => {
146
+ this.formulaVisible = false;
147
+ }, 100);
148
+ },
149
+
150
+ latexMessage(event) {
151
+ if (!this.formulaVisible) return;
152
+ const { mceAction, content } = event.data || {};
153
+ if (mceAction !== 'insertContent') return;
154
+ if (!content) return;
155
+
156
+ const match = content.match(/data-latex="([^"]+)"/);
157
+ if (!match) return;
158
+
159
+ let latex = match[1].replace(/\s/g, '');
160
+ if (latex === '\\placeholder') return;
161
+
162
+ if (!this.editor) return;
163
+
164
+ try {
165
+ this.editor.restoreSelection(); // 加 try-catch 兜底
166
+ } catch (e) {
167
+ // restoreSelection 失败时,把光标移到末尾
168
+ this.editor.focus(true);
169
+ }
170
+
171
+ const formulaNode = {
172
+ type: 'formula',
173
+ value: latex,
174
+ children: [{ text: '' }],
175
+ };
176
+ this.editor.insertNode(formulaNode);
177
+ },
178
+
179
+ customPaste(editor, event, callback) {
180
+ const text = event.clipboardData.getData('text/plain');
181
+ editor.insertText(text);
182
+ event.preventDefault();
183
+ callback(false);
184
+ },
185
+ },
186
+ };
187
+ </script>
188
+
189
+ <style lang="scss" scoped>
190
+ .ri-wrap {
191
+ width: 100%;
192
+ }
193
+
194
+ .ri-editor-wrap {
195
+ border: 1px solid #e0d8ff;
196
+ border-radius: 8px;
197
+ overflow: hidden;
198
+ transition: border-color 0.2s;
199
+ background: #fff;
200
+
201
+ &.ri-editor-focus {
202
+ border-color: #6040e0;
203
+ }
204
+ }
205
+
206
+ .ri-editor {
207
+ background: transparent;
208
+ font-size: 13px;
209
+ color: #333;
210
+ }
211
+
212
+ .ri-toolbar {
213
+ display: flex;
214
+ justify-content: flex-end;
215
+ margin-top: 4px;
216
+ }
217
+
218
+ .ri-formula-btn {
219
+ display: inline-flex;
220
+ align-items: center;
221
+ gap: 3px;
222
+ font-size: 12px;
223
+ color: #6040e0;
224
+ cursor: pointer;
225
+ user-select: none;
226
+ font-weight: bold;
227
+ padding: 2px 6px;
228
+ border-radius: 4px;
229
+ transition: background 0.2s;
230
+
231
+ &:hover {
232
+ background: rgba(96, 64, 224, 0.08);
233
+ }
234
+ }
235
+
236
+ .ri-fx-img {
237
+ width: 16px;
238
+ height: 16px;
239
+ object-fit: contain;
240
+ vertical-align: middle;
241
+ }
242
+
243
+ .ri-formula-iframe {
244
+ width: 782px;
245
+ height: 420px;
246
+ border: none;
247
+ outline: none;
248
+ }
249
+
250
+ .ri-formula-save {
251
+ position: absolute;
252
+ right: 24px;
253
+ bottom: 10px;
254
+ }
255
+
256
+ .ri-formula-container {
257
+ position: relative;
258
+ padding: 24px;
259
+ border-radius: 16px;
260
+ background: linear-gradient(
261
+ 103.5deg,
262
+ rgba(255, 238, 238, 1) 0%,
263
+ rgba(255, 255, 255, 1) 29%,
264
+ rgba(255, 255, 255, 1) 71%,
265
+ rgba(204, 238, 255, 1) 100%
266
+ );
267
+ }
268
+ </style>
269
+
270
+ <style lang="scss">
271
+ .ri-formula-popover {
272
+ padding: 2px;
273
+ border-radius: 16px;
274
+ z-index: 3000 !important;
275
+ }
276
+
277
+ /* 覆盖 wangeditor 默认样式,使其适配字段高度 */
278
+ .ri-editor-wrap {
279
+ .w-e-text-container {
280
+ background: transparent !important;
281
+
282
+ p {
283
+ span {
284
+ background-color: transparent !important;
285
+ }
286
+ }
287
+ }
288
+
289
+ .w-e-text-placeholder {
290
+ font-size: 13px;
291
+ color: #bbb;
292
+ }
293
+
294
+ /* 隐藏工具栏(simple 模式无工具栏,仅保留悬浮栏) */
295
+ .w-e-toolbar {
296
+ display: none !important;
297
+ }
298
+ }
299
+ </style>
300
+
301
+ <style src="@wangeditor/editor/dist/css/style.css"></style>
@@ -52,7 +52,8 @@
52
52
  <read-over :exercises-data="activeItem"
53
53
  :analyze-data="analyzeData"
54
54
  @on-reanswer="onReanswer"
55
- @on-noty-apply="notyApply"/>
55
+ @on-noty-apply="notyApply"
56
+ @on-edit="onEditAnalyze"/>
56
57
  <div class="member-table" v-if="stretch">
57
58
  <member-table :check-data="showTableList"
58
59
  :show-data="tabList"
@@ -68,7 +69,7 @@
68
69
  import ReadOver from './read-over.vue';
69
70
  import { getUserInfo } from '../utils/config';
70
71
  // import {generateReview, registerSse, criticismApply, reanswerReview} from '../api/index';
71
- import { generateReview, loopPoll, criticismApply, reanswerReview } from '../api/index';
72
+ import { generateReview, loopPoll, criticismApply, reanswerReview, analysisSave } from '../api/index';
72
73
  import Cookies from "js-cookie";
73
74
  import TabFilter from './tab-filter.vue';
74
75
  import MemberTable from './member-table.vue';
@@ -307,6 +308,20 @@ export default {
307
308
  if (!this.listData || this.listData.length < 1) {
308
309
  throw new Error('list-data 不能为空或者长度不能为0');
309
310
  }
311
+ },
312
+ async onEditAnalyze(data) {
313
+ this.$set(this.replyCache, this.activeItem.id, data);
314
+ this.analyzeData = data;
315
+ try {
316
+ await analysisSave({
317
+ id: data.id,
318
+ analysis: JSON.stringify(data.jsonContent),
319
+ score: data.score,
320
+ });
321
+ this.$message.success('保存成功');
322
+ } catch (e) {
323
+ this.$message.error('保存失败,请重试');
324
+ }
310
325
  }
311
326
  }
312
327
  }
@@ -27,8 +27,12 @@
27
27
  <span class="score-number">{{ analyzeData.score }}</span>分
28
28
  </div>
29
29
  <div class="type" :class="{ 'type-active': analyzeData.accepted }" @click="notyApply">应用</div>
30
+ <div class="edit-btn" v-if="!analyzeData.accepted" @click="openEdit">
31
+ <i class="el-icon-edit"></i>
32
+ 编辑
33
+ </div>
30
34
  <div class="exercises-read-over-content">
31
- <div class="list" v-for="item in analyzeData.jsonContent">
35
+ <div class="list" v-for="(item, index) in analyzeData.jsonContent" :key="index">
32
36
  <p class="title" v-if="item.title">{{ item.title }}</p>
33
37
  <p class="content" v-html="renderFormulas(item.analysis)"></p>
34
38
  </div>
@@ -39,17 +43,58 @@
39
43
  </div>
40
44
  </template>
41
45
  </div>
46
+
47
+ <el-dialog :visible.sync="editVisible" width="600px" append-to-body custom-class="read-over-edit-dialog"
48
+ :close-on-click-modal="false" @opened="onDialogOpened">
49
+ <div slot="title" class="edit-dialog-header">
50
+ <div class="edit-dialog-title">
51
+ <i class="el-icon-edit-outline"></i>
52
+ 编辑批阅解析
53
+ </div>
54
+ </div>
55
+ <div class="edit-dialog-body">
56
+ <div class="edit-score-field">
57
+ <span class="edit-score-label">得分</span>
58
+ <el-input-number
59
+ v-model="editScore"
60
+ :min="0"
61
+ :max="exercisesData.score"
62
+ :precision="1"
63
+ :step="0.5"
64
+ size="small"
65
+ />
66
+ <span class="edit-score-max">/ {{ exercisesData.score }} 分</span>
67
+ </div>
68
+ <span class="edit-label">批阅解析</span>
69
+ <div class="edit-item" v-for="(item, index) in editList" :key="index">
70
+ <rich-input
71
+ :value="item.analysisHtml"
72
+ placeholder="请输入解析内容,支持公式编辑"
73
+ :height="120"
74
+ @input="(v) => $set(editList[index], 'analysisHtml', v)"
75
+ />
76
+ </div>
77
+ </div>
78
+ <div slot="footer" class="edit-dialog-footer">
79
+ <el-button class="edit-btn-cancel" @click="editVisible = false">取消</el-button>
80
+ <el-button class="edit-btn-save" @click="saveEdit">
81
+ 保存
82
+ </el-button>
83
+ </div>
84
+ </el-dialog>
42
85
  </div>
43
86
  </template>
44
87
  <script>
45
88
  import ChatTools from "./chat-tools.vue";
89
+ import RichInput from "./RichInput.vue";
46
90
  import katex from 'katex';
47
91
  import 'katex/dist/katex.min.css';
48
92
  import 'katex/contrib/mhchem/mhchem.js';
49
93
  export default {
50
94
  name: 'ReadOver',
51
95
  components: {
52
- ChatTools
96
+ ChatTools,
97
+ RichInput,
53
98
  },
54
99
  props: {
55
100
  exercisesData: {
@@ -63,7 +108,9 @@ export default {
63
108
  },
64
109
  data() {
65
110
  return {
66
-
111
+ editVisible: false,
112
+ editList: [],
113
+ editScore: 0,
67
114
  }
68
115
  },
69
116
  methods: {
@@ -73,6 +120,62 @@ export default {
73
120
  notyApply() {
74
121
  this.$emit('on-noty-apply', this.analyzeData, this.exercisesData);
75
122
  },
123
+ openEdit() {
124
+ this.editScore = this.analyzeData.score ?? 0;
125
+ this.editList = (this.analyzeData.jsonContent || []).map((item) => ({
126
+ ...item,
127
+ analysisHtml: this.plainToHtml(item.analysis),
128
+ }));
129
+ this.editVisible = true;
130
+ },
131
+ onDialogOpened() {
132
+ this.editList = this.editList.map((item, index) => {
133
+ const origin = this.analyzeData.jsonContent[index];
134
+ return {
135
+ ...item,
136
+ analysisHtml: this.plainToHtml(origin.analysis),
137
+ };
138
+ });
139
+ },
140
+ saveEdit() {
141
+ const jsonContent = this.editList.map((item) => ({
142
+ ...item,
143
+ analysis: this.htmlToPlain(item.analysisHtml),
144
+ analysisHtml: undefined,
145
+ }));
146
+ this.$emit('on-edit', { ...this.analyzeData, score: this.editScore, jsonContent });
147
+ this.editVisible = false;
148
+ },
149
+ plainToHtml(text) {
150
+ if (!text) return '';
151
+ const escaped = text
152
+ .replace(/\$\$([^$]+)\$\$/g, (_, latex) =>
153
+ `<span data-w-e-type="formula" data-w-e-is-void data-w-e-is-inline data-value="${this.escAttr(latex)}"></span>`
154
+ )
155
+ .replace(/\$([^$]+)\$/g, (_, latex) =>
156
+ `<span data-w-e-type="formula" data-w-e-is-void data-w-e-is-inline data-value="${this.escAttr(latex)}"></span>`
157
+ );
158
+ return escaped.split('\n').map((line) => `<p>${line || '<br>'}</p>`).join('');
159
+ },
160
+ htmlToPlain(html) {
161
+ if (!html) return '';
162
+ let result = html.replace(
163
+ /<span[^>]+data-w-e-type="formula"[^>]+data-value="([^"]*)"[^>]*>[\s\S]*?<\/span>/g,
164
+ (_, latex) => `$${this.unescAttr(latex)}$`
165
+ );
166
+ result = result.replace(/<\/p>\s*<p>/gi, '\n');
167
+ result = result.replace(/<[^>]+>/g, '');
168
+ return result
169
+ .replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
170
+ .replace(/&nbsp;/g, ' ').replace(/&#39;/g, "'").replace(/&quot;/g, '"')
171
+ .trim();
172
+ },
173
+ escAttr(str) {
174
+ return str.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
175
+ },
176
+ unescAttr(str) {
177
+ return str.replace(/&quot;/g, '"').replace(/&#39;/g, "'");
178
+ },
76
179
  renderFormulas(text) {
77
180
  if (!text) return '';
78
181
  text = text.replace(/&gt;/g, '>')
@@ -300,5 +403,205 @@ export default {
300
403
  .type-active {
301
404
  background: linear-gradient(90deg, rgb(48, 144, 240) 0%, rgb(48, 192, 144) 100%);
302
405
  }
406
+ .edit-btn {
407
+ position: absolute;
408
+ right: 32px;
409
+ top: 70px;
410
+ width: 64px;
411
+ height: 32px;
412
+ color: rgba(96, 96, 224, 1);
413
+ text-align: center;
414
+ line-height: 32px;
415
+ border-radius: 20px;
416
+ font-size: 12px;
417
+ border: 1px solid rgba(96, 96, 224, 0.5);
418
+ cursor: pointer;
419
+ i {
420
+ font-size: 13px;
421
+ }
422
+ &:hover {
423
+ background-color: rgba(96, 96, 224, 0.08);
424
+ border-color: rgba(96, 96, 224, 0.8);
425
+ }
426
+ }
427
+ }
428
+ :deep(.read-over-edit-dialog) {
429
+ border-radius: 16px;
430
+ overflow: hidden;
431
+
432
+ .el-dialog__header {
433
+ padding: 24px 24px 16px;
434
+ border-bottom: 1px solid rgba(96, 96, 224, 0.12);
435
+ background: linear-gradient(135deg, rgba(255, 255, 255, 1) 0%, rgba(238, 238, 255, 0.6) 100%);
436
+ }
437
+
438
+ .el-dialog__body {
439
+ padding: 0;
440
+ }
441
+
442
+ .el-dialog__footer {
443
+ padding: 0;
444
+ border-top: 1px solid rgba(96, 96, 224, 0.12);
445
+ }
446
+ }
447
+
448
+ .edit-dialog-header {
449
+ .edit-dialog-title {
450
+ font-size: 18px;
451
+ font-weight: 700;
452
+ color: rgb(32, 40, 64);
453
+ display: flex;
454
+ align-items: center;
455
+ gap: 8px;
456
+ i {
457
+ font-size: 20px;
458
+ background: linear-gradient(90deg, rgb(0, 144, 240), rgb(144, 48, 240));
459
+ -webkit-background-clip: text;
460
+ background-clip: text;
461
+ -webkit-text-fill-color: transparent;
462
+ }
463
+ }
464
+ .edit-dialog-subtitle {
465
+ font-size: 12px;
466
+ color: rgba(32, 40, 64, 0.45);
467
+ margin-top: 4px;
468
+ padding-left: 28px;
469
+ }
470
+ }
471
+
472
+ .edit-dialog-body {
473
+ max-height: 480px;
474
+ overflow-y: auto;
475
+ padding: 20px 24px;
476
+ display: flex;
477
+ flex-direction: column;
478
+ gap: 20px;
479
+
480
+ .edit-score-field {
481
+ display: flex;
482
+ align-items: center;
483
+ gap: 10px;
484
+ padding: 12px 16px;
485
+ background: rgba(248, 248, 255, 1);
486
+ border: 1px solid rgba(96, 96, 224, 0.15);
487
+ border-radius: 10px;
488
+
489
+ .edit-score-label {
490
+ font-size: 13px;
491
+ font-weight: 600;
492
+ color: rgb(32, 40, 64);
493
+ flex-shrink: 0;
494
+ }
495
+
496
+ .edit-score-max {
497
+ font-size: 13px;
498
+ color: rgba(32, 40, 64, 0.5);
499
+ flex-shrink: 0;
500
+ }
501
+
502
+ :deep(.el-input-number) {
503
+ width: 120px;
504
+ .el-input__inner {
505
+ border-color: rgba(96, 96, 224, 0.3);
506
+ border-radius: 8px;
507
+ text-align: center;
508
+ &:focus {
509
+ border-color: rgba(96, 96, 224, 0.7);
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ &::-webkit-scrollbar {
516
+ width: 6px;
517
+ }
518
+ &::-webkit-scrollbar-track {
519
+ background: transparent;
520
+ }
521
+ &::-webkit-scrollbar-thumb {
522
+ background: rgba(96, 96, 224, 0.3);
523
+ border-radius: 4px;
524
+ }
525
+
526
+ .edit-label {
527
+ color: #000000;
528
+ font-weight: bold;
529
+ }
530
+
531
+ .edit-item {
532
+ .edit-item-header {
533
+ display: flex;
534
+ align-items: center;
535
+ gap: 8px;
536
+ margin-bottom: 8px;
537
+ }
538
+ .edit-item-index {
539
+ width: 20px;
540
+ height: 20px;
541
+ border-radius: 50%;
542
+ background: linear-gradient(135deg, rgb(0, 144, 240), rgb(96, 96, 224));
543
+ color: #fff;
544
+ font-size: 11px;
545
+ font-weight: 600;
546
+ display: inline-flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ flex-shrink: 0;
550
+ }
551
+ .edit-item-title {
552
+ font-size: 13px;
553
+ font-weight: 600;
554
+ color: rgb(96, 96, 224);
555
+ }
556
+ .edit-item-title--default {
557
+ color: rgba(32, 40, 64, 0.4);
558
+ font-weight: 400;
559
+ }
560
+ }
561
+ }
562
+
563
+ .edit-dialog-footer {
564
+ display: flex;
565
+ justify-content: flex-end;
566
+ align-items: center;
567
+ gap: 5px;
568
+ padding: 16px 24px;
569
+
570
+ .edit-btn-cancel {
571
+ height: 36px;
572
+ padding: 0 20px;
573
+ border-radius: 18px;
574
+ border: 1px solid rgba(96, 96, 224, 0.4);
575
+ color: rgba(96, 96, 224, 1);
576
+ background: transparent;
577
+ font-size: 13px;
578
+ transition: all 0.2s;
579
+
580
+ &:hover {
581
+ background: rgba(96, 96, 224, 0.06);
582
+ border-color: rgba(96, 96, 224, 0.7);
583
+ }
584
+ }
585
+
586
+ .edit-btn-save {
587
+ height: 36px;
588
+ padding: 0 24px;
589
+ border-radius: 18px;
590
+ border: none;
591
+ color: #fff;
592
+ background: linear-gradient(90deg, rgb(0, 144, 240) 0%, rgb(144, 48, 240) 100%);
593
+ font-size: 13px;
594
+ font-weight: 600;
595
+ transition: all 0.2s;
596
+
597
+ i {
598
+ margin-right: 4px;
599
+ }
600
+
601
+ &:hover {
602
+ opacity: 0.88;
603
+ box-shadow: 0 4px 12px rgba(96, 96, 224, 0.35);
604
+ }
605
+ }
303
606
  }
304
607
  </style>