feat: 收紧用户管理部门隔离并重构知识库共享权限
- 普通管理员创建用户时固定归属本部门,用户列表/访问选项/详情/更新/删除均限制在本部门 - share_config 改为三档:global(全局共享)、department(部门共享)、user(指定人可访问) - 部门共享必须包含当前用户部门,指定人可访问必须包含当前用户 - 前端 ShareConfigForm 重构为三档卡片选择+下拉菜单选人/选部门 - 补充部门隔离和共享权限过滤的集成测试
This commit is contained in:
parent
5693fdd4a7
commit
c342f3e1ee
@ -27,6 +27,6 @@
|
|||||||
- [ ] Config spacy model 的 load
|
- [ ] Config spacy model 的 load
|
||||||
- [x] 在工作区的文件编辑的时候,保存和取消的按钮应该是悬浮在编辑框的右上角,而不是在 header 上面
|
- [x] 在工作区的文件编辑的时候,保存和取消的按钮应该是悬浮在编辑框的右上角,而不是在 header 上面
|
||||||
- [ ] default enable all build in tools / kbs / skills / mcps / subagents
|
- [ ] default enable all build in tools / kbs / skills / mcps / subagents
|
||||||
- [ ] 链接 Notion 和 feishu 目前来看,都是支持的
|
- [x] 链接 Notion 和 feishu 目前来看,都是支持的
|
||||||
- [ ] 知识库的权限调整,修改为三个等级,全局共享、部门共享(选择多个部门,默认是自己部门且必须包含自己部门)、指定人可访问(选择多个用户,默认是仅自己,可以添加其他人)。UI 上也需要调整,三个卡片不再是等宽,而是选中的会宽一点,并展示描述以及选择按钮,未选中的则是默认宽度,仅显示标题。对于选中的卡片除了展示描述、按钮之外,还包括“X 个部门可访问”、“X 个用户可访问”的信息展示。全局的就看是所有用户可访问。所以等级的字段配置也要重新设计,不需要考虑兼容,所有知识库都会重新构建。
|
- [x] 知识库的权限调整,修改为三个等级,全局共享、部门共享(选择多个部门,默认是自己部门且必须包含自己部门)、指定人可访问(选择多个用户,默认是仅自己,可以添加其他人)。UI 上也需要调整,三个卡片不再是等宽,而是选中的会宽一点,并展示描述以及选择按钮,未选中的则是默认宽度,仅显示标题。对于选中的卡片除了展示描述、按钮之外,还包括“X 个部门可访问”、“X 个用户可访问”的信息展示。全局的就看是所有用户可访问。所以等级的字段配置也要重新设计,不需要考虑兼容,所有知识库都会重新构建。
|
||||||
- [ ] databaseinfo 的重构,在左侧展示那个 tab 标签吧,将文件管理(filetable)以及右侧的那些图谱、检索、检索配置、评估之类的,都列为不同的 tab,进入之后默认激活的是 filetable。这样页面布局就好的多。作恶侧边栏除了这些 tab 之外,顶部是和 Skill Detail 那里的 header 一样,
|
- [ ] databaseinfo 的重构,在左侧展示那个 tab 标签吧,将文件管理(filetable)以及右侧的那些图谱、检索、检索配置、评估之类的,都列为不同的 tab,进入之后默认激活的是 filetable。这样页面布局就好的多。作恶侧边栏除了这些 tab 之外,顶部是和 Skill Detail 那里的 header 一样,
|
||||||
@ -9,6 +9,10 @@ from yuxi.utils import logger
|
|||||||
from yuxi.utils.datetime_utils import utc_isoformat
|
from yuxi.utils.datetime_utils import utc_isoformat
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SHARE_CONFIG = {"access_level": "global", "department_ids": [], "user_uids": []}
|
||||||
|
ACCESS_LEVELS = {"global", "department", "user"}
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeBaseManager:
|
class KnowledgeBaseManager:
|
||||||
"""
|
"""
|
||||||
知识库管理器
|
知识库管理器
|
||||||
@ -150,6 +154,47 @@ class KnowledgeBaseManager:
|
|||||||
"""
|
"""
|
||||||
return await self._get_kb_for_database(db_id)
|
return await self._get_kb_for_database(db_id)
|
||||||
|
|
||||||
|
def _normalize_share_config(
|
||||||
|
self,
|
||||||
|
share_config: dict | None,
|
||||||
|
*,
|
||||||
|
user_uid: str | None = None,
|
||||||
|
department_id: int | str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
config = share_config or {}
|
||||||
|
access_level = config.get("access_level") or "global"
|
||||||
|
if access_level not in ACCESS_LEVELS:
|
||||||
|
raise ValueError("无效的知识库权限等级")
|
||||||
|
|
||||||
|
if access_level == "global":
|
||||||
|
return DEFAULT_SHARE_CONFIG.copy()
|
||||||
|
|
||||||
|
if access_level == "department":
|
||||||
|
department_ids = self._normalize_department_ids(config.get("department_ids"))
|
||||||
|
if department_id is not None:
|
||||||
|
department_ids.append(int(department_id))
|
||||||
|
department_ids = sorted(set(department_ids))
|
||||||
|
if not department_ids:
|
||||||
|
raise ValueError("部门共享至少需要选择一个部门")
|
||||||
|
return {"access_level": "department", "department_ids": department_ids, "user_uids": []}
|
||||||
|
|
||||||
|
user_uids = self._normalize_user_uids(config.get("user_uids"))
|
||||||
|
if user_uid:
|
||||||
|
user_uids.append(str(user_uid))
|
||||||
|
user_uids = sorted(set(user_uids))
|
||||||
|
if not user_uids:
|
||||||
|
raise ValueError("指定人可访问至少需要选择一个用户")
|
||||||
|
return {"access_level": "user", "department_ids": [], "user_uids": user_uids}
|
||||||
|
|
||||||
|
def _normalize_department_ids(self, department_ids: list | None) -> list[int]:
|
||||||
|
normalized = []
|
||||||
|
for department_id in department_ids or []:
|
||||||
|
normalized.append(int(department_id))
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
def _normalize_user_uids(self, user_uids: list | None) -> list[str]:
|
||||||
|
return [uid for uid in (str(uid).strip() for uid in user_uids or []) if uid]
|
||||||
|
|
||||||
async def get_databases(self) -> dict:
|
async def get_databases(self) -> dict:
|
||||||
"""获取所有数据库信息"""
|
"""获取所有数据库信息"""
|
||||||
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
||||||
@ -178,7 +223,7 @@ class KnowledgeBaseManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 补充 share_config 和 additional_params
|
# 补充 share_config 和 additional_params
|
||||||
db_info["share_config"] = row.share_config or {"is_shared": True, "accessible_departments": []}
|
db_info["share_config"] = row.share_config or DEFAULT_SHARE_CONFIG.copy()
|
||||||
db_info["additional_params"] = kb_instance.normalize_additional_params(row.additional_params)
|
db_info["additional_params"] = kb_instance.normalize_additional_params(row.additional_params)
|
||||||
db_info["created_by"] = row.created_by
|
db_info["created_by"] = row.created_by
|
||||||
all_databases.append(db_info)
|
all_databases.append(db_info)
|
||||||
@ -205,28 +250,30 @@ class KnowledgeBaseManager:
|
|||||||
if kb is None:
|
if kb is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
share_config = kb.share_config or {}
|
user_uid = str(user.get("uid") or "")
|
||||||
is_shared = share_config.get("is_shared", True)
|
if user_uid and kb.created_by == user_uid:
|
||||||
|
|
||||||
# 如果是全员共享,则有权限
|
|
||||||
if is_shared:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 检查部门权限
|
share_config = kb.share_config or DEFAULT_SHARE_CONFIG.copy()
|
||||||
user_department_id = user.get("department_id")
|
access_level = share_config.get("access_level")
|
||||||
accessible_departments = share_config.get("accessible_departments", [])
|
|
||||||
|
|
||||||
if user_department_id is None:
|
if access_level == "global":
|
||||||
return False
|
return True
|
||||||
|
|
||||||
# 转换为整数进行比较(前端可能传递字符串,后端存储为整数)
|
if access_level == "department":
|
||||||
try:
|
user_department_id = user.get("department_id")
|
||||||
user_department_id = int(user_department_id)
|
if user_department_id is None:
|
||||||
accessible_departments = [int(d) for d in accessible_departments]
|
return False
|
||||||
except (ValueError, TypeError):
|
try:
|
||||||
return False
|
department_ids = [int(dept_id) for dept_id in share_config.get("department_ids") or []]
|
||||||
|
return int(user_department_id) in department_ids
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
return user_department_id in accessible_departments
|
if access_level == "user":
|
||||||
|
return bool(user_uid and user_uid in (share_config.get("user_uids") or []))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
async def get_databases_by_uid(self, uid: str) -> dict:
|
async def get_databases_by_uid(self, uid: str) -> dict:
|
||||||
"""根据 uid 获取知识库列表"""
|
"""根据 uid 获取知识库列表"""
|
||||||
@ -248,6 +295,7 @@ class KnowledgeBaseManager:
|
|||||||
user_info = user
|
user_info = user
|
||||||
else:
|
else:
|
||||||
user_info = {
|
user_info = {
|
||||||
|
"uid": user.uid,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
"department_id": user.department_id,
|
"department_id": user.department_id,
|
||||||
}
|
}
|
||||||
@ -304,6 +352,7 @@ class KnowledgeBaseManager:
|
|||||||
llm_model_spec: str | None = None,
|
llm_model_spec: str | None = None,
|
||||||
share_config: dict | None = None,
|
share_config: dict | None = None,
|
||||||
created_by: str | None = None,
|
created_by: str | None = None,
|
||||||
|
created_by_department_id: int | str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
@ -317,6 +366,7 @@ class KnowledgeBaseManager:
|
|||||||
llm_model_spec: LLM 模型 spec
|
llm_model_spec: LLM 模型 spec
|
||||||
share_config: 共享配置
|
share_config: 共享配置
|
||||||
created_by: 创建者 uid
|
created_by: 创建者 uid
|
||||||
|
created_by_department_id: 创建者部门 ID
|
||||||
**kwargs: 其他配置参数
|
**kwargs: 其他配置参数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -330,9 +380,11 @@ class KnowledgeBaseManager:
|
|||||||
if await self.database_name_exists(database_name):
|
if await self.database_name_exists(database_name):
|
||||||
raise ValueError(f"知识库名称 '{database_name}' 已存在,请使用其他名称")
|
raise ValueError(f"知识库名称 '{database_name}' 已存在,请使用其他名称")
|
||||||
|
|
||||||
# 默认共享配置
|
share_config = self._normalize_share_config(
|
||||||
if share_config is None:
|
share_config,
|
||||||
share_config = {"is_shared": True, "accessible_departments": []}
|
user_uid=created_by,
|
||||||
|
department_id=created_by_department_id,
|
||||||
|
)
|
||||||
|
|
||||||
kb_instance = self._get_or_create_kb_instance(kb_type)
|
kb_instance = self._get_or_create_kb_instance(kb_type)
|
||||||
kwargs = kb_instance.normalize_additional_params(kwargs)
|
kwargs = kb_instance.normalize_additional_params(kwargs)
|
||||||
@ -444,7 +496,7 @@ class KnowledgeBaseManager:
|
|||||||
|
|
||||||
# 添加数据库中的附加字段
|
# 添加数据库中的附加字段
|
||||||
db_info["additional_params"] = kb_instance.normalize_additional_params(kb.additional_params)
|
db_info["additional_params"] = kb_instance.normalize_additional_params(kb.additional_params)
|
||||||
db_info["share_config"] = kb.share_config or {"is_shared": True, "accessible_departments": []}
|
db_info["share_config"] = kb.share_config or DEFAULT_SHARE_CONFIG.copy()
|
||||||
db_info["mindmap"] = kb.mindmap
|
db_info["mindmap"] = kb.mindmap
|
||||||
db_info["sample_questions"] = kb.sample_questions or []
|
db_info["sample_questions"] = kb.sample_questions or []
|
||||||
db_info["query_params"] = kb.query_params
|
db_info["query_params"] = kb.query_params
|
||||||
@ -622,6 +674,8 @@ class KnowledgeBaseManager:
|
|||||||
update_llm_model_spec: bool = False,
|
update_llm_model_spec: bool = False,
|
||||||
additional_params: dict | None = None,
|
additional_params: dict | None = None,
|
||||||
share_config: dict | None = None,
|
share_config: dict | None = None,
|
||||||
|
operator_uid: str | None = None,
|
||||||
|
operator_department_id: int | str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""更新数据库"""
|
"""更新数据库"""
|
||||||
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
||||||
@ -655,7 +709,11 @@ class KnowledgeBaseManager:
|
|||||||
kb_instance.databases_meta[db_id]["metadata"] = merged_additional_params
|
kb_instance.databases_meta[db_id]["metadata"] = merged_additional_params
|
||||||
|
|
||||||
if share_config is not None:
|
if share_config is not None:
|
||||||
update_data["share_config"] = share_config
|
update_data["share_config"] = self._normalize_share_config(
|
||||||
|
share_config,
|
||||||
|
user_uid=operator_uid,
|
||||||
|
department_id=operator_department_id,
|
||||||
|
)
|
||||||
|
|
||||||
# 保存到数据库
|
# 保存到数据库
|
||||||
await kb_repo.update(db_id, update_data)
|
await kb_repo.update(db_id, update_data)
|
||||||
|
|||||||
@ -87,6 +87,14 @@ class UserResponse(BaseModel):
|
|||||||
last_login: str | None = None
|
last_login: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccessOption(BaseModel):
|
||||||
|
uid: str
|
||||||
|
username: str
|
||||||
|
role: str
|
||||||
|
department_id: int | None = None
|
||||||
|
department_name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class InitializeAdmin(BaseModel):
|
class InitializeAdmin(BaseModel):
|
||||||
uid: str # 直接输入用户ID
|
uid: str # 直接输入用户ID
|
||||||
password: str
|
password: str
|
||||||
@ -477,6 +485,11 @@ async def create_user(
|
|||||||
else:
|
else:
|
||||||
# 普通管理员创建用户时,自动继承该管理员的部门
|
# 普通管理员创建用户时,自动继承该管理员的部门
|
||||||
department_id = current_user.department_id
|
department_id = current_user.department_id
|
||||||
|
if department_id is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="管理员必须属于部门才能创建用户",
|
||||||
|
)
|
||||||
# 非超级管理员不能指定部门
|
# 非超级管理员不能指定部门
|
||||||
if user_data.department_id is not None:
|
if user_data.department_id is not None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -528,6 +541,41 @@ async def read_users(
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_user_in_current_department(current_user: User, target_user: User) -> None:
|
||||||
|
if current_user.role == "superadmin":
|
||||||
|
return
|
||||||
|
if target_user.department_id != current_user.department_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="只能管理本部门用户",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@auth.get("/users/access-options", response_model=list[UserAccessOption])
|
||||||
|
async def read_user_access_options(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 1000,
|
||||||
|
current_user: User = Depends(get_admin_user),
|
||||||
|
):
|
||||||
|
user_repo = UserRepository()
|
||||||
|
if current_user.role == "superadmin":
|
||||||
|
users_with_dept = await user_repo.list_with_department(skip=skip, limit=limit)
|
||||||
|
else:
|
||||||
|
users_with_dept = await user_repo.list_with_department(
|
||||||
|
skip=skip, limit=limit, department_id=current_user.department_id
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"uid": user.uid,
|
||||||
|
"username": user.username,
|
||||||
|
"role": user.role,
|
||||||
|
"department_id": user.department_id,
|
||||||
|
"department_name": dept_name,
|
||||||
|
}
|
||||||
|
for user, dept_name in users_with_dept
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# 路由:获取特定用户信息(管理员权限)
|
# 路由:获取特定用户信息(管理员权限)
|
||||||
@auth.get("/users/{user_id}", response_model=UserResponse)
|
@auth.get("/users/{user_id}", response_model=UserResponse)
|
||||||
async def read_user(user_id: int, current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
async def read_user(user_id: int, current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
||||||
@ -538,6 +586,7 @@ async def read_user(user_id: int, current_user: User = Depends(get_admin_user),
|
|||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="用户不存在",
|
detail="用户不存在",
|
||||||
)
|
)
|
||||||
|
_ensure_user_in_current_department(current_user, user)
|
||||||
return user.to_dict()
|
return user.to_dict()
|
||||||
|
|
||||||
|
|
||||||
@ -570,6 +619,8 @@ async def update_user(
|
|||||||
detail="用户不存在",
|
detail="用户不存在",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_ensure_user_in_current_department(current_user, user)
|
||||||
|
|
||||||
# 检查权限
|
# 检查权限
|
||||||
if user.role == "superadmin" and current_user.role != "superadmin":
|
if user.role == "superadmin" and current_user.role != "superadmin":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -584,6 +635,18 @@ async def update_user(
|
|||||||
detail="不能降级超级管理员账户",
|
detail="不能降级超级管理员账户",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if current_user.role == "admin":
|
||||||
|
if user.role != "user":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="管理员只能修改普通用户账户",
|
||||||
|
)
|
||||||
|
if user_data.role is not None and user_data.role != "user":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="管理员只能将用户角色设置为普通用户",
|
||||||
|
)
|
||||||
|
|
||||||
# 更新信息
|
# 更新信息
|
||||||
update_details = []
|
update_details = []
|
||||||
|
|
||||||
@ -664,6 +727,8 @@ async def delete_user(
|
|||||||
detail="用户不存在",
|
detail="用户不存在",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_ensure_user_in_current_department(current_user, user)
|
||||||
|
|
||||||
# 不能删除超级管理员账户
|
# 不能删除超级管理员账户
|
||||||
if user.role == "superadmin":
|
if user.role == "superadmin":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -671,6 +736,12 @@ async def delete_user(
|
|||||||
detail="不能删除超级管理员账户",
|
detail="不能删除超级管理员账户",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if current_user.role == "admin" and user.role != "user":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="管理员只能删除普通用户账户",
|
||||||
|
)
|
||||||
|
|
||||||
# 检查是否是部门的唯一管理员
|
# 检查是否是部门的唯一管理员
|
||||||
if user.role == "admin" and current_user.role != "superadmin":
|
if user.role == "admin" and current_user.role != "superadmin":
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
@ -849,7 +920,7 @@ async def impersonate_user(
|
|||||||
return {
|
return {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"token_type": "bearer",
|
"token_type": "bearer",
|
||||||
"uid": target_user.id,
|
"user_id": target_user.id,
|
||||||
"username": target_user.username,
|
"username": target_user.username,
|
||||||
"uid": target_user.uid,
|
"uid": target_user.uid,
|
||||||
"phone_number": target_user.phone_number,
|
"phone_number": target_user.phone_number,
|
||||||
|
|||||||
@ -190,6 +190,7 @@ async def create_database(
|
|||||||
llm_model_spec=llm_model_spec,
|
llm_model_spec=llm_model_spec,
|
||||||
share_config=share_config,
|
share_config=share_config,
|
||||||
created_by=current_user.uid,
|
created_by=current_user.uid,
|
||||||
|
created_by_department_id=current_user.department_id,
|
||||||
**additional_params,
|
**additional_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -276,6 +277,8 @@ async def update_database_info(
|
|||||||
update_llm_model_spec=update_llm_model_spec,
|
update_llm_model_spec=update_llm_model_spec,
|
||||||
additional_params=additional_params,
|
additional_params=additional_params,
|
||||||
share_config=data.share_config,
|
share_config=data.share_config,
|
||||||
|
operator_uid=current_user.uid,
|
||||||
|
operator_department_id=current_user.department_id,
|
||||||
)
|
)
|
||||||
return {"message": "更新成功", "database": database}
|
return {"message": "更新成功", "database": database}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -36,6 +36,7 @@ class UpdateWorkspaceFileContentRequest(BaseModel):
|
|||||||
async def _ensure_knowledge_read_access(current_user: User, db_id: str) -> None:
|
async def _ensure_knowledge_read_access(current_user: User, db_id: str) -> None:
|
||||||
allowed = await knowledge_base.check_accessible(
|
allowed = await knowledge_base.check_accessible(
|
||||||
{
|
{
|
||||||
|
"uid": current_user.uid,
|
||||||
"role": current_user.role,
|
"role": current_user.role,
|
||||||
"department_id": current_user.department_id,
|
"department_id": current_user.department_id,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -11,6 +11,68 @@ import pytest
|
|||||||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||||||
|
|
||||||
|
|
||||||
|
async def _require_superadmin(test_client, headers):
|
||||||
|
response = await test_client.get("/api/auth/me", headers=headers)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
if response.json()["role"] != "superadmin":
|
||||||
|
pytest.fail("This test requires TEST_USERNAME to be a superadmin account.")
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_department_with_admin(test_client, headers, label: str) -> dict:
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
admin_uid = f"adm{label}_{suffix}"
|
||||||
|
admin_password = f"Pw!{suffix}"
|
||||||
|
response = await test_client.post(
|
||||||
|
"/api/departments",
|
||||||
|
json={
|
||||||
|
"name": f"pytest_{label}_{suffix}",
|
||||||
|
"description": "pytest managed department",
|
||||||
|
"admin_uid": admin_uid,
|
||||||
|
"admin_password": admin_password,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 201, response.text
|
||||||
|
|
||||||
|
login_response = await test_client.post(
|
||||||
|
"/api/auth/token",
|
||||||
|
data={"username": admin_uid, "password": admin_password},
|
||||||
|
)
|
||||||
|
assert login_response.status_code == 200, login_response.text
|
||||||
|
|
||||||
|
login_payload = login_response.json()
|
||||||
|
return {
|
||||||
|
"department": response.json(),
|
||||||
|
"admin_id": login_payload["user_id"],
|
||||||
|
"admin_headers": {"Authorization": f"Bearer {login_payload['access_token']}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_user(test_client, headers, label: str, role: str = "user", department_id: int | None = None) -> dict:
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
payload = {
|
||||||
|
"username": f"u{label}_{suffix}",
|
||||||
|
"password": f"Pw!{suffix}",
|
||||||
|
"role": role,
|
||||||
|
}
|
||||||
|
if department_id is not None:
|
||||||
|
payload["department_id"] = department_id
|
||||||
|
|
||||||
|
response = await test_client.post("/api/auth/users", json=payload, headers=headers)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def _cleanup_user(test_client, headers, user_id: int) -> None:
|
||||||
|
response = await test_client.delete(f"/api/auth/users/{user_id}", headers=headers)
|
||||||
|
assert response.status_code in {200, 404}, response.text
|
||||||
|
|
||||||
|
|
||||||
|
async def _cleanup_department(test_client, headers, department_id: int) -> None:
|
||||||
|
response = await test_client.delete(f"/api/departments/{department_id}", headers=headers)
|
||||||
|
assert response.status_code in {200, 404}, response.text
|
||||||
|
|
||||||
|
|
||||||
async def test_login_with_invalid_credentials(test_client):
|
async def test_login_with_invalid_credentials(test_client):
|
||||||
response = await test_client.post("/api/auth/token", data={"username": "invalid", "password": "invalid"})
|
response = await test_client.post("/api/auth/token", data={"username": "invalid", "password": "invalid"})
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
@ -25,9 +87,7 @@ async def test_user_is_locked_after_repeated_failed_logins(test_client, standard
|
|||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.json()["detail"] == "用户名或密码错误"
|
assert response.json()["detail"] == "用户名或密码错误"
|
||||||
|
|
||||||
locked_response = await test_client.post(
|
locked_response = await test_client.post("/api/auth/token", data={"username": uid, "password": "wrong-password"})
|
||||||
"/api/auth/token", data={"username": uid, "password": "wrong-password"}
|
|
||||||
)
|
|
||||||
assert locked_response.status_code == 423, locked_response.text
|
assert locked_response.status_code == 423, locked_response.text
|
||||||
assert "X-Lock-Remaining" in locked_response.headers
|
assert "X-Lock-Remaining" in locked_response.headers
|
||||||
assert "账户已被锁定" in locked_response.json()["detail"]
|
assert "账户已被锁定" in locked_response.json()["detail"]
|
||||||
@ -47,7 +107,7 @@ async def test_admin_can_login_and_fetch_profile(test_client, admin_headers):
|
|||||||
data = profile_response.json()
|
data = profile_response.json()
|
||||||
assert data["role"] in {"admin", "superadmin"}
|
assert data["role"] in {"admin", "superadmin"}
|
||||||
assert data["username"]
|
assert data["username"]
|
||||||
assert data["user_id"]
|
assert data["id"]
|
||||||
|
|
||||||
|
|
||||||
async def test_admin_can_create_and_delete_user(test_client, admin_headers):
|
async def test_admin_can_create_and_delete_user(test_client, admin_headers):
|
||||||
@ -71,6 +131,96 @@ async def test_admin_can_create_and_delete_user(test_client, admin_headers):
|
|||||||
assert delete_payload["message"] == "用户已删除"
|
assert delete_payload["message"] == "用户已删除"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_department_admin_is_limited_to_own_department_users(test_client, admin_headers):
|
||||||
|
await _require_superadmin(test_client, admin_headers)
|
||||||
|
|
||||||
|
user_ids: list[int] = []
|
||||||
|
admin_ids: list[int] = []
|
||||||
|
department_ids: list[int] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
dept_a = await _create_department_with_admin(test_client, admin_headers, "a")
|
||||||
|
dept_b = await _create_department_with_admin(test_client, admin_headers, "b")
|
||||||
|
department_a = dept_a["department"]
|
||||||
|
department_b = dept_b["department"]
|
||||||
|
department_ids.extend([department_a["id"], department_b["id"]])
|
||||||
|
admin_ids.extend([dept_a["admin_id"], dept_b["admin_id"]])
|
||||||
|
|
||||||
|
user_a = await _create_user(test_client, dept_a["admin_headers"], "a")
|
||||||
|
user_b = await _create_user(test_client, dept_b["admin_headers"], "b")
|
||||||
|
superadmin_created_user = await _create_user(test_client, admin_headers, "s", department_id=department_b["id"])
|
||||||
|
user_ids.extend([user_a["id"], user_b["id"], superadmin_created_user["id"]])
|
||||||
|
|
||||||
|
assert user_a["department_id"] == department_a["id"]
|
||||||
|
assert superadmin_created_user["department_id"] == department_b["id"]
|
||||||
|
|
||||||
|
forbidden_create = await test_client.post(
|
||||||
|
"/api/auth/users",
|
||||||
|
json={
|
||||||
|
"username": f"ux_{uuid.uuid4().hex[:8]}",
|
||||||
|
"password": "routerTest123!",
|
||||||
|
"role": "user",
|
||||||
|
"department_id": department_b["id"],
|
||||||
|
},
|
||||||
|
headers=dept_a["admin_headers"],
|
||||||
|
)
|
||||||
|
assert forbidden_create.status_code == 403, forbidden_create.text
|
||||||
|
|
||||||
|
list_response = await test_client.get("/api/auth/users", headers=dept_a["admin_headers"])
|
||||||
|
assert list_response.status_code == 200, list_response.text
|
||||||
|
listed_users = list_response.json()
|
||||||
|
listed_user_ids = {user["id"] for user in listed_users}
|
||||||
|
assert user_a["id"] in listed_user_ids
|
||||||
|
assert user_b["id"] not in listed_user_ids
|
||||||
|
assert all(user["department_id"] == department_a["id"] for user in listed_users)
|
||||||
|
|
||||||
|
options_response = await test_client.get("/api/auth/users/access-options", headers=dept_a["admin_headers"])
|
||||||
|
assert options_response.status_code == 200, options_response.text
|
||||||
|
access_options = options_response.json()
|
||||||
|
option_uids = {user["uid"] for user in access_options}
|
||||||
|
assert user_a["uid"] in option_uids
|
||||||
|
assert user_b["uid"] not in option_uids
|
||||||
|
assert all(user["department_id"] == department_a["id"] for user in access_options)
|
||||||
|
|
||||||
|
superadmin_list_response = await test_client.get("/api/auth/users", headers=admin_headers)
|
||||||
|
assert superadmin_list_response.status_code == 200, superadmin_list_response.text
|
||||||
|
superadmin_user_ids = {user["id"] for user in superadmin_list_response.json()}
|
||||||
|
assert user_a["id"] in superadmin_user_ids
|
||||||
|
assert user_b["id"] in superadmin_user_ids
|
||||||
|
|
||||||
|
own_read = await test_client.get(f"/api/auth/users/{user_a['id']}", headers=dept_a["admin_headers"])
|
||||||
|
assert own_read.status_code == 200, own_read.text
|
||||||
|
|
||||||
|
cross_read = await test_client.get(f"/api/auth/users/{user_b['id']}", headers=dept_a["admin_headers"])
|
||||||
|
assert cross_read.status_code == 403, cross_read.text
|
||||||
|
|
||||||
|
cross_update = await test_client.put(
|
||||||
|
f"/api/auth/users/{user_b['id']}",
|
||||||
|
json={"username": f"ub_{uuid.uuid4().hex[:8]}"},
|
||||||
|
headers=dept_a["admin_headers"],
|
||||||
|
)
|
||||||
|
assert cross_update.status_code == 403, cross_update.text
|
||||||
|
|
||||||
|
role_escalation = await test_client.put(
|
||||||
|
f"/api/auth/users/{user_a['id']}", json={"role": "admin"}, headers=dept_a["admin_headers"]
|
||||||
|
)
|
||||||
|
assert role_escalation.status_code == 403, role_escalation.text
|
||||||
|
|
||||||
|
cross_delete = await test_client.delete(f"/api/auth/users/{user_b['id']}", headers=dept_a["admin_headers"])
|
||||||
|
assert cross_delete.status_code == 403, cross_delete.text
|
||||||
|
|
||||||
|
own_delete = await test_client.delete(f"/api/auth/users/{user_a['id']}", headers=dept_a["admin_headers"])
|
||||||
|
assert own_delete.status_code == 200, own_delete.text
|
||||||
|
user_ids.remove(user_a["id"])
|
||||||
|
finally:
|
||||||
|
for user_id in user_ids:
|
||||||
|
await _cleanup_user(test_client, admin_headers, user_id)
|
||||||
|
for admin_id in admin_ids:
|
||||||
|
await _cleanup_user(test_client, admin_headers, admin_id)
|
||||||
|
for department_id in department_ids:
|
||||||
|
await _cleanup_department(test_client, admin_headers, department_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_invalid_token_is_rejected(test_client):
|
async def test_invalid_token_is_rejected(test_client):
|
||||||
headers = {"Authorization": "Bearer not-a-real-token"}
|
headers = {"Authorization": "Bearer not-a-real-token"}
|
||||||
response = await test_client.get("/api/auth/me", headers=headers)
|
response = await test_client.get("/api/auth/me", headers=headers)
|
||||||
|
|||||||
@ -20,6 +20,94 @@ def _assert_forbidden_response(response):
|
|||||||
assert isinstance(payload["detail"], str)
|
assert isinstance(payload["detail"], str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_test_department(test_client, admin_headers, prefix="pytest_dept"):
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
admin_uid = f"deptadmin_{suffix}"
|
||||||
|
response = await test_client.post(
|
||||||
|
"/api/departments",
|
||||||
|
json={
|
||||||
|
"name": f"{prefix}_{suffix}",
|
||||||
|
"description": "pytest department",
|
||||||
|
"admin_uid": admin_uid,
|
||||||
|
"admin_password": f"Pw!{suffix}",
|
||||||
|
},
|
||||||
|
headers=admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 201, response.text
|
||||||
|
payload = response.json()
|
||||||
|
payload["admin_uid"] = admin_uid
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_test_user(test_client, admin_headers, department_id):
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
password = f"Pw!{suffix}"
|
||||||
|
response = await test_client.post(
|
||||||
|
"/api/auth/users",
|
||||||
|
json={
|
||||||
|
"username": f"pytest_user_{suffix}",
|
||||||
|
"password": password,
|
||||||
|
"role": "user",
|
||||||
|
"department_id": department_id,
|
||||||
|
},
|
||||||
|
headers=admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
user = response.json()
|
||||||
|
|
||||||
|
login_response = await test_client.post(
|
||||||
|
"/api/auth/token",
|
||||||
|
data={"username": user["uid"], "password": password},
|
||||||
|
)
|
||||||
|
assert login_response.status_code == 200, login_response.text
|
||||||
|
return {"user": user, "headers": {"Authorization": f"Bearer {login_response.json()['access_token']}"}}
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_user_by_id(test_client, admin_headers, user_id):
|
||||||
|
response = await test_client.delete(f"/api/auth/users/{user_id}", headers=admin_headers)
|
||||||
|
assert response.status_code in (200, 404), response.text
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_user_id_by_uid(test_client, admin_headers, uid):
|
||||||
|
response = await test_client.get("/api/auth/users", headers=admin_headers)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
for user in response.json():
|
||||||
|
if user["uid"] == uid:
|
||||||
|
return user["id"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_department_with_admin(test_client, admin_headers, department):
|
||||||
|
admin_user_id = await _find_user_id_by_uid(test_client, admin_headers, department["admin_uid"])
|
||||||
|
if admin_user_id:
|
||||||
|
await _delete_user_by_id(test_client, admin_headers, admin_user_id)
|
||||||
|
response = await test_client.delete(f"/api/departments/{department['id']}", headers=admin_headers)
|
||||||
|
assert response.status_code in (200, 404), response.text
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_test_database(test_client, admin_headers, share_config=None):
|
||||||
|
response = await test_client.post(
|
||||||
|
"/api/knowledge/databases",
|
||||||
|
json={
|
||||||
|
"database_name": f"pytest_acl_{uuid.uuid4().hex[:8]}",
|
||||||
|
"description": "Knowledge permission test",
|
||||||
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
||||||
|
"kb_type": "milvus",
|
||||||
|
"additional_params": {},
|
||||||
|
"share_config": share_config,
|
||||||
|
},
|
||||||
|
headers=admin_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def _accessible_db_ids(test_client, headers):
|
||||||
|
response = await test_client.get("/api/knowledge/databases/accessible", headers=headers)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
return {item["db_id"] for item in response.json().get("databases", [])}
|
||||||
|
|
||||||
|
|
||||||
async def test_admin_can_manage_knowledge_databases(test_client, admin_headers, knowledge_database):
|
async def test_admin_can_manage_knowledge_databases(test_client, admin_headers, knowledge_database):
|
||||||
db_id = knowledge_database["db_id"]
|
db_id = knowledge_database["db_id"]
|
||||||
|
|
||||||
@ -395,6 +483,96 @@ async def test_get_accessible_databases(test_client, admin_headers, knowledge_da
|
|||||||
assert knowledge_database["db_id"] in db_ids
|
assert knowledge_database["db_id"] in db_ids
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_database_defaults_to_global_share_config(test_client, admin_headers):
|
||||||
|
database = await _create_test_database(test_client, admin_headers)
|
||||||
|
db_id = database["db_id"]
|
||||||
|
try:
|
||||||
|
assert database["share_config"] == {"access_level": "global", "department_ids": [], "user_uids": []}
|
||||||
|
finally:
|
||||||
|
await test_client.delete(f"/api/knowledge/databases/{db_id}", headers=admin_headers)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_department_share_config_filters_accessible_databases(test_client, admin_headers):
|
||||||
|
department_a = await _create_test_department(test_client, admin_headers, "pytest_dept_a")
|
||||||
|
department_b = await _create_test_department(test_client, admin_headers, "pytest_dept_b")
|
||||||
|
user_a = user_b = None
|
||||||
|
database = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_a = await _create_test_user(test_client, admin_headers, department_a["id"])
|
||||||
|
user_b = await _create_test_user(test_client, admin_headers, department_b["id"])
|
||||||
|
database = await _create_test_database(
|
||||||
|
test_client,
|
||||||
|
admin_headers,
|
||||||
|
{"access_level": "department", "department_ids": [department_a["id"]], "user_uids": []},
|
||||||
|
)
|
||||||
|
|
||||||
|
saved_config = database["share_config"]
|
||||||
|
assert saved_config["access_level"] == "department"
|
||||||
|
assert department_a["id"] in saved_config["department_ids"]
|
||||||
|
|
||||||
|
assert database["db_id"] in await _accessible_db_ids(test_client, user_a["headers"])
|
||||||
|
assert database["db_id"] not in await _accessible_db_ids(test_client, user_b["headers"])
|
||||||
|
finally:
|
||||||
|
if database:
|
||||||
|
await test_client.delete(f"/api/knowledge/databases/{database['db_id']}", headers=admin_headers)
|
||||||
|
if user_a:
|
||||||
|
await _delete_user_by_id(test_client, admin_headers, user_a["user"]["id"])
|
||||||
|
if user_b:
|
||||||
|
await _delete_user_by_id(test_client, admin_headers, user_b["user"]["id"])
|
||||||
|
await _delete_department_with_admin(test_client, admin_headers, department_a)
|
||||||
|
await _delete_department_with_admin(test_client, admin_headers, department_b)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_share_config_filters_accessible_databases(test_client, admin_headers):
|
||||||
|
department_a = await _create_test_department(test_client, admin_headers, "pytest_dept_a")
|
||||||
|
department_b = await _create_test_department(test_client, admin_headers, "pytest_dept_b")
|
||||||
|
user_a = user_b = None
|
||||||
|
database = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_a = await _create_test_user(test_client, admin_headers, department_a["id"])
|
||||||
|
user_b = await _create_test_user(test_client, admin_headers, department_b["id"])
|
||||||
|
database = await _create_test_database(
|
||||||
|
test_client,
|
||||||
|
admin_headers,
|
||||||
|
{"access_level": "user", "department_ids": [], "user_uids": [user_a["user"]["uid"]]},
|
||||||
|
)
|
||||||
|
|
||||||
|
saved_config = database["share_config"]
|
||||||
|
assert saved_config["access_level"] == "user"
|
||||||
|
assert user_a["user"]["uid"] in saved_config["user_uids"]
|
||||||
|
|
||||||
|
assert database["db_id"] in await _accessible_db_ids(test_client, user_a["headers"])
|
||||||
|
assert database["db_id"] not in await _accessible_db_ids(test_client, user_b["headers"])
|
||||||
|
finally:
|
||||||
|
if database:
|
||||||
|
await test_client.delete(f"/api/knowledge/databases/{database['db_id']}", headers=admin_headers)
|
||||||
|
if user_a:
|
||||||
|
await _delete_user_by_id(test_client, admin_headers, user_a["user"]["id"])
|
||||||
|
if user_b:
|
||||||
|
await _delete_user_by_id(test_client, admin_headers, user_b["user"]["id"])
|
||||||
|
await _delete_department_with_admin(test_client, admin_headers, department_a)
|
||||||
|
await _delete_department_with_admin(test_client, admin_headers, department_b)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_access_options_include_all_departments_for_admin(test_client, admin_headers):
|
||||||
|
department = await _create_test_department(test_client, admin_headers, "pytest_access_options")
|
||||||
|
user = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = await _create_test_user(test_client, admin_headers, department["id"])
|
||||||
|
response = await test_client.get("/api/auth/users/access-options", headers=admin_headers)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
uids = {item["uid"] for item in response.json()}
|
||||||
|
assert user["user"]["uid"] in uids
|
||||||
|
assert department["admin_uid"] in uids
|
||||||
|
finally:
|
||||||
|
if user:
|
||||||
|
await _delete_user_by_id(test_client, admin_headers, user["user"]["id"])
|
||||||
|
await _delete_department_with_admin(test_client, admin_headers, department)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_knowledge_base_types(test_client, admin_headers):
|
async def test_get_knowledge_base_types(test_client, admin_headers):
|
||||||
"""测试获取支持的知识库类型"""
|
"""测试获取支持的知识库类型"""
|
||||||
response = await test_client.get("/api/knowledge/types", headers=admin_headers)
|
response = await test_client.get("/api/knowledge/types", headers=admin_headers)
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
* 认证相关 API
|
* 认证相关 API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { apiAdminGet } from './base'
|
||||||
|
|
||||||
async function parseErrorDetail(response, fallbackMessage) {
|
async function parseErrorDetail(response, fallbackMessage) {
|
||||||
const contentType = response.headers.get('content-type') || ''
|
const contentType = response.headers.get('content-type') || ''
|
||||||
|
|
||||||
@ -57,6 +59,10 @@ async function getOIDCLoginUrl(redirectPath = '/') {
|
|||||||
* department_name: string | null
|
* department_name: string | null
|
||||||
* }>}
|
* }>}
|
||||||
*/
|
*/
|
||||||
|
async function getUserAccessOptions() {
|
||||||
|
return apiAdminGet('/api/auth/users/access-options')
|
||||||
|
}
|
||||||
|
|
||||||
async function exchangeOIDCCode(code) {
|
async function exchangeOIDCCode(code) {
|
||||||
const response = await fetch('/api/auth/oidc/exchange-code', {
|
const response = await fetch('/api/auth/oidc/exchange-code', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -77,5 +83,6 @@ async function exchangeOIDCCode(code) {
|
|||||||
export const authApi = {
|
export const authApi = {
|
||||||
getOIDCConfig,
|
getOIDCConfig,
|
||||||
getOIDCLoginUrl,
|
getOIDCLoginUrl,
|
||||||
|
getUserAccessOptions,
|
||||||
exchangeOIDCCode
|
exchangeOIDCCode
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑对话框 -->
|
<!-- 编辑对话框 -->
|
||||||
<a-modal v-model:open="editModalVisible" title="编辑知识库信息">
|
<a-modal v-model:open="editModalVisible" title="编辑知识库信息" width="700px">
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-button danger @click="deleteDatabase" style="margin-right: auto; margin-left: 0">
|
<a-button danger @click="deleteDatabase" style="margin-right: auto; margin-left: 0">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@ -140,11 +140,11 @@
|
|||||||
<!-- 非编辑状态下显示共享配置信息 -->
|
<!-- 非编辑状态下显示共享配置信息 -->
|
||||||
<a-form-item v-else-if="database.share_config" label="共享设置" name="share_config_readonly">
|
<a-form-item v-else-if="database.share_config" label="共享设置" name="share_config_readonly">
|
||||||
<div class="share-config-readonly">
|
<div class="share-config-readonly">
|
||||||
<a-tag :color="database.share_config.is_shared !== false ? 'green' : 'blue'">
|
<a-tag :color="shareConfigDisplay.color">
|
||||||
{{ database.share_config.is_shared !== false ? '全员共享' : '指定部门' }}
|
{{ shareConfigDisplay.label }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<span v-if="database.share_config.is_shared === false" class="dept-names">
|
<span class="access-names">
|
||||||
{{ getAccessibleDeptNames() }}
|
{{ shareConfigDisplay.detail }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@ -162,6 +162,7 @@ import { message } from 'ant-design-vue'
|
|||||||
import { LeftOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue'
|
import { LeftOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import { Pencil, Trash2, Copy } from 'lucide-vue-next'
|
import { Pencil, Trash2, Copy } from 'lucide-vue-next'
|
||||||
import { departmentApi } from '@/apis/department_api'
|
import { departmentApi } from '@/apis/department_api'
|
||||||
|
import { authApi } from '@/apis/auth_api'
|
||||||
import AiTextarea from '@/components/AiTextarea.vue'
|
import AiTextarea from '@/components/AiTextarea.vue'
|
||||||
import ShareConfigForm from '@/components/ShareConfigForm.vue'
|
import ShareConfigForm from '@/components/ShareConfigForm.vue'
|
||||||
|
|
||||||
@ -174,10 +175,9 @@ const isDifyKb = computed(() => database.value?.kb_type === 'dify')
|
|||||||
const isNotionKb = computed(() => database.value?.kb_type === 'notion')
|
const isNotionKb = computed(() => database.value?.kb_type === 'notion')
|
||||||
const isReadOnlyConnector = computed(() => isDifyKb.value || isNotionKb.value)
|
const isReadOnlyConnector = computed(() => isDifyKb.value || isNotionKb.value)
|
||||||
|
|
||||||
// 部门列表(用于显示部门名称)
|
|
||||||
const departments = ref([])
|
const departments = ref([])
|
||||||
|
const users = ref([])
|
||||||
|
|
||||||
// 加载部门列表
|
|
||||||
const loadDepartments = async () => {
|
const loadDepartments = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await departmentApi.getDepartments()
|
const res = await departmentApi.getDepartments()
|
||||||
@ -188,21 +188,57 @@ const loadDepartments = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化时加载部门
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
users.value = await authApi.getUserAccessOptions()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载用户列表失败:', e)
|
||||||
|
users.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDepartments()
|
loadDepartments()
|
||||||
|
loadUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取可访问的部门名称
|
const shareConfigDisplay = computed(() => {
|
||||||
const getAccessibleDeptNames = () => {
|
const shareConfig = database.value?.share_config || { access_level: 'global' }
|
||||||
const deptIds = database.value?.share_config?.accessible_departments || []
|
if (shareConfig.access_level === 'department') {
|
||||||
if (deptIds.length === 0) return '无'
|
const departmentIds = shareConfig.department_ids || []
|
||||||
return deptIds
|
const names = departmentIds.map((id) => getDepartmentName(id)).join('、') || '无'
|
||||||
.map((id) => {
|
return {
|
||||||
const dept = departments.value.find((d) => d.id === id)
|
color: 'blue',
|
||||||
return dept?.name || `部门${id}`
|
label: '部门共享',
|
||||||
})
|
detail: `${departmentIds.length} 个部门可访问:${names}`
|
||||||
.join('、')
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shareConfig.access_level === 'user') {
|
||||||
|
const userUids = shareConfig.user_uids || []
|
||||||
|
const names = userUids.map((uid) => getUserName(uid)).join('、') || '无'
|
||||||
|
return {
|
||||||
|
color: 'purple',
|
||||||
|
label: '指定人可访问',
|
||||||
|
detail: `${userUids.length} 个用户可访问:${names}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: 'green',
|
||||||
|
label: '全局共享',
|
||||||
|
detail: '所有用户可访问'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDepartmentName = (id) => {
|
||||||
|
const dept = departments.value.find((item) => Number(item.id) === Number(id))
|
||||||
|
return dept?.name || `部门${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserName = (uid) => {
|
||||||
|
const user = users.value.find((item) => item.uid === uid)
|
||||||
|
return user?.username || uid
|
||||||
}
|
}
|
||||||
|
|
||||||
// 是否可以编辑共享配置
|
// 是否可以编辑共享配置
|
||||||
@ -306,30 +342,15 @@ const handleEditSubmit = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 ShareConfigForm 组件直接获取当前值
|
const formConfig = shareConfigFormRef.value?.config || { access_level: 'global' }
|
||||||
let finalIsShared = true
|
|
||||||
let finalDeptIds = []
|
|
||||||
|
|
||||||
if (shareConfigFormRef.value) {
|
|
||||||
const formConfig = shareConfigFormRef.value.config
|
|
||||||
finalIsShared = formConfig.is_shared
|
|
||||||
finalDeptIds = formConfig.accessible_department_ids || []
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[handleEditSubmit] 直接从组件获取 - is_shared:',
|
|
||||||
finalIsShared,
|
|
||||||
'dept_ids:',
|
|
||||||
JSON.stringify(finalDeptIds)
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: editForm.name,
|
name: editForm.name,
|
||||||
description: editForm.description,
|
description: editForm.description,
|
||||||
additional_params: {},
|
additional_params: {},
|
||||||
share_config: {
|
share_config: {
|
||||||
is_shared: finalIsShared,
|
access_level: formConfig.access_level,
|
||||||
accessible_departments: finalIsShared ? [] : finalDeptIds
|
department_ids: formConfig.access_level === 'department' ? formConfig.department_ids || [] : [],
|
||||||
|
user_uids: formConfig.access_level === 'user' ? formConfig.user_uids || [] : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +423,7 @@ const deleteDatabase = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.dept-names {
|
.access-names {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--gray-600);
|
color: var(--gray-600);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="share-config-form">
|
<div class="share-config-form">
|
||||||
<div class="share-config-content">
|
<div class="share-mode-cards" :class="`active-${config.access_level}`" role="radiogroup" aria-label="共享设置">
|
||||||
<div class="share-mode-cards" role="radiogroup" aria-label="共享设置">
|
<div
|
||||||
<button
|
v-for="option in shareModeOptions"
|
||||||
v-for="option in shareModeOptions"
|
:key="option.value"
|
||||||
:key="option.value"
|
role="radio"
|
||||||
type="button"
|
class="share-mode-card"
|
||||||
role="radio"
|
:class="{ active: config.access_level === option.value }"
|
||||||
class="share-mode-card"
|
:aria-checked="config.access_level === option.value"
|
||||||
:class="{ active: config.is_shared === option.value }"
|
:tabindex="config.access_level === option.value ? 0 : -1"
|
||||||
:aria-checked="config.is_shared === option.value"
|
@click="setAccessLevel(option.value)"
|
||||||
:tabindex="config.is_shared === option.value ? 0 : -1"
|
@keydown.enter.prevent="setAccessLevel(option.value)"
|
||||||
@click="setShareMode(option.value)"
|
@keydown.space.prevent="setAccessLevel(option.value)"
|
||||||
@keydown.enter.prevent="setShareMode(option.value)"
|
>
|
||||||
@keydown.space.prevent="setShareMode(option.value)"
|
<div class="card-main">
|
||||||
>
|
<div class="card-header">
|
||||||
<div class="card-icon-wrapper" aria-hidden="true">
|
<div class="card-icon-wrapper" aria-hidden="true">
|
||||||
<component :is="option.icon" class="card-icon" :size="20" />
|
<component :is="option.icon" class="card-icon" :size="20" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
|
||||||
<div class="card-title">{{ option.title }}</div>
|
<div class="card-title">{{ option.title }}</div>
|
||||||
<div class="card-description">{{ option.description }}</div>
|
<div
|
||||||
|
v-if="config.access_level === option.value && option.value !== 'global'"
|
||||||
|
class="card-action"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<a-dropdown :trigger="['click']" placement="bottomRight" overlay-class-name="share-selection-popover">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
class="select-action lucide-icon-btn"
|
||||||
|
:aria-label="option.value === 'department' ? '选择部门' : '选择用户'"
|
||||||
|
>
|
||||||
|
<UserPlus class="select-action-icon" :size="14" />
|
||||||
|
<span class="access-count">{{ getAccessCount(option.value) }}</span>
|
||||||
|
</a-button>
|
||||||
|
<template #overlay>
|
||||||
|
<div class="selection-dropdown" @mousedown.stop @click.stop>
|
||||||
|
<div class="selection-dropdown-header">
|
||||||
|
<div class="selection-dropdown-title">
|
||||||
|
{{ option.value === 'department' ? '可访问部门' : '可访问用户' }}
|
||||||
|
</div>
|
||||||
|
<div class="selection-dropdown-subtitle">
|
||||||
|
{{ getAccessSummary(option.value) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-input
|
||||||
|
v-model:value="selectionSearch[option.value]"
|
||||||
|
size="small"
|
||||||
|
allow-clear
|
||||||
|
class="selection-search"
|
||||||
|
:placeholder="option.value === 'department' ? '搜索部门' : '搜索用户'"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div v-if="getSelectionOptions(option.value).length" class="selection-list">
|
||||||
|
<div
|
||||||
|
v-for="item in getSelectionOptions(option.value)"
|
||||||
|
:key="item.value"
|
||||||
|
role="checkbox"
|
||||||
|
:aria-checked="isSelected(option.value, item.value)"
|
||||||
|
:tabindex="item.disabled ? -1 : 0"
|
||||||
|
class="selection-item"
|
||||||
|
:class="{ selected: isSelected(option.value, item.value), locked: item.disabled }"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))"
|
||||||
|
@keydown.enter.prevent="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))"
|
||||||
|
@keydown.space.prevent="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))"
|
||||||
|
>
|
||||||
|
<span class="selection-item-content">
|
||||||
|
<a-checkbox
|
||||||
|
:checked="isSelected(option.value, item.value)"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
@click.stop
|
||||||
|
@change="toggleSelection(option.value, item.value, $event.target.checked)"
|
||||||
|
/>
|
||||||
|
<span class="selection-label">{{ item.label }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="item.disabled" class="selection-required">必选</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="selection-empty">暂无可选项</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<div class="card-description">{{ option.description }}</div>
|
||||||
</div>
|
|
||||||
<div v-if="!config.is_shared" class="compact-dept-selection">
|
|
||||||
<div class="dept-selection-header">
|
|
||||||
<div class="dept-selection-label">可访问部门</div>
|
|
||||||
<div class="dept-selection-hint">请选择可访问该知识库的部门</div>
|
|
||||||
</div>
|
</div>
|
||||||
<a-select
|
|
||||||
v-model:value="config.accessible_department_ids"
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="请选择部门"
|
|
||||||
size="small"
|
|
||||||
class="full-width dept-selection-input"
|
|
||||||
:options="departmentOptions"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
|
||||||
import { Globe, Building2 } from 'lucide-vue-next'
|
import { Globe, Building2, Users, UserPlus } from 'lucide-vue-next'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { departmentApi } from '@/apis/department_api'
|
import { departmentApi } from '@/apis/department_api'
|
||||||
|
import { authApi } from '@/apis/auth_api'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const departments = ref([])
|
const departments = ref([])
|
||||||
|
const users = ref([])
|
||||||
|
const syncingFromProps = ref(false)
|
||||||
|
|
||||||
const shareModeOptions = [
|
const shareModeOptions = [
|
||||||
{
|
{
|
||||||
value: true,
|
value: 'global',
|
||||||
title: '公开给团队',
|
title: '全局共享',
|
||||||
description: '团队内所有成员都可以访问这个知识库',
|
description: '所有用户都可以访问',
|
||||||
icon: Globe
|
icon: Globe
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: false,
|
value: 'department',
|
||||||
title: '仅指定部门',
|
title: '部门共享',
|
||||||
description: '只有选中的部门成员可以访问这个知识库',
|
description: '选中的部门成员可以访问',
|
||||||
icon: Building2
|
icon: Building2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'user',
|
||||||
|
title: '指定人可访问',
|
||||||
|
description: '选中的用户可以访问',
|
||||||
|
icon: Users
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -71,11 +129,11 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
is_shared: true,
|
access_level: 'global',
|
||||||
accessible_department_ids: []
|
department_ids: [],
|
||||||
|
user_uids: []
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// 是否自动选中当前用户所在部门
|
|
||||||
autoSelectUserDept: {
|
autoSelectUserDept: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@ -84,145 +142,228 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
// 本地状态,直接从 props 初始化
|
|
||||||
const config = reactive({
|
const config = reactive({
|
||||||
is_shared: true,
|
access_level: 'global',
|
||||||
accessible_department_ids: []
|
department_ids: [],
|
||||||
|
user_uids: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 初始化 config
|
const selectionSearch = reactive({
|
||||||
const initConfig = () => {
|
department: '',
|
||||||
// 后端返回的是 accessible_departments,前端使用 accessible_department_ids
|
user: ''
|
||||||
const sourceDepts =
|
|
||||||
props.modelValue.accessible_department_ids ?? props.modelValue.accessible_departments ?? []
|
|
||||||
config.is_shared = props.modelValue.is_shared ?? true
|
|
||||||
config.accessible_department_ids = sourceDepts.map((id) => Number(id))
|
|
||||||
console.log('[ShareConfigForm] initConfig:', JSON.stringify(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只在组件挂载时初始化一次
|
|
||||||
onMounted(() => {
|
|
||||||
initConfig()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听本地 config 变化,同步到父组件
|
const currentDepartmentId = computed(() => {
|
||||||
watch(
|
if (!userStore.departmentId) return null
|
||||||
config,
|
return Number(userStore.departmentId)
|
||||||
(newVal) => {
|
})
|
||||||
console.log('[ShareConfigForm] config 变化,emit:', JSON.stringify(newVal))
|
|
||||||
emit('update:modelValue', {
|
|
||||||
is_shared: newVal.is_shared,
|
|
||||||
accessible_department_ids: newVal.accessible_department_ids
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const setShareMode = (isShared) => {
|
const currentUserUid = computed(() => userStore.uid || '')
|
||||||
if (config.is_shared === isShared) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
config.is_shared = isShared
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听共享模式变化
|
|
||||||
watch(
|
|
||||||
() => config.is_shared,
|
|
||||||
(newVal) => {
|
|
||||||
if (!newVal && props.autoSelectUserDept && config.accessible_department_ids.length === 0) {
|
|
||||||
// 切换到指定部门模式且未选中任何部门时,默认选中当前用户所在部门
|
|
||||||
tryAutoSelectUserDept()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 尝试自动选中用户所在部门
|
|
||||||
const tryAutoSelectUserDept = () => {
|
|
||||||
const userDeptId = userStore.departmentId
|
|
||||||
if (userDeptId) {
|
|
||||||
const deptExists = departments.value.find((d) => d.id === userDeptId)
|
|
||||||
if (deptExists) {
|
|
||||||
// 确保存储为数字类型(a-select 返回的是字符串)
|
|
||||||
config.accessible_department_ids = [Number(userDeptId)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听用户部门变化(处理时序问题:departmentId 可能在组件 mounted 后才获取到)
|
|
||||||
watch(
|
|
||||||
() => userStore.departmentId,
|
|
||||||
(newDeptId) => {
|
|
||||||
if (
|
|
||||||
props.autoSelectUserDept &&
|
|
||||||
!config.is_shared &&
|
|
||||||
config.accessible_department_ids.length === 0 &&
|
|
||||||
newDeptId
|
|
||||||
) {
|
|
||||||
tryAutoSelectUserDept()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 部门选项
|
|
||||||
const departmentOptions = computed(() =>
|
const departmentOptions = computed(() =>
|
||||||
departments.value.map((dept) => ({
|
departments.value.map((dept) => {
|
||||||
label: dept.name,
|
const value = Number(dept.id)
|
||||||
value: Number(dept.id)
|
return {
|
||||||
|
label: dept.name,
|
||||||
|
value,
|
||||||
|
disabled: value === currentDepartmentId.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const userOptions = computed(() =>
|
||||||
|
users.value.map((user) => ({
|
||||||
|
label: user.department_name ? `${user.username}(${user.department_name})` : user.username,
|
||||||
|
value: user.uid,
|
||||||
|
disabled: user.uid === currentUserUid.value
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
// 加载部门列表
|
const normalizeDepartmentIds = (ids) =>
|
||||||
|
Array.from(new Set((ids || []).map((id) => Number(id)).filter((id) => Number.isFinite(id))))
|
||||||
|
|
||||||
|
const normalizeUserUids = (uids) =>
|
||||||
|
Array.from(new Set((uids || []).map((uid) => String(uid).trim()).filter(Boolean)))
|
||||||
|
|
||||||
|
const ensureCurrentDepartment = () => {
|
||||||
|
if (!props.autoSelectUserDept || !currentDepartmentId.value) return
|
||||||
|
if (!config.department_ids.includes(currentDepartmentId.value)) {
|
||||||
|
config.department_ids = [currentDepartmentId.value, ...config.department_ids]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureCurrentUser = () => {
|
||||||
|
if (!currentUserUid.value) return
|
||||||
|
if (!config.user_uids.includes(currentUserUid.value)) {
|
||||||
|
config.user_uids = [currentUserUid.value, ...config.user_uids]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeActiveConfig = () => {
|
||||||
|
if (config.access_level === 'global') {
|
||||||
|
config.department_ids = []
|
||||||
|
config.user_uids = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.access_level === 'department') {
|
||||||
|
config.department_ids = normalizeDepartmentIds(config.department_ids)
|
||||||
|
config.user_uids = []
|
||||||
|
ensureCurrentDepartment()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.department_ids = []
|
||||||
|
config.user_uids = normalizeUserUids(config.user_uids)
|
||||||
|
ensureCurrentUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
const initConfig = () => {
|
||||||
|
syncingFromProps.value = true
|
||||||
|
const accessLevel = ['global', 'department', 'user'].includes(props.modelValue?.access_level)
|
||||||
|
? props.modelValue.access_level
|
||||||
|
: 'global'
|
||||||
|
config.access_level = accessLevel
|
||||||
|
config.department_ids = normalizeDepartmentIds(props.modelValue?.department_ids)
|
||||||
|
config.user_uids = normalizeUserUids(props.modelValue?.user_uids)
|
||||||
|
normalizeActiveConfig()
|
||||||
|
nextTick(() => {
|
||||||
|
syncingFromProps.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitConfig = () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
access_level: config.access_level,
|
||||||
|
department_ids: config.access_level === 'department' ? normalizeDepartmentIds(config.department_ids) : [],
|
||||||
|
user_uids: config.access_level === 'user' ? normalizeUserUids(config.user_uids) : []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAccessLevel = (accessLevel) => {
|
||||||
|
if (config.access_level === accessLevel) return
|
||||||
|
config.access_level = accessLevel
|
||||||
|
normalizeActiveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccessSummary = (accessLevel) => {
|
||||||
|
if (accessLevel === 'global') return '所有用户可访问'
|
||||||
|
if (accessLevel === 'department') return `${config.department_ids.length} 个部门可访问`
|
||||||
|
if (accessLevel === 'user' && config.user_uids.length === 1) return '仅自己可访问'
|
||||||
|
return `${config.user_uids.length} 个用户可访问`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccessCount = (accessLevel) => {
|
||||||
|
if (accessLevel === 'department') return config.department_ids.length
|
||||||
|
if (accessLevel === 'user') return config.user_uids.length
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelectionOptions = (accessLevel) => {
|
||||||
|
const options = accessLevel === 'department' ? departmentOptions.value : userOptions.value
|
||||||
|
const query = selectionSearch[accessLevel]?.trim().toLowerCase()
|
||||||
|
if (!query) return options
|
||||||
|
return options.filter((item) => item.label.toLowerCase().includes(query))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = (accessLevel, value) => {
|
||||||
|
if (accessLevel === 'department') return config.department_ids.includes(Number(value))
|
||||||
|
if (accessLevel === 'user') return config.user_uids.includes(String(value))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelection = (accessLevel, value, checked) => {
|
||||||
|
if (accessLevel === 'department') {
|
||||||
|
const departmentId = Number(value)
|
||||||
|
const selected = checked
|
||||||
|
? [...config.department_ids, departmentId]
|
||||||
|
: config.department_ids.filter((id) => id !== departmentId)
|
||||||
|
config.department_ids = normalizeDepartmentIds(selected)
|
||||||
|
ensureCurrentDepartment()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = String(value)
|
||||||
|
const selected = checked ? [...config.user_uids, uid] : config.user_uids.filter((item) => item !== uid)
|
||||||
|
config.user_uids = normalizeUserUids(selected)
|
||||||
|
ensureCurrentUser()
|
||||||
|
}
|
||||||
|
|
||||||
const loadDepartments = async () => {
|
const loadDepartments = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await departmentApi.getDepartments()
|
const res = await departmentApi.getDepartments()
|
||||||
departments.value = res.departments || res || []
|
departments.value = res.departments || res || []
|
||||||
|
if (config.access_level === 'department') ensureCurrentDepartment()
|
||||||
// 如果需要,自动选中用户所在部门
|
|
||||||
if (
|
|
||||||
props.autoSelectUserDept &&
|
|
||||||
!config.is_shared &&
|
|
||||||
config.accessible_department_ids.length === 0
|
|
||||||
) {
|
|
||||||
tryAutoSelectUserDept()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载部门列表失败:', e)
|
console.error('加载部门列表失败:', e)
|
||||||
departments.value = []
|
departments.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const loadUsers = async () => {
|
||||||
loadDepartments()
|
try {
|
||||||
|
users.value = await authApi.getUserAccessOptions()
|
||||||
|
if (config.access_level === 'user') ensureCurrentUser()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载用户列表失败:', e)
|
||||||
|
users.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => initConfig(),
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
config,
|
||||||
|
() => {
|
||||||
|
if (!syncingFromProps.value) emitConfig()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(currentDepartmentId, () => {
|
||||||
|
if (config.access_level === 'department') ensureCurrentDepartment()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(currentUserUid, () => {
|
||||||
|
if (config.access_level === 'user') ensureCurrentUser()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 验证当前用户所在部门是否在可访问范围内
|
|
||||||
// 返回 { valid: boolean, message: string }
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
// 全员共享模式不需要验证
|
normalizeActiveConfig()
|
||||||
if (config.is_shared) {
|
|
||||||
|
if (config.access_level === 'global') {
|
||||||
return { valid: true, message: '' }
|
return { valid: true, message: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 指定部门模式,需要验证当前用户所在部门是否在列表中
|
if (config.access_level === 'department') {
|
||||||
const userDeptId = userStore.departmentId
|
if (!currentDepartmentId.value) {
|
||||||
if (!userDeptId) {
|
return { valid: false, message: '您不属于任何部门,无法使用部门共享模式' }
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: '您不属于任何部门,无法使用指定部门共享模式'
|
|
||||||
}
|
}
|
||||||
|
if (!config.department_ids.includes(currentDepartmentId.value)) {
|
||||||
|
return { valid: false, message: '您所在的部门必须在可访问部门范围内' }
|
||||||
|
}
|
||||||
|
return { valid: true, message: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.accessible_department_ids.includes(userDeptId)) {
|
if (!currentUserUid.value) {
|
||||||
return {
|
return { valid: false, message: '无法获取当前用户,无法使用指定人可访问模式' }
|
||||||
valid: false,
|
}
|
||||||
message: '您所在的部门必须在可访问部门范围内'
|
if (!config.user_uids.includes(currentUserUid.value)) {
|
||||||
}
|
return { valid: false, message: '当前用户必须在可访问用户范围内' }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: '' }
|
return { valid: true, message: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暴露方法给父组件
|
onMounted(() => {
|
||||||
|
initConfig()
|
||||||
|
loadDepartments()
|
||||||
|
loadUsers()
|
||||||
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
config,
|
config,
|
||||||
validate
|
validate
|
||||||
@ -231,150 +372,256 @@ defineExpose({
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.share-config-form {
|
.share-config-form {
|
||||||
.share-config-content {
|
.share-mode-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-mode-card {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 76px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--gray-0);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease,
|
||||||
|
box-shadow 180ms ease,
|
||||||
|
transform 180ms ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--main-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: var(--main-color);
|
||||||
|
background: linear-gradient(180deg, var(--main-10) 0%, var(--gray-0) 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px var(--main-20),
|
||||||
|
0 10px 24px rgb(0 0 0 / 6%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
background: var(--gray-50);
|
||||||
|
transition:
|
||||||
|
background-color 180ms ease,
|
||||||
|
box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.share-mode-cards {
|
.card-icon {
|
||||||
display: grid;
|
color: var(--gray-500);
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
transition: color 180ms ease;
|
||||||
gap: 8px;
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.share-mode-card.active .card-icon-wrapper {
|
||||||
grid-template-columns: 1fr;
|
background: var(--main-0);
|
||||||
}
|
box-shadow: inset 0 0 0 1px var(--main-20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-mode-card {
|
.share-mode-card.active .card-icon {
|
||||||
position: relative;
|
color: var(--main-color);
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
min-height: 76px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid var(--gray-200);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--gray-0);
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
border-color 0.2s ease,
|
|
||||||
background-color 0.2s ease,
|
|
||||||
box-shadow 0.2s ease;
|
|
||||||
|
|
||||||
&:hover,
|
.card-title {
|
||||||
&:focus-visible {
|
font-size: 14px;
|
||||||
border-color: var(--main-color);
|
font-weight: 600;
|
||||||
}
|
color: var(--gray-800);
|
||||||
|
line-height: 1.35;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
.card-description {
|
||||||
outline: none;
|
font-size: 12px;
|
||||||
box-shadow: 0 0 0 2px var(--main-20);
|
line-height: 1.45;
|
||||||
}
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
.access-count {
|
||||||
border-color: var(--main-color);
|
color: var(--main-color);
|
||||||
background: var(--main-10);
|
font-size: 12px;
|
||||||
box-shadow: 0 0 0 1px var(--main-20);
|
font-weight: 500;
|
||||||
}
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.card-icon-wrapper {
|
.select-action {
|
||||||
display: inline-flex;
|
width: 44px;
|
||||||
align-items: center;
|
min-width: 24px;
|
||||||
justify-content: center;
|
height: 24px;
|
||||||
width: 36px;
|
padding: 0;
|
||||||
height: 36px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--gray-50);
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-icon {
|
color: var(--main-color);
|
||||||
color: var(--gray-500);
|
}
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
.select-action-icon {
|
||||||
min-width: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 3px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
font-size: 14px;
|
.share-mode-card,
|
||||||
font-weight: 600;
|
.card-icon-wrapper,
|
||||||
color: var(--gray-800);
|
.card-icon {
|
||||||
line-height: 1.35;
|
transition: none;
|
||||||
}
|
|
||||||
|
|
||||||
&.active .card-icon-wrapper {
|
|
||||||
background: var(--main-0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active .card-icon {
|
|
||||||
color: var(--main-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-description {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.45;
|
|
||||||
color: var(--gray-600);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dept-selection {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-dept-selection {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--gray-150);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: linear-gradient(180deg, var(--gray-0) 0%, var(--gray-50) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dept-selection-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dept-selection-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--gray-800);
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dept-selection-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--gray-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dept-selection-input {
|
|
||||||
:deep(.ant-select-selector) {
|
|
||||||
min-height: 36px;
|
|
||||||
padding: 3px 8px !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
border-color: var(--gray-200) !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:deep(.ant-select-focused .ant-select-selector),
|
|
||||||
&:deep(.ant-select-selector:hover) {
|
|
||||||
border-color: var(--main-color) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.share-selection-popover {
|
||||||
|
.selection-dropdown {
|
||||||
|
width: 280px;
|
||||||
|
max-height: 340px;
|
||||||
|
padding: 8px;
|
||||||
|
overflow: hidden auto;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--gray-0);
|
||||||
|
box-shadow:
|
||||||
|
0 14px 36px rgb(0 0 0 / 12%),
|
||||||
|
0 2px 8px rgb(0 0 0 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-dropdown-header {
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-bottom: 1px solid var(--gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-dropdown-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--gray-900);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-dropdown-subtitle {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-search {
|
||||||
|
margin: 8px;
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 38px;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: var(--gray-800);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: var(--main-10);
|
||||||
|
color: var(--gray-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.locked {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-required {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--gray-500);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-empty {
|
||||||
|
display: block;
|
||||||
|
padding: 14px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--gray-600);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -150,7 +150,7 @@
|
|||||||
<!-- 共享配置 -->
|
<!-- 共享配置 -->
|
||||||
<div class="form-section compact-section">
|
<div class="form-section compact-section">
|
||||||
<h3 class="section-title">共享设置</h3>
|
<h3 class="section-title">共享设置</h3>
|
||||||
<ShareConfigForm v-model="shareConfig" :auto-select-user-dept="true" />
|
<ShareConfigForm ref="shareConfigFormRef" v-model="shareConfig" :auto-select-user-dept="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -270,12 +270,15 @@ const state = reactive({
|
|||||||
openNewDatabaseModel: false
|
openNewDatabaseModel: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 共享配置状态(用于提交数据)
|
const createDefaultShareConfig = () => ({
|
||||||
const shareConfig = ref({
|
access_level: 'global',
|
||||||
is_shared: true,
|
department_ids: [],
|
||||||
accessible_department_ids: []
|
user_uids: []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const shareConfig = ref(createDefaultShareConfig())
|
||||||
|
const shareConfigFormRef = ref(null)
|
||||||
|
|
||||||
const chunkPresetOptions = CHUNK_PRESET_OPTIONS.map(({ label, value }) => ({ label, value }))
|
const chunkPresetOptions = CHUNK_PRESET_OPTIONS.map(({ label, value }) => ({ label, value }))
|
||||||
|
|
||||||
const createEmptyDatabaseForm = () => ({
|
const createEmptyDatabaseForm = () => ({
|
||||||
@ -341,11 +344,7 @@ const resetNewDatabase = () => {
|
|||||||
Object.assign(newDatabase, createEmptyDatabaseForm())
|
Object.assign(newDatabase, createEmptyDatabaseForm())
|
||||||
newDatabase.kb_type = kbTypes.value[0] || ''
|
newDatabase.kb_type = kbTypes.value[0] || ''
|
||||||
resetCreateParamValues()
|
resetCreateParamValues()
|
||||||
// 重置共享配置
|
shareConfig.value = createDefaultShareConfig()
|
||||||
shareConfig.value = {
|
|
||||||
is_shared: true,
|
|
||||||
accessible_department_ids: []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelCreateDatabase = () => {
|
const cancelCreateDatabase = () => {
|
||||||
@ -406,12 +405,10 @@ const buildRequestData = () => {
|
|||||||
requestData.additional_params.chunk_preset_id = newDatabase.chunk_preset_id || 'general'
|
requestData.additional_params.chunk_preset_id = newDatabase.chunk_preset_id || 'general'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加共享配置
|
|
||||||
requestData.share_config = {
|
requestData.share_config = {
|
||||||
is_shared: shareConfig.value.is_shared,
|
access_level: shareConfig.value.access_level,
|
||||||
accessible_departments: shareConfig.value.is_shared
|
department_ids: shareConfig.value.access_level === 'department' ? shareConfig.value.department_ids || [] : [],
|
||||||
? []
|
user_uids: shareConfig.value.access_level === 'user' ? shareConfig.value.user_uids || [] : []
|
||||||
: shareConfig.value.accessible_department_ids || []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据类型添加特定配置
|
// 根据类型添加特定配置
|
||||||
@ -445,6 +442,14 @@ const handleCreateDatabase = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shareConfigFormRef.value) {
|
||||||
|
const validation = shareConfigFormRef.value.validate()
|
||||||
|
if (!validation.valid) {
|
||||||
|
message.warning(validation.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const requestData = buildRequestData()
|
const requestData = buildRequestData()
|
||||||
try {
|
try {
|
||||||
await databaseStore.createDatabase(requestData)
|
await databaseStore.createDatabase(requestData)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user