feat: AI Model

This commit is contained in:
fit2cloud-chenyw
2025-05-07 16:21:17 +08:00
parent 2de7ece73b
commit 178d41a1e9
15 changed files with 766 additions and 23 deletions
@@ -0,0 +1,43 @@
"""004_add_aimodel_auto
Revision ID: 8fe654655905
Revises: d116056121c3
Create Date: 2025-05-07 15:51:34.768842
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '8fe654655905'
down_revision = 'd116056121c3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('ai_model',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
sa.Column('type', sa.Integer(), nullable=False),
sa.Column('api_key', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
sa.Column('endpoint', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
sa.Column('max_context_window', sa.Integer(), nullable=False),
sa.Column('temperature', sa.Float(), nullable=False),
sa.Column('status', sa.Boolean(), nullable=False),
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
sa.Column('create_time', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_ai_model_id'), 'ai_model', ['id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_ai_model_id'), table_name='ai_model')
op.drop_table('ai_model')
# ### end Alembic commands ###
+2 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from apps.system.api import login, user
from apps.system.api import login, user, aimodel
from apps.settings.api import terminology
from apps.datasource.api import datasource
@@ -8,6 +8,7 @@ from apps.datasource.api import datasource
api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(user.router)
api_router.include_router(aimodel.router)
api_router.include_router(terminology.router)
api_router.include_router(datasource.router)
+86
View File
@@ -0,0 +1,86 @@
from datetime import datetime
from fastapi import APIRouter
from sqlmodel import select
from apps.system.models.system_modle import AiModelDetail
from apps.system.schemas.system_schema import model_status
from common.core.deps import SessionDep
from common.core.pagination import Paginator
from common.core.schemas import PaginatedResponse, PaginationParams
from common.utils.time import get_timestamp
router = APIRouter(tags=["system/aimodel"], prefix="/system/aimodel")
@router.get("/pager/{pageNum}/{pageSize}", response_model=PaginatedResponse[AiModelDetail])
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(
model=AiModelDetail,
pagination=pagination,
**filters)
@router.get("/{id}", response_model=AiModelDetail)
async def get_model_by_id(
session: SessionDep,
id: int
):
term = session.get(AiModelDetail, id)
return term
@router.post("", response_model=AiModelDetail)
async def add_model(
session: SessionDep,
creator: AiModelDetail
):
data = AiModelDetail.model_validate(creator)
data.create_time = get_timestamp()
session.add(data)
session.commit()
return creator
@router.put("", response_model=AiModelDetail)
async def update_terminology(
session: SessionDep,
model: AiModelDetail
):
model.id = int(model.id)
term = session.exec(select(AiModelDetail).where(AiModelDetail.id == model.id)).first()
update_data = model.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(term, field, value)
session.add(term)
session.commit()
return model
@router.delete("/{id}", response_model=AiModelDetail)
async def delete_terminology(
session: SessionDep,
id: int
):
term = session.exec(select(AiModelDetail).where(AiModelDetail.id == id)).first()
session.delete(term)
session.commit()
return {
"message": f"AiModel with ID {id} deleted successfully."
}
@router.patch("/status", response_model=dict)
async def status(session: SessionDep, dto: model_status):
ids = dto.ids
status = dto.status
if not ids:
return {"message": "ids is empty"}
statement = select(AiModelDetail).where(AiModelDetail.id.in_(ids))
terms = session.exec(statement).all()
for term in terms:
term.status = status
session.add(term)
session.commit()
return {"message": f"AiModel with IDs {ids} updated successfully."}
@@ -0,0 +1,18 @@
from sqlmodel import BigInteger, Field, SQLModel
from common.core.models import SnowflakeBase
class AiModelBase:
name: str = Field(max_length=255, nullable=False)
type: int = Field(nullable=False)
class AiModelDetail(AiModelBase, SnowflakeBase, table=True):
__tablename__ = "ai_model"
api_key: str | None = Field(max_length=255, nullable=True)
endpoint: str = Field(max_length=255, nullable=False)
max_context_window: int = Field(default=0)
temperature: float = Field(default=0.0)
status: bool = Field(default=True)
description: str | None = Field(max_length=255, nullable=True)
create_time: int = Field(default=0, sa_type=BigInteger())
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class model_status(BaseModel):
status: bool
ids: list[int]
+1 -15
View File
@@ -13,21 +13,7 @@ class SnowflakeBase(SQLModel):
nullable=False
)
@field_validator("id", mode="before")
def validate_id(cls, v):
if isinstance(v, str):
if not v:
return None
try:
return int(v)
except ValueError:
raise ValueError("Invalid bigint string")
elif isinstance(v, int):
return v
raise TypeError("BigInt must be int or string")
class Config:
json_encoders = {
int: lambda v: str(v) if v > 2**53-1 else v
int: lambda v: str(v) if isinstance(v, int) and v > (2**53 - 1) else v
}
+6
View File
@@ -0,0 +1,6 @@
from datetime import datetime
def get_timestamp():
dt_millis = int(datetime.now().timestamp() * 1000)
return dt_millis
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
usage() {
echo -e "${GREEN}Usage:${NC} $0 [\"migration message\"]"
echo -e "Example: $0 \"Added user table\""
exit 1
}
if [ "$#" -eq 0 ]; then
echo -e "${RED}Error:${NC} No migration message provided"
usage
fi
echo -e "${GREEN}Generating migration with message:${NC} \"$1\""
alembic revision --autogenerate -m "$1"
if [ $? -eq 0 ]; then
echo -e "${GREEN}Migration created successfully!${NC}"
else
echo -e "${RED}Error:${NC} Failed to create migration"
exit 1
fi
+3
View File
@@ -11,6 +11,7 @@ declare module 'vue' {
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -22,7 +23,9 @@ declare module 'vue' {
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
+10
View File
@@ -0,0 +1,10 @@
import { request } from '@/utils/request'
export const modelApi = {
pager: (pageNumber: number, pageSize: number) => request.get(`/system/aimodel/pager/${pageNumber}/${pageSize}`),
add: (data: any) => request.post('/system/aimodel', data),
edit: (data: any) => request.put('/system/aimodel', data),
delete: (id: number) => request.delete(`/system/aimodel/${id}`),
query: (id: number) => request.get(`/system/aimodel/${id}`),
status: (data: any) => request.patch('/system/aimodel/status', data),
}
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 0a512 512 0 1 0 0 1024A512 512 0 0 0 512 0zM113.777778 512a398.222222 398.222222 0 0 1 398.222222-398.222222c89.429333 0 171.690667 29.809778 238.193778 79.587555L193.422222 750.136889A395.377778 395.377778 0 0 1 113.777778 512z m398.222222 398.222222a395.719111 395.719111 0 0 1-238.136889-79.587555L830.577778 273.806222A396.060444 396.060444 0 0 1 910.222222 512a398.222222 398.222222 0 0 1-398.222222 398.222222z" fill="#1E2330" /></svg>

After

Width:  |  Height:  |  Size: 713 B

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 0a512 512 0 1 0 0 1024A512 512 0 0 0 512 0z m0 910.222222A398.222222 398.222222 0 1 1 512 113.777778a398.222222 398.222222 0 0 1 0 796.444444z" fill="#1E2330" /><path d="M448.056889 584.305778L322.389333 458.638222 243.541333 537.543111l205.596445 205.539556h0.056889l0.512 0.512 325.859555-325.859556-80.440889-80.440889z" fill="#1E2330" /></svg>

After

Width:  |  Height:  |  Size: 619 B

+2 -3
View File
@@ -83,7 +83,7 @@ class HttpService {
// Response interceptor
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
console.log(`[Response] ${response.config.url}`, response.data)
// console.log(`[Response] ${response.config.url}`, response.data)
// Return raw response if configured
if ((response.config as FullRequestConfig).requestOptions?.rawResponse) {
@@ -164,8 +164,7 @@ class HttpService {
// ElMessage.error(errorMessage)
ElMessage({
message: errorMessage,
type: 'error',
duration: 0
type: 'error'
})
}
+516 -2
View File
@@ -1,3 +1,517 @@
<template>
<h2>this is ai model page</h2>
</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"
@keyup.enter.native="handleSearch"
>
<template #prefix>
<el-icon class="el-input__icon"><IconSearch /></el-icon>
</template>
</el-input>
</div>
<div class="tool-right">
<div class="tool-btn secondary-btn" @click="batchEnableHandler">
<el-icon><IconEnable /></el-icon>
<span>Batch Enable</span>
</div>
<div class="tool-btn secondary-btn" @click="batchDisableHandler">
<el-icon><IconDisable /></el-icon>
<span>Batch Disable</span>
</div>
<div class="tool-btn primary-btn" @click="openDialog">
<el-icon><IconOpeAdd /></el-icon>
<span>Add Model</span>
</div>
</div>
</div>
<div class="sqlbot-table">
<el-table :data="state.tableData" style="width: 100%" row-key="id" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="Model Name" width="280" />
<el-table-column prop="type" label="Model Type" width="280" />
<el-table-column prop="api_key" label="API KEY" width="280">
<template #default="scope">
<div class="user-status-container" :class="[ scope.row.api_key ? 'active' : 'disabled' ]">
<span>{{ scope.row.api_key ? 'Configured' : 'Not Configured' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="Status" width="180">
<template #default="scope">
<div class="user-status-container" :class="[ scope.row.status ? 'active' : 'disabled' ]">
<el-icon>
<SuccessFilled v-if="scope.row.status"/>
<CircleCloseFilled v-else />
</el-icon>
<span>{{ scope.row.status ? 'Enabled' : 'Disabled' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="create_time" label="Create time">
<template #default="scope">
<span>{{ formatTimestamp(scope.row.create_time, 'YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</el-table-column>
<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 class="pagination-container">
<el-pagination
v-model:current-page="state.pageInfo.currentPage"
v-model:page-size="state.pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="state.pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
<el-dialog v-model="dialogFormVisible" title="Add Ai Model" destroy-on-close width="500" @close="onFormClose" class="sqlbot-dialog">
<el-form :model="state.form"
label-width="180px"
label-position="top"
class="sqlbot-form"
size="large"
ref="termFormRef"
>
<el-form-item label="Model Name" >
<el-input v-model="state.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="Model Type">
<el-select v-model="state.form.type" placeholder="Please select a type">
<el-option label="domain1" value="0" />
<el-option label="domain2" value="1" />
<el-option label="domain3" value="2" />
</el-select>
</el-form-item>
<el-form-item label="API KEY" >
<el-input type="password" v-model="state.form.api_key" show-password placeholder="Please input password" />
</el-form-item>
<el-form-item label="API Endpoint URL" >
<el-input v-model="state.form.endpoint" placeholder="Please input endpoint" />
</el-form-item>
<el-form-item label="Maximum Context Window" >
<el-input v-model="state.form.max_context_window" type="number" placeholder="Please input max_context_window" />
</el-form-item>
<el-form-item label="Temperature" >
<el-slider v-model="state.form.temperature" :format-tooltip="formatTooltip" />
</el-form-item>
<el-form-item label="Model Status" >
<el-checkbox v-model="state.form.status" label="" size="large" />
</el-form-item>
<el-form-item label="Description" >
<el-input type="textarea" v-model="state.form.description" />
</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 IconEnable from '@/assets/svg/operate/ope-enable.svg';
import IconDisable from '@/assets/svg/operate/ope-disable.svg';
import IconOpeAdd from '@/assets/svg/operate/ope-add.svg';
import { SuccessFilled, CircleCloseFilled, Search as IconSearch } from '@element-plus/icons-vue'
import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg';
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg';
import { modelApi } from '@/api/system'
import { formatTimestamp } from '@/utils/date'
const keyword = ref('')
const dialogFormVisible = ref(false)
const termFormRef = ref()
const state = reactive({
tableData: [
],
form: {
id: '',
name: '',
type: 0,
api_key: '',
endpoint: '',
max_context_window: 0,
temperature: 0,
status: false,
description: ''
},
pageInfo: {
currentPage: 1,
pageSize: 20,
total: 0
},
selectedIds: []
})
onMounted(() => {
search()
})
const handleSearch = (e: any) => {
console.log('search', e);
}
const editHandler = (id: any) => {
modelApi.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(() => {
modelApi.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) return
Object.keys(state.form).forEach((key) => {
state.form[key as keyof typeof state.form] = null
})
}
const closeForm = () => {
dialogFormVisible.value = false
}
const onFormClose = () => {
resetForm()
dialogFormVisible.value = false
}
const search = () => {
modelApi.pager(state.pageInfo.currentPage, state.pageInfo.pageSize).then((res: any) => {
state.tableData = res.items
state.pageInfo.total = res.total
})
}
const addTerm = () => {
const param: any = {
...state.form,
}
delete param.id
modelApi.add(param).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: 'Add completed',
})
})
}
const editTerm = () => {
modelApi.edit(state.form).then((res: any) => {
console.log('edit term', res)
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: 'Edit completed',
})
})
}
const saveHandler = () => {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
}
const handleSizeChange = (val: number) => {
state.pageInfo.pageSize = val
search()
}
const handleCurrentChange = (val: number) => {
state.pageInfo.currentPage = val
search()
}
const formatTooltip = (val: number) => {
return val / 100
}
const handleSelectionChange = (rows: any) => {
state.selectedIds = rows.map((item: any) => item.id)
}
const batchDisableHandler = () => {
if (state.selectedIds.length === 0) {
ElMessage({
type: 'warning',
message: 'Please select at least one item',
})
return
}
const param = {
ids: state.selectedIds,
status: false
}
ElMessageBox.confirm(
'Are you sure to execute this operation?',
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
modelApi.status(param).then(() => {
search()
ElMessage({
type: 'success',
message: 'Batch disable completed',
})
})
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Execute canceled',
})
})
}
const batchEnableHandler = () => {
if (state.selectedIds.length === 0) {
ElMessage({
type: 'warning',
message: 'Please select at least one item',
})
return
}
const param = {
ids: state.selectedIds,
status: true
}
modelApi.status(param).then(() => {
search()
ElMessage({
type: 'success',
message: 'Batch enable completed',
})
})
}
</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;
background-color: var(--white);
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
.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: #1890ff;
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(.el-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;
}
/* border-left: 1px solid var(--el-table-border-color);
border-right: 1px solid var(--el-table-border-color); */
}
.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: #4285f4;
}
}
}
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 20px;
background-color: var(--white);
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
}
.sqlbot-form {
width: 100%;
.el-form-item {
margin-bottom: 16px;
text-align: left;
label {
font-size: 14px;
color: #606266;
}
}
}
.sqlbot-dialog {
width: 500px;
:deep(.el-dialog__header) {
text-align: left !important;
font-size: 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
color: #606266;
font-size: 16px;
}
}
.user-status-container {
display: flex;
align-items: center;
column-gap: 8px;
font-size: 14px;
color: #606266;
padding: 4px 8px;
width: fit-content;
border-radius: 4px;
&.active {
background-color: #e6f7ee;
color: #389e0d;
}
&.disabled {
background-color: #fff1f0;
color: #cf1322;
}
}
</style>
+46 -2
View File
@@ -63,6 +63,19 @@
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="state.pageInfo.currentPage"
v-model:page-size="state.pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="state.pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
@@ -124,6 +137,11 @@ const state = reactive({
definition: '',
domain: ''
},
pageInfo: {
currentPage: 1,
pageSize: 20,
total: 0
}
})
const handleSearch = (e: any) => {
console.log('search', e);
@@ -182,8 +200,9 @@ const onFormClose = () => {
}
const search = () => {
userApi.pager(1, 20).then((res: any) => {
userApi.pager(state.pageInfo.currentPage, state.pageInfo.pageSize).then((res: any) => {
state.tableData = res.items
state.pageInfo.total = res.total
})
}
const addTerm = () => {
@@ -214,6 +233,14 @@ const saveHandler = () => {
addTerm()
}
}
const handleSizeChange = (val: number) => {
state.pageInfo.pageSize = val
search()
}
const handleCurrentChange = (val: number) => {
state.pageInfo.currentPage = val
search()
}
onMounted(() => {
search()
})
@@ -228,7 +255,11 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
background-color: var(--white);
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
.tool-left {
display: flex;
align-items: center;
@@ -295,11 +326,14 @@ onMounted(() => {
td {
height: 52px;
}
border-left: 1px solid var(--el-table-border-color);
border-right: 1px solid var(--el-table-border-color);
}
.table-operate {
display: flex;
justify-content: flex-end;
gap: 8px;
.opt-btn {
width: 36px;
height: 36px;
@@ -319,6 +353,16 @@ onMounted(() => {
}
}
}
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 20px;
background-color: var(--white);
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
}
.sqlbot-form {