diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 068baed..473b491 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 - attributes: - label: RAGFlow workspace code commit ID - description: Enter the commit ID associated with the issue. - placeholder: e.g., 26d3480e - 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 + value: | + 尽管问,不限语言。(All language is welcomed) - type: textarea attributes: - label: Other environment information - description: | - Enter the environment details: - value: | - - Hardware parameters: - - OS type: - - Others: - render: Markdown + label: 描述你的问题 + description: 尽可能清晰地描述,如果是bug,建议附上详细步骤和日志。 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 \ No newline at end of file + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index f50fc33..1470335 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/subtask.yml b/.github/ISSUE_TEMPLATE/subtask.yml index e2cac09..2378fed 100644 --- a/.github/ISSUE_TEMPLATE/subtask.yml +++ b/.github/ISSUE_TEMPLATE/subtask.yml @@ -1,5 +1,5 @@ name: Subtask -description: "Propose a subtask for RAGFlow" +description: "Propose a subtask for RAGFlow-Plus" title: "[Subtask]: " labels: [subtask] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8257a2d..aabc2dd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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): \ No newline at end of file +- [ ] Bug修复(Bug Fix) +- [ ] 新功能(New Feature) +- [ ] 文档更新(Documentation Update) +- [ ] 重构(Refactoring) +- [ ] 性能优化(Performance Improvement) +- [ ] 其他(Other) diff --git a/README.md b/README.md index 2981652..4cf5c1b 100644 --- a/README.md +++ b/README.md @@ -113,13 +113,6 @@ pnpm dev 交流群 -## 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. - ## 鸣谢 本项目基于以下开源项目开发: diff --git a/api/apps/document_app.py b/api/apps/document_app.py index ec7db5f..c27a4d9 100644 --- a/api/apps/document_app.py +++ b/api/apps/document_app.py @@ -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) diff --git a/api/db/services/document_service.py b/api/db/services/document_service.py index 16482c8..470f24a 100644 --- a/api/db/services/document_service.py +++ b/api/db/services/document_service.py @@ -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 = {} diff --git a/api/db/services/task_service.py b/api/db/services/task_service.py index 2d3147c..d61feef 100644 --- a/api/db/services/task_service.py +++ b/api/db/services/task_service.py @@ -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 diff --git a/deepdoc/parser/docx_parser.py b/deepdoc/parser/docx_parser.py index dfe3f37..a15acca 100644 --- a/deepdoc/parser/docx_parser.py +++ b/deepdoc/parser/docx_parser.py @@ -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 diff --git a/deepdoc/parser/pdf_parser.py b/deepdoc/parser/pdf_parser.py index 30a30f5..b8a14ca 100644 --- a/deepdoc/parser/pdf_parser.py +++ b/deepdoc/parser/pdf_parser.py @@ -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 = [] diff --git a/deepdoc/vision/ocr.py b/deepdoc/vision/ocr.py index 87ba2b6..ddd0834 100644 --- a/deepdoc/vision/ocr.py +++ b/deepdoc/vision/ocr.py @@ -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': { diff --git a/management/server/app.py b/management/server/app.py index 918e098..206d875 100644 --- a/management/server/app.py +++ b/management/server/app.py @@ -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/', 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/', 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) \ No newline at end of file diff --git a/management/server/database.py b/management/server/database.py index c3accf3..d2aa5e3 100644 --- a/management/server/database.py +++ b/management/server/database.py @@ -10,260 +10,4 @@ db_config = { "user": "root", "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 \ No newline at end of file +} \ No newline at end of file diff --git a/management/server/routes/__init__.py b/management/server/routes/__init__.py new file mode 100644 index 0000000..c86b2b9 --- /dev/null +++ b/management/server/routes/__init__.py @@ -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) \ No newline at end of file diff --git a/management/server/routes/teams/__init__.py b/management/server/routes/teams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/server/routes/teams/routes.py b/management/server/routes/teams/routes.py new file mode 100644 index 0000000..b9afb2a --- /dev/null +++ b/management/server/routes/teams/routes.py @@ -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('/', 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('/', 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('/', 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('//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('//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('//members/', 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 \ No newline at end of file diff --git a/management/server/routes/tenants/__init__.py b/management/server/routes/tenants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/server/routes/tenants/routes.py b/management/server/routes/tenants/routes.py new file mode 100644 index 0000000..a4747a8 --- /dev/null +++ b/management/server/routes/tenants/routes.py @@ -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('/', 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 \ No newline at end of file diff --git a/management/server/routes/users/__init__.py b/management/server/routes/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/server/routes/users/routes.py b/management/server/routes/users/routes.py new file mode 100644 index 0000000..68808f8 --- /dev/null +++ b/management/server/routes/users/routes.py @@ -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('/', 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('/', 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": "获取用户信息成功" + }) \ No newline at end of file diff --git a/management/server/services/teams/__init__.py b/management/server/services/teams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/server/services/teams/service.py b/management/server/services/teams/service.py new file mode 100644 index 0000000..f4f88bb --- /dev/null +++ b/management/server/services/teams/service.py @@ -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 \ No newline at end of file diff --git a/management/server/services/tenants/__init__.py b/management/server/services/tenants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/server/services/tenants/service.py b/management/server/services/tenants/service.py new file mode 100644 index 0000000..999bef8 --- /dev/null +++ b/management/server/services/tenants/service.py @@ -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 \ No newline at end of file diff --git a/management/server/services/users/__init__.py b/management/server/services/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/server/services/users/service.py b/management/server/services/users/service.py new file mode 100644 index 0000000..4ec9480 --- /dev/null +++ b/management/server/services/users/service.py @@ -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 \ No newline at end of file diff --git a/management/web/public/favicon.ico b/management/web/public/favicon.ico index c641cd0..69a2f10 100644 Binary files a/management/web/public/favicon.ico and b/management/web/public/favicon.ico differ diff --git a/management/web/src/common/apis/configs/index.ts b/management/web/src/common/apis/configs/index.ts new file mode 100644 index 0000000..9d53fbb --- /dev/null +++ b/management/web/src/common/apis/configs/index.ts @@ -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({ + url: "api/v1/tenants", + method: "get", + params + }) +} diff --git a/management/web/src/common/apis/configs/type.ts b/management/web/src/common/apis/configs/type.ts new file mode 100644 index 0000000..f70917e --- /dev/null +++ b/management/web/src/common/apis/configs/type.ts @@ -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 +}> diff --git a/management/web/src/common/apis/teams/index.ts b/management/web/src/common/apis/teams/index.ts new file mode 100644 index 0000000..d7e0ffd --- /dev/null +++ b/management/web/src/common/apis/teams/index.ts @@ -0,0 +1,44 @@ +import type * as Tables from "./type" +import { request } from "@/http/axios" + +// 查询团队整体数据 +export function getTableDataApi(params: Tables.TableRequestData) { + return request({ + 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" + }) +} diff --git a/management/web/src/common/apis/teams/type.ts b/management/web/src/common/apis/teams/type.ts new file mode 100644 index 0000000..52d3ba6 --- /dev/null +++ b/management/web/src/common/apis/teams/type.ts @@ -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 +}> diff --git a/management/web/src/common/assets/icons/team-management.svg b/management/web/src/common/assets/icons/team-management.svg new file mode 100644 index 0000000..ccaa694 --- /dev/null +++ b/management/web/src/common/assets/icons/team-management.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/management/web/src/common/assets/icons/user-config.svg b/management/web/src/common/assets/icons/user-config.svg new file mode 100644 index 0000000..2d417dc --- /dev/null +++ b/management/web/src/common/assets/icons/user-config.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/management/web/src/common/assets/icons/user-management.svg b/management/web/src/common/assets/icons/user-management.svg new file mode 100644 index 0000000..0b8fd3f --- /dev/null +++ b/management/web/src/common/assets/icons/user-management.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/management/web/src/common/assets/images/docs/preview.png b/management/web/src/common/assets/images/docs/preview.png deleted file mode 100644 index 860ea00..0000000 Binary files a/management/web/src/common/assets/images/docs/preview.png and /dev/null differ diff --git a/management/web/src/common/assets/images/layouts/logo.png b/management/web/src/common/assets/images/layouts/logo.png index d64acf0..5aba7e2 100644 Binary files a/management/web/src/common/assets/images/layouts/logo.png and b/management/web/src/common/assets/images/layouts/logo.png differ diff --git a/management/web/src/layouts/config.ts b/management/web/src/layouts/config.ts index 3ab74bc..ba3cf96 100644 --- a/management/web/src/layouts/config.ts +++ b/management/web/src/layouts/config.ts @@ -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, diff --git a/management/web/src/pages/demo/composable-demo/apis/use-fetch-select.ts b/management/web/src/pages/demo/composable-demo/apis/use-fetch-select.ts deleted file mode 100644 index 3dc6f2d..0000000 --- a/management/web/src/pages/demo/composable-demo/apis/use-fetch-select.ts +++ /dev/null @@ -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((resolve, reject) => { - // 模拟接口响应时间 2s - setTimeout(() => { - if (Math.random() < 0.8) { - // 模拟接口调用成功 - resolve(SELECT_RESPONSE_DATA) - } else { - // 模拟接口调用出错 - reject(new Error(ERROR_MESSAGE)) - ElMessage.error(ERROR_MESSAGE) - } - }, 2000) - }) -} diff --git a/management/web/src/pages/demo/composable-demo/apis/use-fullscreen-loading.ts b/management/web/src/pages/demo/composable-demo/apis/use-fullscreen-loading.ts deleted file mode 100644 index 756cd7e..0000000 --- a/management/web/src/pages/demo/composable-demo/apis/use-fullscreen-loading.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** 模拟接口响应数据 */ -const SUCCESS_RESPONSE_DATA = { - code: 0, - data: { - list: [] as number[] - }, - message: "获取成功" -} - -/** 模拟请求接口成功 */ -export function getSuccessApi(list: number[]) { - return new Promise((resolve) => { - setTimeout(() => { - resolve({ ...SUCCESS_RESPONSE_DATA, data: { list } }) - }, 1000) - }) -} - -/** 模拟请求接口失败 */ -export function getErrorApi() { - return new Promise((_resolve, reject) => { - setTimeout(() => { - reject(new Error("发生错误")) - }, 1000) - }) -} diff --git a/management/web/src/pages/demo/composable-demo/use-fetch-select.vue b/management/web/src/pages/demo/composable-demo/use-fetch-select.vue deleted file mode 100644 index 6fa5f95..0000000 --- a/management/web/src/pages/demo/composable-demo/use-fetch-select.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/management/web/src/pages/demo/composable-demo/use-fullscreen-loading.vue b/management/web/src/pages/demo/composable-demo/use-fullscreen-loading.vue deleted file mode 100644 index c04225b..0000000 --- a/management/web/src/pages/demo/composable-demo/use-fullscreen-loading.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - - diff --git a/management/web/src/pages/demo/composable-demo/use-watermark.vue b/management/web/src/pages/demo/composable-demo/use-watermark.vue deleted file mode 100644 index 5438772..0000000 --- a/management/web/src/pages/demo/composable-demo/use-watermark.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - - - diff --git a/management/web/src/pages/demo/level2/index.vue b/management/web/src/pages/demo/level2/index.vue deleted file mode 100644 index 3018f18..0000000 --- a/management/web/src/pages/demo/level2/index.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/management/web/src/pages/demo/level2/level3/index.vue b/management/web/src/pages/demo/level2/level3/index.vue deleted file mode 100644 index a1ad378..0000000 --- a/management/web/src/pages/demo/level2/level3/index.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/management/web/src/pages/demo/permission/button-level.vue b/management/web/src/pages/demo/permission/button-level.vue deleted file mode 100644 index 34c2b31..0000000 --- a/management/web/src/pages/demo/permission/button-level.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/management/web/src/pages/demo/permission/components/SwitchRoles.vue b/management/web/src/pages/demo/permission/components/SwitchRoles.vue deleted file mode 100644 index a1ce0a2..0000000 --- a/management/web/src/pages/demo/permission/components/SwitchRoles.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - - - diff --git a/management/web/src/pages/demo/permission/page-level.vue b/management/web/src/pages/demo/permission/page-level.vue deleted file mode 100644 index 4474602..0000000 --- a/management/web/src/pages/demo/permission/page-level.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/management/web/src/pages/demo/unocss/index.vue b/management/web/src/pages/demo/unocss/index.vue deleted file mode 100644 index 64b69bf..0000000 --- a/management/web/src/pages/demo/unocss/index.vue +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/management/web/src/pages/demo/vxe-table/index.vue b/management/web/src/pages/demo/vxe-table/index.vue deleted file mode 100644 index b05b5de..0000000 --- a/management/web/src/pages/demo/vxe-table/index.vue +++ /dev/null @@ -1,425 +0,0 @@ - - - - - diff --git a/management/web/src/pages/demo/vxe-table/tsx/RoleColumnSlots.tsx b/management/web/src/pages/demo/vxe-table/tsx/RoleColumnSlots.tsx deleted file mode 100644 index d8f075d..0000000 --- a/management/web/src/pages/demo/vxe-table/tsx/RoleColumnSlots.tsx +++ /dev/null @@ -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 [{cellValue}] - } -} diff --git a/management/web/src/pages/demo/vxe-table/tsx/StatusColumnSlots.tsx b/management/web/src/pages/demo/vxe-table/tsx/StatusColumnSlots.tsx deleted file mode 100644 index 94e532a..0000000 --- a/management/web/src/pages/demo/vxe-table/tsx/StatusColumnSlots.tsx +++ /dev/null @@ -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 [{value}] - } -} diff --git a/management/web/src/pages/team-management/index.vue b/management/web/src/pages/team-management/index.vue new file mode 100644 index 0000000..b85d351 --- /dev/null +++ b/management/web/src/pages/team-management/index.vue @@ -0,0 +1,392 @@ + + + + + diff --git a/management/web/src/pages/user-config/index.vue b/management/web/src/pages/user-config/index.vue new file mode 100644 index 0000000..2577949 --- /dev/null +++ b/management/web/src/pages/user-config/index.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/management/web/src/pages/demo/element-plus/index.vue b/management/web/src/pages/user-management/index.vue similarity index 99% rename from management/web/src/pages/demo/element-plus/index.vue rename to management/web/src/pages/user-management/index.vue index 233c994..ec30392 100644 --- a/management/web/src/pages/demo/element-plus/index.vue +++ b/management/web/src/pages/user-management/index.vue @@ -8,7 +8,7 @@ import { cloneDeep } from "lodash-es" defineOptions({ // 命名当前组件 - name: "ElementPlus" + name: "UserManagement" }) const loading = ref(false) diff --git a/management/web/src/router/index.ts b/management/web/src/router/index.ts index 2fa71b3..3a660e5 100644 --- a/management/web/src/router/index.ts +++ b/management/web/src/router/index.ts @@ -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: "/", diff --git a/management/web/types/auto/components.d.ts b/management/web/types/auto/components.d.ts index 01223a2..1e86400 100644 --- a/management/web/types/auto/components.d.ts +++ b/management/web/types/auto/components.d.ts @@ -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'] diff --git a/management/web/types/auto/svg-component-global.d.ts b/management/web/types/auto/svg-component-global.d.ts index 8c7ee10..3ef8ffe 100644 --- a/management/web/types/auto/svg-component-global.d.ts +++ b/management/web/types/auto/svg-component-global.d.ts @@ -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; + 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"; }>; } } diff --git a/management/web/types/auto/svg-component.d.ts b/management/web/types/auto/svg-component.d.ts index ca3f244..cc795b0 100644 --- a/management/web/types/auto/svg-component.d.ts +++ b/management/web/types/auto/svg-component.d.ts @@ -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; + 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; }