diff --git a/backend/apps/data_training/api/data_training.py b/backend/apps/data_training/api/data_training.py index 2b07a8f04..ed4d7387a 100644 --- a/backend/apps/data_training/api/data_training.py +++ b/backend/apps/data_training/api/data_training.py @@ -74,8 +74,8 @@ def inner(): data_list.append(_data) fields = [] - fields.append(AxisObj(name=trans('i18n_data_training.data_training'), value='question')) - fields.append(AxisObj(name=trans('i18n_data_training.problem_description'), value='description')) + fields.append(AxisObj(name=trans('i18n_data_training.problem_description'), value='question')) + fields.append(AxisObj(name=trans('i18n_data_training.sample_sql'), value='description')) fields.append(AxisObj(name=trans('i18n_data_training.effective_data_sources'), value='datasource_name')) if current_user.oid == 1: fields.append( @@ -98,6 +98,43 @@ def inner(): return StreamingResponse(result, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") +@router.get("/template") +async def excel_template(trans: Trans, current_user: CurrentUser): + def inner(): + data_list = [] + _data1 = { + "question": '查询TEST表内所有ID', + "description": 'SELECT id FROM TEST', + "datasource_name": '生效数据源1', + "advanced_application_name": '生效高级应用名称', + } + data_list.append(_data1) + + fields = [] + fields.append(AxisObj(name=trans('i18n_data_training.problem_description_template'), value='question')) + fields.append(AxisObj(name=trans('i18n_data_training.sample_sql_template'), value='description')) + fields.append(AxisObj(name=trans('i18n_data_training.effective_data_sources_template'), value='datasource_name')) + if current_user.oid == 1: + fields.append( + AxisObj(name=trans('i18n_data_training.advanced_application_template'), value='advanced_application_name')) + + md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list) + + df = pd.DataFrame(md_data, columns=_fields_list) + + buffer = io.BytesIO() + + with pd.ExcelWriter(buffer, engine='xlsxwriter', + engine_kwargs={'options': {'strings_to_numbers': False}}) as writer: + df.to_excel(writer, sheet_name='Sheet1', index=False) + + buffer.seek(0) + return io.BytesIO(buffer.getvalue()) + + result = await asyncio.to_thread(inner) + return StreamingResponse(result, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + path = settings.EXCEL_PATH from sqlalchemy.orm import sessionmaker, scoped_session diff --git a/backend/apps/terminology/api/terminology.py b/backend/apps/terminology/api/terminology.py index fe2af92c5..16cb6cffb 100644 --- a/backend/apps/terminology/api/terminology.py +++ b/backend/apps/terminology/api/terminology.py @@ -98,6 +98,51 @@ def inner(): return StreamingResponse(result, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") +@router.get("/template") +async def excel_template(trans: Trans): + def inner(): + data_list = [] + _data1 = { + "word": trans('i18n_terminology.term_name_template_example_1'), + "other_words": trans('i18n_terminology.synonyms_template_example_1'), + "description": trans('i18n_terminology.term_description_template_example_1'), + "all_data_sources": 'N', + "datasource": trans('i18n_terminology.effective_data_sources_template_example_1'), + } + data_list.append(_data1) + _data2 = { + "word": trans('i18n_terminology.term_name_template_example_2'), + "other_words": trans('i18n_terminology.synonyms_template_example_2'), + "description": trans('i18n_terminology.term_description_template_example_2'), + "all_data_sources": 'Y', + "datasource": '', + } + data_list.append(_data2) + + fields = [] + fields.append(AxisObj(name=trans('i18n_terminology.term_name_template'), value='word')) + fields.append(AxisObj(name=trans('i18n_terminology.synonyms_template'), value='other_words')) + fields.append(AxisObj(name=trans('i18n_terminology.term_description_template'), value='description')) + fields.append(AxisObj(name=trans('i18n_terminology.effective_data_sources_template'), value='datasource')) + fields.append(AxisObj(name=trans('i18n_terminology.all_data_sources_template'), value='all_data_sources')) + + md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list) + + df = pd.DataFrame(md_data, columns=_fields_list) + + buffer = io.BytesIO() + + with pd.ExcelWriter(buffer, engine='xlsxwriter', + engine_kwargs={'options': {'strings_to_numbers': False}}) as writer: + df.to_excel(writer, sheet_name='Sheet1', index=False) + + buffer.seek(0) + return io.BytesIO(buffer.getvalue()) + + result = await asyncio.to_thread(inner) + return StreamingResponse(result, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + path = settings.EXCEL_PATH from sqlalchemy.orm import sessionmaker, scoped_session diff --git a/backend/locales/en.json b/backend/locales/en.json index bf64bb073..c43d4fb88 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -48,6 +48,18 @@ "effective_data_sources": "Effective Data Sources", "all_data_sources": "All Data Sources", "synonyms": "Synonyms", + "term_name_template": "Terminology Name (Required)", + "term_description_template": "Terminology Description (Required)", + "effective_data_sources_template": "Effective Data Sources (Multiple supported, separated by \",\")", + "all_data_sources_template": "All Data Sources (Y: Apply to all data sources, N: Apply to specified data sources)", + "synonyms_template": "Synonyms (Multiple supported, separated by \",\")", + "term_name_template_example_1": "Term1", + "term_description_template_example_1": "Term1 Description", + "effective_data_sources_template_example_1": "Datasource1, Datasource2", + "synonyms_template_example_1": "Synonym1, Synonym2", + "term_name_template_example_2": "Term2", + "term_description_template_example_2": "Term2 Description", + "synonyms_template_example_2": "Synonym3", "word_cannot_be_empty": "Term cannot be empty", "description_cannot_be_empty": "Term description cannot be empty", "datasource_not_found": "Datasource not found" @@ -62,6 +74,13 @@ "sample_sql": "Sample SQL", "effective_data_sources": "Effective Data Sources", "advanced_application": "Advanced Application", + "problem_description_template": "Problem Description (Required)", + "sample_sql_template": "Sample SQL (Required)", + "effective_data_sources_template": "Effective Data Sources", + "advanced_application_template": "Advanced Application", + "problem_description_template_example": "Query all IDs in the TEST table", + "effective_data_sources_template_example": "Effective Datasource 1", + "advanced_application_template_example": "Effective Advanced Application Name", "error_info": "Error Information", "question_cannot_be_empty": "Question cannot be empty", "description_cannot_be_empty": "Sample SQL cannot be empty", @@ -75,6 +94,15 @@ "prompt_word_content": "Prompt word content", "effective_data_sources": "Effective Data Sources", "all_data_sources": "All Data Sources", + "prompt_word_name_template": "Prompt Name (Required)", + "prompt_word_content_template": "Prompt Content (Required)", + "effective_data_sources_template": "Effective Data Sources (Multiple supported, separated by \",\")", + "all_data_sources_template": "All Data Sources (Y: Apply to all data sources, N: Apply to specified data sources)", + "prompt_word_name_template_example1": "Prompt1", + "prompt_word_content_template_example1": "Describe your prompt in detail", + "effective_data_sources_template_example1": "Datasource1, Datasource2", + "prompt_word_name_template_example2": "Prompt2", + "prompt_word_content_template_example2": "Describe your prompt in detail", "name_cannot_be_empty": "Name cannot be empty", "prompt_cannot_be_empty": "Prompt content cannot be empty", "type_cannot_be_empty": "Type cannot be empty", diff --git a/backend/locales/ko-KR.json b/backend/locales/ko-KR.json index ca72a448e..87d883f92 100644 --- a/backend/locales/ko-KR.json +++ b/backend/locales/ko-KR.json @@ -48,6 +48,18 @@ "effective_data_sources": "유효 데이터 소스", "all_data_sources": "모든 데이터 소스", "synonyms": "동의어", + "term_name_template": "용어 이름 (필수)", + "term_description_template": "용어 설명 (필수)", + "effective_data_sources_template": "유효 데이터 소스 (여러 개 지원, \",\"로 구분)", + "all_data_sources_template": "모든 데이터 소스 (Y: 모든 데이터 소스에 적용, N: 지정된 데이터 소스에 적용)", + "synonyms_template": "동의어 (여러 개 지원, \",\"로 구분)", + "term_name_template_example_1": "용어1", + "term_description_template_example_1": "용어1 설명", + "effective_data_sources_template_example_1": "데이터소스1, 데이터소스2", + "synonyms_template_example_1": "동의어1, 동의어2", + "term_name_template_example_2": "용어2", + "term_description_template_example_2": "용어2 설명", + "synonyms_template_example_2": "동의어3", "word_cannot_be_empty": "용어는 비울 수 없습니다", "description_cannot_be_empty": "용어 설명은 비울 수 없습니다", "datasource_not_found": "데이터 소스를 찾을 수 없음" @@ -62,6 +74,13 @@ "sample_sql": "예시 SQL", "effective_data_sources": "유효 데이터 소스", "advanced_application": "고급 애플리케이션", + "problem_description_template": "문제 설명 (필수)", + "sample_sql_template": "예시 SQL (필수)", + "effective_data_sources_template": "유효 데이터 소스", + "advanced_application_template": "고급 애플리케이션", + "problem_description_template_example": "TEST 테이블 내 모든 ID 조회", + "effective_data_sources_template_example": "유효 데이터소스1", + "advanced_application_template_example": "유효 고급 애플리케이션 이름", "error_info": "오류 정보", "question_cannot_be_empty": "질문은 비울 수 없습니다", "description_cannot_be_empty": "예시 SQL은 비울 수 없습니다", @@ -75,6 +94,15 @@ "prompt_word_content": "프롬프트 내용", "effective_data_sources": "유효 데이터 소스", "all_data_sources": "모든 데이터 소스", + "prompt_word_name_template": "프롬프트 이름 (필수)", + "prompt_word_content_template": "프롬프트 내용 (필수)", + "effective_data_sources_template": "유효 데이터 소스 (여러 개 지원, \",\"로 구분)", + "all_data_sources_template": "모든 데이터 소스 (Y: 모든 데이터 소스에 적용, N: 지정된 데이터 소스에 적용)", + "prompt_word_name_template_example1": "프롬프트1", + "prompt_word_content_template_example1": "프롬프트를 상세히 설명해 주세요", + "effective_data_sources_template_example1": "데이터소스1, 데이터소스2", + "prompt_word_name_template_example2": "프롬프트2", + "prompt_word_content_template_example2": "프롬프트를 상세히 설명해 주세요", "name_cannot_be_empty": "이름은 비울 수 없습니다", "prompt_cannot_be_empty": "프롬프트 내용은 비울 수 없습니다", "type_cannot_be_empty": "유형은 비울 수 없습니다", diff --git a/backend/locales/zh-CN.json b/backend/locales/zh-CN.json index 242253a82..3393f402c 100644 --- a/backend/locales/zh-CN.json +++ b/backend/locales/zh-CN.json @@ -48,6 +48,18 @@ "effective_data_sources": "生效数据源", "all_data_sources": "所有数据源", "synonyms": "同义词", + "term_name_template": "术语名称(必填)", + "term_description_template": "术语描述(必填)", + "effective_data_sources_template": "生效数据源(支持多个,用\",\"分割)", + "all_data_sources_template": "所有数据源(Y:应用到全部数据源,N:应用到指定数据源)", + "synonyms_template": "同义词(支持多个,用\",\"分割)", + "term_name_template_example_1": "术语1", + "term_description_template_example_1": "术语1描述", + "effective_data_sources_template_example_1": "生效数据源1, 生效数据源2", + "synonyms_template_example_1": "同义词1, 同义词2", + "term_name_template_example_2": "术语2", + "term_description_template_example_2": "术语2描述", + "synonyms_template_example_2": "同义词3", "word_cannot_be_empty": "术语不能为空", "description_cannot_be_empty": "术语描述不能为空", "datasource_not_found": "找不到数据源" @@ -62,6 +74,13 @@ "sample_sql": "示例 SQL", "effective_data_sources": "生效数据源", "advanced_application": "高级应用", + "problem_description_template": "问题描述(必填)", + "sample_sql_template": "示例 SQL(必填)", + "effective_data_sources_template": "生效数据源", + "advanced_application_template": "高级应用", + "problem_description_template_example": "查询TEST表内所有ID", + "effective_data_sources_template_example": "生效数据源1", + "advanced_application_template_example": "生效高级应用名称", "error_info": "错误信息", "question_cannot_be_empty": "问题不能为空", "description_cannot_be_empty": "示例 SQL 不能为空", @@ -75,6 +94,15 @@ "prompt_word_content": "提示词内容", "effective_data_sources": "生效数据源", "all_data_sources": "所有数据源", + "prompt_word_name_template": "提示词名称(必填)", + "prompt_word_content_template": "提示词内容(必填)", + "effective_data_sources_template": "生效数据源(支持多个,用\",\"分割)", + "all_data_sources_template": "所有数据源(Y:应用到全部数据源,N:应用到指定数据源)", + "prompt_word_name_template_example1": "提示词1", + "prompt_word_content_template_example1": "详细描述你的提示词", + "effective_data_sources_template_example1": "生效数据源1, 生效数据源2", + "prompt_word_name_template_example2": "提示词2", + "prompt_word_content_template_example2": "详细描述你的提示词", "name_cannot_be_empty": "名称不能为空", "prompt_cannot_be_empty": "提示词内容不能为空", "type_cannot_be_empty": "类型不能为空", diff --git a/backend/pyproject.toml b/backend/pyproject.toml index cc2428727..fde8ed756 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ "pyyaml (>=6.0.2,<7.0.0)", "fastapi-mcp (>=0.3.4,<0.4.0)", "tabulate>=0.9.0", - "sqlbot-xpack>=0.0.3.47,<1.0.0", + "sqlbot-xpack>=0.0.3.53,<1.0.0", "fastapi-cache2>=0.2.2", "sqlparse>=0.5.3", "redis>=6.2.0", diff --git a/frontend/src/api/setting.ts b/frontend/src/api/setting.ts index 456e5b6dc..354aac582 100644 --- a/frontend/src/api/setting.ts +++ b/frontend/src/api/setting.ts @@ -17,4 +17,10 @@ export const settingsApi = { requestOptions: { customError: true }, } ), + + downloadTemplate: (url: any) => + request.get(url, { + responseType: 'blob', + requestOptions: { customError: true }, + }), } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 5fd06752a..278f42060 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -117,6 +117,12 @@ "english": "English", "re_upload": "Re-upload", "not_exceed_50mb": "Supports XLS, XLSX, CSV formats, file size does not exceed 50MB", + "excel_file_type_limit": "Only XLS and XLSX formats are supported", + "click_to_select_file": "Click to select file", + "upload_hint_first": "Please ", + "upload_hint_download_template": "download the template", + "upload_hint_end": " first, then upload after filling it out as required", + "continue_to_upload": "Continue to import", "reset_password": "Reset password", "password_reset_successful": "Password reset successful", "or": "Or", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 8021e4693..562017179 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -117,6 +117,12 @@ "english": "English", "re_upload": "다시 업로드", "not_exceed_50mb": "XLS, XLSX, CSV 형식을 지원하며, 파일 크기는 50MB를 초과할 수 없습니다", + "excel_file_type_limit": "XLS 및 XLSX 형식만 지원됩니다", + "click_to_select_file": "파일 선택을 클릭하세요", + "upload_hint_first": "먼저 ", + "upload_hint_download_template": "템플릿을 다운로드", + "upload_hint_end": ", 한 후 요구사항에 따라 작성하여 업로드하세요", + "continue_to_upload": "계속 가져오기", "reset_password": "비밀번호 재설정", "password_reset_successful": "비밀번호 재설정 성공", "or": "또는", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 037c9e345..b9fbec8c1 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -117,6 +117,12 @@ "english": "English", "re_upload": "重新上传", "not_exceed_50mb": "支持 XLS、XLSX、CSV 格式,文件大小不超过 50MB", + "excel_file_type_limit": "仅支持 XLS、XLSX 格式", + "click_to_select_file": "点击选择文件", + "upload_hint_first": "先", + "upload_hint_download_template": "下载模板", + "upload_hint_end": ",按要求填写后上传", + "continue_to_upload": "继续导入", "reset_password": "重置密码", "password_reset_successful": "重置密码成功", "or": "或者", diff --git a/frontend/src/views/system/excel-upload/Uploader.vue b/frontend/src/views/system/excel-upload/Uploader.vue new file mode 100644 index 000000000..dbf5d33ea --- /dev/null +++ b/frontend/src/views/system/excel-upload/Uploader.vue @@ -0,0 +1,372 @@ + + + + + + diff --git a/frontend/src/views/system/professional/index.vue b/frontend/src/views/system/professional/index.vue index 9658a4eb4..5c3fde245 100644 --- a/frontend/src/views/system/professional/index.vue +++ b/frontend/src/views/system/professional/index.vue @@ -4,20 +4,17 @@ import icon_export_outlined from '@/assets/svg/icon_export_outlined.svg' import { professionalApi } from '@/api/professional' import { formatTimestamp } from '@/utils/date' import { datasourceApi } from '@/api/datasource' -import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg' import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg' import IconOpeEdit from '@/assets/svg/icon_edit_outlined.svg' import IconOpeDelete from '@/assets/svg/icon_delete.svg' import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg' import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue' import { useI18n } from 'vue-i18n' -import { cloneDeep, endsWith, startsWith } from 'lodash-es' -import { genFileId, type UploadInstance, type UploadProps, type UploadRawFile } from 'element-plus' -import { useCache } from '@/utils/useCache.ts' -import { settingsApi } from '@/api/setting.ts' +import { cloneDeep } from 'lodash-es' import { convertFilterText, FilterText } from '@/components/filter-text' import { DrawerMain } from '@/components/drawer-main' import iconFilter from '@/assets/svg/icon-filter_outlined.svg' +import Uploader from '@/views/system/excel-upload/Uploader.vue' interface Form { id?: string | null @@ -30,7 +27,6 @@ interface Form { } const { t } = useI18n() -const { wsCache } = useCache() const multipleSelectionAll = ref([]) const allDsList = ref([]) const keywords = ref('') @@ -83,101 +79,6 @@ const cancelDelete = () => { isIndeterminate.value = false } -const uploadRef = ref() -const uploadLoading = ref(false) - -const token = wsCache.get('user.token') -const headers = ref({ 'X-SQLBOT-TOKEN': `Bearer ${token}` }) -const getUploadURL = import.meta.env.VITE_API_BASE_URL + '/system/terminology/uploadExcel' - -const handleExceed: UploadProps['onExceed'] = (files) => { - uploadRef.value!.clearFiles() - const file = files[0] as UploadRawFile - file.uid = genFileId() - uploadRef.value!.handleStart(file) -} - -const beforeUpload = (rawFile: any) => { - if (rawFile.size / 1024 / 1024 > 50) { - ElMessage.error(t('common.not_exceed_50mb')) - return false - } - uploadLoading.value = true - return true -} -const onSuccess = (response: any) => { - uploadRef.value!.clearFiles() - search() - - if (response?.data?.failed_count > 0 && response?.data?.error_excel_filename) { - ElMessage.error( - t('training.upload_failed', { - success: response.data.success_count, - fail: response.data.failed_count, - fail_info: response.data.error_excel_filename, - }) - ) - settingsApi - .downloadError(response.data.error_excel_filename) - .then((res) => { - const blob = new Blob([res], { - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - }) - const link = document.createElement('a') - link.href = URL.createObjectURL(blob) - link.download = response.data.error_excel_filename - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - }) - .catch(async (error) => { - if (error.response) { - try { - let text = await error.response.data.text() - try { - text = JSON.parse(text) - } finally { - ElMessage({ - message: text, - type: 'error', - showClose: true, - }) - } - } catch (e) { - console.error('Error processing error response:', e) - } - } else { - console.error('Other error:', error) - ElMessage({ - message: error, - type: 'error', - showClose: true, - }) - } - }) - .finally(() => { - uploadLoading.value = false - }) - } else { - ElMessage.success(t('training.upload_success')) - uploadLoading.value = false - } -} - -const onError = (err: Error) => { - uploadLoading.value = false - uploadRef.value!.clearFiles() - let msg = err.message - if (startsWith(msg, '"') && endsWith(msg, '"')) { - msg = msg.slice(1, msg.length - 1) - } - ElMessage({ - message: msg, - type: 'error', - showClose: true, - }) -} - const exportExcel = () => { ElMessageBox.confirm(t('professional.export_hint', { msg: pageInfo.total }), { confirmButtonType: 'primary', @@ -543,33 +444,19 @@ const changeStatus = (id: any, val: any) => { {{ $t('professional.export_all') }} - - - - {{ $t('user.batch_import') }} - - - + + {{ $t('user.filter') }} - + @@ -878,6 +765,9 @@ const changeStatus = (id: any, val: any) => {