mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-29 07:52:50 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
c33acb6f76
28
README.md
28
README.md
|
|
@ -10,35 +10,28 @@
|
|||
|
||||
MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。
|
||||
|
||||
- **多模型**:支持对接主流的大模型,包括本地私有大模型(如 Llama 2)、Azure OpenAI 和百度千帆大模型等;
|
||||
- **多模型支持**:支持对接主流的大模型,包括本地私有大模型(如 Llama 2)、Azure OpenAI 和百度千帆大模型等;
|
||||
- **开箱即用**:支持直接上传文档、自动爬取在线文档,支持文本自动拆分、向量化,智能问答交互体验好;
|
||||
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```
|
||||
docker run -d --name=maxkb -p 8080:8080 1panel/maxkb
|
||||
docker run -d --name=maxkb -p 8080:8000 -v /opt/maxkb/data:/var/lib/postgresql/data 1panel/maxkb
|
||||
```
|
||||
## 自定义持久化数据
|
||||
|
||||
- 在主机系统上创建一个存储数据的目录,例如:/opt/maxkb/data
|
||||
- 在主机系统上创建一个存储配置文件的目录,例如:/opt/maxkb/conf
|
||||
```
|
||||
docker run --name=maxkb -p 8080:8000 -v /opt/maxkb/data:/var/lib/postgresql/data -v /opt/maxkb/conf:/opt/maxkb/conf -d 1panel/maxkb
|
||||
```
|
||||
也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB + Ollama(Llama 2),30 分钟内即可上线基于本地大模型的知识库问答系统。
|
||||
|
||||
|
||||
也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB + Ollama + Llama 2,30 分钟内即可上线基于本地大模型的知识库问答系统。
|
||||
|
||||
## UI 展示
|
||||
|
||||
<table style="border-collapse: collapse; border: 1px solid black;">
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/2b893a25-ae46-48da-b6a1-61d23015565e" alt="Demo1" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/3e50e7ff-cdc4-4a37-b430-d84975f11d4e" alt="Demo2" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/2b893a25-ae46-48da-b6a1-61d23015565e" alt="MaxKB Demo1" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/3e50e7ff-cdc4-4a37-b430-d84975f11d4e" alt="MaxKB Demo2" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/dfdcc03f-ef36-4f75-bb82-797c0f9da1ad" alt="Demo3" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/884a9db1-3f93-4013-bc8f-a3f0dbcfeb2f" alt="Demo4" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/dfdcc03f-ef36-4f75-bb82-797c0f9da1ad" alt="MaxKB Demo3" /></td>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/80892890/884a9db1-3f93-4013-bc8f-a3f0dbcfeb2f" alt="MaxKB Demo4" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
|
@ -46,13 +39,12 @@ docker run --name=maxkb -p 8080:8000 -v /opt/maxkb/data:/var/lib/postgresql/data
|
|||
|
||||
[论坛](https://bbs.fit2cloud.com/c/mk/11)
|
||||
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 前端:[Vue.js](https://cn.vuejs.org/)
|
||||
- 后端:[Django](https://www.djangoproject.com/)
|
||||
- 后端:[Python / Django](https://www.djangoproject.com/)
|
||||
- Langchain:[Langchain](https://www.langchain.com/)
|
||||
- 向量数据库:[PostgreSQL](https://www.postgresql.org/)
|
||||
- 向量数据库:[PostgreSQL / pgvector](https://www.postgresql.org/)
|
||||
- 大模型:Azure OpenAI、百度千帆大模型、[Ollama](https://github.com/ollama/ollama)
|
||||
|
||||
## License
|
||||
|
|
|
|||
|
|
@ -11,7 +11,16 @@ import os
|
|||
|
||||
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
|
||||
from Crypto.PublicKey import RSA
|
||||
from django.db.models import QuerySet
|
||||
from django.core import cache
|
||||
|
||||
from setting.models import SystemSetting, SettingType
|
||||
|
||||
import threading
|
||||
|
||||
lock = threading.Lock()
|
||||
rsa_cache = cache.caches['default']
|
||||
cache_key = "rsa_key"
|
||||
# 对密钥加密的密码
|
||||
secret_code = "mac_kb_password"
|
||||
|
||||
|
|
@ -31,15 +40,40 @@ def generate():
|
|||
|
||||
|
||||
def get_key_pair():
|
||||
if not os.path.exists("/opt/maxkb/conf/receiver.pem"):
|
||||
rsa_value = rsa_cache.get(cache_key)
|
||||
if rsa_value is None:
|
||||
lock.acquire()
|
||||
rsa_value = rsa_cache.get(cache_key)
|
||||
if rsa_value is not None:
|
||||
return rsa_value
|
||||
try:
|
||||
rsa_value = get_key_pair_by_sql()
|
||||
rsa_cache.set(cache_key, rsa_value)
|
||||
finally:
|
||||
lock.release()
|
||||
return rsa_value
|
||||
|
||||
|
||||
def get_key_pair_by_sql():
|
||||
system_setting = QuerySet(SystemSetting).filter(type=SettingType.RSA.value).first()
|
||||
if system_setting is None:
|
||||
kv = generate()
|
||||
private_file_out = open("/opt/maxkb/conf/private.pem", "wb")
|
||||
private_file_out.write(kv.get('value'))
|
||||
private_file_out.close()
|
||||
receiver_file_out = open("/opt/maxkb/conf/receiver.pem", "wb")
|
||||
receiver_file_out.write(kv.get('key'))
|
||||
receiver_file_out.close()
|
||||
return {'key': open("/opt/maxkb/conf/receiver.pem").read(), 'value': open("/opt/maxkb/conf/private.pem").read()}
|
||||
system_setting = SystemSetting(type=SettingType.RSA.value,
|
||||
meta={'key': kv.get('key').decode(), 'value': kv.get('value').decode()})
|
||||
system_setting.save()
|
||||
return system_setting.meta
|
||||
|
||||
|
||||
# def get_key_pair():
|
||||
# if not os.path.exists("/opt/maxkb/conf/receiver.pem"):
|
||||
# kv = generate()
|
||||
# private_file_out = open("/opt/maxkb/conf/private.pem", "wb")
|
||||
# private_file_out.write(kv.get('value'))
|
||||
# private_file_out.close()
|
||||
# receiver_file_out = open("/opt/maxkb/conf/receiver.pem", "wb")
|
||||
# receiver_file_out.write(kv.get('key'))
|
||||
# receiver_file_out.close()
|
||||
# return {'key': open("/opt/maxkb/conf/receiver.pem").read(), 'value': open("/opt/maxkb/conf/private.pem").read()}
|
||||
|
||||
|
||||
def encrypt(msg, public_key: str | None = None):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 4.1.10 on 2024-03-19 16:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('setting', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SystemSetting',
|
||||
fields=[
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
|
||||
('type', models.IntegerField(choices=[('0', '邮箱')], default='0', max_length=5, primary_key=True, serialize=False, verbose_name='设置类型')),
|
||||
('meta', models.JSONField(default=dict, verbose_name='配置数据')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'system_setting',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -8,3 +8,4 @@
|
|||
"""
|
||||
from .team_management import *
|
||||
from .model_management import *
|
||||
from .system_management import *
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: system_management.py
|
||||
@date:2024/3/19 13:47
|
||||
@desc: 邮箱管理
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
from common.mixins.app_model_mixin import AppModelMixin
|
||||
|
||||
|
||||
class SettingType(models.IntegerChoices):
|
||||
"""系统设置类型"""
|
||||
EMAIL = 0, '邮箱'
|
||||
|
||||
RSA = 1, "私钥秘钥"
|
||||
|
||||
|
||||
class SystemSetting(AppModelMixin):
|
||||
"""
|
||||
系统设置
|
||||
"""
|
||||
type = models.IntegerField(primary_key=True, verbose_name='设置类型', max_length=5, choices=SettingType.choices,
|
||||
default=SettingType.EMAIL)
|
||||
|
||||
meta = models.JSONField(verbose_name="配置数据", default=dict)
|
||||
|
||||
class Meta:
|
||||
db_table = "system_setting"
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: system_setting.py
|
||||
@date:2024/3/19 16:29
|
||||
@desc:
|
||||
"""
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.field_message import ErrMessage
|
||||
from setting.models.system_management import SystemSetting, SettingType
|
||||
|
||||
|
||||
class SystemSettingSerializer(serializers.Serializer):
|
||||
class EmailSerializer(serializers.Serializer):
|
||||
@staticmethod
|
||||
def one():
|
||||
system_setting = QuerySet(SystemSetting).filter(type=SettingType.EMAIL.value).first()
|
||||
if system_setting is None:
|
||||
return {}
|
||||
return system_setting.meta
|
||||
|
||||
class Create(serializers.Serializer):
|
||||
email_host = serializers.CharField(required=True, error_messages=ErrMessage.char("SMTP 主机"))
|
||||
email_port = serializers.IntegerField(required=True, error_messages=ErrMessage.char("SMTP 端口"))
|
||||
email_host_user = serializers.CharField(required=True, error_messages=ErrMessage.char("发件人邮箱"))
|
||||
email_host_password = serializers.CharField(required=True, error_messages=ErrMessage.char("密码"))
|
||||
email_use_tls = serializers.BooleanField(required=True, error_messages=ErrMessage.char("是否开启TLS"))
|
||||
email_use_ssl = serializers.BooleanField(required=True, error_messages=ErrMessage.char("是否开启SSL"))
|
||||
from_email = serializers.EmailField(required=True, error_messages=ErrMessage.char("发送人邮箱"))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
try:
|
||||
EmailBackend(self.data.get("email_host"),
|
||||
self.data.get("email_port"),
|
||||
self.data.get("email_host_user"),
|
||||
self.data.get("email_host_password"),
|
||||
self.data.get("email_use_tls"),
|
||||
False,
|
||||
self.data.get("email_use_ssl")
|
||||
).open()
|
||||
except Exception as e:
|
||||
raise AppApiException(1004, "邮箱校验失败")
|
||||
|
||||
def update_or_save(self):
|
||||
self.is_valid(raise_exception=True)
|
||||
system_setting = QuerySet(SystemSetting).filter(type=SettingType.EMAIL.value).first()
|
||||
if system_setting is None:
|
||||
system_setting = SystemSetting(type=SettingType.EMAIL.value)
|
||||
system_setting.meta = self.to_email_meta()
|
||||
system_setting.save()
|
||||
return system_setting.meta
|
||||
|
||||
def to_email_meta(self):
|
||||
return {'email_host': self.data.get('email_host'),
|
||||
'email_port': self.data.get('email_port'),
|
||||
'email_host_user': self.data.get('email_host_user'),
|
||||
'email_host_password': self.data.get('email_host_password'),
|
||||
'email_use_tls': self.data.get('email_use_tls'),
|
||||
'email_use_ssl': self.data.get('email_use_ssl'),
|
||||
'from_email': self.data.get('from_email')
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: system_setting.py
|
||||
@date:2024/3/19 16:05
|
||||
@desc:
|
||||
"""
|
||||
from drf_yasg import openapi
|
||||
|
||||
from common.mixins.api_mixin import ApiMixin
|
||||
|
||||
|
||||
class SystemSettingEmailApi(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
return openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
title="邮箱相关参数",
|
||||
description="邮箱相关参数",
|
||||
required=['email_host', 'email_port', 'email_host_user', 'email_host_password',
|
||||
'email_use_tls', 'email_use_ssl', 'from_email'],
|
||||
properties={
|
||||
'email_host': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="SMTP 主机",
|
||||
description="SMTP 主机"),
|
||||
'email_port': openapi.Schema(type=openapi.TYPE_NUMBER,
|
||||
title="SMTP 端口",
|
||||
description="SMTP 端口"),
|
||||
'email_host_user': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="发件人邮箱",
|
||||
description="发件人邮箱"),
|
||||
'email_host_password': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="密码",
|
||||
description="密码"),
|
||||
'email_use_tls': openapi.Schema(type=openapi.TYPE_BOOLEAN,
|
||||
title="是否开启TLS",
|
||||
description="是否开启TLS"),
|
||||
'email_use_ssl': openapi.Schema(type=openapi.TYPE_BOOLEAN,
|
||||
title="是否开启SSL",
|
||||
description="是否开启SSL"),
|
||||
'from_email': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="发送人邮箱",
|
||||
description="发送人邮箱")
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
title="邮箱相关参数",
|
||||
description="邮箱相关参数",
|
||||
required=['email_host', 'email_port', 'email_host_user', 'email_host_password',
|
||||
'email_use_tls', 'email_use_ssl', 'from_email'],
|
||||
properties={
|
||||
'email_host': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="SMTP 主机",
|
||||
description="SMTP 主机"),
|
||||
'email_port': openapi.Schema(type=openapi.TYPE_NUMBER,
|
||||
title="SMTP 端口",
|
||||
description="SMTP 端口"),
|
||||
'email_host_user': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="发件人邮箱",
|
||||
description="发件人邮箱"),
|
||||
'email_host_password': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="密码",
|
||||
description="密码"),
|
||||
'email_use_tls': openapi.Schema(type=openapi.TYPE_BOOLEAN,
|
||||
title="是否开启TLS",
|
||||
description="是否开启TLS"),
|
||||
'email_use_ssl': openapi.Schema(type=openapi.TYPE_BOOLEAN,
|
||||
title="是否开启SSL",
|
||||
description="是否开启SSL"),
|
||||
'from_email': openapi.Schema(type=openapi.TYPE_STRING,
|
||||
title="发送人邮箱",
|
||||
description="发送人邮箱")
|
||||
}
|
||||
)
|
||||
|
|
@ -15,6 +15,7 @@ urlpatterns = [
|
|||
path('provider/model_form', views.Provide.ModelForm.as_view(),
|
||||
name="provider/model_form"),
|
||||
path('model', views.Model.as_view(), name='model'),
|
||||
path('model/<str:model_id>', views.Model.Operate.as_view(), name='model/operate')
|
||||
path('model/<str:model_id>', views.Model.Operate.as_view(), name='model/operate'),
|
||||
path('email_setting', views.SystemSetting.Email.as_view(), name='email_setting')
|
||||
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@
|
|||
"""
|
||||
from .Team import *
|
||||
from .model import *
|
||||
from .system_setting import *
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: system_setting.py
|
||||
@date:2024/3/19 16:01
|
||||
@desc:
|
||||
"""
|
||||
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import PermissionConstants, RoleConstants
|
||||
from common.response import result
|
||||
from setting.serializers.system_setting import SystemSettingSerializer
|
||||
from setting.swagger_api.system_setting import SystemSettingEmailApi
|
||||
|
||||
|
||||
class SystemSetting(APIView):
|
||||
class Email(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['PUT'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="创建或者修改邮箱设置",
|
||||
operation_id="创建或者修改邮箱设置",
|
||||
request_body=SystemSettingEmailApi.get_request_body_api(), tags=["邮箱设置"],
|
||||
responses=result.get_api_response(SystemSettingEmailApi.get_response_body_api()))
|
||||
@has_permissions(RoleConstants.ADMIN)
|
||||
def put(self, request: Request):
|
||||
return result.success(
|
||||
SystemSettingSerializer.EmailSerializer.Create(
|
||||
data=request.data).update_or_save())
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="测试邮箱设置",
|
||||
operation_id="测试邮箱设置",
|
||||
request_body=SystemSettingEmailApi.get_request_body_api(),
|
||||
responses=result.get_default_response(),
|
||||
tags=["邮箱设置"])
|
||||
@has_permissions(RoleConstants.ADMIN)
|
||||
def post(self, request: Request):
|
||||
return result.success(
|
||||
SystemSettingSerializer.EmailSerializer.Create(
|
||||
data=request.data).is_valid())
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="获取邮箱设置",
|
||||
operation_id="获取邮箱设置",
|
||||
responses=result.get_api_response(SystemSettingEmailApi.get_response_body_api()),
|
||||
tags=["邮箱设置"])
|
||||
@has_permissions(RoleConstants.ADMIN)
|
||||
def get(self, request: Request):
|
||||
return result.success(
|
||||
SystemSettingSerializer.EmailSerializer.one())
|
||||
|
|
@ -14,6 +14,7 @@ import uuid
|
|||
|
||||
from django.core import validators, signing, cache
|
||||
from django.core.mail import send_mail
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, QuerySet
|
||||
from drf_yasg import openapi
|
||||
|
|
@ -28,9 +29,8 @@ from common.mixins.api_mixin import ApiMixin
|
|||
from common.response.result import get_api_response
|
||||
from common.util.field_message import ErrMessage
|
||||
from common.util.lock import lock
|
||||
from setting.models import Team
|
||||
from setting.models import Team, SystemSetting, SettingType
|
||||
from smartdoc.conf import PROJECT_DIR
|
||||
from smartdoc.settings import EMAIL_ADDRESS
|
||||
from users.models.user import User, password_encrypt, get_user_dynamics_permission
|
||||
|
||||
user_cache = cache.caches['user_cache']
|
||||
|
|
@ -345,13 +345,25 @@ class SendEmailSerializer(ApiMixin, serializers.Serializer):
|
|||
code_cache_key_lock = code_cache_key + "_lock"
|
||||
# 设置缓存
|
||||
user_cache.set(code_cache_key_lock, code, timeout=datetime.timedelta(minutes=1))
|
||||
system_setting = QuerySet(SystemSetting).filter(type=SettingType.EMAIL.value).first()
|
||||
if system_setting is None:
|
||||
user_cache.delete(code_cache_key_lock)
|
||||
raise AppApiException(1004, "邮箱未设置,请联系管理员设置")
|
||||
try:
|
||||
connection = EmailBackend(system_setting.meta.get("email_host"),
|
||||
system_setting.meta.get('email_port'),
|
||||
system_setting.meta.get('email_host_user'),
|
||||
system_setting.meta.get('email_host_password'),
|
||||
system_setting.meta.get('email_use_tls'),
|
||||
False,
|
||||
system_setting.meta.get('email_use_ssl')
|
||||
)
|
||||
# 发送邮件
|
||||
send_mail(f'【MaxKB 智能知识库-{"用户注册" if state == "register" else "修改密码"}】',
|
||||
'',
|
||||
html_message=f'{content.replace("${code}", code)}',
|
||||
from_email=EMAIL_ADDRESS,
|
||||
recipient_list=[email], fail_silently=False)
|
||||
from_email=system_setting.meta.get('from_email'),
|
||||
recipient_list=[email], fail_silently=False, connection=connection)
|
||||
except Exception as e:
|
||||
user_cache.delete(code_cache_key_lock)
|
||||
raise AppApiException(500, f"{str(e)}邮件发送失败")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Result } from '@/request/Result'
|
||||
import { get, post, postStream, del, put } from '@/request/index'
|
||||
import { get, post, del, put } from '@/request/index'
|
||||
|
||||
import { type Ref } from 'vue'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Result } from '@/request/Result'
|
||||
import { get, post, postStream, del, put } from '@/request/index'
|
||||
import { get, post, del, put } from '@/request/index'
|
||||
import type { pageRequest } from '@/api/type/common'
|
||||
import { type Ref } from 'vue'
|
||||
|
||||
|
|
|
|||
|
|
@ -89,11 +89,11 @@ interface ResetPasswordRequest {
|
|||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email: string
|
||||
email?: string
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
code: string
|
||||
code?: string
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { Result } from '@/request/Result'
|
||||
import { get, post, del, put } from '@/request/index'
|
||||
import type { pageRequest } from '@/api/type/common'
|
||||
import { type Ref } from 'vue'
|
||||
|
||||
const prefix = '/user_manage'
|
||||
/**
|
||||
* 用户分页列表
|
||||
* @param 参数
|
||||
* page {
|
||||
"current_page": "string",
|
||||
"page_size": "string",
|
||||
}
|
||||
* @query 参数
|
||||
email_or_username: string
|
||||
*/
|
||||
const getUserManage: (
|
||||
page: pageRequest,
|
||||
email_or_username: string,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<any>> = (page, email_or_username, loading) => {
|
||||
return get(
|
||||
`${prefix}/${page.current_page}/${page.page_size}`,
|
||||
email_or_username ? { email_or_username } : undefined,
|
||||
loading
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param 参数 user_id,
|
||||
*/
|
||||
const delUserManage: (user_id: string, loading?: Ref<boolean>) => Promise<Result<boolean>> = (
|
||||
user_id,
|
||||
loading
|
||||
) => {
|
||||
return del(`${prefix}/${user_id}`, undefined, {}, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
const postUserManage: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
data,
|
||||
loading
|
||||
) => {
|
||||
return post(`${prefix}`, data, undefined, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户
|
||||
*/
|
||||
const putUserManage: (
|
||||
user_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<any>> = (user_id, data, loading) => {
|
||||
return put(`${prefix}/${user_id}`, data, undefined, loading)
|
||||
}
|
||||
/**
|
||||
* 修改用户密码
|
||||
*/
|
||||
const putUserManagePassword: (
|
||||
user_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<any>> = (user_id, data, loading) => {
|
||||
return put(`${prefix}/${user_id}/re_password`, data, undefined, loading)
|
||||
}
|
||||
|
||||
export default {
|
||||
getUserManage,
|
||||
delUserManage,
|
||||
postUserManage,
|
||||
putUserManage,
|
||||
putUserManagePassword
|
||||
}
|
||||
|
|
@ -3,20 +3,34 @@ const settingRouter = {
|
|||
path: '/setting',
|
||||
name: 'setting',
|
||||
meta: { icon: 'Setting', title: '系统设置', permission: 'SETTING:READ' },
|
||||
redirect: '/setting',
|
||||
redirect: '/user',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '/setting',
|
||||
name: 'setting',
|
||||
path: '/user',
|
||||
name: 'user',
|
||||
meta: {
|
||||
icon: 'User',
|
||||
iconActive: 'UserFilled',
|
||||
title: '用户管理',
|
||||
activeMenu: '/setting',
|
||||
parentPath: '/setting',
|
||||
parentName: 'setting'
|
||||
},
|
||||
component: () => import('@/views/user-manage/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/team',
|
||||
name: 'team',
|
||||
meta: {
|
||||
icon: 'app-team',
|
||||
iconActive: 'app-team-active',
|
||||
title: '团队管理',
|
||||
activeMenu: '/setting',
|
||||
parentPath: '/setting',
|
||||
parentName: 'setting'
|
||||
},
|
||||
component: () => import('@/views/setting/index.vue')
|
||||
component: () => import('@/views/team/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/template',
|
||||
|
|
|
|||
|
|
@ -144,3 +144,4 @@ onMounted(() => {
|
|||
})
|
||||
</script>
|
||||
<style lang="scss" scope></style>
|
||||
../utils
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<el-dialog :title="title" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="userFormRef"
|
||||
:model="userForm"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item prop="username" label="用户名">
|
||||
<el-input
|
||||
v-model="userForm.username"
|
||||
placeholder="请输入用户名"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名">
|
||||
<el-input
|
||||
v-model="userForm.nick_name"
|
||||
placeholder="请输入姓名"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input type="email" v-model="userForm.email" placeholder="请输入邮箱"> </el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input type="email" v-model="userForm.phone" placeholder="请输入手机号"> </el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录密码" prop="password" v-if="!isEdit">
|
||||
<el-input
|
||||
type="password"
|
||||
v-model="userForm.password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="submit(userFormRef)" :loading="loading"> 保存 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import userApi from '@/api/user-manage'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
|
||||
const props = defineProps({
|
||||
title: String
|
||||
})
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const userFormRef = ref()
|
||||
const userForm = ref<any>({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
nick_name: ''
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
username: [{ required: true, message: '请输入用户名' }],
|
||||
email: [{ required: true, message: '请输入邮箱' }],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '长度在 6 到 20 个字符',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
userForm.value = {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
nick_name: ''
|
||||
}
|
||||
userFormRef.value?.clearValidate()
|
||||
isEdit.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
if (data) {
|
||||
userForm.value = cloneDeep(data)
|
||||
isEdit.value = true
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
if (isEdit.value) {
|
||||
userApi.putUserManage(userForm.value.id, userForm.value, loading).then((res) => {
|
||||
emit('refresh')
|
||||
MsgSuccess('编辑成功')
|
||||
dialogVisible.value = false
|
||||
})
|
||||
} else {
|
||||
userApi.postUserManage(userForm.value, loading).then((res) => {
|
||||
emit('refresh')
|
||||
MsgSuccess('创建成功')
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('error submit!', fields)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scope></style>
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<el-dialog title="修改用户密码" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="userFormRef"
|
||||
:model="userForm"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input
|
||||
type="password"
|
||||
v-model="userForm.password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="re_password">
|
||||
<el-input
|
||||
type="password"
|
||||
v-model="userForm.re_password"
|
||||
placeholder="请输入确认密码"
|
||||
show-password
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="submit(userFormRef)" :loading="loading"> 保存 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { ResetPasswordRequest } from '@/api/type/user'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import userApi from '@/api/user-manage'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const userFormRef = ref()
|
||||
const userForm = ref<any>({
|
||||
password: '',
|
||||
re_password: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules<ResetPasswordRequest>>({
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入新密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '长度在 6 到 20 个字符',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
re_password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入确认密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '长度在 6 到 20 个字符',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (userFormRef.value.password != userFormRef.value.re_password) {
|
||||
callback(new Error('密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
const userId = ref('')
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
userForm.value = {
|
||||
password: '',
|
||||
re_password: ''
|
||||
}
|
||||
userFormRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
userId.value = data.id
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
userApi.putUserManagePassword(userId.value, userForm.value, loading).then((res) => {
|
||||
emit('refresh')
|
||||
MsgSuccess('修改用户密码成功')
|
||||
dialogVisible.value = false
|
||||
})
|
||||
} else {
|
||||
console.log('error submit!', fields)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scope></style>
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<LayoutContainer header="用户管理">
|
||||
<div class="p-24">
|
||||
<div class="flex-between">
|
||||
<el-button type="primary" @click="createUser">创建用户</el-button>
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
@change="searchHandle"
|
||||
placeholder="搜索"
|
||||
prefix-icon="Search"
|
||||
class="w-240"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<app-table
|
||||
class="mt-16"
|
||||
:data="tableData"
|
||||
:pagination-config="paginationConfig"
|
||||
@sizeChange="handleSizeChange"
|
||||
@changePage="getList"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="nick_name" label="姓名" />
|
||||
<el-table-column prop="email" label="邮箱" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column label="状态" width="60">
|
||||
<template #default="{ row }">
|
||||
<div @click.stop>
|
||||
<el-switch size="small" v-model="row.is_active" @change="changeState($event, row)" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ datetimeFormat(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="110" align="left">
|
||||
<template #default="{ row }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" content="编辑" placement="top">
|
||||
<el-button type="primary" text @click.stop="editUser(row)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" content="修改用户密码" placement="top">
|
||||
<el-button type="primary" text @click.stop="editPwdUser(row)">
|
||||
<el-icon><Lock /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" content="删除" placement="top">
|
||||
<el-button type="primary" text @click.stop="deleteUserManage(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</app-table>
|
||||
</div>
|
||||
<UserDialog :title="title" ref="UserDialogRef" @refresh="refresh" />
|
||||
<UserPwdDialog ref="UserPwdDialogRef" @refresh="refresh" />
|
||||
</LayoutContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, watch, computed } from 'vue'
|
||||
import UserDialog from './component/UserDialog.vue'
|
||||
import UserPwdDialog from './component/UserPwdDialog.vue'
|
||||
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
|
||||
import userApi from '@/api/user-manage'
|
||||
import { datetimeFormat } from '@/utils/time'
|
||||
|
||||
const UserDialogRef = ref()
|
||||
const UserPwdDialogRef = ref()
|
||||
const title = ref('')
|
||||
const loading = ref(false)
|
||||
const paginationConfig = reactive({
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const searchValue = ref('')
|
||||
|
||||
function searchHandle() {
|
||||
paginationConfig.total = 0
|
||||
paginationConfig.current_page = 1
|
||||
tableData.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
function editPwdUser(row: any) {
|
||||
UserPwdDialogRef.value.open(row)
|
||||
}
|
||||
function editUser(row: any) {
|
||||
title.value = '编辑用户'
|
||||
UserDialogRef.value.open(row)
|
||||
}
|
||||
|
||||
function createUser() {
|
||||
title.value = '创建用户'
|
||||
UserDialogRef.value.open()
|
||||
}
|
||||
|
||||
function deleteUserManage(row: any) {
|
||||
MsgConfirm(
|
||||
`是否删除用户:${row.username} ?`,
|
||||
`删除用户,该用户创建的资源(应用、知识库、模型)都会删除,请谨慎操作。`,
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
confirmButtonClass: 'danger'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
loading.value = true
|
||||
userApi.delUserManage(row.id, loading).then(() => {
|
||||
MsgSuccess('删除成功')
|
||||
getList()
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
paginationConfig.current_page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
function getList() {
|
||||
return userApi.getUserManage(paginationConfig, searchValue.value, loading).then((res) => {
|
||||
tableData.value = res.data.records
|
||||
paginationConfig.total = res.data.total
|
||||
})
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
getList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.log-table tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue