From 7c6b83139c319b0249a3d06d732ee54ce24f9313 Mon Sep 17 00:00:00 2001 From: dengjingren Date: Mon, 22 Jun 2026 22:34:04 +0800 Subject: [PATCH 1/2] feat: add Text2SQL views and i18n - Add text2sql page: dataset, tables, examples panes - Update chat composables and sender toolbar - Add zh-CN/en-US i18n entries for text2sql - Register text2sql route --- .gitignore | 8 + api/chat.ts | 3 + api/index.ts | 224 +++++++++++++++ langs/en-US/ai.json | 3 +- langs/zh-CN/ai.json | 3 +- routes/index.ts | 9 + views/chat/composables/use-chat-settings.ts | 7 + views/chat/composables/use-sender-toolbar.ts | 61 +++- views/chat/index.vue | 13 + views/text2sql/data.ts | 285 +++++++++++++++++++ views/text2sql/dataset-examples-pane.vue | 176 ++++++++++++ views/text2sql/dataset-tables-pane.vue | 253 ++++++++++++++++ views/text2sql/index.vue | 229 +++++++++++++++ 13 files changed, 1270 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 views/text2sql/data.ts create mode 100644 views/text2sql/dataset-examples-pane.vue create mode 100644 views/text2sql/dataset-tables-pane.vue create mode 100644 views/text2sql/index.vue diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec83a3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.omc/ +.DS_Store +node_modules/ +dist/ +*.local +.idea/ +.vscode/ +.cursor/ diff --git a/api/chat.ts b/api/chat.ts index 5bf3205..242429a 100644 --- a/api/chat.ts +++ b/api/chat.ts @@ -28,6 +28,7 @@ export type AIActionResult = null | string; export interface AIChatForwardedPropsParams { enableBuiltinTools?: boolean; + text2SqlDatasetId?: null | number; extraBody?: null | Recordable; extraHeaders?: null | Recordable; frequencyPenalty?: null | number; @@ -205,6 +206,7 @@ export interface AIChatComposerParams { extra_body?: null | string; thinking?: AIChatThinkingType | boolean | null; enable_builtin_tools?: boolean; + text2sql_dataset_id?: null | number; mcp_ids?: null | number[]; web_search?: AIWebSearchType; } @@ -295,6 +297,7 @@ function toForwardedProps( ): AIChatForwardedPropsParams { return { enableBuiltinTools: params.enable_builtin_tools ?? true, + text2SqlDatasetId: params.text2sql_dataset_id ?? null, extraBody: parseExtraBody(params.extra_body), extraHeaders: params.extra_headers ?? undefined, frequencyPenalty: params.frequency_penalty, diff --git a/api/index.ts b/api/index.ts index 8bebde9..565a61e 100644 --- a/api/index.ts +++ b/api/index.ts @@ -256,4 +256,228 @@ export async function deleteAIQuickPhraseApi(pk: number) { return requestClient.delete(`/api/v1/quick-phrases/${pk}`); } +// ---------------- Text2SQL ---------------- + +// 数据集 +export interface AIText2SqlDatasetParams { + description?: null | string; + enabled?: number; + name: string; + sort?: number; +} + +export type AIText2SqlDatasetUpdateParams = Partial; + +export interface AIText2SqlDatasetResult extends AIText2SqlDatasetParams { + created_time: string; + id: number; + updated_time?: null | string; +} + +export interface Text2SqlDatasetEnabled { + description?: null | string; + id: number; + name: string; +} + +export interface Text2SqlTableSelectable { + table_comment?: null | string; + table_name: string; + selected: boolean; +} + +export interface AIText2SqlTableParams { + dataset_id: number; + custom_desc?: null | string; + enabled?: number; + schema_name?: string; + sort?: number; + table_comment?: null | string; + table_name: string; +} + +export type AIText2SqlTableUpdateParams = Partial; + +export interface AIText2SqlTableResult extends AIText2SqlTableParams { + created_time: string; + id: number; + updated_time?: null | string; +} + +export interface AIText2SqlExampleParams { + dataset_id: number; + enabled?: number; + note?: null | string; + question: string; + related_tables?: null | string; + sql: string; + sort?: number; +} + +export type AIText2SqlExampleUpdateParams = Partial; + +export interface AIText2SqlExampleResult extends AIText2SqlExampleParams { + created_time: string; + id: number; + updated_time?: null | string; +} + +export interface Text2SqlQueryParams { + question: string; +} + +export interface Text2SqlQueryResult { + columns: string[]; + duration_ms: number; + history_id?: null | number; + row_count: number; + rows: Recordable[]; + sql: string; + summary: string; +} + +// 数据集 +export async function getEnabledDatasetsApi() { + return requestClient.get( + '/api/v1/text2sql/datasets/enabled', + ); +} + +export async function getDatasetListApi(params?: Recordable) { + return requestClient.get>( + '/api/v1/text2sql/datasets', + { params }, + ); +} + +export async function getAllDatasetsApi() { + return requestClient.get( + '/api/v1/text2sql/datasets/all', + ); +} + +export async function getDatasetApi(pk: number) { + return requestClient.get( + `/api/v1/text2sql/datasets/${pk}`, + ); +} + +export async function createDatasetApi(data: AIText2SqlDatasetParams) { + return requestClient.post('/api/v1/text2sql/datasets', data); +} + +export async function updateDatasetApi( + pk: number, + data: AIText2SqlDatasetUpdateParams, +) { + return requestClient.put( + `/api/v1/text2sql/datasets/${pk}`, + data, + ); +} + +export async function deleteDatasetApi(pk: number) { + return requestClient.delete(`/api/v1/text2sql/datasets/${pk}`); +} + +export async function getSelectableTablesApi( + datasetId: number, + tableSchema?: string, +) { + return requestClient.get( + '/api/v1/text2sql/tables', + { params: { dataset_id: datasetId, table_schema: tableSchema } }, + ); +} + +export async function getTableColumnsApi( + tableName: string, + tableSchema?: string, +) { + return requestClient.get[]>( + `/api/v1/text2sql/tables/${tableName}/columns`, + { params: { table_schema: tableSchema } }, + ); +} + +export async function getSelectedTableListApi(params?: Recordable) { + return requestClient.get>( + '/api/v1/text2sql/selected-tables', + { params }, + ); +} + +export async function getAllSelectedTablesApi() { + return requestClient.get( + '/api/v1/text2sql/selected-tables/all', + ); +} + +export async function getSelectedTableApi(pk: number) { + return requestClient.get( + `/api/v1/text2sql/selected-tables/${pk}`, + ); +} + +export async function createSelectedTableApi(data: AIText2SqlTableParams) { + return requestClient.post( + '/api/v1/text2sql/selected-tables', + data, + ); +} + +export async function updateSelectedTableApi( + pk: number, + data: AIText2SqlTableUpdateParams, +) { + return requestClient.put( + `/api/v1/text2sql/selected-tables/${pk}`, + data, + ); +} + +export async function deleteSelectedTableApi(pk: number) { + return requestClient.delete( + `/api/v1/text2sql/selected-tables/${pk}`, + ); +} + +export async function getExampleListApi(params?: Recordable) { + return requestClient.get>( + '/api/v1/text2sql/examples', + { params }, + ); +} + +export async function getAllExamplesApi() { + return requestClient.get( + '/api/v1/text2sql/examples/all', + ); +} + +export async function createExampleApi(data: AIText2SqlExampleParams) { + return requestClient.post('/api/v1/text2sql/examples', data); +} + +export async function updateExampleApi( + pk: number, + data: AIText2SqlExampleUpdateParams, +) { + return requestClient.put( + `/api/v1/text2sql/examples/${pk}`, + data, + ); +} + +export async function deleteExampleApi(pk: number) { + return requestClient.delete(`/api/v1/text2sql/examples/${pk}`); +} + +export async function queryText2SqlApi(data: Text2SqlQueryParams) { + return requestClient.post( + '/api/v1/text2sql/queries', + data, + ); +} + export * from './chat'; diff --git a/langs/en-US/ai.json b/langs/en-US/ai.json index f607d3b..898cd43 100644 --- a/langs/en-US/ai.json +++ b/langs/en-US/ai.json @@ -4,5 +4,6 @@ "provider": "Provider", "model": "Providers & Models", "mcp": "MCP", - "quick_phrase": "Quick Phrase" + "quick_phrase": "Quick Phrase", + "text2sql": "Dataset" } diff --git a/langs/zh-CN/ai.json b/langs/zh-CN/ai.json index c4063e3..964c314 100644 --- a/langs/zh-CN/ai.json +++ b/langs/zh-CN/ai.json @@ -4,5 +4,6 @@ "provider": "供应商管理", "model": "供应商与模型", "mcp": "MCP 管理", - "quick_phrase": "快捷短语" + "quick_phrase": "快捷短语", + "text2sql": "数据集" } diff --git a/routes/index.ts b/routes/index.ts index f158b85..99cc6f2 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -48,6 +48,15 @@ const routes: RouteRecordRaw[] = [ icon: 'mdi:lightning-bolt-outline', }, }, + { + name: 'PluginAIText2Sql', + path: '/plugins/ai/text2sql', + component: () => import('#/plugins/ai/views/text2sql/index.vue'), + meta: { + title: $t('ai.text2sql'), + icon: 'mdi:database-search-outline', + }, + }, ]; export default routes; diff --git a/views/chat/composables/use-chat-settings.ts b/views/chat/composables/use-chat-settings.ts index e335a6a..42de63f 100644 --- a/views/chat/composables/use-chat-settings.ts +++ b/views/chat/composables/use-chat-settings.ts @@ -20,6 +20,7 @@ type ChatThinkingValue = AIChatComposerParams['thinking']; type ChatWebSearchType = NonNullable; interface ChatSessionScopedConfig { enableBuiltinTools: boolean; + text2sqlDatasetId: number | undefined; generationType: ChatGenerationType; modelId?: string; parallelToolCalls: boolean; @@ -34,6 +35,7 @@ const DEFAULT_CHAT_SESSION_SCOPED_CONFIG: Omit< 'modelId' | 'providerId' > = { enableBuiltinTools: true, + text2sqlDatasetId: undefined, generationType: 'text', parallelToolCalls: true, selectedMcpIds: [], @@ -128,6 +130,7 @@ export function useChatSettings(options: UseChatSettingsOptions) { const parallelToolCalls = ref(true); const thinking = ref(undefined); const enableBuiltinTools = ref(true); + const text2sqlDatasetId = ref(); const selectedMcpIds = ref([]); const webSearch = ref('off'); const stopSequences = ref(''); @@ -188,6 +191,7 @@ export function useChatSettings(options: UseChatSettingsOptions) { function buildCurrentChatSessionScopedConfig(): ChatSessionScopedConfig { return { enableBuiltinTools: enableBuiltinTools.value, + text2sqlDatasetId: text2sqlDatasetId.value, generationType: generationType.value, modelId: selectedModelId.value, parallelToolCalls: parallelToolCalls.value, @@ -221,6 +225,8 @@ export function useChatSettings(options: UseChatSettingsOptions) { selectedMcpIds.value = [...(config.selectedMcpIds ?? [])]; webSearch.value = config.webSearch ?? DEFAULT_CHAT_SESSION_SCOPED_CONFIG.webSearch; + text2sqlDatasetId.value = + config.text2sqlDatasetId ?? DEFAULT_CHAT_SESSION_SCOPED_CONFIG.text2sqlDatasetId; } function rememberConversationSessionConfig(conversationId?: null | string) { @@ -356,6 +362,7 @@ export function useChatSettings(options: UseChatSettingsOptions) { WEB_SEARCH_OPTIONS, conversationSessionConfigs, enableBuiltinTools, + text2sqlDatasetId, extraBody, extraHeaders, frequencyPenalty, diff --git a/views/chat/composables/use-sender-toolbar.ts b/views/chat/composables/use-sender-toolbar.ts index 0bbded5..19ed0f7 100644 --- a/views/chat/composables/use-sender-toolbar.ts +++ b/views/chat/composables/use-sender-toolbar.ts @@ -6,9 +6,10 @@ import type { AIChatComposerParams, AIMcpResult, AIQuickPhraseResult, + Text2SqlDatasetEnabled, } from '#/plugins/ai/api'; -import { h, ref, resolveComponent } from 'vue'; +import { computed, h, ref, resolveComponent } from 'vue'; import { IconifyIcon } from '@vben/icons'; @@ -30,7 +31,9 @@ export interface UseSenderToolbarOptions { confirmClearConversationContext: () => void; confirmClearMessages: () => void; createNewConversation: () => void; + datasets: Ref; enableBuiltinTools: Ref; + text2sqlDatasetId: Ref; generationType: Ref; generationTypeButtonLabel: Ref; GENERATION_TYPE_OPTIONS: Array<{ @@ -75,6 +78,8 @@ export function useSenderToolbar(options: UseSenderToolbarOptions) { confirmClearConversationContext, confirmClearMessages, createNewConversation, + datasets, + text2sqlDatasetId, generationType, generationTypeButtonLabel, GENERATION_TYPE_OPTIONS, @@ -134,6 +139,7 @@ export function useSenderToolbar(options: UseSenderToolbarOptions) { } function renderFooterIconButton(opts: { + active?: boolean; disabled?: boolean; icon: string; onClick?: () => void; @@ -151,7 +157,7 @@ export function useSenderToolbar(options: UseSenderToolbarOptions) { shape: 'circle', size: 'small', title: opts.title, - type: 'text', + type: opts.active ? 'primary' : 'text', }); } @@ -363,6 +369,37 @@ export function useSenderToolbar(options: UseSenderToolbarOptions) { }); } + const text2sqlDatasetLabel = computed(() => { + if (text2sqlDatasetId.value === undefined) { + return datasets.value.length === 0 ? '无数据集' : '关闭'; + } + return ( + datasets.value.find((item) => item.id === text2sqlDatasetId.value)?.name || + 'Text2SQL' + ); + }); + + function renderText2SqlPopoverContent() { + const items: SenderToolbarOption[] = [ + { desc: '不启用 Text2SQL 取数', key: 'off', label: '关闭' }, + ...datasets.value.map((item) => ({ + desc: item.description ?? undefined, + key: String(item.id), + label: item.name, + })), + ]; + return renderSelectableOptionsContent({ + activeKey: + text2sqlDatasetId.value === undefined + ? 'off' + : String(text2sqlDatasetId.value), + items, + onSelect: (key) => { + text2sqlDatasetId.value = key === 'off' ? undefined : Number(key); + }, + }); + } + function getMcpDescription(item: AIMcpResult) { return item.description || item.command || item.url || `MCP #${item.id}`; } @@ -489,6 +526,26 @@ export function useSenderToolbar(options: UseSenderToolbarOptions) { }), }, ), + h( + aPopover, + { placement: 'topLeft', title: 'Text2SQL 数据集', trigger: 'click' }, + { + content: () => renderText2SqlPopoverContent(), + default: () => + renderFooterIconButton({ + active: text2sqlDatasetId.value !== undefined, + disabled: + sending.value || + generationType.value === 'image' || + datasets.value.length === 0, + icon: + text2sqlDatasetId.value !== undefined + ? 'mdi:database-search' + : 'mdi:database-search-outline', + title: `Text2SQL:${text2sqlDatasetLabel.value}`, + }), + }, + ), h( aPopover, { diff --git a/views/chat/index.vue b/views/chat/index.vue index ccc7d9f..c2324a2 100644 --- a/views/chat/index.vue +++ b/views/chat/index.vue @@ -12,6 +12,7 @@ import type { AIModelResult, AIProviderResult, AIQuickPhraseResult, + Text2SqlDatasetEnabled, } from '#/plugins/ai/api'; import type { AIChatComposerParams, @@ -45,6 +46,7 @@ import { getAllAIModelApi, getAllAIProviderApi, getAllAIQuickPhraseApi, + getEnabledDatasetsApi, } from '#/plugins/ai/api'; import { buildChatCompletionRequest, @@ -104,6 +106,7 @@ const regeneratingMessageIndex = ref(); const providers = ref([]); const models = ref([]); const mcps = ref([]); +const datasets = ref([]); const quickPhrases = ref([]); const resourcesLoading = ref(false); @@ -192,6 +195,7 @@ const { THINKING_OPTIONS, WEB_SEARCH_OPTIONS, enableBuiltinTools, + text2sqlDatasetId, extraBody, extraHeaders, frequencyPenalty, @@ -261,6 +265,10 @@ async function fetchMcps() { mcps.value = await getAllAIMcpApi(); } +async function fetchDatasets() { + datasets.value = await getEnabledDatasetsApi(); +} + async function fetchModelsByProvider(providerId?: number) { const fetchId = ++currentModelFetchId; @@ -294,6 +302,7 @@ async function refreshChatResources() { fetchProviders(), fetchModelsByProvider(selectedProviderId.value), fetchMcps(), + fetchDatasets(), fetchQuickPhrases(), ]); } @@ -534,6 +543,7 @@ async function submitChat( conversation_id: activeConversationId.value, extra_body: extraBody.value.trim() || undefined, enable_builtin_tools: enableBuiltinTools.value, + text2sql_dataset_id: text2sqlDatasetId.value ?? undefined, extra_headers: parseJsonField>( extraHeaders.value, '额外请求头', @@ -1080,7 +1090,9 @@ const { fetchQuickPhrases: fetchQuickPhrasesFromToolbar, renderSenderFooter } = confirmClearConversationContext, confirmClearMessages, createNewConversation, + datasets, enableBuiltinTools, + text2sqlDatasetId, generationType, generationTypeButtonLabel, GENERATION_TYPE_OPTIONS, @@ -1156,6 +1168,7 @@ const [RenameConversationModal, renameConversationModalApi] = useVbenModal({ onMounted(async () => { await fetchProviders(); await fetchMcps(); + await fetchDatasets(); await fetchQuickPhrasesFromToolbar(); await initializeSession(); diff --git a/views/text2sql/data.ts b/views/text2sql/data.ts new file mode 100644 index 0000000..9f74a0f --- /dev/null +++ b/views/text2sql/data.ts @@ -0,0 +1,285 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeGridProps } from '#/adapter/vxe-table'; +import type { + AIText2SqlDatasetResult, + AIText2SqlExampleResult, + AIText2SqlTableResult, +} from '#/plugins/ai/api'; + +import { $t } from '@vben/locales'; + +// ---------------- 数据集 ---------------- + +export function useDatasetColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return [ + { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, + { field: 'name', title: '数据集名称', minWidth: 160 }, + { field: 'description', title: '描述', align: 'left' }, + { + field: 'enabled', + title: '启用', + width: 80, + formatter({ cellValue }) { + return cellValue ? '是' : '否'; + }, + }, + { + field: 'operation', + title: $t('common.table.operation'), + align: 'center', + fixed: 'right', + width: 140, + cellRender: { + attrs: { + onClick: onActionClick, + }, + name: 'CellOperation', + options: ['edit', 'delete'], + }, + }, + ]; +} + +export const datasetQuerySchema: VbenFormSchema[] = [ + { component: 'Input', fieldName: 'name', label: '名称' }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, + ], + }, + fieldName: 'enabled', + label: '状态', + }, +]; + +export const datasetSchema: VbenFormSchema[] = [ + { + component: 'Input', + fieldName: 'name', + label: '数据集名称', + rules: 'required', + }, + { + component: 'Textarea', + componentProps: { autoSize: { minRows: 2, maxRows: 4 } }, + fieldName: 'description', + label: '描述', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 1, + fieldName: 'enabled', + label: '状态', + }, + { + component: 'InputNumber', + componentProps: { class: 'w-full', min: 0 }, + defaultValue: 0, + fieldName: 'sort', + label: '排序', + }, +]; + +// ---------------- 已选数据表(数据源) ---------------- + +export function useSelectedTableColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return [ + { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, + { field: 'table_name', title: '表名', width: 180 }, + { field: 'schema_name', title: '库', width: 100 }, + { field: 'table_comment', title: '表注释', align: 'left' }, + { field: 'custom_desc', title: '自定义描述', align: 'left' }, + { + field: 'enabled', + title: '启用', + width: 80, + formatter({ cellValue }) { + return cellValue ? '是' : '否'; + }, + }, + { + field: 'operation', + title: $t('common.table.operation'), + align: 'center', + fixed: 'right', + width: 140, + cellRender: { + attrs: { + onClick: onActionClick, + }, + name: 'CellOperation', + options: ['edit', 'delete'], + }, + }, + ]; +} + +export const selectedTableQuerySchema: VbenFormSchema[] = [ + { component: 'Input', fieldName: 'table_name', label: '表名' }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, + ], + }, + fieldName: 'enabled', + label: '状态', + }, +]; + +export const selectedTableSchema: VbenFormSchema[] = [ + { + component: 'Input', + componentProps: { disabled: true }, + fieldName: 'table_name', + label: '表名', + }, + { + component: 'Textarea', + componentProps: { + autoSize: { minRows: 3, maxRows: 6 }, + placeholder: '补充业务语义,提升生成精度', + }, + fieldName: 'custom_desc', + help: '补充业务语义,提升生成精度', + label: '自定义描述', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 1, + fieldName: 'enabled', + label: '状态', + }, + { + component: 'InputNumber', + componentProps: { class: 'w-full', min: 0 }, + defaultValue: 0, + fieldName: 'sort', + label: '排序', + }, +]; + +// ---------------- Few-shot 样例 ---------------- + +export function useExampleColumns( + onActionClick?: OnActionClickFn, +): VxeGridProps['columns'] { + return [ + { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, + { field: 'question', title: '问题', align: 'left', minWidth: 200 }, + { field: 'sql', title: '示范 SQL', align: 'left', minWidth: 240 }, + { field: 'related_tables', title: '相关表', width: 160 }, + { field: 'note', title: '备注', align: 'left' }, + { + field: 'enabled', + title: '启用', + width: 80, + formatter({ cellValue }) { + return cellValue ? '是' : '否'; + }, + }, + { + field: 'operation', + title: $t('common.table.operation'), + align: 'center', + fixed: 'right', + width: 140, + cellRender: { + attrs: { + onClick: onActionClick, + }, + name: 'CellOperation', + options: ['edit', 'delete'], + }, + }, + ]; +} + +export const exampleQuerySchema: VbenFormSchema[] = [ + { component: 'Input', fieldName: 'question', label: '问题' }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, + ], + }, + fieldName: 'enabled', + label: '状态', + }, +]; + +export const exampleSchema: VbenFormSchema[] = [ + { + component: 'Textarea', + componentProps: { autoSize: { minRows: 2, maxRows: 4 } }, + fieldName: 'question', + label: '自然语言问题', + rules: 'required', + }, + { + component: 'Textarea', + componentProps: { autoSize: { minRows: 3, maxRows: 8 } }, + fieldName: 'sql', + label: '示范 SQL', + rules: 'required', + }, + { + component: 'Input', + componentProps: { placeholder: 'users,orders' }, + fieldName: 'related_tables', + help: '相关表名,逗号分隔,用于召回', + label: '相关表', + }, + { component: 'Textarea', fieldName: 'note', label: '备注' }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 1, + fieldName: 'enabled', + label: '状态', + }, + { + component: 'InputNumber', + componentProps: { class: 'w-full', min: 0 }, + defaultValue: 0, + fieldName: 'sort', + label: '排序', + }, +]; diff --git a/views/text2sql/dataset-examples-pane.vue b/views/text2sql/dataset-examples-pane.vue new file mode 100644 index 0000000..dbec2fb --- /dev/null +++ b/views/text2sql/dataset-examples-pane.vue @@ -0,0 +1,176 @@ + + + diff --git a/views/text2sql/dataset-tables-pane.vue b/views/text2sql/dataset-tables-pane.vue new file mode 100644 index 0000000..87afbb1 --- /dev/null +++ b/views/text2sql/dataset-tables-pane.vue @@ -0,0 +1,253 @@ + + + diff --git a/views/text2sql/index.vue b/views/text2sql/index.vue new file mode 100644 index 0000000..17996a4 --- /dev/null +++ b/views/text2sql/index.vue @@ -0,0 +1,229 @@ + + + + + From 585e2aee59b90ff1927e88493b50ab25643080fb Mon Sep 17 00:00:00 2001 From: dengjingren Date: Thu, 25 Jun 2026 23:08:35 +0800 Subject: [PATCH 2/2] feat(text2sql): full i18n (zh-CN / en-US) Externalize all hardcoded strings in views/text2sql/* through $t() from @vben/locales; add ai.text2sqlPage.* keys to both langs/zh-CN/ai.json and langs/en-US/ai.json with full parity (57 keys resolve in both locales). No logic changes -- string externalization only. Namespace is text2sqlPage (not text2sql) to avoid colliding with the flat 'text2sql' key used by routes/index.ts for the menu title. --- langs/en-US/ai.json | 67 ++++++++++++++- langs/zh-CN/ai.json | 67 ++++++++++++++- views/text2sql/data.ts | 104 +++++++++++------------ views/text2sql/dataset-examples-pane.vue | 25 +++--- views/text2sql/dataset-tables-pane.vue | 35 ++++---- views/text2sql/index.vue | 25 +++--- 6 files changed, 231 insertions(+), 92 deletions(-) diff --git a/langs/en-US/ai.json b/langs/en-US/ai.json index a22c905..9e77bca 100644 --- a/langs/en-US/ai.json +++ b/langs/en-US/ai.json @@ -7,5 +7,70 @@ "model": "Providers & Models", "mcp": "MCP", "quick_phrase": "Quick Phrase", - "text2sql": "Dataset" + "text2sql": "Dataset", + "text2sqlPage": { + "dataset": { + "title": "Datasets", + "add": "Add", + "empty": "No datasets", + "disabled": "Disabled", + "pleaseSelect": "Please select a dataset", + "name": "Dataset name", + "nameQuery": "Name", + "description": "Description", + "enabled": "Enabled", + "disabledOption": "Disabled", + "sort": "Sort", + "status": "Status", + "editTitle": "Edit Dataset", + "createTitle": "Add Dataset", + "deleteConfirm": "Are you sure you want to delete dataset \"{name}\"? Its tables and examples will be retained but no longer aggregated by this dataset.", + "saved": "Saved", + "deleted": "Deleted" + }, + "table": { + "tab": "Tables", + "name": "Table name", + "schema": "Schema", + "comment": "Table comment", + "customDesc": "Custom description", + "customDescPlaceholder": "Add business semantics to improve generation accuracy", + "customDescHelp": "Add business semantics to improve generation accuracy", + "enabled": "Enabled", + "disabledOption": "Disabled", + "status": "Status", + "sort": "Sort", + "pick": "Pick Tables", + "pickPlaceholder": "Select an unselected table", + "pickCustomDescPlaceholder": "Custom description (optional; add business semantics to improve generation accuracy)", + "pickAlert": "You can only pick tables that haven't been selected yet. Table names are resolved from the fba schema.", + "editTitle": "Edit Selected Table", + "pleaseSelectDataset": "Please select a dataset first", + "pleaseSelectTable": "Please select a table to pick", + "picked": "Picked", + "unselected": "Unselected", + "allPicked": "All available tables have been picked", + "updated": "Updated" + }, + "example": { + "tab": "Examples", + "question": "Question", + "sql": "Example SQL", + "relatedTables": "Related tables", + "relatedTablesPlaceholder": "users,orders", + "relatedTablesHelp": "Comma-separated table names used for retrieval", + "note": "Note", + "enabled": "Enabled", + "disabledOption": "Disabled", + "status": "Status", + "sort": "Sort", + "naturalLanguageQuestion": "Natural-language question", + "add": "Add Example", + "editTitle": "Edit Example", + "createTitle": "Add Example", + "pleaseSelectDataset": "Please select a dataset first", + "saved": "Saved", + "deleted": "Deleted" + } + } } diff --git a/langs/zh-CN/ai.json b/langs/zh-CN/ai.json index a0a4fce..5ee22e6 100644 --- a/langs/zh-CN/ai.json +++ b/langs/zh-CN/ai.json @@ -7,5 +7,70 @@ "model": "供应商与模型", "mcp": "MCP 管理", "quick_phrase": "快捷短语", - "text2sql": "数据集" + "text2sql": "数据集", + "text2sqlPage": { + "dataset": { + "title": "数据集", + "add": "新增", + "empty": "暂无数据集", + "disabled": "停用", + "pleaseSelect": "请选择数据集", + "name": "数据集名称", + "nameQuery": "名称", + "description": "描述", + "enabled": "启用", + "disabledOption": "停用", + "sort": "排序", + "status": "状态", + "editTitle": "编辑数据集", + "createTitle": "新增数据集", + "deleteConfirm": "确认删除数据集「{name}」吗?其下表与样例保留但不再被该数据集聚合。", + "saved": "已保存", + "deleted": "已删除" + }, + "table": { + "tab": "表", + "name": "表名", + "schema": "库", + "comment": "表注释", + "customDesc": "自定义描述", + "customDescPlaceholder": "补充业务语义,提升生成精度", + "customDescHelp": "补充业务语义,提升生成精度", + "enabled": "启用", + "disabledOption": "停用", + "status": "状态", + "sort": "排序", + "pick": "挑选表", + "pickPlaceholder": "选择未挑选的表", + "pickCustomDescPlaceholder": "自定义描述(可选,补充业务语义以提升生成精度)", + "pickAlert": "仅可选择尚未挑选的表,表名来自 fba 库反查", + "editTitle": "编辑已选表", + "pleaseSelectDataset": "请先选择数据集", + "pleaseSelectTable": "请选择要挑选的表", + "picked": "已挑选", + "unselected": "已取消挑选", + "allPicked": "已挑选全部可用的表", + "updated": "已更新" + }, + "example": { + "tab": "样例", + "question": "问题", + "sql": "示范 SQL", + "relatedTables": "相关表", + "relatedTablesPlaceholder": "users,orders", + "relatedTablesHelp": "相关表名,逗号分隔,用于召回", + "note": "备注", + "enabled": "启用", + "disabledOption": "停用", + "status": "状态", + "sort": "排序", + "naturalLanguageQuestion": "自然语言问题", + "add": "新增样例", + "editTitle": "编辑样例", + "createTitle": "新增样例", + "pleaseSelectDataset": "请先选择数据集", + "saved": "已保存", + "deleted": "已删除" + } + } } diff --git a/views/text2sql/data.ts b/views/text2sql/data.ts index 5b064d5..856fd65 100644 --- a/views/text2sql/data.ts +++ b/views/text2sql/data.ts @@ -15,14 +15,14 @@ export function useDatasetColumns( ): VxeGridProps['columns'] { return [ { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, - { field: 'name', title: '数据集名称', minWidth: 160 }, - { field: 'description', title: '描述', align: 'left' }, + { field: 'name', title: $t('ai.text2sqlPage.dataset.name'), minWidth: 160 }, + { field: 'description', title: $t('ai.text2sqlPage.dataset.description'), align: 'left' }, { field: 'enabled', - title: '启用', + title: $t('ai.text2sqlPage.dataset.status'), width: 80, formatter({ cellValue }) { - return cellValue ? '是' : '否'; + return cellValue ? $t('common.yes') : $t('common.no'); }, }, { @@ -43,18 +43,18 @@ export function useDatasetColumns( } export const datasetQuerySchema: VbenFormSchema[] = [ - { component: 'Input', fieldName: 'name', label: '名称' }, + { component: 'Input', fieldName: 'name', label: $t('ai.text2sqlPage.dataset.nameQuery') }, { component: 'Select', componentProps: { allowClear: true, options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.dataset.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.dataset.disabledOption'), value: 0 }, ], }, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.dataset.status'), }, ]; @@ -62,35 +62,35 @@ export const datasetSchema: VbenFormSchema[] = [ { component: 'Input', fieldName: 'name', - label: '数据集名称', + label: $t('ai.text2sqlPage.dataset.name'), rules: 'required', }, { component: 'Textarea', componentProps: { autoSize: { minRows: 2, maxRows: 4 } }, fieldName: 'description', - label: '描述', + label: $t('ai.text2sqlPage.dataset.description'), }, { component: 'RadioGroup', componentProps: { buttonStyle: 'solid', options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.dataset.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.dataset.disabledOption'), value: 0 }, ], optionType: 'button', }, defaultValue: 1, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.dataset.status'), }, { component: 'InputNumber', componentProps: { class: 'w-full', min: 0 }, defaultValue: 0, fieldName: 'sort', - label: '排序', + label: $t('ai.text2sqlPage.dataset.sort'), }, ]; @@ -101,16 +101,16 @@ export function useSelectedTableColumns( ): VxeGridProps['columns'] { return [ { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, - { field: 'table_name', title: '表名', width: 180 }, - { field: 'schema_name', title: '库', width: 100 }, - { field: 'table_comment', title: '表注释', align: 'left' }, - { field: 'custom_desc', title: '自定义描述', align: 'left' }, + { field: 'table_name', title: $t('ai.text2sqlPage.table.name'), width: 180 }, + { field: 'schema_name', title: $t('ai.text2sqlPage.table.schema'), width: 100 }, + { field: 'table_comment', title: $t('ai.text2sqlPage.table.comment'), align: 'left' }, + { field: 'custom_desc', title: $t('ai.text2sqlPage.table.customDesc'), align: 'left' }, { field: 'enabled', - title: '启用', + title: $t('ai.text2sqlPage.table.status'), width: 80, formatter({ cellValue }) { - return cellValue ? '是' : '否'; + return cellValue ? $t('common.yes') : $t('common.no'); }, }, { @@ -131,18 +131,18 @@ export function useSelectedTableColumns( } export const selectedTableQuerySchema: VbenFormSchema[] = [ - { component: 'Input', fieldName: 'table_name', label: '表名' }, + { component: 'Input', fieldName: 'table_name', label: $t('ai.text2sqlPage.table.name') }, { component: 'Select', componentProps: { allowClear: true, options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.table.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.table.disabledOption'), value: 0 }, ], }, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.table.status'), }, ]; @@ -151,38 +151,38 @@ export const selectedTableSchema: VbenFormSchema[] = [ component: 'Input', componentProps: { disabled: true }, fieldName: 'table_name', - label: '表名', + label: $t('ai.text2sqlPage.table.name'), }, { component: 'Textarea', componentProps: { autoSize: { minRows: 3, maxRows: 6 }, - placeholder: '补充业务语义,提升生成精度', + placeholder: $t('ai.text2sqlPage.table.customDescPlaceholder'), }, fieldName: 'custom_desc', - help: '补充业务语义,提升生成精度', - label: '自定义描述', + help: $t('ai.text2sqlPage.table.customDescHelp'), + label: $t('ai.text2sqlPage.table.customDesc'), }, { component: 'RadioGroup', componentProps: { buttonStyle: 'solid', options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.table.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.table.disabledOption'), value: 0 }, ], optionType: 'button', }, defaultValue: 1, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.table.status'), }, { component: 'InputNumber', componentProps: { class: 'w-full', min: 0 }, defaultValue: 0, fieldName: 'sort', - label: '排序', + label: $t('ai.text2sqlPage.table.sort'), }, ]; @@ -193,16 +193,16 @@ export function useExampleColumns( ): VxeGridProps['columns'] { return [ { field: 'seq', title: $t('common.table.id'), type: 'seq', width: 50 }, - { field: 'question', title: '问题', align: 'left', minWidth: 200 }, - { field: 'sql', title: '示范 SQL', align: 'left', minWidth: 240 }, - { field: 'related_tables', title: '相关表', width: 160 }, - { field: 'note', title: '备注', align: 'left' }, + { field: 'question', title: $t('ai.text2sqlPage.example.question'), align: 'left', minWidth: 200 }, + { field: 'sql', title: $t('ai.text2sqlPage.example.sql'), align: 'left', minWidth: 240 }, + { field: 'related_tables', title: $t('ai.text2sqlPage.example.relatedTables'), width: 160 }, + { field: 'note', title: $t('ai.text2sqlPage.example.note'), align: 'left' }, { field: 'enabled', - title: '启用', + title: $t('ai.text2sqlPage.example.status'), width: 80, formatter({ cellValue }) { - return cellValue ? '是' : '否'; + return cellValue ? $t('common.yes') : $t('common.no'); }, }, { @@ -223,18 +223,18 @@ export function useExampleColumns( } export const exampleQuerySchema: VbenFormSchema[] = [ - { component: 'Input', fieldName: 'question', label: '问题' }, + { component: 'Input', fieldName: 'question', label: $t('ai.text2sqlPage.example.question') }, { component: 'Select', componentProps: { allowClear: true, options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.example.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.example.disabledOption'), value: 0 }, ], }, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.example.status'), }, ]; @@ -243,43 +243,43 @@ export const exampleSchema: VbenFormSchema[] = [ component: 'Textarea', componentProps: { autoSize: { minRows: 2, maxRows: 4 } }, fieldName: 'question', - label: '自然语言问题', + label: $t('ai.text2sqlPage.example.naturalLanguageQuestion'), rules: 'required', }, { component: 'Textarea', componentProps: { autoSize: { minRows: 3, maxRows: 8 } }, fieldName: 'sql', - label: '示范 SQL', + label: $t('ai.text2sqlPage.example.sql'), rules: 'required', }, { component: 'Input', - componentProps: { placeholder: 'users,orders' }, + componentProps: { placeholder: $t('ai.text2sqlPage.example.relatedTablesPlaceholder') }, fieldName: 'related_tables', - help: '相关表名,逗号分隔,用于召回', - label: '相关表', + help: $t('ai.text2sqlPage.example.relatedTablesHelp'), + label: $t('ai.text2sqlPage.example.relatedTables'), }, - { component: 'Textarea', fieldName: 'note', label: '备注' }, + { component: 'Textarea', fieldName: 'note', label: $t('ai.text2sqlPage.example.note') }, { component: 'RadioGroup', componentProps: { buttonStyle: 'solid', options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.example.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.example.disabledOption'), value: 0 }, ], optionType: 'button', }, defaultValue: 1, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.example.status'), }, { component: 'InputNumber', componentProps: { class: 'w-full', min: 0 }, defaultValue: 0, fieldName: 'sort', - label: '排序', + label: $t('ai.text2sqlPage.example.sort'), }, ]; diff --git a/views/text2sql/dataset-examples-pane.vue b/views/text2sql/dataset-examples-pane.vue index 171fea4..08ebf75 100644 --- a/views/text2sql/dataset-examples-pane.vue +++ b/views/text2sql/dataset-examples-pane.vue @@ -10,6 +10,7 @@ import { computed, ref, watch } from 'vue'; import { VbenButton, useVbenModal } from '@vben/common-ui'; import { MaterialSymbolsAdd } from '@vben/icons'; +import { $t } from '@vben/locales'; import { message } from 'antdv-next'; @@ -29,20 +30,20 @@ const props = defineProps<{ datasetId?: number }>(); const formOptions: VbenFormProps = { collapsed: true, showCollapseButton: true, - submitButtonOptions: { content: '查询' }, + submitButtonOptions: { content: $t('common.query') }, schema: [ - { component: 'Input', fieldName: 'question', label: '问题' }, + { component: 'Input', fieldName: 'question', label: $t('ai.text2sqlPage.example.question') }, { component: 'Select', componentProps: { allowClear: true, options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.example.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.example.disabledOption'), value: 0 }, ], }, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.example.status'), }, ], }; @@ -92,7 +93,7 @@ function onActionClick({ code, row }: OnActionClickParams { - message.success({ content: '已删除', key: 'action_process_msg' }); + message.success({ content: $t('ai.text2sqlPage.example.deleted'), key: 'action_process_msg' }); onRefresh(); }); break; @@ -111,7 +112,11 @@ const [Form, formApi] = useVbenForm({ }); const formData = ref(); -const modalTitle = computed(() => (formData.value?.id ? '编辑样例' : '新增样例')); +const modalTitle = computed(() => + formData.value?.id + ? $t('ai.text2sqlPage.example.editTitle') + : $t('ai.text2sqlPage.example.createTitle'), +); const [Modal, modalApi] = useVbenModal({ destroyOnClose: true, @@ -121,7 +126,7 @@ const [Modal, modalApi] = useVbenModal({ return; } if (!props.datasetId) { - message.warning('请先选择数据集'); + message.warning($t('ai.text2sqlPage.example.pleaseSelectDataset')); return; } modalApi.lock(); @@ -134,7 +139,7 @@ const [Modal, modalApi] = useVbenModal({ } else { await createExampleApi(payload); } - message.success('已保存'); + message.success($t('ai.text2sqlPage.example.saved')); await modalApi.close(); onRefresh(); } catch (error) { @@ -166,7 +171,7 @@ const [Modal, modalApi] = useVbenModal({ @click="() => modalApi.setData(null).open()" > - 新增样例 + {{ $t('ai.text2sqlPage.example.add') }} diff --git a/views/text2sql/dataset-tables-pane.vue b/views/text2sql/dataset-tables-pane.vue index 6faed1a..045a452 100644 --- a/views/text2sql/dataset-tables-pane.vue +++ b/views/text2sql/dataset-tables-pane.vue @@ -10,6 +10,7 @@ import { ref, watch } from 'vue'; import { VbenButton, useVbenModal } from '@vben/common-ui'; import { MaterialSymbolsAdd } from '@vben/icons'; +import { $t } from '@vben/locales'; import { message } from 'antdv-next'; @@ -30,20 +31,20 @@ const props = defineProps<{ datasetId?: number }>(); const formOptions: VbenFormProps = { collapsed: true, showCollapseButton: true, - submitButtonOptions: { content: '查询' }, + submitButtonOptions: { content: $t('common.query') }, schema: [ - { component: 'Input', fieldName: 'table_name', label: '表名' }, + { component: 'Input', fieldName: 'table_name', label: $t('ai.text2sqlPage.table.name') }, { component: 'Select', componentProps: { allowClear: true, options: [ - { label: '启用', value: 1 }, - { label: '停用', value: 0 }, + { label: $t('ai.text2sqlPage.table.enabled'), value: 1 }, + { label: $t('ai.text2sqlPage.table.disabledOption'), value: 0 }, ], }, fieldName: 'enabled', - label: '状态', + label: $t('ai.text2sqlPage.table.status'), }, ], }; @@ -93,7 +94,7 @@ function onActionClick({ code, row }: OnActionClickParams switch (code) { case 'delete': { deleteSelectedTableApi(row.id).then(() => { - message.success({ content: '已取消挑选', key: 'action_process_msg' }); + message.success({ content: $t('ai.text2sqlPage.table.unselected'), key: 'action_process_msg' }); onRefresh(); }); break; @@ -132,7 +133,7 @@ const [Modal, modalApi] = useVbenModal({ enabled: data.enabled, sort: data.sort, }); - message.success('已更新'); + message.success($t('ai.text2sqlPage.table.updated')); await modalApi.close(); onRefresh(); } catch (error) { @@ -165,11 +166,11 @@ const [PickModal, pickModalApi] = useVbenModal({ destroyOnClose: true, async onConfirm() { if (!props.datasetId) { - message.warning('请先选择数据集'); + message.warning($t('ai.text2sqlPage.table.pleaseSelectDataset')); return; } if (!pickTable.value) { - message.warning('请选择要挑选的表'); + message.warning($t('ai.text2sqlPage.table.pleaseSelectTable')); return; } pickModalApi.lock(); @@ -183,7 +184,7 @@ const [PickModal, pickModalApi] = useVbenModal({ custom_desc: pickCustomDesc.value.trim() || undefined, }; await createSelectedTableApi(payload); - message.success('已挑选'); + message.success($t('ai.text2sqlPage.table.picked')); await pickModalApi.close(); onRefresh(); } catch (error) { @@ -207,7 +208,7 @@ const [PickModal, pickModalApi] = useVbenModal({ value: table.table_name, })); if (pickOptions.value.length === 0) { - message.info('已挑选全部可用的表'); + message.info($t('ai.text2sqlPage.table.allPicked')); } } catch { pickOptions.value = []; @@ -225,28 +226,28 @@ const [PickModal, pickModalApi] = useVbenModal({ @click="() => pickModalApi.setData(null).open()" > - 挑选表 + {{ $t('ai.text2sqlPage.table.pick') }} - +
- +
- +
diff --git a/views/text2sql/index.vue b/views/text2sql/index.vue index 9a209a2..63f516d 100644 --- a/views/text2sql/index.vue +++ b/views/text2sql/index.vue @@ -8,6 +8,7 @@ import { computed, onMounted, ref } from 'vue'; import { Page, confirm, useVbenModal } from '@vben/common-ui'; import { IconifyIcon } from '@vben/icons'; +import { $t } from '@vben/locales'; import { message } from 'antdv-next'; @@ -50,7 +51,9 @@ const [DatasetForm, datasetFormApi] = useVbenForm({ const datasetFormData = ref(); const datasetModalTitle = computed(() => - datasetFormData.value?.id ? '编辑数据集' : '新增数据集', + datasetFormData.value?.id + ? $t('ai.text2sqlPage.dataset.editTitle') + : $t('ai.text2sqlPage.dataset.createTitle'), ); const [DatasetModal, datasetModalApi] = useVbenModal({ @@ -69,7 +72,7 @@ const [DatasetModal, datasetModalApi] = useVbenModal({ } else { await createDatasetApi(data); } - message.success('已保存'); + message.success($t('ai.text2sqlPage.dataset.saved')); await datasetModalApi.close(); await loadDatasets(); } catch (error) { @@ -102,11 +105,11 @@ function openEditDataset(row: AIText2SqlDatasetResult) { function confirmDeleteDataset(row: AIText2SqlDatasetResult) { confirm({ - content: `确认删除数据集「${row.name}」吗?其下表与样例保留但不再被该数据集聚合。`, + content: $t('ai.text2sqlPage.dataset.deleteConfirm', { name: row.name }), icon: 'warning', }).then(async () => { await deleteDatasetApi(row.id); - message.success('已删除'); + message.success($t('ai.text2sqlPage.dataset.deleted')); await loadDatasets(); }); } @@ -123,13 +126,13 @@ onMounted(loadDatasets);
- 数据集 + {{ $t('ai.text2sqlPage.dataset.title') }} - 新增 + {{ $t('ai.text2sqlPage.dataset.add') }}
- +
{{ d.name }} - 停用 + {{ $t('ai.text2sqlPage.dataset.disabled') }}
- {{ selectedDataset ? selectedDataset.name : '请选择数据集' }} + {{ selectedDataset ? selectedDataset.name : $t('ai.text2sqlPage.dataset.pleaseSelect') }}
- + - +