init
This commit is contained in:
70
.gitignore
vendored
Normal file
70
.gitignore
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
**/.idea/
|
||||
.idea/**
|
||||
**/*.pyc
|
||||
__pycache__/
|
||||
build/
|
||||
*.egg-info/
|
||||
.python-version
|
||||
.pytest_cache/
|
||||
dist/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
.DS_Store
|
||||
docs/_build/
|
||||
.env
|
||||
*.env
|
||||
**/local_settings.py
|
||||
static/
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
dist.zip
|
||||
dist.tar
|
||||
dist.war
|
||||
.nitro
|
||||
.output
|
||||
*-dist.zip
|
||||
*-dist.tar
|
||||
*-dist.war
|
||||
coverage
|
||||
*.local
|
||||
**/.vitepress/cache
|
||||
.cache
|
||||
.turbo
|
||||
.temp
|
||||
dev-dist
|
||||
.stylelintcache
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
.VSCodeCounter
|
||||
**/backend-mock/data
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.eslintcache
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
vite.config.mts.*
|
||||
vite.config.mjs.*
|
||||
vite.config.js.*
|
||||
vite.config.ts.*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
# .vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.history
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 XIE7654
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
backend/backend/__init__.py
Normal file
4
backend/backend/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
16
backend/backend/asgi.py
Normal file
16
backend/backend/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for backend project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
19
backend/backend/celery.py
Normal file
19
backend/backend/celery.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
from celery import Celery
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') # 请将 myproject 替换为你的项目名称
|
||||
app = Celery('backend')
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
app.autodiscover_tasks()
|
||||
|
||||
# Windows 兼容性设置
|
||||
if os.name == 'nt':
|
||||
app.conf.update(
|
||||
task_serializer='json',
|
||||
accept_content=['json'], # Ignore other content
|
||||
result_serializer='json',
|
||||
timezone='Asia/Shanghai',
|
||||
enable_utc=True,
|
||||
)
|
||||
# 强制使用 single-threaded 执行模式
|
||||
app.conf.worker_pool = 'solo'
|
||||
172
backend/backend/settings.py
Normal file
172
backend/backend/settings.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Django settings for backend project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.1.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-m4@pv814c_m^pgpyhz^i96a@mcqh_@m9ccu(17*895t!79e!nb'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'*',
|
||||
]
|
||||
INTERNAL_IPS = [
|
||||
'*',
|
||||
]
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True # 允许跨域名访问
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS =True
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"simpleui",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"rest_framework",
|
||||
'django_filters',
|
||||
'corsheaders',
|
||||
'rest_framework.authtoken',
|
||||
"system",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'system.User'
|
||||
ROOT_URLCONF = 'backend.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'backend.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'django-vue',
|
||||
'USER': 'root',
|
||||
'PASSWORD': '',
|
||||
'HOST': 'localhost',
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
# 设置为中国标准时间(CST,东八区)
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
# 启用国际化(多语言)
|
||||
USE_I18N = True
|
||||
|
||||
# 启用本地化(格式化日期、数字等)
|
||||
USE_L10N = True
|
||||
|
||||
# 是否使用时区支持(建议开启)
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #自己在根目录下创建media文件夹
|
||||
# Default primary key field type
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
|
||||
# celery 配置
|
||||
CELERY_BROKER_URL = 'redis://localhost:6379/6'
|
||||
CELERY_RESULT_BACKEND = 'redis://localhost:6379/6'
|
||||
# 时区设置
|
||||
CELERY_TIMEZONE = 'Asia/Shanghai'
|
||||
# 任务序列化方式
|
||||
CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_RESULT_SERIALIZER = 'json'
|
||||
CELERY_ACCEPT_CONTENT = ['json']
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
'every-15-minutes': {
|
||||
'task': 'system.tasks.add', # 任务路径
|
||||
'schedule': 900.0, # 每15分钟执行一次
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if os.path.exists(os.path.join(BASE_DIR, 'backend/local_settings.py')):
|
||||
from backend.local_settings import *
|
||||
23
backend/backend/urls.py
Normal file
23
backend/backend/urls.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
URL configuration for backend project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/system/', include('system.urls')),
|
||||
]
|
||||
16
backend/backend/wsgi.py
Normal file
16
backend/backend/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for backend project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
backend/manage.py
Executable file
22
backend/manage.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
13
backend/requirements.txt
Normal file
13
backend/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Django==5.2.1
|
||||
djangorestframework==3.16.0
|
||||
django-filter==25.1
|
||||
django-cors-headers==4.7.0
|
||||
django-ckeditor==6.7.2
|
||||
openpyxl==3.1.5
|
||||
mysqlclient==2.2.7
|
||||
django-simpleui==2025.5.17
|
||||
requests==2.32.3
|
||||
celery==5.5.3
|
||||
redis==6.2.0
|
||||
eventlet==0.40.0
|
||||
goofish_api==0.0.6
|
||||
0
backend/system/__init__.py
Normal file
0
backend/system/__init__.py
Normal file
3
backend/system/admin.py
Normal file
3
backend/system/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/system/apps.py
Normal file
6
backend/system/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SystemConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'system'
|
||||
990
backend/system/migrations/0001_initial.py
Normal file
990
backend/system/migrations/0001_initial.py
Normal file
@@ -0,0 +1,990 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-29 13:08
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import utils.utils
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DictType",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
default="", max_length=100, verbose_name="字典名称"
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
db_index=True,
|
||||
default="",
|
||||
max_length=100,
|
||||
verbose_name="字典类型",
|
||||
),
|
||||
),
|
||||
("status", models.BooleanField(default=True)),
|
||||
(
|
||||
"deleted_time",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="删除时间"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "字典类型",
|
||||
"verbose_name_plural": "字典类型",
|
||||
"db_table": "system_dict_type",
|
||||
"ordering": ["-id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="MenuMeta",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("title", models.CharField(max_length=200, verbose_name="标题")),
|
||||
(
|
||||
"icon",
|
||||
models.CharField(blank=True, max_length=100, verbose_name="图标"),
|
||||
),
|
||||
("order", models.IntegerField(default=0, verbose_name="排序")),
|
||||
(
|
||||
"affix_tab",
|
||||
models.BooleanField(default=False, verbose_name="固定标签页"),
|
||||
),
|
||||
(
|
||||
"badge",
|
||||
models.CharField(
|
||||
blank=True, max_length=50, verbose_name="徽章文本"
|
||||
),
|
||||
),
|
||||
(
|
||||
"badge_type",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, verbose_name="徽章类型"
|
||||
),
|
||||
),
|
||||
(
|
||||
"badge_variants",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, verbose_name="徽章样式"
|
||||
),
|
||||
),
|
||||
("iframe_src", models.URLField(blank=True, verbose_name="内嵌页面URL")),
|
||||
("link", models.URLField(blank=True, verbose_name="外部链接")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "菜单元数据",
|
||||
"verbose_name_plural": "菜单元数据",
|
||||
"db_table": "system_menu_meta",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Role",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="角色名称")),
|
||||
(
|
||||
"status",
|
||||
models.IntegerField(
|
||||
choices=[(1, "启用"), (0, "禁用")],
|
||||
default=1,
|
||||
verbose_name="角色状态",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sort",
|
||||
models.IntegerField(
|
||||
default=0, help_text="数值越小越靠前", verbose_name="显示排序"
|
||||
),
|
||||
),
|
||||
("remark", models.TextField(blank=True, verbose_name="备注")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "角色管理",
|
||||
"verbose_name_plural": "角色管理",
|
||||
"ordering": ["-create_time"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Dept",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="部门名称")),
|
||||
(
|
||||
"status",
|
||||
models.SmallIntegerField(
|
||||
choices=[(0, "禁用"), (1, "启用")],
|
||||
default=0,
|
||||
verbose_name="部门状态",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
||||
),
|
||||
(
|
||||
"sort",
|
||||
models.IntegerField(
|
||||
default=0, help_text="数值越小越靠前", verbose_name="显示排序"
|
||||
),
|
||||
),
|
||||
(
|
||||
"leader",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, null=True, verbose_name="负责人"
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, null=True, verbose_name="联系电话"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, null=True, verbose_name="邮箱"
|
||||
),
|
||||
),
|
||||
("remark", models.TextField(blank=True, verbose_name="备注")),
|
||||
(
|
||||
"pid",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="children",
|
||||
to="system.dept",
|
||||
verbose_name="父部门 ID",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "部门管理",
|
||||
"verbose_name_plural": "部门管理",
|
||||
"ordering": ["-create_time"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
(
|
||||
"mobile",
|
||||
models.CharField(
|
||||
db_comment="手机号",
|
||||
max_length=11,
|
||||
null=True,
|
||||
validators=[utils.utils.validate_mobile],
|
||||
),
|
||||
),
|
||||
(
|
||||
"nickname",
|
||||
models.CharField(
|
||||
blank=True, db_comment="昵称", max_length=50, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"gender",
|
||||
models.SmallIntegerField(
|
||||
blank=True, db_comment="性别", default=0, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"language",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="语言",
|
||||
max_length=20,
|
||||
null=True,
|
||||
verbose_name="语言",
|
||||
),
|
||||
),
|
||||
(
|
||||
"city",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="城市",
|
||||
max_length=20,
|
||||
null=True,
|
||||
verbose_name="城市",
|
||||
),
|
||||
),
|
||||
(
|
||||
"province",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="省份",
|
||||
max_length=50,
|
||||
null=True,
|
||||
verbose_name="省份",
|
||||
),
|
||||
),
|
||||
(
|
||||
"country",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="国家",
|
||||
max_length=50,
|
||||
null=True,
|
||||
verbose_name="国家",
|
||||
),
|
||||
),
|
||||
(
|
||||
"avatarUrl",
|
||||
models.URLField(
|
||||
blank=True, db_comment="头像", null=True, verbose_name="头像"
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.BooleanField(
|
||||
db_comment="帐号状态",
|
||||
default=False,
|
||||
verbose_name="<帐号状态>(1正常 0停用)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"login_date",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
db_comment="最后登录时间",
|
||||
null=True,
|
||||
verbose_name="<最后登录时间>",
|
||||
),
|
||||
),
|
||||
(
|
||||
"login_ip",
|
||||
models.GenericIPAddressField(
|
||||
blank=True, db_comment="最后登录IP", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
(
|
||||
"dept",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
related_name="users",
|
||||
to="system.dept",
|
||||
verbose_name="部门",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "用户数据",
|
||||
"verbose_name_plural": "用户数据",
|
||||
"db_table": "system_users",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="DictData",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("sort", models.IntegerField(default=0, verbose_name="字典排序")),
|
||||
(
|
||||
"label",
|
||||
models.CharField(
|
||||
default="", max_length=100, verbose_name="字典标签"
|
||||
),
|
||||
),
|
||||
(
|
||||
"value",
|
||||
models.CharField(
|
||||
default="", max_length=100, verbose_name="字典键值"
|
||||
),
|
||||
),
|
||||
("status", models.BooleanField(default=True)),
|
||||
(
|
||||
"color_type",
|
||||
models.CharField(
|
||||
blank=True, default="", max_length=100, verbose_name="颜色类型"
|
||||
),
|
||||
),
|
||||
(
|
||||
"css_class",
|
||||
models.CharField(
|
||||
blank=True, default="", max_length=100, verbose_name="css 样式"
|
||||
),
|
||||
),
|
||||
(
|
||||
"dict_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="dict_data",
|
||||
to="system.dicttype",
|
||||
verbose_name="字典类型",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "字典数据",
|
||||
"verbose_name_plural": "字典数据",
|
||||
"db_table": "system_dict_data",
|
||||
"ordering": ["sort", "id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Menu",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="菜单名称")),
|
||||
(
|
||||
"status",
|
||||
models.IntegerField(
|
||||
choices=[(1, "启用"), (0, "禁用")],
|
||||
default=1,
|
||||
verbose_name="状态",
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("catalog", "目录"),
|
||||
("menu", "菜单"),
|
||||
("button", "按钮"),
|
||||
("embedded", "内嵌页面"),
|
||||
("link", "外部链接"),
|
||||
],
|
||||
max_length=20,
|
||||
verbose_name="菜单类型",
|
||||
),
|
||||
),
|
||||
(
|
||||
"path",
|
||||
models.CharField(
|
||||
blank=True, max_length=200, verbose_name="路由路径"
|
||||
),
|
||||
),
|
||||
(
|
||||
"component",
|
||||
models.CharField(
|
||||
blank=True, max_length=200, verbose_name="组件路径"
|
||||
),
|
||||
),
|
||||
(
|
||||
"auth_code",
|
||||
models.CharField(
|
||||
blank=True, max_length=100, verbose_name="权限编码"
|
||||
),
|
||||
),
|
||||
(
|
||||
"pid",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="children",
|
||||
to="system.menu",
|
||||
verbose_name="父菜单",
|
||||
),
|
||||
),
|
||||
(
|
||||
"meta",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="system.menumeta",
|
||||
verbose_name="元数据",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "菜单",
|
||||
"verbose_name_plural": "菜单管理",
|
||||
"ordering": ["meta__order", "id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RolePermission",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="权限关联时间"
|
||||
),
|
||||
),
|
||||
(
|
||||
"menu",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="system.menu",
|
||||
verbose_name="菜单/权限",
|
||||
),
|
||||
),
|
||||
(
|
||||
"role",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="system.role",
|
||||
verbose_name="角色",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "角色权限关联",
|
||||
"verbose_name_plural": "角色权限关联",
|
||||
"db_table": "system_role_permission",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="permissions",
|
||||
field=models.ManyToManyField(
|
||||
through="system.RolePermission",
|
||||
to="system.menu",
|
||||
verbose_name="关联权限",
|
||||
),
|
||||
),
|
||||
]
|
||||
0
backend/system/migrations/__init__.py
Normal file
0
backend/system/migrations/__init__.py
Normal file
248
backend/system/models.py
Normal file
248
backend/system/models.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from backend import settings
|
||||
from utils.models import CoreModel
|
||||
from utils.utils import validate_mobile
|
||||
|
||||
|
||||
# 定义状态枚举(可根据实际业务扩展)
|
||||
class DepartmentStatus(models.IntegerChoices):
|
||||
DISABLED = 0, "禁用" # 对应数据中的 status: 0
|
||||
ENABLED = 1, "启用" # 对应数据中的 status: 1
|
||||
|
||||
class Dept(CoreModel):
|
||||
pid = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
verbose_name="父部门 ID"
|
||||
)
|
||||
name = models.CharField(max_length=100, verbose_name="部门名称")
|
||||
status = models.SmallIntegerField(
|
||||
choices=DepartmentStatus.choices,
|
||||
default=DepartmentStatus.DISABLED,
|
||||
verbose_name="部门状态"
|
||||
)
|
||||
create_time = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name="创建时间",
|
||||
# 若数据中时间需自动解析,可在保存时处理:
|
||||
# default=timezone.now # 或通过数据导入时赋值
|
||||
)
|
||||
sort = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name="显示排序",
|
||||
help_text="数值越小越靠前"
|
||||
)
|
||||
leader = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=20,
|
||||
verbose_name="负责人"
|
||||
)
|
||||
phone = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="联系电话"
|
||||
)
|
||||
email = models.EmailField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="邮箱"
|
||||
)
|
||||
remark = models.TextField(blank=True, verbose_name="备注")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "部门管理"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ["-create_time"] # 按创建时间倒序排列
|
||||
|
||||
# 菜单类型枚举
|
||||
class MenuType(models.TextChoices):
|
||||
CATALOG = 'catalog', '目录'
|
||||
MENU = 'menu', '菜单'
|
||||
BUTTON = 'button', '按钮'
|
||||
EMBEDDED = 'embedded', '内嵌页面'
|
||||
LINK = 'link', '外部链接'
|
||||
|
||||
# 菜单状态枚举
|
||||
class MenuStatus(models.IntegerChoices):
|
||||
ENABLED = 1, '启用'
|
||||
DISABLED = 0, '禁用'
|
||||
|
||||
# 菜单元数据模型(单独存储元数据,避免 JSONField)
|
||||
class MenuMeta(CoreModel):
|
||||
title = models.CharField(max_length=200, verbose_name='标题')
|
||||
icon = models.CharField(max_length=100, blank=True, verbose_name='图标')
|
||||
order = models.IntegerField(default=0, verbose_name='排序')
|
||||
affix_tab = models.BooleanField(default=False, verbose_name='固定标签页')
|
||||
badge = models.CharField(max_length=50, blank=True, verbose_name='徽章文本')
|
||||
badge_type = models.CharField(max_length=20, blank=True, verbose_name='徽章类型')
|
||||
badge_variants = models.CharField(max_length=20, blank=True, verbose_name='徽章样式')
|
||||
iframe_src = models.URLField(blank=True, verbose_name='内嵌页面URL')
|
||||
link = models.URLField(blank=True, verbose_name='外部链接')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
db_table = 'system_menu_meta'
|
||||
verbose_name = '菜单元数据'
|
||||
verbose_name_plural = '菜单元数据'
|
||||
|
||||
# 主菜单模型
|
||||
class Menu(CoreModel):
|
||||
pid = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='children',
|
||||
verbose_name='父菜单'
|
||||
)
|
||||
name = models.CharField(max_length=100, verbose_name='菜单名称')
|
||||
status = models.IntegerField(choices=MenuStatus.choices, default=MenuStatus.ENABLED, verbose_name='状态')
|
||||
type = models.CharField(choices=MenuType.choices, max_length=20, verbose_name='菜单类型')
|
||||
path = models.CharField(max_length=200, blank=True, verbose_name='路由路径')
|
||||
component = models.CharField(max_length=200, blank=True, verbose_name='组件路径')
|
||||
auth_code = models.CharField(max_length=100, blank=True, verbose_name='权限编码')
|
||||
meta = models.OneToOneField(MenuMeta, on_delete=models.CASCADE, verbose_name='元数据')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = '菜单'
|
||||
verbose_name_plural = '菜单管理'
|
||||
ordering = ['meta__order', 'id']
|
||||
|
||||
# 角色状态枚举
|
||||
class RoleStatus(models.IntegerChoices):
|
||||
ENABLED = 1, '启用'
|
||||
DISABLED = 0, '禁用'
|
||||
|
||||
class Role(CoreModel):
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name='角色名称'
|
||||
)
|
||||
status = models.IntegerField(
|
||||
choices=RoleStatus.choices,
|
||||
default=RoleStatus.ENABLED,
|
||||
verbose_name='角色状态'
|
||||
)
|
||||
sort = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name="显示排序",
|
||||
help_text="数值越小越靠前"
|
||||
)
|
||||
remark = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='备注'
|
||||
)
|
||||
# 与菜单权限的多对多关联(假设菜单模型为 Menu,权限字段为 auth_code)
|
||||
permissions = models.ManyToManyField(
|
||||
'Menu', # 引用之前设计的 Menu 模型
|
||||
through='RolePermission',
|
||||
verbose_name='关联权限'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '角色管理'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ["-create_time"] # 按创建时间倒序排列
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# 中间表:角色与权限的关联(可扩展字段如权限生效时间)
|
||||
class RolePermission(CoreModel):
|
||||
role = models.ForeignKey(
|
||||
Role,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name='角色'
|
||||
)
|
||||
menu = models.ForeignKey(
|
||||
'Menu',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name='菜单/权限'
|
||||
)
|
||||
# 可选:记录权限关联时间
|
||||
create_time = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name='权限关联时间'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'system_role_permission'
|
||||
verbose_name = '角色权限关联'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
class DictType(CoreModel):
|
||||
"""字典类型表"""
|
||||
name = models.CharField(max_length=100, default='', verbose_name='字典名称')
|
||||
type = models.CharField(max_length=100, default='', verbose_name='字典类型', db_index=True)
|
||||
status = models.BooleanField(default=True)
|
||||
deleted_time = models.DateTimeField(null=True, blank=True, verbose_name='删除时间')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '字典类型'
|
||||
verbose_name_plural = '字典类型'
|
||||
db_table = 'system_dict_type'
|
||||
ordering = ['-id']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class DictData(CoreModel):
|
||||
"""字典数据表"""
|
||||
sort = models.IntegerField(default=0, verbose_name='字典排序')
|
||||
label = models.CharField(max_length=100, default='', verbose_name='字典标签')
|
||||
value = models.CharField(max_length=100, default='', verbose_name='字典键值')
|
||||
dict_type = models.ForeignKey(
|
||||
DictType,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='dict_data',
|
||||
verbose_name='字典类型'
|
||||
)
|
||||
status = models.BooleanField(default=True)
|
||||
color_type = models.CharField(max_length=100, blank=True, default='', verbose_name='颜色类型')
|
||||
css_class = models.CharField(max_length=100, blank=True, default='', verbose_name='css 样式')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '字典数据'
|
||||
verbose_name_plural = '字典数据'
|
||||
db_table = 'system_dict_data'
|
||||
ordering = ['sort', 'id']
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
|
||||
class User(AbstractUser, CoreModel):
|
||||
mobile = models.CharField(max_length=11, null=True, validators=[validate_mobile], db_comment="手机号")
|
||||
nickname = models.CharField(max_length=50, blank=True, null=True, db_comment="昵称")
|
||||
gender = models.SmallIntegerField(blank=True, null=True, default=0, db_comment='性别')
|
||||
language = models.CharField('语言', max_length=20, blank=True, null=True, db_comment="语言")
|
||||
city = models.CharField('城市', max_length=20, blank=True, null=True, db_comment="城市")
|
||||
province = models.CharField('省份', max_length=50, blank=True, null=True, db_comment="省份")
|
||||
country = models.CharField('国家', max_length=50, blank=True, null=True, db_comment="国家")
|
||||
avatarUrl = models.URLField('头像', blank=True, null=True, db_comment="头像")
|
||||
|
||||
dept = models.ManyToManyField(
|
||||
'Dept', blank=True, verbose_name='部门', db_constraint=False,
|
||||
related_name='users'
|
||||
)
|
||||
status = models.BooleanField(default=False, verbose_name='<帐号状态>(1正常 0停用)', db_comment="帐号状态")
|
||||
login_date = models.DateTimeField("<最后登录时间>", blank=True, null=True, db_comment="最后登录时间")
|
||||
login_ip = models.GenericIPAddressField(blank=True, null=True, db_comment="最后登录IP")
|
||||
|
||||
class Meta:
|
||||
verbose_name = '用户数据'
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = 'system_users'
|
||||
5
backend/system/serializers.py
Normal file
5
backend/system/serializers.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import serializers
|
||||
from .models import Department, Menu, MenuMeta, Role
|
||||
|
||||
14
backend/system/tasks.py
Normal file
14
backend/system/tasks.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# 某个 app 目录下的 tasks.py
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
@shared_task
|
||||
def sync_temu_order():
|
||||
pass
|
||||
|
||||
@shared_task
|
||||
def sync_temu_shipping():
|
||||
pass
|
||||
3
backend/system/tests.py
Normal file
3
backend/system/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
19
backend/system/urls.py
Normal file
19
backend/system/urls.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'dept', views.DeptViewSet)
|
||||
router.register(r'menu-meta', views.MenuMetaViewSet)
|
||||
router.register(r'menu', views.MenuViewSet)
|
||||
router.register(r'role', views.RoleViewSet)
|
||||
router.register(r'dict_data', views.DictDataViewSet)
|
||||
router.register(r'dict_type', views.DictTypeViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('login/', views.user.UserLogin.as_view()),
|
||||
path('info/', views.user.UserInfo.as_view()),
|
||||
path('codes/', views.user.Codes.as_view()),
|
||||
]
|
||||
16
backend/system/views/__init__.py
Normal file
16
backend/system/views/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
__all__ = [
|
||||
'DeptViewSet',
|
||||
'MenuViewSet',
|
||||
'MenuMetaViewSet',
|
||||
'RoleViewSet',
|
||||
'DictDataViewSet',
|
||||
'DictTypeViewSet',
|
||||
]
|
||||
|
||||
from system.views.dict_data import DictDataViewSet
|
||||
from system.views.dict_type import DictTypeViewSet
|
||||
from system.views.menu import MenuViewSet, MenuMetaViewSet
|
||||
from system.views.role import RoleViewSet
|
||||
|
||||
from system.views.dept import DeptViewSet
|
||||
from system.views.user import *
|
||||
84
backend/system/views/dept.py
Normal file
84
backend/system/views/dept.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from datetime import timezone, datetime
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from system.models import Dept
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class DeptSerializer(serializers.ModelSerializer):
|
||||
"""部门序列化器"""
|
||||
children = serializers.SerializerMethodField()
|
||||
status_text = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'create_time']
|
||||
|
||||
def get_children(self, obj):
|
||||
"""获取子部门"""
|
||||
children = obj.children.all().order_by('id')
|
||||
if children:
|
||||
return DeptSerializer(children, many=True).data
|
||||
return []
|
||||
|
||||
def get_status_text(self, obj):
|
||||
"""获取状态文本"""
|
||||
return obj.get_status_display()
|
||||
|
||||
|
||||
class DeptViewSet(CustomModelViewSet):
|
||||
"""部门管理视图集"""
|
||||
queryset = Dept.objects.filter(pid__isnull=True).order_by('id', 'status')
|
||||
serializer_class = DeptSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status', 'pid']
|
||||
search_fields = ['name']
|
||||
ordering_fields = ['create_time', 'name']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# 自动设置创建时间
|
||||
if 'create_time' not in serializer.validated_data:
|
||||
serializer.validated_data['create_time'] = datetime.now()
|
||||
serializer.save()
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
pk = kwargs['pk']
|
||||
instance = Dept.objects.get(pk=pk)
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
|
||||
if getattr(instance, '_prefetched_objects_cache', None):
|
||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||
# forcibly invalidate the prefetch cache on the instance.
|
||||
instance._prefetched_objects_cache = {}
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
pk = kwargs['pk']
|
||||
instance = Dept.objects.get(pk=pk)
|
||||
self.perform_destroy(instance)
|
||||
return self._build_response(
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def tree(self, request):
|
||||
"""获取部门树形结构"""
|
||||
queryset = self.get_queryset().filter(pid__isnull=True)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
16
backend/system/views/dict_data.py
Normal file
16
backend/system/views/dict_data.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework import serializers, viewsets
|
||||
from system.models import DictData
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class DictDataSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DictData
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class DictDataViewSet(CustomModelViewSet):
|
||||
queryset = DictData.objects.filter(is_deleted=False)
|
||||
serializer_class = DictDataSerializer
|
||||
filterset_fields = ['dict_type']
|
||||
15
backend/system/views/dict_type.py
Normal file
15
backend/system/views/dict_type.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers, viewsets
|
||||
from system.models import DictType
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class DictTypeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DictType
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class DictTypeViewSet(CustomModelViewSet):
|
||||
queryset = DictType.objects.filter(is_deleted=False)
|
||||
serializer_class = DictTypeSerializer
|
||||
131
backend/system/views/menu.py
Normal file
131
backend/system/views/menu.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import viewsets, serializers, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from system.models import Menu, MenuMeta
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class MenuMetaSerializer(serializers.ModelSerializer):
|
||||
"""菜单元数据序列化器"""
|
||||
class Meta:
|
||||
model = MenuMeta
|
||||
fields = '__all__'
|
||||
|
||||
class MenuSerializer(serializers.ModelSerializer):
|
||||
"""菜单序列化器"""
|
||||
parent = serializers.CharField(source='pid.name', read_only=True)
|
||||
meta = MenuMetaSerializer()
|
||||
children = serializers.SerializerMethodField()
|
||||
status_text = serializers.SerializerMethodField()
|
||||
type_text = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'create_time', 'update_time']
|
||||
|
||||
def get_children(self, obj):
|
||||
"""获取子菜单"""
|
||||
children = obj.children.all()
|
||||
if children:
|
||||
return MenuSerializer(children, many=True).data
|
||||
return []
|
||||
|
||||
def get_status_text(self, obj):
|
||||
"""获取状态文本"""
|
||||
return obj.get_status_display()
|
||||
|
||||
def get_type_text(self, obj):
|
||||
"""获取菜单类型文本"""
|
||||
return obj.get_type_display()
|
||||
|
||||
def create(self, validated_data):
|
||||
"""创建菜单及关联的元数据"""
|
||||
meta_data = validated_data.pop('meta')
|
||||
meta = MenuMeta.objects.create(**meta_data)
|
||||
menu = Menu.objects.create(meta=meta, **validated_data)
|
||||
return menu
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""更新菜单及关联的元数据"""
|
||||
meta_data = validated_data.pop('meta', {})
|
||||
meta_serializer = self.fields['meta']
|
||||
meta_serializer.update(instance.meta, meta_data)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
|
||||
class MenuMetaViewSet(viewsets.ModelViewSet):
|
||||
"""菜单元数据视图集"""
|
||||
queryset = MenuMeta.objects.all()
|
||||
serializer_class = MenuMetaSerializer
|
||||
|
||||
|
||||
class MenuViewSet(CustomModelViewSet):
|
||||
"""菜单管理视图集"""
|
||||
queryset = Menu.objects.filter(pid__isnull=True).order_by('id', 'status')
|
||||
serializer_class = MenuSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status', 'type', 'pid', 'name']
|
||||
search_fields = ['name', 'path', 'auth_code']
|
||||
ordering_fields = ['meta__order', 'create_time']
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def tree(self, request):
|
||||
"""获取菜单树形结构"""
|
||||
queryset = self.get_queryset().filter(pid__isnull=True)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='name-exists')
|
||||
def name_exists(self, request):
|
||||
return self._build_response()
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='name-search')
|
||||
def name_search(self, request):
|
||||
name = request.GET.get('name')
|
||||
pk = request.GET.get('id', None)
|
||||
queryset = Menu.objects.all()
|
||||
if pk:
|
||||
queryset = queryset.exclude(pk=pk)
|
||||
if name:
|
||||
queryset = queryset.filter(name=name)
|
||||
has_menu_name = queryset.exists()
|
||||
print(has_menu_name, 'has_menu_name')
|
||||
return self._build_response(data=has_menu_name)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='path-exists')
|
||||
def path_exists(self, request):
|
||||
return self._build_response()
|
||||
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
pk = kwargs['pk']
|
||||
instance = Menu.objects.get(pk=pk)
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
|
||||
if getattr(instance, '_prefetched_objects_cache', None):
|
||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||
# forcibly invalidate the prefetch cache on the instance.
|
||||
instance._prefetched_objects_cache = {}
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
pk = kwargs['pk']
|
||||
instance = Menu.objects.get(pk=pk)
|
||||
self.perform_destroy(instance)
|
||||
return self._build_response(
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
92
backend/system/views/role.py
Normal file
92
backend/system/views/role.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from system.models import RolePermission, Menu, Role
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
"""角色序列化器"""
|
||||
permissions = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Menu.objects.all(),
|
||||
many=True,
|
||||
required=False
|
||||
)
|
||||
status_text = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'create_time']
|
||||
|
||||
def get_status_text(self, obj):
|
||||
"""获取状态文本"""
|
||||
return obj.get_status_display()
|
||||
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet):
|
||||
"""角色管理视图集"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status']
|
||||
search_fields = ['name']
|
||||
ordering_fields = ['create_time']
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def assign_permissions(self, request, pk=None):
|
||||
"""分配角色权限"""
|
||||
role = self.get_object()
|
||||
menu_ids = request.data.get('menu_ids', [])
|
||||
|
||||
# 清除原有权限
|
||||
role.permissions.clear()
|
||||
|
||||
# 添加新权限
|
||||
for menu_id in menu_ids:
|
||||
menu = get_object_or_404(Menu, id=menu_id)
|
||||
RolePermission.objects.create(role=role, menu=menu)
|
||||
|
||||
serializer = self.get_serializer(role)
|
||||
return Response(serializer.data)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
# 获取请求数据
|
||||
data = request.data.copy()
|
||||
permissions = data.pop('permissions', []) # 提取权限列表
|
||||
|
||||
# 创建角色(不包含权限)
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
role = serializer.save()
|
||||
|
||||
# 处理权限关联(可根据需求自定义)
|
||||
if permissions:
|
||||
try:
|
||||
# 验证权限ID是否存在
|
||||
valid_permissions = Menu.objects.filter(id__in=permissions)
|
||||
|
||||
# 创建中间表记录(如果需要保存额外字段)
|
||||
for menu in valid_permissions:
|
||||
RolePermission.objects.create(
|
||||
role=role,
|
||||
menu=menu,
|
||||
)
|
||||
except Exception as e:
|
||||
# 如果关联失败,删除已创建的角色
|
||||
role.delete()
|
||||
return Response(
|
||||
{'error': f'权限关联失败: {str(e)}'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
79
backend/system/views/user.py
Normal file
79
backend/system/views/user.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.authtoken.views import ObtainAuthToken
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from system.models import User
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class UserSerializer(CustomModelViewSet):
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = ('password',)
|
||||
|
||||
|
||||
class UserLogin(ObtainAuthToken):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.serializer_class(data=request.data,
|
||||
context={'request': request})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data['user']
|
||||
token, created = Token.objects.get_or_create(user=user)
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": user.id,
|
||||
"password": user.password,
|
||||
"realName": user.nickname,
|
||||
"roles": [
|
||||
"super"
|
||||
],
|
||||
"username": user.username,
|
||||
"accessToken": token.key
|
||||
},
|
||||
"error": None,
|
||||
"message": "ok"
|
||||
})
|
||||
|
||||
|
||||
class UserInfo(APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user = self.request.user
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": user.id,
|
||||
"realName": user.username,
|
||||
"roles": [
|
||||
"super"
|
||||
],
|
||||
"username": user.username,
|
||||
},
|
||||
"error": None,
|
||||
"message": "ok"
|
||||
})
|
||||
|
||||
|
||||
class Codes(APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": [
|
||||
"AC_100100",
|
||||
"AC_100110",
|
||||
"AC_100120",
|
||||
"AC_100010"
|
||||
],
|
||||
"error": None,
|
||||
"message": "ok"
|
||||
})
|
||||
|
||||
|
||||
class UserViewSet(ModelViewSet):
|
||||
queryset = User.objects.all().order_by('id')
|
||||
serializer_class = UserSerializer
|
||||
7
backend/utils/authentication.py
Normal file
7
backend/utils/authentication.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
class BearerTokenAuthentication(TokenAuthentication):
|
||||
"""
|
||||
使用 'Bearer' 前缀的 Token 认证
|
||||
"""
|
||||
keyword = 'Bearer'
|
||||
149
backend/utils/custom_model_viewSet.py
Normal file
149
backend/utils/custom_model_viewSet.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
class CustomModelViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
自定义ModelViewSet,提供以下增强功能:
|
||||
- 基于动作的序列化器选择
|
||||
- 基于动作的权限控制
|
||||
- 标准化响应格式
|
||||
- 软删除支持
|
||||
- 批量操作支持
|
||||
"""
|
||||
# 动作到序列化器类的映射
|
||||
action_serializers = {}
|
||||
# 动作到权限类的映射
|
||||
action_permissions = {}
|
||||
# 软删除字段名
|
||||
soft_delete_field = 'is_deleted'
|
||||
# 是否支持软删除
|
||||
enable_soft_delete = False
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""根据当前动作获取序列化器类"""
|
||||
return self.action_serializers.get(
|
||||
self.action,
|
||||
super().get_serializer_class()
|
||||
)
|
||||
|
||||
def get_permissions(self):
|
||||
"""根据当前动作获取权限类"""
|
||||
permissions = self.action_permissions.get(
|
||||
self.action,
|
||||
self.permission_classes
|
||||
)
|
||||
return [permission() for permission in permissions]
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""重写列表视图,支持软删除过滤"""
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# 应用软删除过滤
|
||||
if self.enable_soft_delete:
|
||||
queryset = queryset.filter(**{self.soft_delete_field: False})
|
||||
|
||||
# 应用搜索和过滤
|
||||
queryset = self.filter_queryset(queryset)
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""重写详情视图,支持软删除检查"""
|
||||
instance = self.get_object()
|
||||
|
||||
# 检查软删除状态
|
||||
if (self.enable_soft_delete and
|
||||
hasattr(instance, self.soft_delete_field) and
|
||||
getattr(instance, self.soft_delete_field)):
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = self.get_serializer(instance)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="Object retrieved successfully",
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""重写创建视图,支持批量创建"""
|
||||
is_many = isinstance(request.data, list)
|
||||
|
||||
if is_many:
|
||||
serializer = self.get_serializer(data=request.data, many=True)
|
||||
else:
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
return self._build_response(
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
|
||||
if getattr(instance, '_prefetched_objects_cache', None):
|
||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||
# forcibly invalidate the prefetch cache on the instance.
|
||||
instance._prefetched_objects_cache = {}
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def _build_response(self, code=0, message="成功", data=None, status=status.HTTP_200_OK):
|
||||
"""
|
||||
构建标准化API响应格式
|
||||
|
||||
参数说明:
|
||||
- code: 业务状态码(0表示成功,非0表示错误)
|
||||
- message: 状态描述信息
|
||||
- data: 响应数据(可为None)
|
||||
- status: HTTP状态码(默认200)
|
||||
"""
|
||||
# 构建基础响应结构
|
||||
response_data = {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
|
||||
# 仅当data不为None时添加到响应中
|
||||
if data is not None:
|
||||
response_data["data"] = data
|
||||
|
||||
# 移除可能的空值(如message为空字符串)
|
||||
response_data = {k: v for k, v in response_data.items() if v is not None and v != ""}
|
||||
|
||||
# 返回DRF的Response对象
|
||||
return Response(
|
||||
data=response_data,
|
||||
status=status,
|
||||
content_type="application/json"
|
||||
)
|
||||
20
backend/utils/models.py
Normal file
20
backend/utils/models.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@Remark: 公共基础model类
|
||||
"""
|
||||
from django.db import models
|
||||
|
||||
class CoreModel(models.Model):
|
||||
remark = models.CharField(max_length=256, verbose_name="备注", null=True, blank=True, help_text="备注")
|
||||
creator = models.CharField(max_length=64, null=True, blank=True, help_text="创建人", verbose_name="创建人")
|
||||
modifier = models.CharField(max_length=64, null=True, blank=True, help_text="修改人", verbose_name="修改人")
|
||||
update_time = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间")
|
||||
create_time = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
|
||||
verbose_name="创建时间")
|
||||
is_deleted = models.BooleanField(default=False, verbose_name='是否软删除')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = '核心模型'
|
||||
verbose_name_plural = verbose_name
|
||||
56
backend/utils/pagination.py
Normal file
56
backend/utils/pagination.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core import paginator
|
||||
from django.core.paginator import Paginator as DjangoPaginator
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.response import Response
|
||||
from django.core.paginator import InvalidPage
|
||||
|
||||
|
||||
class CustomPagination(PageNumberPagination):
|
||||
page_size = 20
|
||||
page_size_query_param = "pageSize"
|
||||
max_page_size = 999
|
||||
django_paginator_class = DjangoPaginator
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
"""
|
||||
重写paginate_queryset让分页超过正常分页:有原来的4000错误无效页面。改写为返回2000成功,data=[]提示
|
||||
"""
|
||||
page_size = self.get_page_size(request)
|
||||
if not page_size:
|
||||
return None
|
||||
paginator = self.django_paginator_class(queryset, page_size)
|
||||
page_number = self.get_page_number(request, paginator)
|
||||
try:
|
||||
self.page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
self.page = []
|
||||
|
||||
if paginator.num_pages > 1 and self.template is not None:
|
||||
# The browsable API should display pagination controls.
|
||||
self.display_page_controls = True
|
||||
|
||||
self.request = request
|
||||
return list(self.page)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
code = 0
|
||||
msg = 'ok'
|
||||
total = self.page.paginator.count if self.page else 0
|
||||
res = {
|
||||
"total": total,
|
||||
"items": data
|
||||
}
|
||||
if not data:
|
||||
code = 0
|
||||
msg = "暂无数据"
|
||||
res['data'] = []
|
||||
|
||||
return Response(OrderedDict([
|
||||
('code', code),
|
||||
('message', msg),
|
||||
('data', res),
|
||||
('error', None),
|
||||
]))
|
||||
8
backend/utils/permissions.py
Normal file
8
backend/utils/permissions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
class IsSuperUserOrReadOnly(permissions.BasePermission):
|
||||
"""超级用户可读写,普通用户只读"""
|
||||
def has_permission(self, request, view):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
return request.user and request.user.is_superuser
|
||||
118
backend/utils/serializers.py
Normal file
118
backend/utils/serializers.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
@Remark: 自定义序列化器
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from django.utils.functional import cached_property
|
||||
from rest_framework.utils.serializer_helpers import BindingDict
|
||||
|
||||
from system.models import User
|
||||
|
||||
|
||||
class CustomModelSerializer(ModelSerializer):
|
||||
"""
|
||||
增强DRF的ModelSerializer,可自动更新模型的审计字段记录
|
||||
(1)self.request能获取到rest_framework.request.Request对象
|
||||
"""
|
||||
# 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
|
||||
modifier_field_id = 'modifier'
|
||||
modifier_name = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_modifier_name(self, instance):
|
||||
if not hasattr(instance, 'modifier'):
|
||||
return None
|
||||
queryset = User.objects.filter(id=instance.modifier).values_list('name', flat=True).first()
|
||||
if queryset:
|
||||
return queryset
|
||||
return None
|
||||
|
||||
# 创建人的审计字段名称, 默认creator, 继承使用时可自定义覆盖
|
||||
creator_field_id = 'creator'
|
||||
# 添加默认时间返回格式
|
||||
create_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", required=False, read_only=True)
|
||||
update_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", required=False)
|
||||
|
||||
def __init__(self, instance=None, data=empty, request=None, **kwargs):
|
||||
super().__init__(instance, data, **kwargs)
|
||||
self.request: Request = request or self.context.get('request', None)
|
||||
|
||||
def save(self, **kwargs):
|
||||
return super().save(**kwargs)
|
||||
|
||||
def create(self, validated_data):
|
||||
if self.request:
|
||||
if self.modifier_field_id in self.fields.fields:
|
||||
validated_data[self.modifier_field_id] = self.get_request_username()
|
||||
if self.creator_field_id in self.fields.fields:
|
||||
validated_data[self.creator_field_id] = self.get_request_username()
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if self.request:
|
||||
if hasattr(self.instance, self.modifier_field_id):
|
||||
self.instance.modifier = self.get_request_username()
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def get_request_username(self):
|
||||
if getattr(self.request, 'user', None):
|
||||
return getattr(self.request.user, 'username', None)
|
||||
return None
|
||||
|
||||
def get_request_name(self):
|
||||
if getattr(self.request, 'user', None):
|
||||
return getattr(self.request.user, 'name', None)
|
||||
return None
|
||||
|
||||
def get_request_user_id(self):
|
||||
if getattr(self.request, 'user', None):
|
||||
return getattr(self.request.user, 'id', None)
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def fields(self):
|
||||
fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
fields[key] = value
|
||||
|
||||
if not hasattr(self, '_context'):
|
||||
return fields
|
||||
is_root = self.root == self
|
||||
parent_is_list_root = self.parent == self.root and getattr(self.parent, 'many', False)
|
||||
if not (is_root or parent_is_list_root):
|
||||
return fields
|
||||
|
||||
try:
|
||||
request = self.request or self.context['request']
|
||||
except KeyError:
|
||||
return fields
|
||||
params = getattr(
|
||||
request, 'query_params', getattr(request, 'GET', None)
|
||||
)
|
||||
if params is None:
|
||||
pass
|
||||
try:
|
||||
filter_fields = params.get('_fields', None).split(',')
|
||||
except AttributeError:
|
||||
filter_fields = None
|
||||
|
||||
try:
|
||||
omit_fields = params.get('_exclude', None).split(',')
|
||||
except AttributeError:
|
||||
omit_fields = []
|
||||
|
||||
existing = set(fields.keys())
|
||||
if filter_fields is None:
|
||||
allowed = existing
|
||||
else:
|
||||
allowed = set(filter(None, filter_fields))
|
||||
|
||||
omitted = set(filter(None, omit_fields))
|
||||
for field in existing:
|
||||
if field not in allowed:
|
||||
fields.pop(field, None)
|
||||
if field in omitted:
|
||||
fields.pop(field, None)
|
||||
|
||||
return fields
|
||||
30
backend/utils/utils.py
Normal file
30
backend/utils/utils.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
|
||||
def validate_mobile(value):
|
||||
if value and not re.findall(r"1\d{10}", value):
|
||||
raise ValidationError('手机格式不正确')
|
||||
|
||||
|
||||
def validate_amount(value):
|
||||
if value is None:
|
||||
raise ValidationError('金额不能为空')
|
||||
if value and value < 0:
|
||||
raise ValidationError('金额不能为负')
|
||||
|
||||
|
||||
def to_cent(value):
|
||||
if value is None:
|
||||
value = 0
|
||||
return Decimal(value).quantize(Decimal('.01'), rounding=ROUND_HALF_UP)
|
||||
|
||||
# 定义一个小工具:从时间戳转换为 aware datetime(如果时间戳有效)
|
||||
def ts_to_aware(ts):
|
||||
if ts:
|
||||
naive_dt = datetime.fromtimestamp(ts)
|
||||
return timezone.make_aware(naive_dt)
|
||||
return None
|
||||
4
web/.browserslistrc
Normal file
4
web/.browserslistrc
Normal file
@@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
1
web/.commitlintrc.js
Normal file
1
web/.commitlintrc.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/commitlint-config';
|
||||
7
web/.dockerignore
Normal file
7
web/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
dist
|
||||
.turbo
|
||||
dist.zip
|
||||
18
web/.editorconfig
Normal file
18
web/.editorconfig
Normal file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
insert_final_newline=true
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
max_line_length = 100
|
||||
trim_trailing_whitespace = true
|
||||
quote_type = single
|
||||
|
||||
[*.{yml,yaml,json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
1
web/.node-version
Normal file
1
web/.node-version
Normal file
@@ -0,0 +1 @@
|
||||
22.1.0
|
||||
13
web/.npmrc
Normal file
13
web/.npmrc
Normal file
@@ -0,0 +1,13 @@
|
||||
registry = "https://registry.npmmirror.com"
|
||||
public-hoist-pattern[]=lefthook
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||
public-hoist-pattern[]=stylelint
|
||||
public-hoist-pattern[]=*postcss*
|
||||
public-hoist-pattern[]=@commitlint/*
|
||||
public-hoist-pattern[]=czg
|
||||
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
dedupe-peer-dependents=true
|
||||
18
web/.prettierignore
Normal file
18
web/.prettierignore
Normal file
@@ -0,0 +1,18 @@
|
||||
dist
|
||||
dev-dist
|
||||
.local
|
||||
.output.js
|
||||
node_modules
|
||||
.nvmrc
|
||||
coverage
|
||||
CODEOWNERS
|
||||
.nitro
|
||||
.output
|
||||
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
public
|
||||
.npmrc
|
||||
*-lock.yaml
|
||||
1
web/.prettierrc.mjs
Normal file
1
web/.prettierrc.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/prettier-config';
|
||||
4
web/.stylelintignore
Normal file
4
web/.stylelintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
public
|
||||
__tests__
|
||||
coverage
|
||||
9
web/LICENSE
Normal file
9
web/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present, Vben
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
153
web/README.ja-JP.md
Normal file
153
web/README.ja-JP.md
Normal file
@@ -0,0 +1,153 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**日本語** | [English](./README.md) | [中文](./README.zh-CN.md)
|
||||
|
||||
## 紹介
|
||||
|
||||
Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術を使用して開発された、無料でオープンソースの中・後端テンプレートです。すぐに使える中・後端のフロントエンドソリューションとして、学習の参考にもなります。
|
||||
|
||||
## アップグレード通知
|
||||
|
||||
これは最新バージョン `5.0` であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
|
||||
|
||||
## 特徴
|
||||
|
||||
- **最新技術スタック**:Vue 3やViteなどの最先端フロントエンド技術で開発
|
||||
- **TypeScript**:アプリケーション規模のJavaScriptのための言語
|
||||
- **テーマ**:複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
|
||||
- **国際化**:完全な内蔵国際化サポート
|
||||
- **権限管理**:動的ルートベースの権限生成ソリューションを内蔵
|
||||
|
||||
## プレビュー
|
||||
|
||||
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
|
||||
|
||||
テストアカウント:vben/123456
|
||||
|
||||
<div align="center">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||
</div>
|
||||
|
||||
### Gitpodを使用
|
||||
|
||||
Gitpod(GitHub用の無料オンライン開発環境)でプロジェクトを開き、すぐにコーディングを開始します。
|
||||
|
||||
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
|
||||
|
||||
## ドキュメント
|
||||
|
||||
[ドキュメント](https://doc.vben.pro/)
|
||||
|
||||
## インストールと使用
|
||||
|
||||
1. プロジェクトコードを取得
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||
```
|
||||
|
||||
2. 依存関係のインストール
|
||||
|
||||
```bash
|
||||
cd vue-vben-admin
|
||||
npm i -g corepack
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. 実行
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. ビルド
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 変更ログ
|
||||
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||
|
||||
## 貢献方法
|
||||
|
||||
ご参加をお待ちしておりますするか、Pull Requestを送信してください。
|
||||
|
||||
**Pull Request プロセス:**
|
||||
|
||||
1. コードをフォーク
|
||||
2. 自分のブランチを作成:`git checkout -b feat/xxxx`
|
||||
3. 変更をコミット:`git commit -am 'feat(function): add xxxxx'`
|
||||
4. ブランチをプッシュ:`git push origin feat/xxxx`
|
||||
5. `pull request`を送信
|
||||
|
||||
## Git貢献提出規則
|
||||
|
||||
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
|
||||
- `feat` 新機能の追加
|
||||
- `fix` 問題/バグの修正
|
||||
- `style` コードスタイルに関連し、実行結果に影響しない
|
||||
- `perf` 最適化/パフォーマンス向上
|
||||
- `refactor` リファクタリング
|
||||
- `revert` 変更の取り消し
|
||||
- `test` テスト関連
|
||||
- `docs` ドキュメント/注釈
|
||||
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
|
||||
- `ci` 継続的インテグレーション
|
||||
- `types` 型定義ファイルの変更
|
||||
|
||||
## ブラウザサポート
|
||||
|
||||
ローカル開発には `Chrome 80+` ブラウザを推奨します
|
||||
|
||||
モダンブラウザをサポートし、IEはサポートしません
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
|
||||
|
||||
## メンテナー
|
||||
|
||||
[@Vben](https://github.com/anncwb)
|
||||
|
||||
## スター歴史
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## 寄付
|
||||
|
||||
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
|
||||
|
||||

|
||||
|
||||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||
|
||||
## 貢献者
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
||||
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
|
||||
|
||||
## ライセンス
|
||||
|
||||
[MIT © Vben-2020](./LICENSE)
|
||||
153
web/README.md
Normal file
153
web/README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
## Introduction
|
||||
|
||||
Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
|
||||
|
||||
## Upgrade Notice
|
||||
|
||||
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
|
||||
|
||||
## Features
|
||||
|
||||
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
|
||||
- **TypeScript**: A language for application-scale JavaScript
|
||||
- **Themes**: Multiple theme colors available with customizable options
|
||||
- **Internationalization**: Comprehensive built-in internationalization support
|
||||
- **Permissions**: Built-in solution for dynamic route-based permission generation
|
||||
|
||||
## Preview
|
||||
|
||||
- [Vben Admin](https://vben.pro/) - Full version Chinese site
|
||||
|
||||
Test Account: vben/123456
|
||||
|
||||
<div align="center">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||
</div>
|
||||
|
||||
### Use Gitpod
|
||||
|
||||
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
|
||||
|
||||
## Documentation
|
||||
|
||||
[Document](https://doc.vben.pro/)
|
||||
|
||||
## Install and Use
|
||||
|
||||
1. Get the project code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
cd vue-vben-admin
|
||||
npm i -g corepack
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Run
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. Build
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Change Log
|
||||
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||
|
||||
## How to Contribute
|
||||
|
||||
You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
|
||||
|
||||
**Pull Request Process:**
|
||||
|
||||
1. Fork the code
|
||||
2. Create your branch: `git checkout -b feat/xxxx`
|
||||
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
|
||||
4. Push your branch: `git push origin feat/xxxx`
|
||||
5. Submit `pull request`
|
||||
|
||||
## Git Contribution Submission Specification
|
||||
|
||||
Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
|
||||
- `feat` Add new features
|
||||
- `fix` Fix the problem/BUG
|
||||
- `style` The code style is related and does not affect the running result
|
||||
- `perf` Optimization/performance improvement
|
||||
- `refactor` Refactor
|
||||
- `revert` Undo edit
|
||||
- `test` Test related
|
||||
- `docs` Documentation/notes
|
||||
- `chore` Dependency update/scaffolding configuration modification etc.
|
||||
- `ci` Continuous integration
|
||||
- `types` Type definition file changes
|
||||
|
||||
## Browser Support
|
||||
|
||||
The `Chrome 80+` browser is recommended for local development
|
||||
|
||||
Support modern browsers, not IE
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
|
||||
## Maintainer
|
||||
|
||||
[@Vben](https://github.com/anncwb)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## Donate
|
||||
|
||||
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
|
||||
|
||||

|
||||
|
||||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
||||
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
|
||||
|
||||
## License
|
||||
|
||||
[MIT © Vben-2020](./LICENSE)
|
||||
153
web/README.zh-CN.md
Normal file
153
web/README.zh-CN.md
Normal file
@@ -0,0 +1,153 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**中文** | [English](./README.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
## 简介
|
||||
|
||||
Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的中后台模板,它采用了最新的 Vue 3、Vite、TypeScript 等主流技术开发,开箱即用,可用于中后台前端开发,也适合学习参考。
|
||||
|
||||
## 升级提示
|
||||
|
||||
该版本为最新版本 `5.0`,与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2)
|
||||
|
||||
## 特性
|
||||
|
||||
- **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发
|
||||
- **TypeScript**:应用程序级 JavaScript 的语言
|
||||
- **主题**:提供多套主题色彩,可配置自定义主题
|
||||
- **国际化**:内置完善的国际化方案
|
||||
- **权限**:内置完善的动态路由权限生成方案
|
||||
|
||||
## 预览
|
||||
|
||||
- [Vben Admin](https://vben.pro/) - 完整版中文站点
|
||||
|
||||
测试账号:vben/123456
|
||||
|
||||
<div align="center">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||
</div>
|
||||
|
||||
### 使用 Gitpod
|
||||
|
||||
在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码。
|
||||
|
||||
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
|
||||
|
||||
## 文档
|
||||
|
||||
[文档地址](https://doc.vben.pro/)
|
||||
|
||||
## 安装使用
|
||||
|
||||
1. 获取项目代码
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
|
||||
```bash
|
||||
cd vue-vben-admin
|
||||
npm i -g corepack
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. 运行
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. 打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||
|
||||
## 如何贡献
|
||||
|
||||
非常欢迎你的加入 或者提交一个 Pull Request。
|
||||
|
||||
**Pull Request 流程:**
|
||||
|
||||
1. Fork 代码
|
||||
2. 创建自己的分支:`git checkout -b feature/xxxx`
|
||||
3. 提交你的修改:`git commit -am 'feat(function): add xxxxx'`
|
||||
4. 推送您的分支:`git push origin feature/xxxx`
|
||||
5. 提交 `pull request`
|
||||
|
||||
## Git 贡献提交规范
|
||||
|
||||
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
本地开发推荐使用 `Chrome 80+` 浏览器
|
||||
|
||||
支持现代浏览器,不支持 IE
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
|
||||
## 维护者
|
||||
|
||||
[@Vben](https://github.com/anncwb)
|
||||
|
||||
## Star 历史
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## 捐赠
|
||||
|
||||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
|
||||
|
||||

|
||||
|
||||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||
|
||||
## 贡献者
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
||||
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT © Vben-2020](./LICENSE)
|
||||
15
web/apps/backend-mock/README.md
Normal file
15
web/apps/backend-mock/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# @vben/backend-mock
|
||||
|
||||
## Description
|
||||
|
||||
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ pnpm run start
|
||||
|
||||
# production mode
|
||||
$ pnpm run build
|
||||
```
|
||||
14
web/apps/backend-mock/api/auth/codes.ts
Normal file
14
web/apps/backend-mock/api/auth/codes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const codes =
|
||||
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
|
||||
|
||||
return useResponseSuccess(codes);
|
||||
});
|
||||
36
web/apps/backend-mock/api/auth/login.post.ts
Normal file
36
web/apps/backend-mock/api/auth/login.post.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
|
||||
import { forbiddenResponse } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { password, username } = await readBody(event);
|
||||
if (!password || !username) {
|
||||
setResponseStatus(event, 400);
|
||||
return useResponseError(
|
||||
'BadRequestException',
|
||||
'Username and password are required',
|
||||
);
|
||||
}
|
||||
|
||||
const findUser = MOCK_USERS.find(
|
||||
(item) => item.username === username && item.password === password,
|
||||
);
|
||||
|
||||
if (!findUser) {
|
||||
clearRefreshTokenCookie(event);
|
||||
return forbiddenResponse(event, 'Username or password is incorrect.');
|
||||
}
|
||||
|
||||
const accessToken = generateAccessToken(findUser);
|
||||
const refreshToken = generateRefreshToken(findUser);
|
||||
|
||||
setRefreshTokenCookie(event, refreshToken);
|
||||
|
||||
return useResponseSuccess({
|
||||
...findUser,
|
||||
accessToken,
|
||||
});
|
||||
});
|
||||
15
web/apps/backend-mock/api/auth/logout.post.ts
Normal file
15
web/apps/backend-mock/api/auth/logout.post.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
getRefreshTokenFromCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const refreshToken = getRefreshTokenFromCookie(event);
|
||||
if (!refreshToken) {
|
||||
return useResponseSuccess('');
|
||||
}
|
||||
|
||||
clearRefreshTokenCookie(event);
|
||||
|
||||
return useResponseSuccess('');
|
||||
});
|
||||
33
web/apps/backend-mock/api/auth/refresh.post.ts
Normal file
33
web/apps/backend-mock/api/auth/refresh.post.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
getRefreshTokenFromCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
import { verifyRefreshToken } from '~/utils/jwt-utils';
|
||||
import { forbiddenResponse } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const refreshToken = getRefreshTokenFromCookie(event);
|
||||
if (!refreshToken) {
|
||||
return forbiddenResponse(event);
|
||||
}
|
||||
|
||||
clearRefreshTokenCookie(event);
|
||||
|
||||
const userinfo = verifyRefreshToken(refreshToken);
|
||||
if (!userinfo) {
|
||||
return forbiddenResponse(event);
|
||||
}
|
||||
|
||||
const findUser = MOCK_USERS.find(
|
||||
(item) => item.username === userinfo.username,
|
||||
);
|
||||
if (!findUser) {
|
||||
return forbiddenResponse(event);
|
||||
}
|
||||
const accessToken = generateAccessToken(findUser);
|
||||
|
||||
setRefreshTokenCookie(event, refreshToken);
|
||||
|
||||
return accessToken;
|
||||
});
|
||||
28
web/apps/backend-mock/api/demo/bigint.ts
Normal file
28
web/apps/backend-mock/api/demo/bigint.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
const data = `
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 123456789012345678901234567890123456789012345678901234567890,
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"email": "john-doe@demo.com"
|
||||
},
|
||||
{
|
||||
"id": 987654321098765432109876543210987654321098765432109876543210,
|
||||
"name": "Jane Smith",
|
||||
"age": 25,
|
||||
"email": "jane@demo.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
setHeader(event, 'Content-Type', 'application/json');
|
||||
return data;
|
||||
});
|
||||
13
web/apps/backend-mock/api/menu/all.ts
Normal file
13
web/apps/backend-mock/api/menu/all.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const menus =
|
||||
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
|
||||
return useResponseSuccess(menus);
|
||||
});
|
||||
5
web/apps/backend-mock/api/status.ts
Normal file
5
web/apps/backend-mock/api/status.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default eventHandler((event) => {
|
||||
const { status } = getQuery(event);
|
||||
setResponseStatus(event, Number(status));
|
||||
return useResponseError(`${status}`);
|
||||
});
|
||||
15
web/apps/backend-mock/api/system/dept/.post.ts
Normal file
15
web/apps/backend-mock/api/system/dept/.post.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
await sleep(600);
|
||||
return useResponseSuccess(null);
|
||||
});
|
||||
15
web/apps/backend-mock/api/system/dept/[id].delete.ts
Normal file
15
web/apps/backend-mock/api/system/dept/[id].delete.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
await sleep(1000);
|
||||
return useResponseSuccess(null);
|
||||
});
|
||||
15
web/apps/backend-mock/api/system/dept/[id].put.ts
Normal file
15
web/apps/backend-mock/api/system/dept/[id].put.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
await sleep(2000);
|
||||
return useResponseSuccess(null);
|
||||
});
|
||||
61
web/apps/backend-mock/api/system/dept/list.ts
Normal file
61
web/apps/backend-mock/api/system/dept/list.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dataItem: Record<string, any> = {
|
||||
id: faker.string.uuid(),
|
||||
pid: 0,
|
||||
name: faker.commerce.department(),
|
||||
status: faker.helpers.arrayElement([0, 1]),
|
||||
createTime: formatterCN.format(
|
||||
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
|
||||
),
|
||||
remark: faker.lorem.sentence(),
|
||||
};
|
||||
if (faker.datatype.boolean()) {
|
||||
dataItem.children = Array.from(
|
||||
{ length: faker.number.int({ min: 1, max: 5 }) },
|
||||
() => ({
|
||||
id: faker.string.uuid(),
|
||||
pid: dataItem.id,
|
||||
name: faker.commerce.department(),
|
||||
status: faker.helpers.arrayElement([0, 1]),
|
||||
createTime: formatterCN.format(
|
||||
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
|
||||
),
|
||||
remark: faker.lorem.sentence(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
dataList.push(dataItem);
|
||||
}
|
||||
|
||||
return dataList;
|
||||
}
|
||||
|
||||
const mockData = generateMockDataList(10);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const listData = structuredClone(mockData);
|
||||
|
||||
return useResponseSuccess(listData);
|
||||
});
|
||||
12
web/apps/backend-mock/api/system/menu/list.ts
Normal file
12
web/apps/backend-mock/api/system/menu/list.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
return useResponseSuccess(MOCK_MENU_LIST);
|
||||
});
|
||||
28
web/apps/backend-mock/api/system/menu/name-exists.ts
Normal file
28
web/apps/backend-mock/api/system/menu/name-exists.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const namesMap: Record<string, any> = {};
|
||||
|
||||
function getNames(menus: any[]) {
|
||||
menus.forEach((menu) => {
|
||||
namesMap[menu.name] = String(menu.id);
|
||||
if (menu.children) {
|
||||
getNames(menu.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
getNames(MOCK_MENU_LIST);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
const { id, name } = getQuery(event);
|
||||
|
||||
return (name as string) in namesMap &&
|
||||
(!id || namesMap[name as string] !== String(id))
|
||||
? useResponseSuccess(true)
|
||||
: useResponseSuccess(false);
|
||||
});
|
||||
28
web/apps/backend-mock/api/system/menu/path-exists.ts
Normal file
28
web/apps/backend-mock/api/system/menu/path-exists.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const pathMap: Record<string, any> = { '/': 0 };
|
||||
|
||||
function getPaths(menus: any[]) {
|
||||
menus.forEach((menu) => {
|
||||
pathMap[menu.path] = String(menu.id);
|
||||
if (menu.children) {
|
||||
getPaths(menu.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
getPaths(MOCK_MENU_LIST);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
const { id, path } = getQuery(event);
|
||||
|
||||
return (path as string) in pathMap &&
|
||||
(!id || pathMap[path as string] !== String(id))
|
||||
? useResponseSuccess(true)
|
||||
: useResponseSuccess(false);
|
||||
});
|
||||
83
web/apps/backend-mock/api/system/role/list.ts
Normal file
83
web/apps/backend-mock/api/system/role/list.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||
|
||||
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
|
||||
const menuIds = getMenuIds(MOCK_MENU_LIST);
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dataItem: Record<string, any> = {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.commerce.product(),
|
||||
status: faker.helpers.arrayElement([0, 1]),
|
||||
createTime: formatterCN.format(
|
||||
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
|
||||
),
|
||||
permissions: faker.helpers.arrayElements(menuIds),
|
||||
remark: faker.lorem.sentence(),
|
||||
};
|
||||
|
||||
dataList.push(dataItem);
|
||||
}
|
||||
|
||||
return dataList;
|
||||
}
|
||||
|
||||
const mockData = generateMockDataList(100);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
name,
|
||||
id,
|
||||
remark,
|
||||
startTime,
|
||||
endTime,
|
||||
status,
|
||||
} = getQuery(event);
|
||||
let listData = structuredClone(mockData);
|
||||
if (name) {
|
||||
listData = listData.filter((item) =>
|
||||
item.name.toLowerCase().includes(String(name).toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (id) {
|
||||
listData = listData.filter((item) =>
|
||||
item.id.toLowerCase().includes(String(id).toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (remark) {
|
||||
listData = listData.filter((item) =>
|
||||
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (startTime) {
|
||||
listData = listData.filter((item) => item.createTime >= startTime);
|
||||
}
|
||||
if (endTime) {
|
||||
listData = listData.filter((item) => item.createTime <= endTime);
|
||||
}
|
||||
if (['0', '1'].includes(status as string)) {
|
||||
listData = listData.filter((item) => item.status === Number(status));
|
||||
}
|
||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||
});
|
||||
73
web/apps/backend-mock/api/table/list.ts
Normal file
73
web/apps/backend-mock/api/table/list.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dataItem = {
|
||||
id: faker.string.uuid(),
|
||||
imageUrl: faker.image.avatar(),
|
||||
imageUrl2: faker.image.avatar(),
|
||||
open: faker.datatype.boolean(),
|
||||
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
|
||||
productName: faker.commerce.productName(),
|
||||
price: faker.commerce.price(),
|
||||
currency: faker.finance.currencyCode(),
|
||||
quantity: faker.number.int({ min: 1, max: 100 }),
|
||||
available: faker.datatype.boolean(),
|
||||
category: faker.commerce.department(),
|
||||
releaseDate: faker.date.past(),
|
||||
rating: faker.number.float({ min: 1, max: 5 }),
|
||||
description: faker.commerce.productDescription(),
|
||||
weight: faker.number.float({ min: 0.1, max: 10 }),
|
||||
color: faker.color.human(),
|
||||
inProduction: faker.datatype.boolean(),
|
||||
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
|
||||
};
|
||||
|
||||
dataList.push(dataItem);
|
||||
}
|
||||
|
||||
return dataList;
|
||||
}
|
||||
|
||||
const mockData = generateMockDataList(100);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
await sleep(600);
|
||||
|
||||
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
|
||||
const listData = structuredClone(mockData);
|
||||
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
|
||||
listData.sort((a, b) => {
|
||||
if (sortOrder === 'asc') {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(a[sortBy as string]) -
|
||||
Number.parseFloat(b[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
} else {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(b[sortBy as string]) -
|
||||
Number.parseFloat(a[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||
});
|
||||
1
web/apps/backend-mock/api/test.get.ts
Normal file
1
web/apps/backend-mock/api/test.get.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default defineEventHandler(() => 'Test get handler');
|
||||
1
web/apps/backend-mock/api/test.post.ts
Normal file
1
web/apps/backend-mock/api/test.post.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default defineEventHandler(() => 'Test post handler');
|
||||
13
web/apps/backend-mock/api/upload.ts
Normal file
13
web/apps/backend-mock/api/upload.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
return useResponseSuccess({
|
||||
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
});
|
||||
// return useResponseError("test")
|
||||
});
|
||||
10
web/apps/backend-mock/api/user/info.ts
Normal file
10
web/apps/backend-mock/api/user/info.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
return useResponseSuccess(userinfo);
|
||||
});
|
||||
7
web/apps/backend-mock/error.ts
Normal file
7
web/apps/backend-mock/error.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { NitroErrorHandler } from 'nitropack';
|
||||
|
||||
const errorHandler: NitroErrorHandler = function (error, event) {
|
||||
event.node.res.end(`[Error Handler] ${error.stack}`);
|
||||
};
|
||||
|
||||
export default errorHandler;
|
||||
19
web/apps/backend-mock/middleware/1.api.ts
Normal file
19
web/apps/backend-mock/middleware/1.api.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { forbiddenResponse, sleep } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
event.node.res.setHeader(
|
||||
'Access-Control-Allow-Origin',
|
||||
event.headers.get('Origin') ?? '*',
|
||||
);
|
||||
if (event.method === 'OPTIONS') {
|
||||
event.node.res.statusCode = 204;
|
||||
event.node.res.statusMessage = 'No Content.';
|
||||
return 'OK';
|
||||
} else if (
|
||||
['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
|
||||
event.path.startsWith('/api/system/')
|
||||
) {
|
||||
await sleep(Math.floor(Math.random() * 2000));
|
||||
return forbiddenResponse(event, '演示环境,禁止修改');
|
||||
}
|
||||
});
|
||||
20
web/apps/backend-mock/nitro.config.ts
Normal file
20
web/apps/backend-mock/nitro.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import errorHandler from './error';
|
||||
|
||||
process.env.COMPATIBILITY_DATE = new Date().toISOString();
|
||||
export default defineNitroConfig({
|
||||
devErrorHandler: errorHandler,
|
||||
errorHandler: '~/error',
|
||||
routeRules: {
|
||||
'/api/**': {
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Allow-Headers':
|
||||
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
|
||||
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Expose-Headers': '*',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
21
web/apps/backend-mock/package.json
Normal file
21
web/apps/backend-mock/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@vben/backend-mock",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"build": "nitro build",
|
||||
"start": "nitro dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "catalog:",
|
||||
"jsonwebtoken": "catalog:",
|
||||
"nitropack": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "catalog:",
|
||||
"h3": "catalog:"
|
||||
}
|
||||
}
|
||||
13
web/apps/backend-mock/routes/[...].ts
Normal file
13
web/apps/backend-mock/routes/[...].ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export default defineEventHandler(() => {
|
||||
return `
|
||||
<h1>Hello Vben Admin</h1>
|
||||
<h2>Mock service is starting</h2>
|
||||
<ul>
|
||||
<li><a href="/api/user">/api/user/info</a></li>
|
||||
<li><a href="/api/menu">/api/menu/all</a></li>
|
||||
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
|
||||
<li><a href="/api/auth/login">/api/auth/login</a></li>
|
||||
<li><a href="/api/upload">/api/upload</a></li>
|
||||
</ul>
|
||||
`;
|
||||
});
|
||||
4
web/apps/backend-mock/tsconfig.build.json
Normal file
4
web/apps/backend-mock/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
3
web/apps/backend-mock/tsconfig.json
Normal file
3
web/apps/backend-mock/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nitro/types/tsconfig.json"
|
||||
}
|
||||
26
web/apps/backend-mock/utils/cookie-utils.ts
Normal file
26
web/apps/backend-mock/utils/cookie-utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
|
||||
deleteCookie(event, 'jwt', {
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function setRefreshTokenCookie(
|
||||
event: H3Event<EventHandlerRequest>,
|
||||
refreshToken: string,
|
||||
) {
|
||||
setCookie(event, 'jwt', refreshToken, {
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60, // unit: seconds
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
|
||||
const refreshToken = getCookie(event, 'jwt');
|
||||
return refreshToken;
|
||||
}
|
||||
59
web/apps/backend-mock/utils/jwt-utils.ts
Normal file
59
web/apps/backend-mock/utils/jwt-utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { UserInfo } from './mock-data';
|
||||
|
||||
// TODO: Replace with your own secret key
|
||||
const ACCESS_TOKEN_SECRET = 'access_token_secret';
|
||||
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
|
||||
|
||||
export interface UserPayload extends UserInfo {
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export function generateAccessToken(user: UserInfo) {
|
||||
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
|
||||
}
|
||||
|
||||
export function generateRefreshToken(user: UserInfo) {
|
||||
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
|
||||
expiresIn: '30d',
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyAccessToken(
|
||||
event: H3Event<EventHandlerRequest>,
|
||||
): null | Omit<UserInfo, 'password'> {
|
||||
const authHeader = getHeader(event, 'Authorization');
|
||||
if (!authHeader?.startsWith('Bearer')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
|
||||
|
||||
const username = decoded.username;
|
||||
const user = MOCK_USERS.find((item) => item.username === username);
|
||||
const { password: _pwd, ...userinfo } = user;
|
||||
return userinfo;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyRefreshToken(
|
||||
token: string,
|
||||
): null | Omit<UserInfo, 'password'> {
|
||||
try {
|
||||
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
|
||||
const username = decoded.username;
|
||||
const user = MOCK_USERS.find((item) => item.username === username);
|
||||
const { password: _pwd, ...userinfo } = user;
|
||||
return userinfo;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
390
web/apps/backend-mock/utils/mock-data.ts
Normal file
390
web/apps/backend-mock/utils/mock-data.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
password: string;
|
||||
realName: string;
|
||||
roles: string[];
|
||||
username: string;
|
||||
homePath?: string;
|
||||
}
|
||||
|
||||
export const MOCK_USERS: UserInfo[] = [
|
||||
{
|
||||
id: 0,
|
||||
password: '123456',
|
||||
realName: 'Vben',
|
||||
roles: ['super'],
|
||||
username: 'vben',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
password: '123456',
|
||||
realName: 'Admin',
|
||||
roles: ['admin'],
|
||||
username: 'admin',
|
||||
homePath: '/workspace',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
password: '123456',
|
||||
realName: 'Jack',
|
||||
roles: ['user'],
|
||||
username: 'jack',
|
||||
homePath: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_CODES = [
|
||||
// super
|
||||
{
|
||||
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
|
||||
username: 'vben',
|
||||
},
|
||||
{
|
||||
// admin
|
||||
codes: ['AC_100010', 'AC_100020', 'AC_100030'],
|
||||
username: 'admin',
|
||||
},
|
||||
{
|
||||
// user
|
||||
codes: ['AC_1000001', 'AC_1000002'],
|
||||
username: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
const dashboardMenus = [
|
||||
{
|
||||
meta: {
|
||||
order: -1,
|
||||
title: 'page.dashboard.title',
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
redirect: '/analytics',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
path: '/analytics',
|
||||
component: '/dashboard/analytics/index',
|
||||
meta: {
|
||||
affixTab: true,
|
||||
title: 'page.dashboard.analytics',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
path: '/workspace',
|
||||
component: '/dashboard/workspace/index',
|
||||
meta: {
|
||||
title: 'page.dashboard.workspace',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
||||
const roleWithMenus = {
|
||||
admin: {
|
||||
component: '/demos/access/admin-visible',
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: 'demos.access.adminVisible',
|
||||
},
|
||||
name: 'AccessAdminVisibleDemo',
|
||||
path: '/demos/access/admin-visible',
|
||||
},
|
||||
super: {
|
||||
component: '/demos/access/super-visible',
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: 'demos.access.superVisible',
|
||||
},
|
||||
name: 'AccessSuperVisibleDemo',
|
||||
path: '/demos/access/super-visible',
|
||||
},
|
||||
user: {
|
||||
component: '/demos/access/user-visible',
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: 'demos.access.userVisible',
|
||||
},
|
||||
name: 'AccessUserVisibleDemo',
|
||||
path: '/demos/access/user-visible',
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: 'demos.title',
|
||||
},
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
redirect: '/demos/access',
|
||||
children: [
|
||||
{
|
||||
name: 'AccessDemos',
|
||||
path: '/demosaccess',
|
||||
meta: {
|
||||
icon: 'mdi:cloud-key-outline',
|
||||
title: 'demos.access.backendPermissions',
|
||||
},
|
||||
redirect: '/demos/access/page-control',
|
||||
children: [
|
||||
{
|
||||
name: 'AccessPageControlDemo',
|
||||
path: '/demos/access/page-control',
|
||||
component: '/demos/access/index',
|
||||
meta: {
|
||||
icon: 'mdi:page-previous-outline',
|
||||
title: 'demos.access.pageAccess',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessButtonControlDemo',
|
||||
path: '/demos/access/button-control',
|
||||
component: '/demos/access/button-control',
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: 'demos.access.buttonControl',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessMenuVisible403Demo',
|
||||
path: '/demos/access/menu-visible-403',
|
||||
component: '/demos/access/menu-visible-403',
|
||||
meta: {
|
||||
authority: ['no-body'],
|
||||
icon: 'mdi:button-cursor',
|
||||
menuVisibleWithForbidden: true,
|
||||
title: 'demos.access.menuVisible403',
|
||||
},
|
||||
},
|
||||
roleWithMenus[role],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const MOCK_MENUS = [
|
||||
{
|
||||
menus: [...dashboardMenus, ...createDemosMenus('super')],
|
||||
username: 'vben',
|
||||
},
|
||||
{
|
||||
menus: [...dashboardMenus, ...createDemosMenus('admin')],
|
||||
username: 'admin',
|
||||
},
|
||||
{
|
||||
menus: [...dashboardMenus, ...createDemosMenus('user')],
|
||||
username: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_MENU_LIST = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Workspace',
|
||||
status: 1,
|
||||
type: 'menu',
|
||||
icon: 'mdi:dashboard',
|
||||
path: '/workspace',
|
||||
component: '/dashboard/workspace/index',
|
||||
meta: {
|
||||
icon: 'carbon:workspace',
|
||||
title: 'page.dashboard.workspace',
|
||||
affixTab: true,
|
||||
order: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
meta: {
|
||||
icon: 'carbon:settings',
|
||||
order: 9997,
|
||||
title: 'system.title',
|
||||
badge: 'new',
|
||||
badgeType: 'normal',
|
||||
badgeVariants: 'primary',
|
||||
},
|
||||
status: 1,
|
||||
type: 'catalog',
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
children: [
|
||||
{
|
||||
id: 201,
|
||||
pid: 2,
|
||||
path: '/system/menu',
|
||||
name: 'SystemMenu',
|
||||
authCode: 'System:Menu:List',
|
||||
status: 1,
|
||||
type: 'menu',
|
||||
meta: {
|
||||
icon: 'carbon:menu',
|
||||
title: 'system.menu.title',
|
||||
},
|
||||
component: '/system/menu/list',
|
||||
children: [
|
||||
{
|
||||
id: 20_101,
|
||||
pid: 201,
|
||||
name: 'SystemMenuCreate',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Menu:Create',
|
||||
meta: { title: 'common.create' },
|
||||
},
|
||||
{
|
||||
id: 20_102,
|
||||
pid: 201,
|
||||
name: 'SystemMenuEdit',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Menu:Edit',
|
||||
meta: { title: 'common.edit' },
|
||||
},
|
||||
{
|
||||
id: 20_103,
|
||||
pid: 201,
|
||||
name: 'SystemMenuDelete',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Menu:Delete',
|
||||
meta: { title: 'common.delete' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
pid: 2,
|
||||
path: '/system/dept',
|
||||
name: 'SystemDept',
|
||||
status: 1,
|
||||
type: 'menu',
|
||||
authCode: 'System:Dept:List',
|
||||
meta: {
|
||||
icon: 'carbon:container-services',
|
||||
title: 'system.dept.title',
|
||||
},
|
||||
component: '/system/dept/list',
|
||||
children: [
|
||||
{
|
||||
id: 20_401,
|
||||
pid: 201,
|
||||
name: 'SystemDeptCreate',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Dept:Create',
|
||||
meta: { title: 'common.create' },
|
||||
},
|
||||
{
|
||||
id: 20_402,
|
||||
pid: 201,
|
||||
name: 'SystemDeptEdit',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Dept:Edit',
|
||||
meta: { title: 'common.edit' },
|
||||
},
|
||||
{
|
||||
id: 20_403,
|
||||
pid: 201,
|
||||
name: 'SystemDeptDelete',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Dept:Delete',
|
||||
meta: { title: 'common.delete' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
order: 9998,
|
||||
title: 'demos.vben.title',
|
||||
icon: 'carbon:data-center',
|
||||
},
|
||||
name: 'Project',
|
||||
path: '/vben-admin',
|
||||
type: 'catalog',
|
||||
status: 1,
|
||||
children: [
|
||||
{
|
||||
id: 901,
|
||||
pid: 9,
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
component: 'IFrameView',
|
||||
type: 'embedded',
|
||||
status: 1,
|
||||
meta: {
|
||||
icon: 'carbon:book',
|
||||
iframeSrc: 'https://doc.vben.pro',
|
||||
title: 'demos.vben.document',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 902,
|
||||
pid: 9,
|
||||
name: 'VbenGithub',
|
||||
path: '/vben-admin/github',
|
||||
component: 'IFrameView',
|
||||
type: 'link',
|
||||
status: 1,
|
||||
meta: {
|
||||
icon: 'carbon:logo-github',
|
||||
link: 'https://github.com/vbenjs/vue-vben-admin',
|
||||
title: 'Github',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 903,
|
||||
pid: 9,
|
||||
name: 'VbenAntdv',
|
||||
path: '/vben-admin/antdv',
|
||||
component: 'IFrameView',
|
||||
type: 'link',
|
||||
status: 0,
|
||||
meta: {
|
||||
icon: 'carbon:hexagon-vertical-solid',
|
||||
badgeType: 'dot',
|
||||
link: 'https://ant.vben.pro',
|
||||
title: 'demos.vben.antdv',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
component: '_core/about/index',
|
||||
type: 'menu',
|
||||
status: 1,
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
order: 9999,
|
||||
title: 'demos.vben.about',
|
||||
},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
},
|
||||
];
|
||||
|
||||
export function getMenuIds(menus: any[]) {
|
||||
const ids: number[] = [];
|
||||
menus.forEach((item) => {
|
||||
ids.push(item.id);
|
||||
if (item.children && item.children.length > 0) {
|
||||
ids.push(...getMenuIds(item.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
68
web/apps/backend-mock/utils/response.ts
Normal file
68
web/apps/backend-mock/utils/response.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
export function useResponseSuccess<T = any>(data: T) {
|
||||
return {
|
||||
code: 0,
|
||||
data,
|
||||
error: null,
|
||||
message: 'ok',
|
||||
};
|
||||
}
|
||||
|
||||
export function usePageResponseSuccess<T = any>(
|
||||
page: number | string,
|
||||
pageSize: number | string,
|
||||
list: T[],
|
||||
{ message = 'ok' } = {},
|
||||
) {
|
||||
const pageData = pagination(
|
||||
Number.parseInt(`${page}`),
|
||||
Number.parseInt(`${pageSize}`),
|
||||
list,
|
||||
);
|
||||
|
||||
return {
|
||||
...useResponseSuccess({
|
||||
items: pageData,
|
||||
total: list.length,
|
||||
}),
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function useResponseError(message: string, error: any = null) {
|
||||
return {
|
||||
code: -1,
|
||||
data: null,
|
||||
error,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function forbiddenResponse(
|
||||
event: H3Event<EventHandlerRequest>,
|
||||
message = 'Forbidden Exception',
|
||||
) {
|
||||
setResponseStatus(event, 403);
|
||||
return useResponseError(message, message);
|
||||
}
|
||||
|
||||
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
|
||||
setResponseStatus(event, 401);
|
||||
return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
|
||||
}
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function pagination<T = any>(
|
||||
pageNo: number,
|
||||
pageSize: number,
|
||||
array: T[],
|
||||
): T[] {
|
||||
const offset = (pageNo - 1) * Number(pageSize);
|
||||
return offset + Number(pageSize) >= array.length
|
||||
? array.slice(offset)
|
||||
: array.slice(offset, offset + Number(pageSize));
|
||||
}
|
||||
7
web/apps/web-antd/.env.analyze
Normal file
7
web/apps/web-antd/.env.analyze
Normal file
@@ -0,0 +1,7 @@
|
||||
# public path
|
||||
VITE_BASE=/
|
||||
|
||||
# Basic interface address SPA
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
VITE_VISUALIZER=true
|
||||
16
web/apps/web-antd/.env.development
Normal file
16
web/apps/web-antd/.env.development
Normal file
@@ -0,0 +1,16 @@
|
||||
# 端口号
|
||||
VITE_PORT=5678
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=http://127.0.0.1:8000/api
|
||||
|
||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||
VITE_NITRO_MOCK=false
|
||||
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
19
web/apps/web-antd/.env.production
Normal file
19
web/apps/web-antd/.env.production
Normal file
@@ -0,0 +1,19 @@
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=http://127.0.0.1:8000/api
|
||||
|
||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||
VITE_COMPRESS=gzip
|
||||
|
||||
# 是否开启 PWA
|
||||
VITE_PWA=false
|
||||
|
||||
# vue-router 的模式
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
35
web/apps/web-antd/index.html
Normal file
35
web/apps/web-antd/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta name="description" content="A Modern Back-end Management System" />
|
||||
<meta name="keywords" content="Vben Admin Vue3 Vite" />
|
||||
<meta name="author" content="Vben" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||
/>
|
||||
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||
<title><%= VITE_APP_TITLE %></title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script>
|
||||
// 生产环境下注入百度统计
|
||||
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement('script');
|
||||
hm.src =
|
||||
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
51
web/apps/web-antd/package.json
Normal file
51
web/apps/web-antd/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.5.7",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "apps/web-antd"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "vben",
|
||||
"email": "ann.vben@gmail.com",
|
||||
"url": "https://github.com/anncwb"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build --mode production",
|
||||
"build:analyze": "pnpm vite build --mode analyze",
|
||||
"dev": "pnpm vite --mode development",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||
},
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/constants": "workspace:*",
|
||||
"@vben/hooks": "workspace:*",
|
||||
"@vben/icons": "workspace:*",
|
||||
"@vben/layouts": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/plugins": "workspace:*",
|
||||
"@vben/preferences": "workspace:*",
|
||||
"@vben/request": "workspace:*",
|
||||
"@vben/stores": "workspace:*",
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"@vben-core/menu-ui": "workspace:*",
|
||||
"ant-design-vue": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
}
|
||||
}
|
||||
1
web/apps/web-antd/postcss.config.mjs
Normal file
1
web/apps/web-antd/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
BIN
web/apps/web-antd/public/favicon.ico
Normal file
BIN
web/apps/web-antd/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
218
web/apps/web-antd/src/adapter/component/index.ts
Normal file
218
web/apps/web-antd/src/adapter/component/index.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
const AutoComplete = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/auto-complete'),
|
||||
);
|
||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||
const Checkbox = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/checkbox'),
|
||||
);
|
||||
const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
const DatePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/date-picker'),
|
||||
);
|
||||
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
|
||||
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
|
||||
const InputNumber = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/input-number'),
|
||||
);
|
||||
const InputPassword = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.InputPassword),
|
||||
);
|
||||
const Mentions = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/mentions'),
|
||||
);
|
||||
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
|
||||
const RadioGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
|
||||
);
|
||||
const RangePicker = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
|
||||
);
|
||||
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
|
||||
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
|
||||
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
|
||||
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
|
||||
const Textarea = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.Textarea),
|
||||
);
|
||||
const TimePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/time-picker'),
|
||||
);
|
||||
const TreeSelect = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/tree-select'),
|
||||
);
|
||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder =
|
||||
props?.placeholder ||
|
||||
attrs?.placeholder ||
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
| 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'addonAfter',
|
||||
inputComponent: Input,
|
||||
modelValueProp: 'value',
|
||||
}),
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
description: content,
|
||||
message: title,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
49
web/apps/web-antd/src/adapter/form.ts
Normal file
49
web/apps/web-antd/src/adapter/form.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type {
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
75
web/apps/web-antd/src/adapter/vxe-table.ts
Normal file
75
web/apps/web-antd/src/adapter/vxe-table.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
minHeight: 180,
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
export type OnActionClickParams<T = Recordable<any>> = {
|
||||
code: string;
|
||||
row: T;
|
||||
};
|
||||
export type OnActionClickFn<T = Recordable<any>> = (
|
||||
params: OnActionClickParams<T>,
|
||||
) => void;
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
51
web/apps/web-antd/src/api/core/auth.ts
Normal file
51
web/apps/web-antd/src/api/core/auth.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
password?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/system/login/', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新accessToken
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return baseRequestClient.post('/system/logout/', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
export async function getAccessCodesApi() {
|
||||
return requestClient.get<string[]>('/system/codes/');
|
||||
}
|
||||
3
web/apps/web-antd/src/api/core/index.ts
Normal file
3
web/apps/web-antd/src/api/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
10
web/apps/web-antd/src/api/core/menu.ts
Normal file
10
web/apps/web-antd/src/api/core/menu.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||
}
|
||||
10
web/apps/web-antd/src/api/core/user.ts
Normal file
10
web/apps/web-antd/src/api/core/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { UserInfo } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/system/info/');
|
||||
}
|
||||
1
web/apps/web-antd/src/api/index.ts
Normal file
1
web/apps/web-antd/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './core';
|
||||
113
web/apps/web-antd/src/api/request.ts
Normal file
113
web/apps/web-antd/src/api/request.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
/**
|
||||
* 重新认证逻辑
|
||||
*/
|
||||
async function doReAuthenticate() {
|
||||
console.warn('Access token or refresh token is invalid or expired. ');
|
||||
const accessStore = useAccessStore();
|
||||
const authStore = useAuthStore();
|
||||
accessStore.setAccessToken(null);
|
||||
if (
|
||||
preferences.app.loginExpiredMode === 'modal' &&
|
||||
accessStore.isAccessChecked
|
||||
) {
|
||||
accessStore.setLoginExpired(true);
|
||||
} else {
|
||||
await authStore.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token逻辑
|
||||
*/
|
||||
async function doRefreshToken() {
|
||||
const accessStore = useAccessStore();
|
||||
const resp = await refreshTokenApi();
|
||||
const newToken = resp.data;
|
||||
accessStore.setAccessToken(newToken);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
function formatToken(token: null | string) {
|
||||
return token ? `Bearer ${token}` : null;
|
||||
}
|
||||
|
||||
// 请求头处理
|
||||
client.addRequestInterceptor({
|
||||
fulfilled: async (config) => {
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
config.headers.Authorization = formatToken(accessStore.accessToken);
|
||||
config.headers['Accept-Language'] = preferences.app.locale;
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
codeField: 'code',
|
||||
dataField: 'data',
|
||||
successCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// token过期的处理
|
||||
client.addResponseInterceptor(
|
||||
authenticateResponseInterceptor({
|
||||
client,
|
||||
doReAuthenticate,
|
||||
doRefreshToken,
|
||||
enableRefreshToken: preferences.app.enableRefreshToken,
|
||||
formatToken,
|
||||
}),
|
||||
);
|
||||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string, error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
// 当前mock接口返回的错误字段是 error 或者 message
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
||||
// 如果没有错误信息,则会根据状态码进行提示
|
||||
message.error(errorMessage || msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
52
web/apps/web-antd/src/api/system/dept.ts
Normal file
52
web/apps/web-antd/src/api/system/dept.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemDeptApi {
|
||||
export interface SystemDept {
|
||||
[key: string]: any;
|
||||
children?: SystemDept[];
|
||||
id: string;
|
||||
name: string;
|
||||
remark?: string;
|
||||
status: 0 | 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门列表数据
|
||||
*/
|
||||
async function getDeptList() {
|
||||
return requestClient.get<Array<SystemDeptApi.SystemDept>>('/system/dept/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建部门
|
||||
* @param data 部门数据
|
||||
*/
|
||||
async function createDept(
|
||||
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
|
||||
) {
|
||||
return requestClient.post('/system/dept/', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新部门
|
||||
*
|
||||
* @param id 部门 ID
|
||||
* @param data 部门数据
|
||||
*/
|
||||
async function updateDept(
|
||||
id: string,
|
||||
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
|
||||
) {
|
||||
return requestClient.put(`/system/dept/${id}/`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @param id 部门 ID
|
||||
*/
|
||||
async function deleteDept(id: string) {
|
||||
return requestClient.delete(`/system/dept/${id}/`);
|
||||
}
|
||||
|
||||
export { createDept, deleteDept, getDeptList, updateDept };
|
||||
74
web/apps/web-antd/src/api/system/dict_data.ts
Normal file
74
web/apps/web-antd/src/api/system/dict_data.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemDictDataApi {
|
||||
export interface SystemDictData {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据列表数据
|
||||
*/
|
||||
async function getDictDataList(params: Recordable<any>) {
|
||||
return requestClient.get<Array<SystemDictDataApi.SystemDictData>>(
|
||||
'/system/dict_data/',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建字典数据
|
||||
* @param data 字典数据数据
|
||||
*/
|
||||
async function createDictData(
|
||||
data: Omit<SystemDictDataApi.SystemDictData, 'id'>,
|
||||
) {
|
||||
return requestClient.post('/system/dict_data/', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典数据
|
||||
*
|
||||
* @param id 字典数据 ID
|
||||
* @param data 字典数据数据
|
||||
*/
|
||||
async function updateDictData(
|
||||
id: string,
|
||||
data: Omit<SystemDictDataApi.SystemDictData, 'id'>,
|
||||
) {
|
||||
return requestClient.put(`/system/dict_data/${id}/`, data);
|
||||
}
|
||||
/**
|
||||
* 更新字典数据
|
||||
*
|
||||
* @param id 字典数据 ID
|
||||
* @param data 字典数据数据
|
||||
*/
|
||||
async function patchDictData(
|
||||
id: string,
|
||||
data: Omit<SystemDictDataApi.SystemDictData, 'id'>,
|
||||
) {
|
||||
return requestClient.patch(`/system/dict_data/${id}/`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典数据
|
||||
* @param id 字典数据 ID
|
||||
*/
|
||||
async function deleteDictData(id: string) {
|
||||
return requestClient.delete(`/system/dict_data/${id}/`);
|
||||
}
|
||||
|
||||
export {
|
||||
createDictData,
|
||||
deleteDictData,
|
||||
getDictDataList,
|
||||
patchDictData,
|
||||
updateDictData,
|
||||
};
|
||||
75
web/apps/web-antd/src/api/system/dict_type.ts
Normal file
75
web/apps/web-antd/src/api/system/dict_type.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemDictTypeApi {
|
||||
export interface SystemDictType {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典类型列表数据
|
||||
*/
|
||||
async function getDictTypeList(params: Recordable<any>) {
|
||||
return requestClient.get<Array<SystemDictTypeApi.SystemDictType>>(
|
||||
'/system/dict_type/',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建字典类型
|
||||
* @param data 字典类型数据
|
||||
*/
|
||||
async function createDictType(
|
||||
data: Omit<SystemDictTypeApi.SystemDictType, 'id'>,
|
||||
) {
|
||||
return requestClient.post('/system/dict_type/', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典类型
|
||||
*
|
||||
* @param id 字典类型 ID
|
||||
* @param data 字典类型数据
|
||||
*/
|
||||
async function updateDictType(
|
||||
id: string,
|
||||
data: Omit<SystemDictTypeApi.SystemDictType, 'id'>,
|
||||
) {
|
||||
return requestClient.put(`/system/dict_type/${id}/`, data);
|
||||
}
|
||||
/**
|
||||
* 更新字典类型
|
||||
*
|
||||
* @param id 字典类型 ID
|
||||
* @param data 字典类型数据
|
||||
*/
|
||||
async function patchDictType(
|
||||
id: string,
|
||||
data: Omit<SystemDictTypeApi.SystemDictType, 'id'>,
|
||||
) {
|
||||
return requestClient.patch(`/system/dict_type/${id}/`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典类型
|
||||
* @param id 字典类型 ID
|
||||
*/
|
||||
async function deleteDictType(id: string) {
|
||||
return requestClient.delete(`/system/dict_type/${id}/`);
|
||||
}
|
||||
|
||||
export {
|
||||
createDictType,
|
||||
deleteDictType,
|
||||
getDictTypeList,
|
||||
patchDictType,
|
||||
updateDictType,
|
||||
};
|
||||
3
web/apps/web-antd/src/api/system/index.ts
Normal file
3
web/apps/web-antd/src/api/system/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './dept';
|
||||
export * from './menu';
|
||||
export * from './role';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user