diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
index c725e11..bb9f173 100644
--- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx
+++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
@@ -4367,7 +4367,14 @@ describe('QueryEditor external SQL save', () => {
const modalSource = readFileSync(new URL('./SnippetSettingsModal.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
- expect(modalSource).toContain('片段语法说明(可选)');
+ expect(modalSource).toContain('片段语法说明(可编辑)');
+ expect(modalSource).toContain('data-sql-snippet-syntax-help-editor="true"');
+ expect(modalSource).toContain("defaultActiveKey={['snippet-help']}");
+ expect(modalSource).toContain('footer={null}');
+ expect(modalSource).toContain('data-sql-snippet-action-row="true"');
+ expect(modalSource).toContain('body: { paddingTop: 8, paddingBottom: 24 }');
+ expect(modalSource).toContain("size=\"large\"");
+ expect(modalSource).toContain('minWidth: 96');
expect(modalSource).toContain('syntaxHelp');
expect(modalSource).toContain('占位符语法参考');
expect(source).toContain('s.syntaxHelp || s.description || s.body');
diff --git a/frontend/src/components/SnippetSettingsModal.tsx b/frontend/src/components/SnippetSettingsModal.tsx
index 1b8faf2..41b3ba3 100644
--- a/frontend/src/components/SnippetSettingsModal.tsx
+++ b/frontend/src/components/SnippetSettingsModal.tsx
@@ -159,29 +159,51 @@ export default function SnippetSettingsModal({
[resetBuiltinSqlSnippet, selectedId],
);
- const syntaxHelpItems = [
- {
- key: 'syntax',
- label: '占位符语法参考',
- children: (
-
-
{'${1:占位符} 第一个 Tab 位,占位符为提示文字'}
-
{'${2:默认值} 第二个 Tab 位,默认值可直接确认'}
-
{'$0 最终光标位置'}
-
{'${1:表名} 同一数字在多处出现时会同步编辑'}
-
{'内置变量(展开时自动替换为实际值):'}
-
{'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} 当前日期'}
-
{'${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} 当前时间'}
-
{'${CURRENT_SECONDS_UNIX} Unix 时间戳'}
-
{'${UUID} 随机 UUID'}
-
{'${RANDOM} 6 位随机数'}
-
- {'示例:SELECT ${1:列名} FROM ${2:表名} WHERE date >= \'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}\';$0'}
+ const syntaxHelpItems = useMemo(
+ () => [
+ {
+ key: 'snippet-help',
+ label: '片段语法说明(可编辑)',
+ children: (
+
setDraft((d) => ({ ...d, syntaxHelp: e.target.value }))}
+ placeholder="展示在补全详情中的用法说明,例如占位符含义、参数约定或注意事项"
+ maxLength={1000}
+ autoSize={{ minRows: 4, maxRows: 8 }}
+ style={{
+ fontSize: 12,
+ resize: 'none',
+ fontFamily: 'var(--gn-font-mono)',
+ }}
+ />
+ ),
+ },
+ {
+ key: 'syntax',
+ label: '占位符语法参考',
+ children: (
+
+
{'${1:占位符} 第一个 Tab 位,占位符为提示文字'}
+
{'${2:默认值} 第二个 Tab 位,默认值可直接确认'}
+
{'$0 最终光标位置'}
+
{'${1:表名} 同一数字在多处出现时会同步编辑'}
+
{'内置变量(展开时自动替换为实际值):'}
+
{'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} 当前日期'}
+
{'${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} 当前时间'}
+
{'${CURRENT_SECONDS_UNIX} Unix 时间戳'}
+
{'${UUID} 随机 UUID'}
+
{'${RANDOM} 6 位随机数'}
+
+ {'示例:SELECT ${1:列名} FROM ${2:表名} WHERE date >= \'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}\';$0'}
+
-
- ),
- },
- ];
+ ),
+ },
+ ],
+ [draft.syntaxHelp, mutedColor, textColor],
+ );
const showEditor = isCreating || selectedSnippet;
@@ -217,14 +239,9 @@ export default function SnippetSettingsModal({
styles={{
content: shellStyle,
header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 },
- body: { paddingTop: 8 },
- footer: { background: 'transparent', borderTop: 'none', paddingTop: 40 },
+ body: { paddingTop: 8, paddingBottom: 24 },
}}
- footer={[
-
,
- ]}
+ footer={null}
>
{/* Left: snippet list */}
@@ -353,18 +370,6 @@ export default function SnippetSettingsModal({
/>
-
-
片段语法说明(可选)
-
setDraft((d) => ({ ...d, syntaxHelp: e.target.value }))}
- placeholder="展示在补全详情中的用法说明,例如占位符含义、参数约定或注意事项"
- maxLength={1000}
- autoSize={{ minRows: 2, maxRows: 4 }}
- style={{ fontSize: 12, resize: 'none' }}
- />
-
-
-
- {draft.isBuiltin && draft.createdAt && (
-
handleReset(draft.id)}
- >
- } size="small">
- 重置为默认
-
-
- )}
- {!draft.isBuiltin && !isCreating && (
-
handleDelete(draft.id)}
- >
- } size="small">
- 删除
-
-
- )}
-
} size="small" onClick={handleSave}>
- 保存
-
-
) : (
+ {showEditor && draft.isBuiltin && draft.createdAt && (
+
handleReset(draft.id)}
+ >
+ } size="large" style={{ minWidth: 118 }}>
+ 重置为默认
+
+
+ )}
+ {showEditor && !draft.isBuiltin && !isCreating && (
+
handleDelete(draft.id)}
+ >
+ } size="large" style={{ minWidth: 96 }}>
+ 删除
+
+
+ )}
+ {showEditor && (
+
} size="large" style={{ minWidth: 96 }} onClick={handleSave}>
+ 保存
+
+ )}
+
+
);
}
diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts
index 5fc2344..4b1a3be 100644
--- a/frontend/src/store.test.ts
+++ b/frontend/src/store.test.ts
@@ -1195,4 +1195,41 @@ describe('store appearance persistence', () => {
windows: { combo: 'Enter', enabled: true },
});
});
+
+ it('updates an existing custom SQL snippet by id and persists editable syntax help', async () => {
+ const { useStore } = await importStore();
+ const original = {
+ id: 'custom-merge',
+ prefix: 'mrg',
+ name: 'MERGE INTO',
+ description: 'Oracle merge 模板',
+ syntaxHelp: '旧说明',
+ body: 'MERGE INTO t USING s ON (t.id = s.id)$0',
+ isBuiltin: false,
+ createdAt: 1710000000000,
+ };
+
+ useStore.getState().saveSqlSnippet(original);
+ useStore.getState().saveSqlSnippet({
+ ...original,
+ name: 'MERGE INTO 更新',
+ syntaxHelp: '新说明:目标表、数据源、关联字段均可修改',
+ body: 'MERGE INTO ${1:目标表} t USING ${2:源表} s ON (${3:关联条件})$0',
+ });
+
+ const snippets = useStore.getState().sqlSnippets.filter((s) => s.id === original.id);
+ expect(snippets).toHaveLength(1);
+ expect(snippets[0]).toMatchObject({
+ prefix: 'mrg',
+ name: 'MERGE INTO 更新',
+ syntaxHelp: '新说明:目标表、数据源、关联字段均可修改',
+ body: 'MERGE INTO ${1:目标表} t USING ${2:源表} s ON (${3:关联条件})$0',
+ isBuiltin: false,
+ });
+
+ const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
+ const persistedSnippets = persisted.state.sqlSnippets.filter((s: { id: string }) => s.id === original.id);
+ expect(persistedSnippets).toHaveLength(1);
+ expect(persistedSnippets[0].syntaxHelp).toBe('新说明:目标表、数据源、关联字段均可修改');
+ });
});