feat: 后台管理系统版本更新至v0.1.1,增加团队管理,用户配置两项功能 (#13)
|
@ -1,73 +1,15 @@
|
|||
name: "🐞 Bug Report"
|
||||
description: Create a bug issue for RAGFlow
|
||||
description: Create a bug issue for RAGFlow-Plus
|
||||
title: "[Bug]: "
|
||||
labels: ["🐞 bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "Please check the following in order to be responded in time :)"
|
||||
options:
|
||||
- label: I have searched for existing issues [search for existing issues](https://github.com/infiniflow/ragflow/issues), including closed ones.
|
||||
required: true
|
||||
- label: I confirm that I am using English to submit this report ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
|
||||
required: true
|
||||
- label: Non-english title submitions will be closed directly ( 非英文标题的提交将会被直接关闭 ) ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Please provide the following information to help us understand the issue."
|
||||
- type: input
|
||||
value: |
|
||||
尽管问,不限语言。(All language is welcomed)
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: RAGFlow workspace code commit ID
|
||||
description: Enter the commit ID associated with the issue.
|
||||
placeholder: e.g., 26d3480e
|
||||
label: 描述你的问题
|
||||
description: 尽可能清晰地描述,如果是bug,建议附上详细步骤和日志。
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: RAGFlow image version
|
||||
description: Enter the image version(shown in RAGFlow UI, `System` page) associated with the issue.
|
||||
placeholder: e.g., 26d3480e(v0.13.0~174)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Other environment information
|
||||
description: |
|
||||
Enter the environment details:
|
||||
value: |
|
||||
- Hardware parameters:
|
||||
- OS type:
|
||||
- Others:
|
||||
render: Markdown
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Describe what you encountered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Describe what you expected.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Steps to reproduce what you encountered.
|
||||
render: Markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |
|
||||
Log, error message, or any other information can help find the root cause.
|
||||
validations:
|
||||
required: false
|
|
@ -1,28 +1,15 @@
|
|||
name: "🙋♀️ Question"
|
||||
description: Ask questions on RAGFlow
|
||||
description: Ask questions on RAGFlow-Plus
|
||||
title: "[Question]: "
|
||||
labels: ["🙋♀️ question"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Self Checks
|
||||
description: "Please check the following in order to be responded in time :)"
|
||||
options:
|
||||
- label: I have searched for existing issues [search for existing issues](https://github.com/infiniflow/ragflow/issues), including closed ones.
|
||||
required: true
|
||||
- label: I confirm that I am using English to submit this report ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
|
||||
required: true
|
||||
- label: Non-english title submitions will be closed directly ( 非英文标题的提交将会被直接关闭 ) ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
|
||||
required: true
|
||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If the previous templates don't fit with what you'd like to report or ask, please use this general question template to file issue.
|
||||
尽管问,不限语言。(All language is welcomed)
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe your problem
|
||||
description: A clear and concise description of your problem.
|
||||
label: 描述你的问题
|
||||
description: 尽可能清晰地描述,如果是bug,建议附上详细步骤和日志。
|
||||
validations:
|
||||
required: true
|
|
@ -1,5 +1,5 @@
|
|||
name: Subtask
|
||||
description: "Propose a subtask for RAGFlow"
|
||||
description: "Propose a subtask for RAGFlow-Plus"
|
||||
title: "[Subtask]: "
|
||||
labels: [subtask]
|
||||
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
### What problem does this PR solve?
|
||||
### 选择一个类型
|
||||
|
||||
_Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._
|
||||
|
||||
### Type of change
|
||||
|
||||
- [ ] Bug Fix (non-breaking change which fixes an issue)
|
||||
- [ ] New Feature (non-breaking change which adds functionality)
|
||||
- [ ] Documentation Update
|
||||
- [ ] Refactoring
|
||||
- [ ] Performance Improvement
|
||||
- [ ] Other (please describe):
|
||||
- [ ] Bug修复(Bug Fix)
|
||||
- [ ] 新功能(New Feature)
|
||||
- [ ] 文档更新(Documentation Update)
|
||||
- [ ] 重构(Refactoring)
|
||||
- [ ] 性能优化(Performance Improvement)
|
||||
- [ ] 其他(Other)
|
||||
|
|
|
@ -113,13 +113,6 @@ pnpm dev
|
|||
<img src="assets/group.jpg" width="300" alt="交流群">
|
||||
</div>
|
||||
|
||||
## License
|
||||
|
||||
版权说明:本项目在 Ragflow 项目基础上进行二开,需要遵守 Ragflow 的开源协议,如下
|
||||
|
||||
This repository is available under the [Ragflow
|
||||
Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions.
|
||||
|
||||
## 鸣谢
|
||||
|
||||
本项目基于以下开源项目开发:
|
||||
|
|
|
@ -349,6 +349,8 @@ def rm():
|
|||
@validate_request("doc_ids", "run")
|
||||
def run():
|
||||
req = request.json
|
||||
|
||||
# 检查用户是否有权限操作这些文档
|
||||
for doc_id in req["doc_ids"]:
|
||||
if not DocumentService.accessible(doc_id, current_user.id):
|
||||
return get_json_result(
|
||||
|
@ -357,29 +359,42 @@ def run():
|
|||
code=settings.RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
try:
|
||||
# 遍历所有需要处理的文档ID
|
||||
for id in req["doc_ids"]:
|
||||
# 准备更新文档状态的信息
|
||||
info = {"run": str(req["run"]), "progress": 0}
|
||||
# 如果是运行状态且需要删除旧数据,则重置统计信息
|
||||
if str(req["run"]) == TaskStatus.RUNNING.value and req.get("delete", False):
|
||||
info["progress_msg"] = ""
|
||||
info["chunk_num"] = 0
|
||||
info["token_num"] = 0
|
||||
|
||||
# 更新文档状态
|
||||
DocumentService.update_by_id(id, info)
|
||||
# 获取租户ID
|
||||
tenant_id = DocumentService.get_tenant_id(id)
|
||||
if not tenant_id:
|
||||
return get_data_error_result(message="Tenant not found!")
|
||||
# 获取文档详情
|
||||
e, doc = DocumentService.get_by_id(id)
|
||||
if not e:
|
||||
return get_data_error_result(message="Document not found!")
|
||||
# 如果需要删除旧数据
|
||||
if req.get("delete", False):
|
||||
# 删除相关任务记录
|
||||
TaskService.filter_delete([Task.doc_id == id])
|
||||
# 如果索引存在,则删除索引中的文档数据
|
||||
if settings.docStoreConn.indexExist(search.index_name(tenant_id), doc.kb_id):
|
||||
settings.docStoreConn.delete({"doc_id": id}, search.index_name(tenant_id), doc.kb_id)
|
||||
|
||||
# 如果是运行状态,则创建解析任务
|
||||
if str(req["run"]) == TaskStatus.RUNNING.value:
|
||||
e, doc = DocumentService.get_by_id(id)
|
||||
doc = doc.to_dict()
|
||||
doc["tenant_id"] = tenant_id
|
||||
# 获取文档存储位置
|
||||
bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"])
|
||||
# 将任务加入队列
|
||||
queue_tasks(doc, bucket, name)
|
||||
|
||||
return get_json_result(data=True)
|
||||
|
|
|
@ -515,6 +515,7 @@ def doc_upload_and_parse(conversation_id, file_objs, user_id):
|
|||
ParserType.EMAIL.value: email
|
||||
}
|
||||
parser_config = {"chunk_token_num": 4096, "delimiter": "\n!?;。;!?", "layout_recognize": "Plain Text"}
|
||||
# 使用线程池执行解析任务
|
||||
exe = ThreadPoolExecutor(max_workers=12)
|
||||
threads = []
|
||||
doc_nm = {}
|
||||
|
|
|
@ -202,76 +202,138 @@ class TaskService(CommonService):
|
|||
|
||||
|
||||
def queue_tasks(doc: dict, bucket: str, name: str):
|
||||
"""
|
||||
将文档解析任务分割并加入队列处理。
|
||||
|
||||
该函数根据文档类型(PDF、表格等)将文档分割成多个子任务,计算任务摘要,
|
||||
检查是否可以重用之前的任务结果,并将未完成的任务加入Redis队列进行处理。
|
||||
|
||||
参数:
|
||||
doc (dict): 文档信息字典,包含id、type、parser_id、parser_config等信息
|
||||
bucket (str): 存储桶名称
|
||||
name (str): 文件名称
|
||||
|
||||
流程:
|
||||
1. 根据文档类型(PDF/表格)将文档分割成多个子任务
|
||||
2. 为每个任务生成唯一摘要(digest)
|
||||
3. 尝试重用之前任务的处理结果
|
||||
4. 清理旧任务并更新文档状态
|
||||
5. 将新任务批量插入数据库
|
||||
6. 将未完成的任务加入Redis队列
|
||||
"""
|
||||
def new_task():
|
||||
"""
|
||||
创建一个新的任务字典,包含基本任务信息。
|
||||
|
||||
返回:
|
||||
dict: 包含任务ID、文档ID、进度和页面范围的任务字典
|
||||
"""
|
||||
return {"id": get_uuid(), "doc_id": doc["id"], "progress": 0.0, "from_page": 0, "to_page": 100000000}
|
||||
|
||||
# 存储所有解析任务的数组
|
||||
parse_task_array = []
|
||||
|
||||
# PDF文档处理逻辑
|
||||
if doc["type"] == FileType.PDF.value:
|
||||
# 从存储中获取文件内容
|
||||
file_bin = STORAGE_IMPL.get(bucket, name)
|
||||
# 获取布局识别方式,默认为"DeepDOC"
|
||||
do_layout = doc["parser_config"].get("layout_recognize", "DeepDOC")
|
||||
# 获取PDF总页数
|
||||
pages = PdfParser.total_page_number(doc["name"], file_bin)
|
||||
# 获取每个任务处理的页数,默认为12页
|
||||
page_size = doc["parser_config"].get("task_page_size", 12)
|
||||
# 对于学术论文类型,默认任务页数为22
|
||||
if doc["parser_id"] == "paper":
|
||||
page_size = doc["parser_config"].get("task_page_size", 22)
|
||||
# 对于特定解析器或非DeepDOC布局识别,将整个文档作为一个任务处理
|
||||
if doc["parser_id"] in ["one", "knowledge_graph"] or do_layout != "DeepDOC":
|
||||
page_size = 10 ** 9
|
||||
# 获取需要处理的页面范围,默认为全部页面
|
||||
page_ranges = doc["parser_config"].get("pages") or [(1, 10 ** 5)]
|
||||
# 根据页面范围和任务页数分割任务
|
||||
for s, e in page_ranges:
|
||||
# 调整页码(从0开始)
|
||||
s -= 1
|
||||
s = max(0, s)
|
||||
# 确保结束页不超过文档总页数
|
||||
e = min(e - 1, pages)
|
||||
# 按照任务页数分割任务
|
||||
for p in range(s, e, page_size):
|
||||
task = new_task()
|
||||
task["from_page"] = p
|
||||
task["to_page"] = min(p + page_size, e)
|
||||
parse_task_array.append(task)
|
||||
|
||||
# 表格文档处理逻辑
|
||||
elif doc["parser_id"] == "table":
|
||||
# 从存储中获取文件内容
|
||||
file_bin = STORAGE_IMPL.get(bucket, name)
|
||||
# 获取表格总行数
|
||||
rn = RAGFlowExcelParser.row_number(doc["name"], file_bin)
|
||||
# 每3000行作为一个任务
|
||||
for i in range(0, rn, 3000):
|
||||
task = new_task()
|
||||
task["from_page"] = i
|
||||
task["to_page"] = min(i + 3000, rn)
|
||||
parse_task_array.append(task)
|
||||
# 其他类型文档,整个文档作为一个任务处理
|
||||
else:
|
||||
parse_task_array.append(new_task())
|
||||
|
||||
# 获取文档的分块配置
|
||||
chunking_config = DocumentService.get_chunking_config(doc["id"])
|
||||
# 为每个任务生成唯一摘要(digest)
|
||||
for task in parse_task_array:
|
||||
# 创建哈希对象
|
||||
hasher = xxhash.xxh64()
|
||||
# 对分块配置中的每个字段进行哈希
|
||||
for field in sorted(chunking_config.keys()):
|
||||
if field == "parser_config":
|
||||
# 移除不需要参与哈希计算的特定配置项
|
||||
for k in ["raptor", "graphrag"]:
|
||||
if k in chunking_config[field]:
|
||||
del chunking_config[field][k]
|
||||
# 将配置字段添加到哈希计算中
|
||||
hasher.update(str(chunking_config[field]).encode("utf-8"))
|
||||
# 将任务特定字段添加到哈希计算中
|
||||
for field in ["doc_id", "from_page", "to_page"]:
|
||||
hasher.update(str(task.get(field, "")).encode("utf-8"))
|
||||
# 生成任务摘要并设置初始进度
|
||||
task_digest = hasher.hexdigest()
|
||||
task["digest"] = task_digest
|
||||
task["progress"] = 0.0
|
||||
|
||||
# 获取文档之前的任务记录
|
||||
prev_tasks = TaskService.get_tasks(doc["id"])
|
||||
# 记录重用的块数量
|
||||
ck_num = 0
|
||||
if prev_tasks:
|
||||
# 尝试重用之前任务的处理结果
|
||||
for task in parse_task_array:
|
||||
ck_num += reuse_prev_task_chunks(task, prev_tasks, chunking_config)
|
||||
# 删除文档之前的任务记录
|
||||
TaskService.filter_delete([Task.doc_id == doc["id"]])
|
||||
# 收集需要删除的块ID
|
||||
chunk_ids = []
|
||||
for task in prev_tasks:
|
||||
if task["chunk_ids"]:
|
||||
chunk_ids.extend(task["chunk_ids"].split())
|
||||
# 从文档存储中删除这些块
|
||||
if chunk_ids:
|
||||
settings.docStoreConn.delete({"id": chunk_ids}, search.index_name(chunking_config["tenant_id"]),
|
||||
chunking_config["kb_id"])
|
||||
# 更新文档的块数量
|
||||
DocumentService.update_by_id(doc["id"], {"chunk_num": ck_num})
|
||||
|
||||
# 将新任务批量插入数据库
|
||||
bulk_insert_into_db(Task, parse_task_array, True)
|
||||
# 开始解析文档
|
||||
DocumentService.begin2parse(doc["id"])
|
||||
|
||||
# 筛选出未完成的任务
|
||||
unfinished_task_array = [task for task in parse_task_array if task["progress"] < 1.0]
|
||||
# 将未完成的任务加入Redis队列
|
||||
for unfinished_task in unfinished_task_array:
|
||||
assert REDIS_CONN.queue_product(
|
||||
SVR_QUEUE_NAME, message=unfinished_task
|
||||
|
|
|
@ -23,16 +23,65 @@ from io import BytesIO
|
|||
|
||||
|
||||
class RAGFlowDocxParser:
|
||||
"""
|
||||
Word文档(.docx)解析器,用于提取文档中的文本内容和表格。
|
||||
|
||||
该解析器能够:
|
||||
1. 按页面范围提取文档中的段落文本及其样式
|
||||
2. 识别文档中的表格并将其转换为结构化文本
|
||||
3. 智能处理表格头部和内容,生成语义化的文本描述
|
||||
"""
|
||||
|
||||
def __extract_table_content(self, tb):
|
||||
"""
|
||||
从Word表格对象中提取内容并转换为DataFrame
|
||||
|
||||
参数:
|
||||
tb: docx库的Table对象
|
||||
|
||||
返回:
|
||||
处理后的表格内容文本列表
|
||||
"""
|
||||
df = []
|
||||
for row in tb.rows:
|
||||
df.append([c.text for c in row.cells])
|
||||
return self.__compose_table_content(pd.DataFrame(df))
|
||||
|
||||
def __compose_table_content(self, df):
|
||||
"""
|
||||
将表格DataFrame转换为语义化的文本描述
|
||||
|
||||
通过识别表格的结构特征(如表头、数据类型等),将表格转换为更易于理解的文本形式
|
||||
|
||||
参数:
|
||||
df: 包含表格内容的DataFrame
|
||||
|
||||
返回:
|
||||
表格内容的文本表示列表
|
||||
"""
|
||||
|
||||
def blockType(b):
|
||||
"""
|
||||
识别单元格内容的类型
|
||||
|
||||
通过正则表达式和文本特征分析,将单元格内容分类为不同类型:
|
||||
- Dt: 日期类型
|
||||
- Nu: 数字类型
|
||||
- Ca: 代码/ID类型
|
||||
- En: 英文文本
|
||||
- NE: 数字和文本混合
|
||||
- Sg: 单字符
|
||||
- Tx: 短文本
|
||||
- Lx: 长文本
|
||||
- Nr: 人名
|
||||
- Ot: 其他类型
|
||||
|
||||
参数:
|
||||
b: 单元格文本内容
|
||||
|
||||
返回:
|
||||
内容类型的字符串标识
|
||||
"""
|
||||
patt = [
|
||||
("^(20|19)[0-9]{2}[年/-][0-9]{1,2}[月/-][0-9]{1,2}日*$", "Dt"),
|
||||
(r"^(20|19)[0-9]{2}年$", "Dt"),
|
||||
|
@ -62,14 +111,21 @@ class RAGFlowDocxParser:
|
|||
|
||||
return "Ot"
|
||||
|
||||
# 表格至少需要两行才能处理
|
||||
if len(df) < 2:
|
||||
return []
|
||||
|
||||
# 统计表格中最常见的内容类型
|
||||
max_type = Counter([blockType(str(df.iloc[i, j])) for i in range(
|
||||
1, len(df)) for j in range(len(df.iloc[i, :]))])
|
||||
max_type = max(max_type.items(), key=lambda x: x[1])[0]
|
||||
|
||||
# 获取表格列数
|
||||
colnm = len(df.iloc[0, :])
|
||||
hdrows = [0] # header is not nessesarily appear in the first line
|
||||
# 默认第一行为表头
|
||||
hdrows = [0] # 表头不一定出现在第一行
|
||||
|
||||
# 如果表格主要是数字类型,则识别非数字行作为表头
|
||||
if max_type == "Nu":
|
||||
for r in range(1, len(df)):
|
||||
tys = Counter([blockType(str(df.iloc[r, j]))
|
||||
|
@ -78,18 +134,26 @@ class RAGFlowDocxParser:
|
|||
if tys != max_type:
|
||||
hdrows.append(r)
|
||||
|
||||
# 处理表格内容,将每行转换为文本
|
||||
lines = []
|
||||
for i in range(1, len(df)):
|
||||
# 跳过表头行
|
||||
if i in hdrows:
|
||||
continue
|
||||
|
||||
# 计算当前行之前的表头行
|
||||
hr = [r - i for r in hdrows]
|
||||
hr = [r for r in hr if r < 0]
|
||||
|
||||
# 找到最近的连续表头行
|
||||
t = len(hr) - 1
|
||||
while t > 0:
|
||||
if hr[t] - hr[t - 1] > 1:
|
||||
hr = hr[t:]
|
||||
break
|
||||
t -= 1
|
||||
|
||||
# 为每列构建表头描述
|
||||
headers = []
|
||||
for j in range(len(df.iloc[i, :])):
|
||||
t = []
|
||||
|
@ -102,6 +166,8 @@ class RAGFlowDocxParser:
|
|||
if t:
|
||||
t += ": "
|
||||
headers.append(t)
|
||||
|
||||
# 构建每行的文本表示
|
||||
cells = []
|
||||
for j in range(len(df.iloc[i, :])):
|
||||
if not str(df.iloc[i, j]):
|
||||
|
@ -109,31 +175,53 @@ class RAGFlowDocxParser:
|
|||
cells.append(headers[j] + str(df.iloc[i, j]))
|
||||
lines.append(";".join(cells))
|
||||
|
||||
# 根据列数决定返回格式
|
||||
if colnm > 3:
|
||||
return lines
|
||||
return ["\n".join(lines)]
|
||||
|
||||
def __call__(self, fnm, from_page=0, to_page=100000000):
|
||||
"""
|
||||
解析Word文档,提取指定页面范围内的文本和表格
|
||||
|
||||
参数:
|
||||
fnm: 文件名或二进制内容
|
||||
from_page: 起始页码(从0开始)
|
||||
to_page: 结束页码
|
||||
|
||||
返回:
|
||||
元组(secs, tbls),其中:
|
||||
- secs: 段落内容列表,每项为(文本内容, 样式名称)的元组
|
||||
- tbls: 表格内容列表
|
||||
"""
|
||||
# 根据输入类型创建Document对象
|
||||
self.doc = Document(fnm) if isinstance(
|
||||
fnm, str) else Document(BytesIO(fnm))
|
||||
pn = 0 # parsed page
|
||||
secs = [] # parsed contents
|
||||
pn = 0 # 当前解析页码
|
||||
secs = [] # 存储解析的段落内容
|
||||
|
||||
# 遍历文档中的所有段落
|
||||
for p in self.doc.paragraphs:
|
||||
# 如果超出指定页码范围,停止解析
|
||||
if pn > to_page:
|
||||
break
|
||||
|
||||
runs_within_single_paragraph = [] # save runs within the range of pages
|
||||
runs_within_single_paragraph = [] # 保存在页面范围内的文本片段
|
||||
# 遍历段落中的所有文本片段(run)
|
||||
for run in p.runs:
|
||||
if pn > to_page:
|
||||
break
|
||||
# 如果当前页码在指定范围内且段落有内容,则添加文本
|
||||
if from_page <= pn < to_page and p.text.strip():
|
||||
runs_within_single_paragraph.append(run.text) # append run.text first
|
||||
runs_within_single_paragraph.append(run.text) # 先添加文本片段
|
||||
|
||||
# wrap page break checker into a static method
|
||||
# 检查页面分隔符
|
||||
if 'lastRenderedPageBreak' in run._element.xml:
|
||||
pn += 1
|
||||
|
||||
secs.append(("".join(runs_within_single_paragraph), p.style.name if hasattr(p.style, 'name') else '')) # then concat run.text as part of the paragraph
|
||||
# 将段落文本和样式添加到结果列表
|
||||
secs.append(("".join(runs_within_single_paragraph), p.style.name if hasattr(p.style, 'name') else '')) # 然后将文本片段连接为段落的一部分
|
||||
|
||||
# 提取所有表格内容
|
||||
tbls = [self.__extract_table_content(tb) for tb in self.doc.tables]
|
||||
return secs, tbls
|
||||
|
|
|
@ -284,6 +284,24 @@ class RAGFlowPdfParser:
|
|||
b["SP"] = ii
|
||||
|
||||
def __ocr(self, pagenum, img, chars, ZM=3):
|
||||
"""
|
||||
对PDF页面图像进行OCR处理,识别文本内容并合并PDF提取的字符
|
||||
|
||||
该方法完成以下任务:
|
||||
1. 使用OCR模型检测图像中的文本框
|
||||
2. 将PDF提取的字符与OCR检测的文本框匹配合并
|
||||
3. 对没有文本的框进行文本识别
|
||||
4. 计算文本高度统计信息
|
||||
|
||||
参数:
|
||||
pagenum: 当前处理的页码
|
||||
img: 页面图像对象
|
||||
chars: 从PDF中提取的字符列表
|
||||
ZM: 缩放因子,用于坐标转换
|
||||
|
||||
返回:
|
||||
无直接返回值,但会更新self.boxes列表
|
||||
"""
|
||||
start = timer()
|
||||
bxs = self.ocr.detect(np.array(img))
|
||||
logging.info(f"__ocr detecting boxes of a image cost ({timer() - start}s)")
|
||||
|
@ -965,6 +983,26 @@ class RAGFlowPdfParser:
|
|||
|
||||
def __images__(self, fnm, zoomin=3, page_from=0,
|
||||
page_to=299, callback=None):
|
||||
"""
|
||||
处理PDF文件并提取图像和文本内容
|
||||
|
||||
该方法是PDF解析的核心,负责:
|
||||
1. 将PDF页面转换为高分辨率图像
|
||||
2. 提取页面中的字符和布局信息
|
||||
3. 使用OCR识别文本内容
|
||||
4. 分析文档语言(英文或中文)
|
||||
5. 提取文档大纲结构
|
||||
|
||||
参数:
|
||||
fnm: 文件名或二进制内容
|
||||
zoomin: 图像缩放因子,用于提高OCR精度
|
||||
page_from: 起始页码(从0开始)
|
||||
page_to: 结束页码
|
||||
callback: 进度回调函数,用于报告处理进度
|
||||
|
||||
返回:
|
||||
无直接返回值,但会更新类的内部状态
|
||||
"""
|
||||
self.lefted_chars = []
|
||||
self.mean_height = []
|
||||
self.mean_width = []
|
||||
|
|
|
@ -123,7 +123,19 @@ def load_model(model_dir, nm):
|
|||
|
||||
|
||||
class TextRecognizer:
|
||||
"""
|
||||
文本识别器类,用于识别检测到的文本区域中的具体文字内容
|
||||
|
||||
该类使用基于CTC(Connectionist Temporal Classification)的文本识别模型,
|
||||
能够将图像中的文本区域转换为文字内容
|
||||
"""
|
||||
def __init__(self, model_dir):
|
||||
"""
|
||||
初始化文本识别器
|
||||
|
||||
参数:
|
||||
model_dir: 模型文件所在目录
|
||||
"""
|
||||
self.rec_image_shape = [int(v) for v in "3, 48, 320".split(",")]
|
||||
self.rec_batch_num = 16
|
||||
postprocess_params = {
|
||||
|
@ -136,6 +148,16 @@ class TextRecognizer:
|
|||
self.input_tensor = self.predictor.get_inputs()[0]
|
||||
|
||||
def resize_norm_img(self, img, max_wh_ratio):
|
||||
"""
|
||||
调整图像大小并进行归一化处理,保持宽高比
|
||||
|
||||
参数:
|
||||
img: 输入图像
|
||||
max_wh_ratio: 最大宽高比
|
||||
|
||||
返回:
|
||||
处理后的图像张量
|
||||
"""
|
||||
imgC, imgH, imgW = self.rec_image_shape
|
||||
|
||||
assert imgC == img.shape[2]
|
||||
|
@ -394,6 +416,12 @@ class TextRecognizer:
|
|||
|
||||
|
||||
class TextDetector:
|
||||
"""
|
||||
文本检测器类,用于检测图像中的文本区域
|
||||
|
||||
该类使用基于DB(Differentiable Binarization)的文本检测模型,
|
||||
能够准确定位图像中的文本区域,并返回文本框的坐标信息
|
||||
"""
|
||||
def __init__(self, model_dir):
|
||||
pre_process_list = [{
|
||||
'DetResizeForTest': {
|
||||
|
|
|
@ -1,89 +1,20 @@
|
|||
from flask import Flask, jsonify, request
|
||||
from flask_cors import CORS
|
||||
import database
|
||||
from routes import register_routes
|
||||
|
||||
app = Flask(__name__)
|
||||
# 启用CORS,允许前端访问
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
|
||||
|
||||
# 添加 v1 前缀
|
||||
# 注册所有路由
|
||||
register_routes(app)
|
||||
|
||||
# 登录路由保留在主文件中
|
||||
@app.route('/api/v1/auth/login', methods=['POST'])
|
||||
def login():
|
||||
# 实现登录逻辑
|
||||
return {"code": 0, "data": {"token": "your-token"}, "message": "登录成功"}
|
||||
|
||||
|
||||
@app.route('/api/v1/users/me', methods=['GET'])
|
||||
def get_current_user():
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"username": "admin",
|
||||
"roles": ["admin"]
|
||||
},
|
||||
"message": "获取用户信息成功"
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/v1/users', methods=['GET'])
|
||||
def get_users():
|
||||
"""获取用户的API端点,支持分页和条件查询"""
|
||||
try:
|
||||
# 获取查询参数
|
||||
current_page = int(request.args.get('currentPage', 1))
|
||||
page_size = int(request.args.get('size', 10))
|
||||
username = request.args.get('username', '')
|
||||
email = request.args.get('email', '')
|
||||
|
||||
# 调用数据库函数获取分页和筛选后的用户数据
|
||||
users, total = database.get_users_with_pagination(current_page, page_size, username, email)
|
||||
|
||||
# 返回符合前端期望格式的数据
|
||||
return jsonify({
|
||||
"code": 0, # 成功状态码
|
||||
"data": {
|
||||
"list": users,
|
||||
"total": total
|
||||
},
|
||||
"message": "获取用户列表成功"
|
||||
})
|
||||
except Exception as e:
|
||||
# 错误处理
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"获取用户列表失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@app.route('/api/v1/users/<string:user_id>', methods=['DELETE'])
|
||||
def delete_user(user_id):
|
||||
"""删除用户的API端点"""
|
||||
database.delete_user(user_id)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": f"用户 {user_id} 删除成功"
|
||||
})
|
||||
|
||||
@app.route('/api/v1/users', methods=['POST'])
|
||||
def create_user():
|
||||
"""创建用户的API端点"""
|
||||
data = request.json
|
||||
# 创建用户
|
||||
database.create_user(user_data=data)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": "用户创建成功"
|
||||
})
|
||||
|
||||
@app.route('/api/v1/users/<string:user_id>', methods=['PUT'])
|
||||
def update_user(user_id):
|
||||
"""更新用户的API端点"""
|
||||
data = request.json
|
||||
user_id = data.get('id')
|
||||
database.update_user(user_id=user_id, user_data=data)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": f"用户 {user_id} 更新成功"
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
|
@ -11,259 +11,3 @@ db_config = {
|
|||
"password": "infini_rag_flow",
|
||||
"database": "rag_flow",
|
||||
}
|
||||
|
||||
def get_users_with_pagination(current_page, page_size, username='', email=''):
|
||||
"""查询用户信息,支持分页和条件筛选"""
|
||||
try:
|
||||
# 建立数据库连接
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 构建WHERE子句和参数
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if username:
|
||||
where_clauses.append("nickname LIKE %s")
|
||||
params.append(f"%{username}%")
|
||||
|
||||
if email:
|
||||
where_clauses.append("email LIKE %s")
|
||||
params.append(f"%{email}%")
|
||||
|
||||
# 组合WHERE子句
|
||||
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
||||
|
||||
# 查询总记录数
|
||||
count_sql = f"SELECT COUNT(*) as total FROM user WHERE {where_sql}"
|
||||
cursor.execute(count_sql, params)
|
||||
total = cursor.fetchone()['total']
|
||||
|
||||
# 计算分页偏移量
|
||||
offset = (current_page - 1) * page_size
|
||||
|
||||
# 执行分页查询
|
||||
query = f"""
|
||||
SELECT id, nickname, email, create_date, update_date, status, is_superuser
|
||||
FROM user
|
||||
WHERE {where_sql}
|
||||
ORDER BY id DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
cursor.execute(query, params + [page_size, offset])
|
||||
results = cursor.fetchall()
|
||||
|
||||
# 关闭连接
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 格式化结果
|
||||
formatted_users = []
|
||||
for user in results:
|
||||
formatted_users.append({
|
||||
"id": user["id"],
|
||||
"username": user["nickname"],
|
||||
"email": user["email"],
|
||||
"createTime": user["create_date"].strftime("%Y-%m-%d %H:%M:%S") if user["create_date"] else "",
|
||||
"updateTime": user["update_date"].strftime("%Y-%m-%d %H:%M:%S") if user["update_date"] else "",
|
||||
})
|
||||
|
||||
return formatted_users, total
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"数据库错误: {err}")
|
||||
return [], 0
|
||||
|
||||
def delete_user(user_id):
|
||||
"""删除指定ID的用户"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 删除 user 表中的用户记录
|
||||
query = "DELETE FROM user WHERE id = %s"
|
||||
cursor.execute(query, (user_id,))
|
||||
|
||||
# 删除 user_tenant 表中的关联记录
|
||||
user_tenant_query = "DELETE FROM user_tenant WHERE user_id = %s"
|
||||
cursor.execute(user_tenant_query, (user_id,))
|
||||
|
||||
# 删除 tenant 表中的关联记录
|
||||
tenant_query = "DELETE FROM tenant WHERE id = %s"
|
||||
cursor.execute(tenant_query, (user_id,))
|
||||
|
||||
# 删除 tenant_llm 表中的关联记录
|
||||
tenant_llm_query = "DELETE FROM tenant_llm WHERE tenant_id = %s"
|
||||
cursor.execute(tenant_llm_query, (user_id,))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
print(f"删除用户错误: {err}")
|
||||
return False
|
||||
|
||||
def create_user(user_data):
|
||||
"""创建新用户,并加入最早用户的团队,并使用相同的模型配置"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 查询最早创建的tenant配置
|
||||
query_earliest_tenant = """
|
||||
SELECT id, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, parser_ids, credit
|
||||
FROM tenant
|
||||
WHERE create_time = (SELECT MIN(create_time) FROM tenant)
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query_earliest_tenant)
|
||||
earliest_tenant = cursor.fetchone()
|
||||
|
||||
# 查询最早创建的tenant配置
|
||||
query_earliest_tenant_llm = """
|
||||
SELECT llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
|
||||
FROM tenant_llm
|
||||
WHERE create_time = (SELECT MIN(create_time) FROM tenant_llm)
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query_earliest_tenant_llm)
|
||||
earliest_tenant_llm = cursor.fetchone()
|
||||
|
||||
# 开始插入
|
||||
user_id = generate_uuid()
|
||||
# 获取基本信息
|
||||
username = user_data.get("username")
|
||||
email = user_data.get("email")
|
||||
password = user_data.get("password")
|
||||
# 加密密码
|
||||
encrypted_password = encrypt_password(password)
|
||||
|
||||
current_datetime = datetime.now()
|
||||
create_time = int(current_datetime.timestamp() * 1000)
|
||||
current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 插入用户表
|
||||
user_insert_query = """
|
||||
INSERT INTO user (
|
||||
id, create_time, create_date, update_time, update_date, access_token,
|
||||
nickname, password, email, avatar, language, color_schema, timezone,
|
||||
last_login_time, is_authenticated, is_active, is_anonymous, login_channel,
|
||||
status, is_superuser
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s
|
||||
)
|
||||
"""
|
||||
user_data = (
|
||||
user_id, create_time, current_date, create_time, current_date, None,
|
||||
username, encrypted_password, email, None, "Chinese", "Bright", "UTC+8 Asia/Shanghai",
|
||||
current_date, 1, 1, 0, "password",
|
||||
1, 0
|
||||
)
|
||||
cursor.execute(user_insert_query, user_data)
|
||||
|
||||
# 插入租户表
|
||||
tenant_insert_query = """
|
||||
INSERT INTO tenant (
|
||||
id, create_time, create_date, update_time, update_date, name,
|
||||
public_key, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id,
|
||||
parser_ids, credit, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s
|
||||
)
|
||||
"""
|
||||
tenant_data = (
|
||||
user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom",
|
||||
None, str(earliest_tenant['llm_id']), str(earliest_tenant['embd_id']),
|
||||
str(earliest_tenant['asr_id']), str(earliest_tenant['img2txt_id']),
|
||||
str(earliest_tenant['rerank_id']), str(earliest_tenant['tts_id']),
|
||||
str(earliest_tenant['parser_ids']), str(earliest_tenant['credit']), 1
|
||||
)
|
||||
cursor.execute(tenant_insert_query, tenant_data)
|
||||
|
||||
# 插入用户租户关系表(owner角色)
|
||||
user_tenant_insert_owner_query = """
|
||||
INSERT INTO user_tenant (
|
||||
id, create_time, create_date, update_time, update_date, user_id,
|
||||
tenant_id, role, invited_by, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
user_tenant_data_owner = (
|
||||
generate_uuid(), create_time, current_date, create_time, current_date, user_id,
|
||||
user_id, "owner", user_id, 1
|
||||
)
|
||||
cursor.execute(user_tenant_insert_owner_query, user_tenant_data_owner)
|
||||
|
||||
# 插入用户租户关系表(normal角色)
|
||||
user_tenant_insert_normal_query = """
|
||||
INSERT INTO user_tenant (
|
||||
id, create_time, create_date, update_time, update_date, user_id,
|
||||
tenant_id, role, invited_by, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
user_tenant_data_normal = (
|
||||
generate_uuid(), create_time, current_date, create_time, current_date, user_id,
|
||||
earliest_tenant['id'], "normal", earliest_tenant['id'], 1
|
||||
)
|
||||
cursor.execute(user_tenant_insert_normal_query, user_tenant_data_normal)
|
||||
|
||||
# 插入租户LLM配置表
|
||||
tenant_llm_insert_query = """
|
||||
INSERT INTO tenant_llm (
|
||||
create_time, create_date, update_time, update_date, tenant_id,
|
||||
llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
tenant_llm_data = (
|
||||
create_time, current_date, create_time, current_date, user_id,
|
||||
str(earliest_tenant_llm['llm_factory']), str(earliest_tenant_llm['model_type']), str(earliest_tenant_llm['llm_name']),
|
||||
str(earliest_tenant_llm['api_key']), str(earliest_tenant_llm['api_base']), str(earliest_tenant_llm['max_tokens']), 0
|
||||
)
|
||||
cursor.execute(tenant_llm_insert_query, tenant_llm_data)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
print(f"创建用户错误: {err}")
|
||||
return False
|
||||
|
||||
def update_user(user_id, user_data):
|
||||
"""更新用户信息"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
UPDATE user SET nickname = %s WHERE id = %s
|
||||
"""
|
||||
cursor.execute(query, (
|
||||
user_data.get("username"),
|
||||
user_id
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
print(f"更新用户错误: {err}")
|
||||
return False
|
|
@ -0,0 +1,18 @@
|
|||
# 路由模块初始化
|
||||
from flask import Blueprint
|
||||
|
||||
# 创建蓝图
|
||||
users_bp = Blueprint('users', __name__, url_prefix='/api/v1/users')
|
||||
teams_bp = Blueprint('teams', __name__, url_prefix='/api/v1/teams')
|
||||
tenants_bp = Blueprint('tenants', __name__, url_prefix='/api/v1/tenants')
|
||||
|
||||
# 导入路由
|
||||
from .users.routes import *
|
||||
from .teams.routes import *
|
||||
from .tenants.routes import *
|
||||
|
||||
def register_routes(app):
|
||||
"""注册所有路由蓝图到应用"""
|
||||
app.register_blueprint(users_bp)
|
||||
app.register_blueprint(teams_bp)
|
||||
app.register_blueprint(tenants_bp)
|
|
@ -0,0 +1,178 @@
|
|||
from flask import jsonify, request
|
||||
from services.teams.service import get_teams_with_pagination, get_team_by_id, delete_team, get_team_members, add_team_member, remove_team_member
|
||||
from .. import teams_bp
|
||||
|
||||
@teams_bp.route('', methods=['GET'])
|
||||
def get_teams():
|
||||
"""获取团队列表的API端点,支持分页和条件查询"""
|
||||
try:
|
||||
# 获取查询参数
|
||||
current_page = int(request.args.get('currentPage', 1))
|
||||
page_size = int(request.args.get('size', 10))
|
||||
team_name = request.args.get('name', '')
|
||||
|
||||
# 调用服务函数获取分页和筛选后的团队数据
|
||||
teams, total = get_teams_with_pagination(current_page, page_size, team_name)
|
||||
|
||||
# 返回符合前端期望格式的数据
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"list": teams,
|
||||
"total": total
|
||||
},
|
||||
"message": "获取团队列表成功"
|
||||
})
|
||||
except Exception as e:
|
||||
# 错误处理
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"获取团队列表失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@teams_bp.route('/<string:team_id>', methods=['GET'])
|
||||
def get_team(team_id):
|
||||
"""获取单个团队详情的API端点"""
|
||||
try:
|
||||
team = get_team_by_id(team_id)
|
||||
if team:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"data": team,
|
||||
"message": "获取团队详情成功"
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"code": 404,
|
||||
"message": f"团队 {team_id} 不存在"
|
||||
}), 404
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"获取团队详情失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@teams_bp.route('', methods=['POST'])
|
||||
def create_team_route():
|
||||
"""创建团队的API端点"""
|
||||
try:
|
||||
data = request.json
|
||||
team_id = create_team(team_data=data)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"data": {"id": team_id},
|
||||
"message": "团队创建成功"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"创建团队失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@teams_bp.route('/<string:team_id>', methods=['PUT'])
|
||||
def update_team_route(team_id):
|
||||
"""更新团队的API端点"""
|
||||
try:
|
||||
data = request.json
|
||||
success = update_team(team_id=team_id, team_data=data)
|
||||
if success:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": f"团队 {team_id} 更新成功"
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"code": 404,
|
||||
"message": f"团队 {team_id} 不存在或更新失败"
|
||||
}), 404
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"更新团队失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@teams_bp.route('/<string:team_id>', methods=['DELETE'])
|
||||
def delete_team_route(team_id):
|
||||
"""删除团队的API端点"""
|
||||
try:
|
||||
success = delete_team(team_id)
|
||||
if success:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": f"团队 {team_id} 删除成功"
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"code": 404,
|
||||
"message": f"团队 {team_id} 不存在或删除失败"
|
||||
}), 404
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"删除团队失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@teams_bp.route('/<string:team_id>/members', methods=['GET'])
|
||||
def get_team_members_route(team_id):
|
||||
"""获取团队成员的API端点"""
|
||||
try:
|
||||
print(f"正在查询团队 {team_id} 的成员")
|
||||
members = get_team_members(team_id)
|
||||
print(f"查询结果: 找到 {len(members)} 个成员")
|
||||
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"data": members,
|
||||
"message": "获取团队成员成功"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"获取团队成员异常: {str(e)}")
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"获取团队成员失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@teams_bp.route('/<string:team_id>/members', methods=['POST'])
|
||||
def add_team_member_route(team_id):
|
||||
"""添加团队成员的API端点"""
|
||||
try:
|
||||
data = request.json
|
||||
user_id = data.get('userId')
|
||||
role = data.get('role', 'member')
|
||||
success = add_team_member(team_id, user_id, role)
|
||||
if success:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": "添加团队成员成功"
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"code": 400,
|
||||
"message": "添加团队成员失败"
|
||||
}), 400
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"添加团队成员失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@teams_bp.route('/<string:team_id>/members/<string:user_id>', methods=['DELETE'])
|
||||
def remove_team_member_route(team_id, user_id):
|
||||
"""移除团队成员的API端点"""
|
||||
try:
|
||||
success = remove_team_member(team_id, user_id)
|
||||
if success:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": "移除团队成员成功"
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"code": 400,
|
||||
"message": "移除团队成员失败"
|
||||
}), 400
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"移除团队成员失败: {str(e)}"
|
||||
}), 500
|
|
@ -0,0 +1,53 @@
|
|||
from flask import jsonify, request
|
||||
from services.tenants.service import get_tenants_with_pagination, update_tenant
|
||||
from .. import tenants_bp
|
||||
|
||||
@tenants_bp.route('', methods=['GET'])
|
||||
def get_tenants():
|
||||
"""获取租户列表的API端点,支持分页和条件查询"""
|
||||
try:
|
||||
# 获取查询参数
|
||||
current_page = int(request.args.get('currentPage', 1))
|
||||
page_size = int(request.args.get('size', 10))
|
||||
username = request.args.get('username', '')
|
||||
|
||||
# 调用服务函数获取分页和筛选后的租户数据
|
||||
tenants, total = get_tenants_with_pagination(current_page, page_size, username)
|
||||
|
||||
# 返回符合前端期望格式的数据
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"list": tenants,
|
||||
"total": total
|
||||
},
|
||||
"message": "获取租户列表成功"
|
||||
})
|
||||
except Exception as e:
|
||||
# 错误处理
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"获取租户列表失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@tenants_bp.route('/<string:tenant_id>', methods=['PUT'])
|
||||
def update_tenant_route(tenant_id):
|
||||
"""更新租户的API端点"""
|
||||
try:
|
||||
data = request.json
|
||||
success = update_tenant(tenant_id=tenant_id, tenant_data=data)
|
||||
if success:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": f"租户 {tenant_id} 更新成功"
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"code": 404,
|
||||
"message": f"租户 {tenant_id} 不存在或更新失败"
|
||||
}), 404
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"更新租户失败: {str(e)}"
|
||||
}), 500
|
|
@ -0,0 +1,74 @@
|
|||
from flask import jsonify, request
|
||||
from services.users.service import get_users_with_pagination, delete_user, create_user, update_user
|
||||
from .. import users_bp
|
||||
|
||||
@users_bp.route('', methods=['GET'])
|
||||
def get_users():
|
||||
"""获取用户的API端点,支持分页和条件查询"""
|
||||
try:
|
||||
# 获取查询参数
|
||||
current_page = int(request.args.get('currentPage', 1))
|
||||
page_size = int(request.args.get('size', 10))
|
||||
username = request.args.get('username', '')
|
||||
email = request.args.get('email', '')
|
||||
|
||||
# 调用服务函数获取分页和筛选后的用户数据
|
||||
users, total = get_users_with_pagination(current_page, page_size, username, email)
|
||||
|
||||
# 返回符合前端期望格式的数据
|
||||
return jsonify({
|
||||
"code": 0, # 成功状态码
|
||||
"data": {
|
||||
"list": users,
|
||||
"total": total
|
||||
},
|
||||
"message": "获取用户列表成功"
|
||||
})
|
||||
except Exception as e:
|
||||
# 错误处理
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": f"获取用户列表失败: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@users_bp.route('/<string:user_id>', methods=['DELETE'])
|
||||
def delete_user_route(user_id):
|
||||
"""删除用户的API端点"""
|
||||
delete_user(user_id)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": f"用户 {user_id} 删除成功"
|
||||
})
|
||||
|
||||
@users_bp.route('', methods=['POST'])
|
||||
def create_user_route():
|
||||
"""创建用户的API端点"""
|
||||
data = request.json
|
||||
# 创建用户
|
||||
create_user(user_data=data)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": "用户创建成功"
|
||||
})
|
||||
|
||||
@users_bp.route('/<string:user_id>', methods=['PUT'])
|
||||
def update_user_route(user_id):
|
||||
"""更新用户的API端点"""
|
||||
data = request.json
|
||||
user_id = data.get('id')
|
||||
update_user(user_id=user_id, user_data=data)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"message": f"用户 {user_id} 更新成功"
|
||||
})
|
||||
|
||||
@users_bp.route('/me', methods=['GET'])
|
||||
def get_current_user():
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"username": "admin",
|
||||
"roles": ["admin"]
|
||||
},
|
||||
"message": "获取用户信息成功"
|
||||
})
|
|
@ -0,0 +1,271 @@
|
|||
import mysql.connector
|
||||
from datetime import datetime
|
||||
from utils import generate_uuid
|
||||
from database import db_config
|
||||
|
||||
def get_teams_with_pagination(current_page, page_size, name=''):
|
||||
"""查询团队信息,支持分页和条件筛选"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 构建WHERE子句和参数
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if name:
|
||||
where_clauses.append("t.name LIKE %s")
|
||||
params.append(f"%{name}%")
|
||||
|
||||
# 组合WHERE子句
|
||||
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
||||
|
||||
# 查询总记录数
|
||||
count_sql = f"SELECT COUNT(*) as total FROM tenant t WHERE {where_sql}"
|
||||
cursor.execute(count_sql, params)
|
||||
total = cursor.fetchone()['total']
|
||||
|
||||
# 计算分页偏移量
|
||||
offset = (current_page - 1) * page_size
|
||||
|
||||
# 执行分页查询,包含负责人信息和成员数量
|
||||
query = f"""
|
||||
SELECT
|
||||
t.id,
|
||||
t.name,
|
||||
t.create_date,
|
||||
t.update_date,
|
||||
t.status,
|
||||
(SELECT u.nickname FROM user_tenant ut JOIN user u ON ut.user_id = u.id
|
||||
WHERE ut.tenant_id = t.id AND ut.role = 'owner' LIMIT 1) as owner_name,
|
||||
(SELECT COUNT(*) FROM user_tenant ut WHERE ut.tenant_id = t.id AND ut.status = 1) as member_count
|
||||
FROM
|
||||
tenant t
|
||||
WHERE
|
||||
{where_sql}
|
||||
ORDER BY
|
||||
t.create_date DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
cursor.execute(query, params + [page_size, offset])
|
||||
results = cursor.fetchall()
|
||||
|
||||
# 关闭连接
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 格式化结果
|
||||
formatted_teams = []
|
||||
for team in results:
|
||||
owner_name = team["owner_name"] if team["owner_name"] else "未指定"
|
||||
formatted_teams.append({
|
||||
"id": team["id"],
|
||||
"name": f"{owner_name}的团队",
|
||||
"ownerName": owner_name,
|
||||
"memberCount": team["member_count"],
|
||||
"createTime": team["create_date"].strftime("%Y-%m-%d %H:%M:%S") if team["create_date"] else "",
|
||||
"updateTime": team["update_date"].strftime("%Y-%m-%d %H:%M:%S") if team["update_date"] else "",
|
||||
"status": team["status"]
|
||||
})
|
||||
|
||||
return formatted_teams, total
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"数据库错误: {err}")
|
||||
return [], 0
|
||||
|
||||
|
||||
def get_team_by_id(team_id):
|
||||
"""根据ID获取团队详情"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
query = """
|
||||
SELECT id, name, create_date, update_date, status, credit
|
||||
FROM tenant
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(query, (team_id,))
|
||||
team = cursor.fetchone()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if team:
|
||||
return {
|
||||
"id": team["id"],
|
||||
"name": team["name"],
|
||||
"createTime": team["create_date"].strftime("%Y-%m-%d %H:%M:%S") if team["create_date"] else "",
|
||||
"updateTime": team["update_date"].strftime("%Y-%m-%d %H:%M:%S") if team["update_date"] else "",
|
||||
"status": team["status"],
|
||||
"credit": team["credit"]
|
||||
}
|
||||
return None
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"数据库错误: {err}")
|
||||
return None
|
||||
|
||||
def delete_team(team_id):
|
||||
"""删除指定ID的团队"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 删除团队成员关联
|
||||
member_query = "DELETE FROM user_tenant WHERE tenant_id = %s"
|
||||
cursor.execute(member_query, (team_id,))
|
||||
|
||||
# 删除团队
|
||||
team_query = "DELETE FROM tenant WHERE id = %s"
|
||||
cursor.execute(team_query, (team_id,))
|
||||
|
||||
affected_rows = cursor.rowcount
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return affected_rows > 0
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"删除团队错误: {err}")
|
||||
return False
|
||||
|
||||
def get_team_members(team_id):
|
||||
"""获取团队成员列表"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
query = """
|
||||
SELECT ut.user_id, u.nickname, u.email, ut.role, ut.create_date
|
||||
FROM user_tenant ut
|
||||
JOIN user u ON ut.user_id = u.id
|
||||
WHERE ut.tenant_id = %s AND ut.status = 1
|
||||
ORDER BY ut.create_date DESC
|
||||
"""
|
||||
cursor.execute(query, (team_id,))
|
||||
results = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 格式化结果
|
||||
formatted_members = []
|
||||
for member in results:
|
||||
# 将 role 转换为前端需要的格式
|
||||
role = "管理员" if member["role"] == "owner" else "普通成员"
|
||||
|
||||
formatted_members.append({
|
||||
"userId": member["user_id"],
|
||||
"username": member["nickname"],
|
||||
"role": member["role"], # 保持原始角色值 "owner" 或 "normal"
|
||||
"joinTime": member["create_date"].strftime("%Y-%m-%d %H:%M:%S") if member["create_date"] else ""
|
||||
})
|
||||
|
||||
return formatted_members
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"获取团队成员错误: {err}")
|
||||
return []
|
||||
|
||||
def add_team_member(team_id, user_id, role="member"):
|
||||
"""添加团队成员"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查用户是否已经是团队成员
|
||||
check_query = """
|
||||
SELECT id FROM user_tenant
|
||||
WHERE tenant_id = %s AND user_id = %s
|
||||
"""
|
||||
cursor.execute(check_query, (team_id, user_id))
|
||||
existing = cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
# 如果已经是成员,更新角色
|
||||
update_query = """
|
||||
UPDATE user_tenant SET role = %s, status = 1
|
||||
WHERE tenant_id = %s AND user_id = %s
|
||||
"""
|
||||
cursor.execute(update_query, (role, team_id, user_id))
|
||||
else:
|
||||
# 如果不是成员,添加新记录
|
||||
current_datetime = datetime.now()
|
||||
create_time = int(current_datetime.timestamp() * 1000)
|
||||
current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
insert_query = """
|
||||
INSERT INTO user_tenant (
|
||||
id, create_time, create_date, update_time, update_date, user_id,
|
||||
tenant_id, role, invited_by, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
# 假设邀请者是系统管理员
|
||||
invited_by = "system"
|
||||
|
||||
user_tenant_data = (
|
||||
generate_uuid(), create_time, current_date, create_time, current_date, user_id,
|
||||
team_id, role, invited_by, 1
|
||||
)
|
||||
cursor.execute(insert_query, user_tenant_data)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"添加团队成员错误: {err}")
|
||||
return False
|
||||
|
||||
def remove_team_member(team_id, user_id):
|
||||
"""移除团队成员"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查是否是团队的唯一所有者
|
||||
check_owner_query = """
|
||||
SELECT COUNT(*) as owner_count FROM user_tenant
|
||||
WHERE tenant_id = %s AND role = 'owner'
|
||||
"""
|
||||
cursor.execute(check_owner_query, (team_id,))
|
||||
owner_count = cursor.fetchone()[0]
|
||||
|
||||
# 检查当前用户是否是所有者
|
||||
check_user_role_query = """
|
||||
SELECT role FROM user_tenant
|
||||
WHERE tenant_id = %s AND user_id = %s
|
||||
"""
|
||||
cursor.execute(check_user_role_query, (team_id, user_id))
|
||||
user_role = cursor.fetchone()
|
||||
|
||||
# 如果是唯一所有者,不允许移除
|
||||
if owner_count == 1 and user_role and user_role[0] == 'owner':
|
||||
return False
|
||||
|
||||
# 移除成员
|
||||
delete_query = """
|
||||
DELETE FROM user_tenant
|
||||
WHERE tenant_id = %s AND user_id = %s
|
||||
"""
|
||||
cursor.execute(delete_query, (team_id, user_id))
|
||||
affected_rows = cursor.rowcount
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return affected_rows > 0
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"移除团队成员错误: {err}")
|
||||
return False
|
|
@ -0,0 +1,121 @@
|
|||
import mysql.connector
|
||||
from datetime import datetime
|
||||
from database import db_config
|
||||
|
||||
def get_tenants_with_pagination(current_page, page_size, username=''):
|
||||
"""查询租户信息,支持分页和条件筛选"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 构建WHERE子句和参数
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if username:
|
||||
where_clauses.append("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_tenant ut
|
||||
JOIN user u ON ut.user_id = u.id
|
||||
WHERE ut.tenant_id = t.id AND u.nickname LIKE %s
|
||||
)
|
||||
""")
|
||||
params.append(f"%{username}%")
|
||||
|
||||
# 组合WHERE子句
|
||||
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
||||
|
||||
# 查询总记录数
|
||||
count_sql = f"""
|
||||
SELECT COUNT(*) as total
|
||||
FROM tenant t
|
||||
WHERE {where_sql}
|
||||
"""
|
||||
cursor.execute(count_sql, params)
|
||||
total = cursor.fetchone()['total']
|
||||
|
||||
# 计算分页偏移量
|
||||
offset = (current_page - 1) * page_size
|
||||
|
||||
# 执行分页查询
|
||||
query = f"""
|
||||
SELECT
|
||||
t.id,
|
||||
(SELECT u.nickname FROM user_tenant ut JOIN user u ON ut.user_id = u.id
|
||||
WHERE ut.tenant_id = t.id AND ut.role = 'owner' LIMIT 1) as username,
|
||||
t.llm_id as chat_model,
|
||||
t.embd_id as embedding_model,
|
||||
t.create_date,
|
||||
t.update_date
|
||||
FROM
|
||||
tenant t
|
||||
WHERE
|
||||
{where_sql}
|
||||
ORDER BY
|
||||
t.create_date DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
cursor.execute(query, params + [page_size, offset])
|
||||
results = cursor.fetchall()
|
||||
|
||||
# 关闭连接
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 格式化结果
|
||||
formatted_tenants = []
|
||||
for tenant in results:
|
||||
formatted_tenants.append({
|
||||
"id": tenant["id"],
|
||||
"username": tenant["username"] if tenant["username"] else "未指定",
|
||||
"chatModel": tenant["chat_model"] if tenant["chat_model"] else "",
|
||||
"embeddingModel": tenant["embedding_model"] if tenant["embedding_model"] else "",
|
||||
"createTime": tenant["create_date"].strftime("%Y-%m-%d %H:%M:%S") if tenant["create_date"] else "",
|
||||
"updateTime": tenant["update_date"].strftime("%Y-%m-%d %H:%M:%S") if tenant["update_date"] else ""
|
||||
})
|
||||
|
||||
return formatted_tenants, total
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"数据库错误: {err}")
|
||||
return [], 0
|
||||
|
||||
def update_tenant(tenant_id, tenant_data):
|
||||
"""更新租户信息"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 更新租户表
|
||||
current_datetime = datetime.now()
|
||||
update_time = int(current_datetime.timestamp() * 1000)
|
||||
current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
query = """
|
||||
UPDATE tenant
|
||||
SET update_time = %s,
|
||||
update_date = %s,
|
||||
llm_id = %s,
|
||||
embd_id = %s
|
||||
WHERE id = %s
|
||||
"""
|
||||
|
||||
cursor.execute(query, (
|
||||
update_time,
|
||||
current_date,
|
||||
tenant_data.get("chatModel", ""),
|
||||
tenant_data.get("embeddingModel", ""),
|
||||
tenant_id
|
||||
))
|
||||
|
||||
affected_rows = cursor.rowcount
|
||||
conn.commit()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return affected_rows > 0
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"更新租户错误: {err}")
|
||||
return False
|
|
@ -0,0 +1,260 @@
|
|||
import mysql.connector
|
||||
from datetime import datetime
|
||||
from utils import generate_uuid, encrypt_password
|
||||
from database import db_config
|
||||
|
||||
def get_users_with_pagination(current_page, page_size, username='', email=''):
|
||||
"""查询用户信息,支持分页和条件筛选"""
|
||||
try:
|
||||
# 建立数据库连接
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 构建WHERE子句和参数
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if username:
|
||||
where_clauses.append("nickname LIKE %s")
|
||||
params.append(f"%{username}%")
|
||||
|
||||
if email:
|
||||
where_clauses.append("email LIKE %s")
|
||||
params.append(f"%{email}%")
|
||||
|
||||
# 组合WHERE子句
|
||||
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
||||
|
||||
# 查询总记录数
|
||||
count_sql = f"SELECT COUNT(*) as total FROM user WHERE {where_sql}"
|
||||
cursor.execute(count_sql, params)
|
||||
total = cursor.fetchone()['total']
|
||||
|
||||
# 计算分页偏移量
|
||||
offset = (current_page - 1) * page_size
|
||||
|
||||
# 执行分页查询
|
||||
query = f"""
|
||||
SELECT id, nickname, email, create_date, update_date, status, is_superuser
|
||||
FROM user
|
||||
WHERE {where_sql}
|
||||
ORDER BY id DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
cursor.execute(query, params + [page_size, offset])
|
||||
results = cursor.fetchall()
|
||||
|
||||
# 关闭连接
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# 格式化结果
|
||||
formatted_users = []
|
||||
for user in results:
|
||||
formatted_users.append({
|
||||
"id": user["id"],
|
||||
"username": user["nickname"],
|
||||
"email": user["email"],
|
||||
"createTime": user["create_date"].strftime("%Y-%m-%d %H:%M:%S") if user["create_date"] else "",
|
||||
"updateTime": user["update_date"].strftime("%Y-%m-%d %H:%M:%S") if user["update_date"] else "",
|
||||
})
|
||||
|
||||
return formatted_users, total
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
print(f"数据库错误: {err}")
|
||||
return [], 0
|
||||
|
||||
def delete_user(user_id):
|
||||
"""删除指定ID的用户"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 删除 user 表中的用户记录
|
||||
query = "DELETE FROM user WHERE id = %s"
|
||||
cursor.execute(query, (user_id,))
|
||||
|
||||
# 删除 user_tenant 表中的关联记录
|
||||
user_tenant_query = "DELETE FROM user_tenant WHERE user_id = %s"
|
||||
cursor.execute(user_tenant_query, (user_id,))
|
||||
|
||||
# 删除 tenant 表中的关联记录
|
||||
tenant_query = "DELETE FROM tenant WHERE id = %s"
|
||||
cursor.execute(tenant_query, (user_id,))
|
||||
|
||||
# 删除 tenant_llm 表中的关联记录
|
||||
tenant_llm_query = "DELETE FROM tenant_llm WHERE tenant_id = %s"
|
||||
cursor.execute(tenant_llm_query, (user_id,))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
print(f"删除用户错误: {err}")
|
||||
return False
|
||||
|
||||
def create_user(user_data):
|
||||
"""创建新用户,并加入最早用户的团队,并使用相同的模型配置"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 查询最早创建的tenant配置
|
||||
query_earliest_tenant = """
|
||||
SELECT id, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, parser_ids, credit
|
||||
FROM tenant
|
||||
WHERE create_time = (SELECT MIN(create_time) FROM tenant)
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query_earliest_tenant)
|
||||
earliest_tenant = cursor.fetchone()
|
||||
|
||||
# 查询最早创建的tenant配置
|
||||
query_earliest_tenant_llm = """
|
||||
SELECT llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
|
||||
FROM tenant_llm
|
||||
WHERE create_time = (SELECT MIN(create_time) FROM tenant_llm)
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query_earliest_tenant_llm)
|
||||
earliest_tenant_llm = cursor.fetchone()
|
||||
|
||||
# 开始插入
|
||||
user_id = generate_uuid()
|
||||
# 获取基本信息
|
||||
username = user_data.get("username")
|
||||
email = user_data.get("email")
|
||||
password = user_data.get("password")
|
||||
# 加密密码
|
||||
encrypted_password = encrypt_password(password)
|
||||
|
||||
current_datetime = datetime.now()
|
||||
create_time = int(current_datetime.timestamp() * 1000)
|
||||
current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 插入用户表
|
||||
user_insert_query = """
|
||||
INSERT INTO user (
|
||||
id, create_time, create_date, update_time, update_date, access_token,
|
||||
nickname, password, email, avatar, language, color_schema, timezone,
|
||||
last_login_time, is_authenticated, is_active, is_anonymous, login_channel,
|
||||
status, is_superuser
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s
|
||||
)
|
||||
"""
|
||||
user_data_tuple = (
|
||||
user_id, create_time, current_date, create_time, current_date, None,
|
||||
username, encrypted_password, email, None, "Chinese", "Bright", "UTC+8 Asia/Shanghai",
|
||||
current_date, 1, 1, 0, "password",
|
||||
1, 0
|
||||
)
|
||||
cursor.execute(user_insert_query, user_data_tuple)
|
||||
|
||||
# 插入租户表
|
||||
tenant_insert_query = """
|
||||
INSERT INTO tenant (
|
||||
id, create_time, create_date, update_time, update_date, name,
|
||||
public_key, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id,
|
||||
parser_ids, credit, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s
|
||||
)
|
||||
"""
|
||||
tenant_data = (
|
||||
user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom",
|
||||
None, str(earliest_tenant['llm_id']), str(earliest_tenant['embd_id']),
|
||||
str(earliest_tenant['asr_id']), str(earliest_tenant['img2txt_id']),
|
||||
str(earliest_tenant['rerank_id']), str(earliest_tenant['tts_id']),
|
||||
str(earliest_tenant['parser_ids']), str(earliest_tenant['credit']), 1
|
||||
)
|
||||
cursor.execute(tenant_insert_query, tenant_data)
|
||||
|
||||
# 插入用户租户关系表(owner角色)
|
||||
user_tenant_insert_owner_query = """
|
||||
INSERT INTO user_tenant (
|
||||
id, create_time, create_date, update_time, update_date, user_id,
|
||||
tenant_id, role, invited_by, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
user_tenant_data_owner = (
|
||||
generate_uuid(), create_time, current_date, create_time, current_date, user_id,
|
||||
user_id, "owner", user_id, 1
|
||||
)
|
||||
cursor.execute(user_tenant_insert_owner_query, user_tenant_data_owner)
|
||||
|
||||
# 插入用户租户关系表(normal角色)
|
||||
user_tenant_insert_normal_query = """
|
||||
INSERT INTO user_tenant (
|
||||
id, create_time, create_date, update_time, update_date, user_id,
|
||||
tenant_id, role, invited_by, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
user_tenant_data_normal = (
|
||||
generate_uuid(), create_time, current_date, create_time, current_date, user_id,
|
||||
earliest_tenant['id'], "normal", earliest_tenant['id'], 1
|
||||
)
|
||||
cursor.execute(user_tenant_insert_normal_query, user_tenant_data_normal)
|
||||
|
||||
# 插入租户LLM配置表
|
||||
tenant_llm_insert_query = """
|
||||
INSERT INTO tenant_llm (
|
||||
create_time, create_date, update_time, update_date, tenant_id,
|
||||
llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
tenant_llm_data = (
|
||||
create_time, current_date, create_time, current_date, user_id,
|
||||
str(earliest_tenant_llm['llm_factory']), str(earliest_tenant_llm['model_type']), str(earliest_tenant_llm['llm_name']),
|
||||
str(earliest_tenant_llm['api_key']), str(earliest_tenant_llm['api_base']), str(earliest_tenant_llm['max_tokens']), 0
|
||||
)
|
||||
cursor.execute(tenant_llm_insert_query, tenant_llm_data)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
print(f"创建用户错误: {err}")
|
||||
return False
|
||||
|
||||
def update_user(user_id, user_data):
|
||||
"""更新用户信息"""
|
||||
try:
|
||||
conn = mysql.connector.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
UPDATE user SET nickname = %s WHERE id = %s
|
||||
"""
|
||||
cursor.execute(query, (
|
||||
user_data.get("username"),
|
||||
user_id
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
print(f"更新用户错误: {err}")
|
||||
return False
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 69 KiB |
|
@ -0,0 +1,20 @@
|
|||
import type * as Tables from "./type"
|
||||
import { request } from "@/http/axios"
|
||||
|
||||
/** 改 */
|
||||
export function updateTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
|
||||
return request({
|
||||
url: `api/v1/tenants/${data.id}`,
|
||||
method: "put",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 查 */
|
||||
export function getTableDataApi(params: Tables.TableRequestData) {
|
||||
return request<Tables.TableResponseData>({
|
||||
url: "api/v1/tenants",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
export interface CreateOrUpdateTableRequestData {
|
||||
id?: number
|
||||
username: string
|
||||
chatModel?: string
|
||||
embeddingModel?: string
|
||||
}
|
||||
|
||||
export interface TableRequestData {
|
||||
/** 当前页码 */
|
||||
currentPage: number
|
||||
/** 查询条数 */
|
||||
size: number
|
||||
/** 查询参数:用户名 */
|
||||
username?: string
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
id: number
|
||||
username: string
|
||||
chatModel: string
|
||||
embeddingModel: string
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
export type TableResponseData = ApiResponseData<{
|
||||
list: TableData[]
|
||||
total: number
|
||||
}>
|
|
@ -0,0 +1,44 @@
|
|||
import type * as Tables from "./type"
|
||||
import { request } from "@/http/axios"
|
||||
|
||||
// 查询团队整体数据
|
||||
export function getTableDataApi(params: Tables.TableRequestData) {
|
||||
return request<Tables.TableResponseData>({
|
||||
url: "api/v1/teams",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取团队成员列表
|
||||
export function getTeamMembersApi(teamId: number) {
|
||||
return request({
|
||||
url: `api/v1/teams/${teamId}/members`,
|
||||
method: "get"
|
||||
})
|
||||
}
|
||||
|
||||
// 添加团队成员
|
||||
export function addTeamMemberApi(data: { teamId: number, userId: number, role: string }) {
|
||||
return request({
|
||||
url: `api/v1/teams/${data.teamId}/members`,
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 移除团队成员
|
||||
export function removeTeamMemberApi(data: { teamId: number, memberId: number }) {
|
||||
return request({
|
||||
url: `api/v1/teams/${data.teamId}/members/${data.memberId}`,
|
||||
method: "delete"
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
export function getUsersApi() {
|
||||
return request({
|
||||
url: "api/v1/users",
|
||||
method: "get"
|
||||
})
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
export interface CreateOrUpdateTableRequestData {
|
||||
id?: number
|
||||
username: string
|
||||
email?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
export interface TableRequestData {
|
||||
/** 当前页码 */
|
||||
currentPage: number
|
||||
/** 查询条数 */
|
||||
size: number
|
||||
/** 查询参数:用户名 */
|
||||
username?: string
|
||||
/** 查询参数:邮箱 */
|
||||
email?: string
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
export type TableResponseData = ApiResponseData<{
|
||||
list: TableData[]
|
||||
total: number
|
||||
}>
|
|
@ -0,0 +1,9 @@
|
|||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
|
||||
<path d="M924.8 385.6c-22.6-53.4-54.9-101.3-96-142.4-41.1-41.1-89-73.4-142.4-96C631.1 123.8 572.5 112 512 112s-119.1 11.8-174.4 35.2c-53.4 22.6-101.3 54.9-142.4 96-41.1 41.1-73.4 89-96 142.4C75.8 440.9 64 499.5 64 560c0 132.7 58.3 257.7 159.9 343.1l1.7 1.4c5.8 4.8 13.1 7.5 20.6 7.5h531.7c7.5 0 14.8-2.7 20.6-7.5l1.7-1.4C901.7 817.7 960 692.7 960 560c0-60.5-11.9-119.1-35.2-174.4zM761.4 836H262.6C184.5 765.5 140 665.6 140 560c0-99.4 38.7-192.8 109-263 70.3-70.3 163.7-109 263-109 99.4 0 192.8 38.7 263 109 70.3 70.3 109 163.7 109 263 0 105.6-44.5 205.5-122.6 276z" p-id="2001"></path>
|
||||
<path d="M512 400c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z" p-id="2002"></path>
|
||||
<path d="M352 480c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z" p-id="2003"></path>
|
||||
<path d="M672 480c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z" p-id="2004"></path>
|
||||
<path d="M512 544c-42.1 0-76.9 32.3-80.7 73.4-1.3 14.6 9.6 27.6 24.2 28.9 0.8 0.1 1.6 0.1 2.4 0.1 13.6 0 25.1-10.3 26.5-24.1 1.1-12.3 11.4-21.7 23.8-22.3 0.6 0 1.3-0.1 1.9-0.1 13.8 0 25 11.2 25 25v90c0 14.9 12.1 27 27 27s27-12.1 27-27v-90c0-42.1-34.1-76.2-76.2-76.2z" p-id="2005"></path>
|
||||
<path d="M352 624c-42.1 0-76.9 32.3-80.7 73.4-1.3 14.6 9.6 27.6 24.2 28.9 0.8 0.1 1.6 0.1 2.4 0.1 13.6 0 25.1-10.3 26.5-24.1 1.1-12.3 11.4-21.7 23.8-22.3 0.6 0 1.3-0.1 1.9-0.1 13.8 0 25 11.2 25 25v30c0 14.9 12.1 27 27 27s27-12.1 27-27v-30c0-42.1-34.1-76.2-76.2-76.2z" p-id="2006"></path>
|
||||
<path d="M672 624c-42.1 0-76.9 32.3-80.7 73.4-1.3 14.6 9.6 27.6 24.2 28.9 0.8 0.1 1.6 0.1 2.4 0.1 13.6 0 25.1-10.3 26.5-24.1 1.1-12.3 11.4-21.7 23.8-22.3 0.6 0 1.3-0.1 1.9-0.1 13.8 0 25 11.2 25 25v30c0 14.9 12.1 27 27 27s27-12.1 27-27v-30c0-42.1-34.1-76.2-76.2-76.2z" p-id="2007"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,9 @@
|
|||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
|
||||
<path d="M924.8 385.6c-22.6-53.4-54.9-101.3-96-142.4-41.1-41.1-89-73.4-142.4-96C631.1 123.8 572.5 112 512 112s-119.1 11.8-174.4 35.2c-53.4 22.6-101.3 54.9-142.4 96-41.1 41.1-73.4 89-96 142.4C75.8 440.9 64 499.5 64 560c0 132.7 58.3 257.7 159.9 343.1l1.7 1.4c5.8 4.8 13.1 7.5 20.6 7.5h531.7c7.5 0 14.8-2.7 20.6-7.5l1.7-1.4C901.7 817.7 960 692.7 960 560c0-60.5-11.9-119.1-35.2-174.4zM761.4 836H262.6C184.5 765.5 140 665.6 140 560c0-99.4 38.7-192.8 109-263 70.3-70.3 163.7-109 263-109 99.4 0 192.8 38.7 263 109 70.3 70.3 109 163.7 109 263 0 105.6-44.5 205.5-122.6 276z" p-id="3001"></path>
|
||||
<path d="M512 320c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144z m0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z" p-id="3002"></path>
|
||||
<path d="M704 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3003"></path>
|
||||
<path d="M592 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3004"></path>
|
||||
<path d="M480 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3005"></path>
|
||||
<path d="M368 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3006"></path>
|
||||
<path d="M704 736h-368c-4.4 0-8 3.6-8 8v32c0 4.4 3.6 8 8 8h368c4.4 0 8-3.6 8-8v-32c0-4.4-3.6-8-8-8z" p-id="3007"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
|
||||
<path d="M512 112c-247.4 0-448 200.6-448 448s200.6 448 448 448 448-200.6 448-448-200.6-448-448-448z m0 76c205.4 0 372 166.6 372 372s-166.6 372-372 372-372-166.6-372-372 166.6-372 372-372z" p-id="1001"></path>
|
||||
<path d="M512 384c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96z m0 144c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z" p-id="1002"></path>
|
||||
<path d="M704 672c-19.2-28.8-44.2-51.6-73.6-67.9-20.5 15.3-46.1 24.4-73.9 24.4-28.7 0-55.1-9.6-76.1-25.8-29.4 16.3-54.3 39.2-73.5 68.1-22.8 34.1-34.9 73.8-34.9 114.2 0 5.5 4.5 10 10 10h344c5.5 0 10-4.5 10-10 0-40.4-12.1-80-32-113z" p-id="1003"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 766 B |
Before Width: | Height: | Size: 492 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 7.1 KiB |
|
@ -37,7 +37,7 @@ export interface LayoutsConfig {
|
|||
const DEFAULT_CONFIG: LayoutsConfig = {
|
||||
layoutMode: LayoutModeEnum.LeftTop,
|
||||
showSettings: true,
|
||||
showTagsView: true,
|
||||
showTagsView: false,
|
||||
fixedHeader: true,
|
||||
showFooter: true,
|
||||
showLogo: true,
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/** 模拟接口响应数据 */
|
||||
const SELECT_RESPONSE_DATA = {
|
||||
code: 0,
|
||||
data: [
|
||||
{
|
||||
label: "苹果",
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: "香蕉",
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: "橘子",
|
||||
value: 3,
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
message: "获取 Select 数据成功"
|
||||
}
|
||||
|
||||
const ERROR_MESSAGE = "接口发生错误"
|
||||
|
||||
/** 模拟接口 */
|
||||
export function getSelectDataApi() {
|
||||
return new Promise<typeof SELECT_RESPONSE_DATA>((resolve, reject) => {
|
||||
// 模拟接口响应时间 2s
|
||||
setTimeout(() => {
|
||||
if (Math.random() < 0.8) {
|
||||
// 模拟接口调用成功
|
||||
resolve(SELECT_RESPONSE_DATA)
|
||||
} else {
|
||||
// 模拟接口调用出错
|
||||
reject(new Error(ERROR_MESSAGE))
|
||||
ElMessage.error(ERROR_MESSAGE)
|
||||
}
|
||||
}, 2000)
|
||||
})
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/** 模拟接口响应数据 */
|
||||
const SUCCESS_RESPONSE_DATA = {
|
||||
code: 0,
|
||||
data: {
|
||||
list: [] as number[]
|
||||
},
|
||||
message: "获取成功"
|
||||
}
|
||||
|
||||
/** 模拟请求接口成功 */
|
||||
export function getSuccessApi(list: number[]) {
|
||||
return new Promise<typeof SUCCESS_RESPONSE_DATA>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ ...SUCCESS_RESPONSE_DATA, data: { list } })
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
/** 模拟请求接口失败 */
|
||||
export function getErrorApi() {
|
||||
return new Promise((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error("发生错误"))
|
||||
}, 1000)
|
||||
})
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { useFetchSelect } from "@@/composables/useFetchSelect"
|
||||
import { getSelectDataApi } from "./apis/use-fetch-select"
|
||||
|
||||
const { loading, options, value } = useFetchSelect({
|
||||
api: getSelectDataApi
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
该示例是演示:通过 composable 自动调用 api 后拿到 Select 组件需要的数据并传递给 Select 组件
|
||||
</el-card>
|
||||
<el-card header="Select 示例" shadow="never" v-loading="loading">
|
||||
<el-select v-model="value" filterable>
|
||||
<el-option v-for="(item, index) in options" v-bind="item" :key="index" placeholder="请选择" />
|
||||
</el-select>
|
||||
</el-card>
|
||||
<el-card header="Select V2 示例(如果数据量过多,可以选择该组件)" shadow="never" v-loading="loading">
|
||||
<el-select-v2 v-model="value" :options="options" filterable placeholder="请选择" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,60 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { useFullscreenLoading } from "@@/composables/useFullscreenLoading"
|
||||
import { getErrorApi, getSuccessApi } from "./apis/use-fullscreen-loading"
|
||||
|
||||
const svg = `
|
||||
<path class="path" d="
|
||||
M 30 15
|
||||
L 28 17
|
||||
M 25.61 25.61
|
||||
A 15 15, 0, 0, 1, 15 30
|
||||
A 15 15, 0, 1, 1, 27.99 7.5
|
||||
L 15 15
|
||||
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
|
||||
`
|
||||
|
||||
const options = {
|
||||
text: "即将发生错误...",
|
||||
background: "#F56C6C20",
|
||||
svg,
|
||||
svgViewBox: "-10, -10, 50, 50"
|
||||
}
|
||||
|
||||
async function querySuccess() {
|
||||
// 注意:
|
||||
// 1. getSuccessApi 是一个函数而非函数调用
|
||||
// 2. 如需给 getSuccessApi 函数传递参数,请在后面的括号中进行(真正的 getSuccessApi 调用)
|
||||
const res = await useFullscreenLoading(getSuccessApi)([1, 2, 3])
|
||||
ElMessage.success(`${res.message},传参为 ${res.data.list.toString()}`)
|
||||
}
|
||||
|
||||
async function queryError() {
|
||||
try {
|
||||
await useFullscreenLoading(getErrorApi, options)()
|
||||
} catch (error) {
|
||||
ElMessage.error((error as Error).message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
该示例是演示:通过将要执行的函数传递给 composable,让 composable 自动开启全屏 loading,函数执行结束后自动关闭 loading
|
||||
</el-card>
|
||||
<el-card header="示例" shadow="never">
|
||||
<el-button type="primary" @click="querySuccess">
|
||||
查询成功
|
||||
</el-button>
|
||||
<el-button type="danger" @click="queryError">
|
||||
查询失败
|
||||
</el-button>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,59 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { useWatermark } from "@@/composables/useWatermark"
|
||||
|
||||
const localRef = ref<HTMLElement | null>(null)
|
||||
const { setWatermark, clearWatermark } = useWatermark(localRef)
|
||||
const { setWatermark: setGlobalWatermark, clearWatermark: clearGlobalWatermark } = useWatermark()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
该示例是演示:通过调用 composable 开启或关闭水印,
|
||||
支持局部、全局、自定义样式(颜色、透明度、字体大小、字体、倾斜角度等),并自带防御(防删、防隐藏)和自适应功能
|
||||
</el-card>
|
||||
<el-card header="示例" shadow="never">
|
||||
<div ref="localRef" class="local" />
|
||||
<template #footer>
|
||||
<el-button-group>
|
||||
<el-button type="primary" @click="setWatermark('局部水印', { color: '#409eff' })">
|
||||
创建局部水印
|
||||
</el-button>
|
||||
<el-button type="warning" @click="setWatermark('没有防御功能的局部水印', { color: '#e6a23c', defense: false })">
|
||||
创建无防御局部水印
|
||||
</el-button>
|
||||
<el-button type="danger" @click="clearWatermark">
|
||||
清除局部水印
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button-group>
|
||||
<el-button type="primary" @click="setGlobalWatermark('全局水印', { color: '#409eff' })">
|
||||
创建全局水印
|
||||
</el-button>
|
||||
<el-button type="warning" @click="setGlobalWatermark('没有防御功能的全局水印', { color: '#e6a23c', defense: false })">
|
||||
创建无防御全局水印
|
||||
</el-button>
|
||||
<el-button type="danger" @click="clearGlobalWatermark">
|
||||
清除全局水印
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.local {
|
||||
height: 35vh;
|
||||
border: 2px dashed var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
margin-right: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<h4>
|
||||
<span>
|
||||
三级及其以上路由缓存功能默认关闭,需要请前往此配置文件中打开:
|
||||
</span>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://github.com/un-pany/v3-admin-vite/blob/main/src/router/config.ts"
|
||||
target="_blank"
|
||||
>
|
||||
src/router/config.ts
|
||||
</el-link>
|
||||
</h4>
|
||||
<el-card header="二级路由">
|
||||
<router-view />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-link {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
|
@ -1,15 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: "Level3"
|
||||
})
|
||||
|
||||
const text = ref("")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card header="三级路由">
|
||||
<el-input v-model="text" placeholder="输入任意字符测试缓存" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
|
@ -1,42 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { checkPermission } from "@@/utils/permission"
|
||||
import SwitchRoles from "./components/SwitchRoles.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<SwitchRoles />
|
||||
<el-card header="权限指令 v-permission 示例" shadow="never" class="margin-top-20">
|
||||
<el-button v-permission="['admin']">
|
||||
admin
|
||||
</el-button>
|
||||
<el-button v-permission="['admin', 'editor']">
|
||||
admin 和 editor
|
||||
</el-button>
|
||||
</el-card>
|
||||
<el-card header="权限函数 checkPermission 示例" shadow="never" class="margin-top-20">
|
||||
<el-text type="warning" size="large">
|
||||
Element Plus 的 el-tab-pane 和 el-table-column 以及其它动态渲染 DOM 的场景不适合使用 v-permission
|
||||
这种情况下你可以通过 v-if + checkPermission 来实现
|
||||
</el-text>
|
||||
<el-tabs type="border-card" class="margin-top-20">
|
||||
<el-tab-pane v-if="checkPermission(['admin'])" label="admin">
|
||||
<el-tag size="large">
|
||||
v-if="checkPermission(['admin'])"
|
||||
</el-tag>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="checkPermission(['admin', 'editor'])" label="admin 和 editor">
|
||||
<el-tag size="large">
|
||||
v-if="checkPermission(['admin', 'editor'])"
|
||||
</el-tag>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.margin-top-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,37 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const switchRoles = ref(userStore.roles[0])
|
||||
|
||||
watch(switchRoles, (value) => {
|
||||
userStore.changeRoles(value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div>
|
||||
<span>你的角色:</span>
|
||||
<el-tag v-for="(role, index) in userStore.roles" :key="index" effect="plain" size="large">
|
||||
{{ role }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="switch-roles">
|
||||
<span>切换用户:</span>
|
||||
<el-radio-group v-model="switchRoles">
|
||||
<el-radio-button label="editor" value="editor" />
|
||||
<el-radio-button label="admin" value="admin" />
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.switch-roles {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import SwitchRoles from "./components/SwitchRoles.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<SwitchRoles />
|
||||
<el-card shadow="never" class="content">
|
||||
当前页面只有「Admin」角色可见,切换角色后将不能进入该页面
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||
<template>
|
||||
<div pa-20px h-full text-center flex select-none all:transition-400>
|
||||
<div ma>
|
||||
<div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s>
|
||||
UnoCSS
|
||||
</div>
|
||||
<div op30 text-lg fw300 m1 dark:op60>
|
||||
该页面是一个 UnoCSS 的使用案例,其他页面依旧采用 Scss
|
||||
</div>
|
||||
<div m2 flex-x-center text-lg op30 hover="op80" dark:op60 dark:hover="op80">
|
||||
<a href="https://antfu.me/posts/reimagine-atomic-css-zh" target="_blank">
|
||||
推荐阅读:重新构想原子化 CSS
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,425 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import type { TableResponseData } from "@@/apis/tables/type"
|
||||
import type { ElMessageBoxOptions } from "element-plus"
|
||||
import type { VxeFormInstance, VxeFormProps, VxeGridInstance, VxeGridProps, VxeModalInstance, VxeModalProps } from "vxe-table"
|
||||
import { deleteTableDataApi, getTableDataApi } from "@@/apis/tables"
|
||||
import { RoleColumnSlots } from "./tsx/RoleColumnSlots"
|
||||
import { StatusColumnSlots } from "./tsx/StatusColumnSlots"
|
||||
|
||||
defineOptions({
|
||||
// 命名当前组件
|
||||
name: "VxeTable"
|
||||
})
|
||||
|
||||
// #region vxe-grid
|
||||
interface RowMeta {
|
||||
id: number
|
||||
username: string
|
||||
// roles: string
|
||||
// phone: string
|
||||
email: string
|
||||
// status: boolean
|
||||
createTime: string
|
||||
updateTime: string
|
||||
/** vxe-table 自动添加上去的属性 */
|
||||
_VXE_ID?: string
|
||||
}
|
||||
const xGridDom = ref<VxeGridInstance>()
|
||||
const xGridOpt: VxeGridProps = reactive({
|
||||
loading: true,
|
||||
autoResize: true,
|
||||
/** 分页配置项 */
|
||||
pagerConfig: {
|
||||
align: "right"
|
||||
},
|
||||
/** 表单配置项 */
|
||||
formConfig: {
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
itemRender: {
|
||||
name: "$input",
|
||||
props: {
|
||||
placeholder: "用户名",
|
||||
clearable: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: "phone",
|
||||
itemRender: {
|
||||
name: "$input",
|
||||
props: {
|
||||
placeholder: "手机号",
|
||||
clearable: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
itemRender: {
|
||||
name: "$buttons",
|
||||
children: [
|
||||
{
|
||||
props: {
|
||||
type: "submit",
|
||||
content: "查询",
|
||||
status: "primary"
|
||||
}
|
||||
},
|
||||
{
|
||||
props: {
|
||||
type: "reset",
|
||||
content: "重置"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
/** 工具栏配置 */
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
custom: true,
|
||||
slots: {
|
||||
buttons: "toolbar-btns"
|
||||
}
|
||||
},
|
||||
/** 自定义列配置项 */
|
||||
customConfig: {
|
||||
/** 是否允许列选中 */
|
||||
checkMethod: ({ column }) => !["username"].includes(column.field)
|
||||
},
|
||||
/** 列配置 */
|
||||
columns: [
|
||||
{
|
||||
type: "checkbox",
|
||||
width: "50px"
|
||||
},
|
||||
{
|
||||
field: "username",
|
||||
title: "用户名"
|
||||
},
|
||||
{
|
||||
field: "roles",
|
||||
title: "角色",
|
||||
/** 自定义列与 type: "html" 的列一起使用,会产生错误,所以采用 TSX 实现 */
|
||||
slots: RoleColumnSlots
|
||||
},
|
||||
{
|
||||
field: "phone",
|
||||
title: "手机号"
|
||||
},
|
||||
{
|
||||
field: "email",
|
||||
title: "邮箱"
|
||||
},
|
||||
{
|
||||
field: "status",
|
||||
title: "状态",
|
||||
slots: StatusColumnSlots
|
||||
},
|
||||
{
|
||||
field: "createTime",
|
||||
title: "创建时间"
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: "150px",
|
||||
fixed: "right",
|
||||
showOverflow: false,
|
||||
slots: {
|
||||
default: "row-operate"
|
||||
}
|
||||
}
|
||||
],
|
||||
/** 数据代理配置项(基于 Promise API) */
|
||||
proxyConfig: {
|
||||
/** 启用动态序号代理 */
|
||||
seq: true,
|
||||
/** 是否代理表单 */
|
||||
form: true,
|
||||
/** 是否自动加载,默认为 true */
|
||||
autoLoad: true,
|
||||
props: {
|
||||
total: "total"
|
||||
},
|
||||
ajax: {
|
||||
query: ({ page, form }) => {
|
||||
xGridOpt.loading = true
|
||||
crudStore.clearTable()
|
||||
return new Promise((resolve) => {
|
||||
let total = 0
|
||||
let result: RowMeta[] = []
|
||||
// 加载数据
|
||||
const callback = (res: TableResponseData) => {
|
||||
if (res?.data) {
|
||||
// 总数
|
||||
total = res.data.total
|
||||
// 列表数据
|
||||
result = res.data.list
|
||||
}
|
||||
xGridOpt.loading = false
|
||||
// 返回值有格式要求,详情见 vxe-table 官方文档
|
||||
resolve({ total, result })
|
||||
}
|
||||
// 接口需要的参数
|
||||
const params = {
|
||||
username: form.username || "",
|
||||
phone: form.phone || "",
|
||||
size: page.pageSize,
|
||||
currentPage: page.currentPage
|
||||
}
|
||||
// 调用接口
|
||||
getTableDataApi(params).then(callback).catch(callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region vxe-modal
|
||||
const xModalDom = ref<VxeModalInstance>()
|
||||
const xModalOpt: VxeModalProps = reactive({
|
||||
title: "",
|
||||
showClose: true,
|
||||
escClosable: true,
|
||||
maskClosable: true,
|
||||
beforeHideMethod: () => {
|
||||
xFormDom.value?.clearValidate()
|
||||
return Promise.resolve()
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region vxe-form
|
||||
const xFormDom = ref<VxeFormInstance>()
|
||||
const xFormOpt: VxeFormProps = reactive({
|
||||
span: 24,
|
||||
titleWidth: "100px",
|
||||
loading: false,
|
||||
/** 是否显示标题冒号 */
|
||||
titleColon: false,
|
||||
/** 表单数据 */
|
||||
data: {
|
||||
username: "",
|
||||
password: ""
|
||||
},
|
||||
/** 项列表 */
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
title: "用户名",
|
||||
itemRender: {
|
||||
name: "$input",
|
||||
props: {
|
||||
placeholder: "请输入"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
title: "密码",
|
||||
itemRender: {
|
||||
name: "$input",
|
||||
props: {
|
||||
placeholder: "请输入"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
align: "right",
|
||||
itemRender: {
|
||||
name: "$buttons",
|
||||
children: [
|
||||
{
|
||||
props: {
|
||||
content: "取消"
|
||||
},
|
||||
events: {
|
||||
click: () => xModalDom.value?.close()
|
||||
}
|
||||
},
|
||||
{
|
||||
props: {
|
||||
type: "submit",
|
||||
content: "确定",
|
||||
status: "primary"
|
||||
},
|
||||
events: {
|
||||
click: () => crudStore.onSubmitForm()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
/** 校验规则 */
|
||||
rules: {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
validator: ({ itemValue }) => {
|
||||
switch (true) {
|
||||
case !itemValue:
|
||||
return new Error("请输入")
|
||||
case !itemValue.trim():
|
||||
return new Error("空格无效")
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
validator: ({ itemValue }) => {
|
||||
switch (true) {
|
||||
case !itemValue:
|
||||
return new Error("请输入")
|
||||
case !itemValue.trim():
|
||||
return new Error("空格无效")
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region 增删改查
|
||||
const crudStore = reactive({
|
||||
/** 表单类型,true 表示修改,false 表示新增 */
|
||||
isUpdate: true,
|
||||
/** 加载表格数据 */
|
||||
commitQuery: () => xGridDom.value?.commitProxy("query"),
|
||||
/** 清空表格数据 */
|
||||
clearTable: () => xGridDom.value?.reloadData([]),
|
||||
/** 点击显示弹窗 */
|
||||
onShowModal: (row?: RowMeta) => {
|
||||
if (row) {
|
||||
crudStore.isUpdate = true
|
||||
xModalOpt.title = "修改用户"
|
||||
// 赋值
|
||||
xFormOpt.data.username = row.username
|
||||
} else {
|
||||
crudStore.isUpdate = false
|
||||
xModalOpt.title = "新增用户"
|
||||
}
|
||||
// 禁用表单项
|
||||
const props = xFormOpt.items?.[0]?.itemRender?.props
|
||||
props && (props.disabled = crudStore.isUpdate)
|
||||
xModalDom.value?.open()
|
||||
nextTick(() => {
|
||||
!crudStore.isUpdate && xFormDom.value?.reset()
|
||||
xFormDom.value?.clearValidate()
|
||||
})
|
||||
},
|
||||
/** 确定并保存 */
|
||||
onSubmitForm: () => {
|
||||
if (xFormOpt.loading) return
|
||||
xFormDom.value?.validate((errMap) => {
|
||||
if (errMap) return
|
||||
xFormOpt.loading = true
|
||||
const callback = () => {
|
||||
xFormOpt.loading = false
|
||||
xModalDom.value?.close()
|
||||
ElMessage.success("操作成功")
|
||||
!crudStore.isUpdate && crudStore.afterInsert()
|
||||
crudStore.commitQuery()
|
||||
}
|
||||
if (crudStore.isUpdate) {
|
||||
// 模拟调用修改接口成功
|
||||
setTimeout(() => callback(), 1000)
|
||||
} else {
|
||||
// 模拟调用新增接口成功
|
||||
setTimeout(() => callback(), 1000)
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 新增后是否跳入最后一页 */
|
||||
afterInsert: () => {
|
||||
const pager = xGridDom.value?.getProxyInfo()?.pager
|
||||
if (pager) {
|
||||
const currentTotal = pager.currentPage * pager.pageSize
|
||||
if (currentTotal === pager.total) {
|
||||
++pager.currentPage
|
||||
}
|
||||
}
|
||||
},
|
||||
/** 删除 */
|
||||
onDelete: (row: RowMeta) => {
|
||||
const tip = `确定 <strong style="color: var(--el-color-danger);"> 删除 </strong> 用户 <strong style="color: var(--el-color-primary);"> ${row.username} </strong> ?`
|
||||
const config: ElMessageBoxOptions = {
|
||||
type: "warning",
|
||||
showClose: true,
|
||||
closeOnClickModal: true,
|
||||
closeOnPressEscape: true,
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: "确定",
|
||||
dangerouslyUseHTMLString: true
|
||||
}
|
||||
ElMessageBox.confirm(tip, "提示", config).then(() => {
|
||||
deleteTableDataApi(row.id).then(() => {
|
||||
ElMessage.success("删除成功")
|
||||
crudStore.afterDelete()
|
||||
crudStore.commitQuery()
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 删除后是否返回上一页 */
|
||||
afterDelete: () => {
|
||||
const tableData: RowMeta[] = xGridDom.value!.getData()
|
||||
const pager = xGridDom.value?.getProxyInfo()?.pager
|
||||
if (pager && pager.currentPage > 1 && tableData.length === 1) {
|
||||
--pager.currentPage
|
||||
}
|
||||
},
|
||||
/** 更多自定义方法 */
|
||||
moreFn: () => {}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-alert
|
||||
title="数据来源"
|
||||
type="success"
|
||||
description="由 Apifox 提供在线 Mock,数据不具备真实性,仅供简单的 CRUD 操作演示。"
|
||||
show-icon
|
||||
/>
|
||||
<!-- 表格 -->
|
||||
<vxe-grid ref="xGridDom" v-bind="xGridOpt">
|
||||
<!-- 左侧按钮列表 -->
|
||||
<template #toolbar-btns>
|
||||
<vxe-button status="primary" icon="vxe-icon-add" @click="crudStore.onShowModal()">
|
||||
新增用户
|
||||
</vxe-button>
|
||||
<vxe-button status="danger" icon="vxe-icon-delete">
|
||||
批量删除
|
||||
</vxe-button>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #row-operate="{ row }">
|
||||
<el-button link type="primary" @click="crudStore.onShowModal(row)">
|
||||
修改
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="crudStore.onDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
<!-- 弹窗 -->
|
||||
<vxe-modal ref="xModalDom" v-bind="xModalOpt">
|
||||
<!-- 表单 -->
|
||||
<vxe-form ref="xFormDom" v-bind="xFormOpt" />
|
||||
</vxe-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,9 +0,0 @@
|
|||
import type { VxeColumnPropTypes } from "vxe-table/types/column"
|
||||
|
||||
export const RoleColumnSlots: VxeColumnPropTypes.Slots = {
|
||||
default: ({ row, column }) => {
|
||||
const cellValue = row[column.field]
|
||||
const type = cellValue === "admin" ? "primary" : "warning"
|
||||
return [<span class={`el-tag el-tag--${type} el-tag--plain`}>{cellValue}</span>]
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import type { VxeColumnPropTypes } from "vxe-table/types/column"
|
||||
|
||||
export const StatusColumnSlots: VxeColumnPropTypes.Slots = {
|
||||
default: ({ row, column }) => {
|
||||
const cellValue = row[column.field]
|
||||
const [type, value] = cellValue ? ["success", "启用"] : ["danger", "禁用"]
|
||||
return [<span class={`el-tag el-tag--${type} el-tag--plain`}>{value}</span>]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,392 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FormInstance } from "element-plus"
|
||||
import { addTeamMemberApi, getTableDataApi, getTeamMembersApi, getUsersApi, removeTeamMemberApi } from "@@/apis/teams"
|
||||
import { usePagination } from "@@/composables/usePagination"
|
||||
import { CirclePlus, Refresh, Search, UserFilled } from "@element-plus/icons-vue"
|
||||
|
||||
defineOptions({
|
||||
name: "TeamManagement"
|
||||
})
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
|
||||
|
||||
// 团队数据结构
|
||||
interface TeamData {
|
||||
id: number
|
||||
name: string
|
||||
ownerName: string
|
||||
memberCount: number
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
// 团队成员数据结构
|
||||
interface TeamMember {
|
||||
userId: number
|
||||
username: string
|
||||
role: string
|
||||
joinTime: string
|
||||
}
|
||||
|
||||
// 删
|
||||
function handleDelete() {
|
||||
ElMessage.success("如需解散该团队,可直接删除负责人账号")
|
||||
}
|
||||
|
||||
const tableData = ref<TeamData[]>([
|
||||
|
||||
])
|
||||
|
||||
const searchData = reactive({
|
||||
name: ""
|
||||
})
|
||||
|
||||
// 存储多选的表格数据
|
||||
const multipleSelection = ref<TeamData[]>([])
|
||||
|
||||
function getTableData() {
|
||||
loading.value = true
|
||||
|
||||
getTableDataApi({
|
||||
currentPage: paginationData.currentPage,
|
||||
size: paginationData.pageSize,
|
||||
name: searchData.name
|
||||
}).then(({ data }) => {
|
||||
paginationData.total = data.total
|
||||
tableData.value = data.list
|
||||
// 清空选中数据
|
||||
multipleSelection.value = []
|
||||
}).catch(() => {
|
||||
tableData.value = []
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
|
||||
}
|
||||
|
||||
const searchFormRef = ref<FormInstance | null>(null)
|
||||
function resetSearch() {
|
||||
searchFormRef.value?.resetFields()
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 表格多选事件处理
|
||||
function handleSelectionChange(selection: TeamData[]) {
|
||||
multipleSelection.value = selection
|
||||
}
|
||||
// 团队成员管理相关
|
||||
const memberDialogVisible = ref<boolean>(false)
|
||||
const currentTeam = ref<TeamData | null>(null)
|
||||
const teamMembers = ref<TeamMember[]>([])
|
||||
const memberLoading = ref<boolean>(false)
|
||||
// 添加成员相关状态
|
||||
const addMemberDialogVisible = ref<boolean>(false)
|
||||
const userList = ref<{ id: number, username: string }[]>([])
|
||||
const userLoading = ref<boolean>(false)
|
||||
const selectedUser = ref<number | null>(null)
|
||||
const selectedRole = ref<string>("normal")
|
||||
|
||||
function handleManageMembers(row: TeamData) {
|
||||
currentTeam.value = row
|
||||
memberDialogVisible.value = true
|
||||
getTeamMembers(row.id)
|
||||
}
|
||||
|
||||
// 获取团队成员列表
|
||||
function getTeamMembers(teamId: number) {
|
||||
memberLoading.value = true
|
||||
getTeamMembersApi(teamId)
|
||||
.then((response) => {
|
||||
if (response.data && Array.isArray(response.data.list)) {
|
||||
teamMembers.value = response.data.list
|
||||
} else if (Array.isArray(response.data)) {
|
||||
teamMembers.value = response.data
|
||||
} else {
|
||||
teamMembers.value = []
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
teamMembers.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
memberLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 添加成员
|
||||
function handleAddMember() {
|
||||
// 打开添加成员对话框
|
||||
addMemberDialogVisible.value = true
|
||||
// 获取可添加的用户列表
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
function getUserList() {
|
||||
userLoading.value = true
|
||||
getUsersApi().then(({ data }) => {
|
||||
userList.value = data.list
|
||||
}).catch(() => {
|
||||
userList.value = []
|
||||
}).finally(() => {
|
||||
userLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 确认添加成员
|
||||
function confirmAddMember() {
|
||||
if (!selectedUser.value) {
|
||||
ElMessage.warning("请选择要添加的用户")
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTeam.value) {
|
||||
ElMessage.error("当前团队信息不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用添加成员API
|
||||
addTeamMemberApi({
|
||||
teamId: currentTeam.value.id,
|
||||
userId: selectedUser.value,
|
||||
role: selectedRole.value
|
||||
}).then(() => {
|
||||
ElMessage.success("添加成员成功")
|
||||
// 关闭对话框
|
||||
addMemberDialogVisible.value = false
|
||||
// 重新获取成员列表
|
||||
getTeamMembers(currentTeam.value!.id)
|
||||
// 刷新团队列表(更新成员数量)
|
||||
getTableData()
|
||||
// 重置选择
|
||||
selectedUser.value = null
|
||||
selectedRole.value = "normal"
|
||||
}).catch((error) => {
|
||||
console.error("添加成员失败:", error)
|
||||
ElMessage.error("添加成员失败")
|
||||
})
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
function handleRemoveMember(member: TeamMember) {
|
||||
ElMessageBox.confirm(`确认将 ${member.username} 从团队中移除吗?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
removeTeamMemberApi({
|
||||
teamId: currentTeam.value?.id,
|
||||
memberId: member.userId
|
||||
}).then(() => {
|
||||
ElMessage.success("成员移除成功")
|
||||
// 重新获取成员列表
|
||||
if (currentTeam.value) {
|
||||
getTeamMembers(currentTeam.value.id)
|
||||
}
|
||||
getTableData()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 监听分页参数的变化
|
||||
watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card v-loading="loading" shadow="never" class="search-wrapper">
|
||||
<el-form ref="searchFormRef" :inline="true" :model="searchData">
|
||||
<el-form-item prop="name" label="团队名称">
|
||||
<el-input v-model="searchData.name" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="resetSearch">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card v-loading="loading" shadow="never">
|
||||
<div class="table-wrapper">
|
||||
<el-table :data="tableData" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column prop="name" label="团队名称" align="center" />
|
||||
<el-table-column prop="ownerName" label="负责人" align="center" />
|
||||
<el-table-column prop="memberCount" label="成员数量" align="center" />
|
||||
<el-table-column prop="createTime" label="创建时间" align="center" />
|
||||
<el-table-column prop="updateTime" label="更新时间" align="center" />
|
||||
<el-table-column fixed="right" label="操作" width="220" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="success" text bg size="small" :icon="UserFilled" @click="handleManageMembers(scope.row)">
|
||||
成员管理
|
||||
</el-button>
|
||||
<el-button type="danger" text bg size="small" @click="handleDelete(scope.row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pager-wrapper">
|
||||
<el-pagination
|
||||
background
|
||||
:layout="paginationData.layout"
|
||||
:page-sizes="paginationData.pageSizes"
|
||||
:total="paginationData.total"
|
||||
:page-size="paginationData.pageSize"
|
||||
:current-page="paginationData.currentPage"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 团队成员管理 -->
|
||||
<el-dialog
|
||||
v-model="memberDialogVisible"
|
||||
:title="`${currentTeam?.name || ''} - 成员管理`"
|
||||
width="50%"
|
||||
>
|
||||
<div v-if="currentTeam">
|
||||
<div class="team-info">
|
||||
<p><strong>团队名称:</strong>{{ currentTeam.name }}</p>
|
||||
<p><strong>负责人:</strong>{{ currentTeam.ownerName }}</p>
|
||||
</div>
|
||||
|
||||
<div class="member-toolbar">
|
||||
<el-button type="primary" :icon="CirclePlus" size="small" @click="handleAddMember">
|
||||
添加成员
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="teamMembers" style="width: 100%" v-loading="memberLoading">
|
||||
<el-table-column prop="username" label="用户名" align="center" />
|
||||
<el-table-column prop="role" label="角色" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.role === 'owner' ? '拥有者' : (scope.row.role === 'normal' ? '普通成员' : scope.row.role) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="joinTime" label="加入时间" align="center" />
|
||||
<el-table-column fixed="right" label="操作" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="danger"
|
||||
text
|
||||
bg
|
||||
size="small"
|
||||
@click="handleRemoveMember(scope.row)"
|
||||
:disabled="scope.row.role === 'owner'"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="teamMembers.length === 0" class="empty-data">
|
||||
<el-empty description="暂无成员数据" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="memberDialogVisible = false">
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加成员对话框 -->
|
||||
<el-dialog
|
||||
v-model="addMemberDialogVisible"
|
||||
title="添加团队成员"
|
||||
width="30%"
|
||||
>
|
||||
<div v-loading="userLoading">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="选择用户">
|
||||
<el-select v-model="selectedUser" placeholder="请选择用户" style="width: 100%">
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:label="user.username"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-radio-group v-model="selectedRole">
|
||||
<el-radio label="normal">
|
||||
普通成员
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="addMemberDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="confirmAddMember">
|
||||
确认
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
margin-bottom: 20px;
|
||||
:deep(.el-card__body) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pager-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.team-info {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.member-toolbar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.empty-data {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,206 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CreateOrUpdateTableRequestData, TableData } from "@@/apis/configs/type"
|
||||
import type { FormInstance, FormRules } from "element-plus"
|
||||
import { getTableDataApi, updateTableDataApi } from "@@/apis/configs"
|
||||
import { usePagination } from "@@/composables/usePagination"
|
||||
import { CirclePlus, Delete, Refresh, RefreshRight, Search } from "@element-plus/icons-vue"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
defineOptions({
|
||||
// 命名当前组件
|
||||
name: "UserConfig"
|
||||
})
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
|
||||
|
||||
// #region 增
|
||||
const DEFAULT_FORM_DATA: CreateOrUpdateTableRequestData = {
|
||||
id: undefined,
|
||||
username: "",
|
||||
chatModel: "",
|
||||
embeddingModel: ""
|
||||
}
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const formData = ref<CreateOrUpdateTableRequestData>(cloneDeep(DEFAULT_FORM_DATA))
|
||||
|
||||
// 删除响应
|
||||
function handleDelete() {
|
||||
ElMessage.success("如需删除租户配置,可直接删除负责人账号")
|
||||
}
|
||||
|
||||
// 改
|
||||
function handleUpdate(row: TableData) {
|
||||
dialogVisible.value = true
|
||||
formData.value = cloneDeep({
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
chatModel: row.chatModel,
|
||||
embeddingModel: row.embeddingModel
|
||||
})
|
||||
}
|
||||
|
||||
// 处理修改表单提交
|
||||
function submitForm() {
|
||||
loading.value = true
|
||||
updateTableDataApi(formData.value)
|
||||
.then(() => {
|
||||
ElMessage.success("修改成功")
|
||||
dialogVisible.value = false
|
||||
getTableData() // 刷新表格数据
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("修改失败")
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 查
|
||||
const tableData = ref<TableData[]>([])
|
||||
const searchFormRef = ref<FormInstance | null>(null)
|
||||
const searchData = reactive({
|
||||
username: ""
|
||||
})
|
||||
|
||||
// 存储多选的表格数据
|
||||
const multipleSelection = ref<TableData[]>([])
|
||||
|
||||
function getTableData() {
|
||||
loading.value = true
|
||||
getTableDataApi({
|
||||
currentPage: paginationData.currentPage,
|
||||
size: paginationData.pageSize,
|
||||
username: searchData.username
|
||||
}).then(({ data }) => {
|
||||
paginationData.total = data.total
|
||||
tableData.value = data.list
|
||||
// 清空选中数据
|
||||
multipleSelection.value = []
|
||||
}).catch(() => {
|
||||
tableData.value = []
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
function handleSearch() {
|
||||
paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
|
||||
}
|
||||
function resetSearch() {
|
||||
searchFormRef.value?.resetFields()
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 表格多选事件处理
|
||||
function handleSelectionChange(selection: TableData[]) {
|
||||
multipleSelection.value = selection
|
||||
}
|
||||
|
||||
// 监听分页参数的变化
|
||||
watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card v-loading="loading" shadow="never" class="search-wrapper">
|
||||
<el-form ref="searchFormRef" :inline="true" :model="searchData">
|
||||
<el-form-item prop="username" label="用户名">
|
||||
<el-input v-model="searchData.username" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="resetSearch">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card v-loading="loading" shadow="never">
|
||||
<div class="table-wrapper">
|
||||
<el-table :data="tableData" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column prop="username" label="用户名" align="center" />
|
||||
<el-table-column prop="chatModel" label="聊天模型" align="center" />
|
||||
<el-table-column prop="embeddingModel" label="嵌入模型" align="center" />
|
||||
<el-table-column prop="updateTime" label="更新时间" align="center" />
|
||||
<el-table-column fixed="right" label="操作" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" text bg size="small" @click="handleUpdate(scope.row)">
|
||||
修改
|
||||
</el-button>
|
||||
<el-button type="danger" text bg size="small" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pager-wrapper">
|
||||
<el-pagination
|
||||
background
|
||||
:layout="paginationData.layout"
|
||||
:page-sizes="paginationData.pageSizes"
|
||||
:total="paginationData.total"
|
||||
:page-size="paginationData.pageSize"
|
||||
:current-page="paginationData.currentPage"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 修改对话框 -->
|
||||
<el-dialog v-model="dialogVisible" title="修改配置" width="30%">
|
||||
<el-form :model="formData" label-width="100px">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="formData.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="聊天模型">
|
||||
<el-input v-model="formData.chatModel" />
|
||||
</el-form-item>
|
||||
<el-form-item label="嵌入模型">
|
||||
<el-input v-model="formData.embeddingModel" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitForm">
|
||||
确认
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
margin-bottom: 20px;
|
||||
:deep(.el-card__body) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pager-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@ import { cloneDeep } from "lodash-es"
|
|||
|
||||
defineOptions({
|
||||
// 命名当前组件
|
||||
name: "ElementPlus"
|
||||
name: "UserManagement"
|
||||
})
|
||||
|
||||
const loading = ref<boolean>(false)
|
|
@ -89,15 +89,51 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||
children: [
|
||||
{
|
||||
path: "dashboard",
|
||||
component: () => import("@/pages/demo/element-plus/index.vue"),
|
||||
name: "ElementPlus",
|
||||
component: () => import("@/pages/user-management/index.vue"),
|
||||
name: "UserManagement",
|
||||
meta: {
|
||||
title: "用户管理",
|
||||
svgIcon: "dashboard",
|
||||
svgIcon: "user-management",
|
||||
affix: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/team",
|
||||
component: Layouts,
|
||||
redirect: "/team/index",
|
||||
children: [
|
||||
{
|
||||
path: "index",
|
||||
component: () => import("@/pages/team-management/index.vue"),
|
||||
name: "Team",
|
||||
meta: {
|
||||
title: "团队管理",
|
||||
svgIcon: "team-management",
|
||||
affix: false,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: Layouts,
|
||||
redirect: "/config/index",
|
||||
children: [
|
||||
{
|
||||
path: "index",
|
||||
component: () => import("@/pages/user-config/index.vue"),
|
||||
name: "UserConfig",
|
||||
meta: {
|
||||
title: "用户配置",
|
||||
svgIcon: "user-config",
|
||||
affix: false,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
// {
|
||||
// path: "/",
|
||||
|
|
|
@ -33,9 +33,13 @@ declare module 'vue' {
|
|||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
|
|
@ -9,18 +9,18 @@ declare module 'vue' {
|
|||
export interface GlobalComponents {
|
||||
SvgIcon: import("vue").DefineComponent<{
|
||||
name: {
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search">;
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
|
||||
default: string;
|
||||
required: true;
|
||||
};
|
||||
}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
name: {
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search">;
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
|
||||
default: string;
|
||||
required: true;
|
||||
};
|
||||
}>>, {
|
||||
name: "dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search";
|
||||
name: "dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,20 +7,20 @@
|
|||
declare module '~virtual/svg-component' {
|
||||
const SvgIcon: import("vue").DefineComponent<{
|
||||
name: {
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search">;
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
|
||||
default: string;
|
||||
required: true;
|
||||
};
|
||||
}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
name: {
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search">;
|
||||
type: import("vue").PropType<"dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
|
||||
default: string;
|
||||
required: true;
|
||||
};
|
||||
}>>, {
|
||||
name: "dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search";
|
||||
name: "dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
|
||||
}>;
|
||||
export const svgNames: ["dashboard", "fullscreen-exit", "fullscreen", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search"];
|
||||
export type SvgName = "dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search";
|
||||
export const svgNames: ["dashboard", "fullscreen-exit", "fullscreen", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"];
|
||||
export type SvgName = "dashboard" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
|
||||
export default SvgIcon;
|
||||
}
|
||||
|
|