Merge remote-tracking branch 'origin/main'

# Conflicts:
#	frontend/src/router/index.ts
This commit is contained in:
wangjiahao
2025-07-18 10:48:01 +08:00
52 changed files with 2175 additions and 248 deletions
+28
View File
@@ -0,0 +1,28 @@
"""028_ds_oid
Revision ID: e96b16d3daab
Revises: b049c9f8ca5b
Create Date: 2025-07-17 14:40:48.522033
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'e96b16d3daab'
down_revision = 'b049c9f8ca5b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('core_datasource', sa.Column('oid', sa.BigInteger(), nullable=True))
op.execute('update core_datasource set oid = 1')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('core_datasource', 'oid')
# ### end Alembic commands ###
@@ -0,0 +1,54 @@
"""029_modify_chat
Revision ID: 77d4c39ec22f
Revises: e96b16d3daab
Create Date: 2025-07-17 17:05:13.392973
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '77d4c39ec22f'
down_revision = 'e96b16d3daab'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('chat', sa.Column('oid', sa.BigInteger(), nullable=True))
op.execute('update chat set oid = 1')
op.alter_column('chat', 'create_time',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True)
op.alter_column('chat_record', 'create_time',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True)
op.alter_column('chat_record', 'finish_time',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('chat_record', 'finish_time',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True)
op.alter_column('chat_record', 'create_time',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True)
op.alter_column('chat', 'create_time',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True)
op.drop_column('chat', 'oid')
# ### end Alembic commands ###
+4 -3
View File
@@ -4,7 +4,6 @@ from typing import List
import orjson
from sqlalchemy import and_
from sqlalchemy.orm import load_only
from sqlmodel import select
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo, RenameChat, ChatQuestion
from apps.datasource.models.datasource import CoreDatasource
@@ -13,8 +12,9 @@ from common.utils.utils import extract_nested_json
def list_chats(session: SessionDep, current_user: CurrentUser) -> List[Chat]:
chart_list = session.exec(
select(Chat).where(Chat.create_by == current_user.id).order_by(Chat.create_time.desc())).all()
oid = current_user.oid if current_user.oid is not None else 1
chart_list = session.query(Chat).filter(and_(Chat.create_by == current_user.id, Chat.oid == oid)).order_by(
Chat.create_time.desc()).all()
return chart_list
@@ -149,6 +149,7 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj:
chat = Chat(create_time=datetime.datetime.now(),
create_by=current_user.id,
oid=current_user.oid if current_user.oid is not None else 1,
brief=create_chat_obj.question.strip()[:20])
ds: CoreDatasource | None = None
if create_chat_obj.datasource:
+5 -3
View File
@@ -17,7 +17,8 @@ from apps.template.filter.generator import get_permissions_template
class Chat(SQLModel, table=True):
__tablename__ = "chat"
id: Optional[int] = Field(sa_column=Column(BigInteger, Identity(always=True), primary_key=True))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
oid: Optional[int] = Field(sa_column=Column(BigInteger, nullable=True, default=1))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True))
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
brief: str = Field(max_length=64, nullable=True)
chat_type: str = Field(max_length=20, default="chat") # chat, datasource
@@ -31,8 +32,8 @@ class ChatRecord(SQLModel, table=True):
chat_id: int = Field(sa_column=Column(BigInteger, nullable=False))
ai_modal_id: Optional[int] = Field(sa_column=Column(BigInteger))
first_chat: bool = Field(sa_column=Column(Boolean, nullable=True, default=False))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
finish_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True))
finish_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True))
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
datasource: int = Field(sa_column=Column(BigInteger, nullable=True))
engine_type: str = Field(max_length=64)
@@ -66,6 +67,7 @@ class ChatRecord(SQLModel, table=True):
analysis_record_id: int = Field(sa_column=Column(BigInteger, nullable=True))
predict_record_id: int = Field(sa_column=Column(BigInteger, nullable=True))
class CreateChat(BaseModel):
id: int = None
question: str = None
+2
View File
@@ -846,6 +846,8 @@ def run_analysis_or_predict_task(llm_service: LLMService, action_type: str, base
try:
llm_service.set_record(save_analysis_predict_record(llm_service.session, base_record, action_type))
yield orjson.dumps({'type': 'id', 'id': llm_service.get_record().id}).decode() + '\n\n'
if action_type == 'analysis':
# generate analysis
analysis_res = llm_service.generate_analysis()
+4 -4
View File
@@ -19,8 +19,8 @@ path = "/opt/sqlbot/data/excel"
@router.get("/list")
async def datasource_list(session: SessionDep):
return get_datasource_list(session=session)
async def datasource_list(session: SessionDep, user: CurrentUser):
return get_datasource_list(session=session, user=user)
@router.post("/get/{id}")
@@ -44,8 +44,8 @@ async def choose_tables(session: SessionDep, id: int, tables: List[CoreTable]):
@router.post("/update", response_model=CoreDatasource)
async def update(session: SessionDep, ds: CoreDatasource):
return update_ds(session, ds)
async def update(session: SessionDep,user: CurrentUser, ds: CoreDatasource):
return update_ds(session,user, ds)
@router.post("/delete/{id}", response_model=CoreDatasource)
+12 -10
View File
@@ -22,10 +22,10 @@ from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, Cor
DatasourceConf, TableAndFields
def get_datasource_list(session: SessionDep):
statement = select(CoreDatasource).order_by(CoreDatasource.create_time.desc())
datasource_list = session.exec(statement).fetchall()
return datasource_list
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(
CoreDatasource.create_time.desc()).all()
def get_ds(session: SessionDep, id: int):
@@ -45,14 +45,15 @@ def check_status(session: SessionDep, ds: CoreDatasource):
return False
def check_name(session: SessionDep, ds: CoreDatasource):
def check_name(session: SessionDep, user: CurrentUser, ds: CoreDatasource):
if ds.id is not None:
ds_list = session.query(CoreDatasource).filter(
and_(CoreDatasource.name == ds.name, CoreDatasource.id != ds.id)).all()
and_(CoreDatasource.name == ds.name, CoreDatasource.id != ds.id, CoreDatasource.oid == user.oid)).all()
if ds_list is not None and len(ds_list) > 0:
raise 'Name exist'
else:
ds_list = session.query(CoreDatasource).filter(CoreDatasource.name == ds.name).all()
ds_list = session.query(CoreDatasource).filter(
and_(CoreDatasource.name == ds.name, CoreDatasource.oid == user.oid)).all()
if ds_list is not None and len(ds_list) > 0:
raise 'Name exist'
@@ -60,10 +61,11 @@ def check_name(session: SessionDep, ds: CoreDatasource):
def create_ds(session: SessionDep, user: CurrentUser, create_ds: CreateDatasource):
ds = CoreDatasource()
deepcopy_ignore_extra(create_ds, ds)
check_name(session, ds)
check_name(session, user, ds)
ds.create_time = datetime.datetime.now()
# status = check_status(session, ds)
ds.create_by = user.id
ds.oid = user.oid if user.oid is not None else 1
ds.status = "Success"
ds.type_name = db_type_relation()[ds.type]
record = CoreDatasource(**ds.model_dump())
@@ -85,9 +87,9 @@ def chooseTables(session: SessionDep, id: int, tables: List[CoreTable]):
updateNum(session, ds)
def update_ds(session: SessionDep, ds: CoreDatasource):
def update_ds(session: SessionDep, user: CurrentUser, ds: CoreDatasource):
ds.id = int(ds.id)
check_name(session, ds)
check_name(session, user, ds)
status = check_status(session, ds)
ds.status = "Success" if status is True else "Fail"
record = session.exec(select(CoreDatasource).where(CoreDatasource.id == ds.id)).first()
@@ -18,6 +18,7 @@ class CoreDatasource(SQLModel, table=True):
create_by: int = Field(sa_column=Column(BigInteger()))
status: str = Field(max_length=64, nullable=True)
num: str = Field(max_length=256, nullable=True)
oid: int = Field(sa_column=Column(BigInteger()))
class CoreTable(SQLModel, table=True):
@@ -53,6 +54,7 @@ class CreateDatasource(BaseModel):
create_by: int = 0
status: str = ''
num: str = ''
oid: int = 1
tables: List[CoreTable] = []
+59 -16
View File
@@ -3,20 +3,33 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException
import jwt
from fastapi import HTTPException, status, APIRouter
from fastapi.responses import StreamingResponse
# from fastapi.security import OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
from pydantic import ValidationError
from sqlmodel import select
from apps.chat.api.chat import create_chat
from apps.chat.models.chat_model import ChatMcp, CreateChat, ChatStart
from apps.chat.task.llm import LLMService, run_task
from apps.datasource.crud.datasource import get_datasource_list
from apps.system.crud.user import authenticate
from apps.system.models.system_model import AiModelDetail
from apps.system.crud.user import get_db_user
from apps.system.models.system_model import UserWsModel
from apps.system.models.user import UserModel
from apps.system.schemas.system_schema import BaseUserDTO
from apps.system.schemas.system_schema import UserInfoDTO
from common.core import security
from common.core.config import settings
from common.core.deps import SessionDep, get_current_user
from common.core.schemas import Token
from common.core.deps import SessionDep
from common.core.schemas import TokenPayload, XOAuth2PasswordBearer, Token
from common.core.security import create_access_token
reusable_oauth2 = XOAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
)
router = APIRouter(tags=["mcp"], prefix="/mcp")
@@ -35,21 +48,24 @@ router = APIRouter(tags=["mcp"], prefix="/mcp")
# ))
@router.get("/ds_list", operation_id="get_datasource_list")
async def datasource_list(session: SessionDep):
return get_datasource_list(session=session)
@router.get("/model_list", operation_id="get_model_list")
async def get_model_list(session: SessionDep):
return session.query(AiModelDetail).all()
# @router.get("/ds_list", operation_id="get_datasource_list")
# async def datasource_list(session: SessionDep):
# return get_datasource_list(session=session)
#
#
# @router.get("/model_list", operation_id="get_model_list")
# async def get_model_list(session: SessionDep):
# return session.query(AiModelDetail).all()
@router.post("/mcp_start", operation_id="mcp_start")
async def mcp_start(session: SessionDep, chat: ChatStart):
user = authenticate(session=session, account=chat.username, password=chat.password)
user: BaseUserDTO = authenticate(session=session, account=chat.username, password=chat.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect account or password")
if not user.oid or user.oid == 0:
raise HTTPException(status_code=400, detail="No associated workspace, Please contact the administrator")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
user_dict = user.to_dict()
t = Token(access_token=create_access_token(
@@ -61,9 +77,36 @@ async def mcp_start(session: SessionDep, chat: ChatStart):
@router.post("/mcp_question", operation_id="mcp_question")
async def mcp_question(session: SessionDep, chat: ChatMcp):
user = await get_current_user(session, chat.token)
try:
payload = jwt.decode(
chat.token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
# session_user = await get_user_info(session=session, user_id=token_data.id)
llm_service = LLMService(session, user, chat)
db_user: UserModel = get_db_user(session=session, user_id=token_data.id)
session_user = UserInfoDTO.model_validate(db_user.model_dump())
session_user.isAdmin = session_user.id == 1 and session_user.account == 'admin'
if session_user.isAdmin:
session_user = session_user
ws_model: UserWsModel = session.exec(
select(UserWsModel).where(UserWsModel.uid == session_user.id, UserWsModel.oid == session_user.oid)).first()
session_user.weight = ws_model.weight if ws_model else -1
session_user = UserInfoDTO.model_validate(session_user)
if not session_user:
raise HTTPException(status_code=404, detail="User not found")
if session_user.status != 1:
raise HTTPException(status_code=400, detail="Inactive user")
# ask
llm_service = LLMService(session, session_user, chat)
llm_service.init_record()
return StreamingResponse(run_task(llm_service, False), media_type="text/event-stream")
+7 -4
View File
@@ -1,7 +1,7 @@
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.crud.user import clean_user_cache, get_db_user, single_delete, user_ws_options
from apps.system.models.system_model import UserWsModel
from apps.system.models.user import UserModel
from apps.system.schemas.auth import CacheName, CacheNamespace
@@ -95,13 +95,14 @@ async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Tran
return await user_ws_options(session, current_user.id, trans)
@router.put("/ws/{oid}")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
# @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):
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!")
user_model: UserModel = get_db_user(session = session, user_id = current_user.id)
user_model.oid = oid
await clean_user_cache(user_model.id)
session.add(user_model)
session.commit()
@@ -173,13 +174,14 @@ async def batch_del(session: SessionDep, id_list: list[int]):
await single_delete(session, id)
@router.put("/language")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
async def langChange(session: SessionDep, current_user: CurrentUser, language: UserLanguage):
lang = language.language
if lang not in ["zh-CN", "en"]:
return {"message": "Language not supported"}
db_user: UserModel = get_db_user(session=session, user_id=current_user.id)
db_user.language = lang
await clean_user_cache(db_user.id)
session.add(db_user)
session.commit()
@@ -194,11 +196,12 @@ async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int):
session.commit()
@router.put("/pwd")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
async def pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdEditor):
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)
await clean_user_cache(db_user.id)
session.add(db_user)
session.commit()
+39 -51
View File
@@ -1,12 +1,27 @@
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache as original_cache
from functools import partial, wraps
from typing import Optional, Set, Any, Dict, Tuple
from typing import Optional, Set, Any, Dict, Tuple, Callable
from inspect import Parameter, signature
import logging
from contextlib import asynccontextmanager
import asyncio
logger = logging.getLogger(__name__)
# 锁管理
_cache_locks = {}
@asynccontextmanager
async def _get_cache_lock(key: str):
lock = _cache_locks.setdefault(key, asyncio.Lock())
async with lock:
try:
yield
finally:
if key in _cache_locks and not lock.locked():
del _cache_locks[key]
def should_skip_param(param: Parameter) -> bool:
"""判断参数是否应该被忽略(依赖注入参数)"""
return (
@@ -26,9 +41,7 @@ def custom_key_builder(
cacheName: Optional[str] = None,
keyExpression: Optional[str] = None,
) -> str:
"""
完全兼容FastAPICache的键生成器
"""
"""完全兼容FastAPICache的键生成器"""
if cacheName:
base_key = f"{namespace}:{cacheName}:"
@@ -38,7 +51,6 @@ def custom_key_builder(
bound_args = sig.bind_partial(*args, **kwargs)
bound_args.apply_defaults()
if keyExpression.startswith("args["):
import re
match = re.match(r"args\[(\d+)\]", keyExpression)
@@ -47,7 +59,6 @@ def custom_key_builder(
value = bound_args.args[index]
base_key += f"{value}:"
else:
parts = keyExpression.split('.')
value = bound_args.arguments[parts[0]]
for part in parts[1:]:
@@ -58,37 +69,27 @@ def custom_key_builder(
logger.warning(f"Failed to evaluate keyExpression '{keyExpression}': {str(e)}")
return base_key
# 获取函数签名
sig = signature(func)
# 自动识别要跳过的参数
sig = signature(func)
auto_skip_args = {
name for name, param in sig.parameters.items()
if should_skip_param(param)
}
# 合并用户指定的额外跳过参数
skip_args = auto_skip_args.union(additional_skip_args or set())
# 过滤kwargs
filtered_kwargs = {
k: v for k, v in kwargs.items() if k not in skip_args
}
# 过滤args - 将位置参数映射到它们的参数名
bound_args = sig.bind_partial(*args, **kwargs)
bound_args.apply_defaults()
filtered_args = []
for i, (name, value) in enumerate(bound_args.arguments.items()):
# 只处理位置参数 (在args中的参数)
if i < len(args) and name not in skip_args:
filtered_args.append(value)
filtered_args = tuple(filtered_args)
# 获取默认键生成器
default_key_builder = FastAPICache.get_key_builder()
# 调用默认键生成器(严格按照其要求的参数格式)
return default_key_builder(
func=func,
namespace=namespace,
@@ -105,9 +106,7 @@ def cache(
cacheName: Optional[str] = None,
keyExpression: Optional[str] = None,
):
"""
完全兼容的缓存装饰器
"""
"""完全兼容的缓存装饰器"""
def decorator(func):
if key_builder is None:
used_key_builder = partial(
@@ -121,24 +120,23 @@ def cache(
@wraps(func)
async def wrapper(*args, **kwargs):
# 准备键生成器参数
key_builder_args = {
"func": func,
"namespace": namespace,
"args": args,
"kwargs": kwargs
}
cache_key = used_key_builder(
func=func,
namespace=namespace or "",
args=args,
kwargs=kwargs
)
# 生成缓存键
cache_key = used_key_builder(**key_builder_args)
logger.debug(f"Generated cache key: {cache_key}")
# 使用原始缓存装饰器
return await original_cache(
expire=expire,
namespace=namespace,
key_builder=lambda *_, **__: cache_key # 直接使用预生成的key
)(func)(*args, **kwargs)
async with _get_cache_lock(cache_key):
backend = FastAPICache.get_backend()
cached_value = await backend.get(cache_key)
if cached_value is not None:
return cached_value
result = await func(*args, **kwargs)
await backend.set(cache_key, result, expire)
return result
return wrapper
return decorator
@@ -147,17 +145,10 @@ def clear_cache(
cacheName: Optional[str] = None,
keyExpression: Optional[str] = None,
):
"""
清除缓存的装饰器,参数与 @cache 保持一致
使用方式:
@clear_cache(namespace="user", cacheName="info", keyExpression="user_id")
async def update_user(user_id: int):
...
"""
"""清除缓存的装饰器"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 1. 生成缓存键(复用 custom_key_builder 逻辑)
cache_key = custom_key_builder(
func=func,
namespace=namespace or "",
@@ -167,13 +158,10 @@ def clear_cache(
keyExpression=keyExpression,
)
logger.debug(f"Clearing cache for key: {cache_key}")
# 2. 清除缓存
if await FastAPICache.get_backend().get(cache_key):
async with _get_cache_lock(cache_key):
await FastAPICache.clear(key=cache_key)
# 3. 执行原函数
return await func(*args, **kwargs)
result = await func(*args, **kwargs)
return result
return wrapper
return decorator
+3 -3
View File
@@ -129,11 +129,11 @@ template:
predict:
system: |
### 说明:
你是一个数据分析师,你的任务是根据给定的数据进行数据预测,我将以json格式给你一组数据,你帮我预测之后的数据(一段可以展示趋势的数据,至少2个周期),用json格式返回。
你是一个数据分析师,你的任务是根据给定的数据进行数据预测,我将以json格式给你一组数据,你帮我预测之后的数据(一段可以展示趋势的数据,至少2个周期),用json格式返回,返回的格式需要与传入的数据格式保持一致。
```json
无法预测或者不支持预测的数据请直接返回:"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)"
不需要返回原有数据,直接返回预测的部份
无法预测或者不支持预测的数据请直接返回(不需要返回JSON格式):"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)"
如果可以预测,则不需要返回原有数据,直接返回预测的部份
user: |
### 请使用 i18n: {lang} 对应的语言输出你的结果
+1
View File
@@ -26,6 +26,7 @@
"element-plus": "^2.10.1",
"element-plus-secondary": "^1.0.0",
"element-resize-detector": "^1.2.4",
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
+78 -2
View File
@@ -40,7 +40,7 @@
const getChatContainerHtml = (data) => {
return `
<div id="sqlbot-assistant-chat-container">
<iframe id="sqlbot-assistant-chat" allow="microphone" src="${data.domain_url}/#/assistant?id=${data.id}"></iframe>
<iframe id="sqlbot-assistant-chat-iframe-${data.id}" allow="microphone" src="${data.domain_url}/#/assistant?id=${data.id}"></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">
@@ -311,7 +311,7 @@
#sqlbot-assistant #sqlbot-assistant-chat-container .sqlbot-assistant-viewportnone{
display:none;
}
#sqlbot-assistant #sqlbot-assistant-chat-container #sqlbot-assistant-chat{
#sqlbot-assistant #sqlbot-assistant-chat-container #sqlbot-assistant-chat-iframe-${data.id}{
height:100%;
width:100%;
border: none;
@@ -334,7 +334,79 @@
const url = new URL(src)
return url.searchParams.get(key)
}
function parsrCertificate(config) {
const certificateList = config.certificate
if (!certificateList?.length) {
return null
}
const list = certificateList.map((item) => formatCertificate(item)).filter((item) => !!item)
return JSON.stringify(list)
}
function isEmpty(obj) {
return obj == null || typeof obj == 'undefined'
}
function formatCertificate(item) {
const { type, source, target, target_key, target_val } = item
let source_val = null
if (type.toLocaleLowerCase() == 'localstorage') {
source_val = localStorage.getItem(source)
}
if (type.toLocaleLowerCase() == 'sessionstorage') {
source_val = sessionStorage.getItem(source)
}
if (type.toLocaleLowerCase() == 'cookie') {
source_val = getCookie(source)
}
if (type.toLocaleLowerCase() == 'custom') {
source_val = source
}
if (isEmpty(source_val)) {
return null
}
return {
target,
key: target_key || source,
value: (target_val && eval(target_val)) || source_val,
}
}
function getCookie(key) {
if (!key || !document.cookie) {
return null
}
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim()
if (cookie.startsWith(key + '=')) {
return decodeURIComponent(cookie.substring(key.length + 1))
}
}
return null
}
function registerMessageEvent(id, data) {
const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`)
const url = iframe.src
const eventName = 'sqlbot_assistant_event'
window.addEventListener('message', (event) => {
if (event.data?.eventName === eventName) {
if (event.data?.messageId !== id) {
return
}
if (event.data?.busi == 'ready' && event.data?.ready) {
const certificate = parsrCertificate(data)
console.log(certificate)
params = {
busi: 'certificate',
certificate,
eventName,
messageId: id,
}
const contentWindow = iframe.contentWindow
contentWindow.postMessage(params, url)
}
}
})
}
function loadScript(src, id) {
const domain_url = getDomain(src)
let url = `${domain_url}/api/v1/system/assistant/info/${id}`
@@ -353,6 +425,10 @@
tempData = Object.assign(tempData, config)
}
initsqlbot_assistant(tempData)
if (data.type == 1) {
registerMessageEvent(id, tempData)
// postMessage the certificate to iframe
}
})
}
function getDomain(src) {
+1
View File
@@ -14,3 +14,4 @@ export const workspaceUwsDelete = (data: any) => request.delete('/system/workspa
export const workspaceDelete = (id: any) => request.delete(`/system/workspace/${id}`)
export const workspaceList = () => request.get('/system/workspace')
export const workspaceDetail = (id: any) => request.get(`/system/workspace/${id}`)
export const uwsOption = (params: any) => request.get('system/workspace/uws/option', { params })
Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 10.5759L13.4212 5.15476C13.5514 5.02459 13.7624 5.02459 13.8926 5.15476L14.364 5.62616C14.4942 5.75634 14.4942 5.96739 14.364 6.09757L8.47145 11.9901C8.2111 12.2505 7.78899 12.2505 7.52864 11.9901L1.63608 6.09757C1.50591 5.96739 1.50591 5.75634 1.63608 5.62616L2.10749 5.15476C2.23766 5.02459 2.44872 5.02459 2.57889 5.15476L8.00004 10.5759Z" fill="#646A73"/>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 10.5759L13.4212 5.15476C13.5514 5.02459 13.7624 5.02459 13.8926 5.15476L14.364 5.62616C14.4942 5.75634 14.4942 5.96739 14.364 6.09757L8.47145 11.9901C8.2111 12.2505 7.78899 12.2505 7.52864 11.9901L1.63608 6.09757C1.50591 5.96739 1.50591 5.75634 1.63608 5.62616L2.10749 5.15476C2.23766 5.02459 2.44872 5.02459 2.57889 5.15476L8.00004 10.5759Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 481 B

After

Width:  |  Height:  |  Size: 455 B

@@ -1,4 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.25 9.75H6.75C4.67893 9.75 2.25 11.1986 2.25 13.8V15.825C2.25 16.1978 2.58579 16.5 3 16.5H15C15.4142 16.5 15.75 16.1978 15.75 15.825V13.8C15.75 11.1964 13.3211 9.75 11.25 9.75Z" fill="#1CBA90"/>
<path d="M5.25 5.25C5.25 7.32107 6.92893 9 9 9C11.0711 9 12.75 7.32107 12.75 5.25C12.75 3.17893 11.0711 1.5 9 1.5C6.92893 1.5 5.25 3.17893 5.25 5.25Z" fill="#1CBA90"/>
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M11.25 9.75H6.75C4.67893 9.75 2.25 11.1986 2.25 13.8V15.825C2.25 16.1978 2.58579 16.5 3 16.5H15C15.4142 16.5 15.75 16.1978 15.75 15.825V13.8C15.75 11.1964 13.3211 9.75 11.25 9.75Z" />
<path d="M5.25 5.25C5.25 7.32107 6.92893 9 9 9C11.0711 9 12.75 7.32107 12.75 5.25C12.75 3.17893 11.0711 1.5 9 1.5C6.92893 1.5 5.25 3.17893 5.25 5.25Z" />
</svg>

Before

Width:  |  Height:  |  Size: 478 B

After

Width:  |  Height:  |  Size: 438 B

@@ -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="M6 8.66667H10C11.841 8.66667 14 9.95237 14 12.2667V14.0667C14 14.398 13.7015 14.6667 13.3333 14.6667H2.66667C2.29848 14.6667 2 14.398 2 14.0667V12.2667C2 9.95432 4.15905 8.66667 6 8.66667ZM12.6667 13.3333V12.2222C12.6667 10.8212 11.2473 10 10 10H6C4.77998 10 3.33333 10.7811 3.33333 12.2222V13.3333H12.6667ZM8 8C6.15905 8 4.66667 6.50762 4.66667 4.66667C4.66667 2.82572 6.15905 1.33333 8 1.33333C9.84095 1.33333 11.3333 2.82572 11.3333 4.66667C11.3333 6.50762 9.84095 8 8 8ZM8 6.66667C9.10457 6.66667 10 5.77123 10 4.66667C10 3.5621 9.10457 2.66667 8 2.66667C6.89543 2.66667 6 3.5621 6 4.66667C6 5.77123 6.89543 6.66667 8 6.66667Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 761 B

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.8057 13.6388C11.7701 14.4894 10.4447 15 9.00003 15C5.68632 15 3.00003 12.3137 3.00003 9C3.00003 8.69123 3.02335 8.38791 3.06833 8.0917L3.77428 8.50919C4.05573 8.67564 4.40051 8.42728 4.3318 8.10761L3.79257 5.59892C3.74659 5.385 3.52799 5.25572 3.31837 5.31849L0.860228 6.05455C0.546995 6.14835 0.495468 6.57013 0.77691 6.73657L1.6979 7.28123C1.56848 7.83317 1.50003 8.40859 1.50003 9C1.50003 13.1421 4.85789 16.5 9.00003 16.5C11.0063 16.5 12.8286 15.7122 14.1745 14.4291L12.8057 13.6388Z" fill="currentColor"/>
<path d="M5.4246 4.1812C6.42315 3.43909 7.66031 3 9.00003 3C12.3137 3 15 5.68629 15 9C15 9.21238 14.989 9.42219 14.9675 9.62887L14.145 9.16094C13.8608 8.99924 13.5203 9.25334 13.5943 9.57182L14.1756 12.0711C14.2251 12.2842 14.4459 12.4098 14.6544 12.3435L17.0999 11.5663C17.4115 11.4673 17.4559 11.0447 17.1717 10.883L16.365 10.424C16.4536 9.96297 16.5 9.48691 16.5 9C16.5 4.85786 13.1422 1.5 9.00003 1.5C7.09588 1.5 5.35746 2.20961 4.03481 3.3788L5.4246 4.1812Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 3V12H15V3H3ZM1.5 2.25C1.5 1.83562 1.8375 1.5 2.25 1.5H15.75C16.1625 1.5 16.5 1.83562 16.5 2.25V12.75C16.5 13.1644 16.1625 13.5 15.75 13.5H2.25C1.8375 13.5 1.5 13.1644 1.5 12.75V2.25Z" fill="currentColor"/>
<path d="M11.5789 5.09473L12.6394 6.15523L9.0881 9.70648L7.35635 7.9751L6.15523 9.1766L5.09473 8.11573L7.35635 5.8541L9.08773 7.58548L11.5789 5.09473Z" fill="currentColor"/>
<path d="M12.75 15H5.25V16.5H12.75V15Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 597 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path d="M2.93782 14.5935C2.1193 13.7061 1.49299 12.6387 1.125 11.4573L2.43707 9.66437C2.72657 9.26876 2.72657 8.73124 2.43707 8.33563L1.125 6.54267C1.49299 5.36128 2.1193 4.29386 2.93782 3.40655L5.14275 3.6464C5.63031 3.69944 6.09604 3.43052 6.29385 2.98174L7.18964 0.949401C7.77221 0.818869 8.37806 0.75 9 0.75C9.62194 0.75 10.2278 0.818869 10.8104 0.949401L11.7062 2.98174C11.904 3.43052 12.3697 3.69944 12.8573 3.6464L15.0622 3.40655C15.8807 4.29386 16.507 5.36128 16.875 6.54267L15.5629 8.33563C15.2734 8.73124 15.2734 9.26876 15.5629 9.66437L16.875 11.4573C16.507 12.6387 15.8807 13.7061 15.0622 14.5935L12.8573 14.3536C12.3697 14.3006 11.904 14.5695 11.7062 15.0183L10.8104 17.0506C10.2278 17.1811 9.62194 17.25 9 17.25C8.37806 17.25 7.77221 17.1811 7.18964 17.0506L6.29385 15.0183C6.09604 14.5695 5.63031 14.3006 5.14275 14.3536L2.93782 14.5935ZM9 12C10.6569 12 12 10.6569 12 9C12 7.34315 10.6569 6 9 6C7.34315 6 6 7.34315 6 9C6 10.6569 7.34315 12 9 12Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.2276 13.6116C6.57261 13.7278 6.65136 14.0653 6.40198 14.3271C6.02398 14.7099 3.73835 15.0264 3.0596 15.1258C2.92985 15.1453 2.81023 15.1843 2.68573 15.1134C2.50685 15.0137 2.47685 14.8389 2.51098 14.606C2.60285 13.976 2.89348 12.3729 3.33823 11.2067C3.37798 11.1024 3.50885 11.0859 3.61273 11.0942C3.82873 11.0942 3.94535 11.1902 4.0241 11.4107C4.32335 12.2717 4.87648 12.9249 5.67448 13.3701C5.84923 13.4661 6.03636 13.5448 6.2276 13.6116ZM15.8674 2.2348C15.9086 2.49017 15.9379 2.7418 15.9337 3.0013C15.8962 4.2913 15.6142 5.53142 15.1459 6.73442C14.586 8.17255 13.7651 9.43742 12.6165 10.4844C12.5212 10.5707 12.492 10.6614 12.5044 10.7807C12.546 11.168 12.5749 11.9304 12.6165 12.3182C12.7076 13.2204 12.3926 13.9577 11.6587 14.4812C10.8709 15.0414 10.0537 15.5608 9.24561 16.088C8.73561 16.418 8.10936 16.1169 8.05123 15.5154C7.98486 14.8232 7.93498 13.7559 7.88548 13.0637C7.86036 12.7134 7.86898 12.7134 7.53298 12.668C6.63298 12.5525 5.93248 12.1281 5.45173 11.3619C5.20273 10.9663 5.07823 10.5294 5.03698 10.0719C5.02423 9.93205 4.96648 9.89905 4.83785 9.8908C4.11223 9.85367 3.0116 9.8083 2.28598 9.75467C1.68485 9.70967 1.37773 9.07892 1.70098 8.55167C1.92523 8.18042 2.16598 7.81817 2.40223 7.45142C2.65498 7.0558 2.9036 6.6643 3.15673 6.26867C3.62098 5.54792 4.28848 5.16055 5.14273 5.1523C5.6321 5.14817 6.49648 5.2348 6.98548 5.28805C7.08111 5.2963 7.14748 5.2843 7.20936 5.21417C8.34561 3.95305 9.74698 3.1003 11.3392 2.53142C12.2354 2.21102 13.1668 1.9995 14.1135 1.90142C14.5942 1.85155 15.0754 1.83505 15.552 1.92167C15.8134 1.97117 15.8261 1.98355 15.8674 2.2348ZM14.2687 3.39317C13.4385 3.47942 12.6307 3.66317 11.844 3.94442C10.4002 4.45967 9.24036 5.20105 8.33136 6.2098C7.92036 6.67255 7.39498 6.8293 6.82236 6.77942C6.68282 6.76378 6.54344 6.74678 6.40423 6.72842C5.99035 6.67978 5.57408 6.65436 5.15735 6.6523C4.82248 6.6553 4.6166 6.7723 4.42048 7.0768L4.04548 7.66405C3.90155 7.8898 3.75743 8.11543 3.6131 8.34092C3.92585 8.35967 4.24085 8.37767 4.55885 8.3938C4.91885 8.41705 5.60885 8.50255 5.9411 8.78005C6.32998 9.10367 6.49273 9.50942 6.53061 9.93655C6.55423 10.196 6.61686 10.3978 6.72186 10.5643C6.95473 10.9355 7.25061 11.1193 7.73361 11.1816C7.79623 11.1902 7.82511 11.1939 7.86261 11.1996C8.00661 11.222 8.12136 11.2464 8.26198 11.2963C8.54061 11.3957 8.79711 11.5625 9.00298 11.8213C9.18673 12.0523 9.28236 12.665 9.33298 12.9152C9.35586 13.0273 9.42523 13.9352 9.44286 14.1699C9.89734 13.8759 10.3457 13.5724 10.7876 13.2598C11.0715 13.0573 11.1626 12.8499 11.1251 12.4775C11.1064 12.3058 11.0265 11.0672 11.0126 10.9363C10.9511 10.346 11.1555 9.7843 11.6062 9.37555C12.5479 8.51717 13.2532 7.46192 13.7482 6.1903C14.1195 5.23705 14.3415 4.30142 14.4124 3.37967C14.3662 3.38342 14.3182 3.38792 14.2687 3.39317ZM11.8125 7.12517C11.5141 7.12517 11.228 7.00665 11.017 6.79567C10.806 6.58469 10.6875 6.29854 10.6875 6.00017C10.6875 5.70181 10.806 5.41566 11.017 5.20468C11.228 4.9937 11.5141 4.87517 11.8125 4.87517C12.1109 4.87517 12.397 4.9937 12.608 5.20468C12.819 5.41566 12.9375 5.70181 12.9375 6.00017C12.9375 6.29854 12.819 6.58469 12.608 6.79567C12.397 7.00665 12.1109 7.12517 11.8125 7.12517Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+19 -5
View File
@@ -25,13 +25,16 @@ const routerList = computed(() => {
if (showSysmenu.value) {
return router.getRoutes().filter((route) => route.path.includes('/system') && !route.redirect)
}
return router.getRoutes().filter((route) => {
return (
!route.path.includes('canvas') &&
!route.path.includes('member') &&
!route.path.includes('permission') &&
!route.path.includes('preview') &&
route.path !== '/login' &&
!route.path.includes('/system') &&
!route.redirect &&
(route.path.includes('set') || !route.redirect) &&
route.path !== '/:pathMatch(.*)*' &&
!route.path.includes('dsTable')
)
@@ -45,17 +48,28 @@ const routerList = computed(() => {
</el-menu>
</template>
<style lang="less" scoped>
<style lang="less">
.ed-menu-vertical {
--ed-menu-item-height: 40px;
--ed-menu-bg-color: transparent;
--ed-menu-base-level-padding: 4px;
border-right: none;
.ed-menu-item {
height: 40px;
height: 40px !important;
border-radius: 6px !important;
&.is-active {
background-color: #fff !important;
border-radius: 6px;
}
}
.ed-sub-menu .ed-sub-menu__title:hover {
border-radius: 6px;
&.is-active.is-active.is-active {
background-color: #1cba901a;
}
.ed-sub-menu.is-active {
.ed-sub-menu__title {
color: #1cba90 !important;
}
}
}
+7 -2
View File
@@ -8,13 +8,16 @@ import model from '@/assets/svg/icon_dataset_filled.svg'
import ds from '@/assets/svg/ds.svg'
import dashboard from '@/assets/svg/dashboard.svg'
import chat from '@/assets/svg/chat.svg'
import setting from '@/assets/svg/icon_setting_filled.svg'
import icon_user from '@/assets/svg/icon_user.svg'
const iconMap = {
icon_ai: model,
ds,
dashboard,
chat,
user,
setting,
icon_user: icon_user,
workspace,
} as { [key: string]: any }
@@ -38,7 +41,9 @@ const MenuItem = defineComponent({
const router = useRouter()
const handleMenuClick = (e: any) => {
router.push(e.index)
if (e.index) {
router.push(e.redirect || e.index)
}
}
return () => {
@@ -50,7 +55,7 @@ const MenuItem = defineComponent({
if (children?.length) {
return h(
ElSubMenu,
{ index: path, onClick: (e: any) => handleMenuClick(e) },
{ index: path, onClick: () => handleMenuClick(props.menu) },
{
title: () => titleWithIcon(props),
default: () => children.map((ele: any) => h(MenuItem, { menu: ele })),
@@ -43,6 +43,7 @@ const handleDefaultWorkspaceChange = (item: any) => {
currentWorkspace.value = { id: item.id, name: item.name }
userApi.ws_change(item.id).then(() => {
ElMessage.success(t('common.switch_success'))
location.reload()
})
emit('selectWorkspace', item)
}
+40 -1
View File
@@ -8,6 +8,7 @@
"common": {
"input_content": "Please Input Content",
"empty": " ",
"may_not_exist": "No results yet, the user may not exist",
"back": "Back",
"confirm": "Confirm",
"cancel": "Cancel",
@@ -94,6 +95,7 @@
"type": "Data Source Type",
"status": "Status",
"actions": "Actions",
"local_excelcsv": "Local Excel/CSV",
"pieces_in_total": "Showing {ms} records",
"test_connection": "Test Connection",
"check": "check",
@@ -308,7 +310,44 @@
"there_are": "There are",
"2_dashboards": "{msg} dashboards",
"smart_data_centers": "{msg} smart data centers",
"confirm_to_delete": "This data source is in use. Deleting it will cause these resources to not work properly. Confirm to delete?"
"confirm_to_delete": "This data source is in use. Deleting it will cause these resources to not work properly. Confirm to delete?",
"id_account_to_add": "Search user ID/account to add",
"find_user": "Find user",
"add_successfully": "Add successfully",
"member_management": "Member management",
"permission_configuration": "Permission configuration",
"set": "Set",
"operate_with_caution": "After deletion, the users under the workspace will be removed and all resources will be deleted. Please operate with caution."
},
"permission": {
"search_rule_group": "Search rule group",
"add_rule_group": "Add rule group",
"permission_rule": "Permission rule",
"restricted_user": "Restricted user",
"set_rule": "Set rule",
"set_user": "Set user",
"2": "{msg}",
"238_people": "{msg} people",
"no_permission_rule": "No permission rule",
"set_permission_rule": "Set permission rule",
"basic_information": "Basic information",
"rule_group_name": "Rule group name",
"rule_name": "Rule name",
"type": "Type",
"data_source": "Data source",
"data_table": "Data table",
"select_restricted_user": "Select restricted user",
"rule_group_1": "Delete rule group: {msg}?",
"no_rule": "No rule",
"row_permission": "Row permission",
"column_permission": "Column permission",
"rule_rule_1": "Delete rule: {msg}?",
"no_content": "No content",
"edit_column_permission": "Edit column permission",
"edit_row_permission": "Edit row permission",
"add_column_permission": "Add column permission",
"add_row_permission": "Add row permission",
"no_fields_yet": "No fields yet"
},
"chat": {
"chart_type": {
+43 -4
View File
@@ -1,5 +1,5 @@
{
"menu": {
"menu": {
"Data Q&A": "智能问数",
"Data Connections": "数据源",
"Dashboard": "仪表盘",
@@ -7,6 +7,7 @@
},
"common": {
"input_content": "请输入内容",
"may_not_exist": "暂无结果,用户可能不存在",
"empty": "",
"back": "返回",
"confirm": "确认",
@@ -94,10 +95,12 @@
"data_predict": "数据分析",
"chat_search": "搜索",
"thinking": "思考中",
"thinking_step": "思考过程"
"thinking_step": "思考过程",
"ask_again": "重新生成"
},
"ds": {
"title": "数据源",
"local_excelcsv": "本地 Excel/CSV",
"add": "添加数据源",
"delete": "删除数据源",
"name": "数据源名称",
@@ -328,7 +331,43 @@
"there_are": "有 ",
"2_dashboards": "{msg} 个仪表板",
"smart_data_centers": "{msg} 个智能问数",
"confirm_to_delete": "正在使用此数据源,删除后会导致这些资源无法正常使用,确认删除?"
"confirm_to_delete": "正在使用此数据源,删除后会导致这些资源无法正常使用,确认删除?",
"id_account_to_add": "搜索用户ID/账号添加",
"find_user": "查找用户",
"add_successfully": "添加成功",
"member_management": "成员管理",
"permission_configuration": "权限配置",
"set": "设置",
"operate_with_caution": "删除后,该工作空间下的用户将被移除,所有资源也将被删除,请谨慎操作。"
},
"permission": {
"search_rule_group": "搜索规则组",
"add_rule_group": "添加规则组",
"permission_rule": "权限规则",
"restricted_user": "受限用户",
"set_rule": "设置规则",
"set_user": "设置用户",
"2": "{msg} 个",
"238_people": "{msg} 人",
"no_permission_rule": "暂无权限规则",
"set_permission_rule": "设置权限规则",
"rule_group_name": "规则组名称",
"rule_name": "规则名称",
"type": "类型",
"data_source": "数据源",
"data_table": "数据表",
"select_restricted_user": "选择受限用户",
"rule_group_1": "是否删除规则组:{msg}?",
"no_rule": "暂无规则",
"row_permission": "行权限",
"column_permission": "列权限",
"rule_rule_1": "是否删除规则:{msg}?",
"no_content": "暂无内容",
"edit_column_permission": "编辑列权限",
"edit_row_permission": "编辑行权限",
"add_column_permission": "添加列权限",
"add_row_permission": "添加行权限",
"no_fields_yet": "暂无字段"
},
"chat": {
"chart_type": {
@@ -366,4 +405,4 @@
"back_community": "还原至社区版",
"confirm_tips": "确定还原至社区版?"
}
}
}
+30 -5
View File
@@ -11,9 +11,14 @@ import DashboardPreview from '@//views/dashboard/preview/SQPreviewSingle.vue'
import Dashboard from '@/views/dashboard/index.vue'
import Model from '@/views/system/model/Model.vue'
import assistant from '@/views/embedded/assistant.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'
import Workspace from '@/views/system/workspace/index.vue'
import { i18n } from '@/i18n'
import { watchRouter } from './watch'
const t = i18n.global.t
const router = createRouter({
history: createWebHashHistory(),
routes: [
@@ -31,7 +36,7 @@ const router = createRouter({
path: 'index',
name: 'chat',
component: chat,
meta: { title: 'Data Q&A', icon: 'chat' },
meta: { title: t('menu.Data Q&A'), icon: 'chat' },
},
],
},
@@ -56,7 +61,7 @@ const router = createRouter({
path: 'index',
name: 'ds',
component: Datasource,
meta: { title: 'Data Connections', icon: 'ds' },
meta: { title: t('menu.Data Connections'), icon: 'ds' },
},
],
},
@@ -69,7 +74,27 @@ const router = createRouter({
path: 'index',
name: 'dashboard',
component: Dashboard,
meta: { title: 'Dashboard', icon: 'dashboard' },
meta: { title: t('dashboard.dashboard'), icon: 'dashboard' },
},
],
},
{
path: '/set',
component: LayoutDsl,
redirect: '/set/member',
meta: { title: t('workspace.set'), icon: 'setting' },
children: [
{
path: '/set/member',
name: 'member',
component: Member,
meta: { title: t('workspace.member_management') },
},
{
path: '/set/permission',
name: 'permission',
component: Permission,
meta: { title: t('workspace.permission_configuration') },
},
],
},
@@ -126,13 +151,13 @@ const router = createRouter({
path: 'user',
name: 'user',
component: User,
meta: { title: '用户管理', icon: 'user' },
meta: { title: t('user.user_management'), icon: 'user' },
},
{
path: 'workspace',
name: 'workspace',
component: Workspace,
meta: { title: '工作空间', icon: 'workspace' },
meta: { title: t('user.workspace'), icon: 'workspace' },
},
{
path: 'model',
+16
View File
@@ -9,6 +9,8 @@ interface AssistantState {
token: string
assistant: boolean
flag: number
type: number
certificate: string
}
export const AssistantStore = defineStore('assistant', {
@@ -17,9 +19,14 @@ export const AssistantStore = defineStore('assistant', {
token: '',
assistant: false,
flag: 0,
type: 0,
certificate: '',
}
},
getters: {
getCertificate(): string {
return this.certificate
},
getToken(): string {
return this.token
},
@@ -29,8 +36,17 @@ export const AssistantStore = defineStore('assistant', {
getFlag(): number {
return this.flag
},
getType(): number {
return this.type
},
},
actions: {
setCertificate(certificate: string) {
this.certificate = certificate
},
setType(type: number) {
this.type = type
},
setToken(token: string) {
this.token = token
},
+9 -11
View File
@@ -2,25 +2,23 @@ import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
const md = new MarkdownIt({
html: true,
linkify: true,
html: false,
linkify: false,
breaks: true,
typographer: true,
quotes: '“”‘’',
highlight: (str: any, lang: any): string => {
if (lang && hljs.getLanguage(lang)) {
try {
return `
<pre>
<code class="hljs">
${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
</code>
</pre>
`
return `<pre class="hljs">
<code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code>
</pre>`
} catch (e) {
console.error(e)
return str
}
}
return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code></pre>'
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
},
})
+56
View File
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { datetimeFormat } from '@/utils/utils.ts'
import type { ChatMessage } from '@/api/chat.ts'
defineProps<{
message: ChatMessage
}>()
</script>
<template>
<div class="tool-container">
<div class="tool-btns">
<slot></slot>
</div>
<div class="tool-times">
<div></div>
<div class="time">
{{ datetimeFormat(message?.record?.create_time) }}
</div>
</div>
</div>
</template>
<style scoped lang="less">
.tool-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
row-gap: 8px;
min-height: 22px;
margin-top: 12px;
margin-bottom: 12px;
.tool-times {
flex: 1;
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: rgba(100, 106, 115, 1);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.time {
white-space: nowrap;
}
}
}
</style>
@@ -53,7 +53,6 @@ function clickQuestion(question: string): void {
<style scoped lang="less">
.recommend-questions {
padding: 8px;
font-size: 14px;
font-weight: 500;
line-height: 22px;
@@ -69,7 +68,7 @@ function clickQuestion(question: string): void {
.question-grid {
display: grid;
grid-gap: 12px;
grid-template-columns: repeat(2, 50%);
grid-template-columns: repeat(2, calc(50% - 6px));
}
.question {
@@ -192,6 +192,7 @@ defineExpose({ sendMessage, index: index.value, chatList: _chatList })
:loading="_loading"
>
<MdComponent :message="message.record?.analysis" style="margin-top: 12px" />
<slot></slot>
<slot name="footer"></slot>
</BaseAnswer>
</template>
+15 -3
View File
@@ -70,9 +70,9 @@ onMounted(() => {
<template>
<div class="base-answer-block">
<el-button v-if="loading || hasReasoning" class="thinking-btn" @click="clickShow">
<el-button v-if="message.isTyping || hasReasoning" class="thinking-btn" @click="clickShow">
<div class="thinking-btn-inner">
<span v-if="loading">{{ t('qa.thinking') }}</span>
<span v-if="message.isTyping">{{ t('qa.thinking') }}</span>
<span v-else>{{ t('qa.thinking_step') }}</span>
<span class="btn-icon">
<el-icon v-if="show">
@@ -140,7 +140,14 @@ onMounted(() => {
line-height: 22px;
font-weight: 400;
font-size: 14px;
color: rgba(143, 149, 158, 1);
color: rgba(143, 149, 158, 1) !important;
.markdown-body {
color: rgba(143, 149, 158, 1) !important;
line-height: 22px;
font-weight: 400;
font-size: 14px;
}
padding-bottom: 8px;
border-bottom: 1px solid rgba(31, 35, 41, 0.15);
@@ -154,6 +161,11 @@ onMounted(() => {
.answer-container {
width: 100%;
line-height: 24px;
font-size: 16px;
font-weight: 400;
color: rgba(31, 35, 41, 1);
}
}
</style>
@@ -193,6 +193,7 @@ defineExpose({ sendMessage, index: index.value, chatList: _chatList })
<template>
<BaseAnswer v-if="message" :message="message" :reasoning-name="['predict']" :loading="_loading">
<MdComponent :message="message.record?.predict_content" style="margin-top: 12px" />
<slot></slot>
<slot name="footer"></slot>
</BaseAnswer>
</template>
@@ -0,0 +1,145 @@
<script setup lang="ts">
import type { ChatMessage } from '@/api/chat.ts'
import DisplayChartBlock from '@/views/chat/component/DisplayChartBlock.vue'
import { computed, ref } from 'vue'
import { concat } from 'lodash-es'
import type { ChartTypes } from '@/views/chat/component/BaseChart.ts'
const props = withDefaults(
defineProps<{
message: ChatMessage
isPredict?: boolean
}>(),
{
isPredict: false,
}
)
const dataObject = computed<{
fields: Array<string>
data: Array<{ [key: string]: any }>
}>(() => {
if (props.message?.record?.data) {
if (typeof props.message?.record?.data === 'string') {
return JSON.parse(props.message.record.data)
} else {
return props.message.record.data
}
}
return {}
})
const data = computed(() => {
if (props.isPredict) {
let _list = []
if (
props.message?.record?.predict_data &&
typeof props.message?.record?.predict_data === 'string'
) {
if (
props.message?.record?.predict_data.length > 0 &&
props.message?.record?.predict_data.trim().startsWith('[') &&
props.message?.record?.predict_data.trim().endsWith(']')
) {
try {
_list = JSON.parse(props.message?.record?.predict_data)
} catch (e) {
console.error(e)
}
}
} else {
if (props.message?.record?.predict_data.length > 0) {
_list = props.message?.record?.predict_data
}
}
if (_list.length == 0) {
return _list
}
if (dataObject.value.data && dataObject.value.data.length > 0) {
return concat(dataObject.value.data, _list)
}
return _list
} else {
return dataObject.value.data
}
})
const chartRef = ref()
const chartObject = computed<{
type: ChartTypes
title: string
axis: {
x: { name: string; value: string }
y: { name: string; value: string }
series: { name: string; value: string }
}
columns: Array<{ name: string; value: string }>
}>(() => {
if (props.message?.record?.chart) {
return JSON.parse(props.message.record.chart)
}
return {}
})
const currentChartType = ref<ChartTypes | undefined>(undefined)
const chartType = computed<ChartTypes>({
get() {
if (currentChartType.value) {
return currentChartType.value
}
return chartObject.value.type ?? 'table'
},
set(v) {
currentChartType.value = v
},
})
</script>
<template>
<div
v-if="
(!isPredict && (message?.record?.sql || message?.record?.chart)) ||
(isPredict && message?.record?.chart && data.length > 0)
"
class="chart-component-container"
>
<div class="header-bar">todo</div>
<div v-if="message?.record?.chart" class="chart-block">
<DisplayChartBlock
:id="message.record.id"
ref="chartRef"
:chart-type="chartType"
:message="message"
:data="data"
/>
</div>
</div>
</template>
<style scoped lang="less">
.chart-component-container {
width: 100%;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
border: 1px solid rgba(222, 224, 227, 1);
border-radius: 12px;
.header-bar {
height: 32px;
}
.chart-block {
height: 352px;
width: 100%;
}
}
</style>
@@ -45,6 +45,7 @@ function clickPredict() {
.prefix-title {
color: rgba(28, 186, 144, 1);
white-space: nowrap;
}
}
</style>
@@ -61,29 +61,26 @@ defineExpose({
</script>
<template>
<div v-if="message?.record">
<div v-if="message.record.chart" class="chart-base-container">
<div v-if="data?.length > 0">
<ChartComponent
v-if="message.record.id"
:id="id ?? 'default_chat_id'"
ref="chartRef"
:type="chartType"
:columns="chartObject?.columns"
:x="xAxis"
:y="yAxis"
:series="series"
:data="data"
/>
</div>
<el-empty v-else description="No Data" />
</div>
<div v-if="message.record?.chart" class="chart-base-container">
<ChartComponent
v-if="message.record.id && data?.length > 0"
:id="id ?? 'default_chat_id'"
ref="chartRef"
:type="chartType"
:columns="chartObject?.columns"
:x="xAxis"
:y="yAxis"
:series="series"
:data="data"
/>
<el-empty v-else description="No Data" />
</div>
</template>
<style scoped lang="less">
.chart-base-container {
padding: 20px;
height: 100%;
width: 100%;
background: rgba(224, 224, 226, 0.29);
}
</style>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import md from '@/utils/markdown.ts'
import 'highlight.js/styles/github-dark-dimmed.css'
import 'highlight.js/styles/github-dark-dimmed.min.css'
import 'github-markdown-css/github-markdown-light.css'
import { computed } from 'vue'
const props = defineProps<{
@@ -13,7 +14,22 @@ const renderMd = computed(() => {
</script>
<template>
<div v-dompurify-html="renderMd"></div>
<div v-dompurify-html="renderMd" class="markdown-body md-render-container"></div>
</template>
<style scoped lang="less"></style>
<style lang="less">
.md-render-container {
.hljs {
overflow: auto;
padding: 1rem;
display: block;
}
//ul {
// padding-left: 16px;
//}
//ol {
// padding-left: 16px;
//}
}
</style>
+136 -25
View File
@@ -116,25 +116,66 @@
@finish="onChartAnswerFinish"
@error="onChartAnswerError"
>
<div style="height: 100px; width: 100%; background: grey">todo</div>
<div style="padding: 0 22px; display: flex; justify-content: flex-end">
<el-button
text
type="primary"
:disabled="isTyping"
@click="clickAnalysis(message.record?.id)"
>
{{ t('chat.data_analysis') }}
</el-button>
<el-button
text
type="primary"
:disabled="isTyping"
@click="clickPredict(message.record?.id)"
>
{{ t('chat.data_predict') }}
</el-button>
<ChartBlock style="margin-top: 12px" :message="message" />
<div
v-if="message.record?.error && message.record?.error?.trim().length > 0"
class="error-container"
>
{{ message.record?.error }}
</div>
<ChatToolBar :message="message">
<div class="tool-btns">
<el-tooltip effect="dark" :content="t('qa.ask_again')" placement="top">
<el-button
class="tool-btn"
text
:disabled="isTyping"
@click="askAgain(message)"
>
<el-icon size="18">
<icon_replace_outlined />
</el-icon>
</el-button>
</el-tooltip>
<template v-if="message.record?.chart">
<div class="divider"></div>
<div>
<el-button
class="tool-btn"
text
:disabled="isTyping"
@click="clickAnalysis(message.record?.id)"
>
<span class="tool-btn-inner">
<el-icon size="18">
<icon_screen_outlined />
</el-icon>
<span class="btn-text">
{{ t('chat.data_analysis') }}
</span>
</span>
</el-button>
</div>
<div>
<el-button
class="tool-btn"
text
:disabled="isTyping"
@click="clickPredict(message.record?.id)"
>
<span class="tool-btn-inner">
<el-icon size="18">
<icon_start_outlined />
</el-icon>
<span class="btn-text">
{{ t('chat.data_predict') }}
</span>
</span>
</el-button>
</div>
</template>
</div>
</ChatToolBar>
<template #footer>
<RecommendQuestion
:questions="message.recommended_question"
@@ -156,7 +197,15 @@
:message="message"
@finish="onAnalysisAnswerFinish"
@error="onAnalysisAnswerError"
/>
>
<div
v-if="message.record?.error && message.record?.error?.trim().length > 0"
class="error-container"
>
{{ message.record?.error }}
</div>
<ChatToolBar :message="message" />
</AnalysisAnswer>
<PredictAnswer
v-if="
message?.record?.predict_record_id !== undefined &&
@@ -170,7 +219,16 @@
:message="message"
@finish="onPredictAnswerFinish"
@error="onPredictAnswerError"
/>
>
<ChartBlock style="margin-top: 12px" :message="message" is-predict />
<div
v-if="message.record?.error && message.record?.error?.trim().length > 0"
class="error-container"
>
{{ message.record?.error }}
</div>
<ChatToolBar :message="message" />
</PredictAnswer>
</template>
</ChatRow>
</template>
@@ -226,13 +284,18 @@ import ChartAnswer from './answer/ChartAnswer.vue'
import AnalysisAnswer from './answer/AnalysisAnswer.vue'
import PredictAnswer from './answer/PredictAnswer.vue'
import UserChat from './chat-block/UserChat.vue'
import ChartBlock from './chat-block/ChartBlock.vue'
import RecommendQuestion from './RecommendQuestion.vue'
import ChatListContainer from './ChatListContainer.vue'
import ChatCreator from '@/views/chat/ChatCreator.vue'
import ChatToolBar from './ChatToolBar.vue'
import { useI18n } from 'vue-i18n'
import { endsWith, find, startsWith } from 'lodash-es'
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
import icon_sidebar_outlined from '@/assets/svg/icon_sidebar_outlined.svg'
import icon_replace_outlined from '@/assets/svg/icon_replace_outlined.svg'
import icon_screen_outlined from '@/assets/svg/icon_screen_outlined.svg'
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'
@@ -536,6 +599,13 @@ function onAnalysisAnswerError() {
isTyping.value = false
}
function askAgain(message: ChatMessage) {
inputMessage.value = message.record?.question ?? ''
nextTick(() => {
sendMessage()
})
}
async function clickAnalysis(id?: number) {
const baseRecord = find(currentChat.value.records, (value) => id === value.id)
if (baseRecord == undefined) {
@@ -750,11 +820,52 @@ defineExpose({
}
}
.analysis-container {
color: var(--ed-text-color-primary);
font-size: 12px;
line-height: 1.7692307692;
padding: 16px 22px;
.error-container {
margin-top: 12px;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: rgba(31, 35, 41, 1);
}
.tool-btns {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
column-gap: 16px;
row-gap: 8px;
.tool-btn {
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: rgba(100, 106, 115, 1);
.tool-btn-inner {
display: flex;
flex-direction: row;
align-items: center;
}
&:hover {
background: rgba(31, 35, 41, 0.1);
}
&:active {
background: rgba(31, 35, 41, 0.1);
}
}
.btn-text {
margin-left: 4px;
}
.divider {
width: 1px;
height: 16px;
border-left: 1px solid rgba(31, 35, 41, 0.15);
}
}
.welcome-content-block {
+2 -1
View File
@@ -133,6 +133,7 @@ const closeField = () => {
}
const refresh = () => {
emits('refresh')
datasourceApi.tableList(props.info.id).then((res) => {
tableList.value = res
if (!currentTable.value.table_name) return
@@ -173,7 +174,7 @@ const changeStatus = (row: any) => {
})
}
const emits = defineEmits(['back'])
const emits = defineEmits(['back', 'refresh'])
const back = () => {
emits('back')
}
+13 -3
View File
@@ -16,7 +16,7 @@ import { datasourceApi } from '@/api/datasource'
import { useEmitt } from '@/utils/useEmitt'
import Card from './Card.vue'
import DelMessageBox from './DelMessageBox.vue'
import { dsTypeWithImgSort } from './js/ds-type'
import { dsTypeWithImg } from './js/ds-type'
import { useI18n } from 'vue-i18n'
interface Datasource {
@@ -41,7 +41,7 @@ const activeType = ref('')
const datasourceFormRef = ref()
const datasourceList = shallowRef([] as Datasource[])
const defaultDatasourceList = shallowRef(dsTypeWithImgSort as (Datasource & { img: string })[])
const defaultDatasourceList = shallowRef(dsTypeWithImg as (Datasource & { img: string })[])
const currentDefaultDatasource = ref('')
const datasourceListWithSearch = computed(() => {
@@ -115,6 +115,10 @@ const refresh = () => {
search()
}
const refreshData = () => {
search()
}
const panelClick = () => {
console.log('panelClick')
}
@@ -329,11 +333,17 @@ const back = () => {
:active-name="activeName"
:active-type="activeType"
@refresh="refresh"
@close="beforeClose"
@change-active-step="(val: number) => (activeStep = val)"
></DatasourceForm>
</el-drawer>
</div>
<DataTable v-if="currentDataTable" :info="currentDataTable" @back="back"></DataTable>
<DataTable
v-if="currentDataTable"
:info="currentDataTable"
@refresh="refreshData"
@back="back"
></DataTable>
</template>
<style lang="less" scoped>
+16 -25
View File
@@ -11,7 +11,7 @@ import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg'
import FixedSizeList from 'element-plus-secondary/es/components/virtual-list/src/components/fixed-size-list.mjs'
import { Plus } from '@element-plus/icons-vue'
import { useCache } from '@/utils/useCache'
import { dsType, haveSchema } from '@/views/ds/js/ds-type'
import { haveSchema } from '@/views/ds/js/ds-type'
import { setSize } from '@/utils/utils'
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import icon_fileExcel_colorful from '@/assets/datasource/icon_excel.png'
@@ -34,7 +34,7 @@ const props = withDefaults(
const { wsCache } = useCache()
const dsFormRef = ref<FormInstance>()
const emit = defineEmits(['refresh', 'changeActiveStep'])
const emit = defineEmits(['refresh', 'changeActiveStep', 'close'])
const isCreate = ref(true)
const isEditTable = ref(false)
const checkList = ref<any>([])
@@ -125,6 +125,7 @@ const close = () => {
dialogVisible.value = false
isCreate.value = true
emit('changeActiveStep', 0)
emit('close')
isEditTable.value = false
checkList.value = []
tableList.value = []
@@ -460,7 +461,19 @@ defineExpose({
class="model-form"
:class="(!isCreate || activeStep === 2) && 'edit-form'"
>
<div v-if="isCreate && activeStep !== 2" class="model-name">{{ activeName }}</div>
<div v-if="isCreate && activeStep !== 2" class="model-name">
{{ activeName }}
<span
v-if="form.type !== 'excel'"
style="margin-left: 8px; color: #8f959e; font-size: 12px; margin-top: 7px"
>
<span>{{ t('ds.form.support_version') }}:&nbsp;</span>
<span v-if="form.type === 'sqlServer'">2012+</span>
<span v-else-if="form.type === 'oracle'">12+</span>
<span v-else-if="form.type === 'mysql'">5.6+</span>
<span v-else-if="form.type === 'pg'">9.6+</span>
</span>
</div>
<div class="form-content">
<el-form
v-show="activeStep === 1"
@@ -536,28 +549,6 @@ defineExpose({
type="textarea"
/>
</el-form-item>
<el-form-item :label="t('ds.type')" prop="type">
<el-select
v-model="form.type"
:placeholder="$t('datasource.Please_select') + $t('common.empty') + t('ds.type')"
:disabled="!isCreate"
>
<el-option
v-for="item in dsType"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<span v-if="form.type !== 'excel'">
<span>{{ t('ds.form.support_version') }}:&nbsp;</span>
<span v-if="form.type === 'sqlServer'">2012+</span>
<span v-else-if="form.type === 'oracle'">12+</span>
<span v-else-if="form.type === 'mysql'">5.6+</span>
<span v-else-if="form.type === 'pg'">9.6+</span>
</span>
<div v-if="form.type !== 'excel'" style="margin-top: 16px">
<el-form-item :label="t('ds.form.host')" prop="host">
<el-input
+6 -12
View File
@@ -3,28 +3,22 @@ import excel from '@/assets/datasource/icon_excel.png'
import oracle from '@/assets/datasource/icon_oracle.png'
import pg from '@/assets/datasource/icon_PostgreSQL.png'
import sqlServer from '@/assets/datasource/icon_SQL_Server.png'
import { i18n } from '@/i18n'
const t = i18n.global.t
export const dsType = [
{ label: t('ds.local_excelcsv'), value: 'excel' },
{ label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'pg' },
{ label: 'Microsoft SQL Server', value: 'sqlServer' },
{ label: 'Oracle', value: 'oracle' },
{ label: 'Excel/CSV', value: 'excel' },
{ label: 'PostgreSQL', value: 'pg' },
{ label: 'SQL Server', value: 'sqlServer' },
]
export const dsTypeWithImg = [
{ name: '本地 Excel/CSV', type: 'excel', img: excel },
{ name: t('ds.local_excelcsv'), type: 'excel', img: excel },
{ name: 'MySQL', type: 'mysql', img: mysql_ds },
{ name: 'SQL Server', type: 'sqlServer', img: sqlServer },
{ name: 'Oracle', type: 'oracle', img: oracle },
{ name: 'PostgreSQL', type: 'pg', img: pg },
]
export const dsTypeWithImgSort = [
{ name: '本地 Excel/CSV', type: 'excel', img: excel },
{ name: 'PostgreSQL', type: 'pg', img: pg },
{ name: 'Oracle', type: 'oracle', img: oracle },
{ name: 'MySQL', type: 'mysql', img: mysql_ds },
{ name: 'SQL Server', type: 'sqlServer', img: sqlServer },
]
+29 -1
View File
@@ -57,7 +57,7 @@
</div>
</template>
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue'
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'
@@ -101,6 +101,21 @@ const validator = ref({
token: '',
})
const loading = ref(true)
const eventName = 'sqlbot_assistant_event'
const communicationCb = async (event: any) => {
if (event.data?.eventName === eventName) {
if (event.data?.messageId !== route.query.id) {
return
}
if (event.data?.busi == 'certificate') {
const certificate = event.data['certificate']
console.log(certificate)
assistantStore.setType(1)
assistantStore.setCertificate(certificate)
// store certificate to pinia
}
}
}
onBeforeMount(async () => {
const assistantId = route.query.id
const online = route.query.online
@@ -116,6 +131,19 @@ onBeforeMount(async () => {
assistantStore.setToken(validator.value.token)
assistantStore.setAssistant(true)
loading.value = false
window.addEventListener('message', communicationCb)
const readyData = {
eventName: 'sqlbot_assistant_event',
busi: 'ready',
ready: true,
messageId: assistantId,
}
window.parent.postMessage(readyData, '*')
})
onBeforeUnmount(() => {
window.removeEventListener('message', communicationCb)
})
</script>
+490
View File
@@ -0,0 +1,490 @@
<script lang="ts" setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { uwsOption, workspaceUserList, workspaceUwsDelete, workspaceCreate } from '@/api/workspace'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import assigned from '@/assets/svg/icon_assigned_outlined.svg'
import SuccessFilled from '@/assets/svg/gou_icon.svg'
import CircleCloseFilled from '@/assets/svg/icon_ban_filled.svg'
import { useUserStore } from '@/stores/user'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const multipleSelectionAll = ref<any[]>([])
const keyword = ref('')
const keywordsMember = ref('')
const userStore = useUserStore()
const workspaceForm = reactive({
name: '',
id: '',
})
const selectable = (row: any) => ![userStore.getUid].includes(row.id)
const fieldListWithSearch = computed(() => {
if (!keyword.value) return fieldList.value
return fieldList.value.filter((ele: any) =>
ele.name.toLowerCase().includes(keywordsMember.value.toLowerCase())
)
})
onMounted(() => {
search()
})
const fieldDialog = ref<boolean>(false)
const multipleTableRef = ref()
const isIndeterminate = ref(true)
const checkAll = ref(false)
const fieldList = ref<any>([])
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const cancelDelete = () => {
handleToggleRowSelection(false)
multipleSelectionAll.value = []
checkAll.value = false
isIndeterminate.value = false
}
const deleteBatchUser = () => {
ElMessageBox.confirm(
t('workspace.selected_2_members', { msg: multipleSelectionAll.value.length }),
{
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
}
).then(() => {
workspaceUwsDelete({
uid_list: multipleSelectionAll.value.map((ele) => ele.id),
}).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
multipleSelectionAll.value = []
search()
})
})
}
const deleteHandler = (row: any) => {
ElMessageBox.confirm(t('workspace.member_feng_yibudao', { msg: row.name }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
}).then(() => {
workspaceUwsDelete({
uid_list: [row.id],
}).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
search()
})
})
}
const handleSelectionChange = (val: any[]) => {
const ids = fieldList.value.map((ele: any) => ele.id)
multipleSelectionAll.value = [
...multipleSelectionAll.value.filter((ele) => !ids.includes(ele.id)),
...val,
]
isIndeterminate.value = !(val.length === 0 || val.length === fieldList.value.length)
checkAll.value = val.length === fieldList.value.length
}
const handleCheckAllChange = (val: any) => {
isIndeterminate.value = false
handleSelectionChange(val ? fieldList.value : [])
if (val) {
handleToggleRowSelection()
} else {
multipleTableRef.value.clearSelection()
}
}
const handleToggleRowSelection = (check: boolean = true) => {
let i = 0
const ids = multipleSelectionAll.value.map((ele: any) => ele.id)
for (const key in fieldList.value) {
if (ids.includes((fieldList.value[key] as any).id)) {
i += 1
multipleTableRef.value.toggleRowSelection(fieldList.value[key], check)
}
}
checkAll.value = i === fieldList.value.length
isIndeterminate.value = !(i === 0 || i === fieldList.value.length)
}
const search = () => {
workspaceUserList(
{ keyword: keywordsMember.value },
pageInfo.currentPage,
pageInfo.pageSize
).then((res) => {
fieldList.value = res.items
pageInfo.total = res.total
})
}
const closeField = () => {
fieldDialog.value = false
afterFind.value = false
userInfo.value.id = ''
userInfo.value.name = ''
userInfo.value.account = ''
}
const saveField = () => {
workspaceCreate({
uid_list: [userInfo.value.id],
weight: 0,
}).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
search()
fieldDialog.value = false
})
}
const userInfo = ref({
id: '',
name: '',
account: '',
})
const afterFind = ref(false)
const findUser = () => {
uwsOption({ keyword: workspaceForm.name })
.then((res: any) => {
userInfo.value = res || {}
})
.finally(() => {
afterFind.value = true
})
}
const addWorkspace = () => {
afterFind.value = false
workspaceForm.name = ''
fieldDialog.value = true
}
const handleSizeChange = (val: number) => {
pageInfo.pageSize = val
search()
}
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
search()
}
</script>
<template>
<div class="member">
<div class="tool-left">
<span class="page-title">{{ $t('workspace.member_management') }}</span>
<div class="search-bar">
<el-input
v-model="keyword"
style="width: 240px; margin-right: 12px"
:placeholder="$t('user.name_account_email')"
@keyup.enter="search"
@blur="search"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined />
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="addWorkspace()">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
{{ $t('workspace.add_member') }}
</el-button>
</div>
</div>
<div class="table-content">
<div class="preview-or-schema">
<el-table
ref="multipleTableRef"
:data="fieldListWithSearch"
:selectable="selectable"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" :label="$t('user.name')" width="160" />
<el-table-column prop="account" :label="$t('user.account')" width="160" />
<el-table-column prop="status" :label="$t('user.user_status')">
<template #default="scope">
<div
class="user-status-container"
:class="[scope.row.status ? 'active' : 'disabled']"
>
<el-icon size="16">
<SuccessFilled v-if="scope.row.status" />
<CircleCloseFilled v-else />
</el-icon>
<span>{{ $t(`user.${scope.row.status ? 'enabled' : 'disabled'}`) }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="email" :label="$t('user.email')" width="204" />
<el-table-column prop="weight" :label="$t('workspace.member_type')" width="120">
<template #default="scope">
<span>{{
scope.row.weight === 1
? t('workspace.administrator')
: t('workspace.ordinary_member')
}}</span>
</template>
</el-table-column>
<el-table-column fixed="right" width="80" :label="t('ds.actions')">
<template #default="scope">
<div class="field-comment">
<el-tooltip
:offset="14"
effect="dark"
:content="$t('workspace.remove')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="deleteHandler(scope.row)">
<assigned></assigned>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<template #empty>
<EmptyBackground
v-if="!keywordsMember && !fieldListWithSearch.length"
:description="$t('workspace.no_user')"
img-type="noneWhite"
/>
<EmptyBackground
v-if="!!keywordsMember && !fieldListWithSearch.length"
:description="$t('datasource.relevant_content_found')"
img-type="tree"
/>
</template>
</el-table>
</div>
<div 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>
</div>
<div v-if="multipleSelectionAll.length" class="bottom-select">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('datasource.select_all') }}
</el-checkbox>
<button class="danger-button" @click="deleteBatchUser">{{ $t('workspace.remove') }}</button>
<span class="selected">{{
$t('user.selected_2_items', { msg: multipleSelectionAll.length })
}}</span>
<el-button text @click="cancelDelete">
{{ $t('common.cancel') }}
</el-button>
</div>
</div>
<el-dialog
v-model="fieldDialog"
:title="$t('workspace.add_member')"
width="600"
modal-class="add-user_dialog"
destroy-on-close
:close-on-click-modal="false"
@before-closed="closeField"
>
<el-input
v-model="workspaceForm.name"
clearable
:placeholder="$t('workspace.id_account_to_add')"
>
<template #append>
<span style="cursor: pointer" @click="findUser">{{ t('workspace.find_user') }}</span>
</template>
</el-input>
<div class="user">
<template v-if="userInfo.id">
<div class="name">{{ userInfo.name }}</div>
<div class="account">{{ userInfo.account }}</div>
</template>
<div v-else-if="afterFind" class="empty">{{ $t('common.may_not_exist') }}</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button secondary @click="closeField">{{ t('common.cancel') }}</el-button>
<el-button v-if="!userInfo.id" disabled type="info">{{ t('model.add') }}</el-button>
<el-button v-else type="primary" @click="saveField">{{ t('model.add') }}</el-button>
</div>
</el-dialog>
</template>
<style lang="less" scoped>
.member {
height: 100%;
position: relative;
.tool-left {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.page-title {
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
}
.table-content {
height: calc(100% - 104px);
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 16px;
}
.preview-or-schema {
.user-status-container {
display: flex;
align-items: center;
font-weight: 400;
font-size: 14px;
line-height: 22px;
.ed-icon {
margin-right: 8px;
}
}
.field-comment {
height: 24px;
.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
.preview-num {
margin: 12px 0;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
}
}
.bottom-select {
position: absolute;
height: 64px;
width: 100%;
left: 0;
bottom: 0;
border-top: 1px solid #1f232926;
display: flex;
align-items: center;
padding-left: 24px;
.danger-button {
border: 1px solid var(--ed-color-danger);
color: var(--ed-color-danger);
border-radius: var(--ed-border-radius-base);
min-width: 80px;
height: 32px;
line-height: 32px;
text-align: center;
cursor: pointer;
margin: 0 16px;
background-color: transparent;
}
.selected {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
margin-right: 12px;
}
}
}
</style>
<style lang="less">
.add-user_dialog {
.ed-input-group__append {
background-color: #fff;
padding: 0 12px;
}
.user {
height: 126px;
font-weight: 400;
padding-top: 16px;
.name {
font-size: 14px;
line-height: 22px;
}
.account {
color: #8f959e;
font-size: 12px;
line-height: 20px;
}
.empty {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #8f959e;
}
}
}
</style>
@@ -46,7 +46,7 @@ const handleModelClick = (item: any) => {
:class="activeName === ele.name && 'isActive'"
@click="handleModelClick(ele)"
>
<img width="32px" height="32px" :src="ele.icon" />
<img width="18px" height="18px" :src="ele.icon" />
<span class="name">{{ ele.name }}</span>
</div>
<EmptyBackground
@@ -0,0 +1,121 @@
<script lang="ts" setup>
import delIcon from '@/assets/svg/icon_delete.svg'
import icon_key_outlined from '@/assets/svg/icon-key_outlined.svg'
import icon_member_outlined from '@/assets/svg/icon_member_outlined.svg'
import Lock from '@/assets/permission/icon_custom-tools_colorful.png'
withDefaults(
defineProps<{
name: string
type: string
num: string
id?: string
}>(),
{
name: '-',
type: '-',
id: '-',
num: '-',
}
)
const emits = defineEmits(['edit', 'del', 'setUser', 'setRule'])
const handleEdit = () => {
emits('edit')
}
const handleDel = () => {
emits('del')
}
const setUser = () => {
emits('setUser')
}
// const setRule = () => {
// emits('setRule')
// }
</script>
<template>
<div class="card">
<div class="name-icon">
<img :src="Lock" width="32px" height="32px" />
<span class="name">{{ name }}</span>
</div>
<div class="type-value">
<span class="type">{{ $t('permission.permission_rule') }}</span>
<span class="value"> {{ $t('permission.2', { msg: type }) }}</span>
</div>
<div class="type-value">
<span class="type">{{ $t('permission.restricted_user') }}</span>
<span class="value"> {{ $t('permission.238_people', { msg: type }) }}</span>
</div>
<div class="methods">
<el-button secondary @click="handleEdit">
<template #icon>
<icon_key_outlined></icon_key_outlined>
</template>
{{ $t('permission.set_rule') }}
</el-button>
<el-button secondary @click="setUser">
<template #icon>
<icon_member_outlined></icon_member_outlined>
</template>
{{ $t('permission.set_user') }}
</el-button>
<el-button secondary @click="handleDel">
<template #icon>
<delIcon></delIcon>
</template>
{{ $t('dashboard.delete') }}
</el-button>
</div>
</div>
</template>
<style lang="less" scoped>
.card {
width: 371px;
height: 176px;
border: 1px solid #dee0e3;
padding: 16px;
border-radius: 12px;
margin: 0 16px 16px 0;
&:hover {
box-shadow: 0px 6px 24px 0px #1f232914;
}
.name-icon {
display: flex;
align-items: center;
margin-bottom: 12px;
.name {
margin-left: 12px;
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
}
.type-value {
margin-top: 8px;
display: flex;
align-items: center;
font-weight: 400;
font-size: 14px;
line-height: 22px;
.type {
color: #646a73;
}
.value {
margin-left: 16px;
}
}
.methods {
margin-top: 16px;
}
}
</style>
@@ -0,0 +1,579 @@
<script lang="ts" setup>
import { ref, computed, reactive } from 'vue'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import icon_close_outlined from '@/assets/svg/operate/ope-close.svg'
import icon_down_outlined from '@/assets/svg/icon_down_outlined.svg'
import Card from './Card.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const keywords = ref('')
const activeStep = ref(0)
const dialogFormVisible = ref(false)
const ruleConfigvVisible = ref(false)
const editRule = ref(0)
const termFormRef = ref()
const columnFormRef = ref()
const drawerTitle = ref('')
const dialogTitle = ref('')
const tableData = ref<any[]>([])
const ruleList = ref<any[]>([
{
id: 1,
num: '10',
type: '90',
name: 'jkjkj',
},
])
const form = reactive({
name: '',
id: '',
})
const tableColumnData = ref<any[]>([])
const searchColumn = ref('')
const columnForm = reactive({
name: '',
id: '',
dataTable: '',
datasource: '',
})
const options = ref<any[]>([])
const ruleListWithSearch = computed(() => {
if (!keywords.value) return ruleList.value
return ruleList.value.filter((ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
)
})
const setDrawerTitle = () => {
if (activeStep.value === 0) {
drawerTitle.value = t('permission.add_rule_group')
} else {
if (editRule.value === 1) {
drawerTitle.value = t('permission.set_permission_rule')
}
if (editRule.value === 2) {
drawerTitle.value = t('permission.select_restricted_user')
}
}
}
const userTypeList = [
{
name: t('permission.row_permission'),
value: 1,
},
{
name: t('permission.column_permission'),
value: 0,
},
]
const handleAddPermission = (val: any) => {
if (val === 1) {
handleRowPermission()
} else {
handleColumnPermission(null)
}
}
const handleRowPermission = () => {
console.log('handleRowPermission')
}
const handleColumnPermission = (row: any) => {
dialogFormVisible.value = true
dialogTitle.value = row?.id
? t('permission.edit_column_permission')
: t('permission.add_column_permission')
}
const beforeClose = () => {
ruleConfigvVisible.value = false
activeStep.value = 0
}
const handleSearch = () => {}
const editHandler = (row: any) => {
editRule.value = 0
setDrawerTitle()
ruleConfigvVisible.value = true
console.log('row', row)
}
const handleEditRule = (row: any) => {
editRule.value = 1
setDrawerTitle()
console.log(row)
}
const deleteHandler = (row: any) => {
console.log(row)
}
const setUser = (row: any) => {
editRule.value = 2
setDrawerTitle()
console.log(row)
}
const rules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('permission.rule_group_name'),
trigger: 'blur',
},
],
}
const closeForm = () => {
dialogFormVisible.value = false
}
const saveHandler = () => {}
const columnRules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('permission.rule_name'),
trigger: 'blur',
},
],
dataTable: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('permission.rule_group_name'),
trigger: 'blur',
},
],
}
</script>
<template>
<div class="permission">
<div class="tool-left">
<span class="page-title">{{ $t('workspace.permission_configuration') }}</span>
<div>
<el-input
v-model="keywords"
style="width: 240px; margin-right: 12px"
:placeholder="$t('permission.search_rule_group')"
@keyup.enter="handleSearch"
@blur="handleSearch"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined />
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="editHandler(null)">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
{{ $t('permission.add_rule_group') }}
</el-button>
</div>
</div>
<EmptyBackground
v-if="!!keywords && !ruleListWithSearch.length"
:description="$t('datasource.relevant_content_found')"
img-type="tree"
/>
<div v-else class="card-content">
<Card
v-for="ele in ruleListWithSearch"
:id="ele.id"
:key="ele.id"
:name="ele.name"
:type="ele.type"
:num="ele.num"
@edit="handleEditRule(ele)"
@del="deleteHandler(ele)"
@set-user="setUser(ele)"
></Card>
</div>
<el-drawer
v-model="ruleConfigvVisible"
:close-on-click-modal="false"
size="calc(100% - 100px)"
modal-class="permission-drawer-fullscreen"
direction="btt"
:before-close="beforeClose"
:show-close="false"
>
<template #header="{ close }">
<span style="white-space: nowrap">{{ drawerTitle }}</span>
<div v-if="!editRule" class="flex-center" style="width: 100%">
<el-steps custom style="max-width: 500px; flex: 1" :active="activeStep" align-center>
<el-step>
<template #title> {{ $t('permission.set_permission_rule') }} </template>
</el-step>
<el-step>
<template #title> {{ $t('permission.select_restricted_user') }} </template>
</el-step>
</el-steps>
</div>
<el-icon style="cursor: pointer" @click="close">
<icon_close_outlined></icon_close_outlined>
</el-icon>
</template>
<div class="drawer-content">
<div class="title">
{{ $t('ds.form.base_info') }}
</div>
<el-form
ref="termFormRef"
:model="form"
label-width="180px"
label-position="top"
:rules="rules"
class="form-content_error"
>
<el-form-item prop="name" :label="t('permission.rule_group_name')">
<el-input
v-model="form.name"
:placeholder="
$t('datasource.please_enter') +
$t('common.empty') +
$t('permission.rule_group_name')
"
autocomplete="off"
/>
</el-form-item>
<el-form-item class="add-permission_form">
<template #label>
<div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
"
>
{{ t('permission.permission_rule') }}
<el-popover popper-class="system-permission_user" placement="bottom">
<template #reference>
<el-button class="add-btn" type="primary">
{{ $t('model.add') }}
<el-icon style="margin-left: 4px" size="16">
<icon_down_outlined></icon_down_outlined>
</el-icon>
</el-button>
</template>
<div class="popover">
<div class="popover-content">
<div
v-for="ele in userTypeList"
:key="ele.name"
class="popover-item"
@click="handleAddPermission(ele.value)"
>
<div class="model-name">{{ ele.name }}</div>
</div>
</div>
</div>
</el-popover>
</div>
</template>
<div class="table-content">
<el-table
:empty-text="$t('permission.no_rule')"
:data="tableData"
style="width: 100%"
>
<el-table-column prop="name" :label="$t('permission.rule_name')" />
<el-table-column prop="type" :label="$t('permission.type')" />
<el-table-column prop="datasource" :label="$t('permission.data_source')" />
<el-table-column prop="data_table" :label="$t('permission.data_table')" />
<el-table-column fixed="right" width="150" :label="$t('ds.actions')">
<template #default="scope">
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editHandler(scope.row)">
<IconOpeEdit></IconOpeEdit>
</el-icon>
</el-tooltip>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('dashboard.delete')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="deleteHandler(scope.row)">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
</el-form>
</div>
</el-drawer>
<el-drawer
v-model="dialogFormVisible"
:title="dialogTitle"
destroy-on-close
size="896px"
modal-class="column-form_drawer"
:before-close="closeForm"
>
<el-form
ref="columnFormRef"
:model="columnForm"
label-width="180px"
label-position="top"
:rules="columnRules"
class="form-content_error"
>
<el-form-item prop="name" :label="t('permission.rule_name')">
<el-input
v-model="columnForm.name"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('permission.rule_name')
"
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="dataTable" :label="t('permission.data_table')">
<el-select
v-model="columnForm.datasource"
style="width: 416px"
:placeholder="
$t('datasource.Please_select') + $t('common.empty') + $t('permission.data_source')
"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select
v-model="columnForm.dataTable"
style="width: 416px; margin-left: auto"
:disabled="!columnForm.datasource"
:placeholder="
$t('datasource.Please_select') + $t('common.empty') + $t('permission.data_table')
"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="$t('permission.set_rule')">
<el-input
v-model="searchColumn"
:placeholder="$t('permission.search_rule_group')"
autocomplete="off"
><template #prefix>
<el-icon>
<icon_searchOutline_outlined />
</el-icon> </template
></el-input>
</el-form-item>
</el-form>
<div class="table-content">
<el-table
:empty-text="$t('permission.no_fields_yet')"
:data="tableColumnData"
style="width: 100%"
>
<el-table-column prop="name" :label="$t('datasource.field_name')" />
<el-table-column prop="type" :label="$t('datasource.field_notes')" />
<el-table-column fixed="right" width="150" :label="$t('ds.actions')">
<template #default="scope">
<el-switch v-model="scope.row.checked" size="small" />
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeForm">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveHandler">
{{ $t('common.save') }}
</el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<style lang="less" scoped>
.permission {
.tool-left {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.page-title {
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
}
.card-content {
display: flex;
flex-wrap: wrap;
max-height: calc(100% - 40px);
overflow-y: auto;
}
}
</style>
<style lang="less">
.permission-drawer-fullscreen {
.title {
font-weight: 500;
font-size: 16px;
line-height: 24px;
margin-top: 8px;
margin-bottom: 16px;
}
.drawer-content {
width: 800px;
margin: 0 auto;
height: calc(100% - 20px);
.add-btn:hover {
.ed-icon {
transform: rotate(180deg);
}
}
.add-permission_form {
.ed-form-item__label {
width: 100%;
padding-right: 0;
}
.table-content {
width: 100%;
margin-top: 16px;
border: 1px solid #1f232926;
border-top: none;
border-radius: 6px;
overflow: hidden;
max-height: calc(100vh - 400px);
.ed-table__empty-text {
padding-top: 0;
}
.ed-icon {
position: relative;
cursor: pointer;
color: #646a73;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
}
}
}
.column-form_drawer {
.table-content {
width: 100%;
margin-top: 16px;
border: 1px solid #1f232926;
border-top: none;
border-radius: 6px;
overflow: hidden;
max-height: calc(100vh - 400px);
.ed-table__empty-text {
padding-top: 0;
}
}
}
.system-permission_user.system-permission_user {
padding: 0;
width: 120px !important;
min-width: 120px !important;
box-shadow: 0px 4px 8px 0px #1f23291a;
border: 1px solid #dee0e3;
.popover {
.popover-content {
padding: 4px;
position: relative;
&::after {
position: absolute;
content: '';
left: 0;
top: 40px;
width: 100%;
height: 1px;
background: #1f232926;
}
}
.popover-item {
height: 32px;
display: flex;
align-items: center;
padding-left: 12px;
padding-right: 8px;
position: relative;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #1f23291a;
}
&:nth-child(2) {
margin: 9px 0 0 0;
}
.model-name {
font-weight: 400;
font-size: 14px;
line-height: 22px;
}
.done {
margin-left: auto;
display: none;
}
&.isActive {
color: var(--ed-color-primary);
.done {
display: block;
}
}
}
}
}
</style>
+28 -21
View File
@@ -74,7 +74,7 @@
<el-table-column prop="create_time" width="180" sortable :label="$t('user.creation_time')">
<template #default="scope">
<span>{{ formatTimestamp(scope.row.create_time) }}</span>
<span>{{ formatTimestamp(scope.row.create_time, 'YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" width="150" :label="$t('ds.actions')">
@@ -165,8 +165,7 @@
:title="dialogTitle"
destroy-on-close
size="600px"
modle-class="sqlbot-drawer"
@close="onFormClose"
:before-close="onFormClose"
>
<el-form
ref="termFormRef"
@@ -343,21 +342,23 @@ const filterOption = ref<any[]>([
property: { placeholder: t('common.empty') + t('user.workspace') },
},
])
const defaultForm = {
id: '',
name: '',
account: '',
oid: 0,
email: '',
status: '',
phoneNumber: '',
oid_list: [],
}
const options = ref<any[]>([])
const state = reactive<any>({
tableData: [],
filterTexts: [],
conditions: [],
form: {
id: '',
name: '',
account: '',
oid: 0,
email: '',
status: '',
phoneNumber: '',
oid_list: [],
},
form: { ...defaultForm },
pageInfo: {
currentPage: 1,
pageSize: 20,
@@ -616,14 +617,19 @@ const search = () => {
})
}
const addTerm = () => {
userApi.add(state.form).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
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(() => {
@@ -721,7 +727,8 @@ onMounted(() => {
.sqlbot-table {
border-radius: 6px;
width: 100%;
max-height: calc(100vh - 200px);
max-height: calc(100vh - 100px);
overflow-y: auto;
&.is-filter {
max-height: calc(100vh - 256px);
+13 -4
View File
@@ -87,6 +87,13 @@ const currentTable = ref<any>({})
const init = () => {
workspaceList().then((res) => {
tableList.value = res
if (currentTable.value.id) {
tableList.value.forEach((ele: any) => {
if (ele.id === currentTable.value.id) {
currentTable.value.name = ele.name
}
})
}
})
}
onMounted(() => {
@@ -119,7 +126,7 @@ const deleteBatchUser = () => {
t('workspace.selected_2_members', { msg: multipleSelectionAll.value.length }),
{
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
confirmButtonText: t('workspace.remove'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
@@ -141,7 +148,7 @@ const deleteBatchUser = () => {
const deleteHandler = (row: any) => {
ElMessageBox.confirm(t('workspace.member_feng_yibudao', { msg: row.name }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
confirmButtonText: t('workspace.remove'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
@@ -231,6 +238,7 @@ const saveField = () => {
const delWorkspace = (row: any) => {
ElMessageBox.confirm(t('workspace.workspace_de_workspace', { msg: row.name }), {
confirmButtonType: 'danger',
tip: t('workspace.operate_with_caution'),
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
@@ -413,7 +421,7 @@ const handleCurrentChange = (val: number) => {
? t('workspace.administrator')
: t('workspace.ordinary_member')
}}</span>
<el-icon size="16">
<el-icon style="display: none" size="16">
<arrow_down></arrow_down>
</el-icon>
</div>
@@ -533,7 +541,7 @@ const handleCurrentChange = (val: number) => {
</el-form>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button @click="closeField">{{ t('common.cancel') }}</el-button>
<el-button secondary @click="closeField">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveField">{{
t(workspaceForm.id ? 'common.save' : 'model.add')
}}</el-button>
@@ -707,6 +715,7 @@ const handleCurrentChange = (val: number) => {
cursor: pointer;
&:hover {
.ed-icon {
display: block !important;
transform: rotate(180deg);
}
}