Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-07-28 11:37:31 +08:00
54 changed files with 1300 additions and 573 deletions
+5
View File
@@ -180,6 +180,7 @@ export class ChatInfo extends Chat {
chat_type: string,
datasource: number,
engine_type: string,
ds_type: string,
datasource_name: string,
datasource_exists: boolean,
records: Array<ChatRecord>
@@ -192,6 +193,7 @@ export class ChatInfo extends Chat {
chat_type?: string,
datasource?: number,
engine_type?: string,
ds_type?: string,
datasource_name?: string,
datasource_exists: boolean = true,
records: Array<ChatRecord> = []
@@ -206,6 +208,7 @@ export class ChatInfo extends Chat {
this.chat_type = param1.chat_type
this.datasource = param1.datasource
this.engine_type = param1.engine_type
this.ds_type = param1.ds_type
} else {
this.id = param1
this.create_time = getDate(create_time)
@@ -214,6 +217,7 @@ export class ChatInfo extends Chat {
this.chat_type = chat_type
this.datasource = datasource
this.engine_type = engine_type
this.ds_type = ds_type
}
}
this.datasource_name = datasource_name
@@ -275,6 +279,7 @@ export const chatApi = {
data.chat_type,
data.datasource,
data.engine_type,
data.ds_type,
data.datasource_name,
data.datasource_exists,
toChatRecordList(data.records)
+1
View File
@@ -5,3 +5,4 @@ export const updateAssistant = (data: any) => request.put('/system/assistant', d
export const saveAssistant = (data: any) => request.post('/system/assistant', data)
export const getOne = (id: any) => request.get(`/system/assistant/${id}`)
export const delOne = (id: any) => request.delete(`/system/assistant/${id}`)
export const dsApi = (id: any) => request.get(`/datasource/ws/${id}`)
+2
View File
@@ -8,4 +8,6 @@ export const modelApi = {
delete: (id: number) => request.delete(`/system/aimodel/${id}`),
query: (id: number) => request.get(`/system/aimodel/${id}`),
setDefault: (id: number) => request.put(`/system/aimodel/default/${id}`),
check: (data: any) =>
request.post('/system/aimodel/status', data, { requestOptions: { silent: true } }),
}
+1
View File
@@ -25,4 +25,5 @@ export const userApi = {
deleteBatch: (data: any) => request.delete(`/user`, { data }),
get: (key: string) => request.get(`/user/${key}`),
pwd: (id: any) => request.patch(`/user/pwd/${id}`),
status: (data: any) => request.patch('/user/status', data),
}
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33333 5.3335V13.3335H10V5.3335H3.33333ZM11.3333 4.66683V14.0742C11.3333 14.4015 11.0548 14.6668 10.7111 14.6668H2.62222C2.27858 14.6668 2 14.4015 2 14.0742V4.59276C2 4.26548 2.27858 4.00016 2.62222 4.00016H10.6667C11.0349 4.00016 11.3333 4.29864 11.3333 4.66683ZM13.8047 1.52876C13.9254 1.6494 14 1.81607 14 2.00016V10.3335C14 10.5176 13.8508 10.6668 13.6667 10.6668H13C12.8159 10.6668 12.6667 10.5176 12.6667 10.3335V2.66683H6.33333C6.14924 2.66683 6 2.51759 6 2.3335V1.66683C6 1.48273 6.14924 1.3335 6.33333 1.3335H13.3333C13.5174 1.3335 13.6841 1.40812 13.8047 1.52876Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 706 B

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23Z"/>
<path d="M11.5 7C11.2239 7 11 7.22386 11 7.5V13.5C11 13.7761 11.2239 14 11.5 14H12.5C12.7761 14 13 13.7761 13 13.5V7.5C13 7.22386 12.7761 7 12.5 7H11.5Z" fill="white"/>
<path d="M11.5 15C11.2239 15 11 15.2239 11 15.5V16.5C11 16.7761 11.2239 17 11.5 17H12.5C12.7761 17 13 16.7761 13 16.5V15.5C13 15.2239 12.7761 15 12.5 15H11.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

+2 -2
View File
@@ -73,9 +73,9 @@ const logout = () => {
<template>
<el-popover trigger="click" popper-class="system-person" :placement="collapse ? 'right' : 'top'">
<template #reference>
<button class="person" :class="collapse && 'collapse'">
<button class="person" :title="name" :class="collapse && 'collapse'">
<img class="default-avatar" :src="Default_avatar" width="32px" height="32px" />
<span v-if="!collapse" class="name ellipsis" :title="name">{{ name }}</span>
<span v-if="!collapse" class="name ellipsis">{{ name }}</span>
</button></template
>
<div class="popover">
+26 -22
View File
@@ -10,6 +10,30 @@ const pwdForm = reactive({
confirm_pwd: '',
})
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const validatePass = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error(t('common.please_input', { msg: t('user.upgrade_pwd.new_pwd') })))
} else {
if (pwdForm.confirm_pwd !== '') {
if (!pwdRef.value) return
pwdRef.value.validateField('confirm_pwd')
}
callback()
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const validatePass2 = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error(t('common.please_input', { msg: t('user.upgrade_pwd.confirm_pwd') })))
} else if (value !== pwdForm.new_pwd) {
callback(new Error(t('user.upgrade_pwd.two_pwd_not_match')))
} else {
callback()
}
}
const rules = {
pwd: [
{
@@ -18,27 +42,8 @@ const rules = {
trigger: 'blur',
},
],
new_pwd: [
{
required: true,
message: t('common.please_input', { msg: t('user.upgrade_pwd.new_pwd') }),
trigger: 'blur',
},
],
confirm_pwd: [
{
required: true,
message: t('common.please_input', { msg: t('user.upgrade_pwd.confirm_pwd') }),
trigger: 'blur',
},
],
}
const initForm = (item: any) => {
if (item) {
Object.assign(pwdForm, { ...item })
}
pwdRef.value.clearValidate()
new_pwd: [{ validator: validatePass, trigger: 'blur' }],
confirm_pwd: [{ validator: validatePass2, trigger: 'blur' }],
}
const emits = defineEmits(['pwdSaved'])
@@ -61,7 +66,6 @@ const submit = () => {
})
}
defineExpose({
initForm,
submit,
})
</script>
+21 -18
View File
@@ -103,25 +103,28 @@ onMounted(async () => {
</template>
</el-input>
<div class="popover-content">
<div
v-for="ele in defaultWorkspaceListWithSearch"
:key="ele.name"
class="popover-item"
:class="currentWorkspace.id === ele.id && 'isActive'"
@click="handleDefaultWorkspaceChange(ele)"
>
<el-icon size="16">
<icon_moments_categories_outlined></icon_moments_categories_outlined>
</el-icon>
<el-scrollbar max-height="400px">
<div
:title="ele.name"
class="datasource-name ellipsis"
v-html="formatKeywords(ele.name)"
></div>
<el-icon size="16" class="done">
<icon_done_outlined></icon_done_outlined>
</el-icon>
</div>
v-for="ele in defaultWorkspaceListWithSearch"
:key="ele.name"
class="popover-item"
:class="currentWorkspace.id === ele.id && 'isActive'"
@click="handleDefaultWorkspaceChange(ele)"
>
<el-icon size="16">
<icon_moments_categories_outlined></icon_moments_categories_outlined>
</el-icon>
<div
:title="ele.name"
class="datasource-name ellipsis"
v-html="formatKeywords(ele.name)"
></div>
<el-icon size="16" class="done">
<icon_done_outlined></icon_done_outlined>
</el-icon>
</div>
</el-scrollbar>
<div v-if="!defaultWorkspaceListWithSearch.length" class="popover-item empty">
{{ $t('model.relevant_results_found') }}
</div>
+14 -2
View File
@@ -6,6 +6,10 @@
"AI Model Configuration": "AI Model Configuration"
},
"common": {
"limited_to_30": "The number of data tables is limited to 30",
"enter_your_password": "Please enter your password",
"your_account_email_address": "Please enter your account/email address",
"the_correct_password": "Please enter the correct password",
"input_content": "Please Input Content",
"empty": " ",
"may_not_exist": "No results yet, the user may not exist",
@@ -118,7 +122,8 @@
"earlier": "Earlier",
"no_time": "No Time",
"rename_conversation_title": "Rename conversation title",
"conversation_title": "Conversation title"
"conversation_title": "Conversation title",
"copied": "Copied"
},
"ds": {
"title": "Data Source",
@@ -224,6 +229,9 @@
"custom_notes": "Custom notes",
"field_type": "Field type",
"field_name": "Field name",
"copy": "Copy",
"incorrect_email_format": "Incorrect email format",
"the_original_one": "Are you sure you want to restore the password to the original one?",
"relevant_content_found": "No relevant content found",
"please_enter": "Please enter",
"Please_select": "Please select",
@@ -458,6 +466,10 @@
"add_interface_credentials": "Add interface credentials",
"edit_interface_credentials": "Edit interface credentials",
"duplicate_name": "Duplicate name",
"duplicate_name_": "Duplicate name",
"duplicate_account": "Duplicate account",
"repeating_parameters": "Repeating Parameters",
"duplicate_email": "Duplicate email",
"interface_credentials": "Interface credentials",
"no_credentials_yet": "No credentials yet",
"intelligent_customer_service": "SQLBot Intelligent Customer Service"
@@ -506,4 +518,4 @@
"back_community": "Restore to Community Edition",
"confirm_tips": "Are you sure to restore to Community Edition?"
}
}
}
+20 -5
View File
@@ -6,6 +6,10 @@
"AI Model Configuration": "模型配置"
},
"common": {
"limited_to_30": "数据表数量限制30",
"enter_your_password": "请输入密码",
"your_account_email_address": "请输入账号/邮箱",
"the_correct_password": "请输入正确的密码",
"input_content": "请输入内容",
"may_not_exist": "暂无结果,用户可能不存在",
"empty": "",
@@ -31,7 +35,8 @@
"re_upload": "重新上传",
"not_exceed_50mb": "支持 XLS、XLSX、CSV 格式,文件大小不超过 50MB",
"reset_password": "重置密码",
"password_reset_successful": "重置密码成功"
"password_reset_successful": "重置密码成功",
"or": "或者"
},
"dashboard": {
"add_success": "添加成功",
@@ -121,7 +126,8 @@
"earlier": "更早以前",
"no_time": "没有时间",
"rename_conversation_title": "重命名对话标题",
"conversation_title": "对话标题"
"conversation_title": "对话标题",
"copied": "已复制"
},
"ds": {
"title": "数据源",
@@ -223,6 +229,9 @@
"edit": "编辑",
"source_connection_failed": "数据源连接失败",
"confirm": "确定",
"copy": "复制",
"the_original_one": "确认恢复为初始密码吗?",
"incorrect_email_format": "邮箱格式不对",
"enabled_status": "启用状态",
"custom_notes": "自定义备注",
"field_type": "字段类型",
@@ -275,7 +284,8 @@
"verification_successful": "校验成功",
"del_default_tip": "无法删除模型: {msg}",
"del_default_warn": "该模型为系统默认模型,请先设置其他模型为系统默认模型,再删除此模型。",
"del_warn_tip": "是否删除模型: {msg}?"
"del_warn_tip": "是否删除模型: {msg}?",
"check_failed": "模型无效【{msg}】"
},
"user": {
"workspace": "工作空间",
@@ -332,7 +342,8 @@
"title": "修改密码",
"old_pwd": "旧密码",
"new_pwd": "新密码",
"confirm_pwd": "确认密码"
"confirm_pwd": "确认密码",
"two_pwd_not_match": "两次输入的密码不一致"
}
},
"workspace": {
@@ -463,6 +474,10 @@
"add_interface_credentials": "添加接口凭证",
"edit_interface_credentials": "编辑接口凭证",
"duplicate_name": "重复名称",
"duplicate_name_": "重复姓名",
"duplicate_account": "重复账号",
"duplicate_email": "重复邮箱",
"repeating_parameters": "重复参数",
"interface_credentials": "接口凭证",
"no_credentials_yet": "暂无凭证",
"intelligent_customer_service": "SQLBot 智能客服",
@@ -518,4 +533,4 @@
"back_community": "还原至社区版",
"confirm_tips": "确定还原至社区版?"
}
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import DashboardPreview from '@//views/dashboard/preview/SQPreviewSingle.vue'
import Dashboard from '@/views/dashboard/index.vue'
import Model from '@/views/system/model/Model.vue'
import Embedded from '@/views/system/embedded/index.vue'
import assistant from '@/views/embedded/assistant.vue'
import assistant from '@/views/embedded/index.vue'
import Member from '@/views/system/member/index.vue'
import Permission from '@/views/system/permission/index.vue'
import User from '@/views/system/user/User.vue'
+1 -1
View File
@@ -19,7 +19,7 @@ export const watchRouter = (router: any) => {
return
}
if (!token) {
ElMessage.error('Please login first')
// ElMessage.error('Please login first')
next('/login')
return
}
+8
View File
@@ -11,6 +11,7 @@ interface AssistantState {
flag: number
type: number
certificate: string
online: boolean
}
export const AssistantStore = defineStore('assistant', {
@@ -21,6 +22,7 @@ export const AssistantStore = defineStore('assistant', {
flag: 0,
type: 0,
certificate: '',
online: false,
}
},
getters: {
@@ -39,6 +41,9 @@ export const AssistantStore = defineStore('assistant', {
getType(): number {
return this.type
},
getOnline(): boolean {
return this.online
},
},
actions: {
setCertificate(certificate: string) {
@@ -61,6 +66,9 @@ export const AssistantStore = defineStore('assistant', {
wsCache.set(flagKey, flag)
}
},
setOnline(online: boolean) {
this.online = !!online
},
async setChat() {
if (!this.assistant) {
return null
+21 -6
View File
@@ -80,6 +80,9 @@ class HttpService {
encodeURIComponent(assistantStore.getCertificate)
)
}
if (!assistantStore.getType) {
config.headers['X-SQLBOT-ASSISTANT-ONLINE'] = assistantStore.getOnline
}
}
const locale = getLocale()
if (locale) {
@@ -168,9 +171,21 @@ class HttpService {
errorMessage = 'Invalid request parameters'
break
case 401:
errorMessage = 'Unauthorized, please login again'
errorMessage = error.response?.data
? error.response.data.toString()
: 'Unauthorized, please login again'
// Redirect to login page if needed
break
ElMessage({
message: errorMessage,
type: 'error',
showClose: true,
})
setTimeout(() => {
wsCache.delete('user.token')
window.location.reload()
}, 1000)
return
// break
case 403:
errorMessage = 'Access denied'
break
@@ -184,10 +199,7 @@ class HttpService {
errorMessage = `Server responded with error: ${error.response.status}`
}
if (error?.response?.data) {
const msgData: any = error.response.data
if (msgData?.msg) {
errorMessage = msgData.msg
}
errorMessage = error.response.data.toString()
}
} else if (error.request) {
errorMessage = 'No response from server'
@@ -249,6 +261,9 @@ class HttpService {
encodeURIComponent(assistantStore.getCertificate)
)
}
if (!assistantStore.getType) {
heads['X-SQLBOT-ASSISTANT-ONLINE'] = assistantStore.getOnline
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -117,14 +117,9 @@ const createNewChat = async () => {
async function doCreateNewChat() {
if (isAssistant.value) {
const assistantChat = await assistantStore.setChat()
if (assistantChat) {
onChatCreated(assistantChat)
}
return
} else {
chatCreatorRef.value?.showDs()
}
chatCreatorRef.value?.showDs()
}
function onClickHistory(chat: Chat) {
@@ -15,6 +15,7 @@ import icon_export_outlined from '@/assets/svg/icon_export_outlined.svg'
import icon_into_item_outlined from '@/assets/svg/icon_into-item_outlined.svg'
import icon_window_max_outlined from '@/assets/svg/icon_window-max_outlined.svg'
import icon_window_mini_outlined from '@/assets/svg/icon_window-mini_outlined.svg'
import icon_copy_outlined from '@/assets/svg/icon_copy_outlined.svg'
import { useI18n } from 'vue-i18n'
import SQLComponent from '@/views/chat/component/SQLComponent.vue'
@@ -189,6 +190,13 @@ function showSql() {
function addToDashboard() {
console.log('todo')
}
function copy() {
if (props.message?.record?.sql) {
navigator.clipboard.writeText(props.message.record.sql)
ElMessage.info(t('qa.copied'))
}
}
</script>
<template>
@@ -209,11 +217,11 @@ function addToDashboard() {
<div class="chart-select-container">
<el-tooltip effect="dark" :content="t('chat.type')" placement="top">
<ChartPopover
:chartTypeList="chartTypeList"
:chartType="chartType"
:title="t('chat.type')"
@typeChange="onTypeChange"
v-if="chartTypeList.length > 0"
:chart-type-list="chartTypeList"
:chart-type="chartType"
:title="t('chat.type')"
@type-change="onTypeChange"
></ChartPopover>
</el-tooltip>
@@ -318,12 +326,17 @@ function addToDashboard() {
direction="rtl"
body-class="chart-sql-drawer-body"
>
<div>
<div class="sql-block">
<SQLComponent
v-if="message.record?.sql"
:sql="message.record?.sql"
style="margin-top: 12px"
/>
<el-button v-if="message.record?.sql" circle class="input-icon" @click="copy">
<el-icon size="16">
<icon_copy_outlined />
</el-icon>
</el-button>
</div>
</el-drawer>
</div>
@@ -490,4 +503,18 @@ function addToDashboard() {
margin-top: 16px;
}
}
.sql-block {
position: relative;
.input-icon {
min-width: unset;
position: absolute;
top: 12px;
right: 12px;
border-color: #dee0e3;
box-shadow: 0px 4px 8px 0px #1f23291a;
}
}
</style>
@@ -17,9 +17,22 @@ const props = defineProps({
},
})
const currentIcon = computed(() => {
return props.chartTypeList.find((ele) => ele.value === props.chartType || ele.value === 'table')
.icon
if (props.chartType === 'table') {
const [ele] = props.chartTypeList || []
if (ele.icon) {
return ele.icon
}
return null
}
return props.chartTypeList.find((ele) => ele.value === props.chartType).icon
})
const firstItem = () => {
if (props.chartType === 'table') {
const [ele] = props.chartTypeList || []
handleDefaultChatChange(ele || {})
}
}
const emits = defineEmits(['typeChange'])
const handleDefaultChatChange = (val: any) => {
emits('typeChange', val.value)
@@ -29,7 +42,11 @@ const handleDefaultChatChange = (val: any) => {
<template>
<el-popover trigger="click" popper-class="chat-type_select" placement="bottom">
<template #reference>
<div class="chat-select_type" :class="chartType && chartType !== 'table' && 'active'">
<div
class="chat-select_type"
:class="chartType && chartType !== 'table' && 'active'"
@click="firstItem"
>
<component :is="currentIcon" />
<el-icon style="transform: scale(0.75)" class="expand" size="16">
<icon_expand_down_filled></icon_expand_down_filled>
@@ -106,7 +106,13 @@ export class Bar extends BaseG2Chart {
},
labels: [
{
text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
text: (data: any) => {
const value = data[y[0].value]
if (value === undefined || value === null) {
return ''
}
return `${value}${_data.isPercent ? '%' : ''}`
},
transform: [
{ type: 'contrastReverse' },
{ type: 'exceedAdjust' },
@@ -95,7 +95,13 @@ export class Column extends BaseG2Chart {
},
labels: [
{
text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
text: (data: any) => {
const value = data[y[0].value]
if (value === undefined || value === null) {
return ''
}
return `${value}${_data.isPercent ? '%' : ''}`
},
position: 'top',
transform: [
{ type: 'contrastReverse' },
@@ -61,7 +61,13 @@ export class Line extends BaseG2Chart {
},
labels: [
{
text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
text: (data: any) => {
const value = data[y[0].value]
if (value === undefined || value === null) {
return ''
}
return `${value}${_data.isPercent ? '%' : ''}`
},
style: {
dx: -10,
dy: -12,
+81 -59
View File
@@ -15,15 +15,16 @@
@on-click-side-bar-btn="hideSideBar"
/>
</el-aside>
<div v-if="!isAssistant && !chatListSideBarShow" class="hidden-sidebar-btn">
<div
v-if="isAssistant || !chatListSideBarShow"
class="hidden-sidebar-btn"
:class="{ 'assistant-popover-sidebar': isAssistant }"
>
<el-popover
:visible="isAssistant ? floatPopoverVisible : null"
:width="280"
placement="bottom-start"
popper-style="padding: 0;
height: 654px;
border: 1px solid rgba(222, 224, 227, 1);
border-radius: 6px;
"
:popper-style="{ ...defaultFloatPopoverStyle }"
>
<template #reference>
<el-button link type="primary" class="icon-btn" @click="showSideBar">
@@ -33,6 +34,7 @@
</el-button>
</template>
<ChatListContainer
ref="floatPopoverRef"
v-model:chat-list="chatList"
v-model:current-chat-id="currentChatId"
v-model:current-chat="currentChat"
@@ -57,7 +59,10 @@
<el-container :loading="loading">
<el-main
class="chat-record-list"
:class="{ 'hide-sidebar': !isAssistant && !chatListSideBarShow }"
:class="{
'hide-sidebar': !isAssistant && !chatListSideBarShow,
'assistant-chat-main': isAssistant,
}"
>
<div v-if="computedMessages.length == 0 && !loading" class="welcome-content-block">
<div class="welcome-content">
@@ -65,11 +70,16 @@
<el-icon size="32">
<logo_fold />
</el-icon>
{{ t('qa.greeting') }}
{{ isAssistant ? t('embedded.i_am_sqlbot') : t('qa.greeting') }}
</div>
<div class="sub">
{{ isAssistant ? t('embedded.predict_data_etc') : t('qa.hint_description') }}
</div>
<div v-if="isAssistant" class="sub assistant-sub">
{{ t('embedded.intelligent_data_query') }}
</div>
<div class="sub">{{ t('qa.hint_description') }}</div>
<el-button
v-if="currentChatId === undefined"
v-if="!isAssistant && currentChatId === undefined"
size="large"
type="primary"
class="greeting-btn"
@@ -255,15 +265,13 @@
</div>
</el-scrollbar>
</el-main>
<el-footer
v-if="computedMessages.length > 0 || (isAssistant && currentChatId)"
class="chat-footer"
>
<el-footer v-if="computedMessages.length > 0 || isAssistant" class="chat-footer">
<div class="input-wrapper" @click="clickInput">
<div class="datasource">
<div v-if="!isAssistant" class="datasource">
<template v-if="currentChat.datasource && currentChat.datasource_name">
{{ t('qa.selected_datasource') }}:
<img
v-if="currentChatEngineType"
style="margin-left: 4px; margin-right: 4px"
:src="currentChatEngineType"
width="16px"
@@ -332,14 +340,21 @@ import icon_start_outlined from '@/assets/svg/icon_start_outlined.svg'
import logo_fold from '@/assets/LOGO-fold.svg'
import logo from '@/assets/LOGO.svg'
import icon_send_filled from '@/assets/svg/icon_send_filled.svg'
import { useAssistantStore } from '@/stores/assistant'
import { onClickOutside } from '@vueuse/core'
const props = defineProps<{
startChatDsId?: number
}>()
const floatPopoverRef = ref()
const floatPopoverVisible = ref(false)
const assistantStore = useAssistantStore()
const defaultFloatPopoverStyle = ref({
padding: '0',
height: '654px',
border: '1px solid rgba(222, 224, 227, 1)',
borderRadius: '6px',
})
const isAssistant = computed(() => assistantStore.getAssistant)
@@ -410,10 +425,8 @@ const createNewChatSimple = async () => {
const createNewChat = async () => {
goEmpty()
if (isAssistant.value) {
const assistantChat = await assistantStore.setChat()
if (assistantChat) {
onChatCreatedQuick(assistantChat as any)
}
currentChat.value = new ChatInfo()
currentChatId.value = undefined
return
}
chatCreatorRef.value?.showDs()
@@ -440,32 +453,9 @@ function onClickHistory(chat: Chat) {
}
const currentChatEngineType = computed(() => {
return (
dsTypeWithImg.find((ele) => currentChat.value.ds_type === ele.type || ele.type === 'excel') ||
{}
).img
return (dsTypeWithImg.find((ele) => currentChat.value.ds_type === ele.type) || {}).img
})
function toAssistantHistory(chat: Chat) {
currentChat.value = new ChatInfo(chat)
if (chat !== undefined && chat.id !== undefined && !loading.value) {
currentChatId.value = chat.id
loading.value = true
chatApi
.get(chat.id)
.then((res) => {
const info = chatApi.toChatInfo(res)
if (info) {
currentChat.value = info
onClickHistory(info)
}
})
.finally(() => {
loading.value = false
})
}
}
function onChatDeleted(id: number) {
console.log('deleted', id)
}
@@ -476,6 +466,10 @@ function onChatRenamed(chat: Chat) {
const chatListSideBarShow = ref<boolean>(true)
function hideSideBar() {
if (isAssistant.value) {
floatPopoverVisible.value = false
return
}
chatListSideBarShow.value = false
}
@@ -542,13 +536,23 @@ function onChatStop() {
isTyping.value = false
console.debug('onChatStop')
}
const assistantPrepareSend = async () => {
if (
isAssistant.value &&
(currentChatId.value == null || typeof currentChatId.value == 'undefined')
) {
const assistantChat = await assistantStore.setChat()
if (assistantChat) {
onChatCreatedQuick(assistantChat as any)
}
}
}
const sendMessage = async () => {
if (!inputMessage.value.trim()) return
loading.value = true
isTyping.value = true
await assistantPrepareSend()
const currentRecord = new ChatRecord()
currentRecord.create_time = new Date()
currentRecord.chat_id = currentChatId.value
@@ -716,13 +720,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
})
}
const getHistoryList = () => {
return chatList.value
}
const getCurrentChatId = () => {
return currentChatId.value
}
const inputRef = ref()
function clickInput() {
@@ -770,12 +767,29 @@ function stop(func?: (...p: any[]) => void, ...param: any[]) {
func(...param)
}
}
const showFloatPopover = () => {
if (isAssistant.value && !floatPopoverVisible.value) {
floatPopoverVisible.value = true
}
}
const assistantPrepareInit = () => {
if (!isAssistant.value) {
return
}
Object.assign(defaultFloatPopoverStyle.value, {
height: '100% !important',
inset: '0px auto auto 0px',
})
goEmpty()
onClickOutside(floatPopoverRef, () => {
if (floatPopoverVisible.value) {
floatPopoverVisible.value = false
}
})
}
defineExpose({
getHistoryList,
toAssistantHistory,
getCurrentChatId,
createNewChat,
showFloatPopover,
})
const hiddenChatCreatorRef = ref()
@@ -793,6 +807,7 @@ function jumpCreatChat() {
onMounted(() => {
getChatList(jumpCreatChat)
assistantPrepareInit()
})
</script>
@@ -802,7 +817,11 @@ onMounted(() => {
position: relative;
border-radius: 12px;
.assistant-popover-sidebar {
button {
display: none;
}
}
.hidden-sidebar-btn {
z-index: 1;
position: absolute;
@@ -841,6 +860,9 @@ onMounted(() => {
border-radius: 12px;
}
}
.assistant-chat-main {
padding: 0 0 20px 0;
}
.chat-scroll {
width: 100%;
+110 -39
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, reactive } from 'vue'
import { datasourceApi } from '@/api/datasource'
import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg'
import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg'
@@ -67,11 +67,32 @@ const currentTable = ref<any>({})
const ds = ref<any>({})
const btnSelect = ref('d')
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const handleSizeChange = (val: number) => {
pageInfo.currentPage = 1
pageInfo.pageSize = val
}
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
}
const fieldListComputed = computed(() => {
const { currentPage, pageSize } = pageInfo
return fieldList.value.slice((currentPage - 1) * pageSize, currentPage * pageSize)
})
const init = () => {
initLoading.value = true
datasourceApi.getDs(props.info.id).then((res) => {
ds.value = res
fieldList.value = []
pageInfo.total = 0
pageInfo.currentPage = 1
datasourceApi.tableList(props.info.id).then((res) => {
tableList.value = res
initLoading.value = false
@@ -100,10 +121,15 @@ const handleSelectTableList = () => {
const clickTable = (table: any) => {
loading.value = true
currentTable.value = table
fieldList.value = []
pageInfo.total = 0
previewData.value = []
datasourceApi
.fieldList(table.id)
.then((res) => {
fieldList.value = res
pageInfo.total = res.length
pageInfo.currentPage = 1
datasourceApi.previewData(props.info.id, buildData()).then((res) => {
previewData.value = res
})
@@ -191,6 +217,8 @@ const btnSelectClick = (val: any) => {
.fieldList(currentTable.value.id)
.then((res) => {
fieldList.value = res
pageInfo.total = res.length
pageInfo.currentPage = 1
})
.finally(() => {
loading.value = false
@@ -307,43 +335,72 @@ const btnSelectClick = (val: any) => {
</el-button>
</div>
<div class="preview-or-schema">
<el-table v-if="btnSelect === 'd'" :data="fieldList" style="width: 100%">
<el-table-column prop="field_name" :label="t('datasource.field_name')" width="180" />
<el-table-column prop="field_type" :label="t('datasource.field_type')" width="180" />
<el-table-column prop="field_comment" :label="t('datasource.field_original_notes')" />
<el-table-column :label="t('datasource.field_notes_1')">
<template #default="scope">
<div class="field-comment">
<span :title="scope.row.custom_comment" class="notes-in_table">{{
scope.row.custom_comment
}}</span>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editField(scope.row)">
<edit></edit>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column :label="t('datasource.enabled_status')" width="180">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-switch
v-model="scope.row.checked"
size="small"
@change="changeStatus(scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
<template v-else>
<div
v-if="!loading"
class="preview-or-schema"
:class="btnSelect === 'q' && 'overflow-preview'"
>
<div v-if="btnSelect === 'd'" class="table-content_preview">
<el-table :data="fieldListComputed" style="width: 100%">
<el-table-column
prop="field_name"
:label="t('datasource.field_name')"
width="180"
/>
<el-table-column
prop="field_type"
:label="t('datasource.field_type')"
width="180"
/>
<el-table-column
prop="field_comment"
:label="t('datasource.field_original_notes')"
/>
<el-table-column :label="t('datasource.field_notes_1')">
<template #default="scope">
<div class="field-comment">
<span :title="scope.row.custom_comment" class="notes-in_table">{{
scope.row.custom_comment
}}</span>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editField(scope.row)">
<edit></edit>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column :label="t('datasource.enabled_status')" width="180">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-switch
v-model="scope.row.checked"
size="small"
@change="changeStatus(scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="fieldList.length && btnSelect === 'd'" class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<template v-if="btnSelect === 'q'">
<div class="preview-num">
{{ t('ds.pieces_in_total', { msg: total, ms: showNum }) }}
</div>
@@ -588,7 +645,21 @@ const btnSelectClick = (val: any) => {
.preview-or-schema {
margin-top: 16px;
height: calc(100% - 50px);
overflow-y: auto;
&.overflow-preview {
overflow-y: auto;
}
.table-content_preview {
max-height: calc(100% - 50px);
overflow-y: auto;
margin-bottom: 16px;
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
.field-comment {
display: flex;
+9 -6
View File
@@ -233,7 +233,6 @@ const save = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
saveLoading.value = true
const list = tableList.value
.filter((ele: any) => {
return checkList.value.includes(ele.tableName)
@@ -242,6 +241,12 @@ const save = async (formEl: FormInstance | undefined) => {
return { table_name: ele.tableName, table_comment: ele.tableComment }
})
if (checkList.value.length > 30) {
ElMessage.error(t('common.limited_to_30'))
return
}
saveLoading.value = true
const requestObj = buildConf()
if (form.value.id) {
if (!isEditTable.value) {
@@ -278,8 +283,6 @@ const save = async (formEl: FormInstance | undefined) => {
.finally(() => {
saveLoading.value = false
})
console.log(a, 'datasourceApi')
}
}
})
@@ -510,12 +513,12 @@ defineExpose({
<div class="name">{{ form.filename }}</div>
<div class="size">{{ form.filename.split('.')[1] }} - {{ fileSize }}</div>
</div>
<el-icon class="action-btn" size="16" @click="clearFile">
<el-icon v-if="!form.id" class="action-btn" size="16" @click="clearFile">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</div>
<el-upload
v-if="form.filename"
v-if="form.filename && !form.id"
class="upload-user"
accept=".xlsx,.xls,.csv"
:headers="headers"
@@ -531,7 +534,7 @@ defineExpose({
</el-button>
</el-upload>
<el-upload
v-else
v-else-if="!form.id"
class="upload-user"
accept=".xlsx,.xls,.csv"
:headers="headers"
+92 -166
View File
@@ -1,69 +1,30 @@
<template>
<div class="sqlbot-assistant-container">
<div class="head">
<div class="flex align-center">
<div class="head-img-div">
<img :src="AssistantGif" />
</div>
<h4>SQLBot 小助手</h4>
</div>
<div class="header">
<el-icon size="20" @click="openHistory">
<icon_sidebar_outlined></icon_sidebar_outlined>
</el-icon>
<img :src="LOGO" class="logo" width="30px" height="30px" alt="" />
<span class="tite">{{ $t('embedded.intelligent_customer_service') }}</span>
<el-tooltip effect="dark" :content="$t('embedded.new_conversation')" placement="top">
<el-icon class="new-chat" size="20" @click="createChat">
<icon_new_chat_outlined></icon_new_chat_outlined>
</el-icon>
</el-tooltip>
</div>
<chat-component v-if="!loading" ref="chatRef" class="sqlbot-chat-container" />
</div>
<div class="sqlbot-top-btn">
<el-icon style="cursor: pointer" @click="createChat">
<icon_new_chat_outlined />
</el-icon>
<el-icon style="cursor: pointer" @click="openHistory">
<history></history>
</el-icon>
</div>
<div class="sqlbot-history-container">
<el-drawer
v-model="drawer"
:append-to-body="false"
class="sqlbot-history-drawer"
modal-class="sqlbot-history-modal"
title=""
direction="ttb"
:with-header="false"
>
<div class="sqlbot-history-title">
<span>历史记录</span>
</div>
<div class="sqlbot-history-content">
<div v-if="!chatList.length" class="history-empty">
<span>暂无历史记录</span>
</div>
<div v-else class="sqlbot-chat-list-inner">
<el-scrollbar max-height="350px" class="sqlbot-chat-scroller">
<template v-for="chat in chatList" :key="chat.id">
<div
class="chat-list-item"
:class="{ 'is-active-chat': currentId === chat.id }"
@click="onClickHistory(chat)"
>
<span class="title">{{ chat.brief ?? 'Untitled' }}</span>
<!-- <div class="history-operate">
<el-icon @click="EditPen(chat)"><IconOpeEdit /></el-icon>
<el-icon @click="Delete(chat)"><IconOpeDelete /></el-icon>
</div> -->
</div>
</template>
</el-scrollbar>
</div>
</div>
</el-drawer>
<div class="sqlbot-chat-container">
<chat-component v-if="!loading" ref="chatRef" />
</div>
<div class="drawer-assistant" @click="openHistory"></div>
</div>
</template>
<script setup lang="ts">
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import ChatComponent from '@/views/chat/index.vue'
import AssistantGif from '@/assets/img/assistant.gif'
import history from '@/assets/svg/chart/history.svg'
import LOGO from '@/assets/embedded/LOGO.png'
import icon_sidebar_outlined from '@/assets/embedded/icon_sidebar_outlined.svg'
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
/* import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg' */
import { useRoute } from 'vue-router'
import { assistantApi } from '@/api/assistant'
import { useAssistantStore } from '@/stores/assistant'
@@ -72,28 +33,14 @@ const assistantStore = useAssistantStore()
const route = useRoute()
const chatRef = ref()
const chatList = ref<Array<any>>([])
const drawer = ref(false)
const currentId = ref()
const createChat = () => {
chatRef.value?.createNewChat()
}
const openHistory = () => {
chatList.value = chatRef.value?.getHistoryList()
currentId.value = chatRef.value?.getCurrentChatId()
drawer.value = true
}
const onClickHistory = (chat: any) => {
chatRef.value?.toAssistantHistory(chat)
chatRef.value?.showFloatPopover()
}
/* const EditPen = (chat: any) => {
chatRef.value?.onChatRenamed(chat)
}
const Delete = (chat: any) => {
chatRef.value?.onChatDeleted(chat.id)
} */
const validator = ref({
id: '',
valid: false,
@@ -111,14 +58,31 @@ const communicationCb = async (event: any) => {
const certificate = event.data['certificate']
assistantStore.setType(1)
assistantStore.setCertificate(certificate)
// store certificate to pinia
}
if (event.data?.busi == 'setOnline') {
setFormatOnline(event.data.online)
}
}
}
const setFormatOnline = (text?: any) => {
if (text === null || typeof text === 'undefined') {
assistantStore.setOnline(false)
return
}
if (typeof text === 'boolean') {
assistantStore.setOnline(text)
return
}
if (typeof text === 'string') {
assistantStore.setOnline(text.toLowerCase() === 'true')
return
}
assistantStore.setOnline(false)
}
onBeforeMount(async () => {
const assistantId = route.query.id
const online = route.query.online
console.log(online)
setFormatOnline(online)
const now = Date.now()
assistantStore.setFlag(now)
const param = {
@@ -151,31 +115,58 @@ onBeforeUnmount(() => {
height: 100%;
width: 100%;
color: rgb(31, 35, 41);
.flex {
display: flex;
}
.head {
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);
position: fixed;
.header {
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: 56px;
line-height: 56px;
box-sizing: border-box;
border-bottom: 1px solid #dee0e3;
.align-center {
align-items: center;
.head-img-div {
display: flex;
margin-left: 24px;
img {
width: 32px;
height: 32px;
background: #1cba9014;
height: 56px;
padding: 0 16px;
display: flex;
align-items: center;
position: fixed;
.logo {
margin: 0 8px 0 16px;
}
.title {
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.ed-icon {
position: relative;
cursor: pointer;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 28px;
height: 28px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
.new-chat {
position: absolute;
right: 88px;
top: 17px;
}
}
.sqlbot-chat-container {
padding-top: 56px;
@@ -183,82 +174,17 @@ onBeforeUnmount(() => {
display: none;
}
}
}
.sqlbot-top-btn {
right: 85px;
z-index: 2009;
position: fixed;
top: 16px;
display: flex;
column-gap: 16px;
// right: 16px;
font-size: 22px;
i:first-child {
color: var(--primary-color);
}
}
.sqlbot-history-container {
width: 100%;
:deep(.sqlbot-history-modal) {
top: 56px !important;
}
:deep(.sqlbot-history-title) {
border-bottom: 1px solid #dee0e3;
padding: 16px 24px;
font-size: 14px;
font-weight: 500;
color: #1f2329;
}
:deep(.ed-drawer__body) {
padding: 0 !important;
}
:deep(.sqlbot-history-drawer) {
height: fit-content !important;
.sqlbot-history-content {
padding: 16px;
.history-empty {
height: 60px;
color: #8f959e !important;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.is-active-chat {
background-color: #ebf1ff;
color: #3370ff;
font-weight: 500;
}
.chat-list-item {
display: flex;
align-items: center;
padding: 10px 8px;
font-size: 14px;
color: #1f2329;
justify-content: space-between;
.history-operate {
display: none;
}
&:hover {
cursor: pointer;
border-radius: 4px;
background: rgba(31, 35, 41, 0.1);
.history-operate {
padding-right: 16px;
display: flex;
column-gap: 16px;
align-items: center;
font-size: 18px;
i {
color: #8f959e;
&:hover {
color: #8f959e99;
}
}
}
}
}
}
.drawer-assistant {
height: 100%;
position: absolute;
left: 0;
top: 0;
width: 50%;
background: #f5f6f7;
box-shadow: 0px 6px 24px 0px #1f232914;
padding: 16px;
border-right: 1px solid #dee0e3;
display: none;
}
}
</style>
+28 -9
View File
@@ -7,21 +7,26 @@
<div class="login-right">
<div class="login-form">
<h2 class="title">Login</h2>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" @keyup.enter="submitForm">
<el-form
ref="loginFormRef"
class="form-content_error"
:model="loginForm"
:rules="rules"
@keyup.enter="submitForm"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="Account"
:prefix-icon="User"
:placeholder="$t('common.your_account_email_address')"
size="large"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
placeholder="Password"
:placeholder="$t('common.enter_your_password')"
type="password"
:prefix-icon="Lock"
show-password
size="large"
></el-input>
</el-form-item>
@@ -39,10 +44,11 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { User, Lock } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const userStore = useUserStore()
const { t } = useI18n()
const loginForm = ref({
username: '',
@@ -50,8 +56,8 @@ const loginForm = ref({
})
const rules = {
username: [{ required: true, message: 'Please input account', trigger: 'blur' }],
password: [{ required: true, message: 'Please input password', trigger: 'blur' }],
username: [{ required: true, message: t('common.your_account_email_address'), trigger: 'blur' }],
password: [{ required: true, message: t('common.the_correct_password'), trigger: 'blur' }],
}
const loginFormRef = ref()
@@ -108,10 +114,24 @@ const submitForm = () => {
padding: 40px;
display: flex;
align-items: center;
position: relative;
height: 361px;
.login-form {
width: 100%;
padding: 0 40px;
position: absolute;
top: 40px;
left: 0;
.form-content_error {
.ed-form-item--default {
margin-bottom: 24px;
&.is-error {
margin-bottom: 48px;
}
}
}
.title {
font-size: 28px;
@@ -125,7 +145,6 @@ const submitForm = () => {
height: 45px;
font-size: 16px;
border-radius: 4px;
margin-top: 20px;
}
.agreement {
+2 -1
View File
@@ -38,7 +38,7 @@ const handleEmbedded = () => {
<div class="card">
<div class="name-icon">
<img :src="Lock" width="32px" height="32px" />
<span class="name">{{ name }}</span>
<span class="name ellipsis" :title="name">{{ name }}</span>
<span class="default" :class="isBase && 'is-base'">{{
isBase ? $t('embedded.basic_application') : $t('embedded.advanced_application')
}}</span>
@@ -88,6 +88,7 @@ const handleEmbedded = () => {
font-weight: 500;
font-size: 16px;
line-height: 24px;
max-width: 200px;
}
.default {
+49 -11
View File
@@ -14,8 +14,7 @@ import icon_copy_outlined from '@/assets/embedded/icon_copy_outlined.svg'
import Card from './Card.vue'
import { workspaceList } from '@/api/workspace'
import DsCard from './DsCard.vue'
import { datasourceApi } from '@/api/datasource'
import { getList, updateAssistant, saveAssistant, delOne } from '@/api/embedded'
import { getList, updateAssistant, saveAssistant, delOne, dsApi } from '@/api/embedded'
import { useI18n } from 'vue-i18n'
import { cloneDeep } from 'lodash-es'
@@ -49,7 +48,7 @@ const currentEmbedded = reactive<any>(cloneDeep(defaultEmbedded))
const isCreate = ref(false)
const defaultForm = {
oid: '',
oid: 1,
private_list: [] as any,
}
const dsForm = reactive(cloneDeep(defaultForm))
@@ -107,9 +106,13 @@ const handleAddEmbedded = (val: any) => {
handleAdvancedEmbedded(null)
}
}
const wsChanged = (val: any) => {
dsForm.private_list = []
dsForm.oid = val
getDsList()
}
const getDsList = () => {
datasourceApi.list().then((res: any) => {
dsApi(dsForm.oid).then((res: any) => {
dsListOptions.value = res || []
if (!currentEmbedded.id) {
dsForm.private_list = dsListOptions.value.map((ele) => ele.id)
@@ -119,10 +122,10 @@ const getDsList = () => {
const handleBaseEmbedded = (row: any) => {
advancedApplication.value = false
initWorkspace()
getDsList()
if (row) {
Object.assign(dsForm, JSON.parse(row.configuration))
}
getDsList()
ruleConfigvVisible.value = true
dialogTitle.value = row?.id
? t('embedded.edit_basic_applications')
@@ -350,16 +353,36 @@ const btnSelect = ref('d')
const dialogVisible = ref(false)
const scriptElement = ref('')
const jsCodeElement = ref('')
const handleEmbedded = (row: any) => {
dialogVisible.value = true
const { origin, pathname } = window.location
scriptElement.value = `g-#script
async
defer
id="${row.id}"
id="sqlbot-assistant-float-script-${row.id}"
src="${origin + pathname}assistant.js?id=${row.id}"k-*g-#/scriptk-*`
.replaceAll('g-#', '<')
.replaceAll('k-*', '>')
jsCodeElement.value = `(function(){
const script = document.createElement('script');
script.defer = true;
script.async = true;
script.src = "${origin + pathname}assistant.js?id=${row.id}";
script.id = "sqlbot-assistant-float-script-${row.id}";
document.head.appendChild(script);
})()`
}
const copyJsCode = () => {
navigator.clipboard
.writeText(jsCodeElement.value)
.then(function () {
ElMessage.success(t('embedded.copy_successful'))
})
.catch(function () {
ElMessage.error(t('embedded.copy_successful'))
})
}
const copyCode = () => {
navigator.clipboard
@@ -417,15 +440,15 @@ const saveHandler = () => {
</script>
<template>
<div v-loading="searchLoading" class="permission">
<div v-loading="searchLoading" class="embedded">
<div class="tool-left">
<div class="btn-select">
<el-button :class="[btnSelect === 'd' && 'is-active']" text @click="btnSelect = 'd'">
{{ t('embedded.embedded_assistant') }}
</el-button>
<el-button :class="[btnSelect === 'q' && 'is-active']" text @click="btnSelect = 'q'">
<!-- <el-button :class="[btnSelect === 'q' && 'is-active']" text @click="btnSelect = 'q'">
{{ t('embedded.embedded_page') }}
</el-button>
</el-button> -->
</div>
<div>
<el-input
@@ -697,6 +720,7 @@ const saveHandler = () => {
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.workspace')
"
@change="wsChanged"
>
<el-option
v-for="item in workspaces"
@@ -770,6 +794,19 @@ const saveHandler = () => {
{{ scriptElement }}
</div>
</div>
<div class="code">
<div class="copy">
{{ $t('common.or') }}
<el-icon size="16" @click="copyJsCode">
<icon_copy_outlined></icon_copy_outlined>
</el-icon>
</div>
<div class="script">
{{ jsCodeElement }}
</div>
</div>
</div>
</el-dialog>
<el-drawer
@@ -857,7 +894,8 @@ const saveHandler = () => {
</template>
<style lang="less" scoped>
.permission {
.embedded {
height: 100%;
.ed-empty {
padding-top: 200px;
padding-bottom: 0;
+43 -19
View File
@@ -69,6 +69,48 @@ const defaultModelListWithSearch = computed(() => {
})
})
const modelCheckHandler = (item: any) => {
setTimeout(() => {
modelApi.check(item).catch((err: any) => {
if (err.response?.data?.msg) {
ElMessage.error(t('model.check_failed', { msg: err.response.data.msg || '' }))
} else {
ElMessage.error(t('model.check_failed', { msg: err.message || '' }))
}
})
}, 1000)
}
const duplicateName = async (item: any) => {
const res = await modelApi.queryAll()
const names = res.filter((ele: any) => ele.id !== item.id).map((ele: any) => ele.name)
if (names.includes(item.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
if (!item.id) {
modelApi.add(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('workspace.add_successfully'),
})
modelCheckHandler(item)
})
return
}
modelApi.edit(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
modelCheckHandler(item)
})
}
const handleDefaultModelChange = (item: any) => {
const current_default_node = modelList.value.find((ele: Model) => ele.default_model)
if (current_default_node?.id === item.id) {
@@ -185,25 +227,7 @@ const search = () => {
search()
const submit = (item: any) => {
if (!item.id) {
modelApi.add(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('workspace.add_successfully'),
})
})
return
}
modelApi.edit(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
duplicateName(item)
}
</script>
+28 -4
View File
@@ -76,7 +76,7 @@ const advancedSettingPagination = computed(() => {
return advancedSetting.value.slice(currentPage.value * 5 - 5, currentPage.value * 5)
})
const handelCurrentChange = (val: any) => {
const handleCurrentChange = (val: any) => {
currentPage.value = val
}
@@ -116,9 +116,22 @@ const addParams = () => {
paramsFormRef.value.submit()
}
const submit = (item: any) => {
const duplicateName = async (item: any) => {
const arr = advancedSetting.value.filter((ele: any) => ele.id !== item.id)
const names = arr.map((ele: any) => ele.name)
const keys = arr.map((ele: any) => ele.key)
if (names.includes(item.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
if (keys.includes(item.key)) {
ElMessage.error(t('embedded.repeating_parameters'))
return
}
if (isCreate.value) {
advancedSetting.value.push({ ...item })
advancedSetting.value.push({ ...item, id: +new Date() })
beforeClose()
return
}
@@ -132,6 +145,10 @@ const submit = (item: any) => {
beforeClose()
}
const submit = (item: any) => {
duplicateName(item)
}
const beforeClose = () => {
paramsFormRef.value.close()
paramsFormDrawer.value = false
@@ -142,6 +159,7 @@ const supplierChang = (supplier: any) => {
modelForm.api_domain = config.api_domain
modelForm.base_model = ''
}
let curId = +new Date()
const initForm = (item?: any) => {
modelForm.id = ''
modelRef.value.clearValidate()
@@ -149,6 +167,12 @@ const initForm = (item?: any) => {
Object.assign(modelForm, { ...item })
if (item?.config_list?.length) {
advancedSetting.value = item.config_list
advancedSetting.value.forEach((ele: any) => {
if (!ele.id) {
ele.id = curId
curId += 1
}
})
}
}
}
@@ -294,7 +318,7 @@ defineExpose({
:default-page-size="5"
layout="prev, pager, next"
:total="advancedSetting.length"
@current-change="handelCurrentChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
@@ -153,7 +153,7 @@ const emits = defineEmits(['update:item', 'del'])
</el-option>
</el-select>
<el-select
v-if="['null', 'empty'].includes(item.term)"
v-if="['null', 'not_null', 'empty', 'not_empty'].includes(item.term)"
v-model="item.term"
style="max-width: 280px; margin-left: 8px"
:placeholder="t('datasource.Please_select')"
+10 -1
View File
@@ -219,7 +219,9 @@ const handleInitDsIdChange = (val: any) => {
activeTable.value = null
fieldListOptions.value = []
columnForm.permissions = []
authTreeRef.value.init({})
if (authTreeRef.value) {
authTreeRef.value.init({})
}
})
}
@@ -375,6 +377,13 @@ const closeForm = () => {
const authTreeRef = ref()
const saveHandler = () => {
columnFormRef.value.validate((res: any) => {
const names = currentPermission.permissions
.filter((ele: any) => ele.id !== columnForm.id)
.map((ele: any) => ele.name)
if (names.includes(columnForm.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
if (res) {
if (columnForm.type === 'row') {
authTreeRef.value.submit()
+188 -56
View File
@@ -23,12 +23,12 @@
</template>
{{ $t('user.filter') }}
</el-button>
<el-button secondary @click="handleUserImport">
<!-- <el-button secondary @click="handleUserImport">
<template #icon>
<ccmUpload></ccmUpload>
</template>
{{ $t('user.batch_import') }}
</el-button>
</el-button> -->
<el-button type="primary" @click="editHandler(null)">
<template #icon>
<icon_add_outlined></icon_add_outlined>
@@ -110,10 +110,57 @@
:content="$t('common.reset_password')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="handleEditPassword(scope.row.id)">
<el-icon
:ref="
(el: any) => {
setButtonRef(el, scope.row)
}
"
v-click-outside="() => onClickOutside(scope.row)"
class="action-btn"
size="16"
>
<IconLock></IconLock>
</el-icon>
</el-tooltip>
<el-popover
:ref="
(el: any) => {
setPopoverRef(el, scope.row)
}
"
placement="right"
virtual-triggering
:width="300"
:virtual-ref="scope.row.buttonRef"
trigger="click"
show-arrow
>
<div class="reset-pwd-confirm">
<div class="confirm-header">
<span class="icon-span">
<el-icon size="24">
<icon_warning_filled class="svg-icon" />
</el-icon>
</span>
<span class="header-span">{{ t('datasource.the_original_one') }}</span>
</div>
<div class="confirm-content">
<span>SQLBot@123456</span>
<el-button style="margin-left: 4px" text @click="copyText">{{
t('datasource.copy')
}}</el-button>
</div>
<div class="confirm-foot">
<el-button secondary @click="closeResetInfo(scope.row)">{{
t('common.cancel')
}}</el-button>
<el-button type="primary" @click="handleEditPassword(scope.row.id)">
{{ t('datasource.confirm') }}
</el-button>
</div>
</div>
</el-popover>
<el-tooltip
:offset="14"
@@ -227,7 +274,11 @@
multiple
:placeholder="$t('datasource.Please_select') + $t('common.empty') + $t('user.workspace')"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id">
<div class="ellipsis" :title="item.name" style="max-width: 500px; padding-right: 30px">
{{ item.name }}
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('user.user_status')">
@@ -295,7 +346,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, unref, reactive, onMounted, nextTick } from 'vue'
import UserImport from './UserImport.vue'
import SuccessFilled from '@/assets/svg/gou_icon.svg'
import CircleCloseFilled from '@/assets/svg/icon_ban_filled.svg'
@@ -308,11 +359,13 @@ import IconLock from '@/assets/svg/icon-key_outlined.svg'
import IconOpeEdit from '@/assets/svg/icon_edit_outlined.svg'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import iconFilter from '@/assets/svg/icon-filter_outlined.svg'
import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg'
// import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { userApi } from '@/api/user'
import { workspaceList } from '@/api/workspace'
import { formatTimestamp } from '@/utils/date'
import { ClickOutside as vClickOutside } from 'element-plus-secondary'
import icon_warning_filled from '@/assets/svg/icon_warning_filled.svg'
const { t } = useI18n()
const keyword = ref('')
@@ -404,6 +457,12 @@ const rules = {
message: t('datasource.please_enter') + t('common.empty') + t('user.email'),
trigger: 'blur',
},
{
required: true,
pattern: /^[a-zA-Z0-9_._-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
message: t('datasource.incorrect_email_format'),
trigger: 'blur',
},
],
}
@@ -424,6 +483,34 @@ const passwordRules = {
],
}
const closeResetInfo = (row: any) => {
row.popoverRef?.hide()
row.resetInfoShow = false
}
const setPopoverRef = (el: any, row: any) => {
row.popoverRef = el
}
const copyText = () => {
navigator.clipboard
.writeText('SQLBot@123456')
.then(function () {
ElMessage.success(t('embedded.copy_successful'))
})
.catch(function () {
ElMessage.error(t('embedded.copy_successful'))
})
}
const setButtonRef = (el: any, row: any) => {
row.buttonRef = el
}
const onClickOutside = (row: any) => {
if (row.popoverRef) {
unref(row.popoverRef).popperRef?.delayHide?.()
}
}
const multipleTableRef = ref()
const multipleSelectionAll = ref<any[]>([])
const dialogTitle = ref('')
@@ -447,9 +534,9 @@ const handleEditPassword = (id: any) => {
})
}
const handleUserImport = () => {
userImportRef.value.showDialog()
}
// const handleUserImport = () => {
// userImportRef.value.showDialog()
// }
const handleConfirmPassword = () => {
passwordRef.value.validate((val: any) => {
@@ -533,8 +620,13 @@ const editHandler = (row: any) => {
}
const statusHandler = (row: any) => {
state.form = { ...row }
editTerm()
/* state.form = { ...row }
editTerm() */
const param = {
id: row.id,
status: row.status,
}
userApi.status(param)
}
const cancelDelete = () => {
@@ -569,30 +661,14 @@ const deleteHandler = (row: any) => {
customClass: 'confirm-no_icon',
autofocus: false,
}).then(() => {
multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => ele.id !== row.id)
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
userApi.delete(row.id).then(() => {
multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => ele.id !== row.id)
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
search()
})
search()
// userApi.delete(row.id).then(() => {
// multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => ele.id !== row.id)
// ElMessage({
// type: 'success',
// message: t('dashboard.delete_success'),
// })
// search()
// })
})
}
/* 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] = ''
})
}
@@ -600,7 +676,7 @@ const closeForm = () => {
dialogFormVisible.value = false
}
const onFormClose = () => {
resetForm()
state.form = { ...defaultForm }
dialogFormVisible.value = false
}
@@ -639,22 +715,8 @@ const search = () => {
})
}
const addTerm = () => {
userApi
.add(state.form)
.then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
.finally(() => {
state.form = { ...defaultForm }
})
}
const editTerm = () => {
userApi.edit(state.form).then(() => {
const { account, email, name, oid, status } = state.form
userApi.add({ account, email, name, oid, status }).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
@@ -663,14 +725,33 @@ const editTerm = () => {
})
})
}
const editTerm = () => {
const { account, id, create_time, email, language, name, oid, oid_list, origin, status } =
state.form
userApi
.edit({ account, id, create_time, email, language, name, oid, oid_list, origin, status })
.then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
}
const duplicateName = () => {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
}
const saveHandler = () => {
termFormRef.value.validate((res: any) => {
if (res) {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
duplicateName()
}
})
}
@@ -839,3 +920,54 @@ onMounted(() => {
}
}
</style>
<style lang="less">
.reset-pwd-confirm {
padding: 5px 15px;
.confirm-header {
width: 100%;
min-height: 40px;
line-height: 40px;
display: flex;
flex-direction: row;
.icon-span {
color: var(--ed-color-warning);
font-size: 22px;
i {
top: 3px;
}
}
.header-span {
font-size: 16px;
font-weight: bold;
margin-left: 10px;
white-space: pre-wrap;
word-break: keep-all;
}
}
.confirm-foot {
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
.ed-button {
min-width: 48px;
height: 28px;
line-height: 28px;
font-size: 12px;
}
}
.confirm-warning {
font-size: 12px;
color: var(--ed-color-danger);
margin-left: 33px;
}
.confirm-content {
margin-left: 33px;
display: flex;
align-items: center;
}
}
</style>
+21 -9
View File
@@ -97,6 +97,26 @@ const init = () => {
}
})
}
const duplicateName = async () => {
const res = await workspaceList()
const names = res.filter((ele: any) => ele.id !== workspaceForm.id).map((ele: any) => ele.name)
if (names.includes(workspaceForm.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
const req = workspaceForm.id ? workspaceUpdate : workspaceCreate
req(workspaceForm).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
init()
fieldDialog.value = false
})
}
onMounted(() => {
init()
})
@@ -224,15 +244,7 @@ const closeField = () => {
const saveField = () => {
workspaceFormRef.value.validate((res: any) => {
if (res) {
const req = workspaceForm.id ? workspaceUpdate : workspaceCreate
req(workspaceForm).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
init()
fieldDialog.value = false
})
duplicateName()
}
})
}