yh-codemirror 1.0.7 → 1.0.9

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.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.0.10
4
+
5
+ - 2026-10-09 15:51 feature: `language` 新增 `json`;未知语言回落 `javascript`
6
+ - slw 的图表配置编辑器要用 JSON 语法高亮,之前只能靠宿主额外传 extensions
7
+ - `getCodeLanguagePlugin` 原来直接 `plugins[language]()`,未知语言(含 undefined)返回 undefined,
8
+ CM6 会抛 `Unrecognized extension value in extension set`,改为回落 javascript
9
+
3
10
  ## 1.0.7
4
11
 
12
+ - 2026-10-08 15:06 feature: 新增 `aiWindowId` 入参与 `ai-activate` 事件
13
+ - AI 面板打开时把 `aiWindowId` 通过 `ai-activate` 抛给宿主,宿主可趁用户还没打字时预取上下文
14
+ - 组件本身不认识任何业务接口(保持零依赖),窗口配置怎么取、怎么缓存完全由宿主决定
15
+ - `aiWindowId` 为空时不触发事件
5
16
  - 2026-09-30 18:54 feature: 新增 `aiPanelSize` 尺寸档位(`'normal' | 'large'`)
6
17
  - 放大场景(window-form 的「放大」抽屉)传 `large`:面板内边距 6/8 → 10/12、字号 12 → 14px、
7
18
  输入框 2 行 → 4 行、历史面板 max-height 62% → 72%(代码区 170px → 300px)
package/CodeMirror.vue CHANGED
@@ -101,6 +101,12 @@
101
101
  class="ai-history-q"
102
102
  @click="toggleHistoryItem(item.id)">
103
103
  <span class="ai-history-time">{{ item.time }}</span>
104
+ <span
105
+ v-if="item.location"
106
+ class="ai-history-loc"
107
+ :title="item.location"
108
+ >{{ item.location }}</span
109
+ >
104
110
  <span class="ai-history-text">{{ item.prompt }}</span>
105
111
  <span class="ai-history-toggle">{{ expandedHistoryId === item.id ? '收起' : '展开' }}</span>
106
112
  </div>
@@ -138,10 +144,10 @@
138
144
  @click="openAi()"
139
145
  >AI</span
140
146
  >
141
- <i
147
+ <!-- <i
142
148
  title="全屏"
143
149
  class="iconfont icon-fullscreen"
144
- @click="fullscreen"></i>
150
+ @click="fullscreen"></i> -->
145
151
  <i
146
152
  title="美化"
147
153
  class="iconfont icon-magic-stick"
@@ -218,10 +224,20 @@ const props = withDefaults(
218
224
  /** 编辑器主题:dark(默认)或 light */
219
225
  theme?: 'dark' | 'light';
220
226
  /**
221
- * AI 输入框/历史面板的尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 'large',
222
- * 否则 12px 字号 + 2 行输入框在大面板里会显得很局促。
227
+ * AI 面板尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 'large'
223
228
  */
224
229
  aiPanelSize?: 'normal' | 'large';
230
+ /**
231
+ * AI 上下文用的「所属窗口 ID」(SLW 为 formConfig.params.parentData.SYS_WINDOW_ID)。
232
+ * 组件本身不知道怎么取窗口配置,只在 AI 被激活时通过 `ai-activate` 事件把它抛给宿主,
233
+ * 由宿主去拉配置;不传就不触发该事件。
234
+ */
235
+ aiWindowId?: string | number;
236
+ /**
237
+ * 这条提问来自哪里(宿主传入,如「区域名称 · 数据初始化」)。
238
+ * 组件只负责原样记进历史并在问题前显示,不解析它的含义
239
+ */
240
+ aiLocation?: string;
225
241
  }>(),
226
242
  {
227
243
  modelValue: '',
@@ -232,6 +248,8 @@ const props = withDefaults(
232
248
  aiScope: 'document',
233
249
  theme: 'dark',
234
250
  aiPanelSize: 'normal',
251
+ aiWindowId: '',
252
+ aiLocation: '',
235
253
  }
236
254
  );
237
255
 
