feat: 后台管理系统版本更新至v0.1.1,增加团队管理,用户配置两项功能 (#13)

This commit is contained in:
zstar 2025-04-05 22:04:05 +08:00 committed by GitHub
parent 4f5be71eb3
commit 5e1039694e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 2036 additions and 1268 deletions

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,5 @@
name: Subtask
description: "Propose a subtask for RAGFlow"
description: "Propose a subtask for RAGFlow-Plus"
title: "[Subtask]: "
labels: [subtask]

View File

@ -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)

View File

@ -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.
## 鸣谢
本项目基于以下开源项目开发:

View File

@ -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)

View File

@ -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 = {}

View File

@ -202,76 +202,138 @@ class TaskService(CommonService):
def queue_tasks(doc: dict, bucket: str, name: str):
"""
将文档解析任务分割并加入队列处理
该函数根据文档类型(PDF表格等)将文档分割成多个子任务计算任务摘要
检查是否可以重用之前的任务结果并将未完成的任务加入Redis队列进行处理
参数:
doc (dict): 文档信息字典包含idtypeparser_idparser_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

View File

@ -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

View File

@ -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 = []

View File

@ -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': {

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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": "获取用户信息成功"
})

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -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
})
}

View File

@ -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
}>

View File

@ -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"
})
}

View File

@ -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
}>

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -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,

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>]
}
}

View File

@ -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>]
}
}

View File

@ -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>

View File

@ -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>

View File

@ -8,7 +8,7 @@ import { cloneDeep } from "lodash-es"
defineOptions({
//
name: "ElementPlus"
name: "UserManagement"
})
const loading = ref<boolean>(false)

View File

@ -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: "/",

View File

@ -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']

View File

@ -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";
}>;
}
}

View File

@ -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;
}