feat: import Sample SQL / Terminologies

This commit is contained in:
ulleo
2025-11-18 19:22:37 +08:00
committed by ulleo
parent a8f6feb2ca
commit 25d2fe1e5e
11 changed files with 57 additions and 534 deletions
+2
View File
@@ -7,6 +7,7 @@ from apps.datasource.api import datasource, table_relation
from apps.mcp import mcp
from apps.system.api import login, user, aimodel, workspace, assistant
from apps.terminology.api import terminology
from apps.settings.api import base
api_router = APIRouter()
api_router.include_router(login.router)
@@ -14,6 +15,7 @@ api_router.include_router(user.router)
api_router.include_router(workspace.router)
api_router.include_router(assistant.router)
api_router.include_router(aimodel.router)
api_router.include_router(base.router)
api_router.include_router(terminology.router)
api_router.include_router(data_training.router)
api_router.include_router(datasource.router)
@@ -8,7 +8,7 @@ from typing import Optional
import pandas as pd
from fastapi import APIRouter, File, UploadFile, Query
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.responses import StreamingResponse
from apps.chat.models.chat_model import AxisObj
from apps.data_training.curd.data_training import page_data_training, create_training, update_training, delete_training, \
@@ -16,7 +16,6 @@ from apps.data_training.curd.data_training import page_data_training, create_tra
from apps.data_training.models.data_training_model import DataTrainingInfo
from common.core.config import settings
from common.core.deps import SessionDep, CurrentUser, Trans
from common.core.file import FileRequest
from common.utils.data_format import DataFormat
router = APIRouter(tags=["DataTraining"], prefix="/system/data-training")
@@ -213,30 +212,3 @@ async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile
}
return await asyncio.to_thread(inner)
@router.post("/download-fail-info")
async def download_excel(req: FileRequest):
"""
根据文件路径下载 Excel 文件
"""
filename = req.file
file_path = os.path.join(path, filename)
# 检查文件是否存在
if not os.path.exists(file_path):
raise HTTPException(404, "File Not Exists")
# 检查文件是否是 Excel 文件
if not filename.endswith('_error.xlsx'):
raise HTTPException(400, "Only support _error.xlsx")
# 获取文件名
filename = os.path.basename(file_path)
# 返回文件
return FileResponse(
path=file_path,
filename=filename,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
+39
View File
@@ -0,0 +1,39 @@
import os
from http.client import HTTPException
from fastapi import APIRouter
from fastapi.responses import FileResponse
from common.core.config import settings
from common.core.file import FileRequest
router = APIRouter(tags=["System"], prefix="/system")
path = settings.EXCEL_PATH
@router.post("/download-fail-info")
async def download_excel(req: FileRequest):
"""
根据文件路径下载 Excel 文件
"""
filename = req.file
file_path = os.path.join(path, filename)
# 检查文件是否存在
if not os.path.exists(file_path):
raise HTTPException(404, "File Not Exists")
# 检查文件是否是 Excel 文件
if not filename.endswith('_error.xlsx'):
raise HTTPException(400, "Only support _error.xlsx")
# 获取文件名
filename = os.path.basename(file_path)
# 返回文件
return FileResponse(
path=file_path,
filename=filename,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
-68
View File
@@ -1,68 +0,0 @@
from fastapi import APIRouter
from sqlmodel import select
from apps.settings.models.setting_models import term_model
from apps.settings.schemas.setting_schemas import term_schema_creator
from common.core.deps import SessionDep
from common.core.pagination import Paginator
from common.core.schemas import PaginatedResponse, PaginationParams
router = APIRouter(tags=["Settings"], prefix="/settings/terminology")
@router.get("/pager/{pageNum}/{pageSize}", response_model=PaginatedResponse[term_model])
async def pager(
session: SessionDep,
pageNum: int,
pageSize: int
):
pagination = PaginationParams(page=pageNum, size=pageSize)
paginator = Paginator(session)
filters = {}
return await paginator.get_paginated_response(
stmt=term_model,
pagination=pagination,
**filters)
@router.get("/{id}", response_model=term_model)
async def get_terminology_by_id(
session: SessionDep,
id: int
):
term = session.get(term_model, id)
return term
@router.post("", response_model=term_model)
async def add_terminology(
session: SessionDep,
creator: term_schema_creator
):
terminology = term_model(**creator.model_dump())
session.add(terminology)
session.commit()
return terminology
@router.put("", response_model=term_model)
async def update_terminology(
session: SessionDep,
terminology: term_model
):
terminology.id = int(terminology.id)
term = session.exec(select(term_model).where(term_model.id == terminology.id)).first()
update_data = terminology.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(term, field, value)
session.add(term)
session.commit()
return terminology
@router.delete("/{id}", response_model=term_model)
async def delete_terminology(
session: SessionDep,
id: int
):
term = session.exec(select(term_model).where(term_model.id == id)).first()
session.delete(term)
session.commit()
return {
"message": f"Terminology with ID {id} deleted successfully."
}
-9
View File
@@ -16,13 +16,4 @@ export const professionalApi = {
responseType: 'blob',
requestOptions: { customError: true },
}),
downloadError: (path: any) =>
request.post(
`/system/terminology/download-fail-info`,
{ file: path },
{
responseType: 'blob',
requestOptions: { customError: true },
}
),
}
+11 -1
View File
@@ -1,10 +1,20 @@
import { request } from '@/utils/request'
export const termApi = {
export const settingsApi = {
pager: (pageNumber: number, pageSize: number) =>
request.get(`/settings/terminology/pager/${pageNumber}/${pageSize}`),
add: (data: any) => request.post('/settings/terminology', data),
edit: (data: any) => request.put('/settings/terminology', data),
delete: (id: number) => request.delete(`/settings/terminology/${id}`),
query: (id: number) => request.get(`/settings/terminology/${id}`),
downloadError: (path: any) =>
request.post(
`/system/download-fail-info`,
{ file: path },
{
responseType: 'blob',
requestOptions: { customError: true },
}
),
}
-9
View File
@@ -15,13 +15,4 @@ export const trainingApi = {
responseType: 'blob',
requestOptions: { customError: true },
}),
downloadError: (path: any) =>
request.post(
`/system/data-training/download-fail-info`,
{ file: path },
{
responseType: 'blob',
requestOptions: { customError: true },
}
),
}
-340
View File
@@ -1,340 +0,0 @@
<template>
<div class="sqlbot-table-container professional-container">
<div class="sqlbot-tool">
<div class="tool-left">
<el-input
v-model="keyword"
class="sqlbot-search"
placeholder="Search"
clearable
prefix-icon="el-icon-search"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon class="el-input__icon"><search /></el-icon>
</template>
</el-input>
</div>
<div class="tool-right">
<div class="tool-btn secondary-btn">
<el-icon><IconOpeUpload /></el-icon>
<span>Import Terminology</span>
</div>
<div class="tool-btn secondary-btn">
<el-icon><IconOpeDownload /></el-icon>
<span>Export Terminology</span>
</div>
<div class="tool-btn primary-btn" @click="openDialog">
<el-icon><IconOpeAdd /></el-icon>
<span>Add Terminology</span>
</div>
</div>
</div>
<div class="sqlbot-table">
<el-table :data="state.tableData" style="width: 100%">
<el-table-column prop="term" label="Term" width="280" />
<el-table-column prop="definition" label="Definition" />
<el-table-column prop="domain" label="Domain" width="180" />
<el-table-column prop="id" label="Actions" width="120" fixed="right">
<template #default="scope">
<div class="table-operate">
<div class="opt-btn" @click="editHandler(scope.row.id)">
<el-icon><IconOpeEdit /></el-icon>
</div>
<div class="opt-btn" @click="deleteHandler(scope.row.id)">
<el-icon><IconOpeDelete /></el-icon>
</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog
v-model="dialogFormVisible"
title="Add Terminology"
destroy-on-close
width="500"
class="sqlbot-dialog"
@close="onFormClose"
>
<el-form
ref="termFormRef"
:model="state.form"
label-width="180px"
label-position="top"
class="sqlbot-form"
size="large"
@submit.prevent
>
<el-form-item label="Term name">
<el-input v-model="state.form.term" clearable autocomplete="off" />
</el-form-item>
<el-form-item label="Definition">
<el-input v-model="state.form.definition" clearable type="textarea" />
</el-form-item>
<el-form-item label="Domain">
<el-select v-model="state.form.domain" placeholder="Please select a domain">
<el-option label="domain1" value="domain1" />
<el-option label="domain2" value="domain2" />
<el-option label="domain3" value="domain3" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeForm">Cancel</el-button>
<el-button type="primary" @click="saveHandler"> save </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import IconOpeUpload from '@/assets/svg/operate/ope-upload.svg'
import IconOpeDownload from '@/assets/svg/operate/ope-download.svg'
import IconOpeAdd from '@/assets/svg/operate/ope-add.svg'
import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg'
import { termApi } from '@/api/setting'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const keyword = ref('')
const dialogFormVisible = ref(false)
const termFormRef = ref()
const state = reactive({
tableData: [],
form: {
id: '',
term: '',
definition: '',
domain: '',
},
})
const handleSearch = (e: any) => {
console.info('search', e)
}
const editHandler = (id: any) => {
termApi.query(id).then((res: any) => {
state.form = res
dialogFormVisible.value = true
})
}
const deleteHandler = (id: any) => {
ElMessageBox.confirm('Are you sure to delete?', 'Warning', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
})
.then(() => {
termApi.delete(id).then(() => {
ElMessage({
type: 'success',
message: 'Delete completed',
})
search()
})
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
}
const openDialog = () => {
dialogFormVisible.value = true
}
const resetForm = () => {
if (!termFormRef.value) return
Object.keys(state.form).forEach((key) => {
state.form[key as keyof typeof state.form] = ''
})
}
const closeForm = () => {
dialogFormVisible.value = false
}
const onFormClose = () => {
resetForm()
dialogFormVisible.value = false
}
const search = () => {
termApi.pager(1, 20).then((res: any) => {
state.tableData = res.items
})
}
const addTerm = () => {
termApi.add(state.form).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: 'Add completed',
})
})
}
const editTerm = () => {
termApi.edit(state.form).then((res: any) => {
console.info('edit term', res)
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
}
const saveHandler = () => {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
}
onMounted(() => {
search()
})
</script>
<style lang="less" scoped>
.sqlbot-table-container {
width: 100%;
height: 100%;
.sqlbot-tool {
height: 42px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.tool-left {
display: flex;
align-items: center;
.sqlbot-search {
width: 300px;
--el-input-inner-height: 40px !important;
--el-input-border-radius: 24px;
--el-input-bg-color: #f5f7fa;
--el-input-border-color: #e5e8ed;
}
}
.tool-right {
display: flex;
align-items: center;
column-gap: 12px;
height: 35px;
.secondary-btn {
background: #f5f5f5;
color: #333;
border: 1px solid #ddd;
}
.primary-btn {
background: var(--el-color-primary);
color: white;
}
.tool-btn {
display: flex;
align-items: center;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
outline: none;
font-weight: 500;
transition: all 0.3s;
font-size: 14px;
gap: 6px;
&:hover {
background-color: #f1f3f4;
}
&.default {
color: #2d2e31;
font-weight: 500;
font-size: 14px;
i {
width: 20px;
height: 20px;
}
}
}
}
}
.sqlbot-table {
border-radius: 6px;
:deep(.ed-table) {
--el-table-header-bg-color: #f5f7fa;
--el-table-border-color: #ebeef5;
--el-table-header-text-color: #606266;
th {
font-weight: 600;
height: 48px;
}
td {
height: 52px;
}
}
.table-operate {
display: flex;
justify-content: flex-end;
gap: 8px;
.opt-btn {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #5f6368;
cursor: pointer;
transition: all 0.2s;
background-color: #f1f3f4;
border: none;
font-size: 18px;
&:hover {
background-color: #e8f0fe;
color: var(--el-color-primary);
}
}
}
}
}
.sqlbot-form {
width: 100%;
.ed-form-item {
margin-bottom: 16px;
text-align: left;
label {
font-size: 14px;
color: #606266;
}
/* .ed-input {
--el-input-border-color: #e5e8ed;
--el-input-border-radius: 4px;
--el-input-inner-height: 40px !important;
}
.ed-select {
--el-select-bg-color: #f5f7fa;
--el-select-border-color: #e5e8ed;
--el-select-border-radius: 4px;
--el-select-inner-height: 40px !important;
} */
}
}
.sqlbot-dialog {
width: 500px;
:deep(.ed-dialog__header) {
text-align: left !important;
font-size: 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
color: #606266;
font-size: 16px;
}
}
</style>
-75
View File
@@ -1,75 +0,0 @@
<template>
<div class="setting-container">
<el-tabs v-model="activeName" class="setting-tabs" @tab-click="handleClick">
<el-tab-pane label="User" name="0">
<template #label>
<span class="custom-tabs-label">
<el-icon><IconProfessional /></el-icon>
<span>Terminology</span>
</span>
</template>
<Professional v-if="activeName == '0'" />
</el-tab-pane>
<el-tab-pane label="Config" name="1">
<template #label>
<span class="custom-tabs-label">
<el-icon><IconTrain /></el-icon>
<span>Data training</span>
</span>
</template>
Data training
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import IconProfessional from '@/assets/svg/professional.svg'
import IconTrain from '@/assets/svg/train.svg'
import Professional from './Professional.vue'
const activeName = ref('0')
const handleClick = (tab: any, event: MouseEvent) => {
console.info(tab, event)
}
</script>
<style lang="less" scoped>
.setting-container {
height: calc(100% - 48px);
background-color: var(--white);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
padding: 24px;
:deep(.ed-tabs) {
--el-tabs-header-height: 55px;
.ed-tabs__header {
margin: 0 0 24px !important;
}
}
.setting-tabs {
height: 100%;
:deep(.custom-tabs-label) {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 500;
column-gap: 4px;
i {
font-size: 20px;
width: 28px;
height: 28px;
}
}
:deep(.ed-tabs__item) {
padding: 0 16px !important;
&:hover {
// color: var(--primary-color);
background-color: rgba(0, 0, 0, 0.02);
}
}
}
}
</style>
@@ -13,8 +13,8 @@ import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import { useI18n } from 'vue-i18n'
import { cloneDeep } from 'lodash-es'
import { genFileId, type UploadInstance, type UploadProps, type UploadRawFile } from 'element-plus'
import { trainingApi } from '@/api/training.ts'
import { useCache } from '@/utils/useCache.ts'
import { settingsApi } from '@/api/setting.ts'
interface Form {
id?: string | null
@@ -105,7 +105,7 @@ const onSuccess = (response: any) => {
fail_info: response.data.error_excel_filename,
})
)
trainingApi
settingsApi
.downloadError(response.data.error_excel_filename)
.then((res) => {
const blob = new Blob([res], {
+2 -1
View File
@@ -2,6 +2,7 @@
import { computed, nextTick, onMounted, reactive, ref, unref } from 'vue'
import icon_export_outlined from '@/assets/svg/icon_export_outlined.svg'
import { trainingApi } from '@/api/training'
import { settingsApi } from '@/api/setting.ts'
import { formatTimestamp } from '@/utils/date'
import { datasourceApi } from '@/api/datasource'
import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg'
@@ -124,7 +125,7 @@ const onSuccess = (response: any) => {
fail_info: response.data.error_excel_filename,
})
)
trainingApi
settingsApi
.downloadError(response.data.error_excel_filename)
.then((res) => {
const blob = new Blob([res], {