@@ -316,7 +334,7 @@ watch(
316
334
  }
317
335
  );
318
336
 
319
- const emits = defineEmits(['update:modelValue', 'ai-apply', 'ai-error']);
337
+ const emits = defineEmits(['update:modelValue', 'ai-apply', 'ai-error', 'ai-activate']);
320
338
 
321
339
  /* ------------------------------ AI 提示编辑 ------------------------------ */
322
340
  const aiEnabled = computed(() => typeof props.aiRequest === 'function');
@@ -376,6 +394,10 @@ const openAi = (presetPrompt?: string) => {
376
394
  aiError.value = '';
377
395
  aiLoading.value = false;
378
396
  aiVisible.value = true;
397
+ // AI 被激活:把窗口 ID 抛给宿主,让它提前把窗口配置拉好(组件本身不认识任何业务接口)
398
+ if (props.aiWindowId !== '' && props.aiWindowId !== null && props.aiWindowId !== undefined) {
399
+ emits('ai-activate', props.aiWindowId);
400
+ }
379
401
  nextTick(() => {
380
402
  aiInputRef.value && aiInputRef.value.focus();
381
403
  });
@@ -476,7 +498,7 @@ const submitAi = async () => {
476
498
  view.focus();
477
499
  const appliedText = view.state.sliceDoc(range.from, range.to);
478
500
  // 记录历史问答(含 AI 最终写入编辑器的完整文本,撤销了也能在这里找回原文)
479
- pushAiHistory({prompt, scope: aiScopeText.value, answer: appliedText});
501
+ pushAiHistory({prompt, scope: aiScopeText.value, location: props.aiLocation, answer: appliedText});
480
502
  emits('ai-apply', {prompt, text: appliedText});
481
503
  ElMessage({
482
504
  message: 'AI 修改已应用',
@@ -651,19 +673,30 @@ defineExpose({
651
673
  &.is-light {
652
674
  background-color: #fff;
653
675
  .cm-editor {
676
+ border: 1px solid var(--el-border-color-light);
677
+ border-radius: 8px;
678
+ cursor: text;
679
+ width: 100%;
680
+ overflow: auto;
681
+ .cm-gutters.cm-gutters-before {
682
+ background-color: #fafcff;
683
+ }
654
684
  .cm-line.cm-activeLine::before {
655
- background-color: #f0f2f5;
685
+ background-color: #f1f7ff;
656
686
  }
657
687
  .cm-activeLineGutter {
658
- background-color: #f0f2f5;
688
+ background-color: #f1f7ff;
659
689
  }
660
690
  .cm-selectionBackground {
661
691
  background-color: #d4e7f9 !important;
662
692
  }
663
- &.cm-focused .cm-selectionBackground {
664
- background-color: #b3d8ff !important;
693
+ &.cm-focused {
694
+ outline: none;
695
+ box-shadow: 1px 1px 8px #b1c7db;
696
+ .cm-selectionBackground {
697
+ background-color: #b3d8ff !important;
698
+ }
665
699
  }
666
- // 双击选词后其它同名 occurrence(highlightSelectionMatches):用暖色,和蓝色选区、中性当前行都区分开
667
700
  .cm-selectionMatch {
668
701
  background-color: #ffe9c2;
669
702
  }
@@ -734,6 +767,10 @@ defineExpose({
734
767
  .ai-history-q {
735
768
  color: #333;
736
769
  }
770
+ .ai-history-loc {
771
+ background: rgba(64, 158, 255, 0.14);
772
+ color: #1a6fc4;
773
+ }
737
774
  .ai-history-label .ai-history-scope {
738
775
  color: #606266;
739
776
  }
@@ -759,16 +796,6 @@ defineExpose({
759
796
  line-height: 25px;
760
797
  font-family: 'Fira Code', 'Roboto Mono', Consolas, 'Microsoft Yahei';
761
798
  }
762
- /* 当前行高亮 vs 选中区,必须一眼能区分 —— 这里真正的坑不是配色而是**绘制层级**:
763
- CM6 的选区画在 .cm-selectionLayer 上(drawSelection 内部 above:false,z-index 取 -1-pos,是负数),
764
- 它和 .cm-content 同在 .cm-scroller 这个层叠上下文里;而 .cm-line 是普通 in-flow 块,
765
- 按 CSS 绘制顺序「负 z-index 后代 → in-flow 块 → 行内内容」,**in-flow 块画在选区层之上**。
766
- 所以给 .cm-line 设不透明背景会把选区整块盖掉 —— baseTheme 自带的
767
- &light .cm-activeLine: #cceeff44 / &dark: #99eeff33 是半透明的,才不会盖住,
768
- 之前改成了不透明色,选区就被糊掉了。
769
- 解法:把当前行高亮挪到 ::before 上并压到 z-index:-100,绘制顺序变成
770
- 当前行底(-100) → 选区层(-2) → 文字
771
- 选中区永远压在当前行高亮之上,同时仍然在文字之下。 */
772
799
  .cm-line.cm-activeLine::before {
773
800
  content: '';
774
801
  position: absolute;
@@ -780,19 +807,12 @@ defineExpose({
780
807
  pointer-events: none;
781
808
  background-color: #173541;
782
809
  }
783
- // 当前行高亮改由上面的 ::before 承载;这里必须把主题自带的**不透明**底色清掉,
784
- // 否则它作为 in-flow 块的背景仍然会盖住选区层(solarizedDark 的 .cm-activeLine 就是 #173541,
785
- // 和它的 selection 同色,这正是最初「选中区看不出来」的根源)。
786
810
  .cm-activeLine {
787
811
  background-color: transparent;
788
812
  }
789
813
  .cm-activeLineGutter {
790
814
  background-color: #173541;
791
815
  }
792
- /* 选区色必须带 !important:CM 6.39 的 baseTheme 把默认色写成了
793
- "&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground"
794
- 这种「5 个类 + 子代选择器」,权重高于本组件、也高于 solarizedDark 自带的主题,
795
- 光堆类名盖不住;用 !important 也不用跟它的 DOM 结构绑死。 */
796
816
  .cm-selectionBackground {
797
817
  /* 失焦:压暗一档 */
798
818
  background-color: #204a5a !important;
@@ -1012,6 +1032,19 @@ defineExpose({
1012
1032
  font-size: 11px;
1013
1033
  opacity: 0.55;
1014
1034
  }
1035
+ .ai-history-loc {
1036
+ flex: none;
1037
+ max-width: 45%;
1038
+ padding: 0 5px;
1039
+ border-radius: 2px;
1040
+ background: rgba(64, 158, 255, 0.22);
1041
+ color: #3070af;
1042
+ font-size: 11px;
1043
+ line-height: 16px;
1044
+ overflow: hidden;
1045
+ white-space: nowrap;
1046
+ text-overflow: ellipsis;
1047
+ }
1015
1048
  .ai-history-text {
1016
1049
  flex: 1;
1017
1050
  overflow: hidden;
@@ -1057,9 +1090,6 @@ defineExpose({
1057
1090
  word-break: break-all;
1058
1091
  }
1059
1092
  }
1060
- /* 放大场景(window-form 的「放大」抽屉):编辑区撑满整个抽屉,AI 面板仍是 12px / 2 行会显得很局促,
1061
- 这里整体提一档字号、加两行输入高度、面板内边距与历史面板高度同步放大。
1062
- 选择器带 .is-ai-large 是为了权重高于上面 .is-light 里的同名规则,不依赖书写顺序。 */
1063
1093
  &.is-ai-large {
1064
1094
  .ai-prompt-panel,
1065
1095
  .ai-history-panel {
package/README.md CHANGED
@@ -13,6 +13,10 @@ Vue3 的 CodeMirror6 封装,在全屏 / 美化 / 全选 / 复制 / 清空 等
13
13
  </yh-codemirror>
14
14
  ```
15
15
 
16
+ `language` 支持 `javascript / sql / css / java / html / json`,可传数组;不在列表内会取不到语言插件。
17
+ `autocomplete` 只对 `javascript` 生效(按语言名取,如 `{javascript: myCompletionSource}`)。
18
+ 不传 `aiRequest` 就是一个纯编辑器,不会出现任何 AI 入口。
19
+
16
20
  ## AI 提示编辑
17
21
 
18
22
  组件本身**不内置任何 AI 服务**(保持零业务依赖),由宿主通过 `aiRequest` 注入请求实现。传入 `aiRequest` 即自动启用:
@@ -24,11 +28,15 @@ Vue3 的 CodeMirror6 封装,在全屏 / 美化 / 全选 / 复制 / 清空 等
24
28
  - 支持流式:调用 `onChunk` 逐段返回,编辑器实时写入;非流式则直接返回完整替换文本
25
29
  - 自动剥离模型常见的 ` ```lang ... ``` ` 包裹
26
30
  - 通过 dispatch 写入,保留 CodeMirror 撤销栈,可 `Ctrl+Z` 回退
27
- - **历史问答**:输入框右上角「历史」可查看本轮会话问过的问题与 AI 的完整回答(可展开、可复制、可清空)
31
+ - **历史问答**:输入框右上角「历史」可查看本轮会话问过的问题与 AI 的完整回答(可展开、可复制、可清空),
32
+ 每条前面会显示宿主传入的 `aiLocation`,建议格式「区域名称 · <按钮/查询条件/字段名称 + 类型> · 编辑器字段名」
33
+ (如「区域信息 · 事件按钮 · 点击事件」「区域信息 · 名称字段 · 字段值更改」;
34
+ 直接在区域事件里提问时没有中间层,就是「区域信息 · 数据初始化」)
28
35
 
29
36
  历史记录存在 `ai-history.js` 的模块级单例里(上限 50 条,最新在前)。之所以不放组件内:
30
37
  配置面板里的编辑器会随切换配置项反复卸载重建,组件内的 ref 一卸载就没了。
31
- 代价是同一页面所有编辑器共用一份(未做分组),所以每条都带时间与问题原文;需要按字段分组时再给 `pushAiHistory` 加 key 维度。
38
+ 代价是同一页面所有编辑器共用一份(未做分组),所以每条都带时间、问题原文和 `aiLocation`——
39
+ 没有归属信息时,一堆「把名称标红」的提问根本分不清是哪块配置。
32
40
 
33
41
  ### Props
34
42
 
@@ -39,12 +47,15 @@ Vue3 的 CodeMirror6 封装,在全屏 / 美化 / 全选 / 复制 / 清空 等
39
47
  | `aiPlaceholder` | `string` | 见源码 | 输入框占位提示 |
40
48
  | `aiScope` | `'document' \| 'cursor'` | `'document'` | 无选区时的作用范围 |
41
49
  | `aiPanelSize` | `'normal' \| 'large'` | `'normal'` | AI 输入框 / 历史面板的尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 `large` |
50
+ | `aiWindowId` | `string \| number` | `''` | AI 上下文用的「所属窗口 ID」。组件不认识任何业务接口,只在 AI 被唤起时通过 `ai-activate` 把它抛给宿主,由宿主去取窗口配置 |
51
+ | `aiLocation` | `string` | `''` | 这条提问来自哪里(建议「区域名 · 按钮/查询条件/字段名 + 类型 · 编辑器字段名」,如「区域信息 · 事件按钮 · 点击事件」,名称一律取 NAME)。组件只原样记进历史并显示在问题前,不解析其含义 |
42
52
 
43
53
  ### Events
44
54
 
45
55
  | 事件 | 参数 | 说明 |
46
56
  | ---------------- | ----------------------------- | ------------------------ |
47
57
  | `update:modelValue` | `string` | 代码变更(原有) |
58
+ | `ai-activate` | `string \| number` | AI 面板刚打开(携带 `aiWindowId`),宿主可趁此时预取上下文;`aiWindowId` 为空时不触发 |
48
59
  | `ai-apply` | `{prompt, text}` | AI 结果已写入编辑器 |
49
60
  | `ai-error` | `Error` | AI 请求失败 |
50
61
 
package/ai-history.js CHANGED
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * AI 问答历史(模块级单例)。
3
3
  *
4
- * 为什么放模块级而不是组件内:window-form 这类配置面板里,代码编辑器会随着切换配置项/关闭抽屉
5
- * 反复卸载重建,组件内的 ref 一卸载就没了,历史根本留不住。放在模块级只要页面不刷新就一直在。
6
- *
7
- * 代价是同一页面里所有编辑器共用一份历史(未做分组),所以每条都带上时间与问题原文,
8
- * 靠问题内容本身区分是哪个字段的提问。需要按字段分组时再给 pushAiHistory 加 key 维度。
4
+ * 同一页面里所有编辑器共用一份历史(未做分组),所以每条都要带上:
5
+ * - location:这条提问来自哪里(宿主传入,如「区域名称 · 数据初始化」)
6
+ * - time / prompt:时间与问题原文。
9
7
  */
10
8
  import {ref} from 'vue';
11
9
 
@@ -13,7 +11,7 @@ import {ref} from 'vue';
13
11
  const MAX_HISTORY = 50;
14
12
 
15
13
  /**
16
- * @type {import('vue').Ref<Array<{id:number, time:string, prompt:string, scope:string, answer:string}>>}
14
+ * @type {import('vue').Ref<Array<{id:number, time:string, prompt:string, scope:string, location:string, answer:string}>>}
17
15
  * 最新的排在最前面
18
16
  */
19
17
  export const aiHistory = ref([]);
@@ -28,13 +26,13 @@ const nowText = () => {
28
26
  )}`;
29
27
  };
30
28
 
31
- /** 追加一条:{prompt, scope, answer} */
32
- export const pushAiHistory = ({prompt, scope = '', answer = ''}) => {
29
+ /** 追加一条:{prompt, scope, location, answer} */
30
+ export const pushAiHistory = ({prompt, scope = '', location = '', answer = ''}) => {
33
31
  if (!prompt && !answer) return;
34
- aiHistory.value = [{id: ++seed, time: nowText(), prompt, scope, answer}, ...aiHistory.value].slice(
35
- 0,
36
- MAX_HISTORY
37
- );
32
+ aiHistory.value = [
33
+ {id: ++seed, time: nowText(), prompt, scope, location, answer},
34
+ ...aiHistory.value,
35
+ ].slice(0, MAX_HISTORY);
38
36
  };
39
37
 
40
38
  export const clearAiHistory = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yh-codemirror",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "wraper codemirror with vue3",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,6 +17,7 @@
17
17
  "@codemirror/lang-css": "^6.2.1",
18
18
  "@codemirror/lang-java": "^6.0.1",
19
19
  "@codemirror/lang-javascript": "^6.2.2",
20
+ "@codemirror/lang-json": "^6.0.1",
20
21
  "@codemirror/lang-sql": "^6.7.0",
21
22
  "@codemirror/lang-html": "^6.4.9",
22
23
  "@codemirror/language": "^6.10.2",
package/util.ts CHANGED
@@ -3,6 +3,7 @@ import { java } from "@codemirror/lang-java";
3
3
  import { css } from "@codemirror/lang-css";
4
4
  import { sql } from "@codemirror/lang-sql";
5
5
  import { html } from "@codemirror/lang-html";
6
+ import { json } from "@codemirror/lang-json";
6
7
 
7
8
  const plugins = {
8
9
  javascript: () => javascript(),
@@ -10,6 +11,7 @@ const plugins = {
10
11
  css: () => css(),
11
12
  java: () => java(),
12
13
  html: () => html(),
14
+ json: () => json(),
13
15
  };
14
16
 
15
17
  /**
@@ -39,5 +41,6 @@ export type AiCodeEditRequest = (
39
41
  ) => Promise<string | void | {text?: string; code?: string; content?: string}>;
40
42
 
41
43
  export const getCodeLanguagePlugin = function (language) {
42
- return plugins[language]();
44
+ // 未知语言(含 undefined)回落 javascript:直接取会拿到 undefined,CM6 会抛 extension 错误
45
+ return (plugins[language] || plugins.javascript)();
43
46
  };