mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -56,6 +56,15 @@ TBD
|
||||
- Backend:[Python / Django](https://www.djangoproject.com/)
|
||||
- Database:[PostgreSQL + pgvector](https://www.postgresql.org/)
|
||||
|
||||
## 飞致云的其他明星项目
|
||||
|
||||
- [DataEase](https://github.com/dataease/dataease/) - 人人可用的开源 BI 工具
|
||||
- [1Panel](https://github.com/1panel-dev/1panel/) - 现代化、开源的 Linux 服务器运维管理面板
|
||||
- [MaxKB](https://github.com/1panel-dev/MaxKB/) - 基于 LLM 大语言模型的开源知识库问答系统
|
||||
- [JumpServer](https://github.com/jumpserver/jumpserver/) - 广受欢迎的开源堡垒机
|
||||
- [Halo](https://github.com/halo-dev/halo/) - 强大易用的开源建站工具
|
||||
- [MeterSphere](https://github.com/metersphere/metersphere/) - 新一代的开源持续测试工具
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#1Panel-dev/SQLBot&Date)
|
||||
|
||||
@@ -536,8 +536,8 @@ class LLMService:
|
||||
).first()
|
||||
if obj is not None:
|
||||
res.append(transRecord2DTO(self.session, permission))
|
||||
wheres = transFilterTree(self.session, res, self.ds)
|
||||
filters.append({"table": table.table_name, "filter": wheres})
|
||||
where_str = transFilterTree(self.session, res, self.ds)
|
||||
filters.append({"table": table.table_name, "filter": where_str})
|
||||
|
||||
filter = json.dumps(filters, ensure_ascii=False)
|
||||
|
||||
@@ -662,6 +662,16 @@ class LLMService:
|
||||
if data['type'] and data['type'] != 'error':
|
||||
# todo type check
|
||||
chart = data
|
||||
if chart.get('columns'):
|
||||
for v in chart.get('columns'):
|
||||
v['value'] = v.get('value').lower()
|
||||
if chart.get('axis'):
|
||||
if chart.get('axis').get('x'):
|
||||
chart.get('axis').get('x')['value'] = chart.get('axis').get('x').get('value').lower()
|
||||
if chart.get('axis').get('y'):
|
||||
chart.get('axis').get('y')['value'] = chart.get('axis').get('y').get('value').lower()
|
||||
if chart.get('axis').get('series'):
|
||||
chart.get('axis').get('series')['value'] = chart.get('axis').get('series').get('value').lower()
|
||||
elif data['type'] == 'error':
|
||||
message = data['reason']
|
||||
error = True
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi import APIRouter, File, UploadFile, HTTPException
|
||||
|
||||
from apps.db.engine import create_table, get_data_engine, insert_data
|
||||
from common.core.deps import SessionDep, CurrentUser, Trans
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
from ..crud.datasource import get_datasource_list, check_status, create_ds, update_ds, delete_ds, getTables, getFields, \
|
||||
execSql, update_table_and_fields, getTablesByDs, chooseTables, preview, updateTable, updateField, get_ds, fieldEnum
|
||||
from ..crud.field import get_fields_by_table_id
|
||||
@@ -17,6 +18,11 @@ from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, Core
|
||||
router = APIRouter(tags=["datasource"], prefix="/datasource")
|
||||
path = "/opt/sqlbot/data/excel"
|
||||
|
||||
@router.get("/ws/{oid}", include_in_schema=False)
|
||||
async def query_by_oid(session: SessionDep, user: CurrentUser, oid: int) -> List[CoreDatasource]:
|
||||
if not user.isAdmin:
|
||||
raise Exception("no permission to execute")
|
||||
return get_datasource_list(session=session, user=user, oid=oid)
|
||||
|
||||
@router.get("/list")
|
||||
async def datasource_list(session: SessionDep, user: CurrentUser):
|
||||
@@ -60,7 +66,11 @@ async def get_tables(session: SessionDep, id: int):
|
||||
|
||||
@router.post("/getTablesByConf")
|
||||
async def get_tables_by_conf(session: SessionDep, ds: CoreDatasource):
|
||||
return getTablesByDs(session, ds)
|
||||
try:
|
||||
return getTablesByDs(session, ds)
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.error(f"get table failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f'Get table Failed: {e.args}')
|
||||
|
||||
|
||||
@router.post("/getFields/{id}/{table_name}")
|
||||
@@ -100,7 +110,11 @@ async def edit_field(session: SessionDep, field: CoreField):
|
||||
|
||||
@router.post("/previewData/{id}")
|
||||
async def edit_local(session: SessionDep, current_user: CurrentUser, id: int, data: TableObj):
|
||||
return preview(session, current_user, id, data)
|
||||
try:
|
||||
return preview(session, current_user, id, data)
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.error(f"Preview failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f'Preview Failed: {e.args}')
|
||||
|
||||
|
||||
@router.post("/fieldEnum/{id}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import datetime
|
||||
import json
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import and_, text, cast, or_, func
|
||||
@@ -25,10 +25,13 @@ from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, Cor
|
||||
DatasourceConf, TableAndFields
|
||||
|
||||
|
||||
def get_datasource_list(session: SessionDep, user: CurrentUser):
|
||||
oid = user.oid if user.oid is not None else 1
|
||||
return session.query(CoreDatasource).filter(CoreDatasource.oid == oid).order_by(
|
||||
func.convert_to(CoreDatasource.name, 'gbk')).all()
|
||||
def get_datasource_list(session: SessionDep, user: CurrentUser, oid: Optional[int] = None) -> List[CoreDatasource]:
|
||||
current_oid = user.oid if user.oid is not None else 1
|
||||
if user.isAdmin and oid:
|
||||
current_oid = oid
|
||||
return session.exec(select(CoreDatasource).where(CoreDatasource.oid == current_oid).order_by(
|
||||
func.convert_to(CoreDatasource.name, 'gbk'))).all()
|
||||
|
||||
|
||||
|
||||
def get_ds(session: SessionDep, id: int):
|
||||
@@ -134,7 +137,7 @@ def getTables(session: SessionDep, id: int):
|
||||
|
||||
|
||||
def getTablesByDs(session: SessionDep, ds: CoreDatasource):
|
||||
check_status(session, ds, True)
|
||||
# check_status(session, ds, True)
|
||||
tables = get_tables(ds)
|
||||
return tables
|
||||
|
||||
@@ -241,7 +244,7 @@ def updateField(session: SessionDep, field: CoreField):
|
||||
|
||||
def preview(session: SessionDep, current_user: CurrentUser, id: int, data: TableObj):
|
||||
ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first()
|
||||
check_status(session, ds, True)
|
||||
# check_status(session, ds, True)
|
||||
|
||||
if data.fields is None or len(data.fields) == 0:
|
||||
return {"fields": [], "data": [], "sql": ''}
|
||||
@@ -278,8 +281,8 @@ def preview(session: SessionDep, current_user: CurrentUser, id: int, data: Table
|
||||
).first()
|
||||
if obj is not None:
|
||||
res.append(transRecord2DTO(session, permission))
|
||||
wheres = transFilterTree(session, res, ds)
|
||||
where = (' where ' + wheres) if wheres is not None and wheres != '' else ''
|
||||
where_str = transFilterTree(session, res, ds)
|
||||
where = (' where ' + where_str) if where_str is not None and where_str != '' else ''
|
||||
|
||||
fields = [f.field_name for f in f_list]
|
||||
if fields is None or len(fields) == 0:
|
||||
|
||||
+14
-17
@@ -254,20 +254,17 @@ def get_fields(ds: CoreDatasource, table_name: str = None):
|
||||
|
||||
|
||||
def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str):
|
||||
session = get_session(ds)
|
||||
result = session.execute(text(sql))
|
||||
try:
|
||||
columns = result.keys()._keys
|
||||
res = result.fetchall()
|
||||
result_list = [
|
||||
{columns[i]: float(value) if isinstance(value, Decimal) else value for i, value in enumerate(tuple_item)}
|
||||
for tuple_item in res
|
||||
]
|
||||
return {"fields": columns, "data": result_list, "sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))}
|
||||
except Exception as ex:
|
||||
raise ex
|
||||
finally:
|
||||
if result is not None:
|
||||
result.close()
|
||||
if session is not None:
|
||||
session.close()
|
||||
with get_session(ds) as session:
|
||||
with session.execute(text(sql)) as result:
|
||||
try:
|
||||
columns = result.keys()._keys
|
||||
res = result.fetchall()
|
||||
result_list = [
|
||||
{columns[i]: float(value) if isinstance(value, Decimal) else value for i, value in
|
||||
enumerate(tuple_item)}
|
||||
for tuple_item in res
|
||||
]
|
||||
return {"fields": columns, "data": result_list,
|
||||
"sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))}
|
||||
except Exception as ex:
|
||||
raise ex
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
import json
|
||||
from typing import List, Union
|
||||
from apps.ai_model.model_factory import LLMConfig, LLMFactory
|
||||
from apps.system.schemas.ai_model_schema import AiModelConfigItem, AiModelCreator, AiModelEditor, AiModelGridItem
|
||||
from fastapi import APIRouter, Query
|
||||
from sqlmodel import func, select, update
|
||||
|
||||
from apps.system.models.system_model import AiModelDetail
|
||||
from common.core.deps import SessionDep
|
||||
from common.core.deps import SessionDep, Trans
|
||||
from common.utils.time import get_timestamp
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
|
||||
router = APIRouter(tags=["system/aimodel"], prefix="/system/aimodel")
|
||||
|
||||
@router.post("/status")
|
||||
async def check_llm(info: AiModelCreator, trans: Trans):
|
||||
try:
|
||||
additional_params = {item.key: item.val for item in info.config_list}
|
||||
config = LLMConfig(
|
||||
model_type="openai",
|
||||
model_name=info.base_model,
|
||||
api_key=info.api_key,
|
||||
api_base_url=info.api_domain,
|
||||
additional_params=additional_params,
|
||||
)
|
||||
llm_instance = LLMFactory.create_llm(config)
|
||||
result = llm_instance.llm.invoke("who are you?")
|
||||
SQLBotLogUtil.info(f"check_llm result: {result}")
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.error(f"Error checking LLM: {e}")
|
||||
raise Exception(trans('i18n_llm.validate_error', msg = str(e)))
|
||||
|
||||
@router.get("", response_model=list[AiModelGridItem])
|
||||
async def query(
|
||||
session: SessionDep,
|
||||
@@ -74,19 +94,20 @@ async def update_model(
|
||||
data["config"] = json.dumps([item.model_dump(exclude_unset=True) for item in editor.config_list])
|
||||
data.pop("config_list", None)
|
||||
db_model = session.get(AiModelDetail, id)
|
||||
update_data = AiModelDetail.model_validate(data)
|
||||
db_model.sqlmodel_update(update_data)
|
||||
#update_data = AiModelDetail.model_validate(data)
|
||||
db_model.sqlmodel_update(data)
|
||||
session.add(db_model)
|
||||
session.commit()
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_model(
|
||||
session: SessionDep,
|
||||
trans: Trans,
|
||||
id: int
|
||||
):
|
||||
item = session.get(AiModelDetail, id)
|
||||
if item.default_model:
|
||||
raise RuntimeError(f"Can not delete [${item.name}], because it is default model!")
|
||||
raise Exception(trans('i18n_llm.delete_default_error', key = item.name))
|
||||
session.delete(item)
|
||||
session.commit()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from apps.system.schemas.system_schema import BaseUserDTO
|
||||
from common.core.deps import SessionDep
|
||||
from common.core.deps import SessionDep, Trans
|
||||
from ..crud.user import authenticate
|
||||
from common.core.security import create_access_token
|
||||
from datetime import timedelta
|
||||
@@ -13,14 +13,16 @@ router = APIRouter(tags=["login"], prefix="/login")
|
||||
@router.post("/access-token")
|
||||
def local_login(
|
||||
session: SessionDep,
|
||||
trans: Trans,
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||
) -> Token:
|
||||
user: BaseUserDTO = authenticate(session=session, account=form_data.username, password=form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect account or password")
|
||||
|
||||
raise HTTPException(status_code=400, detail=trans('i18n_login.account_pwd_error'))
|
||||
if not user.oid or user.oid == 0:
|
||||
raise HTTPException(status_code=400, detail="No associated workspace, Please contact the administrator")
|
||||
raise HTTPException(status_code=400, detail=trans('i18n_login.no_associated_ws', msg = trans('i18n_concat_admin')))
|
||||
if user.status != 1:
|
||||
raise HTTPException(status_code=400, detail=trans('i18n_login.user_disable', msg = trans('i18n_concat_admin')))
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
user_dict = user.to_dict()
|
||||
return Token(access_token=create_access_token(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from sqlmodel import func, or_, select, delete as sqlmodel_delete
|
||||
from apps.system.crud.user import get_db_user, single_delete, user_ws_options
|
||||
from apps.system.models.system_model import UserWsModel
|
||||
from apps.system.crud.user import check_account_exists, check_email_exists, check_email_format, check_pwd_format, get_db_user, single_delete, user_ws_options
|
||||
from apps.system.models.system_model import UserWsModel, WorkspaceModel
|
||||
from apps.system.models.user import UserModel
|
||||
from apps.system.schemas.auth import CacheName, CacheNamespace
|
||||
from apps.system.schemas.system_schema import PwdEditor, UserCreator, UserEditor, UserGrid, UserLanguage, UserWs
|
||||
from apps.system.schemas.system_schema import PwdEditor, UserCreator, UserEditor, UserGrid, UserLanguage, UserStatus, UserWs
|
||||
from common.core.deps import CurrentUser, SessionDep, Trans
|
||||
from common.core.pagination import Paginator
|
||||
from common.core.schemas import PaginatedResponse, PaginationParams
|
||||
@@ -96,10 +96,13 @@ async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Tran
|
||||
|
||||
@router.put("/ws/{oid}")
|
||||
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
|
||||
async def ws_change(session: SessionDep, current_user: CurrentUser, oid: int):
|
||||
async def ws_change(session: SessionDep, current_user: CurrentUser, trans:Trans, oid: int):
|
||||
ws_list: list[UserWs] = await user_ws_options(session, current_user.id)
|
||||
if not any(x.id == oid for x in ws_list):
|
||||
raise HTTPException(f"oid [{oid}] is invalid!")
|
||||
db_ws = session.get(WorkspaceModel, oid)
|
||||
if db_ws:
|
||||
raise Exception(trans('i18n_user.ws_miss', ws = db_ws.name))
|
||||
raise Exception(trans('i18n_not_exist', msg = f"{trans('i18n_ws.title')}[{oid}]"))
|
||||
user_model: UserModel = get_db_user(session = session, user_id = current_user.id)
|
||||
user_model.oid = oid
|
||||
session.add(user_model)
|
||||
@@ -115,7 +118,13 @@ async def query(session: SessionDep, trans: Trans, id: int) -> UserEditor:
|
||||
return result
|
||||
|
||||
@router.post("")
|
||||
async def create(session: SessionDep, creator: UserCreator):
|
||||
async def create(session: SessionDep, creator: UserCreator, trans: Trans):
|
||||
if check_account_exists(session=session, account=creator.account):
|
||||
raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.account')} [{creator.account}]"))
|
||||
if check_email_exists(session=session, email=creator.email):
|
||||
raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.email')} [{creator.email}]"))
|
||||
if not check_email_format(creator.email):
|
||||
raise Exception(trans('i18n_format_invalid', key = f"{trans('i18n_user.email')} [{creator.email}]"))
|
||||
data = creator.model_dump(exclude_unset=True)
|
||||
user_model = UserModel.model_validate(data)
|
||||
#user_model.create_time = get_timestamp()
|
||||
@@ -138,8 +147,16 @@ async def create(session: SessionDep, creator: UserCreator):
|
||||
|
||||
@router.put("")
|
||||
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="editor.id")
|
||||
async def update(session: SessionDep, editor: UserEditor):
|
||||
async def update(session: SessionDep, editor: UserEditor, trans: Trans):
|
||||
user_model: UserModel = get_db_user(session = session, user_id = editor.id)
|
||||
if not user_model:
|
||||
raise Exception(f"User with id [{editor.id}] not found!")
|
||||
if editor.account != user_model.account:
|
||||
raise Exception(f"account cannot be changed!")
|
||||
if editor.email != user_model.email and check_email_exists(session=session, account=editor.email):
|
||||
raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.email')} [{editor.email}]"))
|
||||
if not check_email_format(editor.email):
|
||||
raise Exception(trans('i18n_format_invalid', key = f"{trans('i18n_user.email')} [{editor.email}]"))
|
||||
origin_oid: int = user_model.oid
|
||||
del_stmt = sqlmodel_delete(UserWsModel).where(UserWsModel.uid == editor.id)
|
||||
session.exec(del_stmt)
|
||||
@@ -174,10 +191,10 @@ async def batch_del(session: SessionDep, id_list: list[int]):
|
||||
|
||||
@router.put("/language")
|
||||
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
|
||||
async def langChange(session: SessionDep, current_user: CurrentUser, language: UserLanguage):
|
||||
async def langChange(session: SessionDep, current_user: CurrentUser, trans: Trans, language: UserLanguage):
|
||||
lang = language.language
|
||||
if lang not in ["zh-CN", "en"]:
|
||||
return {"message": "Language not supported"}
|
||||
raise Exception(trans('i18n_user.language_not_support', key = lang))
|
||||
db_user: UserModel = get_db_user(session=session, user_id=current_user.id)
|
||||
db_user.language = lang
|
||||
session.add(db_user)
|
||||
@@ -185,9 +202,9 @@ async def langChange(session: SessionDep, current_user: CurrentUser, language: U
|
||||
|
||||
@router.patch("/pwd/{id}")
|
||||
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
|
||||
async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int):
|
||||
async def pwdReset(session: SessionDep, current_user: CurrentUser, trans: Trans, id: int):
|
||||
if not current_user.isAdmin:
|
||||
raise HTTPException('only for admin')
|
||||
raise Exception(trans('i18n_permission.no_permission', url = " patch[/user/pwd/id],", msg = trans('i18n_permission.only_admin')))
|
||||
db_user: UserModel = get_db_user(session=session, user_id=id)
|
||||
db_user.password = default_md5_pwd()
|
||||
session.add(db_user)
|
||||
@@ -195,10 +212,26 @@ async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int):
|
||||
|
||||
@router.put("/pwd")
|
||||
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
|
||||
async def pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdEditor):
|
||||
async def pwdUpdate(session: SessionDep, current_user: CurrentUser, trans: Trans, editor: PwdEditor):
|
||||
new_pwd = editor.new_pwd
|
||||
if not check_pwd_format(new_pwd):
|
||||
raise Exception(trans('i18n_format_invalid', key = trans('i18n_user.password')))
|
||||
db_user: UserModel = get_db_user(session=session, user_id=current_user.id)
|
||||
if not verify_md5pwd(editor.pwd, db_user.password):
|
||||
raise HTTPException("pwd error")
|
||||
db_user.password = md5pwd(editor.new_pwd)
|
||||
raise Exception(trans('i18n_error', key = trans('i18n_user.password')))
|
||||
db_user.password = md5pwd(new_pwd)
|
||||
session.add(db_user)
|
||||
session.commit()
|
||||
|
||||
@router.patch("/status")
|
||||
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="statusDto.id")
|
||||
async def langChange(session: SessionDep, current_user: CurrentUser, trans: Trans, statusDto: UserStatus):
|
||||
if not current_user.isAdmin:
|
||||
raise Exception(trans('i18n_permission.no_permission', url = ", ", msg = trans('i18n_permission.only_admin')))
|
||||
status = statusDto.status
|
||||
if status not in [0, 1]:
|
||||
return {"message": "status not supported"}
|
||||
db_user: UserModel = get_db_user(session=session, user_id=statusDto.id)
|
||||
db_user.status = status
|
||||
session.add(db_user)
|
||||
session.commit()
|
||||
@@ -17,15 +17,16 @@ router = APIRouter(tags=["system/workspace"], prefix="/system/workspace")
|
||||
async def option_pager(
|
||||
session: SessionDep,
|
||||
current_user: CurrentUser,
|
||||
trans: Trans,
|
||||
pageNum: int,
|
||||
pageSize: int,
|
||||
oid: int = Query(description="空间ID"),
|
||||
keyword: Optional[str] = Query(None, description="搜索关键字(可选)"),
|
||||
):
|
||||
if not current_user.isAdmin:
|
||||
raise RuntimeError('only for admin')
|
||||
raise Exception(trans('i18n_permission.no_permission', url = ", ", msg = trans('i18n_permission.only_admin')))
|
||||
if not oid:
|
||||
raise RuntimeError('oid miss error')
|
||||
raise Exception(trans('i18n_miss_args', key = '[oid]'))
|
||||
pagination = PaginationParams(page=pageNum, size=pageSize)
|
||||
paginator = Paginator(session)
|
||||
stmt = select(UserModel.id, UserModel.account, UserModel.name).where(
|
||||
@@ -50,12 +51,13 @@ async def option_pager(
|
||||
async def option_user(
|
||||
session: SessionDep,
|
||||
current_user: CurrentUser,
|
||||
trans: Trans,
|
||||
keyword: str = Query(description="搜索关键字")
|
||||
):
|
||||
if not keyword:
|
||||
raise HTTPException("keyword is required")
|
||||
raise Exception(trans('i18n_miss_args', key = '[keyword]'))
|
||||
if (not current_user.isAdmin) and current_user.weight == 0:
|
||||
raise RuntimeError("no permission to execute this api")
|
||||
raise Exception(trans('i18n_permission.no_permission', url = '', msg = ''))
|
||||
oid = current_user.oid
|
||||
|
||||
stmt = select(UserModel.id, UserModel.account, UserModel.name).where(
|
||||
@@ -76,13 +78,14 @@ async def option_user(
|
||||
async def pager(
|
||||
session: SessionDep,
|
||||
current_user: CurrentUser,
|
||||
trans: Trans,
|
||||
pageNum: int,
|
||||
pageSize: int,
|
||||
keyword: Optional[str] = Query(None, description="搜索关键字(可选)"),
|
||||
oid: Optional[int] = Query(None, description="空间ID(仅admin用户生效)"),
|
||||
):
|
||||
if not current_user.isAdmin and current_user.weight == 0:
|
||||
raise HTTPException("no permission to execute")
|
||||
raise Exception(trans('i18n_permission.no_permission', url = '', msg = ''))
|
||||
if current_user.isAdmin:
|
||||
workspace_id = oid if oid else current_user.oid
|
||||
else:
|
||||
@@ -112,9 +115,9 @@ async def pager(
|
||||
|
||||
|
||||
@router.post("/uws")
|
||||
async def create(session: SessionDep, current_user: CurrentUser, creator: UserWsDTO):
|
||||
async def create(session: SessionDep, current_user: CurrentUser, trans: Trans, creator: UserWsDTO):
|
||||
if not current_user.isAdmin and current_user.weight == 0:
|
||||
raise HTTPException("no permission to execute")
|
||||
raise Exception(trans('i18n_permission.no_permission', url = '', msg = ''))
|
||||
oid: int = creator.oid if (current_user.isAdmin and creator.oid) else current_user.oid
|
||||
weight = creator.weight if (current_user.isAdmin and creator.weight) else 0
|
||||
# 判断uid_list以及oid合法性
|
||||
@@ -134,9 +137,9 @@ async def create(session: SessionDep, current_user: CurrentUser, creator: UserWs
|
||||
session.commit()
|
||||
|
||||
@router.put("/uws")
|
||||
async def edit(session: SessionDep, editor: UserWsEditor):
|
||||
async def edit(session: SessionDep, trans: Trans, editor: UserWsEditor):
|
||||
if not editor.oid or not editor.uid:
|
||||
raise HTTPException("param [oid, uid] miss")
|
||||
raise Exception(trans('i18n_miss_args', key = '[oid, uid]'))
|
||||
db_model = session.exec(select(UserWsModel).where(UserWsModel.uid == editor.uid, UserWsModel.oid == editor.oid)).first()
|
||||
if not db_model:
|
||||
raise HTTPException("uws not exist")
|
||||
@@ -150,9 +153,9 @@ async def edit(session: SessionDep, editor: UserWsEditor):
|
||||
session.commit()
|
||||
|
||||
@router.delete("/uws")
|
||||
async def delete(session: SessionDep, current_user: CurrentUser, dto: UserWsBase):
|
||||
async def delete(session: SessionDep, current_user: CurrentUser, trans: Trans, dto: UserWsBase):
|
||||
if not current_user.isAdmin and current_user.weight == 0:
|
||||
raise HTTPException("no permission to execute")
|
||||
raise Exception(trans('i18n_permission.no_permission', url = '', msg = ''))
|
||||
oid: int = dto.oid if (current_user.isAdmin and dto.oid) else current_user.oid
|
||||
db_model_list: list[UserWsModel] = session.exec(select(UserWsModel).where(UserWsModel.uid.in_(dto.uid_list), UserWsModel.oid == oid)).all()
|
||||
if not db_model_list:
|
||||
|
||||
@@ -33,11 +33,12 @@ def get_assistant_ds(llm_service) -> list[dict]:
|
||||
configuration = assistant.configuration
|
||||
if configuration:
|
||||
config: dict[any] = json.loads(configuration)
|
||||
oid: str = config['oid']
|
||||
oid: int = int(config['oid'])
|
||||
stmt = select(CoreDatasource.id, CoreDatasource.name, CoreDatasource.description).where(CoreDatasource.oid == oid)
|
||||
private_list:list[int] = config.get('private_list') or None
|
||||
if private_list:
|
||||
stmt.where(~CoreDatasource.id.in_(private_list))
|
||||
if not assistant.online:
|
||||
private_list:list[int] = config.get('private_list') or None
|
||||
if private_list:
|
||||
stmt = stmt.where(~CoreDatasource.id.in_(private_list))
|
||||
db_ds_list = session.exec(stmt)
|
||||
|
||||
result_list = [
|
||||
@@ -131,7 +132,7 @@ class AssistantOutDs:
|
||||
def get_db_schema(self, ds_id: int) -> str:
|
||||
ds = self.get_ds(ds_id)
|
||||
schema_str = ""
|
||||
db_name = ds.schema
|
||||
db_name = ds.db_schema
|
||||
schema_str += f"【DB_ID】 {db_name}\n【Schema】\n"
|
||||
for table in ds.tables:
|
||||
schema_str += f"# Table: {db_name}.{table.name}"
|
||||
@@ -159,7 +160,9 @@ class AssistantOutDs:
|
||||
if attr in ds_dict:
|
||||
id_marker += str(ds_dict.get(attr, '')) + '--sqlbot--'
|
||||
id = string_to_numeric_hash(id_marker)
|
||||
return AssistantOutDsSchema(**{**ds_dict, "id": id})
|
||||
db_schema = ds_dict.get('schema', ds_dict.get('db_schema', ''))
|
||||
ds_dict.pop("schema", None)
|
||||
return AssistantOutDsSchema(**{**ds_dict, "id": id, "db_schema": db_schema})
|
||||
|
||||
class AssistantOutDsFactory:
|
||||
@staticmethod
|
||||
@@ -177,12 +180,12 @@ def get_ds_engine(ds: AssistantOutDsSchema) -> Engine:
|
||||
database=ds.dataBase,
|
||||
driver='',
|
||||
extraJdbc=ds.extraParams,
|
||||
dbSchema=ds.schema or ''
|
||||
dbSchema=ds.db_schema or ''
|
||||
)
|
||||
from apps.db.db import get_uri_from_config
|
||||
uri = get_uri_from_config(ds.type, conf)
|
||||
if ds.type == "pg" and ds.schema:
|
||||
connect_args.update({"options": f"-c search_path={ds.schema}"})
|
||||
if ds.type == "pg" and ds.db_schema:
|
||||
connect_args.update({"options": f"-c search_path={ds.db_schema}"})
|
||||
engine = create_engine(uri, connect_args=connect_args, pool_timeout=timeout, pool_size=20, max_overflow=10)
|
||||
return engine
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
|
||||
from typing import Optional
|
||||
from sqlmodel import Session, select, delete as sqlmodel_delete
|
||||
from sqlmodel import Session, func, select, delete as sqlmodel_delete
|
||||
from apps.system.models.system_model import UserWsModel, WorkspaceModel
|
||||
from apps.system.schemas.auth import CacheName, CacheNamespace
|
||||
from apps.system.schemas.system_schema import BaseUserDTO, UserInfoDTO, UserWs
|
||||
from apps.system.schemas.system_schema import EMAIL_REGEX, PWD_REGEX, BaseUserDTO, UserInfoDTO, UserWs
|
||||
from common.core.deps import SessionDep
|
||||
from common.core.sqlbot_cache import cache, clear_cache
|
||||
from common.utils.locale import I18n
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
from ..models.user import UserModel
|
||||
from common.core.security import verify_md5pwd
|
||||
import re
|
||||
|
||||
def get_db_user(*, session: Session, user_id: int) -> UserModel:
|
||||
db_user = session.get(UserModel, user_id)
|
||||
@@ -69,3 +70,17 @@ async def single_delete(session: SessionDep, id: int):
|
||||
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
|
||||
async def clean_user_cache(id: int):
|
||||
SQLBotLogUtil.info(f"User cache for [{id}] has been cleaned")
|
||||
|
||||
|
||||
def check_account_exists(*, session: Session, account: str) -> bool:
|
||||
return session.exec(select(func.count()).select_from(UserModel).where(UserModel.account == account)).one() > 0
|
||||
def check_email_exists(*, session: Session, email: str) -> bool:
|
||||
return session.exec(select(func.count()).select_from(UserModel).where(UserModel.email == email)).one() > 0
|
||||
|
||||
|
||||
|
||||
def check_email_format(email: str) -> bool:
|
||||
return bool(EMAIL_REGEX.fullmatch(email))
|
||||
|
||||
def check_pwd_format(pwd: str) -> bool:
|
||||
return bool(PWD_REGEX.fullmatch(pwd))
|
||||
|
||||
@@ -36,7 +36,7 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
request.state.current_user = validator[1]
|
||||
request.state.assistant = validator[2]
|
||||
return await call_next(request)
|
||||
return JSONResponse({"msg": f"Unauthorized:[{validator[1]}]"}, status_code=401, headers={"Access-Control-Allow-Origin": "*"})
|
||||
return JSONResponse(f"Unauthorized:[{validator[1]}]", status_code=401, headers={"Access-Control-Allow-Origin": "*"})
|
||||
#validate pass
|
||||
tokenkey = settings.TOKEN_KEY
|
||||
token = request.headers.get(tokenkey)
|
||||
@@ -44,7 +44,7 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
if validate_pass:
|
||||
request.state.current_user = data
|
||||
return await call_next(request)
|
||||
return JSONResponse({"msg": f"Unauthorized:[{data}]"}, status_code=401, headers={"Access-Control-Allow-Origin": "*"})
|
||||
return JSONResponse(f"Unauthorized:[{data}]", status_code=401, headers={"Access-Control-Allow-Origin": "*"})
|
||||
|
||||
def is_options(self, request: Request):
|
||||
return request.method == "OPTIONS"
|
||||
@@ -62,7 +62,13 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
try:
|
||||
with Session(engine) as session:
|
||||
session_user = await get_user_info(session = session, user_id = token_data.id)
|
||||
if not session_user:
|
||||
raise Exception(f"User not found with id: {token_data.id}")
|
||||
session_user = UserInfoDTO.model_validate(session_user)
|
||||
if session_user.status != 1:
|
||||
raise Exception(f"User is not active!")
|
||||
if not session_user.oid or session_user.oid == 0:
|
||||
raise Exception(f"User default space is not set!")
|
||||
""" if token_data.oid != session_user.oid:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from common.core.schemas import BaseCreatorDTO
|
||||
import re
|
||||
|
||||
class model_status(BaseModel):
|
||||
status: bool
|
||||
ids: list[int]
|
||||
EMAIL_REGEX = re.compile(
|
||||
r"^[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*@"
|
||||
r"([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+"
|
||||
r"[a-zA-Z]{2,}$"
|
||||
)
|
||||
PWD_REGEX = re.compile(
|
||||
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)"
|
||||
r"(?=.*[~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./])"
|
||||
r"[A-Za-z\d~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./]{8,20}$"
|
||||
)
|
||||
class UserStatus(BaseCreatorDTO):
|
||||
status: int = 1
|
||||
|
||||
|
||||
|
||||
class UserLanguage(BaseModel):
|
||||
@@ -13,24 +24,36 @@ class UserLanguage(BaseModel):
|
||||
|
||||
|
||||
class BaseUser(BaseModel):
|
||||
account: str
|
||||
account: str = Field(min_length=1, max_length=100, description="用户账号")
|
||||
oid: int
|
||||
|
||||
|
||||
class BaseUserDTO(BaseUser, BaseCreatorDTO):
|
||||
language: str
|
||||
language: str = Field(pattern=r"^(zh-CN|en)$", default="zh-CN", description="用户语言")
|
||||
password: str
|
||||
status: int = 1
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"account": self.account,
|
||||
"oid": self.oid
|
||||
}
|
||||
@field_validator("language")
|
||||
def validate_language(cls, lang: str) -> str:
|
||||
if not re.fullmatch(r"^(zh-CN|en)$", lang):
|
||||
raise ValueError("Language must be 'zh-CN' or 'en'")
|
||||
return lang
|
||||
|
||||
class UserCreator(BaseUser):
|
||||
name: str
|
||||
email: str
|
||||
name: str = Field(min_length=1, max_length=100, description="用户名")
|
||||
email: str = Field(min_length=1, max_length=100, description="用户邮箱")
|
||||
status: int = 1
|
||||
oid_list: Optional[list[int]] = None
|
||||
|
||||
""" @field_validator("email")
|
||||
def validate_email(cls, lang: str) -> str:
|
||||
if not re.fullmatch(EMAIL_REGEX, lang):
|
||||
raise ValueError("Email format is invalid!")
|
||||
return lang """
|
||||
|
||||
class UserEditor(UserCreator, BaseCreatorDTO):
|
||||
pass
|
||||
@@ -139,7 +162,7 @@ class AssistantOutDsSchema(AssistantOutDsBase):
|
||||
dataBase: Optional[str] = None
|
||||
user: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
schema: Optional[str] = None
|
||||
db_schema: Optional[str] = None
|
||||
extraParams: Optional[str] = None
|
||||
tables: Optional[list[AssistantTableSchema]] = None
|
||||
|
||||
@@ -25,6 +25,9 @@ async def get_current_assistant(request: Request) -> AssistantHeader | None:
|
||||
if request.headers.get("X-SQLBOT-ASSISTANT-CERTIFICATE"):
|
||||
entry_certificate = request.headers['X-SQLBOT-ASSISTANT-CERTIFICATE']
|
||||
base_assistant.certificate = unquote(base64.b64decode(entry_certificate).decode('utf-8'))
|
||||
if request.headers.get("X-SQLBOT-ASSISTANT-ONLINE"):
|
||||
entry_online = request.headers['X-SQLBOT-ASSISTANT-ONLINE']
|
||||
base_assistant.online = str(entry_online).lower() == 'true' if entry_online else False
|
||||
return base_assistant
|
||||
|
||||
CurrentAssistant = Annotated[AssistantHeader, Depends(get_current_assistant)]
|
||||
|
||||
@@ -14,7 +14,8 @@ class ResponseMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
if isinstance(response, JSONResponse) or request.url.path == f"{settings.API_V1_STR}/openapi.json":
|
||||
return response
|
||||
|
||||
if response.status_code != 200:
|
||||
return response
|
||||
if response.headers.get("content-type") == "application/json":
|
||||
try:
|
||||
body = b""
|
||||
@@ -51,11 +52,7 @@ class ResponseMiddleware(BaseHTTPMiddleware):
|
||||
SQLBotLogUtil.error(f"Response processing error: {str(e)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"code": 500,
|
||||
"data": None,
|
||||
"msg": str(e)
|
||||
},
|
||||
content=str(e),
|
||||
headers={
|
||||
k: v for k, v in response.headers.items()
|
||||
if k.lower() not in ("content-length", "content-type")
|
||||
@@ -71,11 +68,7 @@ class exception_handler():
|
||||
SQLBotLogUtil.error(f"HTTP Exception: {exc.detail}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"code": exc.status_code,
|
||||
"msg": exc.detail,
|
||||
"data": None
|
||||
},
|
||||
content=exc.detail,
|
||||
headers={"Access-Control-Allow-Origin": "*"}
|
||||
)
|
||||
|
||||
@@ -85,11 +78,7 @@ class exception_handler():
|
||||
SQLBotLogUtil.error(f"Unhandled Exception: {str(exc)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"code": 500,
|
||||
"msg": str(exc),
|
||||
"data": None
|
||||
},
|
||||
content=str(exc),
|
||||
headers={"Access-Control-Allow-Origin": "*"}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Any
|
||||
from fastapi import Request
|
||||
|
||||
class I18n:
|
||||
def __init__(self, locale_dir: str = "locales"):
|
||||
self.locale_dir = Path(locale_dir)
|
||||
self.translations: Dict[str, Dict[str, str]] = {}
|
||||
self.translations: Dict[str, Dict[str, Any]] = {}
|
||||
self.load_translations()
|
||||
|
||||
def load_translations(self):
|
||||
|
||||
if not self.locale_dir.exists():
|
||||
self.locale_dir.mkdir()
|
||||
return
|
||||
@@ -34,9 +33,21 @@ class I18nHelper:
|
||||
self.request = request
|
||||
self.lang = i18n.get_language(request)
|
||||
|
||||
def __call__(self, key: str, **kwargs) -> str:
|
||||
def _get_nested_translation(self, data: Dict[str, Any], key_path: str) -> str:
|
||||
keys = key_path.split('.')
|
||||
current = data
|
||||
|
||||
for key in keys:
|
||||
if isinstance(current, dict) and key in current:
|
||||
current = current[key]
|
||||
else:
|
||||
return key_path # 如果找不到,返回原键
|
||||
|
||||
return current if isinstance(current, str) else key_path
|
||||
|
||||
def __call__(self, arg_key: str, **kwargs) -> str:
|
||||
lang_data = self.i18n.translations.get(self.lang, {})
|
||||
text = lang_data.get(key, key) # 找不到则返回原key
|
||||
text = self._get_nested_translation(lang_data, arg_key)
|
||||
|
||||
if kwargs:
|
||||
try:
|
||||
|
||||
+30
-1
@@ -1,4 +1,33 @@
|
||||
{
|
||||
"i18n_default_workspace": "Default workspace",
|
||||
"i18n_ds_name_exist": "Name exist"
|
||||
"i18n_ds_name_exist": "Name already exists",
|
||||
"i18n_concat_admin": "Please contact administrator!",
|
||||
"i18n_exist": "{msg} already exists!",
|
||||
"i18n_not_exist": "{msg} not exists",
|
||||
"i18n_error": "{key} error!",
|
||||
"i18n_miss_args": "Missing {key} parameter!",
|
||||
"i18n_format_invalid": "{key} format is incorrect!",
|
||||
"i18n_login": {
|
||||
"account_pwd_error": "Account or password error!",
|
||||
"no_associated_ws": "No associated workspace, {msg}",
|
||||
"user_disable": "Account disabled, {msg}"
|
||||
},
|
||||
"i18n_user": {
|
||||
"account": "Account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"language_not_support": "System does not support [{key}] language!",
|
||||
"ws_miss": "The current user is not in the workspace [{ws}]!"
|
||||
},
|
||||
"i18n_ws": {
|
||||
"title": "Workspace"
|
||||
},
|
||||
"i18n_permission": {
|
||||
"only_admin": "Only administrators can call this!",
|
||||
"no_permission": "No permission to access {url}{msg}"
|
||||
},
|
||||
"i18n_llm": {
|
||||
"validate_error": "Validation failed [{msg}]",
|
||||
"delete_default_error": "Cannot delete default model [{key}]!"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,34 @@
|
||||
{
|
||||
"i18n_default_workspace": "默认工作空间",
|
||||
"i18n_ds_name_exist": "名称已存在"
|
||||
"i18n_ds_name_exist": "名称已存在",
|
||||
"i18n_concat_admin": "请联系管理员!",
|
||||
"i18n_exist": "{msg}已存在!",
|
||||
"i18n_not_exist": "{msg}不存在!",
|
||||
"i18n_error": "{key}错误!",
|
||||
"i18n_miss_args": "缺失{key}参数!",
|
||||
"i18n_format_invalid": "{key}格式不正确!",
|
||||
"i18n_login": {
|
||||
"account_pwd_error": "账号或密码错误!",
|
||||
"no_associated_ws": "没有关联的工作空间,{msg}",
|
||||
"user_disable": "账号已禁用,{msg}"
|
||||
},
|
||||
"i18n_user": {
|
||||
"account": "账号",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"language_not_support": "系统不支持[{key}]语言!",
|
||||
"ws_miss": "当前用户不在工作空间[{ws}]中!"
|
||||
},
|
||||
"i18n_ws": {
|
||||
"title": "工作空间"
|
||||
},
|
||||
"i18n_permission": {
|
||||
"only_admin": "仅支持管理员调用!",
|
||||
"no_permission": "无权调用{url}{msg}"
|
||||
|
||||
},
|
||||
"i18n_llm": {
|
||||
"validate_error": "校验失败[{msg}]",
|
||||
"delete_default_error": "无法删除默认模型[{key}]!"
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ dependencies = [
|
||||
"pyyaml (>=6.0.2,<7.0.0)",
|
||||
"fastapi-mcp (>=0.3.4,<0.4.0)",
|
||||
"tabulate>=0.9.0",
|
||||
"sqlbot-xpack==0.0.3.7",
|
||||
"sqlbot-xpack==0.0.3.8",
|
||||
"fastapi-cache2>=0.2.2",
|
||||
"sqlparse>=0.5.3",
|
||||
"redis>=6.2.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
;(function () {
|
||||
window.sqlbot_assistant_handler = window.sqlbot_assistant_handler || {}
|
||||
const defaultData = {
|
||||
id: '1',
|
||||
show_guide: false,
|
||||
@@ -34,14 +35,41 @@
|
||||
|
||||
const chatButtonHtml = (data) => `
|
||||
<div class="sqlbot-assistant-chat-button">
|
||||
<img style="height:100%;width:100%;" src="${data.float_icon}">
|
||||
<img style="height:100%;width:100%;display:none;" src="${data.float_icon}">
|
||||
<svg style="display:none;margin-right: 20px;" data-v-39a51454="" xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="none">
|
||||
<path fill="#149CC5" d="M28.333 13.669h-.231c.138.758.207 1.527.206 2.298.005.946-.107 1.89-.335 2.808h.65a1.05 1.05 0 0 0 1.05-1.05V15.01a1.343 1.343 0 0 0-1.34-1.341"></path>
|
||||
<path fill="#69CAA4" d="M1.692 15.967c0-.77.068-1.54.206-2.298h-.23a1.34 1.34 0 0 0-1.342 1.34v2.716a1.05 1.05 0 0 0 1.05 1.05h.651a11.4 11.4 0 0 1-.335-2.808"></path>
|
||||
<path fill="url(#a)" d="M15 3.795c-6.89 0-12.474 5.282-12.474 12.171 0 6.89 5.585 10.239 12.474 10.239s12.474-3.35 12.474-10.238c0-6.89-5.585-12.172-12.474-12.172m3.026 17.33h-6.052a6 6 0 0 1-2.484-.535c-.564-.256-2.208.282-2.663-.127-.551-.495.117-1.969-.221-2.636a6.014 6.014 0 0 1 5.368-8.73h6.052a6.014 6.014 0 0 1 0 12.029"></path>
|
||||
<path fill="url(#b)" d="M15 3.795c-6.89 0-12.474 5.282-12.474 12.171 0 6.89 5.585 10.239 12.474 10.239s12.474-3.35 12.474-10.238c0-6.89-5.585-12.172-12.474-12.172m3.026 17.33h-6.052a6 6 0 0 1-2.484-.535c-.564-.256-2.208.282-2.663-.127-.551-.495.117-1.969-.221-2.636a6.014 6.014 0 0 1 5.368-8.73h6.052a6.014 6.014 0 0 1 0 12.029"></path>
|
||||
<path fill="#75CCCC" d="M10.546 14.915h-1.78v3.88h1.78zM14.12 13.193h-1.78v5.602h1.78zM17.694 15.438h-1.78v3.357h1.78zM21.233 12.452h-1.779v6.343h1.78z"></path>
|
||||
<defs>
|
||||
<linearGradient id="a" x1="6.65" x2="25.099" y1="4.441" y2="29.249" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#B2F288"></stop>
|
||||
<stop offset=".181" stop-color="#80D79C"></stop>
|
||||
<stop offset=".371" stop-color="#52BEAD"></stop>
|
||||
<stop offset=".554" stop-color="#2FAABB"></stop>
|
||||
<stop offset=".724" stop-color="#159CC5"></stop>
|
||||
<stop offset=".878" stop-color="#0594CB"></stop>
|
||||
<stop offset="1" stop-color="#0091CD"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" x1="6.65" x2="25.099" y1="4.441" y2="29.249" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#B2F288"></stop>
|
||||
<stop offset=".181" stop-color="#80D79C"></stop>
|
||||
<stop offset=".371" stop-color="#52BEAD"></stop>
|
||||
<stop offset=".554" stop-color="#2FAABB"></stop>
|
||||
<stop offset=".724" stop-color="#159CC5"></stop>
|
||||
<stop offset=".878" stop-color="#0594CB"></stop>
|
||||
<stop offset="1" stop-color="#0091CD"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>`
|
||||
|
||||
const getChatContainerHtml = (data) => {
|
||||
return `
|
||||
<div id="sqlbot-assistant-chat-container">
|
||||
<iframe id="sqlbot-assistant-chat-iframe-${data.id}" allow="microphone" src="${data.domain_url}/#/assistant?id=${data.id}"></iframe>
|
||||
<div class="sqlbot-assistant-operate">
|
||||
<iframe id="sqlbot-assistant-chat-iframe-${data.id}" allow="microphone" src="${data.domain_url}/#/assistant?id=${data.id}&online=${!!data.online}"></iframe>
|
||||
<div class="sqlbot-assistant-operate">
|
||||
<div class="sqlbot-assistant-closeviewport sqlbot-assistant-viewportnone">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M7.507 11.6645C7.73712 11.6645 7.94545 11.7578 8.09625 11.9086C8.24706 12.0594 8.34033 12.2677 8.34033 12.4978V16.7976C8.34033 17.0277 8.15378 17.2143 7.92366 17.2143H7.09033C6.86021 17.2143 6.67366 17.0277 6.67366 16.7976V14.5812L3.41075 17.843C3.24803 18.0057 2.98421 18.0057 2.82149 17.843L2.23224 17.2537C2.06952 17.091 2.06952 16.8272 2.23224 16.6645L5.56668 13.3311H3.19634C2.96622 13.3311 2.77967 13.1446 2.77967 12.9145V12.0811C2.77967 11.851 2.96622 11.6645 3.19634 11.6645H7.507ZM16.5991 2.1572C16.7619 1.99448 17.0257 1.99448 17.1884 2.1572L17.7777 2.74645C17.9404 2.90917 17.9404 3.17299 17.7777 3.33571L14.4432 6.66904H16.8136C17.0437 6.66904 17.2302 6.85559 17.2302 7.08571V7.91904C17.2302 8.14916 17.0437 8.33571 16.8136 8.33571H12.5029C12.2728 8.33571 12.0644 8.24243 11.9136 8.09163C11.7628 7.94082 11.6696 7.73249 11.6696 7.50237V3.20257C11.6696 2.97245 11.8561 2.7859 12.0862 2.7859H12.9196C13.1497 2.7859 13.3362 2.97245 13.3362 3.20257V5.419L16.5991 2.1572Z" fill="${data.header_font_color}"/>
|
||||
@@ -83,7 +111,11 @@
|
||||
root.insertAdjacentHTML('beforeend', getChatContainerHtml(data))
|
||||
// 按钮元素
|
||||
const chat_button = root.querySelector('.sqlbot-assistant-chat-button')
|
||||
const chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > img')
|
||||
let chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > svg')
|
||||
if (data.float_icon) {
|
||||
chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > img')
|
||||
}
|
||||
chat_button_img.style.display = 'block'
|
||||
// 对话框元素
|
||||
const chat_container = root.querySelector('#sqlbot-assistant-chat-container')
|
||||
// 引导层
|
||||
@@ -260,8 +292,8 @@
|
||||
|
||||
}
|
||||
#sqlbot-assistant-chat-container {
|
||||
width: 450px;
|
||||
height: 600px;
|
||||
width: 460px;
|
||||
height: 640px;
|
||||
display:none;
|
||||
}
|
||||
@media only screen and (max-width: 768px) {
|
||||
@@ -282,7 +314,7 @@
|
||||
#sqlbot-assistant #sqlbot-assistant-chat-container{
|
||||
z-index:10000;position: relative;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ffffff;
|
||||
//border: 1px solid #ffffff;
|
||||
background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1;
|
||||
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.10);
|
||||
position: fixed;bottom: 16px;right: 16px;overflow: hidden;
|
||||
@@ -409,6 +441,7 @@
|
||||
}
|
||||
function loadScript(src, id) {
|
||||
const domain_url = getDomain(src)
|
||||
const online = getParam(src, 'online')
|
||||
let url = `${domain_url}/api/v1/system/assistant/info/${id}`
|
||||
if (domain_url.includes('5173')) {
|
||||
url = url.replace('5173', '8000')
|
||||
@@ -424,6 +457,7 @@
|
||||
Object.assign(tempData, config)
|
||||
tempData = Object.assign(tempData, config)
|
||||
}
|
||||
tempData['online'] = online && online.toString().toLowerCase() == 'true'
|
||||
initsqlbot_assistant(tempData)
|
||||
if (data.type == 1) {
|
||||
registerMessageEvent(id, tempData)
|
||||
@@ -440,13 +474,93 @@
|
||||
const src_list = scriptsArray.map((script) => script.src)
|
||||
src_list.forEach((src) => {
|
||||
const id = getParam(src, 'id')
|
||||
window.sqlbot_assistant_handler[id] = window.sqlbot_assistant_handler[id] || {}
|
||||
window.sqlbot_assistant_handler[id]['id'] = id
|
||||
const propName = script_id_prefix + id + '-state'
|
||||
if (window[propName]) {
|
||||
return true
|
||||
}
|
||||
window[propName] = true
|
||||
loadScript(src, id)
|
||||
expposeGlobalMethods(id)
|
||||
})
|
||||
}
|
||||
window.addEventListener('load', init)
|
||||
function updateParam(target_url, key, newValue) {
|
||||
try {
|
||||
const url = new URL(target_url)
|
||||
const [hashPath, hashQuery] = url.hash.split('?')
|
||||
let searchParams
|
||||
if (hashQuery) {
|
||||
searchParams = new URLSearchParams(hashQuery)
|
||||
} else {
|
||||
searchParams = url.searchParams
|
||||
}
|
||||
searchParams.set(key, newValue)
|
||||
if (hashQuery) {
|
||||
url.hash = `${hashPath}?${searchParams.toString()}`
|
||||
} else {
|
||||
url.search = searchParams.toString()
|
||||
}
|
||||
return url.toString()
|
||||
} catch (e) {
|
||||
console.error('Invalid URL:', target_url)
|
||||
return target_url
|
||||
}
|
||||
}
|
||||
function expposeGlobalMethods(id) {
|
||||
window.sqlbot_assistant_handler[id]['setOnline'] = (online) => {
|
||||
if (online != null && typeof online != 'boolean') {
|
||||
throw new Error('The parameter can only be of type boolean')
|
||||
}
|
||||
const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
|
||||
if (iframe) {
|
||||
const url = iframe.src
|
||||
const eventName = 'sqlbot_assistant_event'
|
||||
const params = {
|
||||
busi: 'setOnline',
|
||||
online,
|
||||
eventName,
|
||||
messageId: id,
|
||||
}
|
||||
const contentWindow = iframe.contentWindow
|
||||
contentWindow.postMessage(params, url)
|
||||
}
|
||||
}
|
||||
window.sqlbot_assistant_handler[id]['refresh'] = (online) => {
|
||||
if (online != null && typeof online != 'boolean') {
|
||||
throw new Error('The parameter can only be of type boolean')
|
||||
}
|
||||
const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
|
||||
if (iframe) {
|
||||
const url = iframe.src
|
||||
let new_url = updateParam(url, 't', Date.now())
|
||||
if (online != null) {
|
||||
new_url = updateParam(new_url, 'online', online)
|
||||
}
|
||||
iframe.src = 'about:blank'
|
||||
setTimeout(() => {
|
||||
iframe.src = new_url
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
// window.addEventListener('load', init)
|
||||
const executeWhenReady = (fn) => {
|
||||
if (
|
||||
document.readyState === 'complete' ||
|
||||
(document.readyState !== 'loading' && !document.documentElement.doScroll)
|
||||
) {
|
||||
setTimeout(fn, 0)
|
||||
} else {
|
||||
const onReady = () => {
|
||||
document.removeEventListener('DOMContentLoaded', onReady)
|
||||
window.removeEventListener('load', onReady)
|
||||
fn()
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', onReady)
|
||||
window.addEventListener('load', onReady)
|
||||
}
|
||||
}
|
||||
|
||||
executeWhenReady(init)
|
||||
})()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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 } }),
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "确定还原至社区版?"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user