commit f6e68e37c8e9b9d429883ae984766dbe06d7562d
Author: xie7654 <765462425@qq.com>
Date: Sun Jun 29 21:45:27 2025 +0800
init
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a16fc91
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..46a6572
--- /dev/null
+++ b/LICENSE
@@ -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.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py
new file mode 100644
index 0000000..62c4f9a
--- /dev/null
+++ b/backend/backend/__init__.py
@@ -0,0 +1,4 @@
+
+from .celery import app as celery_app
+
+__all__ = ('celery_app',)
diff --git a/backend/backend/asgi.py b/backend/backend/asgi.py
new file mode 100644
index 0000000..6aa1b52
--- /dev/null
+++ b/backend/backend/asgi.py
@@ -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()
diff --git a/backend/backend/celery.py b/backend/backend/celery.py
new file mode 100644
index 0000000..8b958a0
--- /dev/null
+++ b/backend/backend/celery.py
@@ -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'
\ No newline at end of file
diff --git a/backend/backend/settings.py b/backend/backend/settings.py
new file mode 100644
index 0000000..51c1e87
--- /dev/null
+++ b/backend/backend/settings.py
@@ -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 *
\ No newline at end of file
diff --git a/backend/backend/urls.py b/backend/backend/urls.py
new file mode 100644
index 0000000..99291cf
--- /dev/null
+++ b/backend/backend/urls.py
@@ -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')),
+]
diff --git a/backend/backend/wsgi.py b/backend/backend/wsgi.py
new file mode 100644
index 0000000..ce5c079
--- /dev/null
+++ b/backend/backend/wsgi.py
@@ -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()
diff --git a/backend/manage.py b/backend/manage.py
new file mode 100755
index 0000000..eb6431e
--- /dev/null
+++ b/backend/manage.py
@@ -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()
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000..6ac4be8
--- /dev/null
+++ b/backend/requirements.txt
@@ -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
\ No newline at end of file
diff --git a/backend/system/__init__.py b/backend/system/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/system/admin.py b/backend/system/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/backend/system/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/backend/system/apps.py b/backend/system/apps.py
new file mode 100644
index 0000000..a2d131d
--- /dev/null
+++ b/backend/system/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class SystemConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'system'
diff --git a/backend/system/migrations/0001_initial.py b/backend/system/migrations/0001_initial.py
new file mode 100644
index 0000000..bdf51a3
--- /dev/null
+++ b/backend/system/migrations/0001_initial.py
@@ -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="关联权限",
+ ),
+ ),
+ ]
diff --git a/backend/system/migrations/__init__.py b/backend/system/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/system/models.py b/backend/system/models.py
new file mode 100644
index 0000000..e98a07b
--- /dev/null
+++ b/backend/system/models.py
@@ -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'
diff --git a/backend/system/serializers.py b/backend/system/serializers.py
new file mode 100644
index 0000000..03c6d3d
--- /dev/null
+++ b/backend/system/serializers.py
@@ -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
+
diff --git a/backend/system/tasks.py b/backend/system/tasks.py
new file mode 100644
index 0000000..c642f91
--- /dev/null
+++ b/backend/system/tasks.py
@@ -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
\ No newline at end of file
diff --git a/backend/system/tests.py b/backend/system/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/backend/system/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/backend/system/urls.py b/backend/system/urls.py
new file mode 100644
index 0000000..7ae2f3c
--- /dev/null
+++ b/backend/system/urls.py
@@ -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()),
+]
\ No newline at end of file
diff --git a/backend/system/views/__init__.py b/backend/system/views/__init__.py
new file mode 100644
index 0000000..f079feb
--- /dev/null
+++ b/backend/system/views/__init__.py
@@ -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 *
\ No newline at end of file
diff --git a/backend/system/views/dept.py b/backend/system/views/dept.py
new file mode 100644
index 0000000..66ee4b0
--- /dev/null
+++ b/backend/system/views/dept.py
@@ -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)
+
diff --git a/backend/system/views/dict_data.py b/backend/system/views/dict_data.py
new file mode 100644
index 0000000..7355068
--- /dev/null
+++ b/backend/system/views/dict_data.py
@@ -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']
\ No newline at end of file
diff --git a/backend/system/views/dict_type.py b/backend/system/views/dict_type.py
new file mode 100644
index 0000000..d4f2760
--- /dev/null
+++ b/backend/system/views/dict_type.py
@@ -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
\ No newline at end of file
diff --git a/backend/system/views/menu.py b/backend/system/views/menu.py
new file mode 100644
index 0000000..03c38ca
--- /dev/null
+++ b/backend/system/views/menu.py
@@ -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,
+ )
diff --git a/backend/system/views/role.py b/backend/system/views/role.py
new file mode 100644
index 0000000..64bd4cc
--- /dev/null
+++ b/backend/system/views/role.py
@@ -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,
+ )
diff --git a/backend/system/views/user.py b/backend/system/views/user.py
new file mode 100644
index 0000000..f7f6409
--- /dev/null
+++ b/backend/system/views/user.py
@@ -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
\ No newline at end of file
diff --git a/backend/utils/authentication.py b/backend/utils/authentication.py
new file mode 100644
index 0000000..095ce9a
--- /dev/null
+++ b/backend/utils/authentication.py
@@ -0,0 +1,7 @@
+from rest_framework.authentication import TokenAuthentication
+
+class BearerTokenAuthentication(TokenAuthentication):
+ """
+ 使用 'Bearer' 前缀的 Token 认证
+ """
+ keyword = 'Bearer'
\ No newline at end of file
diff --git a/backend/utils/custom_model_viewSet.py b/backend/utils/custom_model_viewSet.py
new file mode 100644
index 0000000..4911547
--- /dev/null
+++ b/backend/utils/custom_model_viewSet.py
@@ -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"
+ )
diff --git a/backend/utils/models.py b/backend/utils/models.py
new file mode 100644
index 0000000..b7c01aa
--- /dev/null
+++ b/backend/utils/models.py
@@ -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
diff --git a/backend/utils/pagination.py b/backend/utils/pagination.py
new file mode 100644
index 0000000..2dbad86
--- /dev/null
+++ b/backend/utils/pagination.py
@@ -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),
+ ]))
diff --git a/backend/utils/permissions.py b/backend/utils/permissions.py
new file mode 100644
index 0000000..93fe57e
--- /dev/null
+++ b/backend/utils/permissions.py
@@ -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
diff --git a/backend/utils/serializers.py b/backend/utils/serializers.py
new file mode 100644
index 0000000..02738b1
--- /dev/null
+++ b/backend/utils/serializers.py
@@ -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
diff --git a/backend/utils/utils.py b/backend/utils/utils.py
new file mode 100644
index 0000000..1a838fe
--- /dev/null
+++ b/backend/utils/utils.py
@@ -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
\ No newline at end of file
diff --git a/web/.browserslistrc b/web/.browserslistrc
new file mode 100644
index 0000000..dc3bc09
--- /dev/null
+++ b/web/.browserslistrc
@@ -0,0 +1,4 @@
+> 1%
+last 2 versions
+not dead
+not ie 11
diff --git a/web/.commitlintrc.js b/web/.commitlintrc.js
new file mode 100644
index 0000000..02e33fa
--- /dev/null
+++ b/web/.commitlintrc.js
@@ -0,0 +1 @@
+export { default } from '@vben/commitlint-config';
diff --git a/web/.dockerignore b/web/.dockerignore
new file mode 100644
index 0000000..52b833a
--- /dev/null
+++ b/web/.dockerignore
@@ -0,0 +1,7 @@
+node_modules
+.git
+.gitignore
+*.md
+dist
+.turbo
+dist.zip
diff --git a/web/.editorconfig b/web/.editorconfig
new file mode 100644
index 0000000..179aec6
--- /dev/null
+++ b/web/.editorconfig
@@ -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
diff --git a/web/.node-version b/web/.node-version
new file mode 100644
index 0000000..ee5c244
--- /dev/null
+++ b/web/.node-version
@@ -0,0 +1 @@
+22.1.0
diff --git a/web/.npmrc b/web/.npmrc
new file mode 100644
index 0000000..21147af
--- /dev/null
+++ b/web/.npmrc
@@ -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
diff --git a/web/.prettierignore b/web/.prettierignore
new file mode 100644
index 0000000..d0b0ca1
--- /dev/null
+++ b/web/.prettierignore
@@ -0,0 +1,18 @@
+dist
+dev-dist
+.local
+.output.js
+node_modules
+.nvmrc
+coverage
+CODEOWNERS
+.nitro
+.output
+
+
+**/*.svg
+**/*.sh
+
+public
+.npmrc
+*-lock.yaml
diff --git a/web/.prettierrc.mjs b/web/.prettierrc.mjs
new file mode 100644
index 0000000..3e25d2c
--- /dev/null
+++ b/web/.prettierrc.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/prettier-config';
diff --git a/web/.stylelintignore b/web/.stylelintignore
new file mode 100644
index 0000000..f4b2db2
--- /dev/null
+++ b/web/.stylelintignore
@@ -0,0 +1,4 @@
+dist
+public
+__tests__
+coverage
diff --git a/web/LICENSE b/web/LICENSE
new file mode 100644
index 0000000..cec5b42
--- /dev/null
+++ b/web/LICENSE
@@ -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.
diff --git a/web/README.ja-JP.md b/web/README.ja-JP.md
new file mode 100644
index 0000000..f7847a1
--- /dev/null
+++ b/web/README.ja-JP.md
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+[](LICENSE)
+
+
Vue Vben Admin
+
+
+[](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
+
+
+
+### 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はサポートしません
+
+| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
+| :-: | :-: | :-: | :-: |
+| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
+
+## メンテナー
+
+[@Vben](https://github.com/anncwb)
+
+## スター歴史
+
+[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
+
+## 寄付
+
+このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
+
+
+
+Paypal Me
+
+## 貢献者
+
+
+
+
+
+## Discord
+
+- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
+
+## ライセンス
+
+[MIT © Vben-2020](./LICENSE)
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..e027949
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+[](LICENSE)
+
+
Vue Vben Admin
+
+
+[](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
+
+
+
+### 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
+
+| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)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!
+
+
+
+Paypal Me
+
+## Contributors
+
+
+
+
+
+## Discord
+
+- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
+
+## License
+
+[MIT © Vben-2020](./LICENSE)
diff --git a/web/README.zh-CN.md b/web/README.zh-CN.md
new file mode 100644
index 0000000..5a6b191
--- /dev/null
+++ b/web/README.zh-CN.md
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+[](LICENSE)
+
+
Vue Vben Admin
+
+
+[](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
+
+
+
+### 使用 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
+
+| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)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)
+
+## 捐赠
+
+如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
+
+
+
+Paypal Me
+
+## 贡献者
+
+
+
+
+
+## Discord
+
+- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
+
+## 许可证
+
+[MIT © Vben-2020](./LICENSE)
diff --git a/web/apps/backend-mock/README.md b/web/apps/backend-mock/README.md
new file mode 100644
index 0000000..401bda7
--- /dev/null
+++ b/web/apps/backend-mock/README.md
@@ -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
+```
diff --git a/web/apps/backend-mock/api/auth/codes.ts b/web/apps/backend-mock/api/auth/codes.ts
new file mode 100644
index 0000000..7ba0127
--- /dev/null
+++ b/web/apps/backend-mock/api/auth/codes.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/api/auth/login.post.ts b/web/apps/backend-mock/api/auth/login.post.ts
new file mode 100644
index 0000000..df5737a
--- /dev/null
+++ b/web/apps/backend-mock/api/auth/login.post.ts
@@ -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,
+ });
+});
diff --git a/web/apps/backend-mock/api/auth/logout.post.ts b/web/apps/backend-mock/api/auth/logout.post.ts
new file mode 100644
index 0000000..ac6afe9
--- /dev/null
+++ b/web/apps/backend-mock/api/auth/logout.post.ts
@@ -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('');
+});
diff --git a/web/apps/backend-mock/api/auth/refresh.post.ts b/web/apps/backend-mock/api/auth/refresh.post.ts
new file mode 100644
index 0000000..7df4d34
--- /dev/null
+++ b/web/apps/backend-mock/api/auth/refresh.post.ts
@@ -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;
+});
diff --git a/web/apps/backend-mock/api/demo/bigint.ts b/web/apps/backend-mock/api/demo/bigint.ts
new file mode 100644
index 0000000..880cc5e
--- /dev/null
+++ b/web/apps/backend-mock/api/demo/bigint.ts
@@ -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;
+});
diff --git a/web/apps/backend-mock/api/menu/all.ts b/web/apps/backend-mock/api/menu/all.ts
new file mode 100644
index 0000000..580cee4
--- /dev/null
+++ b/web/apps/backend-mock/api/menu/all.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/api/status.ts b/web/apps/backend-mock/api/status.ts
new file mode 100644
index 0000000..41773e1
--- /dev/null
+++ b/web/apps/backend-mock/api/status.ts
@@ -0,0 +1,5 @@
+export default eventHandler((event) => {
+ const { status } = getQuery(event);
+ setResponseStatus(event, Number(status));
+ return useResponseError(`${status}`);
+});
diff --git a/web/apps/backend-mock/api/system/dept/.post.ts b/web/apps/backend-mock/api/system/dept/.post.ts
new file mode 100644
index 0000000..c529ea1
--- /dev/null
+++ b/web/apps/backend-mock/api/system/dept/.post.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/api/system/dept/[id].delete.ts b/web/apps/backend-mock/api/system/dept/[id].delete.ts
new file mode 100644
index 0000000..e48f051
--- /dev/null
+++ b/web/apps/backend-mock/api/system/dept/[id].delete.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/api/system/dept/[id].put.ts b/web/apps/backend-mock/api/system/dept/[id].put.ts
new file mode 100644
index 0000000..aa55c08
--- /dev/null
+++ b/web/apps/backend-mock/api/system/dept/[id].put.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/api/system/dept/list.ts b/web/apps/backend-mock/api/system/dept/list.ts
new file mode 100644
index 0000000..ae819b6
--- /dev/null
+++ b/web/apps/backend-mock/api/system/dept/list.ts
@@ -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 = {
+ 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);
+});
diff --git a/web/apps/backend-mock/api/system/menu/list.ts b/web/apps/backend-mock/api/system/menu/list.ts
new file mode 100644
index 0000000..5328b2f
--- /dev/null
+++ b/web/apps/backend-mock/api/system/menu/list.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/api/system/menu/name-exists.ts b/web/apps/backend-mock/api/system/menu/name-exists.ts
new file mode 100644
index 0000000..5599c22
--- /dev/null
+++ b/web/apps/backend-mock/api/system/menu/name-exists.ts
@@ -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 = {};
+
+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);
+});
diff --git a/web/apps/backend-mock/api/system/menu/path-exists.ts b/web/apps/backend-mock/api/system/menu/path-exists.ts
new file mode 100644
index 0000000..64774f7
--- /dev/null
+++ b/web/apps/backend-mock/api/system/menu/path-exists.ts
@@ -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 = { '/': 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);
+});
diff --git a/web/apps/backend-mock/api/system/role/list.ts b/web/apps/backend-mock/api/system/role/list.ts
new file mode 100644
index 0000000..4d5f923
--- /dev/null
+++ b/web/apps/backend-mock/api/system/role/list.ts
@@ -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 = {
+ 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);
+});
diff --git a/web/apps/backend-mock/api/table/list.ts b/web/apps/backend-mock/api/table/list.ts
new file mode 100644
index 0000000..3e6f705
--- /dev/null
+++ b/web/apps/backend-mock/api/table/list.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/api/test.get.ts b/web/apps/backend-mock/api/test.get.ts
new file mode 100644
index 0000000..ca4a500
--- /dev/null
+++ b/web/apps/backend-mock/api/test.get.ts
@@ -0,0 +1 @@
+export default defineEventHandler(() => 'Test get handler');
diff --git a/web/apps/backend-mock/api/test.post.ts b/web/apps/backend-mock/api/test.post.ts
new file mode 100644
index 0000000..698cf21
--- /dev/null
+++ b/web/apps/backend-mock/api/test.post.ts
@@ -0,0 +1 @@
+export default defineEventHandler(() => 'Test post handler');
diff --git a/web/apps/backend-mock/api/upload.ts b/web/apps/backend-mock/api/upload.ts
new file mode 100644
index 0000000..1bb9e60
--- /dev/null
+++ b/web/apps/backend-mock/api/upload.ts
@@ -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")
+});
diff --git a/web/apps/backend-mock/api/user/info.ts b/web/apps/backend-mock/api/user/info.ts
new file mode 100644
index 0000000..cfa2346
--- /dev/null
+++ b/web/apps/backend-mock/api/user/info.ts
@@ -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);
+});
diff --git a/web/apps/backend-mock/error.ts b/web/apps/backend-mock/error.ts
new file mode 100644
index 0000000..e20beac
--- /dev/null
+++ b/web/apps/backend-mock/error.ts
@@ -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;
diff --git a/web/apps/backend-mock/middleware/1.api.ts b/web/apps/backend-mock/middleware/1.api.ts
new file mode 100644
index 0000000..bad9a41
--- /dev/null
+++ b/web/apps/backend-mock/middleware/1.api.ts
@@ -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, '演示环境,禁止修改');
+ }
+});
diff --git a/web/apps/backend-mock/nitro.config.ts b/web/apps/backend-mock/nitro.config.ts
new file mode 100644
index 0000000..c0fc13e
--- /dev/null
+++ b/web/apps/backend-mock/nitro.config.ts
@@ -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': '*',
+ },
+ },
+ },
+});
diff --git a/web/apps/backend-mock/package.json b/web/apps/backend-mock/package.json
new file mode 100644
index 0000000..cc0b8d5
--- /dev/null
+++ b/web/apps/backend-mock/package.json
@@ -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:"
+ }
+}
diff --git a/web/apps/backend-mock/routes/[...].ts b/web/apps/backend-mock/routes/[...].ts
new file mode 100644
index 0000000..99f544b
--- /dev/null
+++ b/web/apps/backend-mock/routes/[...].ts
@@ -0,0 +1,13 @@
+export default defineEventHandler(() => {
+ return `
+Hello Vben Admin
+Mock service is starting
+
+`;
+});
diff --git a/web/apps/backend-mock/tsconfig.build.json b/web/apps/backend-mock/tsconfig.build.json
new file mode 100644
index 0000000..64f86c6
--- /dev/null
+++ b/web/apps/backend-mock/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
+}
diff --git a/web/apps/backend-mock/tsconfig.json b/web/apps/backend-mock/tsconfig.json
new file mode 100644
index 0000000..43008af
--- /dev/null
+++ b/web/apps/backend-mock/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "./.nitro/types/tsconfig.json"
+}
diff --git a/web/apps/backend-mock/utils/cookie-utils.ts b/web/apps/backend-mock/utils/cookie-utils.ts
new file mode 100644
index 0000000..78f3aab
--- /dev/null
+++ b/web/apps/backend-mock/utils/cookie-utils.ts
@@ -0,0 +1,26 @@
+import type { EventHandlerRequest, H3Event } from 'h3';
+
+export function clearRefreshTokenCookie(event: H3Event) {
+ deleteCookie(event, 'jwt', {
+ httpOnly: true,
+ sameSite: 'none',
+ secure: true,
+ });
+}
+
+export function setRefreshTokenCookie(
+ event: H3Event,
+ refreshToken: string,
+) {
+ setCookie(event, 'jwt', refreshToken, {
+ httpOnly: true,
+ maxAge: 24 * 60 * 60, // unit: seconds
+ sameSite: 'none',
+ secure: true,
+ });
+}
+
+export function getRefreshTokenFromCookie(event: H3Event) {
+ const refreshToken = getCookie(event, 'jwt');
+ return refreshToken;
+}
diff --git a/web/apps/backend-mock/utils/jwt-utils.ts b/web/apps/backend-mock/utils/jwt-utils.ts
new file mode 100644
index 0000000..8cfc684
--- /dev/null
+++ b/web/apps/backend-mock/utils/jwt-utils.ts
@@ -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,
+): null | Omit {
+ 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 {
+ 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;
+ }
+}
diff --git a/web/apps/backend-mock/utils/mock-data.ts b/web/apps/backend-mock/utils/mock-data.ts
new file mode 100644
index 0000000..192f30a
--- /dev/null
+++ b/web/apps/backend-mock/utils/mock-data.ts
@@ -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;
+}
diff --git a/web/apps/backend-mock/utils/response.ts b/web/apps/backend-mock/utils/response.ts
new file mode 100644
index 0000000..2a5a908
--- /dev/null
+++ b/web/apps/backend-mock/utils/response.ts
@@ -0,0 +1,68 @@
+import type { EventHandlerRequest, H3Event } from 'h3';
+
+export function useResponseSuccess(data: T) {
+ return {
+ code: 0,
+ data,
+ error: null,
+ message: 'ok',
+ };
+}
+
+export function usePageResponseSuccess(
+ 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,
+ message = 'Forbidden Exception',
+) {
+ setResponseStatus(event, 403);
+ return useResponseError(message, message);
+}
+
+export function unAuthorizedResponse(event: H3Event) {
+ setResponseStatus(event, 401);
+ return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
+}
+
+export function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export function pagination(
+ 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));
+}
diff --git a/web/apps/web-antd/.env.analyze b/web/apps/web-antd/.env.analyze
new file mode 100644
index 0000000..ffafa8d
--- /dev/null
+++ b/web/apps/web-antd/.env.analyze
@@ -0,0 +1,7 @@
+# public path
+VITE_BASE=/
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/api
+
+VITE_VISUALIZER=true
diff --git a/web/apps/web-antd/.env.development b/web/apps/web-antd/.env.development
new file mode 100644
index 0000000..1257291
--- /dev/null
+++ b/web/apps/web-antd/.env.development
@@ -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
diff --git a/web/apps/web-antd/.env.production b/web/apps/web-antd/.env.production
new file mode 100644
index 0000000..e7f2e81
--- /dev/null
+++ b/web/apps/web-antd/.env.production
@@ -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
diff --git a/web/apps/web-antd/index.html b/web/apps/web-antd/index.html
new file mode 100644
index 0000000..480eb84
--- /dev/null
+++ b/web/apps/web-antd/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+ <%= VITE_APP_TITLE %>
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-antd/package.json b/web/apps/web-antd/package.json
new file mode 100644
index 0000000..c3925c0
--- /dev/null
+++ b/web/apps/web-antd/package.json
@@ -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:"
+ }
+}
diff --git a/web/apps/web-antd/postcss.config.mjs b/web/apps/web-antd/postcss.config.mjs
new file mode 100644
index 0000000..3d80704
--- /dev/null
+++ b/web/apps/web-antd/postcss.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';
diff --git a/web/apps/web-antd/public/favicon.ico b/web/apps/web-antd/public/favicon.ico
new file mode 100644
index 0000000..fcf9818
Binary files /dev/null and b/web/apps/web-antd/public/favicon.ico differ
diff --git a/web/apps/web-antd/src/adapter/component/index.ts b/web/apps/web-antd/src/adapter/component/index.ts
new file mode 100644
index 0000000..d452d52
--- /dev/null
+++ b/web/apps/web-antd/src/adapter/component/index.ts
@@ -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 = (
+ component: T,
+ type: 'input' | 'select',
+ componentProps: Recordable = {},
+) => {
+ 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 = {};
+ 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> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // 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 };
diff --git a/web/apps/web-antd/src/adapter/form.ts b/web/apps/web-antd/src/adapter/form.ts
new file mode 100644
index 0000000..983a7f5
--- /dev/null
+++ b/web/apps/web-antd/src/adapter/form.ts
@@ -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({
+ 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;
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema = FormSchema;
+export type { VbenFormProps };
diff --git a/web/apps/web-antd/src/adapter/vxe-table.ts b/web/apps/web-antd/src/adapter/vxe-table.ts
new file mode 100644
index 0000000..8d17d9d
--- /dev/null
+++ b/web/apps/web-antd/src/adapter/vxe-table.ts
@@ -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> = {
+ code: string;
+ row: T;
+};
+export type OnActionClickFn> = (
+ params: OnActionClickParams,
+) => void;
+export type * from '@vben/plugins/vxe-table';
diff --git a/web/apps/web-antd/src/api/core/auth.ts b/web/apps/web-antd/src/api/core/auth.ts
new file mode 100644
index 0000000..1a1ec18
--- /dev/null
+++ b/web/apps/web-antd/src/api/core/auth.ts
@@ -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('/system/login/', data);
+}
+
+/**
+ * 刷新accessToken
+ */
+export async function refreshTokenApi() {
+ return baseRequestClient.post('/auth/refresh', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 退出登录
+ */
+export async function logoutApi() {
+ return baseRequestClient.post('/system/logout/', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 获取用户权限码
+ */
+export async function getAccessCodesApi() {
+ return requestClient.get('/system/codes/');
+}
diff --git a/web/apps/web-antd/src/api/core/index.ts b/web/apps/web-antd/src/api/core/index.ts
new file mode 100644
index 0000000..28a5aef
--- /dev/null
+++ b/web/apps/web-antd/src/api/core/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './menu';
+export * from './user';
diff --git a/web/apps/web-antd/src/api/core/menu.ts b/web/apps/web-antd/src/api/core/menu.ts
new file mode 100644
index 0000000..9ef60b1
--- /dev/null
+++ b/web/apps/web-antd/src/api/core/menu.ts
@@ -0,0 +1,10 @@
+import type { RouteRecordStringComponent } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户所有菜单
+ */
+export async function getAllMenusApi() {
+ return requestClient.get('/menu/all');
+}
diff --git a/web/apps/web-antd/src/api/core/user.ts b/web/apps/web-antd/src/api/core/user.ts
new file mode 100644
index 0000000..830f4dc
--- /dev/null
+++ b/web/apps/web-antd/src/api/core/user.ts
@@ -0,0 +1,10 @@
+import type { UserInfo } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户信息
+ */
+export async function getUserInfoApi() {
+ return requestClient.get('/system/info/');
+}
diff --git a/web/apps/web-antd/src/api/index.ts b/web/apps/web-antd/src/api/index.ts
new file mode 100644
index 0000000..4b0e041
--- /dev/null
+++ b/web/apps/web-antd/src/api/index.ts
@@ -0,0 +1 @@
+export * from './core';
diff --git a/web/apps/web-antd/src/api/request.ts b/web/apps/web-antd/src/api/request.ts
new file mode 100644
index 0000000..288dddd
--- /dev/null
+++ b/web/apps/web-antd/src/api/request.ts
@@ -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 });
diff --git a/web/apps/web-antd/src/api/system/dept.ts b/web/apps/web-antd/src/api/system/dept.ts
new file mode 100644
index 0000000..6faa76e
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/dept.ts
@@ -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>('/system/dept/');
+}
+
+/**
+ * 创建部门
+ * @param data 部门数据
+ */
+async function createDept(
+ data: Omit,
+) {
+ return requestClient.post('/system/dept/', data);
+}
+
+/**
+ * 更新部门
+ *
+ * @param id 部门 ID
+ * @param data 部门数据
+ */
+async function updateDept(
+ id: string,
+ data: Omit,
+) {
+ 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 };
diff --git a/web/apps/web-antd/src/api/system/dict_data.ts b/web/apps/web-antd/src/api/system/dict_data.ts
new file mode 100644
index 0000000..e24cf39
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/dict_data.ts
@@ -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) {
+ return requestClient.get>(
+ '/system/dict_data/',
+ {
+ params,
+ },
+ );
+}
+
+/**
+ * 创建字典数据
+ * @param data 字典数据数据
+ */
+async function createDictData(
+ data: Omit,
+) {
+ return requestClient.post('/system/dict_data/', data);
+}
+
+/**
+ * 更新字典数据
+ *
+ * @param id 字典数据 ID
+ * @param data 字典数据数据
+ */
+async function updateDictData(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.put(`/system/dict_data/${id}/`, data);
+}
+/**
+ * 更新字典数据
+ *
+ * @param id 字典数据 ID
+ * @param data 字典数据数据
+ */
+async function patchDictData(
+ id: string,
+ data: Omit,
+) {
+ 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,
+};
diff --git a/web/apps/web-antd/src/api/system/dict_type.ts b/web/apps/web-antd/src/api/system/dict_type.ts
new file mode 100644
index 0000000..a7ca410
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/dict_type.ts
@@ -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) {
+ return requestClient.get>(
+ '/system/dict_type/',
+ {
+ params,
+ },
+ );
+}
+
+/**
+ * 创建字典类型
+ * @param data 字典类型数据
+ */
+async function createDictType(
+ data: Omit,
+) {
+ return requestClient.post('/system/dict_type/', data);
+}
+
+/**
+ * 更新字典类型
+ *
+ * @param id 字典类型 ID
+ * @param data 字典类型数据
+ */
+async function updateDictType(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.put(`/system/dict_type/${id}/`, data);
+}
+/**
+ * 更新字典类型
+ *
+ * @param id 字典类型 ID
+ * @param data 字典类型数据
+ */
+async function patchDictType(
+ id: string,
+ data: Omit,
+) {
+ 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,
+};
diff --git a/web/apps/web-antd/src/api/system/index.ts b/web/apps/web-antd/src/api/system/index.ts
new file mode 100644
index 0000000..f2a248f
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/index.ts
@@ -0,0 +1,3 @@
+export * from './dept';
+export * from './menu';
+export * from './role';
diff --git a/web/apps/web-antd/src/api/system/menu.ts b/web/apps/web-antd/src/api/system/menu.ts
new file mode 100644
index 0000000..b81d16a
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/menu.ts
@@ -0,0 +1,167 @@
+import type { Recordable } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+export namespace SystemMenuApi {
+ /** 徽标颜色集合 */
+ export const BadgeVariants = [
+ 'default',
+ 'destructive',
+ 'primary',
+ 'success',
+ 'warning',
+ ] as const;
+ /** 徽标类型集合 */
+ export const BadgeTypes = ['dot', 'normal'] as const;
+ /** 菜单类型集合 */
+ export const MenuTypes = [
+ 'catalog',
+ 'menu',
+ 'embedded',
+ 'link',
+ 'button',
+ ] as const;
+ /** 系统菜单 */
+ export interface SystemMenu {
+ [key: string]: any;
+ /** 后端权限标识 */
+ authCode: string;
+ /** 子级 */
+ children?: SystemMenu[];
+ /** 组件 */
+ component?: string;
+ /** 菜单ID */
+ id: string;
+ /** 菜单元数据 */
+ meta?: {
+ /** 激活时显示的图标 */
+ activeIcon?: string;
+ /** 作为路由时,需要激活的菜单的Path */
+ activePath?: string;
+ /** 固定在标签栏 */
+ affixTab?: boolean;
+ /** 在标签栏固定的顺序 */
+ affixTabOrder?: number;
+ /** 徽标内容(当徽标类型为normal时有效) */
+ badge?: string;
+ /** 徽标类型 */
+ badgeType?: (typeof BadgeTypes)[number];
+ /** 徽标颜色 */
+ badgeVariants?: (typeof BadgeVariants)[number];
+ /** 在菜单中隐藏下级 */
+ hideChildrenInMenu?: boolean;
+ /** 在面包屑中隐藏 */
+ hideInBreadcrumb?: boolean;
+ /** 在菜单中隐藏 */
+ hideInMenu?: boolean;
+ /** 在标签栏中隐藏 */
+ hideInTab?: boolean;
+ /** 菜单图标 */
+ icon?: string;
+ /** 内嵌Iframe的URL */
+ iframeSrc?: string;
+ /** 是否缓存页面 */
+ keepAlive?: boolean;
+ /** 外链页面的URL */
+ link?: string;
+ /** 同一个路由最大打开的标签数 */
+ maxNumOfOpenTab?: number;
+ /** 无需基础布局 */
+ noBasicLayout?: boolean;
+ /** 是否在新窗口打开 */
+ openInNewWindow?: boolean;
+ /** 菜单排序 */
+ order?: number;
+ /** 额外的路由参数 */
+ query?: Recordable;
+ /** 菜单标题 */
+ title?: string;
+ };
+ /** 菜单名称 */
+ name: string;
+ /** 路由路径 */
+ path: string;
+ /** 父级ID */
+ pid: string;
+ /** 重定向 */
+ redirect?: string;
+ /** 菜单类型 */
+ type: (typeof MenuTypes)[number];
+ }
+}
+
+/**
+ * 获取菜单数据列表
+ */
+async function getMenuList() {
+ return requestClient.get>('/system/menu/');
+}
+
+async function isMenuNameExists(
+ name: string,
+ id?: SystemMenuApi.SystemMenu['id'],
+) {
+ const url = id ? `/system/menu/${id}/` : `/system/menu/`;
+ return requestClient.get(url, {
+ params: { id, name },
+ });
+}
+
+async function isMenuSearchExists(
+ name: string,
+ id?: SystemMenuApi.SystemMenu['id'],
+) {
+ return requestClient.get('/system/menu/name-search', {
+ params: { name, id },
+ });
+}
+
+async function isMenuPathExists(
+ path: string,
+ id?: SystemMenuApi.SystemMenu['id'],
+) {
+ return requestClient.get('/system/menu/path-exists', {
+ params: { id, path },
+ });
+}
+
+/**
+ * 创建菜单
+ * @param data 菜单数据
+ */
+async function createMenu(
+ data: Omit,
+) {
+ return requestClient.post('/system/menu/', data);
+}
+
+/**
+ * 更新菜单
+ *
+ * @param id 菜单 ID
+ * @param data 菜单数据
+ */
+async function updateMenu(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.put(`/system/menu/${id}/`, data);
+}
+
+/**
+ * 删除菜单
+ * @param id 菜单 ID
+ */
+async function deleteMenu(id: string) {
+ return requestClient.delete(`/system/menu/${id}/`);
+}
+
+export {
+ createMenu,
+ deleteMenu,
+ getMenuList,
+ isMenuNameExists,
+ isMenuPathExists,
+ isMenuSearchExists,
+ updateMenu,
+};
diff --git a/web/apps/web-antd/src/api/system/role.ts b/web/apps/web-antd/src/api/system/role.ts
new file mode 100644
index 0000000..a0da3ec
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/role.ts
@@ -0,0 +1,68 @@
+import type { Recordable } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+export namespace SystemRoleApi {
+ export interface SystemRole {
+ [key: string]: any;
+ id: string;
+ name: string;
+ profile: {
+ create_time: string;
+ remark?: string;
+ status: 0 | 1;
+ };
+ }
+}
+
+/**
+ * 获取角色列表数据
+ */
+async function getRoleList(params: Recordable) {
+ return requestClient.get>('/system/role/', {
+ params,
+ });
+}
+
+/**
+ * 创建角色
+ * @param data 角色数据
+ */
+async function createRole(data: Omit) {
+ return requestClient.post('/system/role/', data);
+}
+
+/**
+ * 更新角色
+ *
+ * @param id 角色 ID
+ * @param data 角色数据
+ */
+async function updateRole(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.put(`/system/role/${id}/`, data);
+}
+/**
+ * 更新角色
+ *
+ * @param id 角色 ID
+ * @param data 角色数据
+ */
+async function patchRole(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.patch(`/system/role/${id}/`, data);
+}
+
+/**
+ * 删除角色
+ * @param id 角色 ID
+ */
+async function deleteRole(id: string) {
+ return requestClient.delete(`/system/role/${id}/`);
+}
+
+export { createRole, deleteRole, getRoleList, patchRole, updateRole };
diff --git a/web/apps/web-antd/src/api/system/tenant_package.ts b/web/apps/web-antd/src/api/system/tenant_package.ts
new file mode 100644
index 0000000..0811749
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/tenant_package.ts
@@ -0,0 +1,74 @@
+import type { Recordable } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+export namespace SystemTenantPackageApi {
+ export interface SystemTenantPackage {
+ [key: string]: any;
+ id: string;
+ name: string;
+ }
+}
+
+/**
+ * 获取租户列表数据
+ */
+async function getTenantPackageList(params: Recordable) {
+ return requestClient.get>(
+ '/system/tenant_package/',
+ {
+ params,
+ },
+ );
+}
+
+/**
+ * 创建租户
+ * @param data 租户数据
+ */
+async function createTenantPackage(
+ data: Omit,
+) {
+ return requestClient.post('/system/tenant_package/', data);
+}
+
+/**
+ * 更新租户
+ *
+ * @param id 租户 ID
+ * @param data 租户数据
+ */
+async function updateTenantPackage(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.put(`/system/tenant_package/${id}/`, data);
+}
+/**
+ * 更新租户
+ *
+ * @param id 租户 ID
+ * @param data 租户数据
+ */
+async function patchTenantPackage(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.patch(`/system/tenant_package/${id}/`, data);
+}
+
+/**
+ * 删除租户
+ * @param id 租户 ID
+ */
+async function deleteTenantPackage(id: string) {
+ return requestClient.delete(`/system/tenant_package/${id}/`);
+}
+
+export {
+ createTenantPackage,
+ deleteTenantPackage,
+ getTenantPackageList,
+ patchTenantPackage,
+ updateTenantPackage,
+};
diff --git a/web/apps/web-antd/src/api/system/tenants.ts b/web/apps/web-antd/src/api/system/tenants.ts
new file mode 100644
index 0000000..c120614
--- /dev/null
+++ b/web/apps/web-antd/src/api/system/tenants.ts
@@ -0,0 +1,71 @@
+import type { Recordable } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+export namespace SystemTenantsApi {
+ export interface SystemTenants {
+ [key: string]: any;
+ id: string;
+ name: string;
+ }
+}
+
+/**
+ * 获取租户列表数据
+ */
+async function getTenantsList(params: Recordable) {
+ return requestClient.get>(
+ '/system/tenants/',
+ {
+ params,
+ });
+}
+
+/**
+ * 创建租户
+ * @param data 租户数据
+ */
+async function createTenants(data: Omit) {
+ return requestClient.post('/system/tenants/', data);
+}
+
+/**
+ * 更新租户
+ *
+ * @param id 租户 ID
+ * @param data 租户数据
+ */
+async function updateTenants(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.put(`/system/tenants/${id}/`, data);
+}
+/**
+ * 更新租户
+ *
+ * @param id 租户 ID
+ * @param data 租户数据
+ */
+async function patchTenants(
+ id: string,
+ data: Omit,
+) {
+ return requestClient.patch(`/system/tenants/${id}/`, data);
+}
+
+/**
+ * 删除租户
+ * @param id 租户 ID
+ */
+async function deleteTenants(id: string) {
+ return requestClient.delete(`/system/tenants/${id}/`);
+}
+
+export {
+ createTenants,
+ deleteTenants,
+ getTenantsList,
+ patchTenants,
+ updateTenants,
+};
diff --git a/web/apps/web-antd/src/app.vue b/web/apps/web-antd/src/app.vue
new file mode 100644
index 0000000..bbaccce
--- /dev/null
+++ b/web/apps/web-antd/src/app.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/bootstrap.ts b/web/apps/web-antd/src/bootstrap.ts
new file mode 100644
index 0000000..ec72112
--- /dev/null
+++ b/web/apps/web-antd/src/bootstrap.ts
@@ -0,0 +1,76 @@
+import { createApp, watchEffect } from 'vue';
+
+import { registerAccessDirective } from '@vben/access';
+import { registerLoadingDirective } from '@vben/common-ui/es/loading';
+import { preferences } from '@vben/preferences';
+import { initStores } from '@vben/stores';
+import '@vben/styles';
+import '@vben/styles/antd';
+
+import { useTitle } from '@vueuse/core';
+
+import { $t, setupI18n } from '#/locales';
+
+import { initComponentAdapter } from './adapter/component';
+import { initSetupVbenForm } from './adapter/form';
+import App from './app.vue';
+import { router } from './router';
+
+async function bootstrap(namespace: string) {
+ // 初始化组件适配器
+ await initComponentAdapter();
+
+ // 初始化表单组件
+ await initSetupVbenForm();
+
+ // // 设置弹窗的默认配置
+ // setDefaultModalProps({
+ // fullscreenButton: false,
+ // });
+ // // 设置抽屉的默认配置
+ // setDefaultDrawerProps({
+ // zIndex: 1020,
+ // });
+
+ const app = createApp(App);
+
+ // 注册v-loading指令
+ registerLoadingDirective(app, {
+ loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+ spinning: 'spinning',
+ });
+
+ // 国际化 i18n 配置
+ await setupI18n(app);
+
+ // 配置 pinia-tore
+ await initStores(app, { namespace });
+
+ // 安装权限指令
+ registerAccessDirective(app);
+
+ // 初始化 tippy
+ const { initTippy } = await import('@vben/common-ui/es/tippy');
+ initTippy(app);
+
+ // 配置路由及路由守卫
+ app.use(router);
+
+ // 配置Motion插件
+ const { MotionPlugin } = await import('@vben/plugins/motion');
+ app.use(MotionPlugin);
+
+ // 动态更新标题
+ watchEffect(() => {
+ if (preferences.app.dynamicTitle) {
+ const routeTitle = router.currentRoute.value.meta?.title;
+ const pageTitle =
+ (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+ useTitle(pageTitle);
+ }
+ });
+
+ app.mount('#app');
+}
+
+export { bootstrap };
diff --git a/web/apps/web-antd/src/layouts/auth.vue b/web/apps/web-antd/src/layouts/auth.vue
new file mode 100644
index 0000000..18d415b
--- /dev/null
+++ b/web/apps/web-antd/src/layouts/auth.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/layouts/basic.vue b/web/apps/web-antd/src/layouts/basic.vue
new file mode 100644
index 0000000..1481dc5
--- /dev/null
+++ b/web/apps/web-antd/src/layouts/basic.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/layouts/index.ts b/web/apps/web-antd/src/layouts/index.ts
new file mode 100644
index 0000000..a432078
--- /dev/null
+++ b/web/apps/web-antd/src/layouts/index.ts
@@ -0,0 +1,6 @@
+const BasicLayout = () => import('./basic.vue');
+const AuthPageLayout = () => import('./auth.vue');
+
+const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
+
+export { AuthPageLayout, BasicLayout, IFrameView };
diff --git a/web/apps/web-antd/src/locales/README.md b/web/apps/web-antd/src/locales/README.md
new file mode 100644
index 0000000..7b45103
--- /dev/null
+++ b/web/apps/web-antd/src/locales/README.md
@@ -0,0 +1,3 @@
+# locale
+
+每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
diff --git a/web/apps/web-antd/src/locales/index.ts b/web/apps/web-antd/src/locales/index.ts
new file mode 100644
index 0000000..7f32bd1
--- /dev/null
+++ b/web/apps/web-antd/src/locales/index.ts
@@ -0,0 +1,102 @@
+import type { Locale } from 'ant-design-vue/es/locale';
+
+import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
+import { ref } from 'vue';
+
+import {
+ $t,
+ setupI18n as coreSetup,
+ loadLocalesMapFromDir,
+} from '@vben/locales';
+import { preferences } from '@vben/preferences';
+
+import antdEnLocale from 'ant-design-vue/es/locale/en_US';
+import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
+import dayjs from 'dayjs';
+
+const antdLocale = ref(antdDefaultLocale);
+
+const modules = import.meta.glob('./langs/**/*.json');
+
+const localesMap = loadLocalesMapFromDir(
+ /\.\/langs\/([^/]+)\/(.*)\.json$/,
+ modules,
+);
+/**
+ * 加载应用特有的语言包
+ * 这里也可以改造为从服务端获取翻译数据
+ * @param lang
+ */
+async function loadMessages(lang: SupportedLanguagesType) {
+ const [appLocaleMessages] = await Promise.all([
+ localesMap[lang]?.(),
+ loadThirdPartyMessage(lang),
+ ]);
+ return appLocaleMessages?.default;
+}
+
+/**
+ * 加载第三方组件库的语言包
+ * @param lang
+ */
+async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
+ await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
+}
+
+/**
+ * 加载dayjs的语言包
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+ let locale;
+ switch (lang) {
+ case 'en-US': {
+ locale = await import('dayjs/locale/en');
+ break;
+ }
+ case 'zh-CN': {
+ locale = await import('dayjs/locale/zh-cn');
+ break;
+ }
+ // 默认使用英语
+ default: {
+ locale = await import('dayjs/locale/en');
+ }
+ }
+ if (locale) {
+ dayjs.locale(locale);
+ } else {
+ console.error(`Failed to load dayjs locale for ${lang}`);
+ }
+}
+
+/**
+ * 加载antd的语言包
+ * @param lang
+ */
+async function loadAntdLocale(lang: SupportedLanguagesType) {
+ switch (lang) {
+ case 'en-US': {
+ antdLocale.value = antdEnLocale;
+ break;
+ }
+ case 'zh-CN': {
+ antdLocale.value = antdDefaultLocale;
+ break;
+ }
+ }
+}
+
+async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD,
+ ...options,
+ });
+}
+
+export { $t, antdLocale, setupI18n };
diff --git a/web/apps/web-antd/src/locales/langs/en-US/demos.json b/web/apps/web-antd/src/locales/langs/en-US/demos.json
new file mode 100644
index 0000000..0715643
--- /dev/null
+++ b/web/apps/web-antd/src/locales/langs/en-US/demos.json
@@ -0,0 +1,12 @@
+{
+ "title": "Demos",
+ "antd": "Ant Design Vue",
+ "vben": {
+ "title": "Project",
+ "about": "About",
+ "document": "Document",
+ "antdv": "Ant Design Vue Version",
+ "naive-ui": "Naive UI Version",
+ "element-plus": "Element Plus Version"
+ }
+}
diff --git a/web/apps/web-antd/src/locales/langs/en-US/page.json b/web/apps/web-antd/src/locales/langs/en-US/page.json
new file mode 100644
index 0000000..618a258
--- /dev/null
+++ b/web/apps/web-antd/src/locales/langs/en-US/page.json
@@ -0,0 +1,14 @@
+{
+ "auth": {
+ "login": "Login",
+ "register": "Register",
+ "codeLogin": "Code Login",
+ "qrcodeLogin": "Qr Code Login",
+ "forgetPassword": "Forget Password"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "analytics": "Analytics",
+ "workspace": "Workspace"
+ }
+}
diff --git a/web/apps/web-antd/src/locales/langs/en-US/system.json b/web/apps/web-antd/src/locales/langs/en-US/system.json
new file mode 100644
index 0000000..8329c37
--- /dev/null
+++ b/web/apps/web-antd/src/locales/langs/en-US/system.json
@@ -0,0 +1,98 @@
+{
+ "title": "System Management",
+ "dept": {
+ "name": "Department",
+ "title": "Department Management",
+ "deptName": "Department Name",
+ "status": "Status",
+ "createTime": "Created At",
+ "remark": "Remarks",
+ "operation": "Actions",
+ "parentDept": "Parent Department"
+ },
+ "menu": {
+ "title": "Menu Management",
+ "parent": "Parent Menu",
+ "menuTitle": "Menu Title",
+ "menuName": "Menu Name",
+ "name": "Menu",
+ "type": "Menu Type",
+ "typeCatalog": "Catalog",
+ "typeMenu": "Menu",
+ "typeButton": "Button",
+ "typeLink": "External Link",
+ "typeEmbedded": "Embedded Page",
+ "icon": "Icon",
+ "activeIcon": "Active Icon",
+ "activePath": "Active Path",
+ "path": "Route Path",
+ "component": "Component",
+ "status": "Status",
+ "authCode": "Permission Code",
+ "badge": "Badge",
+ "operation": "Actions",
+ "linkSrc": "Link URL",
+ "affixTab": "Pin Tab",
+ "keepAlive": "Keep Alive",
+ "hideInMenu": "Hide in Menu",
+ "hideInTab": "Hide in Tab Bar",
+ "hideChildrenInMenu": "Hide Children in Menu",
+ "hideInBreadcrumb": "Hide in Breadcrumb",
+ "advancedSettings": "Advanced Settings",
+ "activePathMustExist": "The path must correspond to a valid menu",
+ "activePathHelp": "When navigating to this route, if it doesn’t appear in the menu, you must specify the menu path that should be activated.",
+ "badgeType": {
+ "title": "Badge Type",
+ "dot": "Dot",
+ "normal": "Text",
+ "none": "None"
+ },
+ "badgeVariants": "Badge Style"
+ },
+ "role": {
+ "title": "Role Management",
+ "list": "Role List",
+ "name": "Role",
+ "roleName": "Role Name",
+ "id": "Role ID",
+ "status": "Status",
+ "remark": "Remarks",
+ "createTime": "Created At",
+ "operation": "Actions",
+ "permissions": "Permissions",
+ "setPermissions": "Configure Permissions"
+ },
+ "tenant": {
+ "name": "Tenant",
+ "contact_mobile": "Contact Mobile",
+ "website": "Website",
+ "package_id": "Package Name",
+ "expire_time": "Expiration Time",
+ "account_count": "Account Limit",
+ "tenantName": "Tenant Name"
+ },
+ "tenant_package": {
+ "name": "Tenant Package",
+ "contact_mobile": "Contact Mobile",
+ "website": "Website",
+ "package_id": "Package Name",
+ "expire_time": "Expiration Time",
+ "account_count": "Account Limit",
+ "tenantName": "Tenant Name"
+ },
+ "dict_type": {
+ "name": "Dictionary Name",
+ "title": "Dictionary Name",
+ "type": "Dictionary Type"
+ },
+ "dict_data": {
+ "name": "Dictionary Label",
+ "title": "Dictionary Data",
+ "type": "Dictionary Value"
+ },
+ "status": "Status",
+ "remark": "Remarks",
+ "createTime": "Created At",
+ "operation": "Actions",
+ "updateTime": "Updated At"
+}
diff --git a/web/apps/web-antd/src/locales/langs/zh-CN/demos.json b/web/apps/web-antd/src/locales/langs/zh-CN/demos.json
new file mode 100644
index 0000000..93ee722
--- /dev/null
+++ b/web/apps/web-antd/src/locales/langs/zh-CN/demos.json
@@ -0,0 +1,12 @@
+{
+ "title": "演示",
+ "antd": "Ant Design Vue",
+ "vben": {
+ "title": "项目",
+ "about": "关于",
+ "document": "文档",
+ "antdv": "Ant Design Vue 版本",
+ "naive-ui": "Naive UI 版本",
+ "element-plus": "Element Plus 版本"
+ }
+}
diff --git a/web/apps/web-antd/src/locales/langs/zh-CN/page.json b/web/apps/web-antd/src/locales/langs/zh-CN/page.json
new file mode 100644
index 0000000..4cb6708
--- /dev/null
+++ b/web/apps/web-antd/src/locales/langs/zh-CN/page.json
@@ -0,0 +1,14 @@
+{
+ "auth": {
+ "login": "登录",
+ "register": "注册",
+ "codeLogin": "验证码登录",
+ "qrcodeLogin": "二维码登录",
+ "forgetPassword": "忘记密码"
+ },
+ "dashboard": {
+ "title": "概览",
+ "analytics": "分析页",
+ "workspace": "工作台"
+ }
+}
diff --git a/web/apps/web-antd/src/locales/langs/zh-CN/system.json b/web/apps/web-antd/src/locales/langs/zh-CN/system.json
new file mode 100644
index 0000000..2dd4ba8
--- /dev/null
+++ b/web/apps/web-antd/src/locales/langs/zh-CN/system.json
@@ -0,0 +1,99 @@
+{
+ "title": "系统管理",
+ "dept": {
+ "list": "部门列表",
+ "createTime": "创建时间",
+ "deptName": "部门名称",
+ "name": "部门",
+ "operation": "操作",
+ "parentDept": "上级部门",
+ "remark": "备注",
+ "status": "状态",
+ "title": "部门管理"
+ },
+ "menu": {
+ "list": "菜单列表",
+ "activeIcon": "激活图标",
+ "activePath": "激活路径",
+ "activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时,需要指定激活路径",
+ "activePathMustExist": "该路径未能找到有效的菜单",
+ "advancedSettings": "其它设置",
+ "affixTab": "固定在标签",
+ "authCode": "权限标识",
+ "badge": "徽章内容",
+ "badgeVariants": "徽标样式",
+ "badgeType": {
+ "dot": "点",
+ "none": "无",
+ "normal": "文字",
+ "title": "徽标类型"
+ },
+ "component": "页面组件",
+ "hideChildrenInMenu": "隐藏子菜单",
+ "hideInBreadcrumb": "在面包屑中隐藏",
+ "hideInMenu": "隐藏菜单",
+ "hideInTab": "在标签栏中隐藏",
+ "icon": "图标",
+ "keepAlive": "缓存标签页",
+ "linkSrc": "链接地址",
+ "menuName": "菜单名称",
+ "menuTitle": "标题",
+ "name": "菜单",
+ "operation": "操作",
+ "parent": "上级菜单",
+ "path": "路由地址",
+ "status": "状态",
+ "title": "菜单管理",
+ "type": "类型",
+ "typeButton": "按钮",
+ "typeCatalog": "目录",
+ "typeEmbedded": "内嵌",
+ "typeLink": "外链",
+ "typeMenu": "菜单"
+ },
+ "role": {
+ "title": "角色管理",
+ "list": "角色列表",
+ "name": "角色",
+ "roleName": "角色名称",
+ "id": "角色ID",
+ "status": "状态",
+ "remark": "备注",
+ "createTime": "创建时间",
+ "operation": "操作",
+ "permissions": "权限",
+ "setPermissions": "授权"
+ },
+ "tenant": {
+ "name": "租户",
+ "contact_mobile": "联系手机",
+ "website": "绑定域名",
+ "package_id": "租户套餐编号",
+ "expire_time": "过期时间",
+ "account_count": "账号数量",
+ "tenantName": "租户名称"
+ },
+ "tenant_package": {
+ "name": "租户套餐",
+ "website": "绑定域名",
+ "package_id": "租户套餐编号",
+ "expire_time": "过期时间",
+ "account_count": "账号数量",
+ "tenantName": "租户名称"
+ },
+ "dict_type": {
+ "name": "字典名称",
+ "title": "字典名称",
+ "type": "字典类型"
+ },
+ "dict_data": {
+ "name": "字典数据",
+ "title": "字典数据",
+ "type": "字典键值"
+ },
+ "status": "状态",
+ "remark": "备注",
+ "createTime": "创建时间",
+ "operation": "操作",
+ "updateTime": "更新时间"
+}
diff --git a/web/apps/web-antd/src/main.ts b/web/apps/web-antd/src/main.ts
new file mode 100644
index 0000000..5d728a0
--- /dev/null
+++ b/web/apps/web-antd/src/main.ts
@@ -0,0 +1,31 @@
+import { initPreferences } from '@vben/preferences';
+import { unmountGlobalLoading } from '@vben/utils';
+
+import { overridesPreferences } from './preferences';
+
+/**
+ * 应用初始化完成之后再进行页面加载渲染
+ */
+async function initApplication() {
+ // name用于指定项目唯一标识
+ // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
+ const env = import.meta.env.PROD ? 'prod' : 'dev';
+ const appVersion = import.meta.env.VITE_APP_VERSION;
+ const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
+
+ // app偏好设置初始化
+ await initPreferences({
+ namespace,
+ overrides: overridesPreferences,
+ });
+
+ // 启动应用并挂载
+ // vue应用主要逻辑及视图
+ const { bootstrap } = await import('./bootstrap');
+ await bootstrap(namespace);
+
+ // 移除并销毁loading
+ unmountGlobalLoading();
+}
+
+initApplication();
diff --git a/web/apps/web-antd/src/models/base.ts b/web/apps/web-antd/src/models/base.ts
new file mode 100644
index 0000000..a679174
--- /dev/null
+++ b/web/apps/web-antd/src/models/base.ts
@@ -0,0 +1,136 @@
+import type { Recordable } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+export interface CoreModel {
+ remark: string;
+ creator: string;
+ modifier: string;
+ update_time: string;
+ create_time: string;
+ is_deleted: boolean;
+}
+
+// 通用Model基类
+export class BaseModel<
+ T,
+ CreateData = Omit,
+ UpdateData = Partial,
+> {
+ protected baseUrl: string;
+
+ constructor(baseUrl: string) {
+ this.baseUrl = baseUrl;
+ }
+
+ /**
+ * 通用操作方法
+ * @param url 操作路径
+ * @param data 请求数据
+ * @param id 是否针对单条记录的操作
+ * @param method 请求方法
+ */
+ async action(
+ url: string,
+ data: any = {},
+ id: null | number = null,
+ method = 'post',
+ ) {
+ const baseUrl = id
+ ? `${this.baseUrl}${id}/${url}/`
+ : `${this.baseUrl}${url}/`;
+
+ const config =
+ method === 'get'
+ ? {
+ url: baseUrl,
+ method: 'get',
+ params: data,
+ }
+ : {
+ url: baseUrl,
+ method,
+ data,
+ };
+
+ return requestClient.request(url, config);
+ }
+
+ /**
+ * 创建记录
+ */
+ async create(data: CreateData) {
+ return requestClient.post(this.baseUrl, data);
+ }
+
+ /**
+ * 删除记录
+ */
+ async delete(id: number) {
+ return requestClient.delete(`${this.baseUrl}${id}/`);
+ }
+
+ /**
+ * 导出数据
+ */
+ /**
+ * 导出数据
+ */
+ async export(params: Recordable = {}) {
+ return requestClient.get(`${this.baseUrl}export/`, {
+ params,
+ responseType: 'blob', // 二进制流
+ });
+ }
+
+ /**
+ * 获取列表数据
+ */
+ async list(params: Recordable = {}) {
+ return requestClient.get>(this.baseUrl, { params });
+ }
+
+ /**
+ * 部分更新记录
+ */
+ async patch(id: number, data: Partial) {
+ return requestClient.patch(`${this.baseUrl}${id}/`, data);
+ }
+
+ /**
+ * 获取单条记录
+ */
+ async retrieve(id: number) {
+ return requestClient.get(`${this.baseUrl}${id}/`);
+ }
+ /**
+ * 全量更新记录
+ */
+ async update(id: number, data: UpdateData) {
+ return requestClient.put(`${this.baseUrl}${id}/`, data);
+ }
+}
+//
+// // 字典类型专用Model
+// export class SystemDictTypeModel extends BaseModel {
+// constructor() {
+// super('/system/dict_type/');
+// }
+// }
+//
+// // AmazonListingModel示例
+// export class AmazonListingModel extends BaseModel {
+// constructor() {
+// super('amazon/amazon_listing/');
+// }
+// }
+// const dictTypeModel = new SystemDictTypeModel();
+//
+// // 获取列表
+// dictTypeModel.list().then(({ data }) => console.log(data));
+//
+// // 创建记录
+// dictTypeModel.create({ name: '新字典', type: 'new_type' });
+//
+// // 更新记录
+// dictTypeModel.patch('123', { name: '更新后的字典' });
diff --git a/web/apps/web-antd/src/models/system.ts b/web/apps/web-antd/src/models/system.ts
new file mode 100644
index 0000000..056c34c
--- /dev/null
+++ b/web/apps/web-antd/src/models/system.ts
@@ -0,0 +1,16 @@
+import { BaseModel } from '#/models/base';
+
+export namespace SystemDictTypeApi {
+ export interface SystemDictType {
+ [key: string]: any;
+ id: string;
+ name: string;
+ type: string;
+ }
+}
+
+export class SystemDictTypeModel extends BaseModel {
+ constructor() {
+ super('/system/dict_type/');
+ }
+}
diff --git a/web/apps/web-antd/src/preferences.ts b/web/apps/web-antd/src/preferences.ts
new file mode 100644
index 0000000..b2e9ace
--- /dev/null
+++ b/web/apps/web-antd/src/preferences.ts
@@ -0,0 +1,13 @@
+import { defineOverridesPreferences } from '@vben/preferences';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ * !!! 更改配置后请清空缓存,否则可能不生效
+ */
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ name: import.meta.env.VITE_APP_TITLE,
+ },
+});
diff --git a/web/apps/web-antd/src/router/access.ts b/web/apps/web-antd/src/router/access.ts
new file mode 100644
index 0000000..3a48be2
--- /dev/null
+++ b/web/apps/web-antd/src/router/access.ts
@@ -0,0 +1,42 @@
+import type {
+ ComponentRecordType,
+ GenerateMenuAndRoutesOptions,
+} from '@vben/types';
+
+import { generateAccessible } from '@vben/access';
+import { preferences } from '@vben/preferences';
+
+import { message } from 'ant-design-vue';
+
+import { getAllMenusApi } from '#/api';
+import { BasicLayout, IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
+
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
+
+ const layoutMap: ComponentRecordType = {
+ BasicLayout,
+ IFrameView,
+ };
+
+ return await generateAccessible(preferences.app.accessMode, {
+ ...options,
+ fetchMenuListAsync: async () => {
+ message.loading({
+ content: `${$t('common.loadingMenu')}...`,
+ duration: 1.5,
+ });
+ return await getAllMenusApi();
+ },
+ // 可以指定没有权限跳转403页面
+ forbiddenComponent,
+ // 如果 route.meta.menuVisibleWithForbidden = true
+ layoutMap,
+ pageMap,
+ });
+}
+
+export { generateAccess };
diff --git a/web/apps/web-antd/src/router/guard.ts b/web/apps/web-antd/src/router/guard.ts
new file mode 100644
index 0000000..a1ad6d8
--- /dev/null
+++ b/web/apps/web-antd/src/router/guard.ts
@@ -0,0 +1,133 @@
+import type { Router } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+import { startProgress, stopProgress } from '@vben/utils';
+
+import { accessRoutes, coreRouteNames } from '#/router/routes';
+import { useAuthStore } from '#/store';
+
+import { generateAccess } from './access';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function setupCommonGuard(router: Router) {
+ // 记录已经加载的页面
+ const loadedPaths = new Set();
+
+ router.beforeEach((to) => {
+ to.meta.loaded = loadedPaths.has(to.path);
+
+ // 页面加载进度条
+ if (!to.meta.loaded && preferences.transition.progress) {
+ startProgress();
+ }
+ return true;
+ });
+
+ router.afterEach((to) => {
+ // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+
+ loadedPaths.add(to.path);
+
+ // 关闭页面加载进度条
+ if (preferences.transition.progress) {
+ stopProgress();
+ }
+ });
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function setupAccessGuard(router: Router) {
+ router.beforeEach(async (to, from) => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const authStore = useAuthStore();
+
+ // 基本路由,这些路由不需要进入权限拦截
+ if (coreRouteNames.includes(to.name as string)) {
+ if (to.path === LOGIN_PATH && accessStore.accessToken) {
+ return decodeURIComponent(
+ (to.query?.redirect as string) ||
+ userStore.userInfo?.homePath ||
+ preferences.app.defaultHomePath,
+ );
+ }
+ return true;
+ }
+
+ // accessToken 检查
+ if (!accessStore.accessToken) {
+ // 明确声明忽略权限访问权限,则可以访问
+ if (to.meta.ignoreAccess) {
+ return true;
+ }
+
+ // 没有访问权限,跳转登录页面
+ if (to.fullPath !== LOGIN_PATH) {
+ return {
+ path: LOGIN_PATH,
+ // 如不需要,直接删除 query
+ query:
+ to.fullPath === preferences.app.defaultHomePath
+ ? {}
+ : { redirect: encodeURIComponent(to.fullPath) },
+ // 携带当前跳转的页面,登录后重新跳转该页面
+ replace: true,
+ };
+ }
+ return to;
+ }
+
+ // 是否已经生成过动态路由
+ if (accessStore.isAccessChecked) {
+ return true;
+ }
+
+ // 生成路由表
+ // 当前登录用户拥有的角色标识列表
+ const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
+ const userRoles = userInfo.roles ?? [];
+
+ // 生成菜单和路由
+ const { accessibleMenus, accessibleRoutes } = await generateAccess({
+ roles: userRoles,
+ router,
+ // 则会在菜单中显示,但是访问会被重定向到403
+ routes: accessRoutes,
+ });
+
+ // 保存菜单信息和路由信息
+ accessStore.setAccessMenus(accessibleMenus);
+ accessStore.setAccessRoutes(accessibleRoutes);
+ accessStore.setIsAccessChecked(true);
+ const redirectPath = (from.query.redirect ??
+ (to.path === preferences.app.defaultHomePath
+ ? userInfo.homePath || preferences.app.defaultHomePath
+ : to.fullPath)) as string;
+
+ return {
+ ...router.resolve(decodeURIComponent(redirectPath)),
+ replace: true,
+ };
+ });
+}
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+ /** 通用 */
+ setupCommonGuard(router);
+ /** 权限访问 */
+ setupAccessGuard(router);
+}
+
+export { createRouterGuard };
diff --git a/web/apps/web-antd/src/router/index.ts b/web/apps/web-antd/src/router/index.ts
new file mode 100644
index 0000000..4840230
--- /dev/null
+++ b/web/apps/web-antd/src/router/index.ts
@@ -0,0 +1,37 @@
+import {
+ createRouter,
+ createWebHashHistory,
+ createWebHistory,
+} from 'vue-router';
+
+import { resetStaticRoutes } from '@vben/utils';
+
+import { createRouterGuard } from './guard';
+import { routes } from './routes';
+
+/**
+ * @zh_CN 创建vue-router实例
+ */
+const router = createRouter({
+ history:
+ import.meta.env.VITE_ROUTER_HISTORY === 'hash'
+ ? createWebHashHistory(import.meta.env.VITE_BASE)
+ : createWebHistory(import.meta.env.VITE_BASE),
+ // 应该添加到路由的初始路由列表。
+ routes,
+ scrollBehavior: (to, _from, savedPosition) => {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
+ },
+ // 是否应该禁止尾部斜杠。
+ // strict: true,
+});
+
+const resetRoutes = () => resetStaticRoutes(router, routes);
+
+// 创建路由守卫
+createRouterGuard(router);
+
+export { resetRoutes, router };
diff --git a/web/apps/web-antd/src/router/routes/core.ts b/web/apps/web-antd/src/router/routes/core.ts
new file mode 100644
index 0000000..949b0b6
--- /dev/null
+++ b/web/apps/web-antd/src/router/routes/core.ts
@@ -0,0 +1,97 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
+/** 全局404页面 */
+const fallbackNotFoundRoute: RouteRecordRaw = {
+ component: () => import('#/views/_core/fallback/not-found.vue'),
+ meta: {
+ hideInBreadcrumb: true,
+ hideInMenu: true,
+ hideInTab: true,
+ title: '404',
+ },
+ name: 'FallbackNotFound',
+ path: '/:path(.*)*',
+};
+
+/** 基本路由,这些路由是必须存在的 */
+const coreRoutes: RouteRecordRaw[] = [
+ /**
+ * 根路由
+ * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+ * 此路由必须存在,且不应修改
+ */
+ {
+ component: BasicLayout,
+ meta: {
+ hideInBreadcrumb: true,
+ title: 'Root',
+ },
+ name: 'Root',
+ path: '/',
+ redirect: preferences.app.defaultHomePath,
+ children: [],
+ },
+ {
+ component: AuthPageLayout,
+ meta: {
+ hideInTab: true,
+ title: 'Authentication',
+ },
+ name: 'Authentication',
+ path: '/auth',
+ redirect: LOGIN_PATH,
+ children: [
+ {
+ name: 'Login',
+ path: 'login',
+ component: () => import('#/views/_core/authentication/login.vue'),
+ meta: {
+ title: $t('page.auth.login'),
+ },
+ },
+ {
+ name: 'CodeLogin',
+ path: 'code-login',
+ component: () => import('#/views/_core/authentication/code-login.vue'),
+ meta: {
+ title: $t('page.auth.codeLogin'),
+ },
+ },
+ {
+ name: 'QrCodeLogin',
+ path: 'qrcode-login',
+ component: () =>
+ import('#/views/_core/authentication/qrcode-login.vue'),
+ meta: {
+ title: $t('page.auth.qrcodeLogin'),
+ },
+ },
+ {
+ name: 'ForgetPassword',
+ path: 'forget-password',
+ component: () =>
+ import('#/views/_core/authentication/forget-password.vue'),
+ meta: {
+ title: $t('page.auth.forgetPassword'),
+ },
+ },
+ {
+ name: 'Register',
+ path: 'register',
+ component: () => import('#/views/_core/authentication/register.vue'),
+ meta: {
+ title: $t('page.auth.register'),
+ },
+ },
+ ],
+ },
+];
+
+export { coreRoutes, fallbackNotFoundRoute };
diff --git a/web/apps/web-antd/src/router/routes/index.ts b/web/apps/web-antd/src/router/routes/index.ts
new file mode 100644
index 0000000..8d1571d
--- /dev/null
+++ b/web/apps/web-antd/src/router/routes/index.ts
@@ -0,0 +1,45 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
+
+import { coreRoutes, fallbackNotFoundRoute } from './core';
+
+const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
+ eager: true,
+});
+
+// 有需要可以自行打开注释,并创建文件夹
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
+
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
+const externalRoutes: RouteRecordRaw[] = [];
+
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ * 无需走权限验证(会一直显示在菜单中) */
+const routes: RouteRecordRaw[] = [
+ ...coreRoutes,
+ ...externalRoutes,
+ fallbackNotFoundRoute,
+];
+
+/** 基本路由列表,这些路由不需要进入权限拦截 */
+const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
+const componentKeys: string[] = Object.keys(
+ import.meta.glob('../../views/**/*.vue'),
+)
+ .filter((item) => !item.includes('/modules/'))
+ .map((v) => {
+ const path = v.replace('../../views/', '/');
+ return path.endsWith('.vue') ? path.slice(0, -4) : path;
+ });
+
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, componentKeys, coreRouteNames, routes };
diff --git a/web/apps/web-antd/src/router/routes/modules/dashboard.ts b/web/apps/web-antd/src/router/routes/modules/dashboard.ts
new file mode 100644
index 0000000..5254dc6
--- /dev/null
+++ b/web/apps/web-antd/src/router/routes/modules/dashboard.ts
@@ -0,0 +1,38 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'lucide:layout-dashboard',
+ order: -1,
+ title: $t('page.dashboard.title'),
+ },
+ name: 'Dashboard',
+ path: '/dashboard',
+ children: [
+ {
+ name: 'Analytics',
+ path: '/analytics',
+ component: () => import('#/views/dashboard/analytics/index.vue'),
+ meta: {
+ affixTab: true,
+ icon: 'lucide:area-chart',
+ title: $t('page.dashboard.analytics'),
+ },
+ },
+ {
+ name: 'Workspace',
+ path: '/workspace',
+ component: () => import('#/views/dashboard/workspace/index.vue'),
+ meta: {
+ icon: 'carbon:workspace',
+ title: $t('page.dashboard.workspace'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-antd/src/router/routes/modules/demos.ts b/web/apps/web-antd/src/router/routes/modules/demos.ts
new file mode 100644
index 0000000..55ade09
--- /dev/null
+++ b/web/apps/web-antd/src/router/routes/modules/demos.ts
@@ -0,0 +1,28 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ children: [
+ {
+ meta: {
+ title: $t('demos.antd'),
+ },
+ name: 'AntDesignDemos',
+ path: '/demos/ant-design',
+ component: () => import('#/views/demos/antd/index.vue'),
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-antd/src/router/routes/modules/system.ts b/web/apps/web-antd/src/router/routes/modules/system.ts
new file mode 100644
index 0000000..0f9bcc4
--- /dev/null
+++ b/web/apps/web-antd/src/router/routes/modules/system.ts
@@ -0,0 +1,84 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ion:settings-outline',
+ order: 9997,
+ title: $t('system.title'),
+ },
+ name: 'System',
+ path: '/system',
+ children: [
+ {
+ path: '/system/role',
+ name: 'SystemRole',
+ meta: {
+ icon: 'mdi:account-group',
+ title: $t('system.role.title'),
+ },
+ component: () => import('#/views/system/role/list.vue'),
+ },
+ {
+ path: '/system/menu',
+ name: 'SystemMenu',
+ meta: {
+ icon: 'mdi:menu',
+ title: $t('system.menu.title'),
+ },
+ component: () => import('#/views/system/menu/list.vue'),
+ },
+ {
+ path: '/system/dept',
+ name: 'SystemDept',
+ meta: {
+ icon: 'charm:organisation',
+ title: $t('system.dept.title'),
+ },
+ component: () => import('#/views/system/dept/list.vue'),
+ },
+ {
+ path: '/system/dict_type',
+ name: 'SystemDictType',
+ meta: {
+ icon: 'mdi:menu',
+ title: '字典列表',
+ },
+ component: () => import('#/views/system/dict_type/list.vue'),
+ },
+ {
+ path: '/system/dict_data',
+ name: 'SystemDictData',
+ meta: {
+ icon: 'mdi:menu',
+ title: '字典数据',
+ hideInMenu: true, // 关键配置:设置为 true 时菜单将被隐藏
+ // affix: true,
+ },
+ component: () => import('#/views/system/dict_data/list.vue'),
+ },
+ // {
+ // path: '/system/tenants',
+ // name: 'SystemTenants',
+ // meta: {
+ // icon: 'mdi:menu',
+ // title: '租户列表',
+ // },
+ // component: () => import('#/views/system/tenants/list.vue'),
+ // },
+ // {
+ // path: '/system/tenant_package',
+ // name: 'SystemTenantPackage',
+ // meta: {
+ // icon: 'charm:organisation',
+ // title: '租户套餐',
+ // },
+ // component: () => import('#/views/system/tenant_package/list.vue'),
+ // },
+ ],
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-antd/src/router/routes/modules/vben.ts b/web/apps/web-antd/src/router/routes/modules/vben.ts
new file mode 100644
index 0000000..98acf58
--- /dev/null
+++ b/web/apps/web-antd/src/router/routes/modules/vben.ts
@@ -0,0 +1,81 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import {
+ VBEN_DOC_URL,
+ VBEN_ELE_PREVIEW_URL,
+ VBEN_GITHUB_URL,
+ VBEN_LOGO_URL,
+ VBEN_NAIVE_PREVIEW_URL,
+} from '@vben/constants';
+
+import { IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ badgeType: 'dot',
+ icon: VBEN_LOGO_URL,
+ order: 9998,
+ title: $t('demos.vben.title'),
+ },
+ name: 'VbenProject',
+ path: '/vben-admin',
+ children: [
+ {
+ name: 'VbenDocument',
+ path: '/vben-admin/document',
+ component: IFrameView,
+ meta: {
+ icon: 'lucide:book-open-text',
+ link: VBEN_DOC_URL,
+ title: $t('demos.vben.document'),
+ },
+ },
+ {
+ name: 'VbenGithub',
+ path: '/vben-admin/github',
+ component: IFrameView,
+ meta: {
+ icon: 'mdi:github',
+ link: VBEN_GITHUB_URL,
+ title: 'Github',
+ },
+ },
+ {
+ name: 'VbenNaive',
+ path: '/vben-admin/naive',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:naiveui',
+ link: VBEN_NAIVE_PREVIEW_URL,
+ title: $t('demos.vben.naive-ui'),
+ },
+ },
+ {
+ name: 'VbenElementPlus',
+ path: '/vben-admin/ele',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:element',
+ link: VBEN_ELE_PREVIEW_URL,
+ title: $t('demos.vben.element-plus'),
+ },
+ },
+ ],
+ },
+ {
+ name: 'VbenAbout',
+ path: '/vben-admin/about',
+ component: () => import('#/views/_core/about/index.vue'),
+ meta: {
+ icon: 'lucide:copyright',
+ title: $t('demos.vben.about'),
+ order: 9999,
+ },
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-antd/src/store/auth.ts b/web/apps/web-antd/src/store/auth.ts
new file mode 100644
index 0000000..bd496d1
--- /dev/null
+++ b/web/apps/web-antd/src/store/auth.ts
@@ -0,0 +1,118 @@
+import type { Recordable, UserInfo } from '@vben/types';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
+
+import { notification } from 'ant-design-vue';
+import { defineStore } from 'pinia';
+
+import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
+import { $t } from '#/locales';
+
+export const useAuthStore = defineStore('auth', () => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const router = useRouter();
+
+ const loginLoading = ref(false);
+
+ /**
+ * 异步处理登录操作
+ * Asynchronously handle the login process
+ * @param params 登录表单数据
+ */
+ async function authLogin(
+ params: Recordable,
+ onSuccess?: () => Promise | void,
+ ) {
+ // 异步处理用户登录操作并获取 accessToken
+ let userInfo: null | UserInfo = null;
+ try {
+ loginLoading.value = true;
+ const { accessToken } = await loginApi(params);
+
+ // 如果成功获取到 accessToken
+ if (accessToken) {
+ accessStore.setAccessToken(accessToken);
+
+ // 获取用户信息并存储到 accessStore 中
+ const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ fetchUserInfo(),
+ getAccessCodesApi(),
+ ]);
+
+ userInfo = fetchUserInfoResult;
+
+ userStore.setUserInfo(userInfo);
+ accessStore.setAccessCodes(accessCodes);
+
+ if (accessStore.loginExpired) {
+ accessStore.setLoginExpired(false);
+ } else {
+ onSuccess
+ ? await onSuccess?.()
+ : await router.push(
+ userInfo.homePath || preferences.app.defaultHomePath,
+ );
+ }
+
+ if (userInfo?.realName) {
+ notification.success({
+ description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
+ duration: 3,
+ message: $t('authentication.loginSuccess'),
+ });
+ }
+ }
+ } finally {
+ loginLoading.value = false;
+ }
+
+ return {
+ userInfo,
+ };
+ }
+
+ async function logout(redirect: boolean = true) {
+ try {
+ await logoutApi();
+ } catch {
+ // 不做任何处理
+ }
+ resetAllStores();
+ accessStore.setLoginExpired(false);
+
+ // 回登录页带上当前路由地址
+ await router.replace({
+ path: LOGIN_PATH,
+ query: redirect
+ ? {
+ redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+ }
+ : {},
+ });
+ }
+
+ async function fetchUserInfo() {
+ let userInfo: null | UserInfo = null;
+ userInfo = await getUserInfoApi();
+ userStore.setUserInfo(userInfo);
+ return userInfo;
+ }
+
+ function $reset() {
+ loginLoading.value = false;
+ }
+
+ return {
+ $reset,
+ authLogin,
+ fetchUserInfo,
+ loginLoading,
+ logout,
+ };
+});
diff --git a/web/apps/web-antd/src/store/index.ts b/web/apps/web-antd/src/store/index.ts
new file mode 100644
index 0000000..269586e
--- /dev/null
+++ b/web/apps/web-antd/src/store/index.ts
@@ -0,0 +1 @@
+export * from './auth';
diff --git a/web/apps/web-antd/src/utils/date.ts b/web/apps/web-antd/src/utils/date.ts
new file mode 100644
index 0000000..77473cf
--- /dev/null
+++ b/web/apps/web-antd/src/utils/date.ts
@@ -0,0 +1,14 @@
+import dayjs from 'dayjs';
+
+/**
+ * 格式化 ISO 时间字符串为 'YYYY-MM-DD HH:mm:ss'
+ * @param value ISO 时间字符串
+ * @param format 自定义格式,默认 'YYYY-MM-DD HH:mm:ss'
+ */
+export function format_datetime(
+ value: Date | string,
+ format = 'YYYY-MM-DD HH:mm:ss',
+): string {
+ if (!value) return '';
+ return dayjs(value).format(format);
+}
diff --git a/web/apps/web-antd/src/utils/dict.ts b/web/apps/web-antd/src/utils/dict.ts
new file mode 100644
index 0000000..84c1aef
--- /dev/null
+++ b/web/apps/web-antd/src/utils/dict.ts
@@ -0,0 +1,5 @@
+export enum DICT_TYPE {
+ USER_TYPE = 'user_type',
+
+ // TEMU_ORDER_STATUS = 'temu_order_status',
+}
diff --git a/web/apps/web-antd/src/views/_core/README.md b/web/apps/web-antd/src/views/_core/README.md
new file mode 100644
index 0000000..8248afe
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/README.md
@@ -0,0 +1,3 @@
+# \_core
+
+此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
diff --git a/web/apps/web-antd/src/views/_core/about/index.vue b/web/apps/web-antd/src/views/_core/about/index.vue
new file mode 100644
index 0000000..0ee5243
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/about/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/authentication/code-login.vue b/web/apps/web-antd/src/views/_core/authentication/code-login.vue
new file mode 100644
index 0000000..acfd1fd
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/authentication/code-login.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/authentication/forget-password.vue b/web/apps/web-antd/src/views/_core/authentication/forget-password.vue
new file mode 100644
index 0000000..fef0d42
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/authentication/forget-password.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/authentication/login.vue b/web/apps/web-antd/src/views/_core/authentication/login.vue
new file mode 100644
index 0000000..c55eb64
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/authentication/login.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/authentication/qrcode-login.vue b/web/apps/web-antd/src/views/_core/authentication/qrcode-login.vue
new file mode 100644
index 0000000..23f5f2d
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/authentication/qrcode-login.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/authentication/register.vue b/web/apps/web-antd/src/views/_core/authentication/register.vue
new file mode 100644
index 0000000..b1a5de7
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/authentication/register.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/fallback/coming-soon.vue b/web/apps/web-antd/src/views/_core/fallback/coming-soon.vue
new file mode 100644
index 0000000..f394930
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/fallback/coming-soon.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/fallback/forbidden.vue b/web/apps/web-antd/src/views/_core/fallback/forbidden.vue
new file mode 100644
index 0000000..8ea65fe
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/fallback/forbidden.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/fallback/internal-error.vue b/web/apps/web-antd/src/views/_core/fallback/internal-error.vue
new file mode 100644
index 0000000..819a47d
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/fallback/internal-error.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/fallback/not-found.vue b/web/apps/web-antd/src/views/_core/fallback/not-found.vue
new file mode 100644
index 0000000..4d178e9
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/fallback/not-found.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/_core/fallback/offline.vue b/web/apps/web-antd/src/views/_core/fallback/offline.vue
new file mode 100644
index 0000000..5de4a88
--- /dev/null
+++ b/web/apps/web-antd/src/views/_core/fallback/offline.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue b/web/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue
new file mode 100644
index 0000000..f1f0b23
--- /dev/null
+++ b/web/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-data.vue b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-data.vue
new file mode 100644
index 0000000..190fb41
--- /dev/null
+++ b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-data.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
new file mode 100644
index 0000000..02f5091
--- /dev/null
+++ b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue
new file mode 100644
index 0000000..0915c7a
--- /dev/null
+++ b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue
new file mode 100644
index 0000000..7e0f101
--- /dev/null
+++ b/web/apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/dashboard/analytics/index.vue b/web/apps/web-antd/src/views/dashboard/analytics/index.vue
new file mode 100644
index 0000000..5e3d6d2
--- /dev/null
+++ b/web/apps/web-antd/src/views/dashboard/analytics/index.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/dashboard/workspace/index.vue b/web/apps/web-antd/src/views/dashboard/workspace/index.vue
new file mode 100644
index 0000000..b95d613
--- /dev/null
+++ b/web/apps/web-antd/src/views/dashboard/workspace/index.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+ 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
+
+ 今日晴,20℃ - 32℃!
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/demos/antd/index.vue b/web/apps/web-antd/src/views/demos/antd/index.vue
new file mode 100644
index 0000000..b3b05cc
--- /dev/null
+++ b/web/apps/web-antd/src/views/demos/antd/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ Default
+ Primary
+ Info
+ Error
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/dept/data.ts b/web/apps/web-antd/src/views/system/dept/data.ts
new file mode 100644
index 0000000..135685a
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dept/data.ts
@@ -0,0 +1,161 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemDeptApi } from '#/api/system/dept';
+
+import { z } from '#/adapter/form';
+import { getDeptList } from '#/api/system/dept';
+import { $t } from '#/locales';
+import { format_datetime } from '#/utils/date';
+/**
+ * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
+ */
+export function useSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: getDeptList,
+ class: 'w-full',
+ resultField: 'items',
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ },
+ fieldName: 'pid',
+ label: $t('system.dept.parentDept'),
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: $t('system.dept.deptName'),
+ rules: z
+ .string()
+ .min(2, $t('ui.formRules.minLength', [$t('system.dept.deptName'), 2]))
+ .max(
+ 20,
+ $t('ui.formRules.maxLength', [$t('system.dept.deptName'), 20]),
+ ),
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'sort',
+ label: '显示排序',
+ },
+ {
+ component: 'Input',
+ fieldName: 'leader',
+ label: '负责人',
+ },
+ {
+ component: 'Input',
+ fieldName: 'phone',
+ label: '联系电话',
+ },
+ {
+ component: 'Input',
+ fieldName: 'email',
+ label: '邮箱',
+ },
+ {
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ options: [
+ { label: $t('common.enabled'), value: 1 },
+ { label: $t('common.disabled'), value: 0 },
+ ],
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ fieldName: 'status',
+ label: $t('system.dept.status'),
+ },
+ {
+ component: 'Textarea',
+ componentProps: {
+ maxLength: 50,
+ rows: 3,
+ showCount: true,
+ },
+ fieldName: 'remark',
+ label: $t('system.dept.remark'),
+ rules: z
+ .string()
+ .max(50, $t('ui.formRules.maxLength', [$t('system.dept.remark'), 50]))
+ .optional(),
+ },
+ ];
+}
+
+/**
+ * 获取表格列配置
+ * @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
+ * @param onActionClick 表格操作按钮点击事件
+ */
+export function useColumns(
+ onActionClick?: OnActionClickFn,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ align: 'left',
+ field: 'name',
+ fixed: 'left',
+ title: $t('system.dept.deptName'),
+ treeNode: true,
+ width: 150,
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'status',
+ title: $t('system.dept.status'),
+ width: 100,
+ },
+ {
+ field: 'create_time',
+ title: $t('system.dept.createTime'),
+ width: 180,
+ formatter: ({ cellValue }) => format_datetime(cellValue),
+ },
+ {
+ field: 'remark',
+ title: $t('system.dept.remark'),
+ },
+ {
+ align: 'right',
+ cellRender: {
+ attrs: {
+ nameField: 'name',
+ nameTitle: $t('system.dept.name'),
+ onClick: onActionClick,
+ },
+ name: 'CellOperation',
+ options: [
+ {
+ code: 'append',
+ text: '新增下级',
+ },
+ 'edit', // 默认的编辑按钮
+ {
+ code: 'delete', // 默认的删除按钮
+ disabled: (row: SystemDeptApi.SystemDept) => {
+ return !!(row.children && row.children.length > 0);
+ },
+ },
+ ],
+ },
+ field: 'operation',
+ fixed: 'right',
+ headerAlign: 'center',
+ showOverflow: false,
+ title: $t('system.dept.operation'),
+ width: 200,
+ },
+ ];
+}
diff --git a/web/apps/web-antd/src/views/system/dept/list.vue b/web/apps/web-antd/src/views/system/dept/list.vue
new file mode 100644
index 0000000..b1b28d0
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dept/list.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+ {{ $t('ui.actionTitle.create', [$t('system.dept.name')]) }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/dept/modules/form.vue b/web/apps/web-antd/src/views/system/dept/modules/form.vue
new file mode 100644
index 0000000..8c7b5c5
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dept/modules/form.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+ {{ $t('common.reset') }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/dict_data/data.ts b/web/apps/web-antd/src/views/system/dict_data/data.ts
new file mode 100644
index 0000000..f7ca842
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dict_data/data.ts
@@ -0,0 +1,214 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemDictDataApi } from '#/api/system/dict_data';
+
+import { z } from '#/adapter/form';
+import { getDictTypeList } from '#/api/system/dict_type';
+import { $t } from '#/locales';
+
+/**
+ * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
+ */
+export function useSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'ApiSelect',
+ componentProps: {
+ allowClear: true,
+ api: getDictTypeList,
+ class: 'w-full',
+ resultField: 'items',
+ labelField: 'name',
+ valueField: 'id',
+ },
+ fieldName: 'dict_type',
+ label: '字典类型',
+ },
+ {
+ component: 'Input',
+ fieldName: 'label',
+ label: '字典标签',
+ rules: z
+ .string()
+ .min(2, $t('ui.formRules.minLength', [$t('system.dict_data.type'), 2]))
+ .max(
+ 20,
+ $t('ui.formRules.maxLength', [$t('system.dict_data.type'), 20]),
+ ),
+ },
+ {
+ component: 'Input',
+ fieldName: 'value',
+ label: '字典键值',
+ rules: z
+ .string()
+ .min(2, $t('ui.formRules.minLength', [$t('system.dict_data.type'), 2]))
+ .max(
+ 50,
+ $t('ui.formRules.maxLength', [$t('system.dict_data.type'), 50]),
+ ),
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'sort',
+ label: '字典排序',
+ },
+ {
+ component: 'ApiSelect',
+ fieldName: 'color_type',
+ label: '颜色类型',
+ componentProps: {
+ name: 'CellTag',
+ options: [
+ {
+ value: 'default',
+ label: '默认',
+ },
+ {
+ value: 'primary',
+ label: '主要',
+ },
+ {
+ value: 'success',
+ label: '成功',
+ },
+ {
+ value: 'info',
+ label: '信息',
+ },
+ {
+ value: 'warning',
+ label: '警告',
+ },
+ {
+ value: 'danger',
+ label: '危险',
+ },
+ ],
+ }
+ },
+ {
+ component: 'Input',
+ fieldName: 'css_class',
+ label: 'CSS Class',
+ },
+ {
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ options: [
+ { label: '开启', value: true },
+ { label: '关闭', value: false },
+ ],
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ fieldName: 'status',
+ label: '状态',
+ },
+ {
+ component: 'Input',
+ componentProps: {
+ maxLength: 50,
+ rows: 3,
+ showCount: true,
+ },
+ fieldName: 'remark',
+ label: '备注',
+ rules: z
+ .string()
+ .max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
+ .optional(),
+ },
+ ];
+}
+
+/**
+ * 获取表格列配置
+ * @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
+ * @param onActionClick 表格操作按钮点击事件
+ */
+export function useColumns(
+ onActionClick?: OnActionClickFn,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ align: 'left',
+ field: 'id',
+ fixed: 'left',
+ title: '字典编码',
+ treeNode: true,
+ width: 150,
+ },
+ {
+ field: 'label',
+ title: '字典标签',
+ },
+ {
+ field: 'value',
+ title: '字典键值',
+ },
+ {
+ field: 'sort',
+ title: '字典排序',
+ },
+ {
+ field: 'color_type',
+ title: '颜色类型',
+
+ },
+ {
+ field: 'css_class',
+ title: 'CSS Class',
+ },
+ {
+ cellRender: {
+ name: 'CellTag',
+ options: [
+ { label: $t('common.enabled'), value: true },
+ { label: $t('common.disabled'), value: false },
+ ],
+ },
+ field: 'status',
+ title: '状态',
+ width: 100,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ },
+ {
+ field: 'create_time',
+ title: '创建时间',
+ width: 180,
+ },
+ {
+ align: 'right',
+ cellRender: {
+ attrs: {
+ nameField: 'name',
+ nameTitle: $t('system.dict_data.name'),
+ onClick: onActionClick,
+ },
+ name: 'CellOperation',
+ options: [
+ 'edit', // 默认的编辑按钮
+ {
+ code: 'delete', // 默认的删除按钮
+ disabled: (row: SystemDictDataApi.SystemDictData) => {
+ return !!(row.children && row.children.length > 0);
+ },
+ },
+ ],
+ },
+ field: 'operation',
+ fixed: 'right',
+ headerAlign: 'center',
+ showOverflow: false,
+ title: '操作',
+ width: 200,
+ },
+ ];
+}
diff --git a/web/apps/web-antd/src/views/system/dict_data/list.vue b/web/apps/web-antd/src/views/system/dict_data/list.vue
new file mode 100644
index 0000000..8d9e40e
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dict_data/list.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+ {{ $t('ui.actionTitle.create', [$t('system.dict_data.name')]) }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/dict_data/modules/form.vue b/web/apps/web-antd/src/views/system/dict_data/modules/form.vue
new file mode 100644
index 0000000..fb65d56
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dict_data/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+ {{ $t('common.reset') }}
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/dict_type/data.ts b/web/apps/web-antd/src/views/system/dict_type/data.ts
new file mode 100644
index 0000000..4264dfa
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dict_type/data.ts
@@ -0,0 +1,148 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemDictTypeApi } from '#/api/system/dict_type';
+
+import { z } from '#/adapter/form';
+import { $t } from '#/locales';
+
+/**
+ * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
+ */
+export function useSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '字典名称',
+ rules: z
+ .string()
+ .min(2, $t('ui.formRules.minLength', [$t('system.dict_type.name'), 2]))
+ .max(
+ 20,
+ $t('ui.formRules.maxLength', [$t('system.dict_type.name'), 20]),
+ ),
+ },
+ {
+ component: 'Input',
+ fieldName: 'type',
+ label: '字典类型',
+ rules: z
+ .string()
+ .min(2, $t('ui.formRules.minLength', [$t('system.dict_type.type'), 2]))
+ .max(
+ 20,
+ $t('ui.formRules.maxLength', [$t('system.dict_type.type'), 20]),
+ ),
+ },
+ {
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ options: [
+ { label: '开启', value: true },
+ { label: '关闭', value: false },
+ ],
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ fieldName: 'status',
+ label: '状态',
+ },
+ {
+ component: 'Input',
+ componentProps: {
+ maxLength: 50,
+ rows: 3,
+ showCount: true,
+ },
+ fieldName: 'remark',
+ label: '备注',
+ rules: z
+ .string()
+ .max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
+ .optional(),
+ },
+ ];
+}
+
+/**
+ * 获取表格列配置
+ * @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
+ * @param onActionClick 表格操作按钮点击事件
+ */
+export function useColumns(
+ onActionClick?: OnActionClickFn,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ align: 'left',
+ field: 'id',
+ fixed: 'left',
+ title: '字典编号',
+ treeNode: true,
+ width: 150,
+ },
+ {
+ field: 'name',
+ title: '字典名称',
+ },
+ {
+ field: 'type',
+ title: '字典类型',
+ width: 180,
+ },
+ {
+ cellRender: {
+ name: 'CellTag',
+ options: [
+ { label: $t('common.enabled'), value: true },
+ { label: $t('common.disabled'), value: false },
+ ],
+ },
+ field: 'status',
+ title: '状态',
+ width: 100,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ },
+ {
+ field: 'create_time',
+ title: '创建时间',
+ width: 180,
+ },
+ {
+ align: 'right',
+ cellRender: {
+ attrs: {
+ nameField: 'name',
+ nameTitle: $t('system.dict_type.name'),
+ onClick: onActionClick,
+ },
+ name: 'CellOperation',
+ options: [
+ 'edit', // 默认的编辑按钮
+ {
+ code: 'view', // 新增查看详情按钮(可自定义code)
+ text: '数据', // 按钮文本(国际化)
+ },
+ {
+ code: 'delete', // 默认的删除按钮
+ disabled: (row: SystemDictTypeApi.SystemDictType) => {
+ return !!(row.children && row.children.length > 0);
+ },
+ },
+ ],
+ },
+ field: 'operation',
+ fixed: 'right',
+ headerAlign: 'center',
+ showOverflow: false,
+ title: '操作',
+ width: 200,
+ },
+ ];
+}
diff --git a/web/apps/web-antd/src/views/system/dict_type/list.vue b/web/apps/web-antd/src/views/system/dict_type/list.vue
new file mode 100644
index 0000000..835d42a
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dict_type/list.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+ {{ $t('ui.actionTitle.create', [$t('system.dict_type.name')]) }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/dict_type/modules/form.vue b/web/apps/web-antd/src/views/system/dict_type/modules/form.vue
new file mode 100644
index 0000000..6c952df
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/dict_type/modules/form.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+ {{ $t('common.reset') }}
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/menu/data.ts b/web/apps/web-antd/src/views/system/menu/data.ts
new file mode 100644
index 0000000..75190b4
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/menu/data.ts
@@ -0,0 +1,109 @@
+import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemMenuApi } from '#/api/system/menu';
+
+import { $t } from '#/locales';
+
+export function getMenuTypeOptions() {
+ return [
+ {
+ color: 'processing',
+ label: $t('system.menu.typeCatalog'),
+ value: 'catalog',
+ },
+ { color: 'default', label: $t('system.menu.typeMenu'), value: 'menu' },
+ { color: 'error', label: $t('system.menu.typeButton'), value: 'button' },
+ {
+ color: 'success',
+ label: $t('system.menu.typeEmbedded'),
+ value: 'embedded',
+ },
+ { color: 'warning', label: $t('system.menu.typeLink'), value: 'link' },
+ ];
+}
+
+export function useColumns(
+ onActionClick: OnActionClickFn,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ align: 'left',
+ field: 'meta.title',
+ fixed: 'left',
+ slots: { default: 'title' },
+ title: $t('system.menu.menuTitle'),
+ treeNode: true,
+ width: 250,
+ },
+ {
+ align: 'center',
+ cellRender: { name: 'CellTag', options: getMenuTypeOptions() },
+ field: 'type',
+ title: $t('system.menu.type'),
+ width: 100,
+ },
+ {
+ field: 'authCode',
+ title: $t('system.menu.authCode'),
+ width: 200,
+ },
+ {
+ align: 'left',
+ field: 'path',
+ title: $t('system.menu.path'),
+ width: 200,
+ },
+
+ {
+ align: 'left',
+ field: 'component',
+ formatter: ({ row }) => {
+ switch (row.type) {
+ case 'catalog':
+ case 'menu': {
+ return row.component ?? '';
+ }
+ case 'embedded': {
+ return row.meta?.iframeSrc ?? '';
+ }
+ case 'link': {
+ return row.meta?.link ?? '';
+ }
+ }
+ return '';
+ },
+ minWidth: 200,
+ title: $t('system.menu.component'),
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'status',
+ title: $t('system.menu.status'),
+ width: 100,
+ },
+
+ {
+ align: 'right',
+ cellRender: {
+ attrs: {
+ nameField: 'name',
+ onClick: onActionClick,
+ },
+ name: 'CellOperation',
+ options: [
+ {
+ code: 'append',
+ text: '新增下级',
+ },
+ 'edit', // 默认的编辑按钮
+ 'delete', // 默认的删除按钮
+ ],
+ },
+ field: 'operation',
+ fixed: 'right',
+ headerAlign: 'center',
+ showOverflow: false,
+ title: $t('system.menu.operation'),
+ width: 200,
+ },
+ ];
+}
diff --git a/web/apps/web-antd/src/views/system/menu/list.vue b/web/apps/web-antd/src/views/system/menu/list.vue
new file mode 100644
index 0000000..c34c31f
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/menu/list.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+ {{ $t('ui.actionTitle.create', [$t('system.menu.name')]) }}
+
+
+
+
+
+
+
+
+
{{ $t(row.meta?.title) }}
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/menu/modules/form.vue b/web/apps/web-antd/src/views/system/menu/modules/form.vue
new file mode 100644
index 0000000..94975b8
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/menu/modules/form.vue
@@ -0,0 +1,506 @@
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/role/data.ts b/web/apps/web-antd/src/views/system/role/data.ts
new file mode 100644
index 0000000..a9a5e8f
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/role/data.ts
@@ -0,0 +1,129 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemRoleApi } from '#/api/system/role';
+
+import { $t } from '#/locales';
+import { format_datetime } from '#/utils/date';
+
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: $t('system.role.roleName'),
+ rules: 'required',
+ },
+ {
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ options: [
+ { label: $t('common.enabled'), value: 1 },
+ { label: $t('common.disabled'), value: 0 },
+ ],
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ fieldName: 'status',
+ label: $t('system.role.status'),
+ },
+ {
+ component: 'Textarea',
+ fieldName: 'remark',
+ label: $t('system.role.remark'),
+ },
+ {
+ component: 'Input',
+ fieldName: 'permissions',
+ formItemClass: 'items-start',
+ label: $t('system.role.setPermissions'),
+ modelPropName: 'modelValue',
+ },
+ ];
+}
+
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: $t('system.role.roleName'),
+ },
+ { component: 'Input', fieldName: 'id', label: $t('system.role.id') },
+ {
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: $t('common.enabled'), value: 1 },
+ { label: $t('common.disabled'), value: 0 },
+ ],
+ },
+ fieldName: 'status',
+ label: $t('system.role.status'),
+ },
+ {
+ component: 'Input',
+ fieldName: 'remark',
+ label: $t('system.role.remark'),
+ },
+ {
+ component: 'RangePicker',
+ fieldName: 'createTime',
+ label: $t('system.role.createTime'),
+ },
+ ];
+}
+
+export function useColumns(
+ onActionClick: OnActionClickFn,
+ onStatusChange?: (newStatus: any, row: T) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: $t('system.role.roleName'),
+ width: 200,
+ },
+ {
+ field: 'id',
+ title: $t('system.role.id'),
+ width: 200,
+ },
+ {
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: onStatusChange ? 'CellSwitch' : 'CellTag',
+ },
+ field: 'status',
+ title: $t('system.role.status'),
+ width: 100,
+ },
+ {
+ field: 'remark',
+ minWidth: 100,
+ title: $t('system.role.remark'),
+ },
+ {
+ field: 'create_time',
+ title: $t('system.role.createTime'),
+ width: 200,
+ formatter: ({ cellValue }) => format_datetime(cellValue),
+ },
+ {
+ align: 'center',
+ cellRender: {
+ attrs: {
+ nameField: 'name',
+ nameTitle: $t('system.role.name'),
+ onClick: onActionClick,
+ },
+ name: 'CellOperation',
+ },
+ field: 'operation',
+ fixed: 'right',
+ title: $t('system.role.operation'),
+ width: 130,
+ },
+ ];
+}
diff --git a/web/apps/web-antd/src/views/system/role/list.vue b/web/apps/web-antd/src/views/system/role/list.vue
new file mode 100644
index 0000000..99932b1
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/role/list.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+ {{ $t('ui.actionTitle.create', [$t('system.role.name')]) }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/role/modules/form.vue b/web/apps/web-antd/src/views/system/role/modules/form.vue
new file mode 100644
index 0000000..6afae5f
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/role/modules/form.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/tenant_package/data.ts b/web/apps/web-antd/src/views/system/tenant_package/data.ts
new file mode 100644
index 0000000..92b84d9
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/tenant_package/data.ts
@@ -0,0 +1,133 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemTenantPackageApi } from '#/api/system/tenant_package';
+
+import { z } from '#/adapter/form';
+import { getTenantPackageList } from '#/api/system/tenant_package';
+import { $t } from '#/locales';
+
+/**
+ * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
+ */
+export function useSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '套餐名',
+ rules: z
+ .string()
+ .min(2, $t('ui.formRules.minLength', [$t('system.dept.deptName'), 2]))
+ .max(
+ 20,
+ $t('ui.formRules.maxLength', [$t('system.dept.deptName'), 20]),
+ ),
+ },
+ // 菜单权限
+ {
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: getTenantPackageList,
+ class: 'w-full',
+ resultField: 'items',
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ },
+ fieldName: 'pid',
+ label: $t('system.dept.parentDept'),
+ },
+ {
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ options: [
+ { label: '开启', value: 1 },
+ { label: '关闭', value: 0 },
+ ],
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ fieldName: 'status',
+ label: '状态',
+ },
+ {
+ component: 'Input',
+ componentProps: {
+ maxLength: 50,
+ rows: 3,
+ showCount: true,
+ },
+ fieldName: 'remark',
+ label: '备注',
+ rules: z
+ .string()
+ .max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
+ .optional(),
+ },
+ ];
+}
+
+/**
+ * 获取表格列配置
+ * @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
+ * @param onActionClick 表格操作按钮点击事件
+ */
+export function useColumns(
+ onActionClick?: OnActionClickFn,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ align: 'left',
+ field: 'name',
+ fixed: 'left',
+ title: '套餐名',
+ treeNode: true,
+ width: 150,
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'status',
+ title: '状态',
+ width: 100,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ },
+ {
+ field: 'create_time',
+ title: '创建时间',
+ width: 180,
+ },
+ {
+ align: 'right',
+ cellRender: {
+ attrs: {
+ nameField: 'name',
+ nameTitle: $t('system.dept.name'),
+ onClick: onActionClick,
+ },
+ name: 'CellOperation',
+ options: [
+ 'edit', // 默认的编辑按钮
+ {
+ code: 'delete', // 默认的删除按钮
+ disabled: (row: SystemTenantPackageApi.SystemTenantPackage) => {
+ return !!(row.children && row.children.length > 0);
+ },
+ },
+ ],
+ },
+ field: 'operation',
+ fixed: 'right',
+ headerAlign: 'center',
+ showOverflow: false,
+ title: '操作',
+ width: 200,
+ },
+ ];
+}
diff --git a/web/apps/web-antd/src/views/system/tenant_package/list.vue b/web/apps/web-antd/src/views/system/tenant_package/list.vue
new file mode 100644
index 0000000..fbca1d9
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/tenant_package/list.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+ {{ $t('ui.actionTitle.create', [$t('system.tenants_package.name')]) }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/tenant_package/modules/form.vue b/web/apps/web-antd/src/views/system/tenant_package/modules/form.vue
new file mode 100644
index 0000000..3a2558f
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/tenant_package/modules/form.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+ {{ $t('common.reset') }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/tenants/data.ts b/web/apps/web-antd/src/views/system/tenants/data.ts
new file mode 100644
index 0000000..a4fccdc
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/tenants/data.ts
@@ -0,0 +1,157 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemTenantsApi } from '#/api/system/tenants';
+
+import { z } from '#/adapter/form';
+import { $t } from '#/locales';
+
+/**
+ * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
+ */
+export function useSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: $t('system.tenant.tenantName'),
+ rules: z
+ .string()
+ .min(
+ 2,
+ $t('ui.formRules.minLength', [$t('system.tenant.tenantName'), 2]),
+ )
+ .max(
+ 20,
+ $t('ui.formRules.maxLength', [$t('system.tenant.tenantName'), 20]),
+ ),
+ },
+ {
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ options: [
+ { label: $t('common.enabled'), value: 1 },
+ { label: $t('common.disabled'), value: 0 },
+ ],
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ fieldName: 'status',
+ label: $t('system.status'),
+ },
+ {
+ component: 'Textarea',
+ componentProps: {
+ maxLength: 50,
+ rows: 3,
+ showCount: true,
+ },
+ fieldName: 'remark',
+ label: $t('system.remark'),
+ rules: z
+ .string()
+ .max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
+ .optional(),
+ },
+ ];
+}
+
+/**
+ * 获取表格列配置
+ * @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
+ * @param onActionClick 表格操作按钮点击事件
+ */
+export function useColumns(
+ onActionClick?: OnActionClickFn,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ align: 'left',
+ field: 'name',
+ fixed: 'left',
+ title: $t('system.tenant.tenantName'),
+ treeNode: true,
+ width: 150,
+ },
+ // `contact_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系人',
+ // `contact_mobile` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系手机',
+ // `status` tinyint NOT NULL DEFAULT '0' COMMENT '租户状态(0正常 1停用)',
+ // `website` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '绑定域名',
+ // `package_id` bigint NOT NULL COMMENT '租户套餐编号',
+ // `expire_time` datetime NOT NULL COMMENT '过期时间',
+ // `account_count` int NOT NULL COMMENT '账号数量',
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'contact_name',
+ title: $t('system.tenant.contact_mobile'),
+ width: 100,
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'website',
+ title: $t('system.tenant.website'),
+ width: 100,
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'package_id',
+ title: $t('system.tenant.package_id'),
+ width: 100,
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'expire_time',
+ title: $t('system.tenant.expire_time'),
+ width: 100,
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'account_count',
+ title: $t('system.tenant.account_count'),
+ width: 100,
+ },
+ {
+ cellRender: { name: 'CellTag' },
+ field: 'status',
+ title: $t('system.status'),
+ width: 100,
+ },
+ {
+ field: 'create_time',
+ title: $t('system.createTime'),
+ width: 180,
+ },
+ {
+ field: 'remark',
+ title: $t('system.remark'),
+ },
+ {
+ align: 'right',
+ cellRender: {
+ attrs: {
+ nameField: 'name',
+ nameTitle: $t('system.name'),
+ onClick: onActionClick,
+ },
+ name: 'CellOperation',
+ options: [
+ 'edit', // 默认的编辑按钮
+ {
+ code: 'delete', // 默认的删除按钮
+ disabled: (row: SystemTenantsApi.SystemTenants) => {
+ return !!(row.children && row.children.length > 0);
+ },
+ },
+ ],
+ },
+ field: 'operation',
+ fixed: 'right',
+ headerAlign: 'center',
+ showOverflow: false,
+ title: $t('system.operation'),
+ width: 200,
+ },
+ ];
+}
diff --git a/web/apps/web-antd/src/views/system/tenants/list.vue b/web/apps/web-antd/src/views/system/tenants/list.vue
new file mode 100644
index 0000000..926d0d1
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/tenants/list.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+ {{ $t('ui.actionTitle.create', [$t('system.tenants.name')]) }}
+
+
+
+
+
diff --git a/web/apps/web-antd/src/views/system/tenants/modules/form.vue b/web/apps/web-antd/src/views/system/tenants/modules/form.vue
new file mode 100644
index 0000000..a420970
--- /dev/null
+++ b/web/apps/web-antd/src/views/system/tenants/modules/form.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+ {{ $t('common.reset') }}
+
+
+
+
+
diff --git a/web/apps/web-antd/tailwind.config.mjs b/web/apps/web-antd/tailwind.config.mjs
new file mode 100644
index 0000000..f17f556
--- /dev/null
+++ b/web/apps/web-antd/tailwind.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config';
diff --git a/web/apps/web-antd/tsconfig.json b/web/apps/web-antd/tsconfig.json
new file mode 100644
index 0000000..02c287f
--- /dev/null
+++ b/web/apps/web-antd/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web-app.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "#/*": ["./src/*"]
+ }
+ },
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/web/apps/web-antd/tsconfig.node.json b/web/apps/web-antd/tsconfig.node.json
new file mode 100644
index 0000000..c2f0d86
--- /dev/null
+++ b/web/apps/web-antd/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/node.json",
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "noEmit": false
+ },
+ "include": ["vite.config.mts"]
+}
diff --git a/web/apps/web-antd/vite.config.mts b/web/apps/web-antd/vite.config.mts
new file mode 100644
index 0000000..b6360f1
--- /dev/null
+++ b/web/apps/web-antd/vite.config.mts
@@ -0,0 +1,20 @@
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ vite: {
+ server: {
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // mock代理目标地址
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});
diff --git a/web/apps/web-ele/.env.analyze b/web/apps/web-ele/.env.analyze
new file mode 100644
index 0000000..ffafa8d
--- /dev/null
+++ b/web/apps/web-ele/.env.analyze
@@ -0,0 +1,7 @@
+# public path
+VITE_BASE=/
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/api
+
+VITE_VISUALIZER=true
diff --git a/web/apps/web-ele/.env.development b/web/apps/web-ele/.env.development
new file mode 100644
index 0000000..8bcb432
--- /dev/null
+++ b/web/apps/web-ele/.env.development
@@ -0,0 +1,16 @@
+# 端口号
+VITE_PORT=5777
+
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=/api
+
+# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
+VITE_NITRO_MOCK=true
+
+# 是否打开 devtools,true 为打开,false 为关闭
+VITE_DEVTOOLS=false
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
diff --git a/web/apps/web-ele/.env.production b/web/apps/web-ele/.env.production
new file mode 100644
index 0000000..5375847
--- /dev/null
+++ b/web/apps/web-ele/.env.production
@@ -0,0 +1,19 @@
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+
+# 是否开启压缩,可以设置为 none, brotli, gzip
+VITE_COMPRESS=none
+
+# 是否开启 PWA
+VITE_PWA=false
+
+# vue-router 的模式
+VITE_ROUTER_HISTORY=hash
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
+
+# 打包后是否生成dist.zip
+VITE_ARCHIVER=true
diff --git a/web/apps/web-ele/index.html b/web/apps/web-ele/index.html
new file mode 100644
index 0000000..2b59b8d
--- /dev/null
+++ b/web/apps/web-ele/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+ <%= VITE_APP_TITLE %>
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-ele/package.json b/web/apps/web-ele/package.json
new file mode 100644
index 0000000..386c368
--- /dev/null
+++ b/web/apps/web-ele/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@vben/web-ele",
+ "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-ele"
+ },
+ "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:",
+ "dayjs": "catalog:",
+ "element-plus": "catalog:",
+ "pinia": "catalog:",
+ "vue": "catalog:",
+ "vue-router": "catalog:"
+ },
+ "devDependencies": {
+ "unplugin-element-plus": "catalog:"
+ }
+}
diff --git a/web/apps/web-ele/postcss.config.mjs b/web/apps/web-ele/postcss.config.mjs
new file mode 100644
index 0000000..3d80704
--- /dev/null
+++ b/web/apps/web-ele/postcss.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';
diff --git a/web/apps/web-ele/public/favicon.ico b/web/apps/web-ele/public/favicon.ico
new file mode 100644
index 0000000..fcf9818
Binary files /dev/null and b/web/apps/web-ele/public/favicon.ico differ
diff --git a/web/apps/web-ele/src/adapter/component/index.ts b/web/apps/web-ele/src/adapter/component/index.ts
new file mode 100644
index 0000000..e2f533c
--- /dev/null
+++ b/web/apps/web-ele/src/adapter/component/index.ts
@@ -0,0 +1,338 @@
+/**
+ * 通用组件共同的使用的基础组件,原先放在 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 { ElNotification } from 'element-plus';
+
+const ElButton = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/button/index'),
+ import('element-plus/es/components/button/style/css'),
+ ]).then(([res]) => res.ElButton),
+);
+const ElCheckbox = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/checkbox/index'),
+ import('element-plus/es/components/checkbox/style/css'),
+ ]).then(([res]) => res.ElCheckbox),
+);
+const ElCheckboxButton = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/checkbox/index'),
+ import('element-plus/es/components/checkbox-button/style/css'),
+ ]).then(([res]) => res.ElCheckboxButton),
+);
+const ElCheckboxGroup = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/checkbox/index'),
+ import('element-plus/es/components/checkbox-group/style/css'),
+ ]).then(([res]) => res.ElCheckboxGroup),
+);
+const ElDatePicker = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/date-picker/index'),
+ import('element-plus/es/components/date-picker/style/css'),
+ ]).then(([res]) => res.ElDatePicker),
+);
+const ElDivider = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/divider/index'),
+ import('element-plus/es/components/divider/style/css'),
+ ]).then(([res]) => res.ElDivider),
+);
+const ElInput = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/input/index'),
+ import('element-plus/es/components/input/style/css'),
+ ]).then(([res]) => res.ElInput),
+);
+const ElInputNumber = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/input-number/index'),
+ import('element-plus/es/components/input-number/style/css'),
+ ]).then(([res]) => res.ElInputNumber),
+);
+const ElRadio = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/radio/index'),
+ import('element-plus/es/components/radio/style/css'),
+ ]).then(([res]) => res.ElRadio),
+);
+const ElRadioButton = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/radio/index'),
+ import('element-plus/es/components/radio-button/style/css'),
+ ]).then(([res]) => res.ElRadioButton),
+);
+const ElRadioGroup = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/radio/index'),
+ import('element-plus/es/components/radio-group/style/css'),
+ ]).then(([res]) => res.ElRadioGroup),
+);
+const ElSelectV2 = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/select-v2/index'),
+ import('element-plus/es/components/select-v2/style/css'),
+ ]).then(([res]) => res.ElSelectV2),
+);
+const ElSpace = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/space/index'),
+ import('element-plus/es/components/space/style/css'),
+ ]).then(([res]) => res.ElSpace),
+);
+const ElSwitch = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/switch/index'),
+ import('element-plus/es/components/switch/style/css'),
+ ]).then(([res]) => res.ElSwitch),
+);
+const ElTimePicker = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/time-picker/index'),
+ import('element-plus/es/components/time-picker/style/css'),
+ ]).then(([res]) => res.ElTimePicker),
+);
+const ElTreeSelect = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/tree-select/index'),
+ import('element-plus/es/components/tree-select/style/css'),
+ ]).then(([res]) => res.ElTreeSelect),
+);
+const ElUpload = defineAsyncComponent(() =>
+ Promise.all([
+ import('element-plus/es/components/upload/index'),
+ import('element-plus/es/components/upload/style/css'),
+ ]).then(([res]) => res.ElUpload),
+);
+
+const withDefaultPlaceholder = (
+ component: T,
+ type: 'input' | 'select',
+ componentProps: Recordable = {},
+) => {
+ 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 = {};
+ 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'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'DatePicker'
+ | 'Divider'
+ | 'IconPicker'
+ | 'Input'
+ | 'InputNumber'
+ | 'RadioGroup'
+ | 'Select'
+ | 'Space'
+ | 'Switch'
+ | 'TimePicker'
+ | 'TreeSelect'
+ | 'Upload'
+ | BaseFormComponentType;
+
+async function initComponentAdapter() {
+ const components: Partial> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // Button: () =>
+ // import('xxx').then((res) => res.Button),
+ ApiSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiSelect',
+ },
+ 'select',
+ {
+ component: ElSelectV2,
+ loadingSlot: 'loading',
+ visibleEvent: 'onVisibleChange',
+ },
+ ),
+ ApiTreeSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiTreeSelect',
+ },
+ 'select',
+ {
+ component: ElTreeSelect,
+ props: { label: 'label', children: 'children' },
+ nodeKey: 'value',
+ loadingSlot: 'loading',
+ optionsPropName: 'data',
+ visibleEvent: 'onVisibleChange',
+ },
+ ),
+ Checkbox: ElCheckbox,
+ CheckboxGroup: (props, { attrs, slots }) => {
+ let defaultSlot;
+ if (Reflect.has(slots, 'default')) {
+ defaultSlot = slots.default;
+ } else {
+ const { options, isButton } = attrs;
+ if (Array.isArray(options)) {
+ defaultSlot = () =>
+ options.map((option) =>
+ h(isButton ? ElCheckboxButton : ElCheckbox, option),
+ );
+ }
+ }
+ return h(
+ ElCheckboxGroup,
+ { ...props, ...attrs },
+ { ...slots, default: defaultSlot },
+ );
+ },
+ // 自定义默认按钮
+ DefaultButton: (props, { attrs, slots }) => {
+ return h(ElButton, { ...props, attrs, type: 'info' }, slots);
+ },
+ // 自定义主要按钮
+ PrimaryButton: (props, { attrs, slots }) => {
+ return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
+ },
+ Divider: ElDivider,
+ IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
+ iconSlot: 'append',
+ modelValueProp: 'model-value',
+ inputComponent: ElInput,
+ }),
+ Input: withDefaultPlaceholder(ElInput, 'input'),
+ InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
+ RadioGroup: (props, { attrs, slots }) => {
+ let defaultSlot;
+ if (Reflect.has(slots, 'default')) {
+ defaultSlot = slots.default;
+ } else {
+ const { options } = attrs;
+ if (Array.isArray(options)) {
+ defaultSlot = () =>
+ options.map((option) =>
+ h(attrs.isButton ? ElRadioButton : ElRadio, option),
+ );
+ }
+ }
+ return h(
+ ElRadioGroup,
+ { ...props, ...attrs },
+ { ...slots, default: defaultSlot },
+ );
+ },
+ Select: (props, { attrs, slots }) => {
+ return h(ElSelectV2, { ...props, attrs }, slots);
+ },
+ Space: ElSpace,
+ Switch: ElSwitch,
+ TimePicker: (props, { attrs, slots }) => {
+ const { name, id, isRange } = props;
+ const extraProps: Recordable = {};
+ if (isRange) {
+ if (name && !Array.isArray(name)) {
+ extraProps.name = [name, `${name}_end`];
+ }
+ if (id && !Array.isArray(id)) {
+ extraProps.id = [id, `${id}_end`];
+ }
+ }
+ return h(
+ ElTimePicker,
+ {
+ ...props,
+ ...attrs,
+ ...extraProps,
+ },
+ slots,
+ );
+ },
+ DatePicker: (props, { attrs, slots }) => {
+ const { name, id, type } = props;
+ const extraProps: Recordable = {};
+ if (type && type.includes('range')) {
+ if (name && !Array.isArray(name)) {
+ extraProps.name = [name, `${name}_end`];
+ }
+ if (id && !Array.isArray(id)) {
+ extraProps.id = [id, `${id}_end`];
+ }
+ }
+ return h(
+ ElDatePicker,
+ {
+ ...props,
+ ...attrs,
+ ...extraProps,
+ },
+ slots,
+ );
+ },
+ TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
+ Upload: ElUpload,
+ };
+
+ // 将组件注册到全局共享状态中
+ globalShareState.setComponents(components);
+
+ // 定义全局共享状态中的消息提示
+ globalShareState.defineMessage({
+ // 复制成功消息提示
+ copyPreferencesSuccess: (title, content) => {
+ ElNotification({
+ title,
+ message: content,
+ position: 'bottom-right',
+ duration: 0,
+ type: 'success',
+ });
+ },
+ });
+}
+
+export { initComponentAdapter };
diff --git a/web/apps/web-ele/src/adapter/form.ts b/web/apps/web-ele/src/adapter/form.ts
new file mode 100644
index 0000000..936c3fe
--- /dev/null
+++ b/web/apps/web-ele/src/adapter/form.ts
@@ -0,0 +1,41 @@
+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({
+ config: {
+ modelPropNameMap: {
+ Upload: 'fileList',
+ CheckboxGroup: 'model-value',
+ },
+ },
+ 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;
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema = FormSchema;
+export type { VbenFormProps };
diff --git a/web/apps/web-ele/src/adapter/vxe-table.ts b/web/apps/web-ele/src/adapter/vxe-table.ts
new file mode 100644
index 0000000..40b8179
--- /dev/null
+++ b/web/apps/web-ele/src/adapter/vxe-table.ts
@@ -0,0 +1,70 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { ElButton, ElImage } from 'element-plus';
+
+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;
+ const src = row[column.field];
+ return h(ElImage, { src, previewSrcList: [src] });
+ },
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellLink' },
+ vxeUI.renderer.add('CellLink', {
+ renderTableDefault(renderOpts) {
+ const { props } = renderOpts;
+ return h(
+ ElButton,
+ { size: 'small', link: true },
+ { default: () => props?.text },
+ );
+ },
+ });
+
+ // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+ // vxeUI.formats.add
+ },
+ useVbenForm,
+});
+
+export { useVbenVxeGrid };
+
+export type * from '@vben/plugins/vxe-table';
diff --git a/web/apps/web-ele/src/api/core/auth.ts b/web/apps/web-ele/src/api/core/auth.ts
new file mode 100644
index 0000000..71d9f99
--- /dev/null
+++ b/web/apps/web-ele/src/api/core/auth.ts
@@ -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('/auth/login', data);
+}
+
+/**
+ * 刷新accessToken
+ */
+export async function refreshTokenApi() {
+ return baseRequestClient.post('/auth/refresh', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 退出登录
+ */
+export async function logoutApi() {
+ return baseRequestClient.post('/auth/logout', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 获取用户权限码
+ */
+export async function getAccessCodesApi() {
+ return requestClient.get('/auth/codes');
+}
diff --git a/web/apps/web-ele/src/api/core/index.ts b/web/apps/web-ele/src/api/core/index.ts
new file mode 100644
index 0000000..28a5aef
--- /dev/null
+++ b/web/apps/web-ele/src/api/core/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './menu';
+export * from './user';
diff --git a/web/apps/web-ele/src/api/core/menu.ts b/web/apps/web-ele/src/api/core/menu.ts
new file mode 100644
index 0000000..9ef60b1
--- /dev/null
+++ b/web/apps/web-ele/src/api/core/menu.ts
@@ -0,0 +1,10 @@
+import type { RouteRecordStringComponent } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户所有菜单
+ */
+export async function getAllMenusApi() {
+ return requestClient.get('/menu/all');
+}
diff --git a/web/apps/web-ele/src/api/core/user.ts b/web/apps/web-ele/src/api/core/user.ts
new file mode 100644
index 0000000..7e28ea8
--- /dev/null
+++ b/web/apps/web-ele/src/api/core/user.ts
@@ -0,0 +1,10 @@
+import type { UserInfo } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户信息
+ */
+export async function getUserInfoApi() {
+ return requestClient.get('/user/info');
+}
diff --git a/web/apps/web-ele/src/api/index.ts b/web/apps/web-ele/src/api/index.ts
new file mode 100644
index 0000000..4b0e041
--- /dev/null
+++ b/web/apps/web-ele/src/api/index.ts
@@ -0,0 +1 @@
+export * from './core';
diff --git a/web/apps/web-ele/src/api/request.ts b/web/apps/web-ele/src/api/request.ts
new file mode 100644
index 0000000..203b35b
--- /dev/null
+++ b/web/apps/web-ele/src/api/request.ts
@@ -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 { ElMessage } from 'element-plus';
+
+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 ?? '';
+ // 如果没有错误信息,则会根据状态码进行提示
+ ElMessage.error(errorMessage || msg);
+ }),
+ );
+
+ return client;
+}
+
+export const requestClient = createRequestClient(apiURL, {
+ responseReturn: 'data',
+});
+
+export const baseRequestClient = new RequestClient({ baseURL: apiURL });
diff --git a/web/apps/web-ele/src/app.vue b/web/apps/web-ele/src/app.vue
new file mode 100644
index 0000000..1217658
--- /dev/null
+++ b/web/apps/web-ele/src/app.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/web/apps/web-ele/src/bootstrap.ts b/web/apps/web-ele/src/bootstrap.ts
new file mode 100644
index 0000000..e5befb5
--- /dev/null
+++ b/web/apps/web-ele/src/bootstrap.ts
@@ -0,0 +1,79 @@
+import { createApp, watchEffect } from 'vue';
+
+import { registerAccessDirective } from '@vben/access';
+import { registerLoadingDirective } from '@vben/common-ui';
+import { preferences } from '@vben/preferences';
+import { initStores } from '@vben/stores';
+import '@vben/styles';
+import '@vben/styles/ele';
+
+import { useTitle } from '@vueuse/core';
+import { ElLoading } from 'element-plus';
+
+import { $t, setupI18n } from '#/locales';
+
+import { initComponentAdapter } from './adapter/component';
+import { initSetupVbenForm } from './adapter/form';
+import App from './app.vue';
+import { router } from './router';
+
+async function bootstrap(namespace: string) {
+ // 初始化组件适配器
+ await initComponentAdapter();
+
+ // 初始化表单组件
+ await initSetupVbenForm();
+
+ // // 设置弹窗的默认配置
+ // setDefaultModalProps({
+ // fullscreenButton: false,
+ // });
+ // // 设置抽屉的默认配置
+ // setDefaultDrawerProps({
+ // zIndex: 2000,
+ // });
+ const app = createApp(App);
+
+ // 注册Element Plus提供的v-loading指令
+ app.directive('loading', ElLoading.directive);
+
+ // 注册Vben提供的v-loading和v-spinning指令
+ registerLoadingDirective(app, {
+ loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令
+ spinning: 'spinning',
+ });
+
+ // 国际化 i18n 配置
+ await setupI18n(app);
+
+ // 配置 pinia-tore
+ await initStores(app, { namespace });
+
+ // 安装权限指令
+ registerAccessDirective(app);
+
+ // 初始化 tippy
+ const { initTippy } = await import('@vben/common-ui/es/tippy');
+ initTippy(app);
+
+ // 配置路由及路由守卫
+ app.use(router);
+
+ // 配置Motion插件
+ const { MotionPlugin } = await import('@vben/plugins/motion');
+ app.use(MotionPlugin);
+
+ // 动态更新标题
+ watchEffect(() => {
+ if (preferences.app.dynamicTitle) {
+ const routeTitle = router.currentRoute.value.meta?.title;
+ const pageTitle =
+ (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+ useTitle(pageTitle);
+ }
+ });
+
+ app.mount('#app');
+}
+
+export { bootstrap };
diff --git a/web/apps/web-ele/src/layouts/auth.vue b/web/apps/web-ele/src/layouts/auth.vue
new file mode 100644
index 0000000..18d415b
--- /dev/null
+++ b/web/apps/web-ele/src/layouts/auth.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-ele/src/layouts/basic.vue b/web/apps/web-ele/src/layouts/basic.vue
new file mode 100644
index 0000000..1481dc5
--- /dev/null
+++ b/web/apps/web-ele/src/layouts/basic.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-ele/src/layouts/index.ts b/web/apps/web-ele/src/layouts/index.ts
new file mode 100644
index 0000000..a432078
--- /dev/null
+++ b/web/apps/web-ele/src/layouts/index.ts
@@ -0,0 +1,6 @@
+const BasicLayout = () => import('./basic.vue');
+const AuthPageLayout = () => import('./auth.vue');
+
+const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
+
+export { AuthPageLayout, BasicLayout, IFrameView };
diff --git a/web/apps/web-ele/src/locales/README.md b/web/apps/web-ele/src/locales/README.md
new file mode 100644
index 0000000..7b45103
--- /dev/null
+++ b/web/apps/web-ele/src/locales/README.md
@@ -0,0 +1,3 @@
+# locale
+
+每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
diff --git a/web/apps/web-ele/src/locales/index.ts b/web/apps/web-ele/src/locales/index.ts
new file mode 100644
index 0000000..57b87df
--- /dev/null
+++ b/web/apps/web-ele/src/locales/index.ts
@@ -0,0 +1,102 @@
+import type { Language } from 'element-plus/es/locale';
+
+import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
+import { ref } from 'vue';
+
+import {
+ $t,
+ setupI18n as coreSetup,
+ loadLocalesMapFromDir,
+} from '@vben/locales';
+import { preferences } from '@vben/preferences';
+
+import dayjs from 'dayjs';
+import enLocale from 'element-plus/es/locale/lang/en';
+import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
+
+const elementLocale = ref(defaultLocale);
+
+const modules = import.meta.glob('./langs/**/*.json');
+
+const localesMap = loadLocalesMapFromDir(
+ /\.\/langs\/([^/]+)\/(.*)\.json$/,
+ modules,
+);
+/**
+ * 加载应用特有的语言包
+ * 这里也可以改造为从服务端获取翻译数据
+ * @param lang
+ */
+async function loadMessages(lang: SupportedLanguagesType) {
+ const [appLocaleMessages] = await Promise.all([
+ localesMap[lang]?.(),
+ loadThirdPartyMessage(lang),
+ ]);
+ return appLocaleMessages?.default;
+}
+
+/**
+ * 加载第三方组件库的语言包
+ * @param lang
+ */
+async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
+ await Promise.all([loadElementLocale(lang), loadDayjsLocale(lang)]);
+}
+
+/**
+ * 加载dayjs的语言包
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+ let locale;
+ switch (lang) {
+ case 'en-US': {
+ locale = await import('dayjs/locale/en');
+ break;
+ }
+ case 'zh-CN': {
+ locale = await import('dayjs/locale/zh-cn');
+ break;
+ }
+ // 默认使用英语
+ default: {
+ locale = await import('dayjs/locale/en');
+ }
+ }
+ if (locale) {
+ dayjs.locale(locale);
+ } else {
+ console.error(`Failed to load dayjs locale for ${lang}`);
+ }
+}
+
+/**
+ * 加载element-plus的语言包
+ * @param lang
+ */
+async function loadElementLocale(lang: SupportedLanguagesType) {
+ switch (lang) {
+ case 'en-US': {
+ elementLocale.value = enLocale;
+ break;
+ }
+ case 'zh-CN': {
+ elementLocale.value = defaultLocale;
+ break;
+ }
+ }
+}
+
+async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD,
+ ...options,
+ });
+}
+
+export { $t, elementLocale, setupI18n };
diff --git a/web/apps/web-ele/src/locales/langs/en-US/demos.json b/web/apps/web-ele/src/locales/langs/en-US/demos.json
new file mode 100644
index 0000000..6eddebb
--- /dev/null
+++ b/web/apps/web-ele/src/locales/langs/en-US/demos.json
@@ -0,0 +1,13 @@
+{
+ "title": "Demos",
+ "elementPlus": "Element Plus",
+ "form": "Form",
+ "vben": {
+ "title": "Project",
+ "about": "About",
+ "document": "Document",
+ "antdv": "Ant Design Vue Version",
+ "naive-ui": "Naive UI Version",
+ "element-plus": "Element Plus Version"
+ }
+}
diff --git a/web/apps/web-ele/src/locales/langs/en-US/page.json b/web/apps/web-ele/src/locales/langs/en-US/page.json
new file mode 100644
index 0000000..618a258
--- /dev/null
+++ b/web/apps/web-ele/src/locales/langs/en-US/page.json
@@ -0,0 +1,14 @@
+{
+ "auth": {
+ "login": "Login",
+ "register": "Register",
+ "codeLogin": "Code Login",
+ "qrcodeLogin": "Qr Code Login",
+ "forgetPassword": "Forget Password"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "analytics": "Analytics",
+ "workspace": "Workspace"
+ }
+}
diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/demos.json b/web/apps/web-ele/src/locales/langs/zh-CN/demos.json
new file mode 100644
index 0000000..ba6d6cc
--- /dev/null
+++ b/web/apps/web-ele/src/locales/langs/zh-CN/demos.json
@@ -0,0 +1,13 @@
+{
+ "title": "演示",
+ "elementPlus": "Element Plus",
+ "form": "表单演示",
+ "vben": {
+ "title": "项目",
+ "about": "关于",
+ "document": "文档",
+ "antdv": "Ant Design Vue 版本",
+ "naive-ui": "Naive UI 版本",
+ "element-plus": "Element Plus 版本"
+ }
+}
diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/page.json b/web/apps/web-ele/src/locales/langs/zh-CN/page.json
new file mode 100644
index 0000000..4cb6708
--- /dev/null
+++ b/web/apps/web-ele/src/locales/langs/zh-CN/page.json
@@ -0,0 +1,14 @@
+{
+ "auth": {
+ "login": "登录",
+ "register": "注册",
+ "codeLogin": "验证码登录",
+ "qrcodeLogin": "二维码登录",
+ "forgetPassword": "忘记密码"
+ },
+ "dashboard": {
+ "title": "概览",
+ "analytics": "分析页",
+ "workspace": "工作台"
+ }
+}
diff --git a/web/apps/web-ele/src/main.ts b/web/apps/web-ele/src/main.ts
new file mode 100644
index 0000000..5d728a0
--- /dev/null
+++ b/web/apps/web-ele/src/main.ts
@@ -0,0 +1,31 @@
+import { initPreferences } from '@vben/preferences';
+import { unmountGlobalLoading } from '@vben/utils';
+
+import { overridesPreferences } from './preferences';
+
+/**
+ * 应用初始化完成之后再进行页面加载渲染
+ */
+async function initApplication() {
+ // name用于指定项目唯一标识
+ // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
+ const env = import.meta.env.PROD ? 'prod' : 'dev';
+ const appVersion = import.meta.env.VITE_APP_VERSION;
+ const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
+
+ // app偏好设置初始化
+ await initPreferences({
+ namespace,
+ overrides: overridesPreferences,
+ });
+
+ // 启动应用并挂载
+ // vue应用主要逻辑及视图
+ const { bootstrap } = await import('./bootstrap');
+ await bootstrap(namespace);
+
+ // 移除并销毁loading
+ unmountGlobalLoading();
+}
+
+initApplication();
diff --git a/web/apps/web-ele/src/preferences.ts b/web/apps/web-ele/src/preferences.ts
new file mode 100644
index 0000000..b2e9ace
--- /dev/null
+++ b/web/apps/web-ele/src/preferences.ts
@@ -0,0 +1,13 @@
+import { defineOverridesPreferences } from '@vben/preferences';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ * !!! 更改配置后请清空缓存,否则可能不生效
+ */
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ name: import.meta.env.VITE_APP_TITLE,
+ },
+});
diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts
new file mode 100644
index 0000000..2d07c89
--- /dev/null
+++ b/web/apps/web-ele/src/router/access.ts
@@ -0,0 +1,42 @@
+import type {
+ ComponentRecordType,
+ GenerateMenuAndRoutesOptions,
+} from '@vben/types';
+
+import { generateAccessible } from '@vben/access';
+import { preferences } from '@vben/preferences';
+
+import { ElMessage } from 'element-plus';
+
+import { getAllMenusApi } from '#/api';
+import { BasicLayout, IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
+
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
+
+ const layoutMap: ComponentRecordType = {
+ BasicLayout,
+ IFrameView,
+ };
+
+ return await generateAccessible(preferences.app.accessMode, {
+ ...options,
+ fetchMenuListAsync: async () => {
+ ElMessage({
+ duration: 1500,
+ message: `${$t('common.loadingMenu')}...`,
+ });
+ return await getAllMenusApi();
+ },
+ // 可以指定没有权限跳转403页面
+ forbiddenComponent,
+ // 如果 route.meta.menuVisibleWithForbidden = true
+ layoutMap,
+ pageMap,
+ });
+}
+
+export { generateAccess };
diff --git a/web/apps/web-ele/src/router/guard.ts b/web/apps/web-ele/src/router/guard.ts
new file mode 100644
index 0000000..a1ad6d8
--- /dev/null
+++ b/web/apps/web-ele/src/router/guard.ts
@@ -0,0 +1,133 @@
+import type { Router } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+import { startProgress, stopProgress } from '@vben/utils';
+
+import { accessRoutes, coreRouteNames } from '#/router/routes';
+import { useAuthStore } from '#/store';
+
+import { generateAccess } from './access';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function setupCommonGuard(router: Router) {
+ // 记录已经加载的页面
+ const loadedPaths = new Set();
+
+ router.beforeEach((to) => {
+ to.meta.loaded = loadedPaths.has(to.path);
+
+ // 页面加载进度条
+ if (!to.meta.loaded && preferences.transition.progress) {
+ startProgress();
+ }
+ return true;
+ });
+
+ router.afterEach((to) => {
+ // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+
+ loadedPaths.add(to.path);
+
+ // 关闭页面加载进度条
+ if (preferences.transition.progress) {
+ stopProgress();
+ }
+ });
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function setupAccessGuard(router: Router) {
+ router.beforeEach(async (to, from) => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const authStore = useAuthStore();
+
+ // 基本路由,这些路由不需要进入权限拦截
+ if (coreRouteNames.includes(to.name as string)) {
+ if (to.path === LOGIN_PATH && accessStore.accessToken) {
+ return decodeURIComponent(
+ (to.query?.redirect as string) ||
+ userStore.userInfo?.homePath ||
+ preferences.app.defaultHomePath,
+ );
+ }
+ return true;
+ }
+
+ // accessToken 检查
+ if (!accessStore.accessToken) {
+ // 明确声明忽略权限访问权限,则可以访问
+ if (to.meta.ignoreAccess) {
+ return true;
+ }
+
+ // 没有访问权限,跳转登录页面
+ if (to.fullPath !== LOGIN_PATH) {
+ return {
+ path: LOGIN_PATH,
+ // 如不需要,直接删除 query
+ query:
+ to.fullPath === preferences.app.defaultHomePath
+ ? {}
+ : { redirect: encodeURIComponent(to.fullPath) },
+ // 携带当前跳转的页面,登录后重新跳转该页面
+ replace: true,
+ };
+ }
+ return to;
+ }
+
+ // 是否已经生成过动态路由
+ if (accessStore.isAccessChecked) {
+ return true;
+ }
+
+ // 生成路由表
+ // 当前登录用户拥有的角色标识列表
+ const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
+ const userRoles = userInfo.roles ?? [];
+
+ // 生成菜单和路由
+ const { accessibleMenus, accessibleRoutes } = await generateAccess({
+ roles: userRoles,
+ router,
+ // 则会在菜单中显示,但是访问会被重定向到403
+ routes: accessRoutes,
+ });
+
+ // 保存菜单信息和路由信息
+ accessStore.setAccessMenus(accessibleMenus);
+ accessStore.setAccessRoutes(accessibleRoutes);
+ accessStore.setIsAccessChecked(true);
+ const redirectPath = (from.query.redirect ??
+ (to.path === preferences.app.defaultHomePath
+ ? userInfo.homePath || preferences.app.defaultHomePath
+ : to.fullPath)) as string;
+
+ return {
+ ...router.resolve(decodeURIComponent(redirectPath)),
+ replace: true,
+ };
+ });
+}
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+ /** 通用 */
+ setupCommonGuard(router);
+ /** 权限访问 */
+ setupAccessGuard(router);
+}
+
+export { createRouterGuard };
diff --git a/web/apps/web-ele/src/router/index.ts b/web/apps/web-ele/src/router/index.ts
new file mode 100644
index 0000000..4840230
--- /dev/null
+++ b/web/apps/web-ele/src/router/index.ts
@@ -0,0 +1,37 @@
+import {
+ createRouter,
+ createWebHashHistory,
+ createWebHistory,
+} from 'vue-router';
+
+import { resetStaticRoutes } from '@vben/utils';
+
+import { createRouterGuard } from './guard';
+import { routes } from './routes';
+
+/**
+ * @zh_CN 创建vue-router实例
+ */
+const router = createRouter({
+ history:
+ import.meta.env.VITE_ROUTER_HISTORY === 'hash'
+ ? createWebHashHistory(import.meta.env.VITE_BASE)
+ : createWebHistory(import.meta.env.VITE_BASE),
+ // 应该添加到路由的初始路由列表。
+ routes,
+ scrollBehavior: (to, _from, savedPosition) => {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
+ },
+ // 是否应该禁止尾部斜杠。
+ // strict: true,
+});
+
+const resetRoutes = () => resetStaticRoutes(router, routes);
+
+// 创建路由守卫
+createRouterGuard(router);
+
+export { resetRoutes, router };
diff --git a/web/apps/web-ele/src/router/routes/core.ts b/web/apps/web-ele/src/router/routes/core.ts
new file mode 100644
index 0000000..949b0b6
--- /dev/null
+++ b/web/apps/web-ele/src/router/routes/core.ts
@@ -0,0 +1,97 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
+/** 全局404页面 */
+const fallbackNotFoundRoute: RouteRecordRaw = {
+ component: () => import('#/views/_core/fallback/not-found.vue'),
+ meta: {
+ hideInBreadcrumb: true,
+ hideInMenu: true,
+ hideInTab: true,
+ title: '404',
+ },
+ name: 'FallbackNotFound',
+ path: '/:path(.*)*',
+};
+
+/** 基本路由,这些路由是必须存在的 */
+const coreRoutes: RouteRecordRaw[] = [
+ /**
+ * 根路由
+ * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+ * 此路由必须存在,且不应修改
+ */
+ {
+ component: BasicLayout,
+ meta: {
+ hideInBreadcrumb: true,
+ title: 'Root',
+ },
+ name: 'Root',
+ path: '/',
+ redirect: preferences.app.defaultHomePath,
+ children: [],
+ },
+ {
+ component: AuthPageLayout,
+ meta: {
+ hideInTab: true,
+ title: 'Authentication',
+ },
+ name: 'Authentication',
+ path: '/auth',
+ redirect: LOGIN_PATH,
+ children: [
+ {
+ name: 'Login',
+ path: 'login',
+ component: () => import('#/views/_core/authentication/login.vue'),
+ meta: {
+ title: $t('page.auth.login'),
+ },
+ },
+ {
+ name: 'CodeLogin',
+ path: 'code-login',
+ component: () => import('#/views/_core/authentication/code-login.vue'),
+ meta: {
+ title: $t('page.auth.codeLogin'),
+ },
+ },
+ {
+ name: 'QrCodeLogin',
+ path: 'qrcode-login',
+ component: () =>
+ import('#/views/_core/authentication/qrcode-login.vue'),
+ meta: {
+ title: $t('page.auth.qrcodeLogin'),
+ },
+ },
+ {
+ name: 'ForgetPassword',
+ path: 'forget-password',
+ component: () =>
+ import('#/views/_core/authentication/forget-password.vue'),
+ meta: {
+ title: $t('page.auth.forgetPassword'),
+ },
+ },
+ {
+ name: 'Register',
+ path: 'register',
+ component: () => import('#/views/_core/authentication/register.vue'),
+ meta: {
+ title: $t('page.auth.register'),
+ },
+ },
+ ],
+ },
+];
+
+export { coreRoutes, fallbackNotFoundRoute };
diff --git a/web/apps/web-ele/src/router/routes/index.ts b/web/apps/web-ele/src/router/routes/index.ts
new file mode 100644
index 0000000..e6fb144
--- /dev/null
+++ b/web/apps/web-ele/src/router/routes/index.ts
@@ -0,0 +1,37 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
+
+import { coreRoutes, fallbackNotFoundRoute } from './core';
+
+const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
+ eager: true,
+});
+
+// 有需要可以自行打开注释,并创建文件夹
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
+
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
+const externalRoutes: RouteRecordRaw[] = [];
+
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ * 无需走权限验证(会一直显示在菜单中) */
+const routes: RouteRecordRaw[] = [
+ ...coreRoutes,
+ ...externalRoutes,
+ fallbackNotFoundRoute,
+];
+
+/** 基本路由列表,这些路由不需要进入权限拦截 */
+const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
+
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };
diff --git a/web/apps/web-ele/src/router/routes/modules/dashboard.ts b/web/apps/web-ele/src/router/routes/modules/dashboard.ts
new file mode 100644
index 0000000..5254dc6
--- /dev/null
+++ b/web/apps/web-ele/src/router/routes/modules/dashboard.ts
@@ -0,0 +1,38 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'lucide:layout-dashboard',
+ order: -1,
+ title: $t('page.dashboard.title'),
+ },
+ name: 'Dashboard',
+ path: '/dashboard',
+ children: [
+ {
+ name: 'Analytics',
+ path: '/analytics',
+ component: () => import('#/views/dashboard/analytics/index.vue'),
+ meta: {
+ affixTab: true,
+ icon: 'lucide:area-chart',
+ title: $t('page.dashboard.analytics'),
+ },
+ },
+ {
+ name: 'Workspace',
+ path: '/workspace',
+ component: () => import('#/views/dashboard/workspace/index.vue'),
+ meta: {
+ icon: 'carbon:workspace',
+ title: $t('page.dashboard.workspace'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-ele/src/router/routes/modules/demos.ts b/web/apps/web-ele/src/router/routes/modules/demos.ts
new file mode 100644
index 0000000..907ea3f
--- /dev/null
+++ b/web/apps/web-ele/src/router/routes/modules/demos.ts
@@ -0,0 +1,36 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ children: [
+ {
+ meta: {
+ title: $t('demos.elementPlus'),
+ },
+ name: 'NaiveDemos',
+ path: '/demos/element',
+ component: () => import('#/views/demos/element/index.vue'),
+ },
+ {
+ meta: {
+ title: $t('demos.form'),
+ },
+ name: 'BasicForm',
+ path: '/demos/form',
+ component: () => import('#/views/demos/form/basic.vue'),
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-ele/src/router/routes/modules/vben.ts b/web/apps/web-ele/src/router/routes/modules/vben.ts
new file mode 100644
index 0000000..20fbc96
--- /dev/null
+++ b/web/apps/web-ele/src/router/routes/modules/vben.ts
@@ -0,0 +1,82 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import {
+ VBEN_ANT_PREVIEW_URL,
+ VBEN_DOC_URL,
+ VBEN_GITHUB_URL,
+ VBEN_LOGO_URL,
+ VBEN_NAIVE_PREVIEW_URL,
+} from '@vben/constants';
+import { SvgAntdvLogoIcon } from '@vben/icons';
+
+import { IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ badgeType: 'dot',
+ icon: VBEN_LOGO_URL,
+ order: 9998,
+ title: $t('demos.vben.title'),
+ },
+ name: 'VbenProject',
+ path: '/vben-admin',
+ children: [
+ {
+ name: 'VbenDocument',
+ path: '/vben-admin/document',
+ component: IFrameView,
+ meta: {
+ icon: 'lucide:book-open-text',
+ link: VBEN_DOC_URL,
+ title: $t('demos.vben.document'),
+ },
+ },
+ {
+ name: 'VbenGithub',
+ path: '/vben-admin/github',
+ component: IFrameView,
+ meta: {
+ icon: 'mdi:github',
+ link: VBEN_GITHUB_URL,
+ title: 'Github',
+ },
+ },
+ {
+ name: 'VbenNaive',
+ path: '/vben-admin/naive',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:naiveui',
+ link: VBEN_NAIVE_PREVIEW_URL,
+ title: $t('demos.vben.naive-ui'),
+ },
+ },
+ {
+ name: 'VbenAntd',
+ path: '/vben-admin/antd',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvLogoIcon,
+ link: VBEN_ANT_PREVIEW_URL,
+ title: $t('demos.vben.antdv'),
+ },
+ },
+ ],
+ },
+ {
+ name: 'VbenAbout',
+ path: '/vben-admin/about',
+ component: () => import('#/views/_core/about/index.vue'),
+ meta: {
+ icon: 'lucide:copyright',
+ title: $t('demos.vben.about'),
+ order: 9999,
+ },
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-ele/src/store/auth.ts b/web/apps/web-ele/src/store/auth.ts
new file mode 100644
index 0000000..74fadfe
--- /dev/null
+++ b/web/apps/web-ele/src/store/auth.ts
@@ -0,0 +1,119 @@
+import type { Recordable, UserInfo } from '@vben/types';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
+
+import { ElNotification } from 'element-plus';
+import { defineStore } from 'pinia';
+
+import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
+import { $t } from '#/locales';
+
+export const useAuthStore = defineStore('auth', () => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const router = useRouter();
+
+ const loginLoading = ref(false);
+
+ /**
+ * 异步处理登录操作
+ * Asynchronously handle the login process
+ * @param params 登录表单数据
+ */
+ async function authLogin(
+ params: Recordable,
+ onSuccess?: () => Promise | void,
+ ) {
+ // 异步处理用户登录操作并获取 accessToken
+ let userInfo: null | UserInfo = null;
+ try {
+ loginLoading.value = true;
+ const { accessToken } = await loginApi(params);
+
+ // 如果成功获取到 accessToken
+ if (accessToken) {
+ // 将 accessToken 存储到 accessStore 中
+ accessStore.setAccessToken(accessToken);
+
+ // 获取用户信息并存储到 accessStore 中
+ const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ fetchUserInfo(),
+ getAccessCodesApi(),
+ ]);
+
+ userInfo = fetchUserInfoResult;
+
+ userStore.setUserInfo(userInfo);
+ accessStore.setAccessCodes(accessCodes);
+
+ if (accessStore.loginExpired) {
+ accessStore.setLoginExpired(false);
+ } else {
+ onSuccess
+ ? await onSuccess?.()
+ : await router.push(
+ userInfo.homePath || preferences.app.defaultHomePath,
+ );
+ }
+
+ if (userInfo?.realName) {
+ ElNotification({
+ message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
+ title: $t('authentication.loginSuccess'),
+ type: 'success',
+ });
+ }
+ }
+ } finally {
+ loginLoading.value = false;
+ }
+
+ return {
+ userInfo,
+ };
+ }
+
+ async function logout(redirect: boolean = true) {
+ try {
+ await logoutApi();
+ } catch {
+ // 不做任何处理
+ }
+ resetAllStores();
+ accessStore.setLoginExpired(false);
+
+ // 回登录页带上当前路由地址
+ await router.replace({
+ path: LOGIN_PATH,
+ query: redirect
+ ? {
+ redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+ }
+ : {},
+ });
+ }
+
+ async function fetchUserInfo() {
+ let userInfo: null | UserInfo = null;
+ userInfo = await getUserInfoApi();
+ userStore.setUserInfo(userInfo);
+ return userInfo;
+ }
+
+ function $reset() {
+ loginLoading.value = false;
+ }
+
+ return {
+ $reset,
+ authLogin,
+ fetchUserInfo,
+ loginLoading,
+ logout,
+ };
+});
diff --git a/web/apps/web-ele/src/store/index.ts b/web/apps/web-ele/src/store/index.ts
new file mode 100644
index 0000000..269586e
--- /dev/null
+++ b/web/apps/web-ele/src/store/index.ts
@@ -0,0 +1 @@
+export * from './auth';
diff --git a/web/apps/web-ele/src/views/_core/README.md b/web/apps/web-ele/src/views/_core/README.md
new file mode 100644
index 0000000..8248afe
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/README.md
@@ -0,0 +1,3 @@
+# \_core
+
+此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
diff --git a/web/apps/web-ele/src/views/_core/about/index.vue b/web/apps/web-ele/src/views/_core/about/index.vue
new file mode 100644
index 0000000..0ee5243
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/about/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/authentication/code-login.vue b/web/apps/web-ele/src/views/_core/authentication/code-login.vue
new file mode 100644
index 0000000..acfd1fd
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/authentication/code-login.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/authentication/forget-password.vue b/web/apps/web-ele/src/views/_core/authentication/forget-password.vue
new file mode 100644
index 0000000..fef0d42
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/authentication/forget-password.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/authentication/login.vue b/web/apps/web-ele/src/views/_core/authentication/login.vue
new file mode 100644
index 0000000..099e4c8
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/authentication/login.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/authentication/qrcode-login.vue b/web/apps/web-ele/src/views/_core/authentication/qrcode-login.vue
new file mode 100644
index 0000000..23f5f2d
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/authentication/qrcode-login.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/authentication/register.vue b/web/apps/web-ele/src/views/_core/authentication/register.vue
new file mode 100644
index 0000000..b1a5de7
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/authentication/register.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/fallback/coming-soon.vue b/web/apps/web-ele/src/views/_core/fallback/coming-soon.vue
new file mode 100644
index 0000000..f394930
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/fallback/coming-soon.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/fallback/forbidden.vue b/web/apps/web-ele/src/views/_core/fallback/forbidden.vue
new file mode 100644
index 0000000..8ea65fe
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/fallback/forbidden.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/fallback/internal-error.vue b/web/apps/web-ele/src/views/_core/fallback/internal-error.vue
new file mode 100644
index 0000000..819a47d
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/fallback/internal-error.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/fallback/not-found.vue b/web/apps/web-ele/src/views/_core/fallback/not-found.vue
new file mode 100644
index 0000000..4d178e9
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/fallback/not-found.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/_core/fallback/offline.vue b/web/apps/web-ele/src/views/_core/fallback/offline.vue
new file mode 100644
index 0000000..5de4a88
--- /dev/null
+++ b/web/apps/web-ele/src/views/_core/fallback/offline.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue b/web/apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue
new file mode 100644
index 0000000..f1f0b23
--- /dev/null
+++ b/web/apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue
new file mode 100644
index 0000000..190fb41
--- /dev/null
+++ b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
new file mode 100644
index 0000000..02f5091
--- /dev/null
+++ b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue
new file mode 100644
index 0000000..0915c7a
--- /dev/null
+++ b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue
new file mode 100644
index 0000000..7e0f101
--- /dev/null
+++ b/web/apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/dashboard/analytics/index.vue b/web/apps/web-ele/src/views/dashboard/analytics/index.vue
new file mode 100644
index 0000000..5e3d6d2
--- /dev/null
+++ b/web/apps/web-ele/src/views/dashboard/analytics/index.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/dashboard/workspace/index.vue b/web/apps/web-ele/src/views/dashboard/workspace/index.vue
new file mode 100644
index 0000000..b95d613
--- /dev/null
+++ b/web/apps/web-ele/src/views/dashboard/workspace/index.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+ 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
+
+ 今日晴,20℃ - 32℃!
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/demos/element/index.vue b/web/apps/web-ele/src/views/demos/element/index.vue
new file mode 100644
index 0000000..0a7012d
--- /dev/null
+++ b/web/apps/web-ele/src/views/demos/element/index.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ 按钮
+
+ Text
+ Default
+ Primary
+ Info
+ Success
+ Warning
+ Error
+
+
+
+ Message
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+ Notification
+
+ 信息
+ 错误
+ 警告
+ 成功
+
+
+
+ Segmented
+
+
+
+ V-Loading
+
+ 一些演示的内容
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-ele/src/views/demos/form/basic.vue b/web/apps/web-ele/src/views/demos/form/basic.vue
new file mode 100644
index 0000000..0ecab58
--- /dev/null
+++ b/web/apps/web-ele/src/views/demos/form/basic.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+ 基础表单演示
+ 设置表单值
+
+
+ 打开抽屉
+
+
+
diff --git a/web/apps/web-ele/tailwind.config.mjs b/web/apps/web-ele/tailwind.config.mjs
new file mode 100644
index 0000000..f17f556
--- /dev/null
+++ b/web/apps/web-ele/tailwind.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config';
diff --git a/web/apps/web-ele/tsconfig.json b/web/apps/web-ele/tsconfig.json
new file mode 100644
index 0000000..02c287f
--- /dev/null
+++ b/web/apps/web-ele/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web-app.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "#/*": ["./src/*"]
+ }
+ },
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/web/apps/web-ele/tsconfig.node.json b/web/apps/web-ele/tsconfig.node.json
new file mode 100644
index 0000000..c2f0d86
--- /dev/null
+++ b/web/apps/web-ele/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/node.json",
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "noEmit": false
+ },
+ "include": ["vite.config.mts"]
+}
diff --git a/web/apps/web-ele/vite.config.mts b/web/apps/web-ele/vite.config.mts
new file mode 100644
index 0000000..9f1e723
--- /dev/null
+++ b/web/apps/web-ele/vite.config.mts
@@ -0,0 +1,27 @@
+import { defineConfig } from '@vben/vite-config';
+
+import ElementPlus from 'unplugin-element-plus/vite';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ vite: {
+ plugins: [
+ ElementPlus({
+ format: 'esm',
+ }),
+ ],
+ server: {
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // mock代理目标地址
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});
diff --git a/web/apps/web-naive/.env.analyze b/web/apps/web-naive/.env.analyze
new file mode 100644
index 0000000..ffafa8d
--- /dev/null
+++ b/web/apps/web-naive/.env.analyze
@@ -0,0 +1,7 @@
+# public path
+VITE_BASE=/
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/api
+
+VITE_VISUALIZER=true
diff --git a/web/apps/web-naive/.env.development b/web/apps/web-naive/.env.development
new file mode 100644
index 0000000..11c5254
--- /dev/null
+++ b/web/apps/web-naive/.env.development
@@ -0,0 +1,16 @@
+# 端口号
+VITE_PORT=5888
+
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=/api
+
+# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
+VITE_NITRO_MOCK=true
+
+# 是否打开 devtools,true 为打开,false 为关闭
+VITE_DEVTOOLS=false
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
diff --git a/web/apps/web-naive/.env.production b/web/apps/web-naive/.env.production
new file mode 100644
index 0000000..5375847
--- /dev/null
+++ b/web/apps/web-naive/.env.production
@@ -0,0 +1,19 @@
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+
+# 是否开启压缩,可以设置为 none, brotli, gzip
+VITE_COMPRESS=none
+
+# 是否开启 PWA
+VITE_PWA=false
+
+# vue-router 的模式
+VITE_ROUTER_HISTORY=hash
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
+
+# 打包后是否生成dist.zip
+VITE_ARCHIVER=true
diff --git a/web/apps/web-naive/index.html b/web/apps/web-naive/index.html
new file mode 100644
index 0000000..7ea6384
--- /dev/null
+++ b/web/apps/web-naive/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+ <%= VITE_APP_TITLE %>
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-naive/package.json b/web/apps/web-naive/package.json
new file mode 100644
index 0000000..b97ab64
--- /dev/null
+++ b/web/apps/web-naive/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@vben/web-naive",
+ "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-naive"
+ },
+ "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:",
+ "naive-ui": "catalog:",
+ "pinia": "catalog:",
+ "vue": "catalog:",
+ "vue-router": "catalog:"
+ }
+}
diff --git a/web/apps/web-naive/postcss.config.mjs b/web/apps/web-naive/postcss.config.mjs
new file mode 100644
index 0000000..3d80704
--- /dev/null
+++ b/web/apps/web-naive/postcss.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';
diff --git a/web/apps/web-naive/public/favicon.ico b/web/apps/web-naive/public/favicon.ico
new file mode 100644
index 0000000..fcf9818
Binary files /dev/null and b/web/apps/web-naive/public/favicon.ico differ
diff --git a/web/apps/web-naive/src/adapter/component/index.ts b/web/apps/web-naive/src/adapter/component/index.ts
new file mode 100644
index 0000000..7ff1153
--- /dev/null
+++ b/web/apps/web-naive/src/adapter/component/index.ts
@@ -0,0 +1,238 @@
+/**
+ * 通用组件共同的使用的基础组件,原先放在 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 { message } from '#/adapter/naive';
+
+const NButton = defineAsyncComponent(() =>
+ import('naive-ui/es/button').then((res) => res.NButton),
+);
+const NCheckbox = defineAsyncComponent(() =>
+ import('naive-ui/es/checkbox').then((res) => res.NCheckbox),
+);
+const NCheckboxGroup = defineAsyncComponent(() =>
+ import('naive-ui/es/checkbox').then((res) => res.NCheckboxGroup),
+);
+const NDatePicker = defineAsyncComponent(() =>
+ import('naive-ui/es/date-picker').then((res) => res.NDatePicker),
+);
+const NDivider = defineAsyncComponent(() =>
+ import('naive-ui/es/divider').then((res) => res.NDivider),
+);
+const NInput = defineAsyncComponent(() =>
+ import('naive-ui/es/input').then((res) => res.NInput),
+);
+const NInputNumber = defineAsyncComponent(() =>
+ import('naive-ui/es/input-number').then((res) => res.NInputNumber),
+);
+const NRadio = defineAsyncComponent(() =>
+ import('naive-ui/es/radio').then((res) => res.NRadio),
+);
+const NRadioButton = defineAsyncComponent(() =>
+ import('naive-ui/es/radio').then((res) => res.NRadioButton),
+);
+const NRadioGroup = defineAsyncComponent(() =>
+ import('naive-ui/es/radio').then((res) => res.NRadioGroup),
+);
+const NSelect = defineAsyncComponent(() =>
+ import('naive-ui/es/select').then((res) => res.NSelect),
+);
+const NSpace = defineAsyncComponent(() =>
+ import('naive-ui/es/space').then((res) => res.NSpace),
+);
+const NSwitch = defineAsyncComponent(() =>
+ import('naive-ui/es/switch').then((res) => res.NSwitch),
+);
+const NTimePicker = defineAsyncComponent(() =>
+ import('naive-ui/es/time-picker').then((res) => res.NTimePicker),
+);
+const NTreeSelect = defineAsyncComponent(() =>
+ import('naive-ui/es/tree-select').then((res) => res.NTreeSelect),
+);
+const NUpload = defineAsyncComponent(() =>
+ import('naive-ui/es/upload').then((res) => res.NUpload),
+);
+
+const withDefaultPlaceholder = (
+ component: T,
+ type: 'input' | 'select',
+ componentProps: Recordable = {},
+) => {
+ 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 = {};
+ 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'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'DatePicker'
+ | 'Divider'
+ | 'IconPicker'
+ | 'Input'
+ | 'InputNumber'
+ | 'RadioGroup'
+ | 'Select'
+ | 'Space'
+ | 'Switch'
+ | 'TimePicker'
+ | 'TreeSelect'
+ | 'Upload'
+ | BaseFormComponentType;
+
+async function initComponentAdapter() {
+ const components: Partial> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // Button: () =>
+ // import('xxx').then((res) => res.Button),
+
+ ApiSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiSelect',
+ },
+ 'select',
+ {
+ component: NSelect,
+ modelPropName: 'value',
+ },
+ ),
+ ApiTreeSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiTreeSelect',
+ },
+ 'select',
+ {
+ component: NTreeSelect,
+ nodeKey: 'value',
+ loadingSlot: 'arrow',
+ keyField: 'value',
+ modelPropName: 'value',
+ optionsPropName: 'options',
+ visibleEvent: 'onVisibleChange',
+ },
+ ),
+ Checkbox: NCheckbox,
+ CheckboxGroup: (props, { attrs, slots }) => {
+ let defaultSlot;
+ if (Reflect.has(slots, 'default')) {
+ defaultSlot = slots.default;
+ } else {
+ const { options } = attrs;
+ if (Array.isArray(options)) {
+ defaultSlot = () => options.map((option) => h(NCheckbox, option));
+ }
+ }
+ return h(
+ NCheckboxGroup,
+ { ...props, ...attrs },
+ { default: defaultSlot },
+ );
+ },
+ DatePicker: NDatePicker,
+ // 自定义默认按钮
+ DefaultButton: (props, { attrs, slots }) => {
+ return h(NButton, { ...props, attrs, type: 'default' }, slots);
+ },
+ // 自定义主要按钮
+ PrimaryButton: (props, { attrs, slots }) => {
+ return h(NButton, { ...props, attrs, type: 'primary' }, slots);
+ },
+ Divider: NDivider,
+ IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
+ iconSlot: 'suffix',
+ inputComponent: NInput,
+ }),
+ Input: withDefaultPlaceholder(NInput, 'input'),
+ InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
+ RadioGroup: (props, { attrs, slots }) => {
+ let defaultSlot;
+ if (Reflect.has(slots, 'default')) {
+ defaultSlot = slots.default;
+ } else {
+ const { options } = attrs;
+ if (Array.isArray(options)) {
+ defaultSlot = () =>
+ options.map((option) =>
+ h(attrs.isButton ? NRadioButton : NRadio, option),
+ );
+ }
+ }
+ const groupRender = h(
+ NRadioGroup,
+ { ...props, ...attrs },
+ { default: defaultSlot },
+ );
+ return attrs.isButton
+ ? h(NSpace, { vertical: true }, () => groupRender)
+ : groupRender;
+ },
+ Select: withDefaultPlaceholder(NSelect, 'select'),
+ Space: NSpace,
+ Switch: NSwitch,
+ TimePicker: NTimePicker,
+ TreeSelect: withDefaultPlaceholder(NTreeSelect, 'select'),
+ Upload: NUpload,
+ };
+
+ // 将组件注册到全局共享状态中
+ globalShareState.setComponents(components);
+
+ // 定义全局共享状态中的消息提示
+ globalShareState.defineMessage({
+ // 复制成功消息提示
+ copyPreferencesSuccess: (title, content) => {
+ message.success(content || title, {
+ duration: 0,
+ });
+ },
+ });
+}
+
+export { initComponentAdapter };
diff --git a/web/apps/web-naive/src/adapter/form.ts b/web/apps/web-naive/src/adapter/form.ts
new file mode 100644
index 0000000..9de44a0
--- /dev/null
+++ b/web/apps/web-naive/src/adapter/form.ts
@@ -0,0 +1,45 @@
+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({
+ config: {
+ // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
+ emptyStateValue: null,
+ baseModelPropName: 'value',
+ modelPropNameMap: {
+ Checkbox: 'checked',
+ Radio: '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;
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema = FormSchema;
+export type { VbenFormProps };
diff --git a/web/apps/web-naive/src/adapter/naive.ts b/web/apps/web-naive/src/adapter/naive.ts
new file mode 100644
index 0000000..1eb7b7b
--- /dev/null
+++ b/web/apps/web-naive/src/adapter/naive.ts
@@ -0,0 +1,25 @@
+import { computed } from 'vue';
+
+import { preferences } from '@vben/preferences';
+import '@vben/styles';
+
+import { createDiscreteApi, darkTheme, lightTheme } from 'naive-ui';
+
+const themeOverridesProviderProps = computed(() => ({
+ themeOverrides: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
+}));
+
+const themeProviderProps = computed(() => ({
+ theme: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
+}));
+
+export const { dialog, loadingBar, message, modal, notification } =
+ createDiscreteApi(
+ ['message', 'dialog', 'notification', 'loadingBar', 'modal'],
+ {
+ configProviderProps: themeProviderProps,
+ loadingBarProviderProps: themeOverridesProviderProps,
+ messageProviderProps: themeOverridesProviderProps,
+ notificationProviderProps: themeOverridesProviderProps,
+ },
+ );
diff --git a/web/apps/web-naive/src/adapter/vxe-table.ts b/web/apps/web-naive/src/adapter/vxe-table.ts
new file mode 100644
index 0000000..3bad067
--- /dev/null
+++ b/web/apps/web-naive/src/adapter/vxe-table.ts
@@ -0,0 +1,69 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { NButton, NImage } from 'naive-ui';
+
+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(NImage, { src: row[column.field] });
+ },
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellLink' },
+ vxeUI.renderer.add('CellLink', {
+ renderTableDefault(renderOpts) {
+ const { props } = renderOpts;
+ return h(
+ NButton,
+ { size: 'small', type: 'primary', quaternary: true },
+ { default: () => props?.text },
+ );
+ },
+ });
+
+ // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+ // vxeUI.formats.add
+ },
+ useVbenForm,
+});
+
+export { useVbenVxeGrid };
+
+export type * from '@vben/plugins/vxe-table';
diff --git a/web/apps/web-naive/src/api/core/auth.ts b/web/apps/web-naive/src/api/core/auth.ts
new file mode 100644
index 0000000..71d9f99
--- /dev/null
+++ b/web/apps/web-naive/src/api/core/auth.ts
@@ -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('/auth/login', data);
+}
+
+/**
+ * 刷新accessToken
+ */
+export async function refreshTokenApi() {
+ return baseRequestClient.post('/auth/refresh', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 退出登录
+ */
+export async function logoutApi() {
+ return baseRequestClient.post('/auth/logout', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 获取用户权限码
+ */
+export async function getAccessCodesApi() {
+ return requestClient.get('/auth/codes');
+}
diff --git a/web/apps/web-naive/src/api/core/index.ts b/web/apps/web-naive/src/api/core/index.ts
new file mode 100644
index 0000000..28a5aef
--- /dev/null
+++ b/web/apps/web-naive/src/api/core/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './menu';
+export * from './user';
diff --git a/web/apps/web-naive/src/api/core/menu.ts b/web/apps/web-naive/src/api/core/menu.ts
new file mode 100644
index 0000000..9ef60b1
--- /dev/null
+++ b/web/apps/web-naive/src/api/core/menu.ts
@@ -0,0 +1,10 @@
+import type { RouteRecordStringComponent } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户所有菜单
+ */
+export async function getAllMenusApi() {
+ return requestClient.get('/menu/all');
+}
diff --git a/web/apps/web-naive/src/api/core/user.ts b/web/apps/web-naive/src/api/core/user.ts
new file mode 100644
index 0000000..7e28ea8
--- /dev/null
+++ b/web/apps/web-naive/src/api/core/user.ts
@@ -0,0 +1,10 @@
+import type { UserInfo } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户信息
+ */
+export async function getUserInfoApi() {
+ return requestClient.get('/user/info');
+}
diff --git a/web/apps/web-naive/src/api/index.ts b/web/apps/web-naive/src/api/index.ts
new file mode 100644
index 0000000..4b0e041
--- /dev/null
+++ b/web/apps/web-naive/src/api/index.ts
@@ -0,0 +1 @@
+export * from './core';
diff --git a/web/apps/web-naive/src/api/request.ts b/web/apps/web-naive/src/api/request.ts
new file mode 100644
index 0000000..f8fbacc
--- /dev/null
+++ b/web/apps/web-naive/src/api/request.ts
@@ -0,0 +1,112 @@
+/**
+ * 该文件可自行根据业务逻辑进行调整
+ */
+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 '#/adapter/naive';
+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 });
diff --git a/web/apps/web-naive/src/app.vue b/web/apps/web-naive/src/app.vue
new file mode 100644
index 0000000..23983c5
--- /dev/null
+++ b/web/apps/web-naive/src/app.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-naive/src/bootstrap.ts b/web/apps/web-naive/src/bootstrap.ts
new file mode 100644
index 0000000..df0b2cb
--- /dev/null
+++ b/web/apps/web-naive/src/bootstrap.ts
@@ -0,0 +1,76 @@
+import { createApp, watchEffect } from 'vue';
+
+import { registerAccessDirective } from '@vben/access';
+import { registerLoadingDirective } from '@vben/common-ui';
+import { preferences } from '@vben/preferences';
+import { initStores } from '@vben/stores';
+import '@vben/styles';
+import '@vben/styles/naive';
+
+import { useTitle } from '@vueuse/core';
+
+import { $t, setupI18n } from '#/locales';
+
+import { initComponentAdapter } from './adapter/component';
+import { initSetupVbenForm } from './adapter/form';
+import App from './app.vue';
+import { router } from './router';
+
+async function bootstrap(namespace: string) {
+ // 初始化组件适配器
+ await initComponentAdapter();
+
+ // 初始化表单组件
+ await initSetupVbenForm();
+
+ // // 设置弹窗的默认配置
+ // setDefaultModalProps({
+ // fullscreenButton: false,
+ // });
+ // // 设置抽屉的默认配置
+ // setDefaultDrawerProps({
+ // // zIndex: 2000,
+ // });
+
+ const app = createApp(App);
+
+ // 注册v-loading指令
+ registerLoadingDirective(app, {
+ loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+ spinning: 'spinning',
+ });
+
+ // 国际化 i18n 配置
+ await setupI18n(app);
+
+ // 配置 pinia-tore
+ await initStores(app, { namespace });
+
+ // 安装权限指令
+ registerAccessDirective(app);
+
+ // 初始化 tippy
+ const { initTippy } = await import('@vben/common-ui/es/tippy');
+ initTippy(app);
+
+ // 配置路由及路由守卫
+ app.use(router);
+
+ // 配置Motion插件
+ const { MotionPlugin } = await import('@vben/plugins/motion');
+ app.use(MotionPlugin);
+
+ // 动态更新标题
+ watchEffect(() => {
+ if (preferences.app.dynamicTitle) {
+ const routeTitle = router.currentRoute.value.meta?.title;
+ const pageTitle =
+ (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+ useTitle(pageTitle);
+ }
+ });
+
+ app.mount('#app');
+}
+
+export { bootstrap };
diff --git a/web/apps/web-naive/src/layouts/auth.vue b/web/apps/web-naive/src/layouts/auth.vue
new file mode 100644
index 0000000..18d415b
--- /dev/null
+++ b/web/apps/web-naive/src/layouts/auth.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-naive/src/layouts/basic.vue b/web/apps/web-naive/src/layouts/basic.vue
new file mode 100644
index 0000000..6918938
--- /dev/null
+++ b/web/apps/web-naive/src/layouts/basic.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-naive/src/layouts/index.ts b/web/apps/web-naive/src/layouts/index.ts
new file mode 100644
index 0000000..a432078
--- /dev/null
+++ b/web/apps/web-naive/src/layouts/index.ts
@@ -0,0 +1,6 @@
+const BasicLayout = () => import('./basic.vue');
+const AuthPageLayout = () => import('./auth.vue');
+
+const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
+
+export { AuthPageLayout, BasicLayout, IFrameView };
diff --git a/web/apps/web-naive/src/locales/README.md b/web/apps/web-naive/src/locales/README.md
new file mode 100644
index 0000000..7b45103
--- /dev/null
+++ b/web/apps/web-naive/src/locales/README.md
@@ -0,0 +1,3 @@
+# locale
+
+每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
diff --git a/web/apps/web-naive/src/locales/index.ts b/web/apps/web-naive/src/locales/index.ts
new file mode 100644
index 0000000..58f63c1
--- /dev/null
+++ b/web/apps/web-naive/src/locales/index.ts
@@ -0,0 +1,38 @@
+import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
+import {
+ $t,
+ setupI18n as coreSetup,
+ loadLocalesMapFromDir,
+} from '@vben/locales';
+import { preferences } from '@vben/preferences';
+
+const modules = import.meta.glob('./langs/**/*.json');
+
+const localesMap = loadLocalesMapFromDir(
+ /\.\/langs\/([^/]+)\/(.*)\.json$/,
+ modules,
+);
+
+/**
+ * 加载应用特有的语言包
+ * 这里也可以改造为从服务端获取翻译数据
+ * @param lang
+ */
+async function loadMessages(lang: SupportedLanguagesType) {
+ const appLocaleMessages = await localesMap[lang]?.();
+ return appLocaleMessages?.default;
+}
+
+async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD,
+ ...options,
+ });
+}
+
+export { $t, setupI18n };
diff --git a/web/apps/web-naive/src/locales/langs/en-US/demos.json b/web/apps/web-naive/src/locales/langs/en-US/demos.json
new file mode 100644
index 0000000..839fc2e
--- /dev/null
+++ b/web/apps/web-naive/src/locales/langs/en-US/demos.json
@@ -0,0 +1,14 @@
+{
+ "title": "Demos",
+ "naive": "Naive UI",
+ "table": "Table",
+ "form": "Form",
+ "vben": {
+ "title": "Project",
+ "about": "About",
+ "document": "Document",
+ "antdv": "Ant Design Vue Version",
+ "naive-ui": "Naive UI Version",
+ "element-plus": "Element Plus Version"
+ }
+}
diff --git a/web/apps/web-naive/src/locales/langs/en-US/page.json b/web/apps/web-naive/src/locales/langs/en-US/page.json
new file mode 100644
index 0000000..618a258
--- /dev/null
+++ b/web/apps/web-naive/src/locales/langs/en-US/page.json
@@ -0,0 +1,14 @@
+{
+ "auth": {
+ "login": "Login",
+ "register": "Register",
+ "codeLogin": "Code Login",
+ "qrcodeLogin": "Qr Code Login",
+ "forgetPassword": "Forget Password"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "analytics": "Analytics",
+ "workspace": "Workspace"
+ }
+}
diff --git a/web/apps/web-naive/src/locales/langs/zh-CN/demos.json b/web/apps/web-naive/src/locales/langs/zh-CN/demos.json
new file mode 100644
index 0000000..e0d7e61
--- /dev/null
+++ b/web/apps/web-naive/src/locales/langs/zh-CN/demos.json
@@ -0,0 +1,14 @@
+{
+ "title": "演示",
+ "naive": "Naive UI",
+ "table": "Table",
+ "form": "表单",
+ "vben": {
+ "title": "项目",
+ "about": "关于",
+ "document": "文档",
+ "antdv": "Ant Design Vue 版本",
+ "naive-ui": "Naive UI 版本",
+ "element-plus": "Element Plus 版本"
+ }
+}
diff --git a/web/apps/web-naive/src/locales/langs/zh-CN/page.json b/web/apps/web-naive/src/locales/langs/zh-CN/page.json
new file mode 100644
index 0000000..4cb6708
--- /dev/null
+++ b/web/apps/web-naive/src/locales/langs/zh-CN/page.json
@@ -0,0 +1,14 @@
+{
+ "auth": {
+ "login": "登录",
+ "register": "注册",
+ "codeLogin": "验证码登录",
+ "qrcodeLogin": "二维码登录",
+ "forgetPassword": "忘记密码"
+ },
+ "dashboard": {
+ "title": "概览",
+ "analytics": "分析页",
+ "workspace": "工作台"
+ }
+}
diff --git a/web/apps/web-naive/src/main.ts b/web/apps/web-naive/src/main.ts
new file mode 100644
index 0000000..5d728a0
--- /dev/null
+++ b/web/apps/web-naive/src/main.ts
@@ -0,0 +1,31 @@
+import { initPreferences } from '@vben/preferences';
+import { unmountGlobalLoading } from '@vben/utils';
+
+import { overridesPreferences } from './preferences';
+
+/**
+ * 应用初始化完成之后再进行页面加载渲染
+ */
+async function initApplication() {
+ // name用于指定项目唯一标识
+ // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
+ const env = import.meta.env.PROD ? 'prod' : 'dev';
+ const appVersion = import.meta.env.VITE_APP_VERSION;
+ const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
+
+ // app偏好设置初始化
+ await initPreferences({
+ namespace,
+ overrides: overridesPreferences,
+ });
+
+ // 启动应用并挂载
+ // vue应用主要逻辑及视图
+ const { bootstrap } = await import('./bootstrap');
+ await bootstrap(namespace);
+
+ // 移除并销毁loading
+ unmountGlobalLoading();
+}
+
+initApplication();
diff --git a/web/apps/web-naive/src/preferences.ts b/web/apps/web-naive/src/preferences.ts
new file mode 100644
index 0000000..b2e9ace
--- /dev/null
+++ b/web/apps/web-naive/src/preferences.ts
@@ -0,0 +1,13 @@
+import { defineOverridesPreferences } from '@vben/preferences';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ * !!! 更改配置后请清空缓存,否则可能不生效
+ */
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ name: import.meta.env.VITE_APP_TITLE,
+ },
+});
diff --git a/web/apps/web-naive/src/router/access.ts b/web/apps/web-naive/src/router/access.ts
new file mode 100644
index 0000000..7a80bac
--- /dev/null
+++ b/web/apps/web-naive/src/router/access.ts
@@ -0,0 +1,40 @@
+import type {
+ ComponentRecordType,
+ GenerateMenuAndRoutesOptions,
+} from '@vben/types';
+
+import { generateAccessible } from '@vben/access';
+import { preferences } from '@vben/preferences';
+
+import { message } from '#/adapter/naive';
+import { getAllMenusApi } from '#/api';
+import { BasicLayout, IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
+
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
+
+ const layoutMap: ComponentRecordType = {
+ BasicLayout,
+ IFrameView,
+ };
+
+ return await generateAccessible(preferences.app.accessMode, {
+ ...options,
+ fetchMenuListAsync: async () => {
+ message.loading(`${$t('common.loadingMenu')}...`, {
+ duration: 1.5,
+ });
+ return await getAllMenusApi();
+ },
+ // 可以指定没有权限跳转403页面
+ forbiddenComponent,
+ // 如果 route.meta.menuVisibleWithForbidden = true
+ layoutMap,
+ pageMap,
+ });
+}
+
+export { generateAccess };
diff --git a/web/apps/web-naive/src/router/guard.ts b/web/apps/web-naive/src/router/guard.ts
new file mode 100644
index 0000000..28d1cea
--- /dev/null
+++ b/web/apps/web-naive/src/router/guard.ts
@@ -0,0 +1,132 @@
+import type { Router } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+import { startProgress, stopProgress } from '@vben/utils';
+
+import { accessRoutes, coreRouteNames } from '#/router/routes';
+import { useAuthStore } from '#/store';
+
+import { generateAccess } from './access';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function setupCommonGuard(router: Router) {
+ // 记录已经加载的页面
+ const loadedPaths = new Set();
+
+ router.beforeEach((to) => {
+ to.meta.loaded = loadedPaths.has(to.path);
+
+ // 页面加载进度条
+ if (!to.meta.loaded && preferences.transition.progress) {
+ startProgress();
+ }
+ return true;
+ });
+
+ router.afterEach((to) => {
+ // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+
+ loadedPaths.add(to.path);
+
+ // 关闭页面加载进度条
+ if (preferences.transition.progress) {
+ stopProgress();
+ }
+ });
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function setupAccessGuard(router: Router) {
+ router.beforeEach(async (to, from) => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const authStore = useAuthStore();
+
+ // 基本路由,这些路由不需要进入权限拦截
+ if (coreRouteNames.includes(to.name as string)) {
+ if (to.path === LOGIN_PATH && accessStore.accessToken) {
+ return decodeURIComponent(
+ (to.query?.redirect as string) ||
+ userStore.userInfo?.homePath ||
+ preferences.app.defaultHomePath,
+ );
+ }
+ return true;
+ }
+
+ // accessToken 检查
+ if (!accessStore.accessToken) {
+ // 明确声明忽略权限访问权限,则可以访问
+ if (to.meta.ignoreAccess) {
+ return true;
+ }
+
+ // 没有访问权限,跳转登录页面
+ if (to.fullPath !== LOGIN_PATH) {
+ return {
+ path: LOGIN_PATH,
+ // 如不需要,直接删除 query
+ query:
+ to.fullPath === preferences.app.defaultHomePath
+ ? {}
+ : { redirect: encodeURIComponent(to.fullPath) },
+ // 携带当前跳转的页面,登录后重新跳转该页面
+ replace: true,
+ };
+ }
+ return to;
+ }
+
+ // 是否已经生成过动态路由
+ if (accessStore.isAccessChecked) {
+ return true;
+ }
+ // 生成路由表
+ // 当前登录用户拥有的角色标识列表
+ const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
+ const userRoles = userInfo.roles ?? [];
+
+ // 生成菜单和路由
+ const { accessibleMenus, accessibleRoutes } = await generateAccess({
+ roles: userRoles,
+ router,
+ // 则会在菜单中显示,但是访问会被重定向到403
+ routes: accessRoutes,
+ });
+
+ // 保存菜单信息和路由信息
+ accessStore.setAccessMenus(accessibleMenus);
+ accessStore.setAccessRoutes(accessibleRoutes);
+ accessStore.setIsAccessChecked(true);
+ const redirectPath = (from.query.redirect ??
+ (to.path === preferences.app.defaultHomePath
+ ? userInfo.homePath || preferences.app.defaultHomePath
+ : to.fullPath)) as string;
+
+ return {
+ ...router.resolve(decodeURIComponent(redirectPath)),
+ replace: true,
+ };
+ });
+}
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+ /** 通用 */
+ setupCommonGuard(router);
+ /** 权限访问 */
+ setupAccessGuard(router);
+}
+
+export { createRouterGuard };
diff --git a/web/apps/web-naive/src/router/index.ts b/web/apps/web-naive/src/router/index.ts
new file mode 100644
index 0000000..4840230
--- /dev/null
+++ b/web/apps/web-naive/src/router/index.ts
@@ -0,0 +1,37 @@
+import {
+ createRouter,
+ createWebHashHistory,
+ createWebHistory,
+} from 'vue-router';
+
+import { resetStaticRoutes } from '@vben/utils';
+
+import { createRouterGuard } from './guard';
+import { routes } from './routes';
+
+/**
+ * @zh_CN 创建vue-router实例
+ */
+const router = createRouter({
+ history:
+ import.meta.env.VITE_ROUTER_HISTORY === 'hash'
+ ? createWebHashHistory(import.meta.env.VITE_BASE)
+ : createWebHistory(import.meta.env.VITE_BASE),
+ // 应该添加到路由的初始路由列表。
+ routes,
+ scrollBehavior: (to, _from, savedPosition) => {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
+ },
+ // 是否应该禁止尾部斜杠。
+ // strict: true,
+});
+
+const resetRoutes = () => resetStaticRoutes(router, routes);
+
+// 创建路由守卫
+createRouterGuard(router);
+
+export { resetRoutes, router };
diff --git a/web/apps/web-naive/src/router/routes/core.ts b/web/apps/web-naive/src/router/routes/core.ts
new file mode 100644
index 0000000..949b0b6
--- /dev/null
+++ b/web/apps/web-naive/src/router/routes/core.ts
@@ -0,0 +1,97 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
+/** 全局404页面 */
+const fallbackNotFoundRoute: RouteRecordRaw = {
+ component: () => import('#/views/_core/fallback/not-found.vue'),
+ meta: {
+ hideInBreadcrumb: true,
+ hideInMenu: true,
+ hideInTab: true,
+ title: '404',
+ },
+ name: 'FallbackNotFound',
+ path: '/:path(.*)*',
+};
+
+/** 基本路由,这些路由是必须存在的 */
+const coreRoutes: RouteRecordRaw[] = [
+ /**
+ * 根路由
+ * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+ * 此路由必须存在,且不应修改
+ */
+ {
+ component: BasicLayout,
+ meta: {
+ hideInBreadcrumb: true,
+ title: 'Root',
+ },
+ name: 'Root',
+ path: '/',
+ redirect: preferences.app.defaultHomePath,
+ children: [],
+ },
+ {
+ component: AuthPageLayout,
+ meta: {
+ hideInTab: true,
+ title: 'Authentication',
+ },
+ name: 'Authentication',
+ path: '/auth',
+ redirect: LOGIN_PATH,
+ children: [
+ {
+ name: 'Login',
+ path: 'login',
+ component: () => import('#/views/_core/authentication/login.vue'),
+ meta: {
+ title: $t('page.auth.login'),
+ },
+ },
+ {
+ name: 'CodeLogin',
+ path: 'code-login',
+ component: () => import('#/views/_core/authentication/code-login.vue'),
+ meta: {
+ title: $t('page.auth.codeLogin'),
+ },
+ },
+ {
+ name: 'QrCodeLogin',
+ path: 'qrcode-login',
+ component: () =>
+ import('#/views/_core/authentication/qrcode-login.vue'),
+ meta: {
+ title: $t('page.auth.qrcodeLogin'),
+ },
+ },
+ {
+ name: 'ForgetPassword',
+ path: 'forget-password',
+ component: () =>
+ import('#/views/_core/authentication/forget-password.vue'),
+ meta: {
+ title: $t('page.auth.forgetPassword'),
+ },
+ },
+ {
+ name: 'Register',
+ path: 'register',
+ component: () => import('#/views/_core/authentication/register.vue'),
+ meta: {
+ title: $t('page.auth.register'),
+ },
+ },
+ ],
+ },
+];
+
+export { coreRoutes, fallbackNotFoundRoute };
diff --git a/web/apps/web-naive/src/router/routes/index.ts b/web/apps/web-naive/src/router/routes/index.ts
new file mode 100644
index 0000000..e6fb144
--- /dev/null
+++ b/web/apps/web-naive/src/router/routes/index.ts
@@ -0,0 +1,37 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
+
+import { coreRoutes, fallbackNotFoundRoute } from './core';
+
+const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
+ eager: true,
+});
+
+// 有需要可以自行打开注释,并创建文件夹
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
+
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
+const externalRoutes: RouteRecordRaw[] = [];
+
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ * 无需走权限验证(会一直显示在菜单中) */
+const routes: RouteRecordRaw[] = [
+ ...coreRoutes,
+ ...externalRoutes,
+ fallbackNotFoundRoute,
+];
+
+/** 基本路由列表,这些路由不需要进入权限拦截 */
+const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
+
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };
diff --git a/web/apps/web-naive/src/router/routes/modules/dashboard.ts b/web/apps/web-naive/src/router/routes/modules/dashboard.ts
new file mode 100644
index 0000000..5254dc6
--- /dev/null
+++ b/web/apps/web-naive/src/router/routes/modules/dashboard.ts
@@ -0,0 +1,38 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'lucide:layout-dashboard',
+ order: -1,
+ title: $t('page.dashboard.title'),
+ },
+ name: 'Dashboard',
+ path: '/dashboard',
+ children: [
+ {
+ name: 'Analytics',
+ path: '/analytics',
+ component: () => import('#/views/dashboard/analytics/index.vue'),
+ meta: {
+ affixTab: true,
+ icon: 'lucide:area-chart',
+ title: $t('page.dashboard.analytics'),
+ },
+ },
+ {
+ name: 'Workspace',
+ path: '/workspace',
+ component: () => import('#/views/dashboard/workspace/index.vue'),
+ meta: {
+ icon: 'carbon:workspace',
+ title: $t('page.dashboard.workspace'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-naive/src/router/routes/modules/demos.ts b/web/apps/web-naive/src/router/routes/modules/demos.ts
new file mode 100644
index 0000000..5e49ffa
--- /dev/null
+++ b/web/apps/web-naive/src/router/routes/modules/demos.ts
@@ -0,0 +1,44 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ children: [
+ {
+ meta: {
+ title: $t('demos.naive'),
+ },
+ name: 'NaiveDemos',
+ path: '/demos/naive',
+ component: () => import('#/views/demos/naive/index.vue'),
+ },
+ {
+ meta: {
+ title: $t('demos.table'),
+ },
+ name: 'Table',
+ path: '/demos/table',
+ component: () => import('#/views/demos/table/index.vue'),
+ },
+ {
+ meta: {
+ title: $t('demos.form'),
+ },
+ name: 'Form',
+ path: '/demos/form',
+ component: () => import('#/views/demos/form/basic.vue'),
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-naive/src/router/routes/modules/vben.ts b/web/apps/web-naive/src/router/routes/modules/vben.ts
new file mode 100644
index 0000000..169de85
--- /dev/null
+++ b/web/apps/web-naive/src/router/routes/modules/vben.ts
@@ -0,0 +1,82 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import {
+ VBEN_ANT_PREVIEW_URL,
+ VBEN_DOC_URL,
+ VBEN_ELE_PREVIEW_URL,
+ VBEN_GITHUB_URL,
+ VBEN_LOGO_URL,
+} from '@vben/constants';
+import { SvgAntdvLogoIcon } from '@vben/icons';
+
+import { IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ badgeType: 'dot',
+ icon: VBEN_LOGO_URL,
+ order: 9998,
+ title: $t('demos.vben.title'),
+ },
+ name: 'VbenProject',
+ path: '/vben-admin',
+ children: [
+ {
+ name: 'VbenDocument',
+ path: '/vben-admin/document',
+ component: IFrameView,
+ meta: {
+ icon: 'lucide:book-open-text',
+ link: VBEN_DOC_URL,
+ title: $t('demos.vben.document'),
+ },
+ },
+ {
+ name: 'VbenGithub',
+ path: '/vben-admin/github',
+ component: IFrameView,
+ meta: {
+ icon: 'mdi:github',
+ link: VBEN_GITHUB_URL,
+ title: 'Github',
+ },
+ },
+ {
+ name: 'VbenAntd',
+ path: '/vben-admin/antd',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: SvgAntdvLogoIcon,
+ link: VBEN_ANT_PREVIEW_URL,
+ title: $t('demos.vben.antdv'),
+ },
+ },
+ {
+ name: 'VbenElementPlus',
+ path: '/vben-admin/ele',
+ component: IFrameView,
+ meta: {
+ badgeType: 'dot',
+ icon: 'logos:element',
+ link: VBEN_ELE_PREVIEW_URL,
+ title: $t('demos.vben.element-plus'),
+ },
+ },
+ ],
+ },
+ {
+ name: 'VbenAbout',
+ path: '/vben-admin/about',
+ component: () => import('#/views/_core/about/index.vue'),
+ meta: {
+ icon: 'lucide:copyright',
+ title: $t('demos.vben.about'),
+ order: 9999,
+ },
+ },
+];
+
+export default routes;
diff --git a/web/apps/web-naive/src/store/auth.ts b/web/apps/web-naive/src/store/auth.ts
new file mode 100644
index 0000000..0ff050b
--- /dev/null
+++ b/web/apps/web-naive/src/store/auth.ts
@@ -0,0 +1,119 @@
+import type { Recordable, UserInfo } from '@vben/types';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
+
+import { defineStore } from 'pinia';
+
+import { notification } from '#/adapter/naive';
+import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
+import { $t } from '#/locales';
+
+export const useAuthStore = defineStore('auth', () => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const router = useRouter();
+
+ const loginLoading = ref(false);
+
+ /**
+ * 异步处理登录操作
+ * Asynchronously handle the login process
+ * @param params 登录表单数据
+ */
+ async function authLogin(
+ params: Recordable,
+ onSuccess?: () => Promise | void,
+ ) {
+ // 异步处理用户登录操作并获取 accessToken
+ let userInfo: null | UserInfo = null;
+ try {
+ loginLoading.value = true;
+ const { accessToken } = await loginApi(params);
+
+ // 如果成功获取到 accessToken
+ if (accessToken) {
+ // 将 accessToken 存储到 accessStore 中
+ accessStore.setAccessToken(accessToken);
+
+ // 获取用户信息并存储到 accessStore 中
+ const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ fetchUserInfo(),
+ getAccessCodesApi(),
+ ]);
+
+ userInfo = fetchUserInfoResult;
+
+ userStore.setUserInfo(userInfo);
+ accessStore.setAccessCodes(accessCodes);
+
+ if (accessStore.loginExpired) {
+ accessStore.setLoginExpired(false);
+ } else {
+ onSuccess
+ ? await onSuccess?.()
+ : await router.push(
+ userInfo.homePath || preferences.app.defaultHomePath,
+ );
+ }
+
+ if (userInfo?.realName) {
+ notification.success({
+ content: $t('authentication.loginSuccess'),
+ description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
+ duration: 3000,
+ });
+ }
+ }
+ } finally {
+ loginLoading.value = false;
+ }
+
+ return {
+ userInfo,
+ };
+ }
+
+ async function logout(redirect: boolean = true) {
+ try {
+ await logoutApi();
+ } catch {
+ // 不做任何处理
+ }
+ resetAllStores();
+ accessStore.setLoginExpired(false);
+
+ // 回登录页带上当前路由地址
+ await router.replace({
+ path: LOGIN_PATH,
+ query: redirect
+ ? {
+ redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+ }
+ : {},
+ });
+ }
+
+ async function fetchUserInfo() {
+ let userInfo: null | UserInfo = null;
+ userInfo = await getUserInfoApi();
+ userStore.setUserInfo(userInfo);
+ return userInfo;
+ }
+
+ function $reset() {
+ loginLoading.value = false;
+ }
+
+ return {
+ $reset,
+ authLogin,
+ fetchUserInfo,
+ loginLoading,
+ logout,
+ };
+});
diff --git a/web/apps/web-naive/src/store/index.ts b/web/apps/web-naive/src/store/index.ts
new file mode 100644
index 0000000..269586e
--- /dev/null
+++ b/web/apps/web-naive/src/store/index.ts
@@ -0,0 +1 @@
+export * from './auth';
diff --git a/web/apps/web-naive/src/views/_core/README.md b/web/apps/web-naive/src/views/_core/README.md
new file mode 100644
index 0000000..8248afe
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/README.md
@@ -0,0 +1,3 @@
+# \_core
+
+此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
diff --git a/web/apps/web-naive/src/views/_core/about/index.vue b/web/apps/web-naive/src/views/_core/about/index.vue
new file mode 100644
index 0000000..0ee5243
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/about/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/authentication/code-login.vue b/web/apps/web-naive/src/views/_core/authentication/code-login.vue
new file mode 100644
index 0000000..acfd1fd
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/authentication/code-login.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/authentication/forget-password.vue b/web/apps/web-naive/src/views/_core/authentication/forget-password.vue
new file mode 100644
index 0000000..fef0d42
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/authentication/forget-password.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/authentication/login.vue b/web/apps/web-naive/src/views/_core/authentication/login.vue
new file mode 100644
index 0000000..099e4c8
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/authentication/login.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/authentication/qrcode-login.vue b/web/apps/web-naive/src/views/_core/authentication/qrcode-login.vue
new file mode 100644
index 0000000..23f5f2d
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/authentication/qrcode-login.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/authentication/register.vue b/web/apps/web-naive/src/views/_core/authentication/register.vue
new file mode 100644
index 0000000..daf89c4
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/authentication/register.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/fallback/coming-soon.vue b/web/apps/web-naive/src/views/_core/fallback/coming-soon.vue
new file mode 100644
index 0000000..f394930
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/fallback/coming-soon.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/fallback/forbidden.vue b/web/apps/web-naive/src/views/_core/fallback/forbidden.vue
new file mode 100644
index 0000000..8ea65fe
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/fallback/forbidden.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/fallback/internal-error.vue b/web/apps/web-naive/src/views/_core/fallback/internal-error.vue
new file mode 100644
index 0000000..819a47d
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/fallback/internal-error.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/fallback/not-found.vue b/web/apps/web-naive/src/views/_core/fallback/not-found.vue
new file mode 100644
index 0000000..4d178e9
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/fallback/not-found.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/_core/fallback/offline.vue b/web/apps/web-naive/src/views/_core/fallback/offline.vue
new file mode 100644
index 0000000..5de4a88
--- /dev/null
+++ b/web/apps/web-naive/src/views/_core/fallback/offline.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue b/web/apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue
new file mode 100644
index 0000000..f1f0b23
--- /dev/null
+++ b/web/apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue
new file mode 100644
index 0000000..190fb41
--- /dev/null
+++ b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
new file mode 100644
index 0000000..02f5091
--- /dev/null
+++ b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue
new file mode 100644
index 0000000..0915c7a
--- /dev/null
+++ b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue
new file mode 100644
index 0000000..7e0f101
--- /dev/null
+++ b/web/apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/dashboard/analytics/index.vue b/web/apps/web-naive/src/views/dashboard/analytics/index.vue
new file mode 100644
index 0000000..5e3d6d2
--- /dev/null
+++ b/web/apps/web-naive/src/views/dashboard/analytics/index.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/dashboard/workspace/index.vue b/web/apps/web-naive/src/views/dashboard/workspace/index.vue
new file mode 100644
index 0000000..b95d613
--- /dev/null
+++ b/web/apps/web-naive/src/views/dashboard/workspace/index.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+ 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
+
+ 今日晴,20℃ - 32℃!
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/demos/form/basic.vue b/web/apps/web-naive/src/views/demos/form/basic.vue
new file mode 100644
index 0000000..fe26624
--- /dev/null
+++ b/web/apps/web-naive/src/views/demos/form/basic.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+ 设置表单值
+
+
+
+
+
diff --git a/web/apps/web-naive/src/views/demos/naive/index.vue b/web/apps/web-naive/src/views/demos/naive/index.vue
new file mode 100644
index 0000000..f72cdb2
--- /dev/null
+++ b/web/apps/web-naive/src/views/demos/naive/index.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ Default
+ Tertiary
+ Primary
+ Info
+ Success
+ Warning
+ Error
+
+
+
+
+
+ 错误
+ 警告
+ 成功
+ 加载中
+
+
+
+
+
+ 错误
+ 警告
+ 成功
+ 加载中
+
+
+
+
diff --git a/web/apps/web-naive/src/views/demos/table/index.vue b/web/apps/web-naive/src/views/demos/table/index.vue
new file mode 100644
index 0000000..ddc958b
--- /dev/null
+++ b/web/apps/web-naive/src/views/demos/table/index.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/apps/web-naive/tailwind.config.mjs b/web/apps/web-naive/tailwind.config.mjs
new file mode 100644
index 0000000..f17f556
--- /dev/null
+++ b/web/apps/web-naive/tailwind.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config';
diff --git a/web/apps/web-naive/tsconfig.json b/web/apps/web-naive/tsconfig.json
new file mode 100644
index 0000000..02c287f
--- /dev/null
+++ b/web/apps/web-naive/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web-app.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "#/*": ["./src/*"]
+ }
+ },
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/web/apps/web-naive/tsconfig.node.json b/web/apps/web-naive/tsconfig.node.json
new file mode 100644
index 0000000..c2f0d86
--- /dev/null
+++ b/web/apps/web-naive/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/node.json",
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "noEmit": false
+ },
+ "include": ["vite.config.mts"]
+}
diff --git a/web/apps/web-naive/vite.config.mts b/web/apps/web-naive/vite.config.mts
new file mode 100644
index 0000000..b6360f1
--- /dev/null
+++ b/web/apps/web-naive/vite.config.mts
@@ -0,0 +1,20 @@
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ vite: {
+ server: {
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // mock代理目标地址
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});
diff --git a/web/cspell.json b/web/cspell.json
new file mode 100644
index 0000000..89545b4
--- /dev/null
+++ b/web/cspell.json
@@ -0,0 +1,68 @@
+{
+ "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
+ "version": "0.2",
+ "language": "en,en-US",
+ "allowCompoundWords": true,
+ "words": [
+ "acmr",
+ "antd",
+ "antdv",
+ "astro",
+ "brotli",
+ "clsx",
+ "defu",
+ "demi",
+ "echarts",
+ "ependencies",
+ "esno",
+ "etag",
+ "execa",
+ "iconify",
+ "iconoir",
+ "intlify",
+ "lockb",
+ "lucide",
+ "minh",
+ "minw",
+ "mkdist",
+ "mockjs",
+ "naiveui",
+ "nocheck",
+ "noopener",
+ "noreferrer",
+ "nprogress",
+ "nuxt",
+ "pinia",
+ "prefixs",
+ "publint",
+ "qrcode",
+ "shadcn",
+ "sonner",
+ "sortablejs",
+ "styl",
+ "taze",
+ "ui-kit",
+ "uicons",
+ "unplugin",
+ "unref",
+ "vben",
+ "vbenjs",
+ "vite",
+ "vitejs",
+ "vitepress",
+ "vnode",
+ "vueuse",
+ "yxxx"
+ ],
+ "ignorePaths": [
+ "**/node_modules/**",
+ "**/dist/**",
+ "**/*-dist/**",
+ "**/icons/**",
+ "pnpm-lock.yaml",
+ "**/*.log",
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/__tests__/**"
+ ]
+}
diff --git a/web/docs/.vitepress/components/demo-preview.vue b/web/docs/.vitepress/components/demo-preview.vue
new file mode 100644
index 0000000..4c8829f
--- /dev/null
+++ b/web/docs/.vitepress/components/demo-preview.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+ ERROR:
+
+ The preview directory does not exist. Please check the 'dir'
+ parameter.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/docs/.vitepress/components/index.ts b/web/docs/.vitepress/components/index.ts
new file mode 100644
index 0000000..9430871
--- /dev/null
+++ b/web/docs/.vitepress/components/index.ts
@@ -0,0 +1 @@
+export { default as DemoPreview } from './demo-preview.vue';
diff --git a/web/docs/.vitepress/components/preview-group.vue b/web/docs/.vitepress/components/preview-group.vue
new file mode 100644
index 0000000..e712157
--- /dev/null
+++ b/web/docs/.vitepress/components/preview-group.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
+
+ {{ open ? 'Collapse code' : 'Expand code' }}
+
+
+
+
+
+
+
diff --git a/web/docs/.vitepress/config/en.mts b/web/docs/.vitepress/config/en.mts
new file mode 100644
index 0000000..a74a7e5
--- /dev/null
+++ b/web/docs/.vitepress/config/en.mts
@@ -0,0 +1,231 @@
+import type { DefaultTheme } from 'vitepress';
+
+import { defineConfig } from 'vitepress';
+
+import { version } from '../../../package.json';
+
+export const en = defineConfig({
+ description: 'Vben Admin & Enterprise level management system framework',
+ lang: 'en-US',
+ themeConfig: {
+ darkModeSwitchLabel: 'Theme',
+ darkModeSwitchTitle: 'Switch to Dark Mode',
+ docFooter: {
+ next: 'Next Page',
+ prev: 'Previous Page',
+ },
+ editLink: {
+ pattern:
+ 'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
+ text: 'Edit this page on GitHub',
+ },
+ footer: {
+ copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
+ message: 'Released under the MIT License.',
+ },
+ langMenuLabel: 'Language',
+ lastUpdated: {
+ formatOptions: {
+ dateStyle: 'short',
+ timeStyle: 'medium',
+ },
+ text: 'Last updated on',
+ },
+ lightModeSwitchTitle: 'Switch to Light Mode',
+ nav: nav(),
+ outline: {
+ label: 'Navigate',
+ },
+ returnToTopLabel: 'Back to top',
+ sidebar: {
+ '/en/commercial/': {
+ base: '/en/commercial/',
+ items: sidebarCommercial(),
+ },
+ '/en/guide/': { base: '/en/guide/', items: sidebarGuide() },
+ },
+ },
+});
+
+function sidebarGuide(): DefaultTheme.SidebarItem[] {
+ return [
+ {
+ collapsed: false,
+ text: 'Introduction',
+ items: [
+ {
+ link: 'introduction/vben',
+ text: 'About Vben Admin',
+ },
+ {
+ link: 'introduction/why',
+ text: 'Why Choose Us?',
+ },
+ { link: 'introduction/quick-start', text: 'Quick Start' },
+ { link: 'introduction/thin', text: 'Lite Version' },
+ ],
+ },
+ {
+ text: 'Basics',
+ items: [
+ { link: 'essentials/concept', text: 'Basic Concepts' },
+ { link: 'essentials/development', text: 'Local Development' },
+ { link: 'essentials/route', text: 'Routing and Menu' },
+ { link: 'essentials/settings', text: 'Configuration' },
+ { link: 'essentials/icons', text: 'Icons' },
+ { link: 'essentials/styles', text: 'Styles' },
+ { link: 'essentials/external-module', text: 'External Modules' },
+ { link: 'essentials/build', text: 'Build and Deployment' },
+ { link: 'essentials/server', text: 'Server Interaction and Data Mock' },
+ ],
+ },
+ {
+ text: 'Advanced',
+ items: [
+ { link: 'in-depth/login', text: 'Login' },
+ { link: 'in-depth/theme', text: 'Theme' },
+ { link: 'in-depth/access', text: 'Access Control' },
+ { link: 'in-depth/locale', text: 'Internationalization' },
+ { link: 'in-depth/features', text: 'Common Features' },
+ { link: 'in-depth/check-updates', text: 'Check Updates' },
+ { link: 'in-depth/loading', text: 'Global Loading' },
+ { link: 'in-depth/ui-framework', text: 'UI Framework Switching' },
+ ],
+ },
+ {
+ text: 'Engineering',
+ items: [
+ { link: 'project/standard', text: 'Standards' },
+ { link: 'project/cli', text: 'CLI' },
+ { link: 'project/dir', text: 'Directory Explanation' },
+ { link: 'project/test', text: 'Unit Testing' },
+ { link: 'project/tailwindcss', text: 'Tailwind CSS' },
+ { link: 'project/changeset', text: 'Changeset' },
+ { link: 'project/vite', text: 'Vite Config' },
+ ],
+ },
+ {
+ text: 'Others',
+ items: [
+ { link: 'other/project-update', text: 'Project Update' },
+ { link: 'other/remove-code', text: 'Remove Code' },
+ { link: 'other/faq', text: 'FAQ' },
+ ],
+ },
+ ];
+}
+
+function sidebarCommercial(): DefaultTheme.SidebarItem[] {
+ return [
+ {
+ link: 'community',
+ text: 'Community',
+ },
+ {
+ link: 'technical-support',
+ text: 'Technical-support',
+ },
+ {
+ link: 'customized',
+ text: 'Customized',
+ },
+ ];
+}
+
+function nav(): DefaultTheme.NavItem[] {
+ return [
+ {
+ activeMatch: '^/en/(guide|components)/',
+ text: 'Doc',
+ items: [
+ {
+ activeMatch: '^/en/guide/',
+ link: '/en/guide/introduction/vben',
+ text: 'Guide',
+ },
+ // {
+ // activeMatch: '^/en/components/',
+ // link: '/en/components/introduction',
+ // text: 'Components',
+ // },
+ {
+ text: 'Historical Versions',
+ items: [
+ {
+ link: 'https://doc.vvbin.cn',
+ text: '2.x Version Documentation',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ text: 'Demo',
+ items: [
+ {
+ text: 'Vben Admin',
+ items: [
+ {
+ link: 'https://www.vben.pro',
+ text: 'Demo Version',
+ },
+ {
+ link: 'https://ant.vben.pro',
+ text: 'Ant Design Vue Version',
+ },
+ {
+ link: 'https://naive.vben.pro',
+ text: 'Naive Version',
+ },
+ {
+ link: 'https://ele.vben.pro',
+ text: 'Element Plus Version',
+ },
+ ],
+ },
+ {
+ text: 'Others',
+ items: [
+ {
+ link: 'https://vben.vvbin.cn',
+ text: 'Vben Admin 2.x',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ text: version,
+ items: [
+ {
+ link: 'https://github.com/vbenjs/vue-vben-admin/releases',
+ text: 'Changelog',
+ },
+ {
+ link: 'https://github.com/orgs/vbenjs/projects/5',
+ text: 'Roadmap',
+ },
+ {
+ link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
+ text: 'Contribution',
+ },
+ ],
+ },
+ {
+ link: '/commercial/technical-support',
+ text: '🦄 Tech Support',
+ },
+ {
+ link: '/sponsor/personal',
+ text: '✨ Sponsor',
+ },
+ {
+ link: '/commercial/community',
+ text: '👨👦👦 Community',
+ },
+ // {
+ // link: '/friend-links/',
+ // text: '🤝 Friend Links',
+ // },
+ ];
+}
diff --git a/web/docs/.vitepress/config/index.mts b/web/docs/.vitepress/config/index.mts
new file mode 100644
index 0000000..6b8cb81
--- /dev/null
+++ b/web/docs/.vitepress/config/index.mts
@@ -0,0 +1,25 @@
+import { withPwa } from '@vite-pwa/vitepress';
+import { defineConfigWithTheme } from 'vitepress';
+
+import { en } from './en.mts';
+import { shared } from './shared.mts';
+import { zh } from './zh.mts';
+
+export default withPwa(
+ defineConfigWithTheme({
+ ...shared,
+ locales: {
+ en: {
+ label: 'English',
+ lang: 'en',
+ link: '/en/',
+ ...en,
+ },
+ root: {
+ label: '简体中文',
+ lang: 'zh-CN',
+ ...zh,
+ },
+ },
+ }),
+);
diff --git a/web/docs/.vitepress/config/plugins/demo-preview.ts b/web/docs/.vitepress/config/plugins/demo-preview.ts
new file mode 100644
index 0000000..03b1698
--- /dev/null
+++ b/web/docs/.vitepress/config/plugins/demo-preview.ts
@@ -0,0 +1,143 @@
+import type { MarkdownEnv, MarkdownRenderer } from 'vitepress';
+
+import crypto from 'node:crypto';
+import { readdirSync } from 'node:fs';
+import { join } from 'node:path';
+
+export const rawPathRegexp =
+ // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/strict
+ /^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/;
+
+function rawPathToToken(rawPath: string) {
+ const [
+ filepath = '',
+ extension = '',
+ region = '',
+ lines = '',
+ lang = '',
+ rawTitle = '',
+ ] = (rawPathRegexp.exec(rawPath) || []).slice(1);
+
+ const title = rawTitle || filepath.split('/').pop() || '';
+
+ return { extension, filepath, lang, lines, region, title };
+}
+
+export const demoPreviewPlugin = (md: MarkdownRenderer) => {
+ md.core.ruler.after('inline', 'demo-preview', (state) => {
+ const insertComponentImport = (importString: string) => {
+ const index = state.tokens.findIndex(
+ (i) => i.type === 'html_block' && i.content.match(/\n`;
+ state.tokens.splice(0, 0, importComponent);
+ } else {
+ if (state.tokens[index]) {
+ const content = state.tokens[index].content;
+ state.tokens[index].content = content.replace(
+ '',
+ `${importString}\n`,
+ );
+ }
+ }
+ };
+ // Define the regular expression to match the desired pattern
+ const regex = /]*\sdir="([^"]*)"/g;
+ // Iterate through the Markdown content and replace the pattern
+ state.src = state.src.replaceAll(regex, (_match, dir) => {
+ const componentDir = join(process.cwd(), 'src', dir).replaceAll(
+ '\\',
+ '/',
+ );
+
+ let childFiles: string[] = [];
+ let dirExists = true;
+
+ try {
+ childFiles =
+ readdirSync(componentDir, {
+ encoding: 'utf8',
+ recursive: false,
+ withFileTypes: false,
+ }) || [];
+ } catch {
+ dirExists = false;
+ }
+
+ if (!dirExists) {
+ return '';
+ }
+
+ const uniqueWord = generateContentHash(componentDir);
+
+ const ComponentName = `DemoComponent_${uniqueWord}`;
+ insertComponentImport(
+ `import ${ComponentName} from '${componentDir}/index.vue'`,
+ );
+ const { path: _path } = state.env as MarkdownEnv;
+
+ const index = state.tokens.findIndex((i) => i.content.match(regex));
+
+ if (!state.tokens[index]) {
+ return '';
+ }
+ const firstString = 'index.vue';
+ childFiles = childFiles.sort((a, b) => {
+ if (a === firstString) return -1;
+ if (b === firstString) return 1;
+ return a.localeCompare(b, 'en', { sensitivity: 'base' });
+ });
+ state.tokens[index].content =
+ `<${ComponentName}/>
+ `;
+
+ const _dummyToken = new state.Token('', '', 0);
+ const tokenArray: Array = [];
+ childFiles.forEach((filename) => {
+ // const slotName = filename.replace(extname(filename), '');
+
+ const templateStart = new state.Token('html_inline', '', 0);
+ templateStart.content = ``;
+ tokenArray.push(templateStart);
+
+ const resolvedPath = join(componentDir, filename);
+
+ const { extension, filepath, lang, lines, title } =
+ rawPathToToken(resolvedPath);
+ // Add code tokens for each line
+ const token = new state.Token('fence', 'code', 0);
+ token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
+ title ? `[${title}]` : ''
+ }`;
+
+ token.content = `<<< ${filepath}`;
+ (token as any).src = [resolvedPath];
+ tokenArray.push(token);
+
+ const templateEnd = new state.Token('html_inline', '', 0);
+ templateEnd.content = ' ';
+ tokenArray.push(templateEnd);
+ });
+ const endTag = new state.Token('html_inline', '', 0);
+ endTag.content = ' ';
+ tokenArray.push(endTag);
+
+ state.tokens.splice(index + 1, 0, ...tokenArray);
+
+ // console.log(
+ // state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
+ // );
+ return '';
+ });
+ });
+};
+
+function generateContentHash(input: string, length: number = 10): string {
+ // 使用 SHA-256 生成哈希值
+ const hash = crypto.createHash('sha256').update(input).digest('hex');
+
+ // 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
+ return Number.parseInt(hash, 16).toString(36).slice(0, length);
+}
diff --git a/web/docs/.vitepress/config/shared.mts b/web/docs/.vitepress/config/shared.mts
new file mode 100644
index 0000000..c48cc60
--- /dev/null
+++ b/web/docs/.vitepress/config/shared.mts
@@ -0,0 +1,172 @@
+import type { PwaOptions } from '@vite-pwa/vitepress';
+import type { HeadConfig } from 'vitepress';
+
+import { resolve } from 'node:path';
+
+import {
+ viteArchiverPlugin,
+ viteVxeTableImportsPlugin,
+} from '@vben/vite-config';
+
+import {
+ GitChangelog,
+ GitChangelogMarkdownSection,
+} from '@nolebase/vitepress-plugin-git-changelog/vite';
+import tailwind from 'tailwindcss';
+import { defineConfig, postcssIsolateStyles } from 'vitepress';
+import {
+ groupIconMdPlugin,
+ groupIconVitePlugin,
+} from 'vitepress-plugin-group-icons';
+
+import { demoPreviewPlugin } from './plugins/demo-preview';
+import { search as zhSearch } from './zh.mts';
+
+export const shared = defineConfig({
+ appearance: 'dark',
+ head: head(),
+ markdown: {
+ preConfig(md) {
+ md.use(demoPreviewPlugin);
+ md.use(groupIconMdPlugin);
+ },
+ },
+ pwa: pwa(),
+ srcDir: 'src',
+ themeConfig: {
+ i18nRouting: true,
+ logo: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
+ search: {
+ options: {
+ locales: {
+ ...zhSearch,
+ },
+ },
+ provider: 'local',
+ },
+ siteTitle: 'Vben Admin',
+ socialLinks: [
+ { icon: 'github', link: 'https://github.com/vbenjs/vue-vben-admin' },
+ ],
+ },
+ title: 'Vben Admin',
+ vite: {
+ build: {
+ chunkSizeWarningLimit: Infinity,
+ minify: 'terser',
+ },
+ css: {
+ postcss: {
+ plugins: [
+ tailwind(),
+ postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] }),
+ ],
+ },
+ preprocessorOptions: {
+ scss: {
+ api: 'modern',
+ },
+ },
+ },
+ json: {
+ stringify: true,
+ },
+ plugins: [
+ GitChangelog({
+ mapAuthors: [
+ {
+ mapByNameAliases: ['Vben'],
+ name: 'vben',
+ username: 'anncwb',
+ },
+ {
+ name: 'vince',
+ username: 'vince292007',
+ },
+ {
+ name: 'Li Kui',
+ username: 'likui628',
+ },
+ ],
+ repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
+ }),
+ GitChangelogMarkdownSection(),
+ viteArchiverPlugin({ outputDir: '.vitepress' }),
+ groupIconVitePlugin(),
+ await viteVxeTableImportsPlugin(),
+ ],
+ server: {
+ fs: {
+ allow: ['../..'],
+ },
+ host: true,
+ port: 6173,
+ },
+
+ ssr: {
+ external: ['@vue/repl'],
+ },
+ },
+});
+
+function head(): HeadConfig[] {
+ return [
+ ['meta', { content: 'Vbenjs Team', name: 'author' }],
+ [
+ 'meta',
+ {
+ content: 'vben, vitejs, vite, shacdn-ui, vue',
+ name: 'keywords',
+ },
+ ],
+ ['link', { href: '/favicon.ico', rel: 'icon', type: 'image/svg+xml' }],
+ [
+ 'meta',
+ {
+ content:
+ 'width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no',
+ name: 'viewport',
+ },
+ ],
+ ['meta', { content: 'vben admin docs', name: 'keywords' }],
+ ['link', { href: '/favicon.ico', rel: 'icon' }],
+ // [
+ // 'script',
+ // {
+ // src: 'https://cdn.tailwindcss.com',
+ // },
+ // ],
+ ];
+}
+
+function pwa(): PwaOptions {
+ return {
+ includeManifestIcons: false,
+ manifest: {
+ description:
+ 'Vben Admin is a modern admin dashboard template based on Vue 3. ',
+ icons: [
+ {
+ sizes: '192x192',
+ src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-192.png',
+ type: 'image/png',
+ },
+ {
+ sizes: '512x512',
+ src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-512.png',
+ type: 'image/png',
+ },
+ ],
+ id: '/',
+ name: 'Vben Admin Doc',
+ short_name: 'vben_admin_doc',
+ theme_color: '#ffffff',
+ },
+ outDir: resolve(process.cwd(), '.vitepress/dist'),
+ registerType: 'autoUpdate',
+ workbox: {
+ globPatterns: ['**/*.{css,js,html,svg,png,ico,txt,woff2}'],
+ maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
+ },
+ };
+}
diff --git a/web/docs/.vitepress/config/zh.mts b/web/docs/.vitepress/config/zh.mts
new file mode 100644
index 0000000..2c3753d
--- /dev/null
+++ b/web/docs/.vitepress/config/zh.mts
@@ -0,0 +1,358 @@
+import type { DefaultTheme } from 'vitepress';
+
+import { defineConfig } from 'vitepress';
+
+import { version } from '../../../package.json';
+
+export const zh = defineConfig({
+ description: 'Vben Admin & 企业级管理系统框架',
+ lang: 'zh-Hans',
+ themeConfig: {
+ darkModeSwitchLabel: '主题',
+ darkModeSwitchTitle: '切换到深色模式',
+ docFooter: {
+ next: '下一页',
+ prev: '上一页',
+ },
+ editLink: {
+ pattern:
+ 'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
+ text: '在 GitHub 上编辑此页面',
+ },
+ footer: {
+ copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
+ message: '基于 MIT 许可发布.',
+ },
+ langMenuLabel: '多语言',
+ lastUpdated: {
+ formatOptions: {
+ dateStyle: 'short',
+ timeStyle: 'medium',
+ },
+ text: '最后更新于',
+ },
+ lightModeSwitchTitle: '切换到浅色模式',
+ nav: nav(),
+
+ outline: {
+ label: '页面导航',
+ },
+ returnToTopLabel: '回到顶部',
+
+ sidebar: {
+ '/commercial/': { base: '/commercial/', items: sidebarCommercial() },
+ '/components/': { base: '/components/', items: sidebarComponents() },
+ '/guide/': { base: '/guide/', items: sidebarGuide() },
+ },
+ sidebarMenuLabel: '菜单',
+ },
+});
+
+function sidebarGuide(): DefaultTheme.SidebarItem[] {
+ return [
+ {
+ collapsed: false,
+ text: '简介',
+ items: [
+ {
+ link: 'introduction/vben',
+ text: '关于 Vben Admin',
+ },
+ {
+ link: 'introduction/why',
+ text: '为什么选择我们?',
+ },
+ { link: 'introduction/quick-start', text: '快速开始' },
+ { link: 'introduction/thin', text: '精简版本' },
+ {
+ base: '/',
+ link: 'components/introduction',
+ text: '组件文档',
+ },
+ ],
+ },
+ {
+ text: '基础',
+ items: [
+ { link: 'essentials/concept', text: '基础概念' },
+ { link: 'essentials/development', text: '本地开发' },
+ { link: 'essentials/route', text: '路由和菜单' },
+ { link: 'essentials/settings', text: '配置' },
+ { link: 'essentials/icons', text: '图标' },
+ { link: 'essentials/styles', text: '样式' },
+ { link: 'essentials/external-module', text: '外部模块' },
+ { link: 'essentials/build', text: '构建与部署' },
+ { link: 'essentials/server', text: '服务端交互与数据Mock' },
+ ],
+ },
+ {
+ text: '深入',
+ items: [
+ { link: 'in-depth/login', text: '登录' },
+ // { link: 'in-depth/layout', text: '布局' },
+ { link: 'in-depth/theme', text: '主题' },
+ { link: 'in-depth/access', text: '权限' },
+ { link: 'in-depth/locale', text: '国际化' },
+ { link: 'in-depth/features', text: '常用功能' },
+ { link: 'in-depth/check-updates', text: '检查更新' },
+ { link: 'in-depth/loading', text: '全局loading' },
+ { link: 'in-depth/ui-framework', text: '组件库切换' },
+ ],
+ },
+ {
+ text: '工程',
+ items: [
+ { link: 'project/standard', text: '规范' },
+ { link: 'project/cli', text: 'CLI' },
+ { link: 'project/dir', text: '目录说明' },
+ { link: 'project/test', text: '单元测试' },
+ { link: 'project/tailwindcss', text: 'Tailwind CSS' },
+ { link: 'project/changeset', text: 'Changeset' },
+ { link: 'project/vite', text: 'Vite Config' },
+ ],
+ },
+ {
+ text: '其他',
+ items: [
+ { link: 'other/project-update', text: '项目更新' },
+ { link: 'other/remove-code', text: '移除代码' },
+ { link: 'other/faq', text: '常见问题' },
+ ],
+ },
+ ];
+}
+
+function sidebarCommercial(): DefaultTheme.SidebarItem[] {
+ return [
+ {
+ link: 'community',
+ text: '交流群',
+ },
+ {
+ link: 'technical-support',
+ text: '技术支持',
+ },
+ {
+ link: 'customized',
+ text: '定制开发',
+ },
+ ];
+}
+
+function sidebarComponents(): DefaultTheme.SidebarItem[] {
+ return [
+ {
+ text: '组件',
+ items: [
+ {
+ link: 'introduction',
+ text: '介绍',
+ },
+ ],
+ },
+ {
+ collapsed: false,
+ text: '布局组件',
+ items: [
+ {
+ link: 'layout-ui/page',
+ text: 'Page 页面',
+ },
+ ],
+ },
+ {
+ collapsed: false,
+ text: '通用组件',
+ items: [
+ {
+ link: 'common-ui/vben-api-component',
+ text: 'ApiComponent Api组件包装器',
+ },
+ {
+ link: 'common-ui/vben-alert',
+ text: 'Alert 轻量提示框',
+ },
+ {
+ link: 'common-ui/vben-modal',
+ text: 'Modal 模态框',
+ },
+ {
+ link: 'common-ui/vben-drawer',
+ text: 'Drawer 抽屉',
+ },
+ {
+ link: 'common-ui/vben-form',
+ text: 'Form 表单',
+ },
+ {
+ link: 'common-ui/vben-vxe-table',
+ text: 'Vxe Table 表格',
+ },
+ {
+ link: 'common-ui/vben-count-to-animator',
+ text: 'CountToAnimator 数字动画',
+ },
+ {
+ link: 'common-ui/vben-ellipsis-text',
+ text: 'EllipsisText 省略文本',
+ },
+ ],
+ },
+ ];
+}
+
+function nav(): DefaultTheme.NavItem[] {
+ return [
+ {
+ activeMatch: '^/(guide|components)/',
+ text: '文档',
+ items: [
+ {
+ activeMatch: '^/guide/',
+ link: '/guide/introduction/vben',
+ text: '指南',
+ },
+ {
+ activeMatch: '^/components/',
+ link: '/components/introduction',
+ text: '组件',
+ },
+ {
+ text: '历史版本',
+ items: [
+ {
+ link: 'https://doc.vvbin.cn',
+ text: '2.x版本文档',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ text: '演示',
+ items: [
+ {
+ text: 'Vben Admin',
+ items: [
+ {
+ link: 'https://www.vben.pro',
+ text: '演示版本',
+ },
+ {
+ link: 'https://ant.vben.pro',
+ text: 'Ant Design Vue 版本',
+ },
+ {
+ link: 'https://naive.vben.pro',
+ text: 'Naive 版本',
+ },
+ {
+ link: 'https://ele.vben.pro',
+ text: 'Element Plus版本',
+ },
+ ],
+ },
+ {
+ text: '其他',
+ items: [
+ {
+ link: 'https://vben.vvbin.cn',
+ text: 'Vben Admin 2.x',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ text: version,
+ items: [
+ {
+ link: 'https://github.com/vbenjs/vue-vben-admin/releases',
+ text: '更新日志',
+ },
+ {
+ link: 'https://github.com/orgs/vbenjs/projects/5',
+ text: '路线图',
+ },
+ {
+ link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
+ text: '贡献',
+ },
+ ],
+ },
+ {
+ link: '/commercial/technical-support',
+ text: '🦄 技术支持',
+ },
+ {
+ link: '/sponsor/personal',
+ text: '✨ 赞助',
+ },
+ {
+ link: '/commercial/community',
+ text: '👨👦👦 交流群',
+ // items: [
+ // {
+ // link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',
+ // text: 'QQ频道',
+ // },
+ // {
+ // link: 'https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=mjZmlhgVzzUxvdxllB6C1vHpX8O8QRL0&authKey=DBdFbBwERmfaKY95JvRWqLCJIRGJAmKyZbrpzZ41EKDMZ5SR6MfbjOBaaNRN73fr&noverify=0&group_code=4286109',
+ // text: 'QQ群',
+ // },
+ // {
+ // link: 'https://discord.gg/VU62jTecad',
+ // text: 'Discord',
+ // },
+ // ],
+ },
+ // {
+ // link: '/friend-links/',
+ // text: '🤝 友情链接',
+ // },
+ ];
+}
+
+export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = {
+ root: {
+ placeholder: '搜索文档',
+ translations: {
+ button: {
+ buttonAriaLabel: '搜索文档',
+ buttonText: '搜索文档',
+ },
+ modal: {
+ errorScreen: {
+ helpText: '你可能需要检查你的网络连接',
+ titleText: '无法获取结果',
+ },
+ footer: {
+ closeText: '关闭',
+ navigateText: '切换',
+ searchByText: '搜索提供者',
+ selectText: '选择',
+ },
+ noResultsScreen: {
+ noResultsText: '无法找到相关结果',
+ reportMissingResultsLinkText: '点击反馈',
+ reportMissingResultsText: '你认为该查询应该有结果?',
+ suggestedQueryText: '你可以尝试查询',
+ },
+ searchBox: {
+ cancelButtonAriaLabel: '取消',
+ cancelButtonText: '取消',
+ resetButtonAriaLabel: '清除查询条件',
+ resetButtonTitle: '清除查询条件',
+ },
+ startScreen: {
+ favoriteSearchesTitle: '收藏',
+ noRecentSearchesText: '没有搜索历史',
+ recentSearchesTitle: '搜索历史',
+ removeFavoriteSearchButtonTitle: '从收藏中移除',
+ removeRecentSearchButtonTitle: '从搜索历史中移除',
+ saveRecentSearchButtonTitle: '保存至搜索历史',
+ },
+ },
+ },
+ },
+};
diff --git a/web/docs/.vitepress/theme/components/site-layout.vue b/web/docs/.vitepress/theme/components/site-layout.vue
new file mode 100644
index 0000000..96427d0
--- /dev/null
+++ b/web/docs/.vitepress/theme/components/site-layout.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/docs/.vitepress/theme/components/vben-contributors.vue b/web/docs/.vitepress/theme/components/vben-contributors.vue
new file mode 100644
index 0000000..9b887d9
--- /dev/null
+++ b/web/docs/.vitepress/theme/components/vben-contributors.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
Contributors
+
+
+
+
+
+
+
diff --git a/web/docs/.vitepress/theme/index.ts b/web/docs/.vitepress/theme/index.ts
new file mode 100644
index 0000000..7d4d3dc
--- /dev/null
+++ b/web/docs/.vitepress/theme/index.ts
@@ -0,0 +1,29 @@
+// https://vitepress.dev/guide/custom-theme
+import type { EnhanceAppContext, Theme } from 'vitepress';
+
+import { NolebaseGitChangelogPlugin } from '@nolebase/vitepress-plugin-git-changelog/client';
+import DefaultTheme from 'vitepress/theme';
+
+import { DemoPreview } from '../components';
+import SiteLayout from './components/site-layout.vue';
+import VbenContributors from './components/vben-contributors.vue';
+import { initHmPlugin } from './plugins/hm';
+
+import './styles';
+
+import 'virtual:group-icons.css';
+import '@nolebase/vitepress-plugin-git-changelog/client/style.css';
+
+export default {
+ async enhanceApp(ctx: EnhanceAppContext) {
+ const { app } = ctx;
+ app.component('VbenContributors', VbenContributors);
+ app.component('DemoPreview', DemoPreview);
+ app.use(NolebaseGitChangelogPlugin);
+
+ // 百度统计
+ initHmPlugin();
+ },
+ extends: DefaultTheme,
+ Layout: SiteLayout,
+} satisfies Theme;
diff --git a/web/docs/.vitepress/theme/plugins/hm.ts b/web/docs/.vitepress/theme/plugins/hm.ts
new file mode 100644
index 0000000..5e0a931
--- /dev/null
+++ b/web/docs/.vitepress/theme/plugins/hm.ts
@@ -0,0 +1,28 @@
+import { inBrowser } from 'vitepress';
+
+const SITE_ID = '2e443a834727c065877c01d89921545e';
+
+declare global {
+ interface Window {
+ _hmt: any;
+ }
+}
+
+function registerAnalytics() {
+ window._hmt = window._hmt || [];
+ const script = document.createElement('script');
+ script.innerHTML = `var _hmt = _hmt || [];
+ (function() {
+ var hm = document.createElement("script");
+ hm.src = "https://hm.baidu.com/hm.js?${SITE_ID}";
+ var s = document.getElementsByTagName("script")[0];
+ s.parentNode.insertBefore(hm, s);
+ })()`;
+ document.querySelector('head')?.append(script);
+}
+
+export function initHmPlugin() {
+ if (inBrowser && import.meta.env.PROD) {
+ registerAnalytics();
+ }
+}
diff --git a/web/docs/.vitepress/theme/styles/base.css b/web/docs/.vitepress/theme/styles/base.css
new file mode 100644
index 0000000..8eb423a
--- /dev/null
+++ b/web/docs/.vitepress/theme/styles/base.css
@@ -0,0 +1,22 @@
+html.dark {
+ color-scheme: dark;
+}
+
+.dark .VPContent {
+ /* background-color: #14161a; */
+}
+
+.form-valid-error p {
+ margin: 0;
+}
+
+/* 顶部导航栏选中项样式 */
+.VPNavBarMenuLink,
+.VPNavBarMenuGroup {
+ border-bottom: 1px solid transparent;
+}
+
+.VPNavBarMenuLink.active,
+.VPNavBarMenuGroup.active {
+ border-bottom-color: var(--vp-c-brand-1);
+}
diff --git a/web/docs/.vitepress/theme/styles/index.ts b/web/docs/.vitepress/theme/styles/index.ts
new file mode 100644
index 0000000..566e63f
--- /dev/null
+++ b/web/docs/.vitepress/theme/styles/index.ts
@@ -0,0 +1,4 @@
+import '@vben/styles';
+
+import './variables.css';
+import './base.css';
diff --git a/web/docs/.vitepress/theme/styles/variables.css b/web/docs/.vitepress/theme/styles/variables.css
new file mode 100644
index 0000000..124bb0c
--- /dev/null
+++ b/web/docs/.vitepress/theme/styles/variables.css
@@ -0,0 +1,132 @@
+/**
+ * Customize default theme styling by overriding CSS variables:
+ * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
+ */
+
+/**
+ * Colors
+ *
+ * Each colors have exact same color scale system with 3 levels of solid
+ * colors with different brightness, and 1 soft color.
+ *
+ * - `XXX-1`: The most solid color used mainly for colored text. It must
+ * satisfy the contrast ratio against when used on top of `XXX-soft`.
+ *
+ * - `XXX-2`: The color used mainly for hover state of the button.
+ *
+ * - `XXX-3`: The color for solid background, such as bg color of the button.
+ * It must satisfy the contrast ratio with pure white (#ffffff) text on
+ * top of it.
+ *
+ * - `XXX-soft`: The color used for subtle background such as custom container
+ * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
+ * on top of it.
+ *
+ * The soft color must be semi transparent alpha channel. This is crucial
+ * because it allows adding multiple "soft" colors on top of each other
+ * to create a accent, such as when having inline code block inside
+ * custom containers.
+ *
+ * - `default`: The color used purely for subtle indication without any
+ * special meanings attched to it such as bg color for menu hover state.
+ *
+ * - `brand`: Used for primary brand colors, such as link text, button with
+ * brand theme, etc.
+ *
+ * - `tip`: Used to indicate useful information. The default theme uses the
+ * brand color for this by default.
+ *
+ * - `warning`: Used to indicate warning to the users. Used in custom
+ * container, badges, etc.
+ *
+ * - `danger`: Used to show error, or dangerous message to the users. Used
+ * in custom container, badges, etc.
+ * -------------------------------------------------------------------------- */
+
+:root {
+ /* --vp-c-indigo-1: #4f69fd; */
+ --vp-c-default-1: var(--vp-c-gray-1);
+ --vp-c-default-2: var(--vp-c-gray-2);
+ --vp-c-default-3: var(--vp-c-gray-3);
+ --vp-c-default-soft: var(--vp-c-gray-soft);
+ --vp-c-brand-1: var(--vp-c-indigo-1);
+ --vp-c-brand-2: var(--vp-c-indigo-2);
+ --vp-c-brand-3: var(--vp-c-indigo-3);
+ --vp-c-brand-soft: var(--vp-c-indigo-soft);
+ --vp-c-tip-1: var(--vp-c-brand-1);
+ --vp-c-tip-2: var(--vp-c-brand-2);
+ --vp-c-tip-3: var(--vp-c-brand-3);
+ --vp-c-tip-soft: var(--vp-c-brand-soft);
+ --vp-c-warning-1: var(--vp-c-yellow-1);
+ --vp-c-warning-2: var(--vp-c-yellow-2);
+ --vp-c-warning-3: var(--vp-c-yellow-3);
+ --vp-c-warning-soft: var(--vp-c-yellow-soft);
+ --vp-c-danger-1: var(--vp-c-red-1);
+ --vp-c-danger-2: var(--vp-c-red-2);
+ --vp-c-danger-3: var(--vp-c-red-3);
+ --vp-c-danger-soft: var(--vp-c-red-soft);
+
+ /**
+ * Component: Button
+ * -------------------------------------------------------------------------- */
+
+ --vp-button-brand-border: transparent;
+ --vp-button-brand-text: var(--vp-c-white);
+ --vp-button-brand-bg: var(--vp-c-brand-3);
+ --vp-button-brand-hover-border: transparent;
+ --vp-button-brand-hover-text: var(--vp-c-white);
+ --vp-button-brand-hover-bg: var(--vp-c-brand-2);
+ --vp-button-brand-active-border: transparent;
+ --vp-button-brand-active-text: var(--vp-c-white);
+ --vp-button-brand-active-bg: var(--vp-c-brand-1);
+
+ /**
+ * Component: Home
+ * -------------------------------------------------------------------------- */
+
+ --vp-home-hero-name-color: transparent;
+ --vp-home-hero-name-background: linear-gradient(
+ 120deg,
+ var(--vp-c-indigo-1) 30%,
+ #18cefe
+ );
+ --vp-home-hero-image-background-image: linear-gradient(
+ -45deg,
+ #18cefe 50%,
+ #c279ed 50%
+ );
+ --vp-home-hero-image-filter: blur(44px);
+
+ /**
+ * Component: Custom Block
+ * -------------------------------------------------------------------------- */
+ --vp-custom-block-tip-border: transparent;
+ --vp-custom-block-tip-text: var(--vp-c-text-1);
+ --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
+ --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
+
+ /**
+ * modal zIndex
+ */
+ --popup-z-index: 1000;
+}
+
+@media (min-width: 640px) {
+ :root {
+ --vp-home-hero-image-filter: blur(56px);
+ }
+}
+
+@media (min-width: 960px) {
+ :root {
+ --vp-home-hero-image-filter: blur(68px);
+ }
+}
+
+/**
+ * Component: Algolia
+ * -------------------------------------------------------------------------- */
+
+.DocSearch {
+ --docsearch-primary-color: var(--vp-c-brand-1) !important;
+}
diff --git a/web/docs/package.json b/web/docs/package.json
new file mode 100644
index 0000000..f57dfc8
--- /dev/null
+++ b/web/docs/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@vben/docs",
+ "version": "5.5.7",
+ "private": true,
+ "scripts": {
+ "build": "vitepress build",
+ "dev": "vitepress dev",
+ "docs:preview": "vitepress preview"
+ },
+ "imports": {
+ "#/*": {
+ "node": "./src/_env/node/*",
+ "default": "./src/_env/*"
+ }
+ },
+ "dependencies": {
+ "@vben-core/shadcn-ui": "workspace:*",
+ "@vben/common-ui": "workspace:*",
+ "@vben/locales": "workspace:*",
+ "@vben/plugins": "workspace:*",
+ "@vben/styles": "workspace:*",
+ "ant-design-vue": "catalog:",
+ "lucide-vue-next": "catalog:",
+ "medium-zoom": "catalog:",
+ "radix-vue": "catalog:",
+ "vitepress-plugin-group-icons": "catalog:"
+ },
+ "devDependencies": {
+ "@nolebase/vitepress-plugin-git-changelog": "catalog:",
+ "@vben/vite-config": "workspace:*",
+ "@vite-pwa/vitepress": "catalog:",
+ "vitepress": "catalog:",
+ "vue": "catalog:"
+ }
+}
diff --git a/web/docs/src/_env/adapter/component.ts b/web/docs/src/_env/adapter/component.ts
new file mode 100644
index 0000000..ec3d019
--- /dev/null
+++ b/web/docs/src/_env/adapter/component.ts
@@ -0,0 +1,128 @@
+/**
+ * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
+ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
+ */
+
+import type { Component, SetupContext } from 'vue';
+
+import type { BaseFormComponentType } from '@vben/common-ui';
+
+import { h } from 'vue';
+
+import { globalShareState } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import {
+ AutoComplete,
+ Button,
+ Checkbox,
+ CheckboxGroup,
+ DatePicker,
+ Divider,
+ Input,
+ InputNumber,
+ InputPassword,
+ Mentions,
+ notification,
+ Radio,
+ RadioGroup,
+ RangePicker,
+ Rate,
+ Select,
+ Space,
+ Switch,
+ Textarea,
+ TimePicker,
+ TreeSelect,
+ Upload,
+} from 'ant-design-vue';
+
+const withDefaultPlaceholder = (
+ component: T,
+ type: 'input' | 'select',
+) => {
+ return (props: any, { attrs, slots }: Omit) => {
+ const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
+ return h(component, { ...props, ...attrs, placeholder }, slots);
+ };
+};
+
+// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
+export type ComponentType =
+ | 'AutoComplete'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'DatePicker'
+ | 'DefaultButton'
+ | 'Divider'
+ | 'Input'
+ | 'InputNumber'
+ | 'InputPassword'
+ | 'Mentions'
+ | 'PrimaryButton'
+ | 'Radio'
+ | 'RadioGroup'
+ | 'RangePicker'
+ | 'Rate'
+ | 'Select'
+ | 'Space'
+ | 'Switch'
+ | 'Textarea'
+ | 'TimePicker'
+ | 'TreeSelect'
+ | 'Upload'
+ | BaseFormComponentType;
+
+async function initComponentAdapter() {
+ const components: Partial> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // Button: () =>
+ // import('xxx').then((res) => res.Button),
+
+ AutoComplete,
+ Checkbox,
+ CheckboxGroup,
+ DatePicker,
+ // 自定义默认按钮
+ DefaultButton: (props, { attrs, slots }) => {
+ return h(Button, { ...props, attrs, type: 'default' }, slots);
+ },
+ Divider,
+ 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 };
diff --git a/web/docs/src/_env/adapter/form.ts b/web/docs/src/_env/adapter/form.ts
new file mode 100644
index 0000000..d8b51c2
--- /dev/null
+++ b/web/docs/src/_env/adapter/form.ts
@@ -0,0 +1,47 @@
+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';
+
+import { initComponentAdapter } from './component';
+
+initComponentAdapter();
+setupVbenForm({
+ config: {
+ baseModelPropName: 'value',
+ // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
+ emptyStateValue: null,
+ 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;
+
+export { useVbenForm, z };
+
+export type VbenFormSchema = FormSchema;
+export type { VbenFormProps };
diff --git a/web/docs/src/_env/adapter/vxe-table.ts b/web/docs/src/_env/adapter/vxe-table.ts
new file mode 100644
index 0000000..bab7f3d
--- /dev/null
+++ b/web/docs/src/_env/adapter/vxe-table.ts
@@ -0,0 +1,70 @@
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { Button, Image } from 'ant-design-vue';
+
+import { useVbenForm } from './form';
+
+if (!import.meta.env.SSR) {
+ setupVbenVxeTable({
+ configVxeTable: (vxeUI) => {
+ vxeUI.setConfig({
+ grid: {
+ align: 'center',
+ border: false,
+ columnConfig: {
+ resizable: true,
+ },
+
+ formConfig: {
+ // 全局禁用vxe-table的表单配置,使用formOptions
+ enabled: false,
+ },
+ minHeight: 180,
+ proxyConfig: {
+ autoLoad: true,
+ response: {
+ result: 'items',
+ total: 'total',
+ list: 'items',
+ },
+ showActiveMsg: true,
+ showResponseMsg: false,
+ },
+ round: true,
+ showOverflow: true,
+ size: 'small',
+ },
+ });
+
+ // 表格配置项可以用 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 * from '@vben/plugins/vxe-table';
diff --git a/web/docs/src/_env/node/adapter/form.ts b/web/docs/src/_env/node/adapter/form.ts
new file mode 100644
index 0000000..a206c0d
--- /dev/null
+++ b/web/docs/src/_env/node/adapter/form.ts
@@ -0,0 +1,4 @@
+export const useVbenForm = () => {};
+export const z = {};
+export type VbenFormSchema = any;
+export type VbenFormProps = any;
diff --git a/web/docs/src/_env/node/adapter/vxe-table.ts b/web/docs/src/_env/node/adapter/vxe-table.ts
new file mode 100644
index 0000000..5ec409f
--- /dev/null
+++ b/web/docs/src/_env/node/adapter/vxe-table.ts
@@ -0,0 +1,3 @@
+export type * from '@vben/plugins/vxe-table';
+
+export const useVbenVxeGrid = () => {};
diff --git a/web/docs/src/commercial/community.md b/web/docs/src/commercial/community.md
new file mode 100644
index 0000000..60d9f25
--- /dev/null
+++ b/web/docs/src/commercial/community.md
@@ -0,0 +1,30 @@
+# 社区交流
+
+社区交流群主要是为了方便大家交流,提问,解答问题,分享经验等。偏自助方式,如果你有问题,可以通过以下方式加入社区交流群:
+
+- [QQ频道](https://pd.qq.com/s/16p8lvvob):推荐!!!主要提供问题解答,分享经验等。
+- QQ群:[大群](https://qm.qq.com/q/MEmHoCLbG0),[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E),[4群](https://qm.qq.com/q/sCzSlm3504),[5群](https://qm.qq.com/q/ya9XrtbS6s),主要的使用者交流群。
+- [Discord](https://discord.com/invite/VU62jTecad): 主要提供问题解答,分享经验等。
+
+::: tip
+
+免费QQ群人数上限200,将会不定期清理。推荐加入QQ频道进行交流
+
+:::
+
+## 微信群
+
+作者主要通过微信群提供帮助,如果你有问题,可以通过以下方式加入微信群。
+
+通过微信联系作者,注明加群来意:
+
+::: tip
+
+因为微信群人数有限制,加微信群要求:
+
+- 通过[赞助](../sponsor/personal.md)任意金额。
+- 发送赞助`截图`,备注`加入微信群`即可。
+
+:::
+
+
diff --git a/web/docs/src/commercial/customized.md b/web/docs/src/commercial/customized.md
new file mode 100644
index 0000000..1f0bcec
--- /dev/null
+++ b/web/docs/src/commercial/customized.md
@@ -0,0 +1,12 @@
+# 定制开发
+
+我们提供基于 Vben Admin 的技术支持服务及定制开发,基本需求我们都可以满足。
+
+详细需求可添加作者了解,并注明来意:
+
+- 通过邮箱联系开发者: [ann.vben@gmail.com](mailto:ann.vben@gmail.com)
+- 通过微信联系开发者:
+
+
+
+我们会在第一时间回复您,定制费用根据需求而定。
diff --git a/web/docs/src/commercial/technical-support.md b/web/docs/src/commercial/technical-support.md
new file mode 100644
index 0000000..ded9bf2
--- /dev/null
+++ b/web/docs/src/commercial/technical-support.md
@@ -0,0 +1,8 @@
+# 技术支持
+
+## 问题反馈
+
+在使用项目的过程中,如果遇到问题,你可以先详细阅读本文档,未找到解决方案时,可以通过以下方式获取技术支持:
+
+- 通过 [GitHub Issues](https://github.com/vbenjs/vue-vben-admin/issues)
+- 通过 [GitHub Discussions](https://github.com/vbenjs/vue-vben-admin/discussions)
diff --git a/web/docs/src/components/common-ui/vben-alert.md b/web/docs/src/components/common-ui/vben-alert.md
new file mode 100644
index 0000000..6541b66
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-alert.md
@@ -0,0 +1,166 @@
+---
+outline: deep
+---
+
+# Vben Alert 轻量提示框
+
+框架提供的一些用于轻量提示的弹窗,仅使用js代码即可快速动态创建提示而不需要在template写任何代码。
+
+::: info 应用场景
+
+Alert提供的功能与Modal类似,但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求,请使用VbenModal
+
+:::
+
+::: tip 注意
+
+Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下,不支持HMR(热更新),代码变更后需要关闭这些弹窗后重新打开。
+
+:::
+
+::: tip README
+
+下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
+
+:::
+
+## 基础用法
+
+使用 `alert` 创建只有一个确认按钮的提示框。
+
+
+
+使用 `confirm` 创建有确认和取消按钮的提示框。
+
+
+
+使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。
+
+
+
+## useAlertContext
+
+当弹窗的content、footer、icon使用自定义组件时,在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
+
+::: tip 注意
+
+`useAlertContext`只能用在setup或者函数式组件中。
+
+:::
+
+### Methods
+
+| 方法 | 描述 | 类型 | 版本要求 |
+| --------- | ------------------ | -------- | -------- |
+| doConfirm | 调用弹窗的确认操作 | ()=>void | >5.5.4 |
+| doCancel | 调用弹窗的取消操作 | ()=>void | >5.5.4 |
+
+## 类型说明
+
+```ts
+/** 预置的图标类型 */
+export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
+
+export type BeforeCloseScope = {
+ /** 是否为点击确认按钮触发的关闭 */
+ isConfirm: boolean;
+};
+
+/**
+ * alert 属性
+ */
+export type AlertProps = {
+ /** 关闭前的回调,如果返回false,则终止关闭 */
+ beforeClose?: (
+ scope: BeforeCloseScope,
+ ) => boolean | Promise | undefined;
+ /** 边框 */
+ bordered?: boolean;
+ /** 按钮对齐方式 */
+ buttonAlign?: 'center' | 'end' | 'start';
+ /** 取消按钮的标题 */
+ cancelText?: string;
+ /** 是否居中显示 */
+ centered?: boolean;
+ /** 确认按钮的标题 */
+ confirmText?: string;
+ /** 弹窗容器的额外样式 */
+ containerClass?: string;
+ /** 弹窗提示内容 */
+ content: Component | string;
+ /** 弹窗内容的额外样式 */
+ contentClass?: string;
+ /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
+ contentMasking?: boolean;
+ /** 弹窗底部内容(与按钮在同一个容器中) */
+ footer?: Component | string;
+ /** 弹窗的图标(在标题的前面) */
+ icon?: Component | IconType;
+ /**
+ * 弹窗遮罩模糊效果
+ */
+ overlayBlur?: number;
+ /** 是否显示取消按钮 */
+ showCancel?: boolean;
+ /** 弹窗标题 */
+ title?: string;
+};
+
+/** prompt 属性 */
+export type PromptProps = {
+ /** 关闭前的回调,如果返回false,则终止关闭 */
+ beforeClose?: (scope: {
+ isConfirm: boolean;
+ value: T | undefined;
+ }) => boolean | Promise | undefined;
+ /** 用于接受用户输入的组件 */
+ component?: Component;
+ /** 输入组件的属性 */
+ componentProps?: Recordable;
+ /** 输入组件的插槽 */
+ componentSlots?: Recordable;
+ /** 默认值 */
+ defaultValue?: T;
+ /** 输入组件的值属性名 */
+ modelPropName?: string;
+} & Omit;
+
+/**
+ * 函数签名
+ * alert和confirm的函数签名相同。
+ * confirm默认会显示取消按钮,而alert默认只有一个按钮
+ * */
+export function alert(options: AlertProps): Promise;
+export function alert(
+ message: string,
+ options?: Partial,
+): Promise;
+export function alert(
+ message: string,
+ title?: string,
+ options?: Partial,
+): Promise;
+
+/**
+ * 弹出输入框的函数签名。
+ * beforeClose的参数会传入用户当前输入的值
+ * component指定接受用户输入的组件,默认为Input
+ * componentProps 为输入组件设置的属性数据
+ * defaultValue 默认的值
+ * modelPropName 输入组件的值属性名称。默认为modelValue
+ */
+export async function prompt(
+ options: Omit & {
+ beforeClose?: (
+ scope: BeforeCloseScope & {
+ /** 输入组件的当前值 */
+ value: T;
+ },
+ ) => boolean | Promise | undefined;
+ component?: Component;
+ componentProps?: Recordable;
+ defaultValue?: T;
+ modelPropName?: string;
+ },
+): Promise;
+```
diff --git a/web/docs/src/components/common-ui/vben-api-component.md b/web/docs/src/components/common-ui/vben-api-component.md
new file mode 100644
index 0000000..2c84e56
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-api-component.md
@@ -0,0 +1,173 @@
+---
+outline: deep
+---
+
+# Vben ApiComponent Api组件包装器
+
+框架提供的API“包装器”,它一般不独立使用,主要用于包装其它组件,为目标组件提供自动获取远程数据的能力,但仍然保持了目标组件的原始用法。
+
+::: info 写在前面
+
+我们在各个应用的组件适配器中,使用ApiComponent包装了Select、TreeSelect组件,使得这些组件可以自动获取远程数据并生成选项。其它类似的组件(比如Cascader)如有需要也可以参考示例代码自行进行包装。
+
+:::
+
+## 基础用法
+
+通过 `component` 传入其它组件的定义,并配置相关的其它属性(主要是一些名称映射)。包装组件将通过`api`获取数据(`beforerFetch`、`afterFetch`将分别在`api`运行前、运行后被调用),使用`resultField`从中提取数组,使用`valueField`、`labelField`等来从数据中提取value和label(如果提供了`childrenField`,会将其作为树形结构递归处理每一级数据),之后将处理好的数据通过`optionsPropName`指定的属性传递给目标组件。
+
+::: details 包装级联选择器,点击下拉时开始加载远程数据
+
+```vue
+
+
+
+
+```
+
+:::
+
+## 并发和缓存
+
+有些场景下可能需要使用多个ApiComponent,它们使用了相同的远程数据源(例如用在可编辑的表格中)。如果直接将请求后端接口的函数传递给api属性,则每一个实例都会访问一次接口,这会造成资源浪费,是完全没有必要的。Tanstack Query提供了并发控制、缓存、重试等诸多特性,我们可以将接口请求函数用useQuery包装一下再传递给ApiComponent,这样的话无论页面有多少个使用相同数据源的ApiComponent实例,都只会发起一次远程请求。演示效果请参考 [Playground vue-query](https://www.vben.pro/#/demos/features/vue-query),具体代码请查看项目文件[concurrency-caching](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/views/demos/features/vue-query/concurrency-caching.vue)
+
+## API
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 | 版本要求 |
+| --- | --- | --- | --- | --- |
+| modelValue(v-model) | 当前值 | `any` | - | - |
+| component | 欲包装的组件(以下称为目标组件) | `Component` | - | - |
+| numberToString | 是否将value从数字转为string | `boolean` | `false` | - |
+| api | 获取数据的函数 | `(arg?: any) => Promise>` | - | - |
+| params | 传递给api的参数 | `Record` | - | - |
+| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | - |
+| labelField | label字段名 | `string` | `label` | - |
+| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | - |
+| valueField | value字段名 | `string` | `value` | - |
+| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | - |
+| modelPropName | 目标组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` | - |
+| immediate | 是否立即调用api | `boolean` | `true` | - |
+| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | - |
+| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction` | - | - |
+| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction` | - | - |
+| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
+| visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
+| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
+| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 |
+
+#### autoSelect 自动设置选项
+
+如果当前值为undefined,在选项数据成功加载之后,自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
+
+- `"first"`:自动选择第一个选项
+- `"last"`:自动选择最后一个选项
+- `"one"`:有且仅有一个选项时,自动选择它
+- `自定义函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项
+- `false`:不自动选择选项
+
+### Methods
+
+| 方法 | 描述 | 类型 | 版本要求 |
+| --- | --- | --- | --- |
+| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
+| updateParam | 设置接口请求参数(将与params属性合并) | (newParams: Record)=>void | >5.5.4 |
+| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 |
+| getValue | 获取当前值 | ()=>any | >5.5.4 |
diff --git a/web/docs/src/components/common-ui/vben-count-to-animator.md b/web/docs/src/components/common-ui/vben-count-to-animator.md
new file mode 100644
index 0000000..5f3ec18
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-count-to-animator.md
@@ -0,0 +1,59 @@
+---
+outline: deep
+---
+
+# Vben CountToAnimator 数字动画
+
+框架提供的数字动画组件,支持数字动画效果。
+
+> 如果文档内没有参数说明,可以尝试在在线示例内寻找
+
+::: info 写在前面
+
+如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
+
+:::
+
+## 基础用法
+
+通过 `start-val` 和 `end-val`设置数字动画的开始值和结束值, 持续时间`3000`ms。
+
+
+
+## 自定义前缀及分隔符
+
+通过 `prefix` 和 `separator` 设置数字动画的前缀和分隔符。
+
+
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| ---------- | -------------- | --------- | -------- |
+| startVal | 起始值 | `number` | `0` |
+| endVal | 结束值 | `number` | `2021` |
+| duration | 动画持续时间 | `number` | `1500` |
+| autoplay | 自动执行 | `boolean` | `true` |
+| prefix | 前缀 | `string` | - |
+| suffix | 后缀 | `string` | - |
+| separator | 分隔符 | `string` | `,` |
+| color | 字体颜色 | `string` | - |
+| useEasing | 是否开启动画 | `boolean` | `true` |
+| transition | 动画效果 | `string` | `linear` |
+| decimals | 保留小数点位数 | `number` | `0` |
+
+### Events
+
+| 事件名 | 描述 | 类型 |
+| -------------- | -------------- | -------------- |
+| started | 动画已开始 | `()=>void` |
+| finished | 动画已结束 | `()=>void` |
+| ~~onStarted~~ | ~~动画已开始~~ | ~~`()=>void`~~ |
+| ~~onFinished~~ | ~~动画已结束~~ | ~~`()=>void`~~ |
+
+### Methods
+
+| 方法名 | 描述 | 类型 |
+| ------ | ------------ | ---------- |
+| start | 开始执行动画 | `()=>void` |
+| reset | 重置 | `()=>void` |
diff --git a/web/docs/src/components/common-ui/vben-drawer.md b/web/docs/src/components/common-ui/vben-drawer.md
new file mode 100644
index 0000000..b66bd3a
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-drawer.md
@@ -0,0 +1,156 @@
+---
+outline: deep
+---
+
+# Vben Drawer 抽屉
+
+框架提供的抽屉组件,支持`自动高度`、`loading`等功能。
+
+> 如果文档内没有参数说明,可以尝试在在线示例内寻找
+
+::: info 写在前面
+
+如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
+
+:::
+
+::: tip README
+
+下方示例代码中的,存在一些国际化、主题色未适配问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
+
+:::
+
+## 基础用法
+
+使用 `useVbenDrawer` 创建最基础的模态框。
+
+
+
+## 组件抽离
+
+Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 drawer 内的内容抽离出来,也方便复用。通过 `connectedComponent` 参数,可以将内外组件进行连接,而不用其他任何操作。
+
+
+
+## 自动计算高度
+
+弹窗会自动计算内容高度,超过一定高度会出现滚动条,同时结合 `loading` 效果以及使用 `prepend-footer` 插槽。
+
+
+
+## 使用 Api
+
+通过 `drawerApi` 可以调用 drawer 的方法以及使用 `setState` 更新 drawer 的状态。
+
+
+
+## 数据共享
+
+如果你使用了 `connectedComponent` 参数,那么内外组件会共享数据,比如一些表单回填等操作。可以用 `drawerApi` 来获取数据和设置数据,配合 `onOpenChange`,可以满足大部分的需求。
+
+
+
+::: info 注意
+
+- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
+- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
+- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
+- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
+
+:::
+
+## API
+
+```ts
+// Drawer 为弹窗组件
+// drawerApi 为弹窗的方法
+const [Drawer, drawerApi] = useVbenDrawer({
+ // 属性
+ // 事件
+});
+```
+
+### Props
+
+所有属性都可以传入 `useVbenDrawer` 的第一个参数中。
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
+| connectedComponent | 连接另一个Modal组件 | `Component` | - |
+| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
+| title | 标题 | `string\|slot` | - |
+| titleTooltip | 标题提示信息 | `string\|slot` | - |
+| description | 描述信息 | `string\|slot` | - |
+| isOpen | 弹窗打开状态 | `boolean` | `false` |
+| loading | 弹窗加载状态 | `boolean` | `false` |
+| closable | 显示关闭按钮 | `boolean` | `true` |
+| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` |
+| modal | 显示遮罩 | `boolean` | `true` |
+| header | 显示header | `boolean` | `true` |
+| footer | 显示footer | `boolean\|slot` | `true` |
+| confirmLoading | 确认按钮loading状态 | `boolean` | `false` |
+| closeOnClickModal | 点击遮罩关闭弹窗 | `boolean` | `true` |
+| closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
+| confirmText | 确认按钮文本 | `string\|slot` | `确认` |
+| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
+| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
+| showCancelButton | 显示取消按钮 | `boolean` | `true` |
+| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
+| class | modal的class,宽度通过这个配置 | `string` | - |
+| contentClass | modal内容区域的class | `string` | - |
+| footerClass | modal底部区域的class | `string` | - |
+| headerClass | modal顶部区域的class | `string` | - |
+| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
+| overlayBlur | 遮罩模糊度 | `number` | - |
+
+::: info appendToMain
+
+`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
+
+:::
+
+### Event
+
+以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
+
+| 事件名 | 描述 | 类型 | 版本限制 |
+| --- | --- | --- | --- |
+| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- |
+| onCancel | 点击取消按钮触发 | `()=>void` | --- |
+| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
+| onConfirm | 点击确认按钮触发 | `()=>void` | --- |
+| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
+| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |
+
+### Slots
+
+除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
+
+| 插槽名 | 描述 |
+| -------------- | -------------------------------------------------- |
+| default | 默认插槽 - 弹窗内容 |
+| prepend-footer | 取消按钮左侧 |
+| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
+| append-footer | 确认按钮右侧 |
+| close-icon | 关闭按钮图标 |
+| extra | 额外内容(标题右侧) |
+
+### drawerApi
+
+| 方法 | 描述 | 类型 | 版本限制 |
+| --- | --- | --- | --- |
+| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial)\| Partial)=>drawerApi` |
+| open | 打开弹窗 | `()=>void` | --- |
+| close | 关闭弹窗 | `()=>void` | --- |
+| setData | 设置共享数据 | `(data:T)=>drawerApi` | --- |
+| getData | 获取共享数据 | `()=>T` | --- |
+| useStore | 获取可响应式状态 | - | --- |
+| lock | 将抽屉标记为提交中,锁定当前状态 | `(isLock:boolean)=>drawerApi` | >5.5.3 |
+| unlock | lock方法的反操作,解除抽屉的锁定状态,也是lock(false)的别名 | `()=>drawerApi` | >5.5.3 |
+
+::: info lock
+
+`lock`方法用于锁定抽屉的状态,一般用于提交数据的过程中防止用户重复提交或者抽屉被意外关闭、表单数据被改变等等。当处于锁定状态时,抽屉的确认按钮会变为loading状态,同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭抽屉、开启抽屉的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的抽屉时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
+
+:::
diff --git a/web/docs/src/components/common-ui/vben-ellipsis-text.md b/web/docs/src/components/common-ui/vben-ellipsis-text.md
new file mode 100644
index 0000000..ce6c033
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-ellipsis-text.md
@@ -0,0 +1,64 @@
+---
+outline: deep
+---
+
+# Vben EllipsisText 省略文本
+
+框架提供的文本展示组件,可配置超长省略、tooltip提示、展开收起等功能。
+
+> 如果文档内没有参数说明,可以尝试在在线示例内寻找
+
+## 基础用法
+
+通过默认插槽设置文本内容,`maxWidth`属性设置最大宽度。
+
+
+
+## 可折叠的文本块
+
+通过`line`设置折叠后的行数,`expand`属性设置是否支持展开收起。
+
+
+
+## 自定义提示浮层
+
+通过名为`tooltip`的插槽定制提示信息。
+
+
+
+## 自动显示 tooltip
+
+通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。
+
+
+
+## API
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| expand | 支持点击展开或收起 | `boolean` | `false` |
+| line | 文本最大行数 | `number` | `1` |
+| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
+| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
+| tooltip | 启用文本提示 | `boolean` | `true` |
+| tooltipWhenEllipsis | 内容超出,自动启用文本提示 | `boolean` | `false` |
+| ellipsisThreshold | 设置 tooltipWhenEllipsis 后才生效,文本截断检测的像素差异阈值,越大则判断越严格,如果碰见异常情况可以自己设置阈值 | `number` | `3` |
+| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
+| tooltipColor | 提示文本的颜色 | `string` | - |
+| tooltipFontSize | 提示文本的大小 | `string` | - |
+| tooltipMaxWidth | 提示浮层的最大宽度。如不设置则保持与文本宽度一致 | `number` | - |
+| tooltipOverlayStyle | 提示框内容区域样式 | `CSSProperties` | `{ textAlign: 'justify' }` |
+
+### Events
+
+| 事件名 | 描述 | 类型 |
+| ------------ | ------------ | -------------------------- |
+| expandChange | 展开状态改变 | `(isExpand:boolean)=>void` |
+
+### Slots
+
+| 插槽名 | 描述 |
+| ------- | -------------------------------- |
+| tooltip | 启用文本提示时,用来定制提示内容 |
diff --git a/web/docs/src/components/common-ui/vben-form.md b/web/docs/src/components/common-ui/vben-form.md
new file mode 100644
index 0000000..7abb305
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-form.md
@@ -0,0 +1,560 @@
+---
+outline: deep
+---
+
+# Vben Form 表单
+
+框架提供的表单组件,可适配 `Element Plus`、`Ant Design Vue`、`Naive UI` 等框架。
+
+> 如果文档内没有参数说明,可以尝试在在线示例内寻找
+
+::: info 写在前面
+
+如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
+
+:::
+
+## 适配器
+
+表单底层使用 [vee-validate](https://vee-validate.logaretm.com/v4/) 进行表单验证,所以你可以使用 `vee-validate` 的所有功能。对于不同的 UI 框架,我们提供了适配器,以便更好的适配不同的 UI 框架。
+
+### 适配器说明
+
+每个应用都有不同的 UI 框架,所以在应用的 `src/adapter/form` 和 `src/adapter/component` 内部,你可以根据自己的需求,进行组件适配。下面是 `Ant Design Vue` 的适配器示例代码,可根据注释查看说明:
+
+::: details ant design vue 表单适配器
+
+```ts
+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';
+
+setupVbenForm({
+ 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;
+
+export { useVbenForm, z };
+export type VbenFormSchema = FormSchema;
+export type { VbenFormProps };
+```
+
+:::
+
+::: details ant design vue 组件适配器
+
+```ts
+/**
+ * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
+ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
+ */
+
+import type { BaseFormComponentType } from '@vben/common-ui';
+
+import type { Component, SetupContext } from 'vue';
+import { h } from 'vue';
+
+import { globalShareState, IconPicker } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import {
+ AutoComplete,
+ Button,
+ Checkbox,
+ CheckboxGroup,
+ DatePicker,
+ Divider,
+ Input,
+ InputNumber,
+ InputPassword,
+ Mentions,
+ notification,
+ Radio,
+ RadioGroup,
+ RangePicker,
+ Rate,
+ Select,
+ Space,
+ Switch,
+ Textarea,
+ TimePicker,
+ TreeSelect,
+ Upload,
+} from 'ant-design-vue';
+
+const withDefaultPlaceholder = (
+ component: T,
+ type: 'input' | 'select',
+) => {
+ return (props: any, { attrs, slots }: Omit) => {
+ const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
+ return h(component, { ...props, ...attrs, placeholder }, slots);
+ };
+};
+
+// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
+export type ComponentType =
+ | 'AutoComplete'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'DatePicker'
+ | 'DefaultButton'
+ | 'Divider'
+ | 'Input'
+ | 'InputNumber'
+ | 'InputPassword'
+ | 'Mentions'
+ | 'PrimaryButton'
+ | 'Radio'
+ | 'RadioGroup'
+ | 'RangePicker'
+ | 'Rate'
+ | 'Select'
+ | 'Space'
+ | 'Switch'
+ | 'Textarea'
+ | 'TimePicker'
+ | 'TreeSelect'
+ | 'Upload'
+ | 'IconPicker';
+ | BaseFormComponentType;
+
+async function initComponentAdapter() {
+ const components: Partial> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // Button: () =>
+ // import('xxx').then((res) => res.Button),
+
+ AutoComplete,
+ Checkbox,
+ CheckboxGroup,
+ DatePicker,
+ // 自定义默认按钮
+ DefaultButton: (props, { attrs, slots }) => {
+ return h(Button, { ...props, attrs, type: 'default' }, slots);
+ },
+ Divider,
+ IconPicker,
+ 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 };
+```
+
+:::
+
+## 基础用法
+
+::: tip README
+
+下方示例代码中的,存在一些国际化、主题色未适配问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
+
+:::
+
+使用 `useVbenForm` 创建最基础的表单。
+
+
+
+## 查询表单
+
+查询表单是一种特殊的表单,用于查询数据。查询表单不会触发表单验证,只会触发查询事件。
+
+
+
+## 表单校验
+
+表单校验是一个非常重要的功能,可以通过 `rules` 属性进行校验。
+
+
+
+## 表单联动
+
+表单联动是一个非常常见的功能,可以通过 `dependencies` 属性进行联动。
+
+_注意_ 需要指定 `dependencies` 的 `triggerFields` 属性,设置由谁的改动来触发,以便表单组件能够正确的联动。
+
+
+
+## 自定义组件
+
+如果你的业务组件库没有提供某个组件,你可以自行封装一个组件,然后加到表单内部。
+
+
+
+## 操作
+
+一些常见的表单操作。
+
+
+
+## API
+
+`useVbenForm` 返回一个数组,第一个元素是表单组件,第二个元素是表单的方法。
+
+```vue
+
+
+
+
+
+```
+
+### FormApi
+
+useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。
+
+| 方法名 | 描述 | 类型 | 版本号 |
+| --- | --- | --- | --- |
+| submitForm | 提交表单 | `(e:Event)=>Promise>` | - |
+| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise>` | - |
+| resetForm | 重置表单 | `()=>Promise` | - |
+| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record, filterFields?: boolean, shouldValidate?: boolean) => Promise` | - |
+| getValues | 获取表单值 | `(fields:Record,shouldValidate: boolean = false)=>Promise` | - |
+| validate | 表单校验 | `()=>Promise` | - |
+| validateField | 校验指定字段 | `(fieldName: string)=>Promise>` | - |
+| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise` | - |
+| resetValidate | 重置表单校验 | `()=>Promise` | - |
+| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - |
+| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise` | - |
+| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial)\| Partial)=>Promise` | - |
+| getState | 获取组件状态(props) | `()=>Promise` | - |
+| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - |
+| getFieldComponentRef | 获取指定字段的组件实例 | `(fieldName: string)=>T` | >5.5.3 |
+| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
+
+## Props
+
+所有属性都可以传入 `useVbenForm` 的第一个参数中。
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` |
+| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
+| wrapperClass | 表单的布局,基于tailwindcss | `any` | - |
+| actionWrapperClass | 表单操作区域class | `any` | - |
+| handleReset | 表单重置回调 | `(values: Record,) => Promise \| void` | - |
+| handleSubmit | 表单提交回调 | `(values: Record,) => Promise \| void` | - |
+| handleValuesChange | 表单值变化回调 | `(values: Record, fieldsChanged: string[]) => void` | - |
+| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
+| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
+| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
+| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
+| collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
+| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
+| collapsedRows | 折叠时保持的行数 | `number` | `1` |
+| fieldMappingTime | 用于将表单内的数组值映射成 2 个字段 | `[string, [string, string],Nullable\|[string,string]\|((any,string)=>any)?][]` | - |
+| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
+| schema | 表单项的每一项配置 | `FormSchema[]` | - |
+| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
+| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
+| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
+
+::: tip handleValuesChange
+
+`handleValuesChange` 回调函数的第一个参数`values`装载了表单改变后的当前值对象,第二个参数`fieldsChanged`是一个数组,包含了所有被改变的字段名。注意:第二个参数仅在v5.5.4(不含)以上版本可用,并且传递的是已在schema中定义的字段名。如果你使用了字段映射并且需要检查是哪些字段发生了变化的话,请注意该参数并不会包含映射后的字段名。
+
+:::
+
+::: tip fieldMappingTime
+
+此属性用于将表单内的数组值映射成 2 个字段,它应当传入一个数组,数组的每一项是一个映射规则,规则的第一个成员是一个字符串,表示需要映射的字段名,第二个成员是一个数组,表示映射后的字段名,第三个成员是一个可选的格式掩码,用于格式化日期时间字段;也可以提供一个格式化函数(参数分别为当前值和当前字段名,返回格式化后的值)。如果明确地将格式掩码设为null,则原值映射而不进行格式化(适用于非日期时间字段)。例如:`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]`,`timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime`和`endTime`字段上。每一项的第三个参数是一个可选的格式掩码,
+
+:::
+
+### TS 类型说明
+
+::: details ActionButtonOptions
+
+```ts
+export interface ActionButtonOptions {
+ /** 样式 */
+ class?: ClassType;
+ /** 是否禁用 */
+ disabled?: boolean;
+ /** 是否加载中 */
+ loading?: boolean;
+ /** 按钮大小 */
+ size?: ButtonVariantSize;
+ /** 按钮类型 */
+ variant?: ButtonVariants;
+ /** 是否显示 */
+ show?: boolean;
+ /** 按钮文本 */
+ content?: string;
+ /** 任意属性 */
+ [key: string]: any;
+}
+```
+
+:::
+
+::: details FormCommonConfig
+
+```ts
+export interface FormCommonConfig {
+ /**
+ * 所有表单项的props
+ */
+ componentProps?: ComponentProps;
+ /**
+ * 所有表单项的控件样式
+ */
+ controlClass?: string;
+ /**
+ * 在表单项的Label后显示一个冒号
+ */
+ colon?: boolean;
+ /**
+ * 所有表单项的禁用状态
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * 所有表单项的控件样式
+ * @default {}
+ */
+ formFieldProps?: Partial;
+ /**
+ * 所有表单项的栅格布局
+ * @default ""
+ */
+ formItemClass?: string;
+ /**
+ * 隐藏所有表单项label
+ * @default false
+ */
+ hideLabel?: boolean;
+ /**
+ * 是否隐藏必填标记
+ * @default false
+ */
+ hideRequiredMark?: boolean;
+ /**
+ * 所有表单项的label样式
+ * @default ""
+ */
+ labelClass?: string;
+ /**
+ * 所有表单项的label宽度
+ */
+ labelWidth?: number;
+ /**
+ * 所有表单项的model属性名。使用自定义组件时可通过此配置指定组件的model属性名。已经在modelPropNameMap中注册的组件不受此配置影响
+ * @default "modelValue"
+ */
+ modelPropName?: string;
+ /**
+ * 所有表单项的wrapper样式
+ */
+ wrapperClass?: string;
+}
+```
+
+:::
+
+::: details FormSchema
+
+```ts
+export interface FormSchema<
+ T extends BaseFormComponentType = BaseFormComponentType,
+> extends FormCommonConfig {
+ /** 组件 */
+ component: Component | T;
+ /** 组件参数 */
+ componentProps?: ComponentProps;
+ /** 默认值 */
+ defaultValue?: any;
+ /** 依赖 */
+ dependencies?: FormItemDependencies;
+ /** 描述 */
+ description?: string;
+ /** 字段名,也作为自定义插槽的名称 */
+ fieldName: string;
+ /** 帮助信息 */
+ help?: CustomRenderType;
+ /** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */
+ label?: CustomRenderType;
+ /** 自定义组件内部渲染 */
+ renderComponentContent?: RenderComponentContentType;
+ /** 字段规则 */
+ rules?: FormSchemaRuleType;
+ /** 后缀 */
+ suffix?: CustomRenderType;
+}
+```
+
+:::
+
+### 表单联动
+
+表单联动需要通过 schema 内的 `dependencies` 属性进行联动,允许您添加字段之间的依赖项,以根据其他字段的值控制字段。
+
+```ts
+dependencies: {
+ // 触发字段。只有这些字段值变动时,联动才会触发
+ triggerFields: ['name'],
+ // 动态判断当前字段是否需要显示,不显示则直接销毁
+ if(values,formApi){},
+ // 动态判断当前字段是否需要显示,不显示用css隐藏
+ show(values,formApi){},
+ // 动态判断当前字段是否需要禁用
+ disabled(values,formApi){},
+ // 字段变更时,都会触发该函数
+ trigger(values,formApi){},
+ // 动态rules
+ rules(values,formApi){},
+ // 动态必填
+ required(values,formApi){},
+ // 动态组件参数
+ componentProps(values,formApi){},
+}
+```
+
+### 表单校验
+
+表单校验需要通过 schema 内的 `rules` 属性进行配置。
+
+rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。
+
+#### 预定义的校验规则
+
+```ts
+// 表示字段必填,默认会根据适配器的required进行国际化
+{
+ rules: 'required';
+}
+
+// 表示字段必填,默认会根据适配器的required进行国际化,用于下拉选择之类
+{
+ rules: 'selectRequired';
+}
+```
+
+#### zod
+
+rules也支持 zod 的 schema,可以进行更复杂的校验,zod 的使用请查看 [zod文档](https://zod.dev/)。
+
+```ts
+import { z } from '#/adapter/form';
+
+// 基础类型
+{
+ rules: z.string().min(1, { message: '请输入字符串' });
+}
+
+// 可选(可以是undefined),并且携带默认值。注意zod的optional不包括空字符串''
+{
+ rules: z.string().default('默认值').optional();
+}
+
+// 可以是空字符串、undefined或者一个邮箱地址(两种不同的用法)
+{
+ rules: z.union([z.string().email().optional(), z.literal('')]);
+}
+
+{
+ rules: z.string().email().or(z.literal('')).optional();
+}
+
+// 复杂校验
+{
+ z.string()
+ .min(1, { message: '请输入' })
+ .refine((value) => value === '123', {
+ message: '值必须为123',
+ });
+}
+```
+
+## Slots
+
+可以使用以下插槽在表单中插入自定义的内容
+
+| 插槽名 | 描述 |
+| ------------- | ------------------ |
+| reset-before | 重置按钮之前的位置 |
+| submit-before | 提交按钮之前的位置 |
+| expand-before | 展开按钮之前的位置 |
+| expand-after | 展开按钮之后的位置 |
+
+::: tip 字段插槽
+
+除了以上内置插槽之外,`schema`属性中每个字段的`fieldName`都可以作为插槽名称,这些字段插槽的优先级高于`component`定义的组件。也就是说,当提供了与`fieldName`同名的插槽时,这些插槽的内容将会作为这些字段的组件,此时`component`的值将会被忽略。
+
+:::
diff --git a/web/docs/src/components/common-ui/vben-modal.md b/web/docs/src/components/common-ui/vben-modal.md
new file mode 100644
index 0000000..3c8200f
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-modal.md
@@ -0,0 +1,164 @@
+---
+outline: deep
+---
+
+# Vben Modal 模态框
+
+框架提供的模态框组件,支持`拖拽`、`全屏`、`自动高度`、`loading`等功能。
+
+> 如果文档内没有参数说明,可以尝试在在线示例内寻找
+
+::: info 写在前面
+
+如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
+
+:::
+
+::: tip README
+
+下方示例代码中的,存在一些国际化、主题色未适配问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
+
+:::
+
+## 基础用法
+
+使用 `useVbenModal` 创建最基础的模态框。
+
+
+
+## 组件抽离
+
+Modal 内的内容一般业务中,会比较复杂,所以我们可以将 modal 内的内容抽离出来,也方便复用。通过 `connectedComponent` 参数,可以将内外组件进行连接,而不用其他任何操作。
+
+
+
+## 开启拖拽
+
+通过 `draggable` 参数,可开启拖拽功能。
+
+
+
+## 自动计算高度
+
+弹窗会自动计算内容高度,超过一定高度会出现滚动条,同时结合 `loading` 效果以及使用 `prepend-footer` 插槽。
+
+
+
+## 使用 Api
+
+通过 `modalApi` 可以调用 modal 的方法以及使用 `setState` 更新 modal 的状态。
+
+
+
+## 数据共享
+
+如果你使用了 `connectedComponent` 参数,那么内外组件会共享数据,比如一些表单回填等操作。可以用 `modalApi` 来获取数据和设置数据,配合 `onOpenChange`,可以满足大部分的需求。
+
+
+
+::: info 注意
+
+- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
+- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。另外,如果设置了`destroyOnClose`,内部Modal及其子组件会在被关闭后完全销毁 。
+- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
+
+:::
+
+## API
+
+```ts
+// Modal 为弹窗组件
+// modalApi 为弹窗的方法
+const [Modal, modalApi] = useVbenModal({
+ // 属性
+ // 事件
+});
+```
+
+### Props
+
+所有属性都可以传入 `useVbenModal` 的第一个参数中。
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
+| connectedComponent | 连接另一个Modal组件 | `Component` | - |
+| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
+| title | 标题 | `string\|slot` | - |
+| titleTooltip | 标题提示信息 | `string\|slot` | - |
+| description | 描述信息 | `string\|slot` | - |
+| isOpen | 弹窗打开状态 | `boolean` | `false` |
+| loading | 弹窗加载状态 | `boolean` | `false` |
+| fullscreen | 全屏显示 | `boolean` | `false` |
+| fullscreenButton | 显示全屏按钮 | `boolean` | `true` |
+| draggable | 可拖拽 | `boolean` | `false` |
+| closable | 显示关闭按钮 | `boolean` | `true` |
+| centered | 居中显示 | `boolean` | `false` |
+| modal | 显示遮罩 | `boolean` | `true` |
+| header | 显示header | `boolean` | `true` |
+| footer | 显示footer | `boolean\|slot` | `true` |
+| confirmDisabled | 禁用确认按钮 | `boolean` | `false` |
+| confirmLoading | 确认按钮loading状态 | `boolean` | `false` |
+| closeOnClickModal | 点击遮罩关闭弹窗 | `boolean` | `true` |
+| closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
+| confirmText | 确认按钮文本 | `string\|slot` | `确认` |
+| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
+| showCancelButton | 显示取消按钮 | `boolean` | `true` |
+| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
+| class | modal的class,宽度通过这个配置 | `string` | - |
+| contentClass | modal内容区域的class | `string` | - |
+| footerClass | modal底部区域的class | `string` | - |
+| headerClass | modal顶部区域的class | `string` | - |
+| bordered | 是否显示border | `boolean` | `false` |
+| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
+| overlayBlur | 遮罩模糊度 | `number` | - |
+| submitting | 标记为提交中,锁定弹窗当前状态 | `boolean` | `false` |
+
+::: info appendToMain
+
+`appendToMain`可以指定将弹窗挂载到内容区域,打开这种弹窗时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,弹窗会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。
+
+:::
+
+### Event
+
+以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。
+
+| 事件名 | 描述 | 类型 | 版本号 |
+| --- | --- | --- | --- |
+| onBeforeClose | 关闭前触发,返回 `false`或者被`reject`则禁止关闭 | `()=>Promise\|boolean` | >5.5.2支持Promise |
+| onCancel | 点击取消按钮触发 | `()=>void` | |
+| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.4.3 |
+| onConfirm | 点击确认按钮触发 | `()=>void` | |
+| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | |
+| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.4.3 |
+
+### Slots
+
+除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
+
+| 插槽名 | 描述 |
+| -------------- | -------------------------------------------------- |
+| default | 默认插槽 - 弹窗内容 |
+| prepend-footer | 取消按钮左侧 |
+| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
+| append-footer | 确认按钮右侧 |
+
+### modalApi
+
+| 方法 | 描述 | 类型 | 版本 |
+| --- | --- | --- | --- |
+| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial)\| Partial)=>modalApi` | - |
+| open | 打开弹窗 | `()=>void` | - |
+| close | 关闭弹窗 | `()=>void` | - |
+| setData | 设置共享数据 | `(data:T)=>modalApi` | - |
+| getData | 获取共享数据 | `()=>T` | - |
+| useStore | 获取可响应式状态 | - | - |
+| lock | 将弹窗标记为提交中,锁定当前状态 | `(isLock:boolean)=>modalApi` | >5.5.2 |
+| unlock | lock方法的反操作,解除弹窗的锁定状态,也是lock(false)的别名 | `()=>modalApi` | >5.5.3 |
+
+::: info lock
+
+`lock`方法用于锁定当前弹窗的状态,一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时,弹窗的确认按钮会变为loading状态,同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
+
+:::
diff --git a/web/docs/src/components/common-ui/vben-vxe-table.md b/web/docs/src/components/common-ui/vben-vxe-table.md
new file mode 100644
index 0000000..344ecae
--- /dev/null
+++ b/web/docs/src/components/common-ui/vben-vxe-table.md
@@ -0,0 +1,276 @@
+---
+outline: deep
+---
+
+# Vben Vxe Table 表格
+
+框架提供的Table 列表组件基于 [vxe-table](https://vxetable.cn/v4/#/grid/api?apiKey=grid),结合`Vben Form 表单`进行了二次封装。
+
+其中,表头的 **表单搜索** 部分采用了`Vben Form表单`,表格主体部分使用了`vxe-grid`组件,支持表格的分页、排序、筛选等功能。
+
+> 如果文档内没有参数说明,可以尝试在在线示例或者在 [vxe-grid 官方API 文档](https://vxetable.cn/v4/#/grid/api?apiKey=grid) 内寻找
+
+::: info 写在前面
+
+如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
+
+:::
+
+## 适配器
+
+表格底层使用 [vxe-table](https://vxetable.cn/#/start/install) 进行实现,所以你可以使用 `vxe-table` 的所有功能。对于不同的 UI 框架,我们提供了适配器,以便更好的适配不同的 UI 框架。
+
+### 适配器说明
+
+每个应用都可以自己配置`vxe-table`的适配器,你可以根据自己的需求。下面是一个简单的配置示例:
+
+::: details vxe-table 表格适配器
+
+```ts
+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',
+ },
+ });
+
+ // 表格配置项可以用 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 * from '@vben/plugins/vxe-table';
+```
+
+:::
+
+## 基础表格
+
+使用 `useVbenVxeGrid` 创建最基础的表格。
+
+
+
+## 远程加载
+
+通过指定 `proxyConfig.ajax` 的 `query` 方法,可以实现远程加载数据。
+
+
+
+## 树形表格
+
+树形表格的数据源为扁平结构,可以指定`treeConfig`配置项,实现树形表格。
+
+```typescript
+treeConfig: {
+ transform: true, // 指定表格为树形表格
+ parentField: 'parentId', // 父节点字段名
+ rowField: 'id', // 行数据字段名
+},
+```
+
+
+
+## 固定表头/列
+
+列固定可选参数: `'left' | 'right' | '' | null`
+
+
+
+## 自定义单元格
+
+自定义单元格有两种实现方式
+
+- 通过 `slots` 插槽
+- 通过 `customCell` 自定义单元格,但是要先添加渲染器
+
+```typescript
+// 表格配置项可以用 cellRender: { name: 'CellImage' },
+vxeUI.renderer.add('CellImage', {
+ renderDefault(_renderOpts, params) {
+ const { column, row } = params;
+ return h(Image, { src: row[column.field] } as any); // 注意此处的Image 组件,来源于Antd,需要自行引入,否则会使用js的Image类
+ },
+});
+
+// 表格配置项可以用 cellRender: { name: 'CellLink' },
+vxeUI.renderer.add('CellLink', {
+ renderDefault(renderOpts) {
+ const { props } = renderOpts;
+ return h(
+ Button,
+ { size: 'small', type: 'link' },
+ { default: () => props?.text },
+ );
+ },
+});
+```
+
+
+
+## 搜索表单
+
+**表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。
+
+当启用了表单搜索时,可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。表格的所有以`form-`开头的命名插槽都会被传递给搜索表单。
+
+### 定制分隔条
+
+当你启用表单搜索时,在表单和表格之间会显示一个分隔条。这个分隔条使用了默认的组件背景色,并且横向贯穿整个Vben Vxe Table在视觉上融入了页面的默认背景中。如果你在Vben Vxe Table的外层包裹了一个不同背景色的容器(如将其放在一个Card内),默认的表单和表格之间的分隔条可能就显得格格不入了,下面的代码演示了如何定制这个分隔条。
+
+```ts
+const [Grid] = useVbenVxeGrid({
+ formOptions: {},
+ gridOptions: {},
+ // 完全移除分隔条
+ separator: false,
+ // 你也可以使用下面的代码来移除分隔条
+ // separator: { show: false },
+ // 或者使用下面的代码来改变分隔条的颜色
+ // separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
+});
+```
+
+
+
+## 单元格编辑
+
+通过指定`editConfig.mode`为`cell`,可以实现单元格编辑。
+
+
+
+## 行编辑
+
+通过指定`editConfig.mode`为`row`,可以实现行编辑。
+
+
+
+## 虚拟滚动
+
+通过 scroll-y.enabled 与 scroll-y.gt 组合开启,其中 enabled 为总开关,gt 是指当总行数大于指定行数时自动开启。
+
+> 参考 [vxe-table 官方文档 - 虚拟滚动](https://vxetable.cn/v4/#/component/grid/scroll/vertical)。
+
+
+
+## API
+
+`useVbenVxeGrid` 返回一个数组,第一个元素是表格组件,第二个元素是表格的方法。
+
+```vue
+
+
+
+
+
+```
+
+### GridApi
+
+useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表单的方法。
+
+| 方法名 | 描述 | 类型 | 说明 |
+| --- | --- | --- | --- |
+| setLoading | 设置loading状态 | `(loading)=>void` | - |
+| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partialvoid` | - |
+| reload | 重载表格,会进行初始化 | `(params:any)=>void` | - |
+| query | 重载表格,会保留当前分页 | `(params:any)=>void` | - |
+| grid | vxe-table grid实例 | `VxeGridInstance` | - |
+| formApi | vbenForm api实例 | `FormApi` | - |
+| toggleSearchForm | 设置搜索表单显示状态 | `(show?: boolean)=>boolean` | 当省略参数时,则将表单在显示和隐藏两种状态之间切换 |
+
+## Props
+
+所有属性都可以传入 `useVbenVxeGrid` 的第一个参数中。
+
+| 属性名 | 描述 | 类型 | 版本要求 |
+| --- | --- | --- | --- |
+| tableTitle | 表格标题 | `string` | - |
+| tableTitleHelp | 表格标题帮助信息 | `string` | - |
+| gridClass | grid组件的class | `string` | - |
+| gridOptions | grid组件的参数 | `VxeTableGridProps` | - |
+| gridEvents | grid组件的触发的事件 | `VxeGridListeners` | - |
+| formOptions | 表单参数 | `VbenFormProps` | - |
+| showSearchForm | 是否显示搜索表单 | `boolean` | - |
+| separator | 搜索表单与表格主体之间的分隔条 | `boolean\|SeparatorOptions` | >5.5.4 |
+
+## Slots
+
+大部分插槽的说明请参考 [vxe-table 官方文档](https://vxetable.cn/v4/#/grid/api),但工具栏部分由于做了一些定制封装,需使用以下插槽定制表格的工具栏:
+
+| 插槽名 | 描述 |
+| --------------- | -------------------------------------------- |
+| toolbar-actions | 工具栏左侧部分(表格标题附近) |
+| toolbar-tools | 工具栏右侧部分(vxeTable原生工具按钮的左侧) |
+| table-title | 表格标题插槽 |
+
+::: info 搜索表单的插槽
+
+对于使用了搜索表单的表格来说,所有以`form-`开头的命名插槽都会传递给表单。
+
+:::
diff --git a/web/docs/src/components/introduction.md b/web/docs/src/components/introduction.md
new file mode 100644
index 0000000..438470e
--- /dev/null
+++ b/web/docs/src/components/introduction.md
@@ -0,0 +1,15 @@
+# 介绍
+
+::: info README
+
+该文档介绍的是框架组件的使用方法、属性、事件等。如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
+
+:::
+
+## 布局组件
+
+布局组件一般在页面内容区域用作顶层容器组件,提供一些统一的布局样式和基本功能。
+
+## 通用组件
+
+通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。
diff --git a/web/docs/src/components/layout-ui/page.md b/web/docs/src/components/layout-ui/page.md
new file mode 100644
index 0000000..29fbdd4
--- /dev/null
+++ b/web/docs/src/components/layout-ui/page.md
@@ -0,0 +1,44 @@
+---
+outline: deep
+---
+
+# Page 常规页面组件
+
+提供一个常规页面布局的组件,包括头部、内容区域、底部三个部分。
+
+::: info 写在前面
+
+本组件是一个基本布局组件。如果有更多的通用页面布局需求(比如双列布局等),可以根据实际需求自行封装。
+
+:::
+
+## 基础用法
+
+将`Page`作为你的业务页面的根组件即可。
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- | --- |
+| title | 页面标题 | `string\|slot` | - | - |
+| description | 页面描述(标题下的内容) | `string\|slot` | - | - |
+| contentClass | 内容区域的class | `string` | - | - |
+| headerClass | 头部区域的class | `string` | - | - |
+| footerClass | 底部区域的class | `string` | - | - |
+| autoContentHeight | 自动调整内容区域的高度 | `boolean` | `false` | - |
+
+::: tip 注意
+
+如果`title`、`description`、`extra`三者均未提供有效内容(通过`props`或者`slots`均可),则页面头部区域不会渲染。
+
+:::
+
+### Slots
+
+| 插槽名称 | 描述 |
+| ----------- | ------------ |
+| default | 页面内容 |
+| title | 页面标题 |
+| description | 页面描述 |
+| extra | 页面头部右侧 |
+| footer | 页面底部 |
diff --git a/web/docs/src/demos/vben-alert/alert/index.vue b/web/docs/src/demos/vben-alert/alert/index.vue
new file mode 100644
index 0000000..9ba18a4
--- /dev/null
+++ b/web/docs/src/demos/vben-alert/alert/index.vue
@@ -0,0 +1,36 @@
+
+
+
+ Alert
+ Alert With Icon
+ Alert With Custom Content
+
+
diff --git a/web/docs/src/demos/vben-alert/confirm/index.vue b/web/docs/src/demos/vben-alert/confirm/index.vue
new file mode 100644
index 0000000..723445d
--- /dev/null
+++ b/web/docs/src/demos/vben-alert/confirm/index.vue
@@ -0,0 +1,75 @@
+
+
+
+ Confirm
+ Confirm With Icon
+ Confirm With Footer
+ Async Confirm
+
+
diff --git a/web/docs/src/demos/vben-alert/prompt/index.vue b/web/docs/src/demos/vben-alert/prompt/index.vue
new file mode 100644
index 0000000..a192213
--- /dev/null
+++ b/web/docs/src/demos/vben-alert/prompt/index.vue
@@ -0,0 +1,118 @@
+
+
+
+ Prompt
+ Prompt With slots
+ Prompt With Select
+ Prompt With Async
+
+
diff --git a/web/docs/src/demos/vben-api-component/cascader/index.vue b/web/docs/src/demos/vben-api-component/cascader/index.vue
new file mode 100644
index 0000000..957964c
--- /dev/null
+++ b/web/docs/src/demos/vben-api-component/cascader/index.vue
@@ -0,0 +1,100 @@
+
+
+
+
diff --git a/web/docs/src/demos/vben-count-to-animator/basic/index.vue b/web/docs/src/demos/vben-count-to-animator/basic/index.vue
new file mode 100644
index 0000000..acc76ed
--- /dev/null
+++ b/web/docs/src/demos/vben-count-to-animator/basic/index.vue
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/web/docs/src/demos/vben-count-to-animator/custom/index.vue b/web/docs/src/demos/vben-count-to-animator/custom/index.vue
new file mode 100644
index 0000000..5a24340
--- /dev/null
+++ b/web/docs/src/demos/vben-count-to-animator/custom/index.vue
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/web/docs/src/demos/vben-drawer/auto-height/drawer.vue b/web/docs/src/demos/vben-drawer/auto-height/drawer.vue
new file mode 100644
index 0000000..9ab433c
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/auto-height/drawer.vue
@@ -0,0 +1,45 @@
+
+
+
+
+ {{ item }}
+
+
+
+ 点击更新数据
+
+
+
+
diff --git a/web/docs/src/demos/vben-drawer/auto-height/index.vue b/web/docs/src/demos/vben-drawer/auto-height/index.vue
new file mode 100644
index 0000000..59294e5
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/auto-height/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Open
+
+
diff --git a/web/docs/src/demos/vben-drawer/basic/index.vue b/web/docs/src/demos/vben-drawer/basic/index.vue
new file mode 100644
index 0000000..bd7d927
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/basic/index.vue
@@ -0,0 +1,11 @@
+
+
+
+ drawerApi.open()">Open
+ drawer content
+
+
diff --git a/web/docs/src/demos/vben-drawer/dynamic/drawer.vue b/web/docs/src/demos/vben-drawer/dynamic/drawer.vue
new file mode 100644
index 0000000..50f6283
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/dynamic/drawer.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+ 内部动态修改标题
+
+
+
+
diff --git a/web/docs/src/demos/vben-drawer/dynamic/index.vue b/web/docs/src/demos/vben-drawer/dynamic/index.vue
new file mode 100644
index 0000000..9546402
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/dynamic/index.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Open
+
+ 从外部修改标题并打开
+
+
+
diff --git a/web/docs/src/demos/vben-drawer/extra/drawer.vue b/web/docs/src/demos/vben-drawer/extra/drawer.vue
new file mode 100644
index 0000000..e84c193
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/extra/drawer.vue
@@ -0,0 +1,8 @@
+
+
+ extra drawer content
+
diff --git a/web/docs/src/demos/vben-drawer/extra/index.vue b/web/docs/src/demos/vben-drawer/extra/index.vue
new file mode 100644
index 0000000..59294e5
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/extra/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Open
+
+
diff --git a/web/docs/src/demos/vben-drawer/shared-data/drawer.vue b/web/docs/src/demos/vben-drawer/shared-data/drawer.vue
new file mode 100644
index 0000000..629199b
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/shared-data/drawer.vue
@@ -0,0 +1,26 @@
+
+
+
+ 外部传递数据: {{ data }}
+
+
diff --git a/web/docs/src/demos/vben-drawer/shared-data/index.vue b/web/docs/src/demos/vben-drawer/shared-data/index.vue
new file mode 100644
index 0000000..fef6cb0
--- /dev/null
+++ b/web/docs/src/demos/vben-drawer/shared-data/index.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Open
+
+
diff --git a/web/docs/src/demos/vben-ellipsis-text/auto-display/index.vue b/web/docs/src/demos/vben-ellipsis-text/auto-display/index.vue
new file mode 100644
index 0000000..fb5a32a
--- /dev/null
+++ b/web/docs/src/demos/vben-ellipsis-text/auto-display/index.vue
@@ -0,0 +1,16 @@
+
+
+
+ {{ text }}
+
+
+
+ {{ text }}
+
+
diff --git a/web/docs/src/demos/vben-ellipsis-text/expand/index.vue b/web/docs/src/demos/vben-ellipsis-text/expand/index.vue
new file mode 100644
index 0000000..842f6b3
--- /dev/null
+++ b/web/docs/src/demos/vben-ellipsis-text/expand/index.vue
@@ -0,0 +1,10 @@
+
+
+ {{ text }}
+
diff --git a/web/docs/src/demos/vben-ellipsis-text/line/index.vue b/web/docs/src/demos/vben-ellipsis-text/line/index.vue
new file mode 100644
index 0000000..dfbf20e
--- /dev/null
+++ b/web/docs/src/demos/vben-ellipsis-text/line/index.vue
@@ -0,0 +1,10 @@
+
+
+ {{ text }}
+
diff --git a/web/docs/src/demos/vben-ellipsis-text/tooltip/index.vue b/web/docs/src/demos/vben-ellipsis-text/tooltip/index.vue
new file mode 100644
index 0000000..e6287a1
--- /dev/null
+++ b/web/docs/src/demos/vben-ellipsis-text/tooltip/index.vue
@@ -0,0 +1,14 @@
+
+
+
+ 住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
+
+
+ 《秦皇岛》 住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦
+ 深海的光 停滞的海浪
+
+
+
+
diff --git a/web/docs/src/demos/vben-form/api/index.vue b/web/docs/src/demos/vben-form/api/index.vue
new file mode 100644
index 0000000..786dc89
--- /dev/null
+++ b/web/docs/src/demos/vben-form/api/index.vue
@@ -0,0 +1,236 @@
+
+
+
+
+
+ updateSchema
+ 更改labelWidth
+ 还原labelWidth
+ 禁用表单
+ 解除禁用
+ 隐藏操作按钮
+ 显示操作按钮
+ 隐藏重置按钮
+ 显示重置按钮
+ 隐藏提交按钮
+ 显示提交按钮
+ 修改重置按钮
+ 修改提交按钮
+
+ 调整操作按钮位置
+
+ 批量添加表单项
+
+ 批量删除表单项
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-form/basic/index.vue b/web/docs/src/demos/vben-form/basic/index.vue
new file mode 100644
index 0000000..88d0142
--- /dev/null
+++ b/web/docs/src/demos/vben-form/basic/index.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-form/custom/index.vue b/web/docs/src/demos/vben-form/custom/index.vue
new file mode 100644
index 0000000..e4da38d
--- /dev/null
+++ b/web/docs/src/demos/vben-form/custom/index.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-form/dynamic/index.vue b/web/docs/src/demos/vben-form/dynamic/index.vue
new file mode 100644
index 0000000..83d1a71
--- /dev/null
+++ b/web/docs/src/demos/vben-form/dynamic/index.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-form/query/index.vue b/web/docs/src/demos/vben-form/query/index.vue
new file mode 100644
index 0000000..a0b64f5
--- /dev/null
+++ b/web/docs/src/demos/vben-form/query/index.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-form/rules/index.vue b/web/docs/src/demos/vben-form/rules/index.vue
new file mode 100644
index 0000000..7abcc6f
--- /dev/null
+++ b/web/docs/src/demos/vben-form/rules/index.vue
@@ -0,0 +1,189 @@
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-modal/auto-height/index.vue b/web/docs/src/demos/vben-modal/auto-height/index.vue
new file mode 100644
index 0000000..2addf2e
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/auto-height/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Open
+
+
diff --git a/web/docs/src/demos/vben-modal/auto-height/modal.vue b/web/docs/src/demos/vben-modal/auto-height/modal.vue
new file mode 100644
index 0000000..8757d5e
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/auto-height/modal.vue
@@ -0,0 +1,45 @@
+
+
+
+
+ {{ item }}
+
+
+
+ 点击更新数据
+
+
+
+
diff --git a/web/docs/src/demos/vben-modal/basic/index.vue b/web/docs/src/demos/vben-modal/basic/index.vue
new file mode 100644
index 0000000..9f89970
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/basic/index.vue
@@ -0,0 +1,11 @@
+
+
+
+ modalApi.open()">Open
+ modal content
+
+
diff --git a/web/docs/src/demos/vben-modal/draggable/index.vue b/web/docs/src/demos/vben-modal/draggable/index.vue
new file mode 100644
index 0000000..2addf2e
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/draggable/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Open
+
+
diff --git a/web/docs/src/demos/vben-modal/draggable/modal.vue b/web/docs/src/demos/vben-modal/draggable/modal.vue
new file mode 100644
index 0000000..ecca497
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/draggable/modal.vue
@@ -0,0 +1,10 @@
+
+
+ modal content
+
diff --git a/web/docs/src/demos/vben-modal/dynamic/index.vue b/web/docs/src/demos/vben-modal/dynamic/index.vue
new file mode 100644
index 0000000..1b02554
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/dynamic/index.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Open
+
+ 从外部修改标题并打开
+
+
+
diff --git a/web/docs/src/demos/vben-modal/dynamic/modal.vue b/web/docs/src/demos/vben-modal/dynamic/modal.vue
new file mode 100644
index 0000000..d461289
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/dynamic/modal.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+ 内部动态修改标题
+
+
+ {{ state.fullscreen ? '退出全屏' : '打开全屏' }}
+
+
+
+
diff --git a/web/docs/src/demos/vben-modal/extra/index.vue b/web/docs/src/demos/vben-modal/extra/index.vue
new file mode 100644
index 0000000..2addf2e
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/extra/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Open
+
+
diff --git a/web/docs/src/demos/vben-modal/extra/modal.vue b/web/docs/src/demos/vben-modal/extra/modal.vue
new file mode 100644
index 0000000..488fd4a
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/extra/modal.vue
@@ -0,0 +1,8 @@
+
+
+ extra modal content
+
diff --git a/web/docs/src/demos/vben-modal/shared-data/index.vue b/web/docs/src/demos/vben-modal/shared-data/index.vue
new file mode 100644
index 0000000..91afeb7
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/shared-data/index.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Open
+
+
diff --git a/web/docs/src/demos/vben-modal/shared-data/modal.vue b/web/docs/src/demos/vben-modal/shared-data/modal.vue
new file mode 100644
index 0000000..806585d
--- /dev/null
+++ b/web/docs/src/demos/vben-modal/shared-data/modal.vue
@@ -0,0 +1,26 @@
+
+
+
+ 外部传递数据: {{ data }}
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/basic/index.vue b/web/docs/src/demos/vben-vxe-table/basic/index.vue
new file mode 100644
index 0000000..4b6b5a6
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/basic/index.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+ {{ showBorder ? '隐藏' : '显示' }}边框
+
+
+ 显示loading
+
+
+ {{ showStripe ? '隐藏' : '显示' }}斑马纹
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/custom-cell/index.vue b/web/docs/src/demos/vben-vxe-table/custom-cell/index.vue
new file mode 100644
index 0000000..517e73f
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/custom-cell/index.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.status }}
+
+
+ 编辑
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/edit-cell/index.vue b/web/docs/src/demos/vben-vxe-table/edit-cell/index.vue
new file mode 100644
index 0000000..711941d
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/edit-cell/index.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/edit-row/index.vue b/web/docs/src/demos/vben-vxe-table/edit-row/index.vue
new file mode 100644
index 0000000..f317f69
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/edit-row/index.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+ 保存
+ 取消
+
+
+ 编辑
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/fixed/index.vue b/web/docs/src/demos/vben-vxe-table/fixed/index.vue
new file mode 100644
index 0000000..6067a5e
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/fixed/index.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ 编辑
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/form/index.vue b/web/docs/src/demos/vben-vxe-table/form/index.vue
new file mode 100644
index 0000000..bcf3f5a
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/form/index.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/mock-api.ts b/web/docs/src/demos/vben-vxe-table/mock-api.ts
new file mode 100644
index 0000000..e5c40b6
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/mock-api.ts
@@ -0,0 +1,36 @@
+import { MOCK_API_DATA } from './table-data';
+
+export namespace DemoTableApi {
+ export interface PageFetchParams {
+ [key: string]: any;
+ page: number;
+ pageSize: number;
+ }
+}
+
+export function sleep(time = 1000) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(true);
+ }, time);
+ });
+}
+
+/**
+ * 获取示例表格数据
+ */
+async function getExampleTableApi(params: DemoTableApi.PageFetchParams) {
+ return new Promise<{ items: any; total: number }>((resolve) => {
+ const { page, pageSize } = params;
+ const items = MOCK_API_DATA.slice((page - 1) * pageSize, page * pageSize);
+
+ sleep(1000).then(() => {
+ resolve({
+ total: items.length,
+ items,
+ });
+ });
+ });
+}
+
+export { getExampleTableApi };
diff --git a/web/docs/src/demos/vben-vxe-table/remote/index.vue b/web/docs/src/demos/vben-vxe-table/remote/index.vue
new file mode 100644
index 0000000..bc93d29
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/remote/index.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+ gridApi.query()">
+ 刷新当前页面
+
+ gridApi.reload()">
+ 刷新并返回第一页
+
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/table-data.ts b/web/docs/src/demos/vben-vxe-table/table-data.ts
new file mode 100644
index 0000000..c37b88a
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/table-data.ts
@@ -0,0 +1,384 @@
+interface TableRowData {
+ address: string;
+ age: number;
+ id: number;
+ name: string;
+ nickname: string;
+ role: string;
+}
+
+const roles = ['User', 'Admin', 'Manager', 'Guest'];
+
+export const MOCK_TABLE_DATA: TableRowData[] = (() => {
+ const data: TableRowData[] = [];
+ for (let i = 0; i < 10; i++) {
+ data.push({
+ address: `New York${i}`,
+ age: i + 1,
+ id: i,
+ name: `Test${i}`,
+ nickname: `Test${i}`,
+ role: roles[Math.floor(Math.random() * roles.length)] as string,
+ });
+ }
+ return data;
+})();
+
+export const MOCK_TREE_TABLE_DATA = [
+ {
+ date: '2020-08-01',
+ id: 10_000,
+ name: 'Test1',
+ parentId: null,
+ size: 1024,
+ type: 'mp3',
+ },
+ {
+ date: '2021-04-01',
+ id: 10_050,
+ name: 'Test2',
+ parentId: null,
+ size: 0,
+ type: 'mp4',
+ },
+ {
+ date: '2020-03-01',
+ id: 24_300,
+ name: 'Test3',
+ parentId: 10_050,
+ size: 1024,
+ type: 'avi',
+ },
+ {
+ date: '2021-04-01',
+ id: 20_045,
+ name: 'Test4',
+ parentId: 24_300,
+ size: 600,
+ type: 'html',
+ },
+ {
+ date: '2021-04-01',
+ id: 10_053,
+ name: 'Test5',
+ parentId: 24_300,
+ size: 0,
+ type: 'avi',
+ },
+ {
+ date: '2021-10-01',
+ id: 24_330,
+ name: 'Test6',
+ parentId: 10_053,
+ size: 25,
+ type: 'txt',
+ },
+ {
+ date: '2020-01-01',
+ id: 21_011,
+ name: 'Test7',
+ parentId: 10_053,
+ size: 512,
+ type: 'pdf',
+ },
+ {
+ date: '2021-06-01',
+ id: 22_200,
+ name: 'Test8',
+ parentId: 10_053,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2020-11-01',
+ id: 23_666,
+ name: 'Test9',
+ parentId: null,
+ size: 2048,
+ type: 'xlsx',
+ },
+ {
+ date: '2021-06-01',
+ id: 23_677,
+ name: 'Test10',
+ parentId: 23_666,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2021-06-01',
+ id: 23_671,
+ name: 'Test11',
+ parentId: 23_677,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2021-06-01',
+ id: 23_672,
+ name: 'Test12',
+ parentId: 23_677,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2021-06-01',
+ id: 23_688,
+ name: 'Test13',
+ parentId: 23_666,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2021-06-01',
+ id: 23_681,
+ name: 'Test14',
+ parentId: 23_688,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2021-06-01',
+ id: 23_682,
+ name: 'Test15',
+ parentId: 23_688,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2020-10-01',
+ id: 24_555,
+ name: 'Test16',
+ parentId: null,
+ size: 224,
+ type: 'avi',
+ },
+ {
+ date: '2021-06-01',
+ id: 24_566,
+ name: 'Test17',
+ parentId: 24_555,
+ size: 1024,
+ type: 'js',
+ },
+ {
+ date: '2021-06-01',
+ id: 24_577,
+ name: 'Test18',
+ parentId: 24_555,
+ size: 1024,
+ type: 'js',
+ },
+];
+
+export const MOCK_API_DATA = [
+ {
+ available: true,
+ category: 'Computers',
+ color: 'purple',
+ currency: 'NAD',
+ description:
+ 'Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support',
+ id: '45a613df-227a-4907-a89f-4a7f1252ca0c',
+ imageUrl: 'https://avatars.githubusercontent.com/u/62715097',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/75395683',
+ inProduction: false,
+ open: true,
+ price: '48.89',
+ productName: 'Handcrafted Steel Salad',
+ quantity: 70,
+ rating: 3.780_582_329_574_367,
+ releaseDate: '2024-09-09T04:06:57.793Z',
+ status: 'error',
+ tags: ['Bespoke', 'Handmade', 'Luxurious'],
+ weight: 1.031_015_671_912_002_5,
+ },
+ {
+ available: true,
+ category: 'Toys',
+ color: 'green',
+ currency: 'CZK',
+ description:
+ 'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
+ id: 'd02e5ee9-bc98-4de2-98fa-25a6567ecc19',
+ imageUrl: 'https://avatars.githubusercontent.com/u/51512330',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/58698113',
+ inProduction: false,
+ open: false,
+ price: '68.15',
+ productName: 'Generic Cotton Gloves',
+ quantity: 3,
+ rating: 1.681_749_367_682_703_3,
+ releaseDate: '2024-06-16T09:00:36.806Z',
+ status: 'warning',
+ tags: ['Rustic', 'Handcrafted', 'Recycled'],
+ weight: 9.601_076_149_300_575,
+ },
+ {
+ available: true,
+ category: 'Beauty',
+ color: 'teal',
+ currency: 'OMR',
+ description:
+ 'The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design',
+ id: '2b72521c-225c-4e64-8030-611b76b10b37',
+ imageUrl: 'https://avatars.githubusercontent.com/u/50300075',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/36541691',
+ inProduction: true,
+ open: true,
+ price: '696.94',
+ productName: 'Gorgeous Soft Ball',
+ quantity: 50,
+ rating: 2.361_581_777_372_057_5,
+ releaseDate: '2024-06-03T13:24:19.809Z',
+ status: 'warning',
+ tags: ['Gorgeous', 'Ergonomic', 'Licensed'],
+ weight: 8.882_340_049_286_19,
+ },
+ {
+ available: true,
+ category: 'Games',
+ color: 'silver',
+ currency: 'SOS',
+ description:
+ 'Carbonite web goalkeeper gloves are ergonomically designed to give easy fit',
+ id: 'bafab694-3801-452c-b102-9eb519bd1143',
+ imageUrl: 'https://avatars.githubusercontent.com/u/89827115',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/55952747',
+ inProduction: false,
+ open: false,
+ price: '553.84',
+ productName: 'Bespoke Soft Computer',
+ quantity: 29,
+ rating: 2.176_412_873_760_271_7,
+ releaseDate: '2024-09-17T12:16:27.034Z',
+ status: 'error',
+ tags: ['Elegant', 'Rustic', 'Recycled'],
+ weight: 9.653_285_869_978_038,
+ },
+ {
+ available: true,
+ category: 'Toys',
+ color: 'indigo',
+ currency: 'BIF',
+ description:
+ 'Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals',
+ id: 'bf6dea6b-2a55-441d-8773-937e03d99389',
+ imageUrl: 'https://avatars.githubusercontent.com/u/21431092',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/3771350',
+ inProduction: true,
+ open: true,
+ price: '237.39',
+ productName: 'Handcrafted Cotton Mouse',
+ quantity: 54,
+ rating: 4.363_265_388_265_461,
+ releaseDate: '2023-10-23T13:42:34.947Z',
+ status: 'error',
+ tags: ['Unbranded', 'Handmade', 'Generic'],
+ weight: 9.513_203_612_535_571,
+ },
+ {
+ available: false,
+ category: 'Tools',
+ color: 'violet',
+ currency: 'TZS',
+ description:
+ 'New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016',
+ id: '135ba6ab-32ee-4989-8189-5cfa658ef970',
+ imageUrl: 'https://avatars.githubusercontent.com/u/29946092',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/23842994',
+ inProduction: false,
+ open: false,
+ price: '825.25',
+ productName: 'Awesome Bronze Ball',
+ quantity: 94,
+ rating: 4.251_159_804_726_753,
+ releaseDate: '2023-12-30T07:31:43.464Z',
+ status: 'warning',
+ tags: ['Handmade', 'Elegant', 'Unbranded'],
+ weight: 2.247_473_385_732_636_8,
+ },
+ {
+ available: true,
+ category: 'Automotive',
+ color: 'teal',
+ currency: 'BOB',
+ description: 'The Football Is Good For Training And Recreational Purposes',
+ id: '652ef256-7d4e-48b7-976c-7afaa781ea92',
+ imageUrl: 'https://avatars.githubusercontent.com/u/2531904',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/15215990',
+ inProduction: false,
+ open: false,
+ price: '780.49',
+ productName: 'Oriental Rubber Pants',
+ quantity: 70,
+ rating: 2.636_323_417_377_916,
+ releaseDate: '2024-02-23T23:30:49.628Z',
+ status: 'success',
+ tags: ['Unbranded', 'Elegant', 'Unbranded'],
+ weight: 4.812_965_858_018_838,
+ },
+ {
+ available: false,
+ category: 'Garden',
+ color: 'plum',
+ currency: 'LRD',
+ description:
+ 'The slim & simple Maple Gaming Keyboard from Dev Byte comes with a sleek body and 7- Color RGB LED Back-lighting for smart functionality',
+ id: '3ea24798-6589-40cc-85f0-ab78752244a0',
+ imageUrl: 'https://avatars.githubusercontent.com/u/23165285',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/14595665',
+ inProduction: false,
+ open: true,
+ price: '583.85',
+ productName: 'Handcrafted Concrete Hat',
+ quantity: 15,
+ rating: 1.371_600_527_752_802_7,
+ releaseDate: '2024-03-02T19:40:50.255Z',
+ status: 'error',
+ tags: ['Rustic', 'Sleek', 'Ergonomic'],
+ weight: 4.926_949_366_405_728_4,
+ },
+ {
+ available: false,
+ category: 'Industrial',
+ color: 'salmon',
+ currency: 'AUD',
+ description:
+ 'The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design',
+ id: '997113dd-f6e4-4acc-9790-ef554c7498d1',
+ imageUrl: 'https://avatars.githubusercontent.com/u/49021914',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/4690621',
+ inProduction: true,
+ open: false,
+ price: '67.99',
+ productName: 'Generic Rubber Bacon',
+ quantity: 68,
+ rating: 4.129_840_682_128_08,
+ releaseDate: '2023-12-17T01:40:25.415Z',
+ status: 'error',
+ tags: ['Oriental', 'Small', 'Handcrafted'],
+ weight: 1.080_114_331_801_906_4,
+ },
+ {
+ available: false,
+ category: 'Tools',
+ color: 'sky blue',
+ currency: 'NOK',
+ description:
+ 'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
+ id: 'f697a250-6cb2-46c8-b0f7-871ab1f2fa8d',
+ imageUrl: 'https://avatars.githubusercontent.com/u/95928385',
+ imageUrl2: 'https://avatars.githubusercontent.com/u/47588244',
+ inProduction: false,
+ open: false,
+ price: '613.89',
+ productName: 'Gorgeous Frozen Ball',
+ quantity: 55,
+ rating: 1.646_947_205_998_534_6,
+ releaseDate: '2024-10-13T12:31:04.929Z',
+ status: 'warning',
+ tags: ['Handmade', 'Unbranded', 'Unbranded'],
+ weight: 9.430_690_557_758_114,
+ },
+];
diff --git a/web/docs/src/demos/vben-vxe-table/tree/index.vue b/web/docs/src/demos/vben-vxe-table/tree/index.vue
new file mode 100644
index 0000000..0024765
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/tree/index.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+ 展开全部
+
+ 折叠全部
+
+
+
+
diff --git a/web/docs/src/demos/vben-vxe-table/virtual/index.vue b/web/docs/src/demos/vben-vxe-table/virtual/index.vue
new file mode 100644
index 0000000..81fa00a
--- /dev/null
+++ b/web/docs/src/demos/vben-vxe-table/virtual/index.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
diff --git a/web/docs/src/en/guide/essentials/build.md b/web/docs/src/en/guide/essentials/build.md
new file mode 100644
index 0000000..7f5c953
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/build.md
@@ -0,0 +1,243 @@
+# Build and Deployment
+
+::: tip Preface
+
+Since this is a demonstration project, the package size after building is relatively large. If there are plugins in the project that are not used, you can delete the corresponding files or routes. If they are not referenced, they will not be packaged.
+
+:::
+
+## Building
+
+After the project development is completed, execute the following command to build:
+
+**Note:** Please execute the following command in the project root directory.
+
+```bash
+pnpm build
+```
+
+After the build is successful, a `dist` folder for the corresponding application will be generated in the root directory, which contains the built and packaged files, for example: `apps/web-antd/dist/`
+
+## Preview
+
+Before publishing, you can preview it locally in several ways, here are two:
+
+- Using the project's custom command for preview (recommended)
+
+**Note:** Please execute the following command in the project root directory.
+
+```bash
+pnpm preview
+```
+
+After waiting for the build to succeed, visit `http://localhost:4173` to view the effect.
+
+- Local server preview
+
+You can globally install a `serve` service on your computer, such as `live-server`,
+
+```bash
+npm i -g live-server
+```
+
+Then execute the `live-server` command in the `dist` directory to view the effect locally.
+
+```bash
+cd apps/web-antd/dist
+# Local preview, default port 8080
+live-server
+# Specify port
+live-server --port 9000
+```
+
+## Compression
+
+### Enable `gzip` Compression
+
+To enable during the build process, change the `.env.production` configuration:
+
+```bash
+VITE_COMPRESS=gzip
+```
+
+### Enable `brotli` Compression
+
+To enable during the build process, change the `.env.production` configuration:
+
+```bash
+VITE_COMPRESS=brotli
+```
+
+### Enable Both `gzip` and `brotli` Compression
+
+To enable during the build process, change the `.env.production` configuration:
+
+```bash
+VITE_COMPRESS=gzip,brotli
+```
+
+::: tip Note
+
+Both `gzip` and `brotli` require specific modules to be installed for use.
+
+:::
+
+::: details gzip 与 brotli 在 nginx 内的配置
+
+```bash
+http {
+ # Enable gzip
+ gzip on;
+ # Enable gzip_static
+ # After enabling gzip_static, there might be errors, requiring the installation of specific modules. The installation method can be researched independently.
+ # Only with this enabled, the .gz files packaged by vue files will be effective; otherwise, there is no need to enable gzip for packaging.
+ gzip_static on;
+ gzip_proxied any;
+ gzip_min_length 1k;
+ gzip_buffers 4 16k;
+ # If nginx uses multiple layers of proxy, this must be set to enable gzip.
+ gzip_http_version 1.0;
+ gzip_comp_level 2;
+ gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
+ gzip_vary off;
+ gzip_disable "MSIE [1-6]\.";
+
+ # Enable brotli compression
+ # Requires the installation of the corresponding nginx module, which can be researched independently.
+ # Can coexist with gzip without conflict.
+ brotli on;
+ brotli_comp_level 6;
+ brotli_buffers 16 8k;
+ brotli_min_length 20;
+ brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;
+}
+```
+
+:::
+
+## Build Analysis
+
+If your build files are large, you can optimize your code by analyzing the code size with the built-in [rollup-plugin-analyzer](https://github.com/doesdev/rollup-plugin-analyzer) plugin. Just execute the following command in the `root directory`:
+
+```bash
+pnpm run build:analyze
+```
+
+After running, you can see the specific distribution of sizes on the automatically opened page to analyze which dependencies are problematic.
+
+
+
+## Deployment
+
+A simple deployment only requires publishing the final static files, the static files in the dist folder, to your CDN or static server. It's important to note that the index.html is usually the entry page for your backend service. After determining the static js and css, you may need to change the page's import path.
+
+For example, to upload to an nginx server, you can upload the files under the dist folder to the server's `/srv/www/project/index.html` directory, and then access the configured domain name.
+
+```bash
+# nginx configuration
+location / {
+ # Do not cache html to prevent cache from continuing to be effective after program updates
+ if ($request_filename ~* .*\.(?:htm|html)$) {
+ add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
+ access_log on;
+ }
+ # This is the storage path for the files inside the vue packaged dist folder
+ root /srv/www/project/;
+ index index.html index.htm;
+}
+```
+
+If you find the resource path is incorrect during deployment, you just need to modify the `.env.production` file.
+
+```bash
+# Configure the change according to your own path
+# Note that it needs to start and end with /
+VITE_BASE=/
+VITE_BASE=/xxx/
+```
+
+### Integration of Frontend Routing and Server
+
+The project uses vue-router for frontend routing, so you can choose between two modes: history and hash.
+
+- `hash` mode will append `#` to the URL by default.
+- `history` mode will not, but `history` mode requires server-side support.
+
+You can modify the mode in `.env.production`:
+
+```bash
+VITE_ROUTER_HISTORY=hash
+```
+
+### Server Configuration for History Mode Routing
+
+Enabling `history` mode requires server configuration. For more details on server configuration, see [history-mode](https://router.vuejs.org/guide/essentials/history-mode.html#html5-mode)
+
+Here is an example of `nginx` configuration:
+
+#### Deployment at the Root Directory
+
+```bash {5}
+server {
+ listen 80;
+ location / {
+ # For use with History mode
+ try_files $uri $uri/ /index.html;
+ }
+}
+```
+
+#### Deployment to a Non-root Directory
+
+- First, you need to change the `.env.production` configuration during packaging:
+
+```bash
+VITE_BASE = /sub/
+```
+
+- Then configure in the nginx configuration file
+
+```bash {8}
+server {
+ listen 80;
+ server_name localhost;
+ location /sub/ {
+ # This is the path where the vue packaged dist files are stored
+ alias /srv/www/project/;
+ index index.html index.htm;
+ try_files $uri $uri/ /sub/index.html;
+ }
+}
+```
+
+## Cross-Domain Handling
+
+Using nginx to handle cross-domain issues after project deployment
+
+1. Configure the frontend project API address in the `.env.production` file in the project directory:
+
+```bash
+VITE_GLOB_API_URL=/api
+```
+
+2. Configure nginx to forward requests to the backend
+
+```bash {10-11}
+server {
+ listen 8080;
+ server_name localhost;
+ # API proxy for solving cross-domain issues
+ location /api {
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # Backend API address
+ proxy_pass http://110.110.1.1:8080/api;
+ rewrite "^/api/(.*)$" /$1 break;
+ proxy_redirect default;
+ add_header Access-Control-Allow-Origin *;
+ add_header Access-Control-Allow-Headers X-Requested-With;
+ add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
+ }
+}
+```
diff --git a/web/docs/src/en/guide/essentials/concept.md b/web/docs/src/en/guide/essentials/concept.md
new file mode 100644
index 0000000..8c940a9
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/concept.md
@@ -0,0 +1,70 @@
+# Basic Concepts
+
+In the new version, the entire project has been restructured. Now, we will introduce some basic concepts to help you better understand the entire document. Please make sure to read this section first.
+
+## Monorepo
+
+Monorepo refers to the repository of the entire project, which includes all code, packages, applications, standards, documentation, configurations, etc., that is, the entire content of a `Monorepo` directory.
+
+## Applications
+
+Applications refer to a complete project; a project can contain multiple applications, which can reuse the code, packages, standards, etc., within the monorepo. Applications are placed in the `apps` directory. Each application is independent and can be run, built, tested, and deployed separately; it can also include different component libraries, etc.
+
+::: tip
+
+Applications are not limited to front-end applications; they can also be back-end applications, mobile applications, etc. For example, `apps/backend-mock` is a back-end service.
+
+:::
+
+## Packages
+
+A package refers to an independent module, which can be a component, a tool, a library, etc. Packages can be referenced by multiple applications or other packages. Packages are placed in the `packages` directory.
+
+You can consider these packages as independent `npm` packages, and they are used in the same way as `npm` packages.
+
+### Package Import
+
+Importing a package in `package.json`:
+
+```json {3}
+{
+ "dependencies": {
+ "@vben/utils": "workspace:*"
+ }
+}
+```
+
+### Package Usage
+
+Importing a package in the code:
+
+```ts
+import { isString } from '@vben/utils';
+```
+
+## Aliases
+
+In the project, you can see some paths starting with `#`, such as `#/api`, `#/views`. These paths are aliases, used for quickly locating a certain directory. They are not implemented through `vite`'s `alias`, but through the principle of [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) in `Node.js` itself. You only need to configure the `imports` field in `package.json`.
+
+```json {3}
+{
+ "imports": {
+ "#/*": "./src/*"
+ }
+}
+```
+
+To make these aliases recognizable by the IDE, we also need to configure them in `tsconfig.json`:
+
+```json {5}
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "#/*": ["src/*"]
+ }
+ }
+}
+```
+
+This way, you can use aliases in your code.
diff --git a/web/docs/src/en/guide/essentials/development.md b/web/docs/src/en/guide/essentials/development.md
new file mode 100644
index 0000000..9ff832b
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/development.md
@@ -0,0 +1,255 @@
+# Local Development {#development}
+
+::: tip Code Acquisition
+
+If you haven't acquired the code yet, you can start by reading the documentation from [Quick Start](../introduction/quick-start.md).
+
+:::
+
+## Prerequisites
+
+For a better development experience, we provide some tool configurations and project descriptions to facilitate your development.
+
+### Required Basic Knowledge
+
+This project requires some basic frontend knowledge. Please ensure you are familiar with the basics of Vue to handle common issues. It is recommended to learn the following topics before development. Understanding these will be very helpful for the project:
+
+- [Vue3](https://vuejs.org/)
+- [Tailwind CSS](https://tailwindcss.com/)
+- [TypeScript](https://www.typescriptlang.org/)
+- [Vue Router](https://router.vuejs.org/)
+- [Vitejs](https://vitejs.dev/)
+- [Pnpm](https://pnpm.io/)
+- [Turbo](https://turbo.build/)
+
+### Tool Configuration
+
+If you are using [vscode](https://code.visualstudio.com/) (recommended) as your IDE, you can install the following tools to improve development efficiency and code formatting:
+
+- [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Official Vue plugin (essential).
+- [Tailwind CSS](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) - Tailwind CSS autocomplete plugin.
+- [CSS Variable Autocomplete](https://marketplace.visualstudio.com/items?itemName=vunguyentuan.vscode-css-variables) - CSS variable autocomplete plugin.
+- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - Iconify icon plugin.
+- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) - i18n plugin.
+- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code linting.
+- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting.
+- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) - CSS formatting.
+- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Spelling checker.
+- [DotENV](https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv) - .env file highlighting.
+
+## Npm Scripts
+
+Npm scripts are common configurations used in the project to perform common tasks such as starting the project, building the project, etc. The following scripts can be found in the `package.json` file at the root of the project.
+
+The execution command is: `pnpm run [script]` or `npm run [script]`.
+
+```json
+{
+ "scripts": {
+ // Build the project
+ "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
+ // Build the project with analysis
+ "build:analyze": "turbo build:analyze",
+ // Build a local Docker image
+ "build:docker": "./build-local-docker-image.sh",
+ // Build the web-antd application separately
+ "build:antd": "pnpm run build --filter=@vben/web-antd",
+ // Build the documentation separately
+ "build:docs": "pnpm run build --filter=@vben/docs",
+ // Build the web-ele application separately
+ "build:ele": "pnpm run build --filter=@vben/web-ele",
+ // Build the web-naive application separately
+ "build:naive": "pnpm run build --filter=@vben/naive",
+ // Build the playground application separately
+ "build:play": "pnpm run build --filter=@vben/playground",
+ // Changeset version management
+ "changeset": "pnpm exec changeset",
+ // Check for various issues in the project
+ "check": "pnpm run check:circular && pnpm run check:dep && pnpm run check:type && pnpm check:cspell",
+ // Check for circular dependencies
+ "check:circular": "vsh check-circular",
+ // Check spelling
+ "check:cspell": "cspell lint **/*.ts **/README.md .changeset/*.md --no-progress"
+ // Check dependencies
+ "check:dep": "vsh check-dep",
+ // Check types
+ "check:type": "turbo run typecheck",
+ // Clean the project (delete node_modules, dist, .turbo, etc.)
+ "clean": "node ./scripts/clean.mjs",
+ // Commit code
+ "commit": "czg",
+ // Start the project (by default, the dev scripts of all packages in the entire repository will run)
+ "dev": "turbo-run dev",
+ // Start the web-antd application
+ "dev:antd": "pnpm -F @vben/web-antd run dev",
+ // Start the documentation
+ "dev:docs": "pnpm -F @vben/docs run dev",
+ // Start the web-ele application
+ "dev:ele": "pnpm -F @vben/web-ele run dev",
+ // Start the web-naive application
+ "dev:naive": "pnpm -F @vben/web-naive run dev",
+ // Start the playground application
+ "dev:play": "pnpm -F @vben/playground run dev",
+ // Format code
+ "format": "vsh lint --format",
+ // Lint code
+ "lint": "vsh lint",
+ // After installing dependencies, execute the stub script for all packages
+ "postinstall": "pnpm -r run stub --if-present",
+ // Only allow using pnpm
+ "preinstall": "npx only-allow pnpm",
+ // Install lefthook
+ "prepare": "is-ci || lefthook install",
+ // Preview the application
+ "preview": "turbo-run preview",
+ // Package specification check
+ "publint": "vsh publint",
+ // Delete all node_modules, yarn.lock, package.lock.json, and reinstall dependencies
+ "reinstall": "pnpm clean --del-lock && pnpm install",
+ // Run vitest unit tests
+ "test:unit": "vitest run --dom",
+ // Update project dependencies
+ "update:deps": " pnpm update --latest --recursive",
+ // Changeset generation and versioning
+ "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
+ }
+}
+```
+
+## Running the Project Locally
+
+To run the documentation locally and make adjustments, you can execute the following command. This command allows you to select the application you want to develop:
+
+```bash
+pnpm dev
+```
+
+If you want to run a specific application directly, you can execute the following commands:
+
+To run the `web-antd` application:
+
+```bash
+pnpm dev:antd
+```
+
+To run the `web-naive` application:
+
+```bash
+pnpm dev:naive
+```
+
+To run the `web-ele` application:
+
+```bash
+pnpm dev:ele
+```
+
+To run the `docs` application:
+
+```bash
+pnpm dev:docs
+```
+
+### Distinguishing Build Environments
+
+In actual business development, multiple environments are usually distinguished during the build process, such as the test environment `test` and the production environment `build`.
+
+At this point, you can modify three files and add corresponding script configurations to distinguish between production environments.
+
+Take the addition of the test environment `test` to `@vben/web-antd` as an example:
+
+- `apps\web-antd\package.json`
+
+```json
+"scripts": {
+ "build:prod": "pnpm vite build --mode production",
+ "build:test": "pnpm vite build --mode test",
+ "build:analyze": "pnpm vite build --mode analyze",
+ "dev": "pnpm vite --mode development",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --noEmit --skipLibCheck"
+}
+```
+
+Add the command `"build:test"` and change the original `"build"` to `"build:prod"` to avoid building packages for two environments simultaneously.
+
+- `package.json`
+
+```json
+"scripts": {
+ "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
+ "build:analyze": "turbo build:analyze",
+ "build:antd": "pnpm run build --filter=@vben/web-antd",
+ "build-test:antd": "pnpm run build --filter=@vben/web-antd build:test",
+
+ ······
+}
+```
+
+Add the command to build the test environment in the root directory `package.json`.
+
+- `turbo.json`
+
+```json
+"tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "outputs": [
+ "dist/**",
+ "dist.zip",
+ ".vitepress/dist.zip",
+ ".vitepress/dist/**"
+ ]
+ },
+
+ "build-test:antd": {
+ "dependsOn": ["@vben/web-antd#build:test"],
+ "outputs": ["dist/**"]
+ },
+
+ "@vben/web-antd#build:test": {
+ "dependsOn": ["^build"],
+ "outputs": ["dist/**"]
+ },
+
+ ······
+```
+
+Add the relevant dependent commands in `turbo.json`.
+
+## Public Static Resources
+
+If you need to use public static resources in the project, such as images, static HTML, etc., and you want to directly import them in the development process through `src="/xxx.png"`.
+
+You need to put the resource in the corresponding project's `public/static` directory. The import path for the resource should be `src="/static/xxx.png"`.
+
+## DevTools
+
+The project has a built-in [Vue DevTools](https://github.com/vuejs/devtools-next) plugin, which can be used during development. It is disabled by default, but can be enabled in the `.env.development` file. After enabling it, restart the project:
+
+```bash
+VITE_DEVTOOLS=true
+```
+
+Once enabled, a Vue DevTools icon will appear at the bottom of the page during project runtime. Click it to open the DevTools.
+
+
+
+## Running Documentation Locally
+
+To run the documentation locally and make adjustments, you can execute the following command:
+
+```bash
+pnpm dev:docs
+```
+
+## Troubleshooting
+
+If you encounter dependency-related issues, you can try reinstalling the dependencies:
+
+```bash
+# Execute this command at the root of the project.
+# This command will delete all node_modules, yarn.lock, and package.lock.json files
+# and then reinstall dependencies (this process will be noticeably slower).
+pnpm reinstall
+```
diff --git a/web/docs/src/en/guide/essentials/external-module.md b/web/docs/src/en/guide/essentials/external-module.md
new file mode 100644
index 0000000..f0a6d6e
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/external-module.md
@@ -0,0 +1,58 @@
+# External Modules
+
+In addition to the external modules that are included by default in the project, sometimes we need to import other external modules. Let's take [ant-design-vue](https://antdv.com/components/overview) as an example:
+
+## Installing Dependencies
+
+::: tip Install dependencies into a specific package
+
+- Since the project uses [pnpm](https://pnpm.io/) as the package management tool, we need to use the `pnpm` command to install dependencies.
+- As the project is managed using a Monorepo module, we need to install dependencies under a specific package. Please make sure you have entered the specific package directory before installing dependencies.
+
+:::
+
+```bash
+# cd /path/to/your/package
+pnpm add ant-design-vue
+```
+
+## Usage
+
+### Global Import
+
+```ts
+import { createApp } from 'vue';
+import Antd from 'ant-design-vue';
+import App from './App';
+import 'ant-design-vue/dist/reset.css';
+
+const app = createApp(App);
+
+app.use(Antd).mount('#app');
+```
+
+#### Usage
+
+```vue
+
+ text
+
+```
+
+### Partial Import
+
+```vue
+
+
+
+ text
+
+```
+
+::: warning Note
+
+- If the component depends on styles, you also need to import the style file.
+
+:::
diff --git a/web/docs/src/en/guide/essentials/icons.md b/web/docs/src/en/guide/essentials/icons.md
new file mode 100644
index 0000000..0c1631f
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/icons.md
@@ -0,0 +1,78 @@
+# Icons
+
+::: tip About Icon Management
+
+- The icons in the project are mainly provided by the `@vben/icons` package. It is recommended to manage them within this package for unified management and maintenance.
+- If you are using `Vscode`, it is recommended to install the [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) plugin, which makes it easy to find and use icons.
+
+:::
+
+There are several ways to use icons in the project, you can choose according to the actual situation:
+
+## Iconify Icons
+
+Integrated with the [iconify](https://github.com/iconify/iconify) icon library
+
+### Adding New Icons
+
+You can add new icons in the `packages/icons/src/iconify` directory:
+
+```ts
+// packages/icons/src/iconify/index.ts
+import { createIconifyIcon } from '@vben-core/icons';
+
+export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');
+```
+
+### Usage
+
+```vue
+
+
+
+
+
+
+```
+
+## SVG Icons
+
+Instead of using Svg Sprite, SVG icons are directly imported,
+
+### Adding New Icons
+
+You can add new icon files `test.svg` in the `packages/icons/src/svg/icons` directory, and then import it in `packages/icons/src/svg/index.ts`:
+
+```ts
+// packages/icons/src/svg/index.ts
+import { createIconifyIcon } from '@vben-core/icons';
+
+const SvgTestIcon = createIconifyIcon('svg:test');
+
+export { SvgTestIcon };
+```
+
+### Usage
+
+```vue
+
+
+
+
+
+
+```
+
+## Tailwind CSS Icons
+
+### Usage
+
+You can use the icons by directly adding the Tailwind CSS icon class names, which can be found on [iconify](https://github.com/iconify/iconify) :
+
+```vue
+
+```
diff --git a/web/docs/src/en/guide/essentials/route.md b/web/docs/src/en/guide/essentials/route.md
new file mode 100644
index 0000000..8fb0a6d
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/route.md
@@ -0,0 +1,603 @@
+---
+outline: deep
+---
+
+# Routes and Menus
+
+::: info
+
+This page is translated by machine translation and may not be very accurate.
+
+:::
+
+In the project, the framework provides a basic routing system and **automatically generates the corresponding menu structure based on the routing files**.
+
+## Types of Routes
+
+Routes are divided into core routes, static routes, and dynamic routes. Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc.; static routes are routes that are determined when the project starts; dynamic routes are generally generated dynamically based on the user's permissions after the user logs in.
+
+Both static and dynamic routes go through permission control, which can be controlled by configuring the `authority` field in the `meta` property of the route.
+
+### Core Routes
+
+Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc. The configuration of core routes is in the `src/router/routes/core` directory under the application.
+
+::: tip
+
+Core routes are mainly used for the basic functions of the framework, so it is not recommended to put business-related routes in core routes. It is recommended to put business-related routes in static or dynamic routes.
+
+:::
+
+### Static Routes
+
+The configuration of static routes is in the `src/router/routes/index` directory under the application. Open the commented file content:
+
+::: tip
+
+Permission control is controlled by the `authority` field in the `meta` property of the route. If your page project does not require permission control, you can omit the `authority` field.
+
+:::
+
+```ts
+// Uncomment if needed and create the folder
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); // [!code --]
+const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); // [!code ++]
+/** Dynamic routes */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** External route list, these pages can be accessed without Layout, possibly used for embedding in other systems */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles) // [!code --]
+const externalRoutes: RouteRecordRaw[] = []; // [!code --]
+const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); // [!code ++]
+```
+
+### Dynamic Routes
+
+The configuration of dynamic routes is in the `src/router/routes/modules` directory under the corresponding application. This directory contains all the route files. The content format of each file is consistent with the Vue Router route configuration format. Below is the configuration of secondary and multi-level routes.
+
+## Route Definition
+
+The configuration method of static routes and dynamic routes is the same. Below is the configuration of secondary and multi-level routes:
+
+### Secondary Routes
+
+::: details Secondary Route Example Code
+
+```ts
+import type { RouteRecordRaw } from 'vue-router';
+
+import { VBEN_LOGO_URL } from '@vben/constants';
+
+import { BasicLayout } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ badgeType: 'dot',
+ badgeVariants: 'destructive',
+ icon: VBEN_LOGO_URL,
+ order: 9999,
+ title: $t('page.vben.title'),
+ },
+ name: 'VbenProject',
+ path: '/vben-admin',
+ redirect: '/vben-admin/about',
+ children: [
+ {
+ name: 'VbenAbout',
+ path: '/vben-admin/about',
+ component: () => import('#/views/_core/about/index.vue'),
+ meta: {
+ badgeType: 'dot',
+ badgeVariants: 'destructive',
+ icon: 'lucide:copyright',
+ title: $t('page.vben.about'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
+```
+
+:::
+
+### Multi-level Routes
+
+::: tip
+
+- The parent route of multi-level routes does not need to set the `component` property, just set the `children` property. Unless you really need to display content nested under the parent route.
+- In most cases, the `redirect` property of the parent route does not need to be specified, it will default to the first child route.
+
+:::
+
+::: details Multi-level Route Example Code
+
+```ts
+import type { RouteRecordRaw } from 'vue-router';
+
+import { BasicLayout } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ redirect: '/demos/access',
+ children: [
+ // Nested menu
+ {
+ meta: {
+ icon: 'ic:round-menu',
+ title: $t('demos.nested.title'),
+ },
+ name: 'NestedDemos',
+ path: '/demos/nested',
+ redirect: '/demos/nested/menu1',
+ children: [
+ {
+ name: 'Menu1Demo',
+ path: '/demos/nested/menu1',
+ component: () => import('#/views/demos/nested/menu-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu1'),
+ },
+ },
+ {
+ name: 'Menu2Demo',
+ path: '/demos/nested/menu2',
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu2'),
+ },
+ redirect: '/demos/nested/menu2/menu2-1',
+ children: [
+ {
+ name: 'Menu21Demo',
+ path: '/demos/nested/menu2/menu2-1',
+ component: () => import('#/views/demos/nested/menu-2-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu2_1'),
+ },
+ },
+ ],
+ },
+ {
+ name: 'Menu3Demo',
+ path: '/demos/nested/menu3',
+ meta: {
+ icon: 'ic:round-menu',
+ title: $t('demos.nested.menu3'),
+ },
+ redirect: '/demos/nested/menu3/menu3-1',
+ children: [
+ {
+ name: 'Menu31Demo',
+ path: 'menu3-1',
+ component: () => import('#/views/demos/nested/menu-3-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu3_1'),
+ },
+ },
+ {
+ name: 'Menu32Demo',
+ path: 'menu3-2',
+ meta: {
+ icon: 'ic:round-menu',
+ title: $t('demos.nested.menu3_2'),
+ },
+ redirect: '/demos/nested/menu3/menu3-2/menu3-2-1',
+ children: [
+ {
+ name: 'Menu321Demo',
+ path: '/demos/nested/menu3/menu3-2/menu3-2-1',
+ component: () =>
+ import('#/views/demos/nested/menu-3-2-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu3_2_1'),
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export default routes;
+```
+
+:::
+
+## Adding a New Page
+
+To add a new page, you only need to add a route and the corresponding page component.
+
+### Adding a Route
+
+Add a route object in the corresponding route file, as follows:
+
+```ts
+import type { RouteRecordRaw } from 'vue-router';
+
+import { VBEN_LOGO_URL } from '@vben/constants';
+
+import { BasicLayout } from '#/layouts';
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'mdi:home',
+ title: $t('page.home.title'),
+ },
+ name: 'Home',
+ path: '/home',
+ redirect: '/home/index',
+ children: [
+ {
+ name: 'HomeIndex',
+ path: '/home/index',
+ component: () => import('#/views/home/index.vue'),
+ meta: {
+ icon: 'mdi:home',
+ title: $t('page.home.index'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
+```
+
+### Adding a Page Component
+
+In `#/views/home/`, add a new `index.vue` file, as follows:
+
+```vue
+
+
+
home page
+
+
+```
+
+### Verification
+
+At this point, the page has been added. Visit `http://localhost:5555/home/index` to see the corresponding page.
+
+## Route Configuration
+
+The route configuration items are mainly in the `meta` property of the route object. The following are common configuration items:
+
+```ts {5-8}
+const routes = [
+ {
+ name: 'HomeIndex',
+ path: '/home/index',
+ meta: {
+ icon: 'mdi:home',
+ title: $t('page.home.index'),
+ },
+ },
+];
+```
+
+::: details Route Meta Configuration Type Definition
+
+```ts
+interface RouteMeta {
+ /**
+ * Active icon (menu)
+ */
+ activeIcon?: string;
+ /**
+ * The currently active menu, sometimes you don't want to activate the existing menu, use this to activate the parent menu
+ */
+ activePath?: string;
+ /**
+ * Whether to fix the tab
+ * @default false
+ */
+ affixTab?: boolean;
+ /**
+ * The order of fixed tabs
+ * @default 0
+ */
+ affixTabOrder?: number;
+ /**
+ * Specific roles required to access
+ * @default []
+ */
+ authority?: string[];
+ /**
+ * Badge
+ */
+ badge?: string;
+ /**
+ * Badge type
+ */
+ badgeType?: 'dot' | 'normal';
+ /**
+ * Badge color
+ */
+ badgeVariants?:
+ | 'default'
+ | 'destructive'
+ | 'primary'
+ | 'success'
+ | 'warning'
+ | string;
+ /**
+ * The children of the current route are not displayed in the menu
+ * @default false
+ */
+ hideChildrenInMenu?: boolean;
+ /**
+ * The current route is not displayed in the breadcrumb
+ * @default false
+ */
+ hideInBreadcrumb?: boolean;
+ /**
+ * The current route is not displayed in the menu
+ * @default false
+ */
+ hideInMenu?: boolean;
+ /**
+ * The current route is not displayed in the tab
+ * @default false
+ */
+ hideInTab?: boolean;
+ /**
+ * Icon (menu/tab)
+ */
+ icon?: string;
+ /**
+ * iframe address
+ */
+ iframeSrc?: string;
+ /**
+ * Ignore permissions, can be accessed directly
+ * @default false
+ */
+ ignoreAccess?: boolean;
+ /**
+ * Enable KeepAlive cache
+ */
+ keepAlive?: boolean;
+ /**
+ * External link - jump path
+ */
+ link?: string;
+ /**
+ * Whether the route has been loaded
+ */
+ loaded?: boolean;
+ /**
+ * Maximum number of open tabs
+ * @default false
+ */
+ maxNumOfOpenTab?: number;
+ /**
+ * The menu can be seen, but access will be redirected to 403
+ */
+ menuVisibleWithForbidden?: boolean;
+ /**
+ * Open in a new window
+ */
+ openInNewWindow?: boolean;
+ /**
+ * Used for route -> menu sorting
+ */
+ order?: number;
+ /**
+ * Parameters carried by the menu
+ */
+ query?: Recordable;
+ /**
+ * Title name
+ */
+ title: string;
+}
+```
+
+:::
+
+### title
+
+- Type: `string`
+- Default: `''`
+
+Used to configure the title of the page, which will be displayed in the menu and tab. Generally used with internationalization.
+
+### icon
+
+- Type: `string`
+- Default: `''`
+
+Used to configure the icon of the page, which will be displayed in the menu and tab. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
+
+### activeIcon
+
+- Type: `string`
+- Default: `''`
+
+Used to configure the active icon of the page, which will be displayed in the menu. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
+
+### keepAlive
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the page cache is enabled. When enabled, the page will be cached and will not reload, only effective when the tab is enabled.
+
+### hideInMenu
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the page is hidden in the menu. When hidden, the page will not be displayed in the menu.
+
+### hideInTab
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the page is hidden in the tab. When hidden, the page will not be displayed in the tab.
+
+### hideInBreadcrumb
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the page is hidden in the breadcrumb. When hidden, the page will not be displayed in the breadcrumb.
+
+### hideChildrenInMenu
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the subpages of the page are hidden in the menu. When hidden, the subpages will not be displayed in the menu.
+
+### authority
+
+- Type: `string[]`
+- Default: `[]`
+
+Used to configure the permissions of the page. Only users with the corresponding permissions can access the page. If not configured, no permissions are required.
+
+### badge
+
+- Type: `string`
+- Default: `''`
+
+Used to configure the badge of the page, which will be displayed in the menu.
+
+### badgeType
+
+- Type: `'dot' | 'normal'`
+- Default: `'normal'`
+
+Used to configure the badge type of the page. `dot` is a small red dot, `normal` is text.
+
+### badgeVariants
+
+- Type: `'default' | 'destructive' | 'primary' | 'success' | 'warning' | string`
+- Default: `'success'`
+
+Used to configure the badge color of the page.
+
+### activePath
+
+- Type: `string`
+- Default: `''`
+
+Used to configure the currently active menu. Sometimes the page is not displayed in the menu, and this is used to activate the parent menu.
+
+### affixTab
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the page is fixed in the tab. When fixed, the page cannot be closed.
+
+### affixTabOrder
+
+- Type: `number`
+- Default: `0`
+
+Used to configure the order of fixed tabs, sorted in ascending order.
+
+### iframeSrc
+
+- Type: `string`
+- Default: `''`
+
+Used to configure the `iframe` address of the embedded page. When set, the corresponding page will be embedded in the current page.
+
+### ignoreAccess
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the page ignores permissions and can be accessed directly.
+
+### link
+
+- Type: `string`
+- Default: `''`
+
+Used to configure the external link jump path, which will open in a new window.
+
+### maxNumOfOpenTab
+
+- Type: `number`
+- Default: `-1`
+
+Used to configure the maximum number of open tabs. When set, the earliest opened tab will be automatically closed when opening a new tab (only effective when opening tabs with the same name).
+
+### menuVisibleWithForbidden
+
+- Type: `boolean`
+- Default: `false`
+
+Used to configure whether the page can be seen in the menu, but access will be redirected to 403.
+
+### openInNewWindow
+
+- Type: `boolean`
+- Default: `false`
+
+When set to `true`, the page will open in a new window.
+
+### order
+
+- Type: `number`
+- Default: `0`
+
+Used to configure the sorting of the page, used for route to menu sorting.
+
+_Note:_ Sorting is only effective for first-level menus. The sorting of second-level menus needs to be set in the corresponding first-level menu in code order.
+
+### query
+
+- Type: `Recordable`
+- Default: `{}`
+
+Used to configure the menu parameters of the page, which will be passed to the page in the menu.
+
+## Route Refresh
+
+The route refresh method is as follows:
+
+```vue
+
+```
diff --git a/web/docs/src/en/guide/essentials/server.md b/web/docs/src/en/guide/essentials/server.md
new file mode 100644
index 0000000..67e0b1f
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/server.md
@@ -0,0 +1,356 @@
+# Server Interaction and Data Mocking
+
+::: tip Note
+
+This document explains how to use Mock data and interact with the server in a development environment, involving technologies such as:
+
+- [Nitro](https://nitro.unjs.io/) A lightweight backend server that can be deployed anywhere, used as a Mock server in the project.
+- [axios](https://axios-http.com/docs/intro) Used to send HTTP requests to interact with the server.
+
+:::
+
+## Interaction in Development Environment
+
+If the frontend application and the backend API server are not running on the same host, you need to proxy the API requests to the API server in the development environment. If they are on the same host, you can directly request the specific API endpoint.
+
+### Local Development CORS Configuration
+
+::: tip Hint
+
+The CORS configuration for local development has already been set up. If you have other requirements, you can add or adjust the configuration as needed.
+
+:::
+
+#### Configuring Local Development API Endpoint
+
+Configure the API endpoint in the `.env.development` file at the project root directory, here it is set to `/api`:
+
+```bash
+VITE_GLOB_API_URL=/api
+```
+
+#### Configuring Development Server Proxy
+
+In the development environment, if you need to handle CORS, configure the API endpoint in the `vite.config.mts` file under the corresponding application directory:
+
+```ts{8-16}
+// apps/web-antd/vite.config.mts
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ vite: {
+ server: {
+ proxy: {// [!code focus:11]
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // mock proxy
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});
+```
+
+#### API Requests
+
+Based on the above configuration, we can use `/api` as the prefix for API requests in our frontend project, for example:
+
+```ts
+import axios from 'axios';
+
+axios.get('/api/user').then((res) => {
+ console.log(res);
+});
+```
+
+At this point, the request will be proxied to `http://localhost:5320/api/user`.
+
+::: warning Note
+
+From the browser's console Network tab, the request appears as `http://localhost:5555/api/user`. This is because the proxy configuration does not change the local request's URL.
+
+:::
+
+### Configuration Without CORS
+
+If there is no CORS issue, you can directly ignore the [Configure Development Server Proxy](./server.md#configure-development-server-proxy) settings and set the API endpoint directly in `VITE_GLOB_API_URL`.
+
+Configure the API endpoint in the `.env.development` file at the project root directory:
+
+```bash
+VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+```
+
+## Production Environment Interaction
+
+### API Endpoint Configuration
+
+Configure the API endpoint in the `.env.production` file at the project root directory:
+
+```bash
+VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+```
+
+::: tip How to Dynamically Modify API Endpoint in Production
+
+Variables starting with `VITE_GLOB_*` in the `.env` file are injected into the `_app.config.js` file during packaging. After packaging, you can modify the corresponding API addresses in `dist/_app.config.js` and refresh the page to apply the changes. This eliminates the need to package multiple times for different environments, allowing a single package to be deployed across multiple API environments.
+
+:::
+
+### Cross-Origin Resource Sharing (CORS) Handling
+
+In the production environment, if CORS issues arise, you can use `nginx` to proxy the API address or enable `cors` on the backend to handle it (refer to the mock service for examples).
+
+## API Request Configuration
+
+The project comes with a default basic request configuration based on `axios`, provided by the `@vben/request` package. The project does not overly complicate things but simply wraps some common configurations. If there are other requirements, you can add or adjust the configurations as needed. Depending on the app, different component libraries and `store` might be used, so under the `src/api/request.ts` folder in the application directory, there are corresponding request configuration files. For example, in the `web-antd` project, there's a `src/api/request.ts` file where you can configure according to your needs.
+
+### Request Examples
+
+#### GET Request
+
+```ts
+import { requestClient } from '#/api/request';
+
+export async function getUserInfoApi() {
+ return requestClient.get('/user/info');
+}
+```
+
+#### POST/PUT Request
+
+```ts
+import { requestClient } from '#/api/request';
+
+export async function saveUserApi(user: UserInfo) {
+ return requestClient.post('/user', user);
+}
+
+export async function saveUserApi(user: UserInfo) {
+ return requestClient.put('/user', user);
+}
+
+export async function saveUserApi(user: UserInfo) {
+ const url = user.id ? `/user/${user.id}` : '/user/';
+ return requestClient.request(url, {
+ data: user,
+ // OR PUT
+ method: user.id ? 'PUT' : 'POST',
+ });
+}
+```
+
+#### DELETE Request
+
+```ts
+import { requestClient } from '#/api/request';
+
+export async function deleteUserApi(userId: number) {
+ return requestClient.delete(`/user/${userId}`);
+}
+```
+
+### Request Configuration
+
+The `src/api/request.ts` within the application can be configured according to the needs of your application:
+
+```ts
+/**
+ * This file can be adjusted according to business logic
+ */
+import type { HttpResponse } from '@vben/request';
+
+import { useAppConfig } from '@vben/hooks';
+import { preferences } from '@vben/preferences';
+import {
+ authenticateResponseInterceptor,
+ 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) {
+ const client = new RequestClient({
+ baseURL,
+ });
+
+ /**
+ * Re-authentication Logic
+ */
+ 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.setLoginExpired(true);
+ } else {
+ await authStore.logout();
+ }
+ }
+
+ /**
+ * Refresh token Logic
+ */
+ 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;
+ }
+
+ // Request Header Processing
+ client.addRequestInterceptor({
+ fulfilled: async (config) => {
+ const accessStore = useAccessStore();
+
+ config.headers.Authorization = formatToken(accessStore.accessToken);
+ config.headers['Accept-Language'] = preferences.app.locale;
+ return config;
+ },
+ });
+
+ // Deal Response Data
+ client.addResponseInterceptor({
+ fulfilled: (response) => {
+ const { data: responseData, status } = response;
+
+ const { code, data } = responseData;
+
+ if (status >= 200 && status < 400 && code === 0) {
+ return data;
+ }
+ throw Object.assign({}, response, { response });
+ },
+ });
+
+ // Handling Token Expiration
+ client.addResponseInterceptor(
+ authenticateResponseInterceptor({
+ client,
+ doReAuthenticate,
+ doRefreshToken,
+ enableRefreshToken: preferences.app.enableRefreshToken,
+ formatToken,
+ }),
+ );
+
+ // Generic error handling; if none of the above error handling logic is triggered, it will fall back to this.
+ 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);
+
+export const baseRequestClient = new RequestClient({ baseURL: apiURL });
+```
+
+### Multiple API Endpoints
+
+To handle multiple API endpoints, simply create multiple `requestClient` instances, as follows:
+
+```ts
+const { apiURL, otherApiURL } = useAppConfig(
+ import.meta.env,
+ import.meta.env.PROD,
+);
+
+export const requestClient = createRequestClient(apiURL);
+
+export const otherRequestClient = createRequestClient(otherApiURL);
+```
+
+## Refresh Token
+
+The project provides a default logic for refreshing tokens. To enable it, follow the configuration below:
+
+- Ensure the refresh token feature is enabled
+
+Adjust the `preferences.ts` in the corresponding application directory to ensure `enableRefreshToken='true'`.
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ enableRefreshToken: true,
+ },
+});
+```
+
+Configure the `doRefreshToken` method in `src/api/request.ts` as follows:
+
+```ts
+// Adjust this to your token format
+function formatToken(token: null | string) {
+ return token ? `Bearer ${token}` : null;
+}
+
+/**
+ * Refresh token logic
+ */
+async function doRefreshToken() {
+ const accessStore = useAccessStore();
+ // Adjust this to your refresh token API
+ const resp = await refreshTokenApi();
+ const newToken = resp.data;
+ accessStore.setAccessToken(newToken);
+ return newToken;
+}
+```
+
+## Data Mocking
+
+::: tip Production Environment Mock
+
+The new version no longer supports mock in the production environment. Please use real interfaces.
+
+:::
+
+Mock data is an indispensable part of frontend development, serving as a key link in separating frontend and backend development. By agreeing on interfaces with the server side in advance and simulating request data and even logic, frontend development can proceed independently, without being blocked by the backend development process.
+
+The project uses [Nitro](https://nitro.unjs.io/) for local mock data processing. The principle is to start an additional backend service locally, which is a real backend service that can handle requests and return data.
+
+### Using Nitro
+
+The mock service code is located in the `apps/backend-mock` directory. It does not need to be started manually and is already integrated into the project. You only need to run `pnpm dev` in the project root directory. After running successfully, the console will print `http://localhost:5320/api`, and you can access this address to view the mock service.
+
+[Nitro](https://nitro.unjs.io/) syntax is simple, and you can configure and develop according to your needs. For specific configurations, you can refer to the [Nitro documentation](https://nitro.unjs.io/).
+
+## Disabling Mock Service
+
+Since mock is essentially a real backend service, if you do not need the mock service, you can configure `VITE_NITRO_MOCK=false` in the `.env.development` file in the project root directory to disable the mock service.
+
+```bash
+# .env.development
+VITE_NITRO_MOCK=false
+```
diff --git a/web/docs/src/en/guide/essentials/settings.md b/web/docs/src/en/guide/essentials/settings.md
new file mode 100644
index 0000000..59a6c5c
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/settings.md
@@ -0,0 +1,626 @@
+# Configuration
+
+## Environment Variable Configuration
+
+The project's environment variable configuration is located in the application directory under `.env`, `.env.development`, `.env.production`.
+
+The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/guide/env-and-mode.html). The format is as follows:
+
+```bash
+.env # Loaded in all environments
+.env.local # Loaded in all environments, but ignored by git
+.env.[mode] # Only loaded in the specified mode
+.env.[mode].local # Only loaded in the specified mode, but ignored by git
+```
+
+::: tip
+
+- Only variables starting with `VITE_` will be embedded into the client-side package. You can access them in the project code like this:
+
+ ```ts
+ console.log(import.meta.env.VITE_PROT);
+ ```
+
+- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
+
+:::
+
+## Environment Configuration Description
+
+::: code-group
+
+```bash [.env]
+# Application title
+VITE_APP_TITLE=Vben Admin
+
+# Application namespace, used as a prefix for caching, store, etc., to ensure isolation
+VITE_APP_NAMESPACE=vben-web-antd
+```
+
+```bash [.env.development]
+# Port Number
+VITE_PORT=5555
+
+# Public Path for Resources, must start and end with /
+VITE_BASE=/
+
+# API URL
+VITE_GLOB_API_URL=/api
+
+# Whether to enable Nitro Mock service, true to enable, false to disable
+VITE_NITRO_MOCK=true
+
+# Whether to open devtools, true to open, false to close
+VITE_DEVTOOLS=true
+
+# Whether to inject global loading
+VITE_INJECT_APP_LOADING=true
+
+# Whether to generate after packaging dist.zip
+VITE_ARCHIVER=true
+```
+
+```bash [.env.production]
+# Public Path for Resources, must start and end with /
+VITE_BASE=/
+
+# API URL
+VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+
+# Whether to enable compression, can be set to none, brotli, gzip
+VITE_COMPRESS=gzip
+
+# Whether to enable PWA
+VITE_PWA=false
+
+# vue-router mode
+VITE_ROUTER_HISTORY=hash
+
+# Whether to inject global loading
+VITE_INJECT_APP_LOADING=true
+
+# Whether to generate dist.zip after packaging
+VITE_ARCHIVER=true
+```
+
+:::
+
+## Dynamic Configuration in Production Environment
+
+When executing `pnpm build` in the root directory of the monorepo, a `dist/_app.config.js` file will be automatically generated in the corresponding application and inserted into `index.html`.
+
+`_app.config.js` is a dynamic configuration file that allows for modifications to the configuration dynamically based on different environments after the project has been built. The content is as follows:
+
+```ts
+window._VBEN_ADMIN_PRO_APP_CONF_ = {
+ VITE_GLOB_API_URL: 'https://mock-napi.vben.pro/api',
+};
+Object.freeze(window._VBEN_ADMIN_PRO_APP_CONF_);
+Object.defineProperty(window, '_VBEN_ADMIN_PRO_APP_CONF_', {
+ configurable: false,
+ writable: false,
+});
+```
+
+### Purpose
+
+`_app.config.js` is used for projects that need to dynamically modify configurations after packaging, such as API endpoints. There's no need to repackage; you can simply modify the variables in `/dist/_app.config.js` after packaging, and refresh to update the variables in the code. A `js` file is used to ensure that the configuration file is loaded early in the order.
+
+### Usage
+
+To access the variables inside `_app.config.js`, you need to use the `useAppConfig` method provided by `@vben/hooks`.
+
+```ts
+const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+```
+
+### Adding New
+
+To add a new dynamically modifiable configuration item, simply follow the steps below:
+
+- First, add the variable that needs to be dynamically configurable in the `.env` file or the corresponding development environment configuration file. The variable must start with `VITE_GLOB_*`, for example:
+
+ ```bash
+ VITE_GLOB_OTHER_API_URL=https://mock-napi.vben.pro/other-api
+ ```
+
+- In `packages/types/global.d.ts`, add the corresponding type definition, such as:
+
+ ```ts
+ export interface VbenAdminProAppConfigRaw {
+ VITE_GLOB_API_URL: string;
+ VITE_GLOB_OTHER_API_URL: string; // [!code ++]
+ }
+
+ export interface ApplicationConfig {
+ apiURL: string;
+ otherApiURL: string; // [!code ++]
+ }
+ ```
+
+- In `packages/effects/hooks/src/use-app-config.ts`, add the corresponding configuration item, such as:
+
+ ```ts
+ export function useAppConfig(
+ env: Record,
+ isProduction: boolean,
+ ): ApplicationConfig {
+ // In production environment, directly use the window._VBEN_ADMIN_PRO_APP_CONF_ global variable
+ const config = isProduction
+ ? window._VBEN_ADMIN_PRO_APP_CONF_
+ : (env as VbenAdminProAppConfigRaw);
+
+ const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
+
+ return {
+ apiURL: VITE_GLOB_API_URL,
+ otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
+ };
+ }
+ ```
+
+At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item.
+
+```ts
+const { otherApiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+```
+
+::: warning Warning
+
+The `useAppConfig` method should only be used within the application and not be coupled with the internals of a package. The reason for passing `import.meta.env` and `import.meta.env.PROD` is to avoid such coupling. A pure package should avoid using variables specific to a particular build tool.
+
+:::
+
+## Preferences
+
+The project offers a wide range of preference settings for dynamically configuring various features of the project:
+
+
+
+If you cannot find documentation for a setting, you can try configuring it yourself and then click `Copy Preferences` to override the project defaults. The configuration file is located in the application directory under `preferences.ts`, where you can override the framework's default configurations to achieve custom settings.
+
+```ts
+import { useAppConfig } from '@vben/hooks';
+import { defineOverridesPreferences } from '@vben/preferences';
+
+/**
+ * @description Project configuration file
+ * Only a part of the configuration in the project needs to be covered, and unnecessary configurations do not need to be covered. The default configuration will be automatically used
+ * !!! Please clear the cache after changing the configuration, otherwise it may not take effect
+ */
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+});
+```
+
+### Framework default configuration
+
+::: details View the default configuration of the framework
+
+```ts
+const defaultPreferences: Preferences = {
+ app: {
+ accessMode: 'frontend',
+ authPageLayout: 'panel-right',
+ checkUpdatesInterval: 1,
+ colorGrayMode: false,
+ colorWeakMode: false,
+ compact: false,
+ contentCompact: 'wide',
+ contentCompactWidth: 1200,
+ contentPadding: 0,
+ contentPaddingBottom: 0,
+ contentPaddingLeft: 0,
+ contentPaddingRight: 0,
+ contentPaddingTop: 0,
+ defaultAvatar:
+ 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
+ defaultHomePath: '/analytics',
+ dynamicTitle: true,
+ enableCheckUpdates: true,
+ enablePreferences: true,
+ enableRefreshToken: false,
+ isMobile: false,
+ layout: 'sidebar-nav',
+ locale: 'zh-CN',
+ loginExpiredMode: 'page',
+ name: 'Vben Admin',
+ preferencesButtonPosition: 'auto',
+ watermark: false,
+ zIndex: 200,
+ },
+ breadcrumb: {
+ enable: true,
+ hideOnlyOne: false,
+ showHome: false,
+ showIcon: true,
+ styleType: 'normal',
+ },
+ copyright: {
+ companyName: 'Vben',
+ companySiteLink: 'https://www.vben.pro',
+ date: '2024',
+ enable: true,
+ icp: '',
+ icpLink: '',
+ settingShow: true,
+ },
+ footer: {
+ enable: false,
+ fixed: false,
+ height: 32,
+ },
+ header: {
+ enable: true,
+ height: 50,
+ hidden: false,
+ menuAlign: 'start',
+ mode: 'fixed',
+ },
+ logo: {
+ enable: true,
+ fit: 'contain',
+ source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
+ },
+ navigation: {
+ accordion: true,
+ split: true,
+ styleType: 'rounded',
+ },
+ shortcutKeys: {
+ enable: true,
+ globalLockScreen: true,
+ globalLogout: true,
+ globalPreferences: true,
+ globalSearch: true,
+ },
+ sidebar: {
+ autoActivateChild: false,
+ collapsed: false,
+ collapsedButton: true,
+ collapsedShowTitle: false,
+ collapseWidth: 60,
+ enable: true,
+ expandOnHover: true,
+ extraCollapse: false,
+ extraCollapsedWidth: 60,
+ fixedButton: true,
+ hidden: false,
+ mixedWidth: 80,
+ width: 224,
+ },
+ tabbar: {
+ draggable: true,
+ enable: true,
+ height: 38,
+ keepAlive: true,
+ maxCount: 0,
+ middleClickToClose: false,
+ persist: true,
+ showIcon: true,
+ showMaximize: true,
+ showMore: true,
+ styleType: 'chrome',
+ wheelable: true,
+ },
+ theme: {
+ builtinType: 'default',
+ colorDestructive: 'hsl(348 100% 61%)',
+ colorPrimary: 'hsl(212 100% 45%)',
+ colorSuccess: 'hsl(144 57% 58%)',
+ colorWarning: 'hsl(42 84% 61%)',
+ mode: 'dark',
+ radius: '0.5',
+ semiDarkHeader: false,
+ semiDarkSidebar: false,
+ },
+ transition: {
+ enable: true,
+ loading: true,
+ name: 'fade-slide',
+ progress: true,
+ },
+ widget: {
+ fullscreen: true,
+ globalSearch: true,
+ languageToggle: true,
+ lockScreen: true,
+ notification: true,
+ refresh: true,
+ sidebarToggle: true,
+ themeToggle: true,
+ },
+};
+```
+
+:::
+
+::: details View the default configuration type of the framework
+
+```ts
+interface AppPreferences {
+ /** Permission mode */
+ accessMode: AccessModeType;
+ /** Layout of the login/registration page */
+ authPageLayout: AuthPageLayoutType;
+ /** Interval for checking updates */
+ checkUpdatesInterval: number;
+ /** Whether to enable gray mode */
+ colorGrayMode: boolean;
+ /** Whether to enable color weakness mode */
+ colorWeakMode: boolean;
+ /** Whether to enable compact mode */
+ compact: boolean;
+ /** Whether to enable content compact mode */
+ contentCompact: ContentCompactType;
+ /** Content compact width */
+ contentCompactWidth: number;
+ /** Content padding */
+ contentPadding: number;
+ /** Content bottom padding */
+ contentPaddingBottom: number;
+ /** Content left padding */
+ contentPaddingLeft: number;
+ /** Content right padding */
+ contentPaddingRight: number;
+ /** Content top padding */
+ contentPaddingTop: number;
+ // /** Default application avatar */
+ defaultAvatar: string;
+ /** Default homepage path */
+ defaultHomePath: string;
+ // /** Enable dynamic title */
+ dynamicTitle: boolean;
+ /** Whether to enable update checks */
+ enableCheckUpdates: boolean;
+ /** Whether to display preferences */
+ enablePreferences: boolean;
+ /**
+ * @zh_CN Whether to enable refreshToken
+ */
+ enableRefreshToken: boolean;
+ /** Whether it's mobile */
+ isMobile: boolean;
+ /** Layout method */
+ layout: LayoutType;
+ /** Supported languages */
+ locale: SupportedLanguagesType;
+ /** Login expiration mode */
+ loginExpiredMode: LoginExpiredModeType;
+ /** Application name */
+ name: string;
+ /** Position of the preferences button */
+ preferencesButtonPosition: PreferencesButtonPositionType;
+ /**
+ * @zh_CN Whether to enable watermark
+ */
+ watermark: boolean;
+ /** z-index */
+ zIndex: number;
+}
+interface BreadcrumbPreferences {
+ /** Whether breadcrumbs are enabled */
+ enable: boolean;
+ /** Whether to hide breadcrumbs when there is only one */
+ hideOnlyOne: boolean;
+ /** Whether the home icon in breadcrumbs is visible */
+ showHome: boolean;
+ /** Whether the icon in breadcrumbs is visible */
+ showIcon: boolean;
+ /** Breadcrumb style */
+ styleType: BreadcrumbStyleType;
+}
+
+interface CopyrightPreferences {
+ /** Copyright company name */
+ companyName: string;
+ /** Link to the copyright company's site */
+ companySiteLink: string;
+ /** Copyright date */
+ date: string;
+ /** Whether copyright is visible */
+ enable: boolean;
+ /** ICP number */
+ icp: string;
+ /** Link to the ICP */
+ icpLink: string;
+ /** Whether to show in settings panel */
+ settingShow?: boolean;
+}
+
+interface FooterPreferences {
+ /** Whether the footer is visible */
+ enable: boolean;
+ /** Whether the footer is fixed */
+ fixed: boolean;
+ /** Footer height */
+ height: number;
+}
+
+interface HeaderPreferences {
+ /** Whether the header is enabled */
+ enable: boolean;
+ /** Header height */
+ height: number;
+ /** Whether the header is hidden, css-hidden */
+ hidden: boolean;
+ /** Header menu alignment */
+ menuAlign: LayoutHeaderMenuAlignType;
+ /** Header display mode */
+ mode: LayoutHeaderModeType;
+}
+
+interface LogoPreferences {
+ /** Whether the logo is visible */
+ enable: boolean;
+ /** Logo image fitting method */
+ fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
+ /** Logo URL */
+ source: string;
+}
+
+interface NavigationPreferences {
+ /** Navigation menu accordion mode */
+ accordion: boolean;
+ /** Whether the navigation menu is split, only effective in layout=mixed-nav */
+ split: boolean;
+ /** Navigation menu style */
+ styleType: NavigationStyleType;
+}
+interface SidebarPreferences {
+ /** Automatically activate child menu when clicking on directory */
+ autoActivateChild: boolean;
+ /** Whether the sidebar is collapsed */
+ collapsed: boolean;
+ /** Whether the sidebar collapse button is visible */
+ collapsedButton: boolean;
+ /** Whether to show title when sidebar is collapsed */
+ collapsedShowTitle: boolean;
+ /** Sidebar collapse width */
+ collapseWidth: number;
+ /** Whether the sidebar is visible */
+ enable: boolean;
+ /** Menu auto-expand state */
+ expandOnHover: boolean;
+ /** Whether the sidebar extension area is collapsed */
+ extraCollapse: boolean;
+ /** Sidebar extension area collapse width */
+ extraCollapsedWidth: number;
+ /** Whether the sidebar fixed button is visible */
+ fixedButton: boolean;
+ /** Whether the sidebar is hidden - css */
+ hidden: boolean;
+ /** Mixed sidebar width */
+ mixedWidth: number;
+ /** Sidebar width */
+ width: number;
+}
+
+interface ShortcutKeyPreferences {
+ /** Whether shortcut keys are enabled globally */
+ enable: boolean;
+ /** Whether the global lock screen shortcut is enabled */
+ globalLockScreen: boolean;
+ /** Whether the global logout shortcut is enabled */
+ globalLogout: boolean;
+ /** Whether the global preferences shortcut is enabled */
+ globalPreferences: boolean;
+ /** Whether the global search shortcut is enabled */
+ globalSearch: boolean;
+}
+
+interface TabbarPreferences {
+ /** Whether dragging of multiple tabs is enabled */
+ draggable: boolean;
+ /** Whether multiple tabs are enabled */
+ enable: boolean;
+ /** Tab height */
+ height: number;
+ /** Whether tab caching is enabled */
+ keepAlive: boolean;
+ /** Maximum number of tabs */
+ maxCount: number;
+ /** Whether to close tab when middle-clicked */
+ middleClickToClose: boolean;
+ /** Whether tabs are persistent */
+ persist: boolean;
+ /** Whether icons in multiple tabs are enabled */
+ showIcon: boolean;
+ /** Whether to show the maximize button */
+ showMaximize: boolean;
+ /** Whether to show the more button */
+ showMore: boolean;
+ /** Tab style */
+ styleType: TabsStyleType;
+ /** Whether mouse wheel response is enabled */
+ wheelable: boolean;
+}
+interface ThemePreferences {
+ /** Built-in theme name */
+ builtinType: BuiltinThemeType;
+ /** Destructive color */
+ colorDestructive: string;
+ /** Primary color */
+ colorPrimary: string;
+ /** Success color */
+ colorSuccess: string;
+ /** Warning color */
+ colorWarning: string;
+ /** Current theme */
+ mode: ThemeModeType;
+ /** Radius */
+ radius: string;
+ /** Whether to enable semi-dark header (only effective when theme='light') */
+ semiDarkHeader: boolean;
+ /** Whether to enable semi-dark sidebar (only effective when theme='light') */
+ semiDarkSidebar: boolean;
+}
+
+interface TransitionPreferences {
+ /** Whether page transition animations are enabled */
+ enable: boolean;
+ // /** Whether page loading loading is enabled */
+ loading: boolean;
+ /** Page transition animation */
+ name: PageTransitionType | string;
+ /** Whether page loading progress animation is enabled */
+ progress: boolean;
+}
+
+interface WidgetPreferences {
+ /** Whether fullscreen widgets are enabled */
+ fullscreen: boolean;
+ /** Whether global search widget is enabled */
+ globalSearch: boolean;
+ /** Whether language switch widget is enabled */
+ languageToggle: boolean;
+ /** Whether lock screen functionality is enabled */
+ lockScreen: boolean;
+ /** Whether notification widget is displayed */
+ notification: boolean;
+ /** Whether to show the refresh button */
+ refresh: boolean;
+ /** Whether sidebar show/hide widget is displayed */
+ sidebarToggle: boolean;
+ /** Whether theme switch widget is displayed */
+ themeToggle: boolean;
+}
+interface Preferences {
+ /** Global configuration */
+ app: AppPreferences;
+ /** Header configuration */
+ breadcrumb: BreadcrumbPreferences;
+ /** Copyright configuration */
+ copyright: CopyrightPreferences;
+ /** Footer configuration */
+ footer: FooterPreferences;
+ /** Breadcrumb configuration */
+ header: HeaderPreferences;
+ /** Logo configuration */
+ logo: LogoPreferences;
+ /** Navigation configuration */
+ navigation: NavigationPreferences;
+ /** Shortcut key configuration */
+ shortcutKeys: ShortcutKeyPreferences;
+ /** Sidebar configuration */
+ sidebar: SidebarPreferences;
+ /** Tab bar configuration */
+ tabbar: TabbarPreferences;
+ /** Theme configuration */
+ theme: ThemePreferences;
+ /** Animation configuration */
+ transition: TransitionPreferences;
+ /** Widget configuration */
+ widget: WidgetPreferences;
+}
+```
+
+:::
+
+::: warning Warning
+
+- The `overridesPreferences` method only needs to override a part of the configurations in the project. There's no need to override configurations that are not needed; they will automatically use the default settings.
+- Any configuration item can be overridden. You just need to override it within the `overridesPreferences` method. Do not modify the default configuration file.
+- Please clear the cache after changing the configuration, otherwise it may not take effect.
+
+:::
diff --git a/web/docs/src/en/guide/essentials/styles.md b/web/docs/src/en/guide/essentials/styles.md
new file mode 100644
index 0000000..16f6681
--- /dev/null
+++ b/web/docs/src/en/guide/essentials/styles.md
@@ -0,0 +1,106 @@
+# Styles
+
+::: tip Preface
+
+For Vue projects, the [official documentation](https://vuejs.org/api/sfc-css-features.html#deep-selectors) already provides a detailed introduction to the syntax. Here, we mainly introduce the structure and usage of style files in the project.
+
+:::
+
+## Project Structure
+
+The style files in the project are stored in `@vben/styles`, which includes some global styles, such as reset styles, global variables, etc. It inherits the styles and capabilities of `@vben-core/design` and can be overridden according to project needs.
+
+## Scss
+
+The project uses `scss` as the style preprocessor, allowing the use of `scss` features such as variables, functions, mixins, etc., within the project.
+
+```vue
+
+```
+
+## Postcss
+
+If you're not accustomed to using `scss`, you can also use `postcss`, which is a more powerful style processor that supports a wider range of plugins. The project includes the [postcss-nested](https://github.com/postcss/postcss-nested) plugin and is configured with `Css Variables`, making it a complete substitute for `scss`.
+
+```vue
+
+```
+
+## Tailwind CSS
+
+The project integrates [Tailwind CSS](https://tailwindcss.com/), allowing the use of `tailwindcss` class names to quickly build pages.
+
+```vue
+
+
+
+```
+
+## BEM Standard
+
+Another option to avoid style conflicts is to use the `BEM` standard. If you choose `scss`, it is recommended to use the `BEM` naming convention for better style management. The project provides a default `useNamespace` function to easily generate namespaces.
+
+```vue
+
+
+
+
+
+```
+
+## CSS Modules
+
+Another solution to address style conflicts is to use the `CSS Modules` modular approach. The usage method is as follows.
+
+```vue
+
+ This should be red
+
+
+
+```
+
+For more usage, see the [CSS Modules official documentation](https://vuejs.org/api/sfc-css-features.html#css-modules).
diff --git a/web/docs/src/en/guide/in-depth/access.md b/web/docs/src/en/guide/in-depth/access.md
new file mode 100644
index 0000000..545ddda
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/access.md
@@ -0,0 +1,356 @@
+---
+outline: deep
+---
+
+# Access Control
+
+The framework has built-in three types of access control methods:
+
+- Determining whether a menu or button can be accessed based on user roles
+- Determining whether a menu or button can be accessed through an API
+- Mixed mode: Using both frontend and backend access control simultaneously
+
+## Frontend Access Control
+
+**Implementation Principle**: The permissions for routes are hardcoded on the frontend, specifying which permissions are required to view certain routes. Only general routes are initialized, and routes that require permissions are not added to the route table. After logging in or obtaining user roles through other means, the roles are used to traverse the route table to generate a route table that the role can access. This table is then added to the router instance using `router.addRoute`, achieving permission filtering.
+
+**Disadvantage**: The permissions are relatively inflexible; if the backend changes roles, the frontend needs to be adjusted accordingly. This is suitable for systems with relatively fixed roles.
+
+### Steps
+
+- Ensure the current mode is set to frontend access control
+
+Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='frontend'`.
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ // Default value, optional
+ accessMode: 'frontend',
+ },
+});
+```
+
+- Configure route permissions
+
+#### If not configured, it is visible by default
+
+```ts {3}
+ {
+ meta: {
+ authority: ['super'],
+ },
+},
+```
+
+- Ensure the roles returned by the interface match the permissions in the route table
+
+You can look under `src/store/auth` in the application to find the following code:
+
+```ts
+// Set the login user information, ensuring that userInfo.roles is an array and contains permissions from the route table
+// For example: userInfo.roles=['super', 'admin']
+authStore.setUserInfo(userInfo);
+```
+
+At this point, the configuration is complete. You need to ensure that the roles returned by the interface after login match the permissions in the route table; otherwise, access will not be possible.
+
+### Menu Visible but Access Forbidden
+
+Sometimes, we need the menu to be visible but access to it forbidden. This can be achieved by setting `menuVisibleWithForbidden` to `true`. In this case, the menu will be visible, but access will be forbidden, redirecting to a 403 page.
+
+```ts
+{
+ meta: {
+ menuVisibleWithForbidden: true,
+ },
+},
+```
+
+## Backend Access Control
+
+**Implementation Principle**: It is achieved by dynamically generating a routing table through an API, which returns data following a certain structure. The frontend processes this data into a recognizable structure, then adds it to the routing instance using `router.addRoute`, realizing the dynamic generation of permissions.
+
+**Disadvantage**: The backend needs to provide a data structure that meets the standards, and the frontend needs to process this structure. This is suitable for systems with more complex permissions.
+
+### Steps
+
+- Ensure the current mode is set to backend access control
+
+Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='backend'`.
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ accessMode: 'backend',
+ },
+});
+```
+
+- Ensure the structure of the menu data returned by the interface is correct
+
+You can look under `src/router/access.ts` in the application to find the following code:
+
+```ts
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ return await generateAccessible(preferences.app.accessMode, {
+ fetchMenuListAsync: async () => {
+ // This interface is for the menu data returned by the backend
+ return await getAllMenus();
+ },
+ });
+}
+```
+
+- Interface returns menu data, see comments for explanation
+
+::: details Example of Interface Returning Menu Data
+
+```ts
+const dashboardMenus = [
+ {
+ // Here, 'BasicLayout' is hardcoded and cannot be changed
+ component: 'BasicLayout',
+ meta: {
+ order: -1,
+ title: 'page.dashboard.title',
+ },
+ name: 'Dashboard',
+ path: '/',
+ redirect: '/analytics',
+ children: [
+ {
+ name: 'Analytics',
+ path: '/analytics',
+ // Here is the path of the page, need to remove 'views/' and '.vue'
+ component: '/dashboard/analytics/index',
+ meta: {
+ affixTab: true,
+ title: 'page.dashboard.analytics',
+ },
+ },
+ {
+ name: 'Workspace',
+ path: '/workspace',
+ component: '/dashboard/workspace/index',
+ meta: {
+ title: 'page.dashboard.workspace',
+ },
+ },
+ ],
+ },
+];
+```
+
+:::
+
+At this point, the configuration is complete. You need to ensure that after logging in, the format of the menu returned by the interface is correct; otherwise, access will not be possible.
+
+## Mixed Access Control
+
+**Implementation Principle**: Mixed mode combines both frontend access control and backend access control methods. The system processes frontend fixed route permissions and backend dynamic menu data in parallel, ultimately merging both parts of routes to provide a more flexible access control solution.
+
+**Advantages**: Combines the performance advantages of frontend control with the flexibility of backend control, suitable for complex business scenarios requiring permission management.
+
+### Steps
+
+- Ensure the current mode is set to mixed access control
+
+Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='mixed'`.
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ accessMode: 'mixed',
+ },
+});
+```
+
+- Configure frontend route permissions
+
+Same as the route permission configuration method in [Frontend Access Control](#frontend-access-control) mode.
+
+- Configure backend menu interface
+
+Same as the interface configuration method in [Backend Access Control](#backend-access-control) mode.
+
+- Ensure roles and permissions match
+
+Must satisfy both frontend route permission configuration and backend menu data return requirements, ensuring user roles match the permission configurations of both modes.
+
+At this point, the configuration is complete. Mixed mode will automatically merge frontend and backend routes, providing complete access control functionality.
+
+## Fine-grained Control of Buttons
+
+In some cases, we need to control the display of buttons with fine granularity. We can control the display of buttons through interfaces or roles.
+
+### Permission Code
+
+The permission code is the code returned by the interface. The logic to determine whether a button is displayed is located under `src/store/auth`:
+
+```ts
+const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ fetchUserInfo(),
+ getAccessCodes(),
+]);
+
+userInfo = fetchUserInfoResult;
+authStore.setUserInfo(userInfo);
+accessStore.setAccessCodes(accessCodes);
+```
+
+Locate the `getAccessCodes` corresponding interface, which can be adjusted according to business logic.
+
+The data structure returned by the permission code is an array of strings, for example: `['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010']`
+
+With the permission codes, you can use the `AccessControl` component and API provided by `@vben/access` to show and hide buttons.
+
+#### Component Method
+
+```vue
+
+
+
+
+
+ Visible to Super account ["AC_1000001"]
+
+
+ Visible to Admin account ["AC_100010"]
+
+
+ Visible to User account ["AC_1000001"]
+
+
+
+ Visible to Super & Admin account ["AC_100100","AC_1000001"]
+
+
+
+```
+
+#### API Method
+
+```vue
+
+
+
+
+ Visible to Super account ["AC_1000001"]
+
+
+ Visible to Admin account ["AC_100010"]
+
+
+ Visible to User account ["AC_1000001"]
+
+
+ Visible to Super & Admin account ["AC_100100","AC_1000001"]
+
+
+```
+
+#### Directive Method
+
+> The directive supports binding single or multiple permission codes. For a single one, you can pass a string or an array containing one permission code, and for multiple permission codes, you can pass an array.
+
+```vue
+
+
+ Visible to Super account 'AC_100100'
+
+
+ Visible to Admin account ["AC_100010"]
+
+
+ Visible to User account ["AC_1000001"]
+
+
+ Visible to Super & Admin account ["AC_100100","AC_1000001"]
+
+
+```
+
+### Roles
+
+The method of determining roles does not require permission codes returned by the interface; it directly determines whether buttons are displayed based on roles.
+
+#### Component Method
+
+```vue
+
+
+
+
+ Visible to Super account
+
+
+ Visible to Admin account
+
+
+ Visible to User account
+
+
+ Super & Visible to Admin account
+
+
+```
+
+#### API Method
+
+```vue
+
+
+
+ Visible to Super account
+ Visible to Admin account
+ Visible to User account
+
+ Super & Visible to Admin account
+
+
+```
+
+#### Directive Method
+
+> The directive supports binding single or multiple permission codes. For a single one, you can pass a string or an array containing one permission code, and for multiple permission codes, you can pass an array.
+
+```vue
+
+
+ Visible to Super account
+
+
+ Visible to Admin account
+
+
+ Visible to User account
+
+
+ Super & Visible to Admin account
+
+
+```
diff --git a/web/docs/src/en/guide/in-depth/check-updates.md b/web/docs/src/en/guide/in-depth/check-updates.md
new file mode 100644
index 0000000..1e5b679
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/check-updates.md
@@ -0,0 +1,48 @@
+# Check Updates
+
+## Introduction
+
+When there are updates to the website, you might need to check for updates. The framework provides this functionality. By periodically checking for updates, you can configure the `checkUpdatesInterval` and `enableCheckUpdates` fields in your application's preferences.ts file to enable and set the interval for checking updates (in minutes).
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ // Whether to enable check for updates
+ enableCheckUpdates: true,
+ // The interval for checking updates, in minutes
+ checkUpdatesInterval: 1,
+ },
+});
+```
+
+## Effect
+
+When an update is detected, a prompt will pop up asking the user whether to refresh the page:
+
+
+
+## Replacing with Other Update Checking Methods
+
+If you need to check for updates in other ways, such as through an API to more flexibly control the update logic (such as force refresh, display update content, etc.), you can do so by modifying the `src/widgets/check-updates/check-updates.vue` file under `@vben/layouts`.
+
+```ts
+// Replace this with your update checking logic
+async function getVersionTag() {
+ try {
+ const response = await fetch('/', {
+ cache: 'no-cache',
+ method: 'HEAD',
+ });
+
+ return (
+ response.headers.get('etag') || response.headers.get('last-modified')
+ );
+ } catch {
+ console.error('Failed to fetch version tag');
+ return null;
+ }
+}
+```
diff --git a/web/docs/src/en/guide/in-depth/features.md b/web/docs/src/en/guide/in-depth/features.md
new file mode 100644
index 0000000..24fecea
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/features.md
@@ -0,0 +1,84 @@
+# Common Features
+
+A collection of some commonly used features.
+
+## Login Authentication Expiry
+
+When the interface returns a `401` status code, the framework will consider the login authentication to have expired. Upon login timeout, it will redirect to the login page or open a login popup. This can be configured in `preferences.ts` in the application directory:
+
+### Redirect to Login Page
+
+Upon login timeout, it will redirect to the login page.
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ loginExpiredMode: 'page',
+ },
+});
+```
+
+### Open Login Popup
+
+When login times out, a login popup will open.
+
+
+
+Configuration:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ loginExpiredMode: 'modal',
+ },
+});
+```
+
+## Dynamic Title
+
+- Default value: `true`
+
+When enabled, the webpage title changes according to the route's `title`. You can enable or disable this in the `preferences.ts` file in your application directory.
+
+```ts
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ dynamicTitle: true,
+ },
+});
+```
+
+## Page Watermark
+
+- Default value: `false`
+
+When enabled, the webpage will display a watermark. You can enable or disable this in the `preferences.ts` file in your application directory.
+
+```ts
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ watermark: true,
+ },
+});
+```
+
+If you want to update the content of the watermark, you can do so. The parameters can be referred to [watermark-js-plus](https://zhensherlock.github.io/watermark-js-plus/):
+
+```ts
+import { useWatermark } from '@vben/hooks';
+
+const { destroyWatermark, updateWatermark } = useWatermark();
+
+await updateWatermark({
+ // watermark content
+ content: 'hello my watermark',
+});
+```
diff --git a/web/docs/src/en/guide/in-depth/layout.md b/web/docs/src/en/guide/in-depth/layout.md
new file mode 100644
index 0000000..412c1cf
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/layout.md
@@ -0,0 +1 @@
+# Layout
diff --git a/web/docs/src/en/guide/in-depth/loading.md b/web/docs/src/en/guide/in-depth/loading.md
new file mode 100644
index 0000000..0f1cff6
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/loading.md
@@ -0,0 +1,44 @@
+# Global Loading
+
+Global loading refers to the loading effect that appears when the page is refreshed, usually a spinning icon:
+
+
+
+## Principle
+
+Implemented by the `vite-plugin-inject-app-loading` plugin, the plugin injects a global `loading html` into each application.
+
+## Disable
+
+If you do not need global loading, you can disable it in the `.env` file:
+
+```bash
+VITE_INJECT_APP_LOADING=false
+```
+
+## Customization
+
+If you want to customize the global loading, you can create a `loading.html` file in the application directory, at the same level as `index.html`. The plugin will automatically read and inject this HTML. You can define the style and animation of this HTML as you wish.
+
+::: tip
+
+- You can use the same syntax as in `index.html`, such as the `VITE_APP_TITLE` variable, to get the application's title.
+- You must ensure there is an element with `id="__app-loading__"`.
+- Add a `hidden` class to the element with `id="__app-loading__"`.
+- You must ensure there is a `style[data-app-loading="inject-css"]` element.
+
+```html{1,4}
+
+
+
+
<%= VITE_APP_TITLE %>
+
+```
diff --git a/web/docs/src/en/guide/in-depth/locale.md b/web/docs/src/en/guide/in-depth/locale.md
new file mode 100644
index 0000000..549bc83
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/locale.md
@@ -0,0 +1,227 @@
+# Internationalization
+
+The project has integrated [Vue i18n](https://kazupon.github.io/vue-i18n/), and Chinese and English language packs have been configured.
+
+## IDE Plugin
+
+If you are using vscode as your development tool, it is recommended to install the [i18n Ally](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) plugin. It can help you manage internationalization copy more conveniently. After installing this plugin, you can see the corresponding language content in your code in real-time:
+
+
+
+## Configure Default Language
+
+You just need to override the default preferences. In the corresponding application, find the `src/preferences.ts` file and modify the value of `locale`:
+
+```ts {3}
+export const overridesPreferences = defineOverridesPreferences({
+ app: {
+ locale: 'en-US',
+ },
+});
+```
+
+## Dynamic Language Switching
+
+Switching languages consists of two parts:
+
+- Updating preferences
+- Loading the corresponding language pack
+
+```ts
+import type { SupportedLanguagesType } from '@vben/locales';
+import { loadLocaleMessages } from '@vben/locales';
+import { updatePreferences } from '@vben/preferences';
+
+async function updateLocale(value: string) {
+ // 1. Update preferences
+ const locale = value as SupportedLanguagesType;
+ updatePreferences({
+ app: {
+ locale,
+ },
+ });
+ // 2. Load the corresponding language pack
+ await loadLocaleMessages(locale);
+}
+
+updateLocale('en-US');
+```
+
+## Adding Translation Texts
+
+::: warning Attention
+
+- Do not place business translation texts inside `@vben/locales` to better manage business and general translation texts.
+- When adding new translation texts and multiple language packs are available, ensure to add the corresponding texts in all language packs.
+
+:::
+
+To add new translation texts, simply find `src/locales/langs/` in the corresponding application and add the texts accordingly, for example:
+
+**src/locales/langs/zh-CN/\*.json**
+
+````ts
+```json
+{
+ "about": {
+ "desc": "Vben Admin 是一个现代的管理模版。"
+ }
+}
+````
+
+**src/locales/langs/en-US.ts**
+
+````ts
+```json
+{
+ "about": {
+ "desc": "Vben Admin is a modern management template."
+ }
+}
+````
+
+## Using Translation Texts
+
+With `@vben/locales`, you can easily use translation texts:
+
+### In Code
+
+```vue
+
+
+ {{ $t('about.desc') }}
+
+ {{ item.title }}
+
+
+```
+
+## Adding a New Language Pack
+
+If you need to add a new language pack, follow these steps:
+
+- Add the corresponding language pack file in the `packages/locales/langs` directory, for example, `zh-TW.json`, and translate the respective texts.
+- In the corresponding application, locate the `src/locales/langs` file and add the new language pack `zh-TW.json`.
+- Add the corresponding language in `packages/constants/src/core.ts`:
+
+ ```ts
+ export interface LanguageOption {
+ label: string;
+ value: 'en-US' | 'zh-CN'; // [!code --]
+ value: 'en-US' | 'zh-CN' | 'zh-TW'; // [!code ++]
+ }
+ export const SUPPORT_LANGUAGES: LanguageOption[] = [
+ {
+ label: '简体中文',
+ value: 'zh-CN',
+ },
+ {
+ label: 'English',
+ value: 'en-US',
+ },
+ {
+ label: '繁体中文', // [!code ++]
+ value: 'zh-TW', // [!code ++]
+ },
+ ];
+ ```
+
+- In `packages/locales/typing.ts`, add a new TypeScript type:
+
+ ```ts
+ export type SupportedLanguagesType = 'en-US' | 'zh-CN'; // [!code --]
+ export type SupportedLanguagesType = 'en-US' | 'zh-CN' | 'zh-TW'; // [!code ++]
+ ```
+
+At this point, you can use the newly added language pack in the project.
+
+## Interface Language Switching Function
+
+If you want to disable the language switching display button on the interface, in the corresponding application, find the `src/preferences.ts` file and modify the value of `locale` accordingly:
+
+```ts {3}
+export const overridesPreferences = defineOverridesPreferences({
+ widget: {
+ languageToggle: false,
+ },
+});
+```
+
+## Remote Loading of Language Packs
+
+::: tip Tip
+
+When making interface requests through the project's built-in `request` tool, the default request header will include [Accept-Language](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language), allowing the server to dynamically internationalize data based on the request header.
+
+:::
+
+Each application has an independent language pack that can override the general language configuration. You can remotely load the corresponding language pack by finding the `src/locales/index.ts` file in the corresponding application and modifying the `loadMessages` method accordingly:
+
+```ts {3-4}
+async function loadMessages(lang: SupportedLanguagesType) {
+ const [appLocaleMessages] = await Promise.all([
+ // Modify here to load data via a remote interface
+ localesMap[lang](),
+ loadThirdPartyMessage(lang),
+ ]);
+ return appLocaleMessages.default;
+}
+```
+
+## Third-Party Language Packs
+
+Different applications may use third-party component libraries or plugins with varying internationalization methods, so they need to be handled differently. If you need to introduce a third-party language pack, you can find the `src/locales/index.ts` file in the corresponding application and modify the `loadThirdPartyMessage` method accordingly:
+
+```ts
+/**
+ * Load the dayjs language pack
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+ let locale;
+ switch (lang) {
+ case 'zh-CN': {
+ locale = await import('dayjs/locale/zh-cn');
+ break;
+ }
+ case 'en-US': {
+ locale = await import('dayjs/locale/en');
+ break;
+ }
+ // Default to using English
+ default: {
+ locale = await import('dayjs/locale/en');
+ }
+ }
+ if (locale) {
+ dayjs.locale(locale);
+ } else {
+ console.error(`Failed to load dayjs locale for ${lang}`);
+ }
+}
+```
+
+## Removing Internationalization
+
+Firstly, it is not recommended to remove internationalization, as it is a good development practice. However, if you really need to remove it, you can directly use Chinese copy and then retain the project's built-in language pack, which will not affect the overall development experience. The steps to remove internationalization are as follows:
+
+- Hide the language switching button on the interface, see: [Interface Language Switching Function](#interface-language-switching-function)
+- Modify the default language, see: [Configure Default Language](#configure-default-language)
+- Disable `vue-i18n` warning prompts, in the `src/locales/index.ts` file, modify `missingWarn` to `false`:
+
+ ```ts
+ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD, // [!code --]
+ missingWarn: false, // [!code ++]
+ ...options,
+ });
+ }
+ ```
diff --git a/web/docs/src/en/guide/in-depth/login.md b/web/docs/src/en/guide/in-depth/login.md
new file mode 100644
index 0000000..7fdac2c
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/login.md
@@ -0,0 +1,119 @@
+# Login
+
+This document explains how to customize the login page of your application.
+
+## Login Page Adjustment
+
+If you want to adjust the title, description, icon, and toolbar of the login page, you can do so by configuring the `props` parameter of the `AuthPageLayout` component.
+
+
+
+You just need to configure the `props` parameter of `AuthPageLayout` in `src/router/routes/core.ts` within your application:
+
+```ts {4-8}
+ {
+ component: AuthPageLayout,
+ props: {
+ sloganImage: "xxx/xxx.png",
+ pageTitle: "开箱即用的大型中后台管理系统",
+ pageDescription: "工程化、高性能、跨组件库的前端模版",
+ toolbar: true,
+ toolbarList: ['color', 'language', 'layout', 'theme'],
+ }
+ // ...
+ },
+```
+
+::: tip
+
+If these configurations do not meet your needs, you can implement your own login page. Simply implement your own `AuthPageLayout`.
+
+:::
+
+## Login Form Adjustment
+
+If you want to adjust the content of the login form, you can configure the `AuthenticationLogin` component parameters in `src/views/_core/authentication/login.vue` within your application:
+
+```vue
+
+```
+
+::: details AuthenticationLogin Component Props
+
+```ts
+{
+ /**
+ * @en Verification code login path
+ */
+ codeLoginPath?: string;
+ /**
+ * @en Forget password path
+ */
+ forgetPasswordPath?: string;
+
+ /**
+ * @en Whether it is in loading state
+ */
+ loading?: boolean;
+
+ /**
+ * @en QR code login path
+ */
+ qrCodeLoginPath?: string;
+
+ /**
+ * @en Registration path
+ */
+ registerPath?: string;
+
+ /**
+ * @en Whether to show verification code login
+ */
+ showCodeLogin?: boolean;
+ /**
+ * @en Whether to show forget password
+ */
+ showForgetPassword?: boolean;
+
+ /**
+ * @en Whether to show QR code login
+ */
+ showQrcodeLogin?: boolean;
+
+ /**
+ * @en Whether to show registration button
+ */
+ showRegister?: boolean;
+
+ /**
+ * @en Whether to show remember account
+ */
+ showRememberMe?: boolean;
+
+ /**
+ * @en Whether to show third-party login
+ */
+ showThirdPartyLogin?: boolean;
+
+ /**
+ * @en Login box subtitle
+ */
+ subTitle?: string;
+
+ /**
+ * @en Login box title
+ */
+ title?: string;
+}
+```
+
+:::
+
+::: tip
+
+If these configurations do not meet your needs, you can implement your own login form and related login logic.
+
+:::
diff --git a/web/docs/src/en/guide/in-depth/theme.md b/web/docs/src/en/guide/in-depth/theme.md
new file mode 100644
index 0000000..164ac17
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/theme.md
@@ -0,0 +1,1295 @@
+# Theme
+
+The framework is built on [shadcn-vue](https://www.shadcn-vue.com/themes.html) and [tailwindcss](https://tailwindcss.com/), offering a rich theme configuration. You can easily switch between various themes through simple configuration to meet personalized needs. You can choose to use CSS variables or Tailwind CSS utility classes for theme settings.
+
+## CSS Variables
+
+The project follows the theme configuration of [shadcn-vue](https://www.shadcn-vue.com/themes.html), for example:
+
+```html
+
+```
+
+We use a simple convention for colors. The `background` variable is used for the background color of components, and the `foreground` variable is used for text color.
+
+For the following components, `background` will be `hsl(var(--primary))`, and `foreground` will be `hsl(var(--primary-foreground))`.
+
+## Detailed List of CSS Variables
+
+::: warning Note
+
+The colors inside CSS variables must use the `hsl` format, such as `0 0% 100%`, without adding `hsl()` and `,`.
+
+:::
+
+You can check the list below to understand all the available variables.
+
+::: details Default theme CSS variables
+
+```css
+:root {
+ --font-family:
+ -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+ arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+ 'Segoe UI Symbol', 'Noto Color Emoji';
+
+ /* Default background color of ...etc */
+ --background: 0 0% 100%;
+
+ /* Main area background color */
+ --background-deep: 216 20.11% 95.47%;
+ --foreground: 210 6% 21%;
+
+ /* Background color for */
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ /* Background color for popovers such as , , */
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ /* Muted backgrounds such as , and */
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ /* Theme Colors */
+
+ --primary: 212 100% 45%;
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* Secondary colors for */
+
+ --secondary: 240 5% 96%;
+ --secondary-foreground: 240 6% 10%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 240 5% 96%;
+ --accent-hover: 200deg 10% 90%;
+ --accent-foreground: 240 6% 10%;
+
+ /* Darker color */
+ --heavy: 192deg 9.43% 89.61%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 5.9% 90%;
+
+ /* Border color for inputs such as , , */
+ --input: 240deg 5.88% 90%;
+ --input-placeholder: 217 10.6% 65%;
+ --input-background: 0 0% 100%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* Border radius for card, input and buttons */
+ --radius: 0.5rem;
+
+ /* ============= custom ============= */
+
+ /* overlay color */
+ --overlay: 0deg 0% 0% / 30%;
+
+ /* base font size */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ /* menu */
+ --sidebar: 0 0% 100%;
+ --sidebar-deep: 216 20.11% 95.47%;
+ --menu: var(--sidebar);
+
+ /* header */
+ --header: 0 0% 100%;
+
+ accent-color: var(--primary);
+ color-scheme: light;
+}
+```
+
+:::
+
+::: details Default theme dark mode CSS variables
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ /* Default background color of ...etc */
+ --background: 222.34deg 10.43% 12.27%;
+
+ /* Main area background color */
+ --background-deep: 220deg 13.06% 9%;
+ --foreground: 0 0% 95%;
+
+ /* Background color for */
+ --card: 222.34deg 10.43% 12.27%;
+
+ /* --card: 222.2 84% 4.9%; */
+ --card-foreground: 210 40% 98%;
+
+ /* Background color for popovers such as , , */
+ --popover: 222.82deg 8.43% 12.27%;
+ --popover-foreground: 210 40% 98%;
+
+ /* Muted backgrounds such as , and */
+ --muted: 220deg 6.82% 17.25%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ /* Theme Colors */
+
+ /* --primary: 245 82% 67%; */
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* secondary color */
+ --secondary: 240 5% 17%;
+ --secondary-foreground: 0 0% 98%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 0deg 0% 100% / 8%;
+ --accent-hover: 0deg 0% 100% / 12%;
+ --accent-foreground: 0 0% 98%;
+
+ /* Darker color */
+ --heavy: 0deg 0% 100% / 12%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 3.7% 15.9%;
+
+ /* Border color for inputs such as , , */
+ --input: 0deg 0% 100% / 10%;
+ --input-placeholder: 218deg 11% 65%;
+ --input-background: 0deg 0% 100% / 5%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* base radius */
+ --radius: 0.5rem;
+
+ /* ============= Custom ============= */
+
+ /* overlay color */
+ --overlay: 0deg 0% 0% / 40%;
+
+ /* base font size */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ --sidebar: 222.34deg 10.43% 12.27%;
+ --sidebar-deep: 220deg 13.06% 9%;
+ --menu: var(--sidebar);
+ --header: 222.34deg 10.43% 12.27%;
+
+ color-scheme: dark;
+}
+```
+
+:::
+
+## Overriding Default CSS Variables
+
+You only need to override the CSS variables you want to change in your project. For example, to change the default card background color, you can add the following content to your CSS file to override it:
+
+### Under the Default Theme
+
+```css
+:root {
+ /* Background color for */
+ --card: 0 0% 30%;
+}
+```
+
+### In Dark Mode
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ /* Background color for */
+ --card: 222.34deg 10.43% 12.27%;
+}
+```
+
+## Changing the Brand Primary Color
+
+::: tip
+
+- You need to use the `hsl` color format.
+- You must clear the cache for the changes to take effect.
+- You can use [third-party tools](https://www.w3schools.com/colors/colors_hsl.asp) to convert colors.
+
+:::
+
+You only need to customize the primary color in the `preferences.ts` file under the application directory:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ // Error color
+ colorDestructive: 'hsl(348 100% 61%)',
+ // Primary color
+ colorPrimary: 'hsl(212 100% 45%)',
+ // Success color
+ colorSuccess: 'hsl(144 57% 58%)',
+ // Warning color
+ colorWarning: 'hsl(42 84% 61%)',
+ },
+});
+```
+
+## Built-in Themes
+
+The framework includes a variety of built-in themes, which you can configure in the `preferences.ts` file:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ builtinType: 'default',
+ },
+});
+```
+
+### Built-in Theme List
+
+The framework includes 16 built-in themes and also supports custom themes. Theoretically, you can expand the themes without limit.
+
+::: details List of Built-in Theme Types
+
+```ts
+type BuiltinThemeType =
+ | 'custom'
+ | 'deep-blue'
+ | 'deep-green'
+ | 'default'
+ | 'gray'
+ | 'green'
+ | 'neutral'
+ | 'orange'
+ | 'pink'
+ | 'red'
+ | 'rose'
+ | 'sky-blue'
+ | 'slate'
+ | 'stone'
+ | 'violet'
+ | 'yellow'
+ | 'zinc'
+ | (Record & string);
+```
+
+:::
+
+::: details Built-in Theme CSS Variables - Light
+
+```css
+:root {
+ --font-family:
+ -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+ arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+ 'Segoe UI Symbol', 'Noto Color Emoji';
+
+ /* Default background color of ...etc */
+ --background: 0 0% 100%;
+
+ /* Main area background color */
+ --background-deep: 216 20.11% 95.47%;
+ --foreground: 222 84% 5%;
+
+ /* Background color for */
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ /* Background color for popovers such as , , */
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ /* Muted backgrounds such as , and */
+
+ /* --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%; */
+
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+
+ /* Theme Colors */
+
+ --primary: 212 100% 45%;
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* Secondary colors for */
+
+ --secondary: 240 5% 96%;
+ --secondary-foreground: 240 6% 10%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 240 5% 96%;
+ --accent-hover: 200deg 10% 90%;
+ --accent-foreground: 240 6% 10%;
+
+ /* Darker color */
+ --heavy: 192deg 9.43% 89.61%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 5.9% 90%;
+
+ /* Border color for inputs such as , , */
+ --input: 240deg 5.88% 90%;
+ --input-placeholder: 217 10.6% 65%;
+ --input-background: 0 0% 100%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* Border radius for card, input and buttons */
+ --radius: 0.5rem;
+
+ /* ============= custom ============= */
+
+ /* overlay color */
+ --overlay: 0deg 0% 0% / 30%;
+
+ /* base font size */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ /* menu */
+ --sidebar: 0 0% 100%;
+ --sidebar-deep: 0 0% 100%;
+ --menu: var(--sidebar);
+
+ /* header */
+ --header: 0 0% 100%;
+
+ accent-color: var(--primary);
+ color-scheme: light;
+}
+
+[data-theme='violet'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 224 71.4% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 224 71.4% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 224 71.4% 4.1%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 220 14.3% 95.9%;
+ --secondary-foreground: 220.9 39.3% 11%;
+ --muted: 220 14.3% 95.9%;
+ --muted-foreground: 220 8.9% 46.1%;
+ --accent: 220 14.3% 95.9%;
+ --accent-foreground: 220.9 39.3% 11%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+ --ring: 262.1 83.3% 57.8%;
+}
+
+[data-theme='pink'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 346.8 77.2% 49.8%;
+}
+
+[data-theme='rose'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 346.8 77.2% 49.8%;
+}
+
+[data-theme='sky-blue'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+}
+
+[data-theme='deep-blue'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+}
+
+[data-theme='green'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 142.1 76.2% 36.3%;
+}
+
+[data-theme='deep-green'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 142.1 76.2% 36.3%;
+}
+
+[data-theme='orange'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 20 14.3% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 20 14.3% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 20 14.3% 4.1%;
+ --primary-foreground: 60 9.1% 97.8%;
+ --secondary: 60 4.8% 95.9%;
+ --secondary-foreground: 24 9.8% 10%;
+ --muted: 60 4.8% 95.9%;
+ --muted-foreground: 25 5.3% 44.7%;
+ --accent: 60 4.8% 95.9%;
+ --accent-foreground: 24 9.8% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 20 5.9% 90%;
+ --input: 20 5.9% 90%;
+ --ring: 24.6 95% 53.1%;
+}
+
+[data-theme='yellow'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 20 14.3% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 20 14.3% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 20 14.3% 4.1%;
+ --primary-foreground: 26 83.3% 14.1%;
+ --secondary: 60 4.8% 95.9%;
+ --secondary-foreground: 24 9.8% 10%;
+ --muted: 60 4.8% 95.9%;
+ --muted-foreground: 25 5.3% 44.7%;
+ --accent: 60 4.8% 95.9%;
+ --accent-foreground: 24 9.8% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 20 5.9% 90%;
+ --input: 20 5.9% 90%;
+ --ring: 20 14.3% 4.1%;
+}
+
+[data-theme='zinc'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 240 5.9% 10%;
+}
+
+[data-theme='neutral'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+}
+
+[data-theme='slate'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+}
+
+[data-theme='gray'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 224 71.4% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 224 71.4% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 224 71.4% 4.1%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 220 14.3% 95.9%;
+ --secondary-foreground: 220.9 39.3% 11%;
+ --muted: 220 14.3% 95.9%;
+ --muted-foreground: 220 8.9% 46.1%;
+ --accent: 220 14.3% 95.9%;
+ --accent-foreground: 220.9 39.3% 11%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+ --ring: 224 71.4% 4.1%;
+}
+```
+
+:::
+
+::: details 内置主题css变量 - dark
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ /* Default background color of ...etc */
+ --background: 222.34deg 10.43% 12.27%;
+
+ /* 主体区域背景色 */
+ --background-deep: 220deg 13.06% 9%;
+ --foreground: 0 0% 95%;
+
+ /* Background color for */
+ --card: 222.34deg 10.43% 12.27%;
+
+ /* --card: 222.2 84% 4.9%; */
+ --card-foreground: 210 40% 98%;
+
+ /* Background color for popovers such as , , */
+ --popover: 222.82deg 8.43% 12.27%;
+ --popover-foreground: 210 40% 98%;
+
+ /* Muted backgrounds such as , and */
+
+ /* --muted: 220deg 6.82% 17.25%; */
+
+ /* --muted-foreground: 215 20.2% 65.1%; */
+
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+
+ /* 主题颜色 */
+
+ /* --primary: 245 82% 67%; */
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* 颜色次要 */
+ --secondary: 240 5% 17%;
+ --secondary-foreground: 0 0% 98%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 216 5% 19%;
+ --accent-hover: 216 5% 24%;
+ --accent-foreground: 0 0% 98%;
+
+ /* Darker color */
+ --heavy: 216 5% 24%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 3.7% 22%;
+
+ /* Border color for inputs such as , , */
+ --input: 0deg 0% 100% / 10%;
+ --input-placeholder: 218deg 11% 65%;
+ --input-background: 0deg 0% 100% / 5%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* 基本圆角大小 */
+ --radius: 0.5rem;
+
+ /* ============= Custom ============= */
+
+ /* overlay color */
+ --overlay: 0deg 0% 0% / 40%;
+
+ /* base font size */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ --sidebar: 222.34deg 10.43% 12.27%;
+ --sidebar-deep: 220deg 13.06% 9%;
+ --menu: var(--sidebar);
+
+ /* header */
+ --header: 222.34deg 10.43% 12.27%;
+
+ color-scheme: dark;
+}
+
+.dark[data-theme='violet'],
+[data-theme='violet'] .dark {
+ --background: 224 71.4% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 210 20% 98%;
+ --card: 224 71.4% 4.1%;
+ --card-foreground: 210 20% 98%;
+ --popover: 224 71.4% 4.1%;
+ --popover-foreground: 210 20% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 215 27.9% 16.9%;
+ --secondary-foreground: 210 20% 98%;
+ --muted: 215 27.9% 16.9%;
+ --muted-foreground: 217.9 10.6% 64.9%;
+ --accent: 215 27.9% 16.9%;
+ --accent-foreground: 210 20% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 215 27.9% 16.9%;
+ --input: 215 27.9% 16.9%;
+ --ring: 263.4 70% 50.4%;
+ --sidebar: 224 71.4% 4.1%;
+ --sidebar-deep: 224 71.4% 4.1%;
+ --header: 224 71.4% 4.1%;
+}
+
+.dark[data-theme='pink'],
+[data-theme='pink'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 95%;
+ --card: 0 0% 9%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 9%;
+ --popover-foreground: 0 0% 95%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 15%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 346.8 77.2% 49.8%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='rose'],
+[data-theme='rose'] .dark {
+ --background: 0 0% 3.9%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary-foreground: 0 85.7% 97.3%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 72.2% 50.6%;
+ --sidebar: 0 0% 3.9%;
+ --sidebar-deep: 0 0% 3.9%;
+ --header: 0 0% 3.9%;
+}
+
+.dark[data-theme='sky-blue'],
+[data-theme='sky-blue'] .dark {
+ --background: 222.2 84% 4.9%;
+ --background-deep: var(--background);
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 48%;
+ --sidebar: 222.2 84% 4.9%;
+ --sidebar-deep: 222.2 84% 4.9%;
+ --header: 222.2 84% 4.9%;
+}
+
+.dark[data-theme='deep-blue'],
+[data-theme='deep-blue'] .dark {
+ --background: 222.2 84% 4.9%;
+ --background-deep: var(--background);
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 48%;
+ --sidebar: 222.2 84% 4.9%;
+ --sidebar-deep: 222.2 84% 4.9%;
+ --header: 222.2 84% 4.9%;
+}
+
+.dark[data-theme='green'],
+[data-theme='green'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 95%;
+ --card: 24 9.8% 6%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 9%;
+ --popover-foreground: 0 0% 95%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 15%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 142.4 71.8% 29.2%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='deep-green'],
+[data-theme='deep-green'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 95%;
+ --card: 24 9.8% 6%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 9%;
+ --popover-foreground: 0 0% 95%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 15%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 142.4 71.8% 29.2%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='orange'],
+[data-theme='orange'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 60 9.1% 97.8%;
+ --card: 20 14.3% 4.1%;
+ --card-foreground: 60 9.1% 97.8%;
+ --popover: 20 14.3% 4.1%;
+ --popover-foreground: 60 9.1% 97.8%;
+ --primary-foreground: 60 9.1% 97.8%;
+ --secondary: 12 6.5% 15.1%;
+ --secondary-foreground: 60 9.1% 97.8%;
+ --muted: 12 6.5% 15.1%;
+ --muted-foreground: 24 5.4% 63.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 60 9.1% 97.8%;
+ --destructive: 0 72.2% 50.6%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 12 6.5% 15.1%;
+ --input: 12 6.5% 15.1%;
+ --ring: 20.5 90.2% 48.2%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='yellow'],
+[data-theme='yellow'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 60 9.1% 97.8%;
+ --card: 20 14.3% 4.1%;
+ --card-foreground: 60 9.1% 97.8%;
+ --popover: 20 14.3% 4.1%;
+ --popover-foreground: 60 9.1% 97.8%;
+ --primary-foreground: 26 83.3% 14.1%;
+ --secondary: 12 6.5% 15.1%;
+ --secondary-foreground: 60 9.1% 97.8%;
+ --muted: 12 6.5% 15.1%;
+ --muted-foreground: 24 5.4% 63.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 60 9.1% 97.8%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 12 6.5% 15.1%;
+ --input: 12 6.5% 15.1%;
+ --ring: 35.5 91.7% 32.9%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='zinc'],
+[data-theme='zinc'] .dark {
+ --background: 240 10% 3.9%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary-foreground: 240 5.9% 10%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 240 4.9% 83.9%;
+ --sidebar: 240 10% 3.9%;
+ --sidebar-deep: 240 10% 3.9%;
+ --header: 240 4.9% 83.9%;
+}
+
+.dark[data-theme='neutral'],
+[data-theme='neutral'] .dark {
+ --background: 0 0% 3.9%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --sidebar: 0 0% 3.9%;
+ --sidebar-deep: 0 0% 3.9%;
+ --header: 0 0% 3.9%;
+}
+
+.dark[data-theme='slate'],
+[data-theme='slate'] .dark {
+ --background: 222.2 84% 4.9%;
+ --background-deep: var(--background);
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9;
+ --sidebar: 222.2 84% 4.9%;
+ --sidebar-deep: 222.2 84% 4.9%;
+ --header: 222.2 84% 4.9%;
+}
+
+.dark[data-theme='gray'],
+[data-theme='gray'] .dark {
+ --background: 224 71.4% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 210 20% 98%;
+ --card: 224 71.4% 4.1%;
+ --card-foreground: 210 20% 98%;
+ --popover: 224 71.4% 4.1%;
+ --popover-foreground: 210 20% 98%;
+ --primary-foreground: 220.9 39.3% 11%;
+ --secondary: 215 27.9% 16.9%;
+ --secondary-foreground: 210 20% 98%;
+ --muted: 215 27.9% 16.9%;
+ --muted-foreground: 217.9 10.6% 64.9%;
+ --accent: 215 27.9% 16.9%;
+ --accent-foreground: 210 20% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 215 27.9% 16.9%;
+ --input: 215 27.9% 16.9%;
+ --ring: 216 12.2% 83.9%;
+ --sidebar: 224 71.4% 4.1%;
+ --sidebar-deep: 224 71.4% 4.1%;
+ --header: 224 71.4% 4.1%;
+}
+```
+
+:::
+
+## Adding a New Theme
+
+To add a new theme, simply follow these steps:
+
+- Add a new theme configuration in the application's `src/preferences.ts`.
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ builtinType: 'my-theme',
+ },
+});
+```
+
+- Add the theme's CSS variables to your CSS file.
+
+```css
+/* light */
+[data-theme='my-theme'] {
+ --foreground: 224 71.4% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 224 71.4% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 224 71.4% 4.1%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 220 14.3% 95.9%;
+ --secondary-foreground: 220.9 39.3% 11%;
+ --muted: 220 14.3% 95.9%;
+ --muted-foreground: 220 8.9% 46.1%;
+ --accent: 220 14.3% 95.9%;
+ --accent-foreground: 220.9 39.3% 11%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+ --ring: 262.1 83.3% 57.8%;
+}
+
+/* dark */
+.dark[data-theme='my-theme'],
+[data-theme='my-theme'] .dark {
+ --background: 224 71.4% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 210 20% 98%;
+ --card: 224 71.4% 4.1%;
+ --card-foreground: 210 20% 98%;
+ --popover: 224 71.4% 4.1%;
+ --popover-foreground: 210 20% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 215 27.9% 16.9%;
+ --secondary-foreground: 210 20% 98%;
+ --muted: 215 27.9% 16.9%;
+ --muted-foreground: 217.9 10.6% 64.9%;
+ --accent: 215 27.9% 16.9%;
+ --accent-foreground: 210 20% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 215 27.9% 16.9%;
+ --input: 215 27.9% 16.9%;
+ --ring: 263.4 70% 50.4%;
+ --sidebar: 224 71.4% 4.1%;
+ --sidebar-deep: 224 71.4% 4.1%;
+}
+```
+
+## Dark Mode
+
+The framework includes a variety of built-in themes, which you can configure in `preferences.ts`. The dark theme also uses CSS variables for configuration:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ mode: 'dark',
+ },
+});
+```
+
+## Customizing Sidebar Color
+
+The sidebar color is configured through the `--sidebar` variable.
+
+### Under the Default Theme
+
+```css
+:root {
+ --sidebar: 0 0% 100%;
+}
+```
+
+### In Dark Mode
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ --sidebar: 222.34deg 10.43% 12.27%;
+}
+```
+
+## Customizing Header Color
+
+The header color is configured through the `--header` variable.
+
+### Under the Default Theme
+
+```css
+:root {
+ --header: 0 0% 100%;
+}
+```
+
+### In Dark Mode
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ --header: 222.34deg 10.43% 12.27%;
+}
+```
+
+## Color Weakness Mode
+
+Typically used in special scenarios, you can set the application to color weakness mode. This can be configured in `preferences.ts`:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ colorWeakMode: true,
+ },
+});
+```
+
+## Gray Mode
+
+Typically used in special scenarios, this mode grays out the webpage. You can configure it in `preferences.ts`:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ colorGrayMode: true,
+ },
+});
+```
diff --git a/web/docs/src/en/guide/in-depth/ui-framework.md b/web/docs/src/en/guide/in-depth/ui-framework.md
new file mode 100644
index 0000000..667619a
--- /dev/null
+++ b/web/docs/src/en/guide/in-depth/ui-framework.md
@@ -0,0 +1,17 @@
+# UI Framework Switching
+
+`Vue Admin` supports your freedom to choose the UI framework. The default UI framework for the demo site is `Ant Design Vue`, consistent with the older version. The framework also has built-in versions for `Element Plus` and `Naive UI`, allowing you to choose according to your preference.
+
+## Adding a New UI Framework
+
+If you want to use a different UI framework, you only need to follow these steps:
+
+1. Create a new folder inside `apps`, for example, `apps/web-xxx`.
+2. Change the `name` field in `apps/web-xxx/package.json` to `web-xxx`.
+3. Remove dependencies and code from other UI frameworks and replace them with your chosen UI framework's logic, which requires minimal changes.
+4. Adjust the language files within `locales`.
+5. Adjust the components in `app.vue`.
+6. Adapt the theme of the UI framework to match `Vben Admin`.
+7. Adjust the application name in `.env`.
+8. Add a `dev:xxx` script in the root directory of the repository.
+9. Run `pnpm install` to install dependencies.
diff --git a/web/docs/src/en/guide/introduction/changelog.md b/web/docs/src/en/guide/introduction/changelog.md
new file mode 100644
index 0000000..83c822b
--- /dev/null
+++ b/web/docs/src/en/guide/introduction/changelog.md
@@ -0,0 +1,3 @@
+# CHANGE LOG
+
+TODO
diff --git a/web/docs/src/en/guide/introduction/quick-start.md b/web/docs/src/en/guide/introduction/quick-start.md
new file mode 100644
index 0000000..757679c
--- /dev/null
+++ b/web/docs/src/en/guide/introduction/quick-start.md
@@ -0,0 +1,95 @@
+---
+outline: deep
+---
+
+# Quick Start {#quick-start}
+
+## Prerequisites
+
+::: info Environment Requirements
+
+Before starting the project, ensure that your environment meets the following requirements:
+
+- [Node.js](https://nodejs.org/en) version 20.15.0 or above. It is recommended to use [fnm](https://github.com/Schniz/fnm), [nvm](https://github.com/nvm-sh/nvm), or directly use [pnpm](https://pnpm.io/cli/env) for version management.
+- [Git](https://git-scm.com/) any version.
+
+To verify if your environment meets the above requirements, you can check the versions using the following commands:
+
+```bash
+# Ensure the correct node LTS version is displayed
+node -v
+# Ensure the correct git version is displayed
+git -v
+```
+
+:::
+
+## Starting the Project
+
+### Obtain the Source Code
+
+::: code-group
+
+```bash [GitHub]
+# Clone the code
+git clone https://github.com/vbenjs/vue-vben-admin.git
+```
+
+```bash [Gitee]
+# Clone the code
+# The Gitee repository may not have the latest code
+git clone https://gitee.com/annsion/vue-vben-admin.git
+```
+
+:::
+
+::: danger Caution
+
+Ensure that the directory where you store the code and all its parent directories do not contain Chinese, Korean, Japanese characters, or spaces, as this may cause errors when installing dependencies and starting the project.
+
+:::
+
+### Install Dependencies
+
+Open a terminal in your code directory and execute the following commands:
+
+```bash
+# Enter the project directory
+cd vue-vben-admin
+
+# Enable the project-specified version of pnpm
+npm i -g corepack
+
+# Install dependencies
+pnpm install
+```
+
+::: tip Note
+
+The project only supports using `pnpm` for installing dependencies. By default, `corepack` will be used to install the specified version of `pnpm`.
+
+:::
+
+### Run the Project
+
+Execute the following command to run the project:
+
+```bash
+# Start the project
+pnpm dev
+```
+
+You will see an output similar to the following, allowing you to select the project you want to run:
+
+```bash
+│
+◆ Select the app you need to run [dev]:
+│ ● @vben/web-antd
+│ ○ @vben/web-ele
+│ ○ @vben/web-naive
+│ ○ @vben/docs
+│ ○ @vben/playground
+└
+```
+
+Now, you can visit `http://localhost:5555` in your browser to view the project.
diff --git a/web/docs/src/en/guide/introduction/roadmap.md b/web/docs/src/en/guide/introduction/roadmap.md
new file mode 100644
index 0000000..8add7b1
--- /dev/null
+++ b/web/docs/src/en/guide/introduction/roadmap.md
@@ -0,0 +1,3 @@
+# Roadmap
+
+TODO:
diff --git a/web/docs/src/en/guide/introduction/thin.md b/web/docs/src/en/guide/introduction/thin.md
new file mode 100644
index 0000000..a310ef3
--- /dev/null
+++ b/web/docs/src/en/guide/introduction/thin.md
@@ -0,0 +1,67 @@
+# Slimmed-Down Version
+
+Starting from version `5.0`, we no longer provide slimmed-down repositories or branches. Our goal is to offer a more consistent development experience while reducing maintenance costs. Here’s how we introduce our project, slim down, and remove unnecessary features.
+
+## Application Slimming
+
+First, identify the version of the `UI` component library you need, and then delete the corresponding applications. For example, if you choose to use `Ant Design Vue`, you can delete the other applications. Simply remove the following two folders:
+
+```bash
+apps/web-ele
+apps/web-native
+
+```
+
+::: tip
+
+If your project doesn’t include the `UI` component library you need, you can delete all other applications and create your own new application as needed.
+
+:::
+
+## Demo Code Slimming
+
+If you don’t need demo code, you can simply delete the `playground` folder
+
+## Documentation Slimming
+
+If you don’t need documentation, you can delete the `docs` folder.
+
+## Remove Mock Service
+
+If you don’t need the `Mock` service, you can delete the `apps/backend-mock` folder. Also, remove the `VITE_NITRO_MOCK` variable from the `.env.development` file in your application.
+
+```bash
+# Whether to enable Nitro Mock service, true to enable, false to disable
+VITE_NITRO_MOCK=false
+```
+
+## Installing Dependencies
+
+Now that you’ve completed the slimming operations, you can install the dependencies and start your project:
+
+```bash
+# Run in the root directory
+pnpm install
+
+```
+
+## Adjusting Commands
+
+After slimming down, you may need to adjust commands according to your project. In the `package.json` file in the root directory, you can adjust the `scripts` field and remove any commands you don’t need.
+
+```json
+{
+ "scripts": {
+ "build:antd": "pnpm run build --filter=@vben/web-antd",
+ "build:docs": "pnpm run build --filter=@vben/docs",
+ "build:ele": "pnpm run build --filter=@vben/web-ele",
+ "build:naive": "pnpm run build --filter=@vben/web-naive",
+ "build:play": "pnpm run build --filter=@vben/playground",
+ "dev:antd": "pnpm -F @vben/web-antd run dev",
+ "dev:docs": "pnpm -F @vben/docs run dev",
+ "dev:ele": "pnpm -F @vben/web-ele run dev",
+ "dev:play": "pnpm -F @vben/playground run dev",
+ "dev:naive": "pnpm -F @vben/web-naive run dev"
+ }
+}
+```
diff --git a/web/docs/src/en/guide/introduction/vben.md b/web/docs/src/en/guide/introduction/vben.md
new file mode 100644
index 0000000..8a2ba1c
--- /dev/null
+++ b/web/docs/src/en/guide/introduction/vben.md
@@ -0,0 +1,49 @@
+# About Vben Admin
+
+::: info You are reading the documentation for [Vben Admin](https://github.com/vbenjs/vue-vben-admin) version `5.0`!
+
+- Vben Admin 2.x is currently archived and only receives critical fixes.
+- The new version is not compatible with the old version. If you are using the old version (v2, v3), please refer to the [Vue Vben Admin 2.x Documentation](https://doc.vvbin.cn).
+- If you find any errors in the documentation, feel free to submit an issue to help us improve.
+- If you just want to experience it, you can check out the [Quick Start](./quick-start.md).
+
+:::
+
+[Vben Admin](https://github.com/vbenjs/vue-vben-admin) is a backend solution based on [Vue 3.0](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), and [TypeScript](https://www.typescriptlang.org/), aimed at providing an out-of-the-box solution for developing medium to large-scale projects. It includes features like component re-encapsulation, utilities, hooks, dynamic menus, permission validation, multi-theme configurations, and button-level permission control. The project uses the latest frontend technology stack, making it a good starting template for quickly building enterprise-level mid- to backend product prototypes. It can also serve as an example for learning `vue3`, `vite`, `ts`, and other mainstream technologies. The project will continue to follow the latest technologies and apply them within the project.
+
+## Features
+
+- **Latest Technology Stack**: Developed using cutting-edge frontend technologies like `Vue 3`, `Vite`, and `TypeScript`.
+- **Internationalization**: Built-in comprehensive internationalization solutions with multi-language support.
+- **Permission Validation**: Comprehensive permission validation solutions, including button-level permission control.
+- **Multi-Theme**: Built-in multiple theme configurations & dark mode to meet personalized needs.
+- **Dynamic Menu**: Supports dynamic menus that can display based on permissions.
+- **Mock Data**: High-performance local Mock data solution based on `Nitro`.
+- **Rich Components**: Provides a wide range of components to meet most business needs.
+- **Standardization**: Code quality is ensured with tools like `ESLint`, `Prettier`, `Stylelint`, `Publint`, and `CSpell`.
+- **Engineering**: Development efficiency is improved with tools like `Pnpm Monorepo`, `TurboRepo`, and `Changeset`.
+- **Multi-UI Library Support**: Supports mainstream UI libraries like `Ant Design Vue`, `Element Plus`, and `Vuetify`, without being restricted to a specific framework.
+
+## Browser Support
+
+- **Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
+
+- **Production environment** supports modern browsers, IE is not supported.
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE | [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
+| :-: | :-: | :-: | :-: | :-: |
+| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## Contribution
+
+- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) is still being actively updated. Contributions are welcome to help maintain and improve the project, aiming to create a better mid- to backend solution.
+- If you wish to join us, you can start by contributing in the following ways, and we will invite you to join based on your activity.
+
+::: info Join Us
+
+- Regularly submit `PRs`.
+- Provide valuable suggestions.
+- Participate in discussions and help resolve some `issues`.
+- Help maintain the documentation.
+
+:::
diff --git a/web/docs/src/en/guide/introduction/why.md b/web/docs/src/en/guide/introduction/why.md
new file mode 100644
index 0000000..f0cabe8
--- /dev/null
+++ b/web/docs/src/en/guide/introduction/why.md
@@ -0,0 +1,9 @@
+# Why Choose Us?
+
+First of all, we do not compare ourselves with other frameworks. We believe that every framework has its own characteristics and is suited for different scenarios. Our goal is to provide a simple and easy-to-use framework that allows developers to get started quickly and focus on developing business logic. Therefore, we will continue to improve and optimize our framework to offer a better experience.
+
+## Framework History
+
+Starting from Vue Vben Admin version 1.x, the framework has undergone numerous iterations and optimizations. From initially using `Vite 0.x` when there were no ready-made plugins available, we developed many custom plugins to bridge the gap between Webpack and Vite. Although many of these have since been replaced, our original intention has remained the same: to provide a simple and easy-to-use framework.
+
+Although the community maintained the project for a period, we have always closely monitored the development of Vue Vben Admin. We have witnessed many developers use Vben Admin and provide valuable suggestions and feedback. We are very grateful for everyone's support and contributions, which continue to drive us to improve Vben Admin. In the new version, we have continuously collected user feedback, started anew, and optimized the framework to provide a better user experience. Our goal is to enable developers to get started quickly and focus on developing business logic.
diff --git a/web/docs/src/en/guide/other/faq.md b/web/docs/src/en/guide/other/faq.md
new file mode 100644
index 0000000..50cc133
--- /dev/null
+++ b/web/docs/src/en/guide/other/faq.md
@@ -0,0 +1,159 @@
+# Frequently Asked Questions #{faq}
+
+::: tip Listed are some common questions
+
+If you have a question, you can first look here. If not found, you can search or submit your question on [GitHub Issue](https://github.com/vbenjs/vue-vben-admin/issues), or if it's a discussion-type question, you can go to [GitHub Discussions](https://github.com/vbenjs/vue-vben-admin/discussions)
+
+:::
+
+## Instructions
+
+If you encounter a problem, you can start looking from the following aspects:
+
+1. Search the corresponding module's GitHub repository [issue](https://github.com/vbenjs/vue-vben-admin/issues)
+2. Search for the problem on [Google](https://www.google.com)
+3. Search for the problem on [Baidu](https://www.baidu.com)
+4. If you can't find the issue in the list below, you can ask in [issues](https://github.com/vbenjs/vue-vben-admin/issues)
+5. If it's not a problem type and needs discussion, please go to [discussions](https://github.com/vbenjs/vue-vben-admin/discussions) to discuss
+
+## Dependency Issues
+
+In a `Monorepo` project, it's important to get into the habit of running `pnpm install` after every `git pull` because new dependencies are often added. The project has configured automatic execution of `pnpm install` in `lefthook.yml`, but sometimes there might be issues. If it does not execute automatically, it is recommended to execute it manually once.
+
+## About Cache Update Issues
+
+The project configuration is by default cached in `localStorage`, so some configurations may not change after a version update.
+
+The solution is to modify the `version` number in `package.json` each time the code is updated. This is because the key for `localStorage` is based on the version number. Therefore, after an update, the configurations from a previous version will become invalid. Simply re-login to apply the new settings.
+
+## About Modifying Configuration Files
+
+When modifying environment files such as `.env` or the `vite.config.ts` file, Vite will automatically restart the service.
+
+There's a chance that automatic restarts may encounter issues. Simply rerunning the project can resolve these problems.
+
+## Errors When Running Locally
+
+Since Vite does not transform code locally and the code uses relatively new syntax such as optional chaining, local development requires using a higher version of the browser (`Chrome 90+`).
+
+## Blank Page After Switching Pages
+
+This issue occurs because route switching animations are enabled, and the corresponding page component has multiple root nodes. Adding a `
` at the outermost layer of the page can solve this problem.
+
+**Incorrect Example**
+
+```vue
+
+
+ text h1
+ text h2
+
+```
+
+**正确示例**
+
+```vue
+
+
+
text h1
+ text h2
+
+
+```
+
+::: tip Tip
+
+- If you want to use multiple root tags, you can disable route switching animations.
+- Root comment nodes under `template` are also counted as a node.
+
+:::
+
+## My code works locally but not when packaged
+
+The reason for this issue could be one of the following. You can check these reasons, and if there are other possibilities, feel free to add them:
+
+- The variable `ctx` was used, which is not exposed in the instance type. The Vue official documentation also advises against using this property as it is intended for internal use only.
+
+```ts
+import { getCurrentInstance } from 'vue';
+getCurrentInstance().ctx.xxxx;
+```
+
+## Dependency Installation Issues
+
+- If you cannot install dependencies or the startup reports an error, you can try executing `pnpm run reinstall`.
+- If you cannot install dependencies or encounter errors, you can try switching to a mobile hotspot for installing dependencies.
+- If that still doesn't work, you can configure a domestic mirror for installation.
+- You can also create a `.npmrc` file in the project root directory with the following content:
+
+```bash
+# .npmrc
+registry = https://registry.npmmirror.com/
+```
+
+## Package File Too Large
+
+- First, the full version will be larger because it includes many library files. You can use the simplified version for development.
+
+- Secondly, it is recommended to enable gzip, which can reduce the size to about 1/3 of the original. Gzip can be enabled directly by the server. If so, the frontend does not need to build `.gz` format files. If the frontend has built `.gz` files, for example, with nginx, you need to enable the `gzip_static: on` option.
+
+- While enabling gzip, you can also enable `brotli` for better compression than gzip. Both can coexist.
+
+**Note**
+
+- gzip_static: This module requires additional installation in nginx, as the default nginx does not include this module.
+
+- Enabling `brotli` also requires additional nginx module installation.
+
+## Runtime Errors
+
+If you encounter errors similar to the following, please check that the full project path (including all parent paths) does not contain Chinese, Japanese, or Korean characters. Otherwise, you will encounter a 404 error for the path, leading to the following issue:
+
+```ts
+[vite] Failed to resolve module import "ant-design-vue/dist/antd.css-vben-adminode_modulesant-design-vuedistantd.css". (imported by /@/setup/ant-design-vue/index.ts)
+```
+
+## Console Route Warning Issue
+
+If you see the following warning in the console, and the page `can open normally`, you can ignore this warning.
+
+Future versions of `vue-router` may provide an option to disable this warning.
+
+```ts
+[Vue Router warn]: No match found for location with path "xxxx"
+```
+
+## Startup Error
+
+If you encounter the following error message, please check if your nodejs version meets the requirements.
+
+```bash
+TypeError: str.matchAll is not a function
+at Object.extractor (vue-vben-admin-main\node_modules@purge-icons\core\dist\index.js:146:27)
+at Extract (vue-vben-admin-main\node_modules@purge-icons\core\dist\index.js:173:54)
+```
+
+## nginx Deployment
+
+After deploying to `nginx`,you might encounter the following error:
+
+```bash
+Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.
+```
+
+Solution 1:
+
+```bash
+http {
+ #If there is such a configuration, it needs to be commented out
+ #include mime.types;
+
+ types {
+ application/javascript js mjs;
+ }
+}
+```
+
+Solution 2:
+
+Open the `mime.types` file under `nginx` and change `application/javascript js;` to `application/javascript js mjs;`
diff --git a/web/docs/src/en/guide/other/project-update.md b/web/docs/src/en/guide/other/project-update.md
new file mode 100644
index 0000000..85ceef4
--- /dev/null
+++ b/web/docs/src/en/guide/other/project-update.md
@@ -0,0 +1,54 @@
+# PROJECT UPDATE
+
+## Why Can't It Be Updated Like a npm Plugin
+
+Because the project is a complete project template, not a plugin or a package, it cannot be updated like a plugin. After you use the code, you will develop it further based on business needs, and you need to manually merge and upgrade.
+
+## What Should I Do
+
+The project is managed using a `Monorepo` approach and has abstracted some of the more core code, such as `packages/@core`, `packages/effects`. As long as the business code has not modified this part of the code, you can directly pull the latest code and then merge it into your branch. You only need to handle some conflicts simply. Other folders will only make some minor adjustments, which will not affect the business code.
+
+::: tip Recommendation
+
+It is recommended to follow the repository updates actively and merge them; do not accumulate over a long time, Otherwise, it will lead to too many merge conflicts and increase the difficulty of merging.
+
+:::
+
+## Updating Code Using Git
+
+1. Clone the code
+
+```bash
+git clone https://github.com/vbenjs/vue-vben-admin.git
+```
+
+2. Add your company's git source address
+
+```bash
+# up is the source name, can be set arbitrarily
+# gitUrl is the latest open-source code
+git remote add up gitUrl;
+```
+
+3. Push the code to your company's git
+
+```bash
+# Push the code to your company
+# main is the branch name, adjust according to your situation
+git push up main
+# Sync the company's code
+# main is the branch name, adjust according to your situation
+git pull up main
+```
+
+4. How to sync the latest open-source code
+
+```bash
+git pull origin main
+```
+
+::: tip Tip
+
+When syncing the code, conflicts may occur. Just resolve the conflicts.
+
+:::
diff --git a/web/docs/src/en/guide/other/remove-code.md b/web/docs/src/en/guide/other/remove-code.md
new file mode 100644
index 0000000..543a937
--- /dev/null
+++ b/web/docs/src/en/guide/other/remove-code.md
@@ -0,0 +1,18 @@
+# Remove Code
+
+## Remove Code
+
+In the corresponding application's `index.html` file, find the following code and delete it:
+
+```html
+
+
+```
diff --git a/web/docs/src/en/guide/project/changeset.md b/web/docs/src/en/guide/project/changeset.md
new file mode 100644
index 0000000..0f2abe7
--- /dev/null
+++ b/web/docs/src/en/guide/project/changeset.md
@@ -0,0 +1,21 @@
+# Changeset
+
+The project has integrated [changeset](https://github.com/changesets/changesets) as a version management tool. Changeset is a version management tool that helps us better manage versions, generate changelogs, and automate releases.
+
+For detailed usage, please refer to the official documentation. If you do not need it, you can ignore it directly.
+
+## Command Line
+
+The changeset command is already built into the project:
+
+### Interactively Add Changesets
+
+```bash
+pnpm run changeset
+```
+
+### Uniformly Increment Version Numbers
+
+```bash
+pnpm run version
+```
diff --git a/web/docs/src/en/guide/project/cli.md b/web/docs/src/en/guide/project/cli.md
new file mode 100644
index 0000000..a9e43ff
--- /dev/null
+++ b/web/docs/src/en/guide/project/cli.md
@@ -0,0 +1,106 @@
+---
+outline: deep
+---
+
+# CLI
+
+In the project, some command-line tools are provided for common operations, located in `scripts`.
+
+## vsh
+
+Used for some project operations, such as cleaning the project, checking the project, etc.
+
+### Usage
+
+```bash
+pnpm vsh [command] [options]
+```
+
+### vsh check-circular
+
+Check for circular references throughout the project. If there are circular references, the modules involved will be output to the console.
+
+#### Usage
+
+```bash
+pnpm vsh check-circular
+```
+
+#### Options
+
+| Option | Description |
+| ---------- | --------------------------------------------------------- |
+| `--staged` | Only check files in the git staging area, default `false` |
+
+### vsh check-dep
+
+Check the dependency situation of the entire project and output `unused dependencies`, `uninstalled dependencies` information to the console.
+
+#### Usage
+
+```bash
+pnpm vsh check-dep
+```
+
+### vsh lint
+
+Lint checks the project to see if the code in the project conforms to standards.
+
+#### Usage
+
+```bash
+pnpm vsh lint
+```
+
+#### Options
+
+| Option | Description |
+| ---------- | -------------------------------------------- |
+| `--format` | Check and try to fix errors, default `false` |
+
+### vsh publint
+
+Perform package standard checks on `Monorepo` projects to see if the packages in the project conform to standards.
+
+#### Usage
+
+```bash
+pnpm vsh publint
+```
+
+#### Options
+
+| Option | Description |
+| --------- | ------------------------------------ |
+| `--check` | Only perform checks, default `false` |
+
+### vsh code-workspace
+
+Generate `vben-admin.code-workspace` file. Currently, it does not need to be executed manually and will be executed automatically when code is committed.
+
+#### Usage
+
+```bash
+pnpm vsh code-workspace
+```
+
+#### Options
+
+| Option | Description |
+| --------------- | --------------------------------------------------------- |
+| `--auto-commit` | Automatically commit during `git commit`, default `false` |
+| `--spaces` | Indentation format, default `2` spaces |
+
+## turbo-run
+
+Used to quickly execute scripts in the large repository and provide option-based interactive selection.
+
+### Usage
+
+```bash
+pnpm turbo-run [command]
+```
+
+### turbo-run dev
+
+Quickly execute the `dev` command and provide option-based interactive selection.
diff --git a/web/docs/src/en/guide/project/dir.md b/web/docs/src/en/guide/project/dir.md
new file mode 100644
index 0000000..e2747f5
--- /dev/null
+++ b/web/docs/src/en/guide/project/dir.md
@@ -0,0 +1,68 @@
+# Directory Explanation
+
+The directory uses Monorepo management, and the project structure is as follows:
+
+```bash
+.
+├── Dockerfile # Docker image build file
+├── README.md # Project documentation
+├── apps # Project applications directory
+│ ├── backend-mock # Backend mock service application
+│ ├── web-antd # Frontend application based on Ant Design Vue
+│ ├── web-ele # Frontend application based on Element Plus
+│ └── web-naive # Frontend application based on Naive UI
+├── build-local-docker-image.sh # Script for building Docker images locally
+├── cspell.json # CSpell configuration file
+├── docs # Project documentation directory
+├── eslint.config.mjs # ESLint configuration file
+├── internal # Internal tools directory
+│ ├── lint-configs # Code linting configurations
+│ │ ├── commitlint-config # Commitlint configuration
+│ │ ├── eslint-config # ESLint configuration
+│ │ ├── prettier-config # Prettier configuration
+│ │ └── stylelint-config # Stylelint configuration
+│ ├── node-utils # Node.js tools
+│ ├── tailwind-config # Tailwind configuration
+│ ├── tsconfig # Common tsconfig settings
+│ └── vite-config # Common Vite configuration
+├── package.json # Project dependency configuration
+├── packages # Project packages directory
+│ ├── @core # Core package
+│ │ ├── base # Base package
+│ │ │ ├── design # Design related
+│ │ │ ├── icons # Icons
+│ │ │ ├── shared # Shared
+│ │ │ └── typings # Type definitions
+│ │ ├── composables # Composable APIs
+│ │ ├── preferences # Preferences
+│ │ └── ui-kit # UI component collection
+│ │ ├── layout-ui # Layout UI
+│ │ ├── menu-ui # Menu UI
+│ │ ├── shadcn-ui # shadcn UI
+│ │ └── tabs-ui # Tabs UI
+│ ├── constants # Constants
+│ ├── effects # Effects related packages
+│ │ ├── access # Access control
+│ │ ├── plugins # Plugins
+│ │ ├── common-ui # Common UI
+│ │ ├── hooks # Composable APIs
+│ │ ├── layouts # Layouts
+│ │ └── request # Request
+│ ├── icons # Icons
+│ ├── locales # Internationalization
+│ ├── preferences # Preferences
+│ ├── stores # State management
+│ ├── styles # Styles
+│ ├── types # Type definitions
+│ └── utils # Utilities
+├── playground # Demo directory
+├── pnpm-lock.yaml # pnpm lock file
+├── pnpm-workspace.yaml # pnpm workspace configuration file
+├── scripts # Scripts directory
+│ ├── turbo-run # Turbo run script
+│ └── vsh # VSH script
+├── stylelint.config.mjs # Stylelint configuration file
+├── turbo.json # Turbo configuration file
+├── vben-admin.code-workspace # VS Code workspace configuration file
+└── vitest.config.ts # Vite configuration file
+```
diff --git a/web/docs/src/en/guide/project/standard.md b/web/docs/src/en/guide/project/standard.md
new file mode 100644
index 0000000..e5417ce
--- /dev/null
+++ b/web/docs/src/en/guide/project/standard.md
@@ -0,0 +1,213 @@
+# Standards
+
+::: tip Contributing Code
+
+- If you want to contribute code to the project, please ensure your code complies with the project's coding standards.
+- If you are using `vscode`, you need to install the following plugins:
+
+ - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code checking
+ - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting
+ - [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Word syntax checking
+ - [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) - CSS formatting
+
+:::
+
+## Purpose
+
+Students with basic engineering literacy always pay attention to coding standards, and code style checking (Code Linting, simply called Lint) is an important means to ensure the consistency of coding standards.
+
+Following the corresponding coding standards has the following benefits:
+
+- Lower bug error rate
+- Efficient development efficiency
+- Higher readability
+
+## Tools
+
+The project's configuration files are located in `internal/lint-configs`, where you can modify various lint configurations.
+
+The project integrates the following code verification tools:
+
+- [ESLint](https://eslint.org/) for JavaScript code checking
+- [Stylelint](https://stylelint.io/) for CSS style checking
+- [Prettier](https://prettier.io/) for code formatting
+- [Commitlint](https://commitlint.js.org/) for checking the standard of git commit messages
+- [Publint](https://publint.dev/) for checking the standard of npm packages
+- [Cspell](https://cspell.org/) for checking spelling errors
+- [lefthook](https://github.com/evilmartians/lefthook) for managing Git hooks, automatically running code checks and formatting before commits
+
+## ESLint
+
+ESLint is a code standard and error checking tool used to identify and report syntax errors in TypeScript code.
+
+### Command
+
+```bash
+pnpm eslint .
+```
+
+### Configuration
+
+The ESLint configuration file is `eslint.config.mjs`, with its core configuration located in the `internal/lint-configs/eslint-config` directory, which can be modified according to project needs.
+
+## Stylelint
+
+Stylelint is used to check the style of CSS within the project. Coupled with the editor's auto-fix feature, it can effectively unify the CSS style within the project.
+
+### Command
+
+```bash
+pnpm stylelint "**/*.{vue,css,less.scss}"
+```
+
+### Configuration
+
+The Stylelint configuration file is `stylelint.config.mjs`, with its core configuration located in the `internal/lint-configs/stylelint-config` directory, which can be modified according to project needs.
+
+## Prettier
+
+Prettier Can be used to unify project code style, consistent indentation, single and double quotes, trailing commas, and other styles.
+
+### Command
+
+```bash
+pnpm prettier .
+```
+
+### Configuration
+
+The Prettier configuration file is `.prettier.mjs`, with its core configuration located in the `internal/lint-configs/prettier-config` directory, which can be modified according to project needs.
+
+## CommitLint
+
+In a team, everyone's git commit messages can vary widely, making it difficult to ensure standardization without a mechanism. How can standardization be achieved? You might think of using git's hook mechanism to write shell scripts to implement this. Of course, this is possible, but actually, JavaScript has a great tool for implementing this template, which is commitlint (used for verifying the standard of git commit messages).
+
+### Configuration
+
+The CommitLint configuration file is `.commitlintrc.mjs`, with its core configuration located in the `internal/lint-configs/commitlint-config` directory, which can be modified according to project needs.
+
+### Git Commit Standards
+
+Refer to [Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
+
+- `feat` Add new features
+- `fix` Fix problems/BUGs
+- `style` Code style changes that do not affect the outcome
+- `perf` Optimization/performance improvement
+- `refactor` Refactoring
+- `revert` Revert changes
+- `test` Related to tests
+- `docs` Documentation/comments
+- `chore` Dependency updates/scaffold configuration modifications, etc.
+- `workflow` Workflow improvements
+- `ci` Continuous integration
+- `types` Type modifications
+
+### Disabling Git Commit Standard Checks
+
+If you want to disable Git commit standard checks, there are two ways:
+
+::: code-group
+
+```bash [Temporary disable]
+git commit -m 'feat: add home page' --no-verify
+```
+
+```bash [Permanent closed]
+# Comment out the following code in .husky/commit-msg to disable
+pnpm exec commitlint --edit "$1" # [!code --]
+```
+
+:::
+
+## Publint
+
+Publint is a tool for checking the standard of npm packages, which can check whether the package version conforms to the standard, whether it conforms to the standard ESM package specification, etc.
+
+### Command
+
+```bash
+pnpm vsh publint
+```
+
+## Cspell
+
+Cspell is a tool for checking spelling errors, which can check for spelling errors in the code, avoiding bugs caused by spelling errors.
+
+### Command
+
+```bash
+pnpm cspell lint \"**/*.ts\" \"**/README.md\" \".changeset/*.md\" --no-progress
+```
+
+### Configuration
+
+The cspell configuration file is `cspell.json`, which can be modified according to project needs.
+
+## Git Hook
+
+Git hooks are generally combined with various lints to check code style during git commits. If the check fails, the commit will not proceed. Developers need to modify and resubmit.
+
+### lefthook
+
+One issue is that the check will verify all code, but we only want to check the code we are committing. This is where lefthook comes in.
+
+The most effective solution is to perform Lint checks locally before committing. A common practice is to use lefthook to perform a Lint check before local submission.
+
+The project defines corresponding hooks inside `lefthook.yml`:
+
+- `pre-commit`: Runs before commit, used for code formatting and checking
+
+ - `code-workspace`: Updates VSCode workspace configuration
+ - `lint-md`: Formats Markdown files
+ - `lint-vue`: Formats and checks Vue files
+ - `lint-js`: Formats and checks JavaScript/TypeScript files
+ - `lint-style`: Formats and checks style files
+ - `lint-package`: Formats package.json
+ - `lint-json`: Formats other JSON files
+
+- `post-merge`: Runs after merge, used for automatic dependency installation
+
+ - `install`: Runs `pnpm install` to install new dependencies
+
+- `commit-msg`: Runs during commit, used for checking commit message format
+ - `commitlint`: Uses commitlint to check commit messages
+
+#### How to Disable lefthook
+
+If you want to disable lefthook, there are two ways:
+
+::: code-group
+
+```bash [Temporary disable]
+git commit -m 'feat: add home page' --no-verify
+```
+
+```bash [Permanent disable]
+# Simply delete the lefthook.yml file
+rm lefthook.yml
+```
+
+:::
+
+#### How to Modify lefthook Configuration
+
+If you want to modify lefthook's configuration, you can edit the `lefthook.yml` file. For example:
+
+```yaml
+pre-commit:
+ parallel: true # Execute tasks in parallel
+ jobs:
+ - name: lint-js
+ run: pnpm prettier --cache --ignore-unknown --write {staged_files}
+ glob: '*.{js,jsx,ts,tsx}'
+```
+
+Where:
+
+- `parallel`: Whether to execute tasks in parallel
+- `jobs`: Defines the list of tasks to execute
+- `name`: Task name
+- `run`: Command to execute
+- `glob`: File pattern to match
+- `{staged_files}`: Represents the list of staged files
diff --git a/web/docs/src/en/guide/project/tailwindcss.md b/web/docs/src/en/guide/project/tailwindcss.md
new file mode 100644
index 0000000..74df1c8
--- /dev/null
+++ b/web/docs/src/en/guide/project/tailwindcss.md
@@ -0,0 +1,13 @@
+# Tailwind CSS
+
+[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for quickly building custom designs.
+
+## Configuration
+
+The project's configuration file is located in `internal/tailwind-config`, where you can modify the Tailwind CSS configuration.
+
+::: tip Restrictions on using tailwindcss in packages
+
+Tailwind CSS compilation will only be enabled if there is a `tailwind.config.mjs` file present in the corresponding package. Otherwise, Tailwind CSS will not be enabled. If you have a pure SDK package that does not require Tailwind CSS, you do not need to create a `tailwind.config.mjs` file.
+
+:::
diff --git a/web/docs/src/en/guide/project/test.md b/web/docs/src/en/guide/project/test.md
new file mode 100644
index 0000000..5989c32
--- /dev/null
+++ b/web/docs/src/en/guide/project/test.md
@@ -0,0 +1,33 @@
+# Unit Testing
+
+The project incorporates [Vitest](https://vitest.dev/) as the unit testing tool. Vitest is a test runner based on Vite, offering a simple API for writing test cases.
+
+## Writing Test Cases
+
+Within the project, we follow the convention of naming test files with a `.test.ts` suffix or placing them inside a `__tests__` directory. For example, if you create a `utils.ts` file, then you would create a corresponding `utils.spec.ts` file in the same directory,
+
+```ts
+// utils.test.ts
+import { expect, test } from 'vitest';
+import { sum } from './sum';
+
+test('adds 1 + 2 to equal 3', () => {
+ expect(sum(1, 2)).toBe(3);
+});
+```
+
+## Running Tests
+
+To run the tests, execute the following command at the root of the monorepo:
+
+```bash
+pnpm test:unit
+```
+
+## Existing Unit Tests
+
+There are already some unit test cases in the project. You can search for files ending with .test.ts to view them. When you make changes to related code, you can run the unit tests to ensure the correctness of your code. It is recommended to maintain the coverage of unit tests during the development process and to run unit tests as part of the CI/CD process to ensure tests pass before deploying the project.
+
+Existing unit test status:
+
+
diff --git a/web/docs/src/en/guide/project/vite.md b/web/docs/src/en/guide/project/vite.md
new file mode 100644
index 0000000..f458335
--- /dev/null
+++ b/web/docs/src/en/guide/project/vite.md
@@ -0,0 +1,33 @@
+# Vite Config
+
+The project encapsulates a layer of Vite configuration and integrates some plugins for easy reuse across multiple packages and applications. The usage is as follows:
+
+## Application
+
+```ts
+// vite.config.mts
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ // Vite configuration, override according to the official Vite documentation
+ vite: {},
+ };
+});
+```
+
+## Package
+
+```ts
+// vite.config.mts
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ library: {},
+ // Vite configuration, override according to the official Vite documentation
+ vite: {},
+ };
+});
+```
diff --git a/web/docs/src/en/index.md b/web/docs/src/en/index.md
new file mode 100644
index 0000000..42aeef3
--- /dev/null
+++ b/web/docs/src/en/index.md
@@ -0,0 +1,76 @@
+---
+# https://vitepress.dev/reference/default-theme-home-page
+layout: home
+sidebar: false
+
+hero:
+ name: Vben Admin
+ text: Enterprise-Level Management System Framework
+ tagline: Fully Upgraded, Ready to Use, Simple and Efficient
+ image:
+ src: https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp
+ alt: Vben Admin
+ actions:
+ - theme: brand
+ text: Get Started ->
+ link: /en/guide/introduction/vben
+ - theme: alt
+ text: Live Preview
+ link: https://www.vben.pro
+ - theme: alt
+ text: View on GitHub
+ link: https://github.com/vbenjs/vue-vben-admin
+
+features:
+ - icon: 🚀
+ title: Latest Technology Stack
+ details: Based on the latest technology stack, including Vue3, Pinia, Vue Router, TypeScript, etc.
+ link: /en/guide/introduction/quick-start
+ linkText: Get Started
+ - icon: 🦄
+ title: Rich Configurations
+ details: An enterprise-level frontend solution for middle and back-end systems, offering a wealth of components, templates, and various preference settings.
+ link: /en/guide/essentials/settings
+ linkText: Configuration Documentation
+ - icon: 🎨
+ title: Theme Customization
+ details: Easily switch between various themes through simple configurations, catering to personalized needs.
+ link: /en/guide/in-depth/theme
+ linkText: Theme Documentation
+ - icon: 🌐
+ title: Internationalization
+ details: Built-in internationalization support with multiple languages to meet global needs.
+ link: /en/guide/in-depth/locale
+ linkText: Internationalization Documentation
+ - icon: 🔐
+ title: Access Control
+ details: Built-in access control solutions supporting various permission management methods to meet different access requirements.
+ link: /en/guide/in-depth/access
+ linkText: Access Documentation
+ - title: Vite
+ icon:
+ src: /logos/vite.svg
+ details: Modern frontend build tool with fast cold start and instant hot updates.
+ link: https://vitejs.dev/
+ linkText: Official Site
+ - title: Shadcn UI
+ icon:
+ src: /logos/shadcn-ui.svg
+ details: Core built on Shadcn UI + Tailwindcss, with business support for any UI framework.
+ link: https://www.shadcn-vue.com/
+ linkText: Official Site
+ - title: Turbo Repo
+ icon:
+ src: /logos/turborepo.svg
+ details: Standardized monorepo architecture using pnpm + monorepo + turbo for enterprise-level development standards.
+ link: https://turbo.build/
+ linkText: Official Site
+ - title: Nitro Mock Server
+ icon:
+ src: /logos/nitro.svg
+ details: Built-in Nitro Mock service makes your mock service more powerful.
+ link: https://nitro.unjs.io/
+ linkText: Official Site
+---
+
+
diff --git a/web/docs/src/friend-links/index.md b/web/docs/src/friend-links/index.md
new file mode 100644
index 0000000..84b6190
--- /dev/null
+++ b/web/docs/src/friend-links/index.md
@@ -0,0 +1,27 @@
+# 友情链接
+
+如果您的网站是与 Vben Admin 相关的,也属于开源项目,欢迎联系我们,我们会将与您的网站加入交换友情链接。
+
+## 交换方式
+
+### 添加作者,并注明来意
+
+- 通过邮箱联系作者: [ann.vben@gmail.com](mailto:ann.vben@gmail.com)
+- 通过微信联系作者:
+
+
+
+### 提供资料
+
+提供您的网站名称、链接、描述、LOGO(可选)等信息,我们会在第一时间添加您的网站。
+
+### 友情链接
+
+- 在您的网站上添加我们的友情链接,链接如下:
+
+ - 名称:Vben Admin
+ - 链接:https://www.vben.pro
+ - 描述:Vben Admin 企业级开箱即用的中后台前端解决方案
+ - Logo:https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp
+
+我们将定期的检查友情链接,如果发现您的网站已经删除了我们的友情链接以及链接地址是否正确。
diff --git a/web/docs/src/guide/essentials/build.md b/web/docs/src/guide/essentials/build.md
new file mode 100644
index 0000000..134a632
--- /dev/null
+++ b/web/docs/src/guide/essentials/build.md
@@ -0,0 +1,243 @@
+# 构建与部署
+
+::: tip 前言
+
+由于是展示项目,所以打包后相对较大,如果项目中没有用到的插件,可以删除对应的文件或者路由,不引用即可,没有引用就不会打包。
+
+:::
+
+## 构建
+
+项目开发完成之后,执行以下命令进行构建:
+
+**注意:** 请在项目根目录下执行以下命令
+
+```bash
+pnpm build
+```
+
+构建打包成功之后,会在根目录生成对应的应用下的 `dist` 文件夹,里面就是构建打包好的文件,例如: `apps/web-antd/dist/`
+
+## 预览
+
+发布之前可以在本地进行预览,有多种方式,这里介绍两种:
+
+- 使用项目自定的命令进行预览(推荐)
+
+**注意:** 请在项目根目录下执行以下命令
+
+```bash
+pnpm preview
+```
+
+等待构建成功后,访问 `http://localhost:4173` 即可查看效果。
+
+- 本地服务器预览
+
+可以在电脑全局安装 `serve` 服务,如 `live-server`,
+
+```bash
+npm i -g live-server
+```
+
+然后在 `dist` 目录下执行 `live-server` 命令,即可在本地查看效果。
+
+```bash
+cd apps/web-antd/dist
+# 本地预览,默认端口8080
+live-server
+# 指定端口
+live-server --port=9000
+```
+
+## 压缩
+
+### 开启 `gzip` 压缩
+
+需要在打包的时候更改`.env.production`配置:
+
+```bash
+VITE_COMPRESS=gzip
+```
+
+### 开启 `brotli` 压缩
+
+需要在打包的时候更改`.env.production`配置:
+
+```bash
+VITE_COMPRESS=brotli
+```
+
+### 同时开启 `gzip` 和 `brotli` 压缩
+
+需要在打包的时候更改`.env.production`配置:
+
+```bash
+VITE_COMPRESS=gzip,brotli
+```
+
+::: tip 提示
+
+`gzip` 和 `brotli` 都需要安装特定模块才能使用。
+
+:::
+
+::: details gzip 与 brotli 在 nginx 内的配置
+
+```bash
+http {
+ # 开启gzip
+ gzip on;
+ # 开启gzip_static
+ # gzip_static 开启后可能会报错,需要安装相应的模块, 具体安装方式可以自行查询
+ # 只有这个开启,vue文件打包的.gz文件才会有效果,否则不需要开启gzip进行打包
+ gzip_static on;
+ gzip_proxied any;
+ gzip_min_length 1k;
+ gzip_buffers 4 16k;
+ #如果nginx中使用了多层代理 必须设置这个才可以开启gzip。
+ gzip_http_version 1.0;
+ gzip_comp_level 2;
+ gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
+ gzip_vary off;
+ gzip_disable "MSIE [1-6]\.";
+
+ # 开启 brotli压缩
+ # 需要安装对应的nginx模块,具体安装方式可以自行查询
+ # 可以与gzip共存不会冲突
+ brotli on;
+ brotli_comp_level 6;
+ brotli_buffers 16 8k;
+ brotli_min_length 20;
+ brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;
+}
+```
+
+:::
+
+## 构建分析
+
+如果你的构建文件很大,可以通过项目内置 [rollup-plugin-analyzer](https://github.com/doesdev/rollup-plugin-analyzer) 插件进行代码体积分析,从而优化你的代码。只需要在`根目录`下执行以下命令:
+
+```bash
+pnpm run build:analyze
+```
+
+运行之后,在自动打开的页面可以看到具体的体积分布,以分析哪些依赖有问题。
+
+
+
+## 部署
+
+简单的部署只需要将最终生成的静态文件,dist 文件夹的静态文件发布到你的 cdn 或者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径。
+
+例如上传到 nginx 服务器,可以将 dist 文件夹下的文件上传到服务器的 `/srv/www/project/index.html` 目录下,然后访问配置好的域名即可。
+
+```bash
+# nginx配置
+location / {
+ # 不缓存html,防止程序更新后缓存继续生效
+ if ($request_filename ~* .*\.(?:htm|html)$) {
+ add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
+ access_log on;
+ }
+ # 这里是vue打包文件dist内的文件的存放路径
+ root /srv/www/project/;
+ index index.html index.htm;
+}
+```
+
+部署时可能会发现资源路径不对,只需要修改`.env.production`文件即可。
+
+```bash
+# 根据自己路径来配置更改
+# 注意需要以 / 开头和结尾
+VITE_BASE=/
+VITE_BASE=/xxx/
+```
+
+### 前端路由与服务端的结合
+
+项目前端路由使用的是 vue-router,所以你可以选择两种方式:history 和 hash。
+
+- `hash` 默认会在 url 后面拼接`#`
+- `history` 则不会,不过 `history` 需要服务器配合
+
+可在 `.env.production` 内进行 mode 修改
+
+```bash
+VITE_ROUTER_HISTORY=hash
+```
+
+### history 路由模式下服务端配置
+
+开启 `history` 模式需要服务器配置,更多的服务器配置详情可以看 [history-mode](https://router.vuejs.org/guide/essentials/history-mode.html#html5-mode)
+
+这里以 `nginx` 配置为例:
+
+#### 部署到根目录
+
+```bash {5}
+server {
+ listen 80;
+ location / {
+ # 用于配合 History 使用
+ try_files $uri $uri/ /index.html;
+ }
+}
+```
+
+#### 部署到非根目录
+
+- 首先需要在打包的时候更改`.env.production`配置:
+
+```bash
+VITE_BASE = /sub/
+```
+
+- 然后在 nginx 配置文件中配置
+
+```bash {8}
+server {
+ listen 80;
+ server_name localhost;
+ location /sub/ {
+ # 这里是vue打包文件dist内的文件的存放路径
+ alias /srv/www/project/;
+ index index.html index.htm;
+ try_files $uri $uri/ /sub/index.html;
+ }
+}
+```
+
+## 跨域处理
+
+使用 nginx 处理项目部署后的跨域问题
+
+1. 配置前端项目接口地址,在项目目录下的`.env.production`文件中配置:
+
+```bash
+VITE_GLOB_API_URL=/api
+```
+
+2. 在 nginx 配置请求转发到后台
+
+```bash {10-11}
+server {
+ listen 8080;
+ server_name localhost;
+ # 接口代理,用于解决跨域问题
+ location /api {
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # 后台接口地址
+ proxy_pass http://110.110.1.1:8080/api;
+ rewrite "^/api/(.*)$" /$1 break;
+ proxy_redirect default;
+ add_header Access-Control-Allow-Origin *;
+ add_header Access-Control-Allow-Headers X-Requested-With;
+ add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
+ }
+}
+```
diff --git a/web/docs/src/guide/essentials/concept.md b/web/docs/src/guide/essentials/concept.md
new file mode 100644
index 0000000..8f9d8b5
--- /dev/null
+++ b/web/docs/src/guide/essentials/concept.md
@@ -0,0 +1,70 @@
+# 基础概念
+
+新版本中,整体工程进行了重构,现在我们将会介绍一些基础概念,以便于你更好的理解整个文档。请务必仔细阅读这一部分。
+
+## 大仓
+
+大仓指的是整个项目的仓库,包含了所有的代码、包、应用、规范、文档、配置等,也就是一整个 `Monorepo` 目录的所有内容。
+
+## 应用
+
+应用指的是一个完整的项目,一个项目可以包含多个应用,这些项目可以复用大仓内的代码、包、规范等。应用都被放置在 `apps` 目录下。每个应用都是独立的,可以单独运行、构建、测试、部署,可以引入不同的组件库等等。
+
+::: tip
+
+应用不限于前端应用,也可以是后端应用、移动端应用等,例如 `apps/backend-mock`就是一个后端服务。
+
+:::
+
+## 包
+
+包指的是一个独立的模块,可以是一个组件、一个工具、一个库等。包可以被多个应用引用,也可以被其他包引用。包都被放置在 `packages` 目录下。
+
+对于这些包,你可以把它看作是一个独立的 `npm` 包,使用方式与 `npm` 包一样。
+
+### 包引入
+
+在 `package.json` 中引入包:
+
+```json {3}
+{
+ "dependencies": {
+ "@vben/utils": "workspace:*"
+ }
+}
+```
+
+### 包使用
+
+在代码中引入包:
+
+```ts
+import { isString } from '@vben/utils';
+```
+
+## 别名
+
+在项目中,你可以看到一些 `#` 开头的路径,例如: `#/api`、`#/views`, 这些路径都是别名,用于快速定位到某个目录。它不是通过 `vite` 的 `alias` 实现的,而是通过 `Node.js` 本身的 [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) 原理。只需要在 `package.json` 中配置 `imports` 字段即可。
+
+```json {3}
+{
+ "imports": {
+ "#/*": "./src/*"
+ }
+}
+```
+
+为了 IDE 能够识别这些别名,我们还需要在`tsconfig.json`内配置:
+
+```json {5}
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "#/*": ["src/*"]
+ }
+ }
+}
+```
+
+这样,你就可以在代码中使用别名了。
diff --git a/web/docs/src/guide/essentials/development.md b/web/docs/src/guide/essentials/development.md
new file mode 100644
index 0000000..cb55b6b
--- /dev/null
+++ b/web/docs/src/guide/essentials/development.md
@@ -0,0 +1,255 @@
+# 本地开发 {#development}
+
+::: tip 代码获取
+
+如果你还没有获取代码,可以先从 [快速开始](../introduction/quick-start.md) 处开始阅读文档。
+
+:::
+
+## 前置准备
+
+为了更好的开发体验,我们提供了一些工具配置、项目说明,以便于您更好的开发。
+
+### 需要掌握的基础知识
+
+本项目需要一定前端基础知识,请确保掌握 Vue 的基础知识,以便能处理一些常见的问题。建议在开发前先学一下以下内容,提前了解和学习这些知识,会对项目理解非常有帮助:
+
+- [Vue3](https://vuejs.org/)
+- [Tailwind CSS](https://tailwindcss.com/)
+- [TypeScript](https://www.typescriptlang.org/)
+- [Vue Router](https://router.vuejs.org/)
+- [Vitejs](https://vitejs.dev/)
+- [Pnpm](https://pnpm.io/)
+- [Turbo](https://turbo.build/)
+
+### 工具配置
+
+如果您使用的 IDE 是[vscode](https://code.visualstudio.com/)(推荐)的话,可以安装以下工具来提高开发效率及代码格式化:
+
+- [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Vue 官方插件(必备)。
+- [Tailwind CSS](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) - Tailwindcss 提示插件。
+- [CSS Variable Autocomplete](https://marketplace.visualstudio.com/items?itemName=bradlc.vunguyentuan.vscode-css-variables) - Css 变量提示插件。
+- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - Iconify 图标插件
+- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) - i18n 插件
+- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查
+- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化
+- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) - css 格式化
+- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查
+- [DotENV](https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv) - .env 文件 高亮
+
+## Npm Scripts
+
+npm 脚本是项目常见的配置,用于执行一些常见的任务,比如启动项目、打包项目等。以下的脚本都可以在项目根目录的 `package.json` 文件中找到。
+
+执行方式为:`pnpm run [script]` 或 `npm run [script]`。
+
+```json
+{
+ "scripts": {
+ // 构建项目
+ "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
+ // 构建项目并分析
+ "build:analyze": "turbo build:analyze",
+ // 构建本地 docker 镜像
+ "build:docker": "./build-local-docker-image.sh",
+ // 单独构建 web-antd 应用
+ "build:antd": "pnpm run build --filter=@vben/web-antd",
+ // 单独构建文档
+ "build:docs": "pnpm run build --filter=@vben/docs",
+ // 单独构建 web-ele 应用
+ "build:ele": "pnpm run build --filter=@vben/web-ele",
+ // 单独构建 web-naive 应用
+ "build:naive": "pnpm run build --filter=@vben/naive",
+ // 单独构建 playground 应用
+ "build:play": "pnpm run build --filter=@vben/playground",
+ // changeset 版本管理
+ "changeset": "pnpm exec changeset",
+ // 检查项目各种问题
+ "check": "pnpm run check:circular && pnpm run check:dep && pnpm run check:type && pnpm check:cspell",
+ // 检查循环引用
+ "check:circular": "vsh check-circular",
+ // 检查拼写
+ "check:cspell": "cspell lint **/*.ts **/README.md .changeset/*.md --no-progress"
+ // 检查依赖
+ "check:dep": "vsh check-dep",
+ // 检查类型
+ "check:type": "turbo run typecheck",
+ // 清理项目(删除node_modules、dist、.turbo)等目录
+ "clean": "node ./scripts/clean.mjs",
+ // 提交代码
+ "commit": "czg",
+ // 启动项目(默认会运行整个仓库所有包的dev脚本)
+ "dev": "turbo-run dev",
+ // 启动web-antd应用
+ "dev:antd": "pnpm -F @vben/web-antd run dev",
+ // 启动文档
+ "dev:docs": "pnpm -F @vben/docs run dev",
+ // 启动web-ele应用
+ "dev:ele": "pnpm -F @vben/web-ele run dev",
+ // 启动web-naive应用
+ "dev:naive": "pnpm -F @vben/web-naive run dev",
+ // 启动演示应用
+ "dev:play": "pnpm -F @vben/playground run dev",
+ // 格式化代码
+ "format": "vsh lint --format",
+ // lint 代码
+ "lint": "vsh lint",
+ // 依赖安装完成之后,执行所有包的stub脚本
+ "postinstall": "pnpm -r run stub --if-present",
+ // 只允许使用pnpm
+ "preinstall": "npx only-allow pnpm",
+ // lefthook的安装
+ "prepare": "is-ci || lefthook install",
+ // 预览应用
+ "preview": "turbo-run preview",
+ // 包规范检查
+ "publint": "vsh publint",
+ // 删除所有的node_modules、yarn.lock、package.lock.json,重新安装依赖
+ "reinstall": "pnpm clean --del-lock && pnpm install",
+ // 运行 vitest 单元测试
+ "test:unit": "vitest run --dom",
+ // 更新项目依赖
+ "update:deps": " pnpm update --latest --recursive",
+ // changeset生成提交集
+ "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
+ }
+}
+```
+
+## 本地运行项目
+
+如需本地运行文档,并进行调整,可以执行以下命令,执行该命令,你可以选择需要的应用进行开发:
+
+```bash
+pnpm dev
+```
+
+如果你想直接运行某个应用,可以执行以下命令:
+
+运行 `web-antd` 应用:
+
+```bash
+pnpm dev:antd
+```
+
+运行 `web-naive` 应用:
+
+```bash
+pnpm dev:naive
+```
+
+运行 `web-ele` 应用:
+
+```bash
+pnpm dev:ele
+```
+
+运行 `docs` 应用:
+
+```bash
+pnpm dev:docs
+```
+
+## 区分构建环境
+
+在实际的业务开发中,通常会在构建时区分多种环境,如测试环境`test`、生产环境`build`等。
+
+此时可以修改三个文件,在其中增加对应的脚本配置来达到区分生产环境的效果。
+
+以`@vben/web-antd`添加测试环境`test`为例:
+
+- `apps\web-antd\package.json`
+
+```json
+"scripts": {
+ "build:prod": "pnpm vite build --mode production",
+ "build:test": "pnpm vite build --mode test",
+ "build:analyze": "pnpm vite build --mode analyze",
+ "dev": "pnpm vite --mode development",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --noEmit --skipLibCheck"
+},
+```
+
+增加命令`"build:test"`, 并将原`"build"`改为`"build:prod"`以避免同时构建两个环境的包。
+
+- `package.json`
+
+```json
+"scripts": {
+ "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
+ "build:analyze": "turbo build:analyze",
+ "build:antd": "pnpm run build --filter=@vben/web-antd",
+ "build-test:antd": "pnpm run build --filter=@vben/web-antd build:test",
+
+ ······
+}
+```
+
+在根目录`package.json`中加入构建测试环境的命令
+
+- `turbo.json`
+
+```json
+"tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "outputs": [
+ "dist/**",
+ "dist.zip",
+ ".vitepress/dist.zip",
+ ".vitepress/dist/**"
+ ]
+ },
+
+ "build-test:antd": {
+ "dependsOn": ["@vben/web-antd#build:test"],
+ "outputs": ["dist/**"]
+ },
+
+ "@vben/web-antd#build:test": {
+ "dependsOn": ["^build"],
+ "outputs": ["dist/**"]
+ },
+
+ ······
+```
+
+在`turbo.json`中加入相关依赖的命令
+
+## 公共静态资源
+
+项目中需要使用到的公共静态资源,如:图片、静态HTML等,需要在开发中通过 `src="/xxx.png"` 直接引入的。
+
+需要将资源放在对应项目的 `public/static` 目录下。引入的路径为:`src="/static/xxx.png"`。
+
+## DevTools
+
+项目内置了 [Vue DevTools](https://github.com/vuejs/devtools-next) 插件,可以在开发过程中使用。默认关闭,可在`.env.development` 内开启,并重新运行项目即可:
+
+```bash
+VITE_DEVTOOLS=true
+```
+
+开启后,项目运行会在页面底部显示一个 Vue DevTools 的图标,点击即可打开 DevTools。
+
+
+
+## 本地运行文档
+
+如需本地运行文档,并进行调整,可以执行以下命令:
+
+```bash
+pnpm dev:docs
+```
+
+## 问题解决
+
+如果你在使用过程中遇到依赖相关的问题,可以尝试以下重新安装依赖:
+
+```bash
+# 请在项目根目录下执行
+# 该命令会删除整个仓库所有的 node_modules、yarn.lock、package.lock.json后
+# 再进行依赖重新安装(安装速度会明显变慢)。
+pnpm reinstall
+```
diff --git a/web/docs/src/guide/essentials/external-module.md b/web/docs/src/guide/essentials/external-module.md
new file mode 100644
index 0000000..fd8e592
--- /dev/null
+++ b/web/docs/src/guide/essentials/external-module.md
@@ -0,0 +1,58 @@
+# 外部模块
+
+除了项目默认引入的外部模块,有时我们还需要引入其他外部模块。我们以 [ant-design-vue](https://antdv.com/components/overview) 为例:
+
+## 安装依赖
+
+::: tip 安装依赖到指定包
+
+- 由于项目采用了 [pnpm](https://pnpm.io/) 作为包管理工具,所以我们需要使用 `pnpm` 命令来安装依赖。
+- 通过采用了 Monorepo 模块来管理项目,所以我们需要在指定包下安装依赖。安装依赖前请确保已经进入到指定包目录下。
+
+:::
+
+```bash
+# cd /path/to/your/package
+pnpm add ant-design-vue
+```
+
+## 使用
+
+### 全局引入
+
+```ts
+import { createApp } from 'vue';
+import Antd from 'ant-design-vue';
+import App from './App';
+import 'ant-design-vue/dist/reset.css';
+
+const app = createApp(App);
+
+app.use(Antd).mount('#app');
+```
+
+#### 使用
+
+```vue
+
+ text
+
+```
+
+### 局部引入
+
+```vue
+
+
+
+ text
+
+```
+
+::: warning 注意
+
+- 如果组件有依赖样式,则需要再引入样式文件
+
+:::
diff --git a/web/docs/src/guide/essentials/icons.md b/web/docs/src/guide/essentials/icons.md
new file mode 100644
index 0000000..db4f01d
--- /dev/null
+++ b/web/docs/src/guide/essentials/icons.md
@@ -0,0 +1,78 @@
+# 图标
+
+::: tip 关于图标的管理
+
+- 项目的图标主要由`@vben/icons`包提供,建议统一在该包内部管理,以便于统一管理和维护。
+- 如果你使用的是 `Vscode`,推荐安装 [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) 插件,可以方便的查找和使用图标。
+
+:::
+
+项目中有以下多种图标使用方式,可以根据实际情况选择使用:
+
+## Iconify 图标
+
+集成了 [iconify](https://github.com/iconify/iconify) 图标库
+
+### 新增
+
+可在 `packages/icons/src/iconify` 目录下新增图标:
+
+```ts
+// packages/icons/src/iconify/index.ts
+import { createIconifyIcon } from '@vben-core/icons';
+
+export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');
+```
+
+### 使用
+
+```vue
+
+
+
+
+
+
+```
+
+## Svg 图标
+
+没有采用 Svg Sprite 的方式,而是直接引入 Svg 图标,
+
+### 新增
+
+可以在 `packages/icons/src/svg/icons` 目录下新增图标文件`test.svg`, 然后在 `packages/icons/src/svg/index.ts` 中引入:
+
+```ts
+// packages/icons/src/svg/index.ts
+import { createIconifyIcon } from '@vben-core/icons';
+
+const SvgTestIcon = createIconifyIcon('svg:test');
+
+export { SvgTestIcon };
+```
+
+### 使用
+
+```vue
+
+
+
+
+
+
+```
+
+## Tailwind CSS 图标
+
+### 使用
+
+直接添加 Tailwind CSS 的图标类名即可使用,图标类名可查看 [iconify](https://github.com/iconify/iconify) :
+
+```vue
+
+```
diff --git a/web/docs/src/guide/essentials/route.md b/web/docs/src/guide/essentials/route.md
new file mode 100644
index 0000000..985c48a
--- /dev/null
+++ b/web/docs/src/guide/essentials/route.md
@@ -0,0 +1,644 @@
+---
+outline: deep
+---
+
+# 路由和菜单
+
+在项目中,框架提供了一套基础的路由系统,并**根据路由文件自动生成对应的菜单结构**。
+
+## 路由类型
+
+路由分为核心路由、静态路由和动态路由,核心路由是框架内置的路由,包含了根路由、登录路由、404路由等;静态路由是在项目启动时就已经确定的路由;动态路由一般是在用户登录后,根据用户的权限动态生成的路由。
+
+静态路由和动态路由都会走权限控制,可以通过配置路由的 `meta` 属性中的 `authority` 字段来控制权限,可以参考[路由权限控制](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/modules/demos.ts)。
+
+### 核心路由
+
+核心路由是框架内置的路由,包含了根路由、登录路由、404路由等,核心路由的配置在应用下 `src/router/routes/core` 目录下
+
+::: tip
+
+核心路由主要用于框架的基础功能,因此不建议将业务相关的路由放在核心路由中,推荐将业务相关的路由放在静态路由或动态路由中。
+
+:::
+
+### 静态路由
+
+静态路由的配置在应用下 `src/router/routes/index` 目录下,打开注释的文件内容:
+
+::: tip
+
+权限控制是通过路由的 `meta` 属性中的 `authority` 字段来控制的,如果你的页面项目不需要权限控制,可以不设置 `authority` 字段。
+
+:::
+
+```ts
+// 有需要可以自行打开注释,并创建文件夹
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); // [!code --]
+const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); // [!code ++]
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统 */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles) // [!code --]
+const externalRoutes: RouteRecordRaw[] = []; // [!code --]
+const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); // [!code ++]
+```
+
+### 动态路由
+
+动态路由的配置在对应应用 `src/router/routes/modules` 目录下,这个目录下存放了所有的路由文件。每个文件的内容格式如下,与 Vue Router 的路由配置格式一致,以下为二级路由和多级路由的配置。
+
+## 路由定义
+
+静态路由与动态路由的配置方式一致,以下为二级路由和多级路由的配置:
+
+### 二级路由
+
+::: details 二级路由示例代码
+
+```ts
+import type { RouteRecordRaw } from 'vue-router';
+
+import { VBEN_LOGO_URL } from '@vben/constants';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ badgeType: 'dot',
+ badgeVariants: 'destructive',
+ icon: VBEN_LOGO_URL,
+ order: 9999,
+ title: $t('page.vben.title'),
+ },
+ name: 'VbenProject',
+ path: '/vben-admin',
+ redirect: '/vben-admin/about',
+ children: [
+ {
+ name: 'VbenAbout',
+ path: '/vben-admin/about',
+ component: () => import('#/views/_core/about/index.vue'),
+ meta: {
+ badgeType: 'dot',
+ badgeVariants: 'destructive',
+ icon: 'lucide:copyright',
+ title: $t('page.vben.about'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
+```
+
+:::
+
+### 多级路由
+
+::: tip
+
+- 如果没有特殊情况,父级路由的 `redirect` 属性,不需要指定,默认会指向第一个子路由。
+
+:::
+
+::: details 多级路由示例代码
+
+```ts
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: $t('demos.title'),
+ },
+ name: 'Demos',
+ path: '/demos',
+ redirect: '/demos/access',
+ children: [
+ // 嵌套菜单
+ {
+ meta: {
+ icon: 'ic:round-menu',
+ title: $t('demos.nested.title'),
+ },
+ name: 'NestedDemos',
+ path: '/demos/nested',
+ redirect: '/demos/nested/menu1',
+ children: [
+ {
+ name: 'Menu1Demo',
+ path: '/demos/nested/menu1',
+ component: () => import('#/views/demos/nested/menu-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu1'),
+ },
+ },
+ {
+ name: 'Menu2Demo',
+ path: '/demos/nested/menu2',
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu2'),
+ },
+ redirect: '/demos/nested/menu2/menu2-1',
+ children: [
+ {
+ name: 'Menu21Demo',
+ path: '/demos/nested/menu2/menu2-1',
+ component: () => import('#/views/demos/nested/menu-2-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu2_1'),
+ },
+ },
+ ],
+ },
+ {
+ name: 'Menu3Demo',
+ path: '/demos/nested/menu3',
+ meta: {
+ icon: 'ic:round-menu',
+ title: $t('demos.nested.menu3'),
+ },
+ redirect: '/demos/nested/menu3/menu3-1',
+ children: [
+ {
+ name: 'Menu31Demo',
+ path: 'menu3-1',
+ component: () => import('#/views/demos/nested/menu-3-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu3_1'),
+ },
+ },
+ {
+ name: 'Menu32Demo',
+ path: 'menu3-2',
+ meta: {
+ icon: 'ic:round-menu',
+ title: $t('demos.nested.menu3_2'),
+ },
+ redirect: '/demos/nested/menu3/menu3-2/menu3-2-1',
+ children: [
+ {
+ name: 'Menu321Demo',
+ path: '/demos/nested/menu3/menu3-2/menu3-2-1',
+ component: () =>
+ import('#/views/demos/nested/menu-3-2-1.vue'),
+ meta: {
+ icon: 'ic:round-menu',
+ keepAlive: true,
+ title: $t('demos.nested.menu3_2_1'),
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export default routes;
+```
+
+:::
+
+## 新增页面
+
+新增一个页面,你只需要添加一个路由及对应的页面组件即可。
+
+### 添加路由
+
+在对应的路由文件中添加一个路由对象,如下:
+
+```ts
+import type { RouteRecordRaw } from 'vue-router';
+
+import { VBEN_LOGO_URL } from '@vben/constants';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'mdi:home',
+ title: $t('page.home.title'),
+ },
+ name: 'Home',
+ path: '/home',
+ redirect: '/home/index',
+ children: [
+ {
+ name: 'HomeIndex',
+ path: '/home/index',
+ component: () => import('#/views/home/index.vue'),
+ meta: {
+ icon: 'mdi:home',
+ title: $t('page.home.index'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
+```
+
+### 添加页面组件
+
+在`#/views/home/`下,新增一个`index.vue`文件,如下:
+
+```vue
+
+
+
home page
+
+
+```
+
+### 验证
+
+到这里页面已添加完成,访问 `http://localhost:5555/home/index` 出现对应的页面即可。
+
+## 路由配置
+
+路由配置项主要在对象路由的 `meta` 属性中,以下为常用的配置项:
+
+```ts {5-8}
+const routes = [
+ {
+ name: 'HomeIndex',
+ path: '/home/index',
+ meta: {
+ icon: 'mdi:home',
+ title: $t('page.home.index'),
+ },
+ },
+];
+```
+
+::: details 路由Meta配置类型定义
+
+```ts
+interface RouteMeta {
+ /**
+ * 激活图标(菜单)
+ */
+ activeIcon?: string;
+ /**
+ * 当前激活的菜单,有时候不想激活现有菜单,需要激活父级菜单时使用
+ */
+ activePath?: string;
+ /**
+ * 是否固定标签页
+ * @default false
+ */
+ affixTab?: boolean;
+ /**
+ * 固定标签页的顺序
+ * @default 0
+ */
+ affixTabOrder?: number;
+ /**
+ * 需要特定的角色标识才可以访问
+ * @default []
+ */
+ authority?: string[];
+ /**
+ * 徽标
+ */
+ badge?: string;
+ /**
+ * 徽标类型
+ */
+ badgeType?: 'dot' | 'normal';
+ /**
+ * 徽标颜色
+ */
+ badgeVariants?:
+ | 'default'
+ | 'destructive'
+ | 'primary'
+ | 'success'
+ | 'warning'
+ | string;
+ /**
+ * 路由的完整路径作为key(默认true)
+ */
+ fullPathKey?: boolean;
+ /**
+ * 当前路由的子级在菜单中不展现
+ * @default false
+ */
+ hideChildrenInMenu?: boolean;
+ /**
+ * 当前路由在面包屑中不展现
+ * @default false
+ */
+ hideInBreadcrumb?: boolean;
+ /**
+ * 当前路由在菜单中不展现
+ * @default false
+ */
+ hideInMenu?: boolean;
+ /**
+ * 当前路由在标签页不展现
+ * @default false
+ */
+ hideInTab?: boolean;
+ /**
+ * 图标(菜单/tab)
+ */
+ icon?: string;
+ /**
+ * iframe 地址
+ */
+ iframeSrc?: string;
+ /**
+ * 忽略权限,直接可以访问
+ * @default false
+ */
+ ignoreAccess?: boolean;
+ /**
+ * 开启KeepAlive缓存
+ */
+ keepAlive?: boolean;
+ /**
+ * 外链-跳转路径
+ */
+ link?: string;
+ /**
+ * 路由是否已经加载过
+ */
+ loaded?: boolean;
+ /**
+ * 标签页最大打开数量
+ * @default false
+ */
+ maxNumOfOpenTab?: number;
+ /**
+ * 菜单可以看到,但是访问会被重定向到403
+ */
+ menuVisibleWithForbidden?: boolean;
+ /**
+ * 当前路由不使用基础布局(仅在顶级生效)
+ */
+ noBasicLayout?: boolean;
+ /**
+ * 在新窗口打开
+ */
+ openInNewWindow?: boolean;
+ /**
+ * 用于路由->菜单排序
+ */
+ order?: number;
+ /**
+ * 菜单所携带的参数
+ */
+ query?: Recordable;
+ /**
+ * 标题名称
+ */
+ title: string;
+}
+```
+
+:::
+
+### title
+
+- 类型:`string`
+- 默认值:`''`
+
+用于配置页面的标题,会在菜单和标签页中显示。一般会配合国际化使用。
+
+### icon
+
+- 类型:`string`
+- 默认值:`''`
+
+用于配置页面的图标,会在菜单和标签页中显示。一般会配合图标库使用,如果是`http`链接,会自动加载图片。
+
+### activeIcon
+
+- 类型:`string`
+- 默认值:`''`
+
+用于配置页面的激活图标,会在菜单中显示。一般会配合图标库使用,如果是`http`链接,会自动加载图片。
+
+### keepAlive
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面是否开启缓存,开启后页面会缓存,不会重新加载,仅在标签页启用时有效。
+
+### hideInMenu
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面是否在菜单中隐藏,隐藏后页面不会在菜单中显示。
+
+### hideInTab
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面是否在标签页中隐藏,隐藏后页面不会在标签页中显示。
+
+### hideInBreadcrumb
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面是否在面包屑中隐藏,隐藏后页面不会在面包屑中显示。
+
+### hideChildrenInMenu
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面的子页面是否在菜单中隐藏,隐藏后子页面不会在菜单中显示。
+
+### authority
+
+- 类型:`string[]`
+- 默认值:`[]`
+
+用于配置页面的权限,只有拥有对应权限的用户才能访问页面,不配置则不需要权限。
+
+### badge
+
+- 类型:`string`
+- 默认值:`''`
+
+用于配置页面的徽标,会在菜单显示。
+
+### badgeType
+
+- 类型:`'dot' | 'normal'`
+- 默认值:`'normal'`
+
+用于配置页面的徽标类型,`dot` 为小红点,`normal` 为文本。
+
+### badgeVariants
+
+- 类型:`'default' | 'destructive' | 'primary' | 'success' | 'warning' | string`
+- 默认值:`'success'`
+
+用于配置页面的徽标颜色。
+
+### fullPathKey
+
+- 类型:`boolean`
+- 默认值:`true`
+
+是否将路由的完整路径作为tab key(默认true)
+
+### activePath
+
+- 类型:`string`
+- 默认值:`''`
+
+用于配置当前激活的菜单,有时候页面没有显示在菜单内,需要激活父级菜单时使用。
+
+### affixTab
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面是否固定标签页,固定后页面不可关闭。
+
+### affixTabOrder
+
+- 类型:`number`
+- 默认值:`0`
+
+用于配置页面固定标签页的排序, 采用升序排序。
+
+### iframeSrc
+
+- 类型:`string`
+- 默认值:`''`
+
+用于配置内嵌页面的 `iframe` 地址,设置后会在当前页面内嵌对应的页面。
+
+### ignoreAccess
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面是否忽略权限,直接可以访问。
+
+### link
+
+- 类型:`string`
+- 默认值:`''`
+
+用于配置外链跳转路径,会在新窗口打开。
+
+### maxNumOfOpenTab
+
+- 类型:`number`
+- 默认值:`-1`
+
+用于配置标签页最大打开数量,设置后会在打开新标签页时自动关闭最早打开的标签页(仅在打开同名标签页时生效)。
+
+### menuVisibleWithForbidden
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置页面在菜单可以看到,但是访问会被重定向到403。
+
+### openInNewWindow
+
+- 类型:`boolean`
+- 默认值:`false`
+
+设置为 `true` 时,会在新窗口打开页面。
+
+### order
+
+- 类型:`number`
+- 默认值:`0`
+
+用于配置页面的排序,用于路由到菜单排序。
+
+_注意:_ 排序仅针对一级菜单有效,二级菜单的排序需要在对应的一级菜单中按代码顺序设置。
+
+### query
+
+- 类型:`Recordable`
+- 默认值:`{}`
+
+用于配置页面的菜单参数,会在菜单中传递给页面。
+
+### noBasicLayout
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置当前路由不使用基础布局,仅在顶级时生效。默认情况下,所有的路由都会被包裹在基础布局中(包含顶部以及侧边等导航部件),如果你的页面不需要这些部件,可以设置 `noBasicLayout` 为 `true`。
+
+## 路由刷新
+
+路由刷新方式如下:
+
+```vue
+
+```
+
+## 标签页与路由控制
+
+在某些场景下,需要单个路由打开多个标签页,或者修改路由的query不打开新的标签页
+
+每个标签页Tab使用唯一的key标识,设置Tab key有三种方式,优先级由高到低:
+
+- 使用路由query参数pageKey
+
+```vue
+
+
+
+
+
+```
+
+## CSS Modules
+
+针对样式冲突问题,还有一种方案是使用 `CSS Modules` 模块化方案。使用方式如下。
+
+```vue
+
+ This should be red
+
+
+
+```
+
+更多用法可以见 [CSS Modules 官方文档](https://vuejs.org/api/sfc-css-features.html#css-modules)。
diff --git a/web/docs/src/guide/in-depth/access.md b/web/docs/src/guide/in-depth/access.md
new file mode 100644
index 0000000..bbce30a
--- /dev/null
+++ b/web/docs/src/guide/in-depth/access.md
@@ -0,0 +1,357 @@
+---
+outline: deep
+---
+
+# 权限
+
+框架内置了三种权限控制方式:
+
+- 通过用户角色来判断菜单或者按钮是否可以访问
+- 通过接口来判断菜单或者按钮是否可以访问
+- 混合模式:同时使用前端和后端权限控制
+
+## 前端访问控制
+
+**实现原理**: 在前端固定写死路由的权限,指定路由有哪些权限可以查看。只初始化通用的路由,需要权限才能访问的路由没有被加入路由表内。在登录后或者其他方式获取用户角色后,通过角色去遍历路由表,获取该角色可以访问的路由表,生成路由表,再通过 `router.addRoute` 添加到路由实例,实现权限的过滤。
+
+**缺点**: 权限相对不自由,如果后台改动角色,前台也需要跟着改动。适合角色较固定的系统
+
+### 步骤
+
+- 确保当前模式为前端访问控制模式
+
+调整对应应用目录下的`preferences.ts`,确保`accessMode='frontend'`。
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ // 默认值,可不填
+ accessMode: 'frontend',
+ },
+});
+```
+
+- 配置路由权限
+
+**如果不配置,默认可见**
+
+```ts {3}
+ {
+ meta: {
+ authority: ['super'],
+ },
+},
+```
+
+- 确保接口返回的角色和路由表的权限匹配
+
+可查看应用下的 `src/store/auth`,找到下面代码,
+
+```ts
+// 设置登录用户信息,需要确保 userInfo.roles 是一个数组,且包含路由表中的权限
+// 例如:userInfo.roles=['super', 'admin']
+authStore.setUserInfo(userInfo);
+```
+
+到这里,就已经配置完成,你需要确保登录后,接口返回的角色和路由表的权限匹配,否则无法访问。
+
+### 菜单可见,但禁止访问
+
+有时候,我们需要菜单可见,但是禁止访问,可以通过下面的方式实现,设置 `menuVisibleWithForbidden` 为 `true`,此时菜单可见,但是禁止访问,会跳转403页面。
+
+```ts
+{
+ meta: {
+ menuVisibleWithForbidden: true,
+ },
+},
+```
+
+## 后端访问控制
+
+**实现原理**: 是通过接口动态生成路由表,且遵循一定的数据结构返回。前端根据需要处理该数据为可识别的结构,再通过 `router.addRoute` 添加到路由实例,实现权限的动态生成。
+
+**缺点**: 后端需要提供符合规范的数据结构,前端需要处理数据结构,适合权限较为复杂的系统。
+
+### 步骤
+
+- 确保当前模式为后端访问控制模式
+
+调整对应应用目录下的`preferences.ts`,确保`accessMode='backend'`。
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ accessMode: 'backend',
+ },
+});
+```
+
+- 确保接口返回的菜单数据结构正确
+
+可查看应用下的 `src/router/access.ts`,找到下面代码,
+
+```ts {5}
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ return await generateAccessible(preferences.app.accessMode, {
+ fetchMenuListAsync: async () => {
+ // 这个接口为后端返回的菜单数据
+ return await getAllMenus();
+ },
+ });
+}
+```
+
+- 接口返回菜单数据,可看注释说明
+
+::: details 接口返回菜单数据示例
+
+```ts
+const dashboardMenus = [
+ {
+ meta: {
+ order: -1,
+ title: 'page.dashboard.title',
+ },
+ name: 'Dashboard',
+ path: '/',
+ redirect: '/analytics',
+ children: [
+ {
+ name: 'Analytics',
+ path: '/analytics',
+ // 这里为页面的路径,需要去掉 views/ 和 .vue
+ component: '/dashboard/analytics/index',
+ meta: {
+ affixTab: true,
+ title: 'page.dashboard.analytics',
+ },
+ },
+ {
+ name: 'Workspace',
+ path: '/workspace',
+ component: '/dashboard/workspace/index',
+ meta: {
+ title: 'page.dashboard.workspace',
+ },
+ },
+ ],
+ },
+ {
+ name: 'Test',
+ path: '/test',
+ component: '/test/index',
+ meta: {
+ title: 'page.test',
+ // 部分特殊页面如果不需要基础布局(页面顶部和侧边栏),可将noBasicLayout设置为true
+ noBasicLayout: true,
+ },
+ },
+];
+```
+
+:::
+
+到这里,就已经配置完成,你需要确保登录后,接口返回的菜单格式正确,否则无法访问。
+
+## 混合访问控制
+
+**实现原理**: 混合模式同时结合了前端访问控制和后端访问控制两种方式。系统会并行处理前端固定路由权限和后端动态菜单数据,最终将两部分路由合并,提供更灵活的权限控制方案。
+
+**优点**: 兼具前端控制的性能优势和后端控制的灵活性,适合复杂业务场景下的权限管理。
+
+### 步骤
+
+- 确保当前模式为混合访问控制模式
+
+调整对应应用目录下的`preferences.ts`,确保`accessMode='mixed'`。
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ accessMode: 'mixed',
+ },
+});
+```
+
+- 配置前端路由权限
+
+同[前端访问控制](#前端访问控制)模式的路由权限配置方式。
+
+- 配置后端菜单接口
+
+同[后端访问控制](#后端访问控制)模式的接口配置方式。
+
+- 确保角色和权限匹配
+
+需要同时满足前端路由权限配置和后端菜单数据返回的要求,确保用户角色与两种模式的权限配置都匹配。
+
+到这里,就已经配置完成,混合模式会自动合并前端和后端的路由,提供完整的权限控制功能。
+
+## 按钮细粒度控制
+
+在某些情况下,我们需要对按钮进行细粒度的控制,我们可以借助接口或者角色来控制按钮的显示。
+
+### 权限码
+
+权限码为接口返回的权限码,通过权限码来判断按钮是否显示,逻辑在`src/store/auth`下:
+
+```ts
+const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ fetchUserInfo(),
+ getAccessCodes(),
+]);
+
+userInfo = fetchUserInfoResult;
+authStore.setUserInfo(userInfo);
+accessStore.setAccessCodes(accessCodes);
+```
+
+找到 `getAccessCodes` 对应的接口,可根据业务逻辑进行调整。
+
+权限码返回的数据结构为字符串数组,例如:`['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010']`
+
+有了权限码,就可以使用 `@vben/access` 提供的`AccessControl`组件及API来进行按钮的显示与隐藏。
+
+#### 组件方式
+
+```vue
+
+
+
+
+
+ Super 账号可见 ["AC_1000001"]
+
+
+ Admin 账号可见 ["AC_100010"]
+
+
+ User 账号可见 ["AC_1000001"]
+
+
+ Super & Admin 账号可见 ["AC_100100","AC_1000001"]
+
+
+```
+
+#### API方式
+
+```vue
+
+
+
+
+ Super 账号可见 ["AC_1000001"]
+
+
+ Admin 账号可见 ["AC_100010"]
+
+
+ User 账号可见 ["AC_1000001"]
+
+
+ Super & Admin 账号可见 ["AC_100100","AC_1000001"]
+
+
+```
+
+#### 指令方式
+
+> 指令支持绑定单个或多个权限码。单个时可以直接传入字符串或数组中包含一个权限码,多个权限码则传入数组。
+
+```vue
+
+
+ Super 账号可见 'AC_100100'
+
+
+ Admin 账号可见 ["AC_100010"]
+
+
+ User 账号可见 ["AC_1000001"]
+
+
+ Super & Admin 账号可见 ["AC_100100","AC_1000001"]
+
+
+```
+
+### 角色
+
+角色判断方式不需要接口返回的权限码,直接通过角色来判断按钮是否显示。
+
+#### 组件方式
+
+```vue
+
+
+
+
+ Super 角色可见
+
+
+ Admin 角色可见
+
+
+ User 角色可见
+
+
+ Super & Admin 角色可见
+
+
+```
+
+#### API方式
+
+```vue
+
+
+
+ Super 账号可见
+ Admin 账号可见
+ User 账号可见
+
+ Super & Admin 账号可见
+
+
+```
+
+#### 指令方式
+
+> 指令支持绑定单个或多个角色。单个时可以直接传入字符串或数组中包含一个角色,多个角色均可访问则传入数组。
+
+```vue
+
+ Super 角色可见
+ Super 角色可见
+ Admin 角色可见
+ User 角色可见
+
+ Super & Admin 角色可见
+
+
+```
diff --git a/web/docs/src/guide/in-depth/check-updates.md b/web/docs/src/guide/in-depth/check-updates.md
new file mode 100644
index 0000000..d4a74da
--- /dev/null
+++ b/web/docs/src/guide/in-depth/check-updates.md
@@ -0,0 +1,92 @@
+# 检查更新
+
+## 介绍
+
+当网站有更新时,您可能需要检查更新。框架提供了这一功能,通过定时检查更新,您可以在应用的 preferences.ts 文件中配置 `checkUpdatesInterval`和 `enableCheckUpdates` 字段,以开启和设置检查更新的时间间隔(单位:分钟)。
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ // 是否开启检查更新
+ enableCheckUpdates: true,
+ // 检查更新的时间间隔,单位为分钟
+ checkUpdatesInterval: 1,
+ },
+});
+```
+
+## 效果
+
+检测到更新时,会弹出提示框,询问用户是否刷新页面:
+
+
+
+## 替换为其他检查更新方式
+
+如果需要通过其他方式检查更新,例如通过接口来更灵活地控制更新逻辑(如强制刷新、显示更新内容等),你可以通过修改 `@vben/layouts` 下面的 `src/widgets/check-updates/check-updates.vue`文件来实现。
+
+```ts
+// 这里可以替换为你的检查更新逻辑
+async function getVersionTag() {
+ try {
+ const response = await fetch('/', {
+ cache: 'no-cache',
+ method: 'HEAD',
+ });
+
+ return (
+ response.headers.get('etag') || response.headers.get('last-modified')
+ );
+ } catch {
+ console.error('Failed to fetch version tag');
+ return null;
+ }
+}
+```
+
+## 替换为第三方库检查更新方式
+
+如果需要通过其他方式检查更新,例如使用其他版本控制方式(chunkHash、version.json)、使用`Web Worker`在后台轮询更新、自定义检查更新时机(不使用轮询),你可以通过JS库`version-polling`来实现。
+
+```bash
+pnpm add version-polling
+```
+
+以`apps/web-antd`项目为例,在项目入口文件`main.ts`或者`app.vue`添加以下代码
+
+```ts
+import { h } from 'vue';
+
+import { Button, notification } from 'ant-design-vue';
+import { createVersionPolling } from 'version-polling';
+
+createVersionPolling({
+ silent: import.meta.env.MODE === 'development', // 开发环境下不检测
+ onUpdate: (self) => {
+ const key = `open${Date.now()}`;
+ notification.info({
+ message: '提示',
+ description: '检测到网页有更新, 是否刷新页面加载最新版本?',
+ btn: () =>
+ h(
+ Button,
+ {
+ type: 'primary',
+ size: 'small',
+ onClick: () => {
+ notification.close(key);
+ self.onRefresh();
+ },
+ },
+ { default: () => '刷新' },
+ ),
+ key,
+ duration: null,
+ placement: 'bottomRight',
+ });
+ },
+});
+```
diff --git a/web/docs/src/guide/in-depth/features.md b/web/docs/src/guide/in-depth/features.md
new file mode 100644
index 0000000..53b9670
--- /dev/null
+++ b/web/docs/src/guide/in-depth/features.md
@@ -0,0 +1,84 @@
+# 常用功能
+
+一些常用的功能合集。
+
+## 登录认证过期
+
+当接口返回`401`状态码时,框架会认为登录认证过期,登录超时会跳转到登录页或者打开登录弹窗。在应用目录下的`preferences.ts`可以配置:
+
+### 跳转登录页面
+
+登录超时会跳转到登录页
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ loginExpiredMode: 'page',
+ },
+});
+```
+
+### 打开登录弹窗
+
+登录超时会打开登录弹窗
+
+
+
+配置:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ loginExpiredMode: 'modal',
+ },
+});
+```
+
+## 动态标题
+
+- 默认值:`true`
+
+开启后网页标题随着路由的`title`而变化。在应用目录下的`preferences.ts`,开启或者关闭即可。
+
+```ts
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ dynamicTitle: true,
+ },
+});
+```
+
+## 页面水印
+
+- 默认值:`false`
+
+开启后网页会显示水印,在应用目录下的`preferences.ts`,开启或者关闭即可。
+
+```ts
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ watermark: true,
+ },
+});
+```
+
+如果你想更新水印的内容,可以这么做,参数可以参考 [watermark-js-plus](https://zhensherlock.github.io/watermark-js-plus/):
+
+```ts
+import { useWatermark } from '@vben/hooks';
+
+const { destroyWatermark, updateWatermark } = useWatermark();
+
+await updateWatermark({
+ // 水印内容
+ content: 'hello my watermark',
+});
+```
diff --git a/web/docs/src/guide/in-depth/layout.md b/web/docs/src/guide/in-depth/layout.md
new file mode 100644
index 0000000..f8317bb
--- /dev/null
+++ b/web/docs/src/guide/in-depth/layout.md
@@ -0,0 +1 @@
+# 布局
diff --git a/web/docs/src/guide/in-depth/loading.md b/web/docs/src/guide/in-depth/loading.md
new file mode 100644
index 0000000..60e6937
--- /dev/null
+++ b/web/docs/src/guide/in-depth/loading.md
@@ -0,0 +1,46 @@
+# 全局loading
+
+全局 loading 指的是页面刷新时出现的加载效果,通常是一个旋转的图标:
+
+
+
+## 原理
+
+由 `vite-plugin-inject-app-loading` 插件实现,插件会在每个应用都注入一个全局的 `loading html`。
+
+## 关闭
+
+如果你不需要全局 loading,可以在 `.env` 文件中关闭:
+
+```bash
+VITE_INJECT_APP_LOADING=false
+```
+
+## 自定义
+
+如果你想要自定义全局 loading,可以在应用目录下,与`index.html`同级,创建一个`loading.html`文件,插件会自动读取并注入。这个html可以自行定义样式和动画。
+
+::: tip
+
+- 你可以使用跟`index.html`一样的语法,比如`VITE_APP_TITLE`变量,来获取应用的标题。
+- 必须保证有一个`id="__app-loading__"`的元素。
+- 给`id="__app-loading__"`的元素,加一个 `hidden` class。
+- 必须保证有一个`style[data-app-loading="inject-css"]`的元素。
+
+```html{1,4}
+
+
+
+
<%= VITE_APP_TITLE %>
+
+```
+
+:::
diff --git a/web/docs/src/guide/in-depth/locale.md b/web/docs/src/guide/in-depth/locale.md
new file mode 100644
index 0000000..e4a4a65
--- /dev/null
+++ b/web/docs/src/guide/in-depth/locale.md
@@ -0,0 +1,227 @@
+# 国际化
+
+项目已经集成了 [Vue i18n](https://kazupon.github.io/vue-i18n/),并且已经配置好了中文和英文的语言包。
+
+## IDE 插件
+
+如果你使用的 vscode 开发工具,则推荐安装 [i18n Ally](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) 这个插件。它可以帮助你更方便的管理国际化的文案,安装了该插件后,你的代码内可以实时看到对应的语言内容:
+
+
+
+## 配置默认语言
+
+只需要覆盖默认的偏好设置即可,在对应的应用内,找到 `src/preferences.ts` 文件,修改 `locale` 的值即可:
+
+```ts {3}
+export const overridesPreferences = defineOverridesPreferences({
+ app: {
+ locale: 'en-US',
+ },
+});
+```
+
+## 动态切换语言
+
+切换语言有两部分组成:
+
+- 更新偏好设置
+- 加载对应的语言包
+
+```ts
+import type { SupportedLanguagesType } from '@vben/locales';
+import { loadLocaleMessages } from '@vben/locales';
+import { updatePreferences } from '@vben/preferences';
+
+async function updateLocale(value: string) {
+ // 1. 更新偏好设置
+ const locale = value as SupportedLanguagesType;
+ updatePreferences({
+ app: {
+ locale,
+ },
+ });
+ // 2. 加载对应的语言包
+ await loadLocaleMessages(locale);
+}
+
+updateLocale('en-US');
+```
+
+## 新增翻译文本
+
+::: warning 注意
+
+- 请不要将业务翻译文本放到 `@vben/locales` 内,这样可以更好的管理业务和通用的翻译文本。
+- 有多个语言包的情况下,新增翻译文本时,需要在所有语言包内新增对应的文本。
+
+:::
+
+新增翻译文本,只需要在对应的应用内,找到 `src/locales/langs/`,新增对应的文本即可,例:
+
+**src/locales/langs/zh-CN/\*.json**
+
+````ts
+```json
+{
+ "about": {
+ "desc": "Vben Admin 是一个现代的管理模版。"
+ }
+}
+````
+
+**src/locales/langs/en-US.ts**
+
+````ts
+```json
+{
+ "about": {
+ "desc": "Vben Admin is a modern management template."
+ }
+}
+````
+
+## 使用翻译文本
+
+通过 `@vben/locales`,你可以很方便的使用翻译文本:
+
+### 在代码中使用
+
+```vue
+
+
+ {{ $t('about.desc') }}
+
+ {{ item.title }}
+
+
+```
+
+## 新增语言包
+
+如果你需要新增语言包,需要按照以下步骤进行:
+
+- 在 `packages/locales/langs` 目录下新增对应的语言包文件,例:`zh-TW.json`,并翻译对应的文本。
+- 在对应的应用内,找到 `src/locales/langs` 文件,新增对应的语言包 `zh-TW.json`
+- 在 `packages/constants/src/core.ts`内,新增对应的语言:
+
+ ```ts
+ export interface LanguageOption {
+ label: string;
+ value: 'en-US' | 'zh-CN'; // [!code --]
+ value: 'en-US' | 'zh-CN' | 'zh-TW'; // [!code ++]
+ }
+ export const SUPPORT_LANGUAGES: LanguageOption[] = [
+ {
+ label: '简体中文',
+ value: 'zh-CN',
+ },
+ {
+ label: 'English',
+ value: 'en-US',
+ },
+ {
+ label: '繁体中文', // [!code ++]
+ value: 'zh-TW', // [!code ++]
+ },
+ ];
+ ```
+
+- 在 `packages/locales/typing.ts`内,新增 Typescript 类型:
+
+ ```ts
+ export type SupportedLanguagesType = 'en-US' | 'zh-CN'; // [!code --]
+ export type SupportedLanguagesType = 'en-US' | 'zh-CN' | 'zh-TW'; // [!code ++]
+ ```
+
+到这里,你就可以在项目内使用新增的语言包了。
+
+## 界面切换语言功能
+
+如果你想关闭界面上的语言切换显示按钮,在对应的应用内,找到 `src/preferences.ts` 文件,修改 `locale` 的值即可:
+
+```ts {3}
+export const overridesPreferences = defineOverridesPreferences({
+ widget: {
+ languageToggle: false,
+ },
+});
+```
+
+## 远程加载语言包
+
+::: tip 提示
+
+通过项目自带的`request`工具进行接口请求时,默认请求头里会带上 [Accept-Language](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Language) ,服务端可根据请求头进行动态数据国际化处理。
+
+:::
+
+每个应用都有一个独立的语言包,它可以覆盖通用的语言配置,你可以通过远程加载的方式来获取对应的语言包,只需要在对应的应用内,找到 `src/locales/index.ts` 文件,修改 `loadMessages` 方法即可:
+
+```ts {3-4}
+async function loadMessages(lang: SupportedLanguagesType) {
+ const [appLocaleMessages] = await Promise.all([
+ // 这里修改为远程接口加载数据即可
+ localesMap[lang](),
+ loadThirdPartyMessage(lang),
+ ]);
+ return appLocaleMessages.default;
+}
+```
+
+## 第三方语言包
+
+不同应用内使用的第三方组件库或者插件国际化方式可能不一致,所以需要差别处理。 如果你需要引入第三方的语言包,你可以在对应的应用内,找到 `src/locales/index.ts` 文件,修改 `loadThirdPartyMessage` 方法即可:
+
+```ts
+/**
+ * 加载dayjs的语言包
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+ let locale;
+ switch (lang) {
+ case 'zh-CN': {
+ locale = await import('dayjs/locale/zh-cn');
+ break;
+ }
+ case 'en-US': {
+ locale = await import('dayjs/locale/en');
+ break;
+ }
+ // 默认使用英语
+ default: {
+ locale = await import('dayjs/locale/en');
+ }
+ }
+ if (locale) {
+ dayjs.locale(locale);
+ } else {
+ console.error(`Failed to load dayjs locale for ${lang}`);
+ }
+}
+```
+
+## 移除国际化
+
+首先,不是很建议移除国际化,因为国际化是一个很好的开发习惯,但是如果你真的需要移除国际化,你可以直接使用中文文案,然后保留项目自带的语言包即可,整体开发体验不会影响。移除国际化的步骤如下:
+
+- 隐藏界面上的语言切换按钮,见:[界面切换语言功能](#界面切换语言功能)
+- 修改默认语言,见:[配置默认语言](#配置默认语言)
+- 关闭 `vue-i18n`的警告提示,在`src/locales/index.ts`文件内,修改`missingWarn`为`false`即可:
+
+ ```ts
+ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD, // [!code --]
+ missingWarn: false, // [!code ++]
+ ...options,
+ });
+ }
+ ```
diff --git a/web/docs/src/guide/in-depth/login.md b/web/docs/src/guide/in-depth/login.md
new file mode 100644
index 0000000..adb89b7
--- /dev/null
+++ b/web/docs/src/guide/in-depth/login.md
@@ -0,0 +1,220 @@
+---
+outline: deep
+---
+
+# 登录
+
+本文介绍如何去改造自己的应用程序登录页以及如何快速的对接登录页面接口。
+
+## 登录页面调整
+
+如果你想调整登录页面的标题、描述和图标以及工具栏,你可以通过配置 `AuthPageLayout` 组件的参数来实现。
+
+
+
+只需要在应用下的 `src/layouts/auth.vue` 内,配置`AuthPageLayout`的 `props`参数即可:
+
+```vue {2-7}
+
+
+```
+
+## 登录表单调整
+
+如果你想调整登录表单的相关内容,你可以在应用下的 `src/views/_core/authentication/login.vue` 内,配置`AuthenticationLogin` 组件参数即可:
+
+```vue
+
+```
+
+::: details AuthenticationLogin 组件参数
+
+```ts
+{
+ /**
+ * @zh_CN 验证码登录路径
+ */
+ codeLoginPath?: string;
+ /**
+ * @zh_CN 忘记密码路径
+ */
+ forgetPasswordPath?: string;
+
+ /**
+ * @zh_CN 是否处于加载处理状态
+ */
+ loading?: boolean;
+
+ /**
+ * @zh_CN 二维码登录路径
+ */
+ qrCodeLoginPath?: string;
+
+ /**
+ * @zh_CN 注册路径
+ */
+ registerPath?: string;
+
+ /**
+ * @zh_CN 是否显示验证码登录
+ */
+ showCodeLogin?: boolean;
+ /**
+ * @zh_CN 是否显示忘记密码
+ */
+ showForgetPassword?: boolean;
+
+ /**
+ * @zh_CN 是否显示二维码登录
+ */
+ showQrcodeLogin?: boolean;
+
+ /**
+ * @zh_CN 是否显示注册按钮
+ */
+ showRegister?: boolean;
+
+ /**
+ * @zh_CN 是否显示记住账号
+ */
+ showRememberMe?: boolean;
+
+ /**
+ * @zh_CN 是否显示第三方登录
+ */
+ showThirdPartyLogin?: boolean;
+
+ /**
+ * @zh_CN 登录框子标题
+ */
+ subTitle?: string;
+
+ /**
+ * @zh_CN 登录框标题
+ */
+ title?: string;
+
+}
+```
+
+:::
+
+::: tip Note
+
+如果这些配置不能满足你的需求,你可以自行实现登录表单及相关登录逻辑或者给我们提交 `PR`。
+
+:::
+
+## 接口对接流程
+
+这里将会快速的介绍如何快速对接自己的后端。
+
+### 前置条件
+
+- 首先文档用的后端服务,接口返回的格式统一如下:
+
+```ts
+interface HttpResponse {
+ /**
+ * 0 表示成功 其他表示失败
+ * 0 means success, others means fail
+ */
+ code: number;
+ data: T;
+ message: string;
+}
+```
+
+如果你不符合这个格式,你需要先阅读 [服务端交互](../essentials/server.md) 文档,改造你的`request.ts`配置。
+
+- 其次你需要在先将本地代理地址改为你的真实后端地址,你可以在应用下的 `vite.config.mts` 内配置:
+
+```ts
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ vite: {
+ server: {
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // 这里改为你的真实接口地址
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});
+```
+
+### 登录接口
+
+为了能正常登录,你的后端最少需要提供 `2-3` 个接口:
+
+- 登录接口
+
+接口地址可在应用下的 `src/api/core/auth` 内修改,以下为默认接口地址:
+
+```ts
+/**
+ * 登录
+ */
+export async function loginApi(data: AuthApi.LoginParams) {
+ return requestClient.post('/auth/login', data);
+}
+
+/** 只需要保证登录接口返回值有 `accessToken` 字段即可 */
+export interface LoginResult {
+ accessToken: string;
+}
+```
+
+- 获取用户信息接口
+
+接口地址可在应用下的 `src/api/core/user` 内修改,以下为默认接口地址:
+
+```ts
+export async function getUserInfoApi() {
+ return requestClient.get('/user/info');
+}
+
+/** 只需要保证登录接口返回值有以下字段即可,多的字段可以自行使用 */
+export interface UserInfo {
+ roles: string[];
+ realName: string;
+}
+```
+
+- 获取权限码 (可选)
+
+这个接口用于获取用户的权限码,权限码是用于控制用户的权限的,接口地址可在应用下的 `src/api/core/auth` 内修改,以下为默认接口地址:
+
+```ts
+export async function getAccessCodesApi() {
+ return requestClient.get('/auth/codes');
+}
+```
+
+如果你不需要这个权限,你只需要把代码改为返回一个空数组即可。
+
+```ts {2}
+export async function getAccessCodesApi() {
+ // 这里返回一个空数组即可
+ return [];
+}
+```
diff --git a/web/docs/src/guide/in-depth/theme.md b/web/docs/src/guide/in-depth/theme.md
new file mode 100644
index 0000000..3d11e00
--- /dev/null
+++ b/web/docs/src/guide/in-depth/theme.md
@@ -0,0 +1,1295 @@
+# 主题
+
+框架基于 [shadcn-vue](https://www.shadcn-vue.com/themes.html) 和 [tailwindcss](https://tailwindcss.com/) 构建,提供了丰富的主题配置,可以通过简单的配置实现各种主题切换,满足个性化需求。您可以选择使用 CSS 变量或 Tailwind CSS 实用程序类进行主题设置。
+
+## Css 变量
+
+项目遵循 [shadcn-vue](https://www.shadcn-vue.com/themes.html) 的主题配置,示例:
+
+```html
+
+```
+
+我们对颜色使用一个简单的约定。`background`变量用于组件的背景颜色,`foreground`变量用于文本颜色。
+
+以下组件的`background`将为`hsl(var(--primary))`,`foreground`将为`hsl(var(--primary-foreground))`。
+
+## 详细的CSS变量列表
+
+::: warning 注意
+
+css 变量内的颜色,必须使用 `hsl` 格式,如 `0 0% 100%`,不需要加 `hsl()`和 `,`。
+
+:::
+
+你可以查看下面的CSS变量列表,以了解所有可用的变量。
+
+::: details 默认主题 css 变量
+
+```css
+:root {
+ --font-family:
+ -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+ arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+ 'Segoe UI Symbol', 'Noto Color Emoji';
+
+ /* Default background color of ...etc */
+ --background: 0 0% 100%;
+
+ /* 主体区域背景色 */
+ --background-deep: 216 20.11% 95.47%;
+ --foreground: 210 6% 21%;
+
+ /* Background color for */
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ /* Background color for popovers such as , , */
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ /* Muted backgrounds such as , and */
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ /* 主题颜色 */
+
+ --primary: 212 100% 45%;
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* Secondary colors for */
+
+ --secondary: 240 5% 96%;
+ --secondary-foreground: 240 6% 10%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 240 5% 96%;
+ --accent-hover: 200deg 10% 90%;
+ --accent-foreground: 240 6% 10%;
+
+ /* Darker color */
+ --heavy: 192deg 9.43% 89.61%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 5.9% 90%;
+
+ /* Border color for inputs such as , , */
+ --input: 240deg 5.88% 90%;
+ --input-placeholder: 217 10.6% 65%;
+ --input-background: 0 0% 100%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* Border radius for card, input and buttons */
+ --radius: 0.5rem;
+
+ /* ============= custom ============= */
+
+ /* 遮罩颜色 */
+ --overlay: 0deg 0% 0% / 30%;
+
+ /* 基本文字大小 */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ /* menu */
+ --sidebar: 0 0% 100%;
+ --sidebar-deep: 216 20.11% 95.47%;
+ --menu: var(--sidebar);
+
+ /* header */
+ --header: 0 0% 100%;
+
+ accent-color: var(--primary);
+ color-scheme: light;
+}
+```
+
+:::
+
+::: details 默认主题黑暗模式 css 变量
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ /* Default background color of ...etc */
+ --background: 222.34deg 10.43% 12.27%;
+
+ /* 主体区域背景色 */
+ --background-deep: 220deg 13.06% 9%;
+ --foreground: 0 0% 95%;
+
+ /* Background color for */
+ --card: 222.34deg 10.43% 12.27%;
+
+ /* --card: 222.2 84% 4.9%; */
+ --card-foreground: 210 40% 98%;
+
+ /* Background color for popovers such as , , */
+ --popover: 222.82deg 8.43% 12.27%;
+ --popover-foreground: 210 40% 98%;
+
+ /* Muted backgrounds such as , and */
+ --muted: 220deg 6.82% 17.25%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ /* 主题颜色 */
+
+ /* --primary: 245 82% 67%; */
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* 颜色次要 */
+ --secondary: 240 5% 17%;
+ --secondary-foreground: 0 0% 98%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 0deg 0% 100% / 8%;
+ --accent-hover: 0deg 0% 100% / 12%;
+ --accent-foreground: 0 0% 98%;
+
+ /* Darker color */
+ --heavy: 0deg 0% 100% / 12%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 3.7% 15.9%;
+
+ /* Border color for inputs such as , , */
+ --input: 0deg 0% 100% / 10%;
+ --input-placeholder: 218deg 11% 65%;
+ --input-background: 0deg 0% 100% / 5%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* 基本圆角大小 */
+ --radius: 0.5rem;
+
+ /* ============= Custom ============= */
+
+ /* 遮罩颜色 */
+ --overlay: 0deg 0% 0% / 40%;
+
+ /* 基本文字大小 */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ --sidebar: 222.34deg 10.43% 12.27%;
+ --sidebar-deep: 220deg 13.06% 9%;
+ --menu: var(--sidebar);
+ --header: 222.34deg 10.43% 12.27%;
+
+ color-scheme: dark;
+}
+```
+
+:::
+
+## 覆盖默认的 CSS 变量
+
+你只需要在你的项目中覆盖你想要修改的 CSS 变量即可。例如,要更改默认卡片背景色,你可以在你的 CSS 文件中添加以下内容进行覆盖:
+
+### 默认主题下
+
+```css
+:root {
+ /* Background color for */
+ --card: 0 0% 30%;
+}
+```
+
+### 黑暗模式下
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ /* Background color for */
+ --card: 222.34deg 10.43% 12.27%;
+}
+```
+
+## 更改品牌主色
+
+::: tip
+
+- 需要使用 `hsl` 格式颜色格式。
+- 修改后需要清空缓存才可生效。
+- 你可以借助 [第三方工具](https://www.w3schools.com/colors/colors_hsl.asp)来转换颜色。
+
+:::
+
+只需要在应用目录下的`preferences.ts`,自定义配置主色即可:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ // 错误色
+ colorDestructive: 'hsl(348 100% 61%)',
+ // 主题色
+ colorPrimary: 'hsl(212 100% 45%)',
+ // 成功色
+ colorSuccess: 'hsl(144 57% 58%)',
+ // 警告色
+ colorWarning: 'hsl(42 84% 61%)',
+ },
+});
+```
+
+## 内置主题
+
+框架中内置了多种主题,你可以在`preferences.ts`中进行配置:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ builtinType: 'default',
+ },
+});
+```
+
+### 内置主题列表
+
+框架内置了 16种主题,且还支持自定义主题。理论上,你可以无限制的扩展主题。
+
+::: details 内置主题类型列表
+
+```ts
+type BuiltinThemeType =
+ | 'custom'
+ | 'deep-blue'
+ | 'deep-green'
+ | 'default'
+ | 'gray'
+ | 'green'
+ | 'neutral'
+ | 'orange'
+ | 'pink'
+ | 'red'
+ | 'rose'
+ | 'sky-blue'
+ | 'slate'
+ | 'stone'
+ | 'violet'
+ | 'yellow'
+ | 'zinc'
+ | (Record & string);
+```
+
+:::
+
+::: details 内置主题css变量 - light
+
+```css
+:root {
+ --font-family:
+ -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+ arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+ 'Segoe UI Symbol', 'Noto Color Emoji';
+
+ /* Default background color of ...etc */
+ --background: 0 0% 100%;
+
+ /* 主体区域背景色 */
+ --background-deep: 216 20.11% 95.47%;
+ --foreground: 222 84% 5%;
+
+ /* Background color for */
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ /* Background color for popovers such as , , */
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ /* Muted backgrounds such as , and */
+
+ /* --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%; */
+
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+
+ /* 主题颜色 */
+
+ --primary: 212 100% 45%;
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* Secondary colors for */
+
+ --secondary: 240 5% 96%;
+ --secondary-foreground: 240 6% 10%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 240 5% 96%;
+ --accent-hover: 200deg 10% 90%;
+ --accent-foreground: 240 6% 10%;
+
+ /* Darker color */
+ --heavy: 192deg 9.43% 89.61%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 5.9% 90%;
+
+ /* Border color for inputs such as , , */
+ --input: 240deg 5.88% 90%;
+ --input-placeholder: 217 10.6% 65%;
+ --input-background: 0 0% 100%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* Border radius for card, input and buttons */
+ --radius: 0.5rem;
+
+ /* ============= custom ============= */
+
+ /* 遮罩颜色 */
+ --overlay: 0deg 0% 0% / 30%;
+
+ /* 基本文字大小 */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ /* menu */
+ --sidebar: 0 0% 100%;
+ --sidebar-deep: 0 0% 100%;
+ --menu: var(--sidebar);
+
+ /* header */
+ --header: 0 0% 100%;
+
+ accent-color: var(--primary);
+ color-scheme: light;
+}
+
+[data-theme='violet'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 224 71.4% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 224 71.4% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 224 71.4% 4.1%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 220 14.3% 95.9%;
+ --secondary-foreground: 220.9 39.3% 11%;
+ --muted: 220 14.3% 95.9%;
+ --muted-foreground: 220 8.9% 46.1%;
+ --accent: 220 14.3% 95.9%;
+ --accent-foreground: 220.9 39.3% 11%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+ --ring: 262.1 83.3% 57.8%;
+}
+
+[data-theme='pink'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 346.8 77.2% 49.8%;
+}
+
+[data-theme='rose'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 346.8 77.2% 49.8%;
+}
+
+[data-theme='sky-blue'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+}
+
+[data-theme='deep-blue'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+}
+
+[data-theme='green'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 142.1 76.2% 36.3%;
+}
+
+[data-theme='deep-green'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 142.1 76.2% 36.3%;
+}
+
+[data-theme='orange'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 20 14.3% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 20 14.3% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 20 14.3% 4.1%;
+ --primary-foreground: 60 9.1% 97.8%;
+ --secondary: 60 4.8% 95.9%;
+ --secondary-foreground: 24 9.8% 10%;
+ --muted: 60 4.8% 95.9%;
+ --muted-foreground: 25 5.3% 44.7%;
+ --accent: 60 4.8% 95.9%;
+ --accent-foreground: 24 9.8% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 20 5.9% 90%;
+ --input: 20 5.9% 90%;
+ --ring: 24.6 95% 53.1%;
+}
+
+[data-theme='yellow'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 20 14.3% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 20 14.3% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 20 14.3% 4.1%;
+ --primary-foreground: 26 83.3% 14.1%;
+ --secondary: 60 4.8% 95.9%;
+ --secondary-foreground: 24 9.8% 10%;
+ --muted: 60 4.8% 95.9%;
+ --muted-foreground: 25 5.3% 44.7%;
+ --accent: 60 4.8% 95.9%;
+ --accent-foreground: 24 9.8% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 20 5.9% 90%;
+ --input: 20 5.9% 90%;
+ --ring: 20 14.3% 4.1%;
+}
+
+[data-theme='zinc'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 240 5.9% 10%;
+}
+
+[data-theme='neutral'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+}
+
+[data-theme='slate'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+}
+
+[data-theme='gray'] {
+ /* --background: 0 0% 100%; */
+ --foreground: 224 71.4% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 224 71.4% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 224 71.4% 4.1%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 220 14.3% 95.9%;
+ --secondary-foreground: 220.9 39.3% 11%;
+ --muted: 220 14.3% 95.9%;
+ --muted-foreground: 220 8.9% 46.1%;
+ --accent: 220 14.3% 95.9%;
+ --accent-foreground: 220.9 39.3% 11%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+ --ring: 224 71.4% 4.1%;
+}
+```
+
+:::
+
+::: details 内置主题css变量 - dark
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ /* Default background color of ...etc */
+ --background: 222.34deg 10.43% 12.27%;
+
+ /* 主体区域背景色 */
+ --background-deep: 220deg 13.06% 9%;
+ --foreground: 0 0% 95%;
+
+ /* Background color for */
+ --card: 222.34deg 10.43% 12.27%;
+
+ /* --card: 222.2 84% 4.9%; */
+ --card-foreground: 210 40% 98%;
+
+ /* Background color for popovers such as , , */
+ --popover: 222.82deg 8.43% 12.27%;
+ --popover-foreground: 210 40% 98%;
+
+ /* Muted backgrounds such as , and */
+
+ /* --muted: 220deg 6.82% 17.25%; */
+
+ /* --muted-foreground: 215 20.2% 65.1%; */
+
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+
+ /* 主题颜色 */
+
+ /* --primary: 245 82% 67%; */
+ --primary-foreground: 0 0% 98%;
+
+ /* Used for destructive actions such as */
+
+ --destructive: 0 78% 68%;
+ --destructive-foreground: 0 0% 98%;
+
+ /* Used for success actions such as */
+
+ --success: 144 57% 58%;
+ --success-foreground: 0 0% 98%;
+
+ /* Used for warning actions such as */
+
+ --warning: 42 84% 61%;
+ --warning-foreground: 0 0% 98%;
+
+ /* 颜色次要 */
+ --secondary: 240 5% 17%;
+ --secondary-foreground: 0 0% 98%;
+
+ /* Used for accents such as hover effects on , ...etc */
+ --accent: 216 5% 19%;
+ --accent-hover: 216 5% 24%;
+ --accent-foreground: 0 0% 98%;
+
+ /* Darker color */
+ --heavy: 216 5% 24%;
+ --heavy-foreground: var(--accent-foreground);
+
+ /* Default border color */
+ --border: 240 3.7% 22%;
+
+ /* Border color for inputs such as , , */
+ --input: 0deg 0% 100% / 10%;
+ --input-placeholder: 218deg 11% 65%;
+ --input-background: 0deg 0% 100% / 5%;
+
+ /* Used for focus ring */
+ --ring: 222.2 84% 4.9%;
+
+ /* 基本圆角大小 */
+ --radius: 0.5rem;
+
+ /* ============= Custom ============= */
+
+ /* 遮罩颜色 */
+ --overlay: 0deg 0% 0% / 40%;
+
+ /* 基本文字大小 */
+ --font-size-base: 16px;
+
+ /* =============component & UI============= */
+
+ --sidebar: 222.34deg 10.43% 12.27%;
+ --sidebar-deep: 220deg 13.06% 9%;
+ --menu: var(--sidebar);
+
+ /* header */
+ --header: 222.34deg 10.43% 12.27%;
+
+ color-scheme: dark;
+}
+
+.dark[data-theme='violet'],
+[data-theme='violet'] .dark {
+ --background: 224 71.4% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 210 20% 98%;
+ --card: 224 71.4% 4.1%;
+ --card-foreground: 210 20% 98%;
+ --popover: 224 71.4% 4.1%;
+ --popover-foreground: 210 20% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 215 27.9% 16.9%;
+ --secondary-foreground: 210 20% 98%;
+ --muted: 215 27.9% 16.9%;
+ --muted-foreground: 217.9 10.6% 64.9%;
+ --accent: 215 27.9% 16.9%;
+ --accent-foreground: 210 20% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 215 27.9% 16.9%;
+ --input: 215 27.9% 16.9%;
+ --ring: 263.4 70% 50.4%;
+ --sidebar: 224 71.4% 4.1%;
+ --sidebar-deep: 224 71.4% 4.1%;
+ --header: 224 71.4% 4.1%;
+}
+
+.dark[data-theme='pink'],
+[data-theme='pink'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 95%;
+ --card: 0 0% 9%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 9%;
+ --popover-foreground: 0 0% 95%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 15%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 346.8 77.2% 49.8%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='rose'],
+[data-theme='rose'] .dark {
+ --background: 0 0% 3.9%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary-foreground: 0 85.7% 97.3%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 72.2% 50.6%;
+ --sidebar: 0 0% 3.9%;
+ --sidebar-deep: 0 0% 3.9%;
+ --header: 0 0% 3.9%;
+}
+
+.dark[data-theme='sky-blue'],
+[data-theme='sky-blue'] .dark {
+ --background: 222.2 84% 4.9%;
+ --background-deep: var(--background);
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 48%;
+ --sidebar: 222.2 84% 4.9%;
+ --sidebar-deep: 222.2 84% 4.9%;
+ --header: 222.2 84% 4.9%;
+}
+
+.dark[data-theme='deep-blue'],
+[data-theme='deep-blue'] .dark {
+ --background: 222.2 84% 4.9%;
+ --background-deep: var(--background);
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 48%;
+ --sidebar: 222.2 84% 4.9%;
+ --sidebar-deep: 222.2 84% 4.9%;
+ --header: 222.2 84% 4.9%;
+}
+
+.dark[data-theme='green'],
+[data-theme='green'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 95%;
+ --card: 24 9.8% 6%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 9%;
+ --popover-foreground: 0 0% 95%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 15%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 142.4 71.8% 29.2%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='deep-green'],
+[data-theme='deep-green'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 95%;
+ --card: 24 9.8% 6%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 9%;
+ --popover-foreground: 0 0% 95%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 15%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 142.4 71.8% 29.2%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='orange'],
+[data-theme='orange'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 60 9.1% 97.8%;
+ --card: 20 14.3% 4.1%;
+ --card-foreground: 60 9.1% 97.8%;
+ --popover: 20 14.3% 4.1%;
+ --popover-foreground: 60 9.1% 97.8%;
+ --primary-foreground: 60 9.1% 97.8%;
+ --secondary: 12 6.5% 15.1%;
+ --secondary-foreground: 60 9.1% 97.8%;
+ --muted: 12 6.5% 15.1%;
+ --muted-foreground: 24 5.4% 63.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 60 9.1% 97.8%;
+ --destructive: 0 72.2% 50.6%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 12 6.5% 15.1%;
+ --input: 12 6.5% 15.1%;
+ --ring: 20.5 90.2% 48.2%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='yellow'],
+[data-theme='yellow'] .dark {
+ --background: 20 14.3% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 60 9.1% 97.8%;
+ --card: 20 14.3% 4.1%;
+ --card-foreground: 60 9.1% 97.8%;
+ --popover: 20 14.3% 4.1%;
+ --popover-foreground: 60 9.1% 97.8%;
+ --primary-foreground: 26 83.3% 14.1%;
+ --secondary: 12 6.5% 15.1%;
+ --secondary-foreground: 60 9.1% 97.8%;
+ --muted: 12 6.5% 15.1%;
+ --muted-foreground: 24 5.4% 63.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 60 9.1% 97.8%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 12 6.5% 15.1%;
+ --input: 12 6.5% 15.1%;
+ --ring: 35.5 91.7% 32.9%;
+ --sidebar: 20 14.3% 4.1%;
+ --sidebar-deep: 20 14.3% 4.1%;
+ --header: 20 14.3% 4.1%;
+}
+
+.dark[data-theme='zinc'],
+[data-theme='zinc'] .dark {
+ --background: 240 10% 3.9%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary-foreground: 240 5.9% 10%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 240 4.9% 83.9%;
+ --sidebar: 240 10% 3.9%;
+ --sidebar-deep: 240 10% 3.9%;
+ --header: 240 4.9% 83.9%;
+}
+
+.dark[data-theme='neutral'],
+[data-theme='neutral'] .dark {
+ --background: 0 0% 3.9%;
+ --background-deep: var(--background);
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --sidebar: 0 0% 3.9%;
+ --sidebar-deep: 0 0% 3.9%;
+ --header: 0 0% 3.9%;
+}
+
+.dark[data-theme='slate'],
+[data-theme='slate'] .dark {
+ --background: 222.2 84% 4.9%;
+ --background-deep: var(--background);
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9;
+ --sidebar: 222.2 84% 4.9%;
+ --sidebar-deep: 222.2 84% 4.9%;
+ --header: 222.2 84% 4.9%;
+}
+
+.dark[data-theme='gray'],
+[data-theme='gray'] .dark {
+ --background: 224 71.4% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 210 20% 98%;
+ --card: 224 71.4% 4.1%;
+ --card-foreground: 210 20% 98%;
+ --popover: 224 71.4% 4.1%;
+ --popover-foreground: 210 20% 98%;
+ --primary-foreground: 220.9 39.3% 11%;
+ --secondary: 215 27.9% 16.9%;
+ --secondary-foreground: 210 20% 98%;
+ --muted: 215 27.9% 16.9%;
+ --muted-foreground: 217.9 10.6% 64.9%;
+ --accent: 215 27.9% 16.9%;
+ --accent-foreground: 210 20% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 215 27.9% 16.9%;
+ --input: 215 27.9% 16.9%;
+ --ring: 216 12.2% 83.9%;
+ --sidebar: 224 71.4% 4.1%;
+ --sidebar-deep: 224 71.4% 4.1%;
+ --header: 224 71.4% 4.1%;
+}
+```
+
+:::
+
+## 新增主题
+
+想要新增主题,只需按照以下步骤进行:
+
+- 在应用的 `src/preferences.ts`内新增一个主题配置。
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ builtinType: 'my-theme',
+ },
+});
+```
+
+- 在你的css文件中,新增主题的css变量。
+
+```css
+/* light */
+[data-theme='my-theme'] {
+ --foreground: 224 71.4% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 224 71.4% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 224 71.4% 4.1%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 220 14.3% 95.9%;
+ --secondary-foreground: 220.9 39.3% 11%;
+ --muted: 220 14.3% 95.9%;
+ --muted-foreground: 220 8.9% 46.1%;
+ --accent: 220 14.3% 95.9%;
+ --accent-foreground: 220.9 39.3% 11%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+ --ring: 262.1 83.3% 57.8%;
+}
+
+/* dark */
+.dark[data-theme='my-theme'],
+[data-theme='my-theme'] .dark {
+ --background: 224 71.4% 4.1%;
+ --background-deep: var(--background);
+ --foreground: 210 20% 98%;
+ --card: 224 71.4% 4.1%;
+ --card-foreground: 210 20% 98%;
+ --popover: 224 71.4% 4.1%;
+ --popover-foreground: 210 20% 98%;
+ --primary-foreground: 210 20% 98%;
+ --secondary: 215 27.9% 16.9%;
+ --secondary-foreground: 210 20% 98%;
+ --muted: 215 27.9% 16.9%;
+ --muted-foreground: 217.9 10.6% 64.9%;
+ --accent: 215 27.9% 16.9%;
+ --accent-foreground: 210 20% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 20% 98%;
+ --border: 215 27.9% 16.9%;
+ --input: 215 27.9% 16.9%;
+ --ring: 263.4 70% 50.4%;
+ --sidebar: 224 71.4% 4.1%;
+ --sidebar-deep: 224 71.4% 4.1%;
+}
+```
+
+## 黑暗模式
+
+框架中内置了多种主题,你可以在`preferences.ts`中进行配置,黑暗主题同样会读取css变量来进行配置:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ theme: {
+ mode: 'dark',
+ },
+});
+```
+
+## 自定义侧边栏颜色
+
+侧边栏颜色通过`--sidebar`变量来配置
+
+### 默认主题下
+
+```css
+:root {
+ --sidebar: 0 0% 100%;
+}
+```
+
+### 黑暗模式下
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ --sidebar: 222.34deg 10.43% 12.27%;
+}
+```
+
+## 自定义顶栏颜色
+
+侧边栏颜色通过`--header`变量来配置
+
+### 默认主题下
+
+```css
+:root {
+ --header: 0 0% 100%;
+}
+```
+
+### 黑暗模式下
+
+```css
+.dark,
+.dark[data-theme='custom'],
+.dark[data-theme='default'] {
+ --header: 222.34deg 10.43% 12.27%;
+}
+```
+
+## 色弱模式
+
+一般用于特殊场景,将设置为色弱模式,你可以在`preferences.ts`中进行配置:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ colorWeakMode: true,
+ },
+});
+```
+
+## 灰色模式
+
+一般用于特殊场景,将网页置灰,你可以在`preferences.ts`中进行配置:
+
+```ts
+import { defineOverridesPreferences } from '@vben/preferences';
+
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ colorGrayMode: true,
+ },
+});
+```
diff --git a/web/docs/src/guide/in-depth/ui-framework.md b/web/docs/src/guide/in-depth/ui-framework.md
new file mode 100644
index 0000000..6a7508e
--- /dev/null
+++ b/web/docs/src/guide/in-depth/ui-framework.md
@@ -0,0 +1,17 @@
+# 组件库切换
+
+`Vue Admin` 支持你自由选择组件库,目前演示站点的默认组件库是 `Ant Design Vue`,与旧版本保持一致。同时框架还内置了 `Element Plus` 版本和 `Naive UI` 版本,你可以根据自己的喜好选择。
+
+## 新增组件库应用
+
+如果你想用其他别的组件库,你只需要按以下步骤进行操作:
+
+1. 在`apps`内创建一个新的文件夹,例如`apps/web-xxx`。
+2. 更改`apps/web-xxx/package.json`的`name`字段为`web-xxx`。
+3. 移除其他组件库依赖及代码,并用你的组件库进行替换相应逻辑,需要改动的地方不多。
+4. 调整`locales`内的语言文件。
+5. 调整 `app.vue` 内的组件。
+6. 自行适配组件库的主题,与 `Vben Admin` 契合。
+7. 调整 `.env` 内的应用名
+8. 在大仓根目录增加 `dev:xxx` 脚本
+9. 执行 `pnpm install` 安装依赖
diff --git a/web/docs/src/guide/introduction/changelog.md b/web/docs/src/guide/introduction/changelog.md
new file mode 100644
index 0000000..782056b
--- /dev/null
+++ b/web/docs/src/guide/introduction/changelog.md
@@ -0,0 +1,3 @@
+# 更新日志
+
+TODO
diff --git a/web/docs/src/guide/introduction/quick-start.md b/web/docs/src/guide/introduction/quick-start.md
new file mode 100644
index 0000000..6b451dd
--- /dev/null
+++ b/web/docs/src/guide/introduction/quick-start.md
@@ -0,0 +1,111 @@
+---
+outline: deep
+---
+
+# 快速开始 {#quick-start}
+
+## 前置准备
+
+::: info 环境要求
+
+在启动项目前,你需要确保你的环境满足以下要求:
+
+- [Node.js](https://nodejs.org/en) 20.15.0 及以上版本,推荐使用 [fnm](https://github.com/Schniz/fnm) 、 [nvm](https://github.com/nvm-sh/nvm) 或者直接使用[pnpm](https://pnpm.io/cli/env) 进行版本管理。
+- [Git](https://git-scm.com/) 任意版本。
+
+验证你的环境是否满足以上要求,你可以通过以下命令查看版本:
+
+```bash
+# 出现相应 node LTS版本即可
+node -v
+# 出现相应 git 版本即可
+git -v
+```
+
+:::
+
+## 启动项目
+
+### 获取源码
+
+::: code-group
+
+```sh [GitHub]
+# clone 代码
+git clone https://github.com/vbenjs/vue-vben-admin.git
+```
+
+```sh [Gitee]
+# clone 代码
+# Gitee 的代码可能不是最新的
+git clone https://gitee.com/annsion/vue-vben-admin.git
+```
+
+:::
+
+::: danger 注意
+
+注意存放代码的目录及所有父级目录不能存在中文、韩文、日文以及空格,否则安装依赖后启动会出错。
+
+:::
+
+### 安装依赖
+
+在你的代码目录内打开终端,并执行以下命令:
+
+```bash
+# 进入项目目录
+cd vue-vben-admin
+
+# 使用项目指定的pnpm版本进行依赖安装
+npm i -g corepack
+
+# 安装依赖
+pnpm install
+```
+
+::: tip 注意
+
+- 项目只支持使用 `pnpm` 进行依赖安装,默认会使用 `corepack` 来安装指定版本的 `pnpm`。:
+- 如果你的网络环境无法访问npm源,你可以设置系统的环境变量`COREPACK_NPM_REGISTRY=https://registry.npmmirror.com`,然后再执行`pnpm install`。
+- 如果你不想使用`corepack`,你需要禁用`corepack`,然后使用你自己的`pnpm`进行安装。
+
+:::
+
+### 运行项目
+
+#### 选择项目
+
+执行以下命令运行项目:
+
+```bash
+# 启动项目
+pnpm dev
+```
+
+此时,你会看到类似如下的输出,选择你需要运行的项目:
+
+```bash
+│
+◆ Select the app you need to run [dev]:
+│ ○ @vben/web-antd
+│ ○ @vben/web-ele
+│ ○ @vben/web-naive
+│ ○ @vben/docs
+│ ● @vben/playground
+└
+```
+
+现在,你可以在浏览器访问 `http://localhost:5555` 查看项目。
+
+#### 运行指定项目
+
+如果你不想选择项目,可以直接运行以下命令运行你需要的应用:
+
+```bash
+pnpm run dev:antd
+pnpm run dev:ele
+pnpm run dev:naive
+pnpm run dev:docs
+pnpm run dev:play
+```
diff --git a/web/docs/src/guide/introduction/roadmap.md b/web/docs/src/guide/introduction/roadmap.md
new file mode 100644
index 0000000..6aa9896
--- /dev/null
+++ b/web/docs/src/guide/introduction/roadmap.md
@@ -0,0 +1,3 @@
+# 路线图
+
+TODO:
diff --git a/web/docs/src/guide/introduction/thin.md b/web/docs/src/guide/introduction/thin.md
new file mode 100644
index 0000000..8088400
--- /dev/null
+++ b/web/docs/src/guide/introduction/thin.md
@@ -0,0 +1,94 @@
+---
+outline: deep
+---
+
+# 精简版本
+
+从 `5.0` 版本开始,我们不再提供精简的仓库或者分支。我们的目标是提供一个更加一致的开发体验,同时减少维护成本。在这里,我们将如何介绍自己的项目,如何去精简以及移除不需要的功能。
+
+## 应用精简
+
+首先,确认你需要的 `UI` 组件库版本,然后删除对应的应用,比如你选择使用 `Ant Design Vue`,那么你可以删除其他应用, 只需要删除下面两个文件夹即可:
+
+```bash
+apps/web-ele
+apps/web-naive
+
+```
+
+::: tip
+
+如果项目没有内置你需要的 `UI` 组件库应用,你可以直接全部删除其他应用。然后自行新建应用即可。
+
+:::
+
+## 演示代码精简
+
+如果你不需要演示代码,你可以直接删除的`playground`文件夹。
+
+## 文档精简
+
+如果你不需要文档,你可以直接删除`docs`文件夹。
+
+## Mock 服务精简
+
+如果你不需要`Mock`服务,你可以直接删除`apps/backend-mock`文件夹。同时在你的应用下`.env.development`文件中删除`VITE_NITRO_MOCK`变量。
+
+```bash
+# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
+VITE_NITRO_MOCK=false
+```
+
+## 安装依赖
+
+到这里,你已经完成了精简操作,接下来你可以安装依赖,并启动你的项目:
+
+```bash
+# 根目录下执行
+pnpm install
+
+```
+
+## 命令调整
+
+在精简后,你可能需要根据你的项目调整命令,在根目录下的`package.json`文件中,你可以调整`scripts`字段,移除你不需要的命令。
+
+```json
+{
+ "scripts": {
+ "build:antd": "pnpm run build --filter=@vben/web-antd",
+ "build:docs": "pnpm run build --filter=@vben/docs",
+ "build:ele": "pnpm run build --filter=@vben/web-ele",
+ "build:naive": "pnpm run build --filter=@vben/web-naive",
+ "build:play": "pnpm run build --filter=@vben/playground",
+ "dev:antd": "pnpm -F @vben/web-antd run dev",
+ "dev:docs": "pnpm -F @vben/docs run dev",
+ "dev:ele": "pnpm -F @vben/web-ele run dev",
+ "dev:play": "pnpm -F @vben/playground run dev",
+ "dev:naive": "pnpm -F @vben/web-naive run dev"
+ }
+}
+```
+
+## 其他
+
+如果你想更进一步精简,你可以删除参考以下文件或者文件夹的作用,判断自己是否需要,不需要删除即可:
+
+- `.changeset` 文件夹用于管理版本变更
+- `.github` 文件夹用于存放 GitHub 的配置文件
+- `.vscode` 文件夹用于存放 VSCode 的配置文件,如果你使用其他编辑器,可以删除
+- `./scripts/deploy` 文件夹用于存放部署脚本,如果你不需要docker部署,可以删除
+
+## 应用精简
+
+当你确定了某个应用,你还可以进一步精简:
+
+### 删除不需要的路由及页面
+
+- 在应用的 `src/router/routes` 文件中,你可以删除不需要的路由。其中 `core` 文件夹内,如果只需要登录和忘记密码,你可以删除其他路由,如忘记密码、注册等。路由删除后,你可以删除对应的页面文件,在 `src/views/_core` 文件夹中。
+
+- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以删除对应的页面文件,在 `src/views` 文件夹中。
+
+### 删除不需要的组件
+
+- 在应用的 `packages/effects/common-ui/src/ui` 文件夹中,你可以删除不需要的组件,如`about`、`dashboard` 目录等。删除之前请先确保你的路由中没有引用到这些组件。
diff --git a/web/docs/src/guide/introduction/vben.md b/web/docs/src/guide/introduction/vben.md
new file mode 100644
index 0000000..a4a5f97
--- /dev/null
+++ b/web/docs/src/guide/introduction/vben.md
@@ -0,0 +1,49 @@
+# 关于 Vben Admin
+
+::: info 你正在阅读的是 [Vben Admin](https://github.com/vbenjs/vue-vben-admin) `5.0`版本的文档!
+
+- Vben Admin 2.x 目前已存档,仅进行重大问题修复。
+- 新版本与旧版本不兼容,如果你使用的是旧版本(v2、v3),请查看 [Vue Vben Admin 2.x 文档](https://doc.vvbin.cn)
+- 如发现文档有误,欢迎提交 [issue](https://github.com/vbenjs/vue-vben-admin/issues) 帮助我们改进。
+- 如果你只是想体验一下,你可以查看[快速开始](./quick-start.md)。
+
+:::
+
+[Vben Admin](https://github.com/vbenjs/vue-vben-admin) 是一个基于 [Vue3.0](https://github.com/vuejs/core)、[Vite](https://github.com/vitejs/vite)、 [TypeScript](https://www.typescriptlang.org/) 的中后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模板,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 `vue3`、`vite`、`ts` 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
+
+## 特点
+
+- **最新技术栈**:使用 `Vue3`、`Vite`、`TypeScript` 等前端前沿技术开发。
+- **国际化**:内置完善的国际化方案,支持多语言切换。
+- **权限验证**:完善的权限验证方案,按钮级别权限控制。
+- **多主题**:内置多种主题配置和黑暗模式,满足个性化需求。
+- **动态菜单**:支持动态菜单,可以根据权限配置显示菜单。
+- **Mock 数据**:基于 `Nitro` 的本地高性能 Mock 数据方案。
+- **组件丰富**:提供了丰富的组件,可以满足大部分的业务需求。
+- **规范**:代码规范,使用 `ESLint`、`Prettier`、`Stylelint`、`Publint`、`CSpell` 等工具保证代码质量。
+- **工程化**:使用 `Pnpm Monorepo`、`TurboRepo`、`Changeset` 等工具,提高开发效率。
+- **多UI库支持**:支持 `Ant Design Vue`、`Element Plus`、`Naive` 等主流 UI 库,不再限制于特定框架。
+
+## 浏览器支持
+
+- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
+
+- **生产环境**支持现代浏览器,不支持 IE。
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE | [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
+| :-: | :-: | :-: | :-: | :-: |
+| 不支持 | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## 贡献
+
+- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) 还在持续更新中,本项目欢迎您的参与,共同维护,逐步完善,打造更好的中后台解决方案。
+- 如果你有兴趣加入我们,可以通过以下方式开始,我们会根据你的活跃度邀请你加入。
+
+::: info 加入我们
+
+- 长期提交 `PR`。
+- 提供有价值的建议。
+- 参与讨论,帮助解决 `issue`。
+- 共同维护文档。
+
+:::
diff --git a/web/docs/src/guide/introduction/why.md b/web/docs/src/guide/introduction/why.md
new file mode 100644
index 0000000..4191b3e
--- /dev/null
+++ b/web/docs/src/guide/introduction/why.md
@@ -0,0 +1,23 @@
+# 为什么选择我们?
+
+::: info 写在前面
+
+我们不会去和其他框架做比较。我们认为每个框架都有自己的特点,适合不同的场景。我们的目标是提供一个简单、易用的框架,让开发者可以快速上手,专注于业务逻辑的开发。所以我们只会不断完善和优化我们的框架,提供更好的体验。
+
+:::
+
+我们致力于为开发者提供一个高效、现代、易用的前端框架。我们的解决方案基于最新的技术栈,如 Vue3、Vite 和 TypeScript,确保您在构建项目时始终走在技术的前沿。同时,我们注重代码的质量与规范,通过严格的工具链保证代码的一致性和可维护性。无论是初创项目还是企业级应用,我们的框架都能帮助您快速构建、迭代和部署。
+
+## 框架历程
+
+从 Vue Vben Admin 1.x 版本开始,框架经历了许多迭代和优化。从一开始使用 `Vite 0.x` 版本,没有现成的插件,开发了很多自定义插件来弥合 Webpack 和 Vite 之间的差异。虽然很多现在已经被代替,但是我们的初衷一直没有变,就是提供一个简单、易用的框架。
+
+虽然中间有段时间由社区维护,但我们一直密切关注 Vue Vben Admin 的发展。见证了许多开发者使用 Vben Admin,并提供了许多宝贵的建议和反馈。非常感谢大家的支持和贡献,这些都是我们持续改进 Vben Admin 的动力。新版本中,我们持续收集用户反馈,重新开始,不断优化框架,以提供更好的用户体验。我们的目标是让开发者能够快速上手,专注于业务逻辑的开发。
+
+## 单元测试
+
+单元测试是确保代码质量的基石。在开发过程中编写和执行单元测试,以捕捉潜在的错误并提升代码的可靠性。框架核心逻辑使用 `vitest` 做了单元测试,并在逐步增加覆盖率。通过单元测试,可以放心地进行代码重构,减少回归问题,从而提高整体开发效率。
+
+## 质量与规范
+
+我们始终高度重视代码的质量与规范。通过使用 ESLint、Prettier、Stylelint、Publint、CSpell 等工具来确保代码质量。我们的代码规范基于 Vue3、Vite、TypeScript 等现代前端技术制定,旨在提供一个简洁、易用的框架,使开发者能够快速上手并专注于业务逻辑的开发。
diff --git a/web/docs/src/guide/other/faq.md b/web/docs/src/guide/other/faq.md
new file mode 100644
index 0000000..33139c4
--- /dev/null
+++ b/web/docs/src/guide/other/faq.md
@@ -0,0 +1,159 @@
+# 常见问题 #{faq}
+
+::: tip 列举了一些常见的问题
+
+有问题可以先来这里寻找,如果没有可以在 [GitHub Issue](https://github.com/vbenjs/vue-vben-admin/issues) 搜索或者提交你的问题, 如果是讨论性的问题可以在 [GitHub Discussions](https://github.com/vbenjs/vue-vben-admin/discussions)
+
+:::
+
+## 说明
+
+遇到问题,可以先从以下几个方面查找
+
+1. 对应模块的 GitHub 仓库 [issue](https://github.com/vbenjs/vue-vben-admin/issues) 搜索
+2. 从[google](https://www.google.com)搜索问题
+3. 从[百度](https://www.baidu.com)搜索问题
+4. 在下面列表找不到问题可以到 issue 提问 [issues](https://github.com/vbenjs/vue-vben-admin/issues)
+5. 如果不是问题类型的,需要讨论的,请到 [discussions](https://github.com/vbenjs/vue-vben-admin/discussions) 讨论
+
+## 依赖问题
+
+在 `Monorepo` 项目下,需要养成每次 `git pull`代码都要执行`pnpm install`的习惯,因为经常会有新的依赖包加入,项目在`lefthook.yml`已经配置了自动执行`pnpm install`,但是有时候会出现问题,如果没有自动执行,建议手动执行一次。
+
+## 关于缓存更新问题
+
+项目配置默认是缓存在 `localStorage` 内,所以版本更新后可能有些配置没改变。
+
+解决方式是每次更新代码的时候修改 `package.json` 内的 `version` 版本号. 因为 localStorage 的 key 是根据版本号来的。所以更新后版本不同前面的配置会失效。重新登录即可
+
+## 关于修改配置文件的问题
+
+当修改 `.env` 等环境文件以及 `vite.config.ts` 文件时,vite 会自动重启服务。
+
+自动重启有几率出现问题,请重新运行项目即可解决.
+
+## 本地运行报错
+
+由于 vite 在本地没有转换代码,且代码中用到了可选链等比较新的语法。所以本地开发需要使用版本较高的浏览器(`Chrome 90+`)进行开发
+
+## 页面切换后页面空白
+
+这是由于开启了路由切换动画,且对应的页面组件存在多个根节点导致的,在页面最外层添加`
`即可
+
+**错误示例**
+
+```vue
+
+
+ text h1
+ text h2
+
+```
+
+**正确示例**
+
+```vue
+
+
+
text h1
+ text h2
+
+
+```
+
+::: tip 提示
+
+- 如果想使用多个根标签,可以禁用路由切换动画
+- template 下面的根注释节点也算一个节点
+
+:::
+
+## 我的代码本地开发可以,打包就不行了
+
+目前发现这个原因可能有以下,可以从以下原因来排查,如果还有别的可能,欢迎补充
+
+- 使用了 ctx 这个变量,ctx 本身未暴露出在实例类型内,Vue官方也是说了不要用这个属性。这个属性只是用于内部使用。
+
+```ts
+import { getCurrentInstance } from 'vue';
+getCurrentInstance().ctx.xxxx;
+```
+
+## 依赖安装问题
+
+- 如果依赖安装不了或者启动报错可以尝试执行`pnpm run reinstall`。
+- 如果依赖安装不了或者报错,可以尝试切换手机热点来进行依赖安装。
+- 如果还是不行,可以自行配置国内镜像安装。
+- 也可以在项目根目录创建 `.npmrc` 文件,内容如下
+
+```bash
+# .npmrc
+registry = https://registry.npmmirror.com/
+```
+
+## 打包文件过大
+
+- 首先,完整版由于引用了比较多的库文件,所以打包会比较大。可以使用精简版来进行开发
+
+- 其次建议开启 gzip,使用之后体积会只有原先 1/3 左右。gzip 可以由服务器直接开启。如果是这样,前端不需要构建 `.gz` 格式的文件,如果前端构建了 `.gz` 文件,以 nginx 为例,nginx 需要开启 `gzip_static: on` 这个选项。
+
+- 开启 gzip 的同时还可以同时开启 `brotli`,比 gzip 更好的压缩。两者可以共存
+
+**注意**
+
+- gzip_static: 这个模块需要 nginx 另外安装,默认的 nginx 没有安装这个模块。
+
+- 开启 `brotli` 也需要 nginx 另外安装模块
+
+## 运行错误
+
+如果出现类似以下错误,请检查项目全路径(包含所有父级路径)不能出现中文、日文、韩文。否则将会出现路径访问 404 导致以下问题
+
+```ts
+[vite] Failed to resolve module import "ant-design-vue/dist/antd.css-vben-adminode_modulesant-design-vuedistantd.css". (imported by /@/setup/ant-design-vue/index.ts)
+```
+
+## 控制台路由警告问题
+
+如果看到控制台有如下警告,且页面**能正常打开** 可以忽略该警告。
+
+后续 `vue-router` 可能会提供配置项来关闭警告
+
+```ts
+[Vue Router warn]: No match found for location with path "xxxx"
+```
+
+## 启动报错
+
+当出现以下错误信息时,请检查你的 nodejs 版本号是否符合要求
+
+```bash
+TypeError: str.matchAll is not a function
+at Object.extractor (vue-vben-admin-main\node_modules@purge-icons\core\dist\index.js:146:27)
+at Extract (vue-vben-admin-main\node_modules@purge-icons\core\dist\index.js:173:54)
+```
+
+## nginx 部署
+
+部署到 `nginx`后,可能会出现以下错误:
+
+```bash
+Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.
+```
+
+解决方式一:
+
+```bash
+http {
+ #如果有此项配置需要注释掉
+ #include mime.types;
+
+ types {
+ application/javascript js mjs;
+ }
+}
+```
+
+解决方式二:
+
+进入 `nginx` 下的`mime.types`文件, 将`application/javascript js;` 修改为 `application/javascript js mjs;`
diff --git a/web/docs/src/guide/other/project-update.md b/web/docs/src/guide/other/project-update.md
new file mode 100644
index 0000000..a403042
--- /dev/null
+++ b/web/docs/src/guide/other/project-update.md
@@ -0,0 +1,55 @@
+# 项目更新
+
+## 为什么无法像 npm 插件一样更新
+
+因为项目是一个完整的项目模版,不是一个插件或者安装包,无法像插件一样更新,你使用代码后,会根据业务需求,进行二次开发,需要自行手动合并升级。
+
+## 我需要怎么做
+
+项目采用了 `Monorepo` 的方式进行管理,并将一些比较核心的代码进行了抽离,比如 `packages/@core`、`packages/effects`,只要业务代码没有修改这部分代码,那么你可以直接拉取最新代码,然后合并到你的分支上,只需要简单的处理部分冲突即可。其余文件夹只会进行一些小的调整,不会对业务代码产生影响。
+
+::: tip 推荐
+
+建议关注仓库动态,积极去合并,不要长时间积累,否则将会导致合并冲突过多,增加合并难度。
+
+:::
+
+## 使用 Git 更新代码
+
+1. 克隆代码
+
+```bash
+git clone https://github.com/vbenjs/vue-vben-admin.git
+```
+
+2. 添加自己的公司 git 源地址
+
+```bash
+# up 为源名称,可以随意设置
+# gitUrl为开源最新代码
+git remote add up gitUrl;
+```
+
+3. 提交代码到自己公司 git
+
+```bash
+# 提交代码到自己公司
+# main为分支名 需要自行根据情况修改
+git push up main
+
+# 同步公司的代码
+# main为分支名 需要自行根据情况修改
+git pull up main
+```
+
+4. 如何同步开源最新代码
+
+```bash
+git pull origin main
+```
+
+::: tip 提示
+
+同步代码的时候会出现冲突。只需要把冲突解决即可
+
+:::
diff --git a/web/docs/src/guide/other/remove-code.md b/web/docs/src/guide/other/remove-code.md
new file mode 100644
index 0000000..8326224
--- /dev/null
+++ b/web/docs/src/guide/other/remove-code.md
@@ -0,0 +1,18 @@
+# 移除代码
+
+## 移除百度统计代码
+
+在对应应用的 `index.html` 文件中,找到如下代码,删除即可:
+
+```html
+
+
+```
diff --git a/web/docs/src/guide/project/changeset.md b/web/docs/src/guide/project/changeset.md
new file mode 100644
index 0000000..cc206ae
--- /dev/null
+++ b/web/docs/src/guide/project/changeset.md
@@ -0,0 +1,21 @@
+# Changeset
+
+项目内置了 [changeset](https://github.com/changesets/changesets) 作为版本管理工具。Changeset 是一个版本管理工具,它可以帮助我们更好的管理版本,生成 changelog,以及自动发布。
+
+详细使用方式可查看官方文档,这里不再阐述。如果你不需要它,可以直接忽略。
+
+## 命令行
+
+changeset 命令在项目中已经内置:
+
+### 交互式填写变更集
+
+```bash
+pnpm run changeset
+```
+
+### 统一提升版本号
+
+```bash
+pnpm run version
+```
diff --git a/web/docs/src/guide/project/cli.md b/web/docs/src/guide/project/cli.md
new file mode 100644
index 0000000..3e35242
--- /dev/null
+++ b/web/docs/src/guide/project/cli.md
@@ -0,0 +1,113 @@
+---
+outline: deep
+---
+
+# CLI
+
+项目中,提供了一些命令行工具,用于一些常用的操作,代码位于 `scrips` 内。
+
+## vsh
+
+用于一些项目操作,如清理项目、检查项目等。
+
+### 用法
+
+```bash
+pnpm vsh [command] [options]
+```
+
+### vsh check-circular
+
+检查整个项目循环引用,如果有循环引用,会在控制台输出循环引用的模块。
+
+#### 用法
+
+```bash
+pnpm vsh check-circular
+```
+
+#### 选项
+
+| 选项 | 说明 |
+| ---------- | ----------------------------------- |
+| `--staged` | 只检查git暂存区内的文件,默认`false` |
+
+### vsh check-dep
+
+检查整个项目依赖情况,并在控制台输出`未使用的依赖`、`未安装的依赖`信息
+
+#### 用法
+
+```bash
+pnpm vsh check-dep
+```
+
+#### 选项
+
+| 选项 | 说明 |
+| ---------------- | --------------------------------------- |
+| `-r,--recursive` | 递归删除整个项目,默认`true` |
+| `--del-lock` | 是否删除`pnpm-lock.yaml`文件,默认`true` |
+
+### vsh lint
+
+对项目进行lint检查,检查项目中的代码是否符合规范。
+
+#### 用法
+
+```bash
+pnpm vsh lint
+```
+
+#### 选项
+
+| 选项 | 说明 |
+| ---------- | ------------------------------ |
+| `--format` | 检查并尝试修复错误,默认`false` |
+
+### vsh publint
+
+对 `Monorepo` 项目进行包规范检查,检查项目中的包是否符合规范。
+
+#### 用法
+
+```bash
+pnpm vsh publint
+```
+
+#### 选项
+
+| 选项 | 说明 |
+| --------- | ---------------------- |
+| `--check` | 仅执行检查,默认`false` |
+
+### vsh code-workspace
+
+生成 `vben-admin.code-workspace` 文件,目前不需要手动执行,会在代码提交时自动执行。
+
+#### 用法
+
+```bash
+pnpm vsh code-workspace
+```
+
+#### 选项
+
+| 选项 | 说明 |
+| --------------- | -------------------------------------- |
+| `--auto-commit` | `git commit`时候,自动提交,默认`false` |
+| `--spaces` | 缩进格式,默认 `2`个缩进 |
+
+## turbo-run
+
+用于快速执行大仓中脚本,并提供选项式交互选择。
+
+### 用法
+
+```bash
+pnpm turbo-run [command]
+```
+
+### turbo-run dev
+
+快速执行`dev`命令,并提供选项式交互选择。
diff --git a/web/docs/src/guide/project/dir.md b/web/docs/src/guide/project/dir.md
new file mode 100644
index 0000000..e653dc2
--- /dev/null
+++ b/web/docs/src/guide/project/dir.md
@@ -0,0 +1,68 @@
+# 目录说明
+
+目录使用 Monorepo 管理,项目结构如下:
+
+```bash
+.
+├── Dockerfile # Docker 镜像构建文件
+├── README.md # 项目说明文档
+├── apps # 项目应用目录
+│ ├── backend-mock # 后端模拟服务应用
+│ ├── web-antd # 基于 Ant Design Vue 的前端应用
+│ ├── web-ele # 基于 Element Plus 的前端应用
+│ └── web-naive # 基于 Naive UI 的前端应用
+├── build-local-docker-image.sh # 本地构建 Docker 镜像脚本
+├── cspell.json # CSpell 配置文件
+├── docs # 项目文档目录
+├── eslint.config.mjs # ESLint 配置文件
+├── internal # 内部工具目录
+│ ├── lint-configs # 代码检查配置
+│ │ ├── commitlint-config # Commitlint 配置
+│ │ ├── eslint-config # ESLint 配置
+│ │ ├── prettier-config # Prettier 配置
+│ │ └── stylelint-config # Stylelint 配置
+│ ├── node-utils # Node.js 工具
+│ ├── tailwind-config # Tailwind 配置
+│ ├── tsconfig # 通用 tsconfig 配置
+│ └── vite-config # 通用Vite 配置
+├── package.json # 项目依赖配置
+├── packages # 项目包目录
+│ ├── @core # 核心包
+│ │ ├── base # 基础包
+│ │ │ ├── design # 设计相关
+│ │ │ ├── icons # 图标
+│ │ │ ├── shared # 共享
+│ │ │ └── typings # 类型定义
+│ │ ├── composables # 组合式 API
+│ │ ├── preferences # 偏好设置
+│ │ └── ui-kit # UI 组件集合
+│ │ ├── layout-ui # 布局 UI
+│ │ ├── menu-ui # 菜单 UI
+│ │ ├── shadcn-ui # shadcn UI
+│ │ └── tabs-ui # 标签页 UI
+│ ├── constants # 常量
+│ ├── effects # 副作用相关包
+│ │ ├── access # 访问控制
+│ │ ├── plugins # 第三方大型依赖插件
+│ │ ├── common-ui # 通用 UI
+│ │ ├── hooks # 组合式 API
+│ │ ├── layouts # 布局
+│ │ └── request # 请求
+│ ├── icons # 图标
+│ ├── locales # 国际化
+│ ├── preferences # 偏好设置
+│ ├── stores # 状态管理
+│ ├── styles # 样式
+│ ├── types # 类型定义
+│ └── utils # 工具
+├── playground # 演示目录
+├── pnpm-lock.yaml # pnpm 锁定文件
+├── pnpm-workspace.yaml # pnpm 工作区配置文件
+├── scripts # 脚本目录
+│ ├── turbo-run # Turbo 运行脚本
+│ └── vsh # VSH 脚本
+├── stylelint.config.mjs # Stylelint 配置文件
+├── turbo.json # Turbo 配置文件
+├── vben-admin.code-workspace # VS Code 工作区配置文件
+└── vitest.config.ts # Vite 配置文件
+```
diff --git a/web/docs/src/guide/project/standard.md b/web/docs/src/guide/project/standard.md
new file mode 100644
index 0000000..12a13da
--- /dev/null
+++ b/web/docs/src/guide/project/standard.md
@@ -0,0 +1,213 @@
+# 规范
+
+::: tip 贡献代码
+
+- 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。
+- 如果你使用的是 `vscode`,需要安装以下插件:
+
+ - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查
+ - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化
+ - [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查
+ - [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) - css 格式化
+
+:::
+
+## 作用
+
+具备基本工程素养的同学都会注重编码规范,而代码风格检查(Code Linting,简称 Lint)是保障代码规范一致性的重要手段。
+
+遵循相应的代码规范有以下好处:
+
+- 较少 bug 错误率
+- 高效的开发效率
+- 更高的可读性
+
+## 工具
+
+项目的配置文件位于 `internal/lint-configs` 下,你可以在这里修改各种lint的配置。
+
+项目内集成了以下几种代码校验工具:
+
+- [ESLint](https://eslint.org/) 用于 JavaScript 代码检查
+- [Stylelint](https://stylelint.io/) 用于 CSS 样式检查
+- [Prettier](https://prettier.io/) 用于代码格式化
+- [Commitlint](https://commitlint.js.org/) 用于检查 git 提交信息的规范
+- [Publint](https://publint.dev/) 用于检查 npm 包的规范
+- [Cspell](https://cspell.org/) 用于检查拼写错误
+- [lefthook](https://github.com/evilmartians/lefthook) 用于管理 Git hooks,在提交前自动运行代码校验和格式化
+
+## ESLint
+
+ESLint 是一个代码规范和错误检查工具,用于识别和报告 TypeScript 代码中的语法错误。
+
+### 命令
+
+```bash
+pnpm eslint .
+```
+
+### 配置
+
+eslint 配置文件为 `eslint.config.mjs`,其核心配置放在`internal/lint-configs/eslint-config`目录下,可以根据项目需求进行修改。
+
+## Stylelint
+
+Stylelint 用于校验项目内部 css 的风格,加上编辑器的自动修复,可以很好的统一项目内部 css 风格
+
+### 命令
+
+```bash
+pnpm stylelint "**/*.{vue,css,less.scss}"
+```
+
+### 配置
+
+Stylelint 配置文件为 `stylelint.config.mjs`,其核心配置放在`internal/lint-configs/stylelint-config`目录下,可以根据项目需求进行修改。
+
+## Prettier
+
+Prettier 可以用于统一项目代码风格,统一的缩进,单双引号,尾逗号等等风格
+
+### 命令
+
+```bash
+pnpm prettier .
+```
+
+### 配置
+
+Prettier 配置文件为 `.prettier.mjs`,其核心配置放在`internal/lint-configs/prettier-config`目录下,可以根据项目需求进行修改。
+
+## CommitLint
+
+在一个团队中,每个人的 git 的 commit 信息都不一样,五花八门,没有一个机制很难保证规范化,如何才能规范化呢?可能你想到的是 git 的 hook 机制,去写 shell 脚本去实现。这当然可以,其实 JavaScript 有一个很好的工具可以实现这个模板,它就是 commitlint(用于校验 git 提交信息规范)。
+
+### 配置
+
+CommitLint 配置文件为 `.commitlintrc.mjs`,其核心配置放在`internal/lint-configs/commitlint-config`目录下,可以根据项目需求进行修改。
+
+### Git 提交规范
+
+参考 [Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
+
+- `feat` 增加新功能
+- `fix` 修复问题/BUG
+- `style` 代码风格相关无影响运行结果的
+- `perf` 优化/性能提升
+- `refactor` 重构
+- `revert` 撤销修改
+- `test` 测试相关
+- `docs` 文档/注释
+- `chore` 依赖更新/脚手架配置修改等
+- `workflow` 工作流改进
+- `ci` 持续集成
+- `types` 类型修改
+
+### 关闭Git提交规范检查
+
+如果你想关闭 Git 提交规范检查,有两种方式:
+
+::: code-group
+
+```bash [临时关闭]
+git commit -m 'feat: add home page' --no-verify
+```
+
+```bash [永久关闭]
+# 在 .husky/commit-msg 内注释以下代码即可
+pnpm exec commitlint --edit "$1" # [!code --]
+```
+
+:::
+
+## Publint
+
+Publint 是一个用于检查 npm 包的规范的工具,可以检查包的版本号是否符合规范,是否符合标准的 ESM 规范包等等。
+
+### 命令
+
+```bash
+pnpm vsh publint
+```
+
+## Cspell
+
+Cspell 是一个用于检查拼写错误的工具,可以检查代码中的拼写错误,避免因为拼写错误导致的 bug。
+
+### 命令
+
+```bash
+pnpm cspell lint \"**/*.ts\" \"**/README.md\" \".changeset/*.md\" --no-progress
+```
+
+### 配置
+
+cspell 配置文件为 `cspell.json`,可以根据项目需求进行修改。
+
+## Git Hook
+
+git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风格校验,如果校验没通过,则不会进行提交。需要开发者自行修改后再次进行提交
+
+### lefthook
+
+有一个问题就是校验会校验全部代码,但是我们只想校验我们自己提交的代码,这个时候就可以使用 lefthook。
+
+最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 lefthook 在本地提交之前先做一次 Lint 校验。
+
+项目在 `lefthook.yml` 内部定义了相应的 hooks:
+
+- `pre-commit`: 在提交前运行,用于代码格式化和检查
+
+ - `code-workspace`: 更新 VSCode 工作区配置
+ - `lint-md`: 格式化 Markdown 文件
+ - `lint-vue`: 格式化并检查 Vue 文件
+ - `lint-js`: 格式化并检查 JavaScript/TypeScript 文件
+ - `lint-style`: 格式化并检查样式文件
+ - `lint-package`: 格式化 package.json
+ - `lint-json`: 格式化其他 JSON 文件
+
+- `post-merge`: 在合并后运行,用于自动安装依赖
+
+ - `install`: 运行 `pnpm install` 安装新依赖
+
+- `commit-msg`: 在提交时运行,用于检查提交信息格式
+ - `commitlint`: 使用 commitlint 检查提交信息
+
+#### 如何关闭 lefthook
+
+如果你想关闭 lefthook,有两种方式:
+
+::: code-group
+
+```bash [临时关闭]
+git commit -m 'feat: add home page' --no-verify
+```
+
+```bash [永久关闭]
+# 删除 lefthook.yml 文件即可
+rm lefthook.yml
+```
+
+:::
+
+#### 如何修改 lefthook 配置
+
+如果你想修改 lefthook 的配置,可以编辑 `lefthook.yml` 文件。例如:
+
+```yaml
+pre-commit:
+ parallel: true # 并行执行任务
+ jobs:
+ - name: lint-js
+ run: pnpm prettier --cache --ignore-unknown --write {staged_files}
+ glob: '*.{js,jsx,ts,tsx}'
+```
+
+其中:
+
+- `parallel`: 是否并行执行任务
+- `jobs`: 定义要执行的任务列表
+- `name`: 任务名称
+- `run`: 要执行的命令
+- `glob`: 匹配的文件模式
+- `{staged_files}`: 表示暂存的文件列表
diff --git a/web/docs/src/guide/project/tailwindcss.md b/web/docs/src/guide/project/tailwindcss.md
new file mode 100644
index 0000000..837076c
--- /dev/null
+++ b/web/docs/src/guide/project/tailwindcss.md
@@ -0,0 +1,17 @@
+# Tailwind CSS
+
+[Tailwind CSS](https://tailwindcss.com/) 是一个实用性优先的CSS框架,用于快速构建自定义设计。
+
+## 配置
+
+项目的配置文件位于 `internal/tailwind-config` 下,你可以在这里修改 Tailwind CSS 的配置。
+
+::: tip 包使用 tailwindcss 的限制
+
+当前只有对应的包下面存在 `tailwind.config.mjs` 文件才会启用 tailwindcss 的编译,否则不会启用 tailwindcss。如果你是纯粹的 SDK 包,不需要使用 tailwindcss,可以不用创建 `tailwind.config.mjs` 文件。
+
+:::
+
+## 提示
+
+现`tailwindcss`已至v4.x版本,使用方法与`tailwindcss: ^3.4.17`有差异,v4.0无法与v3.x版本兼容,在开发前请确认`package.json`中的`tailwindcss`版本。
diff --git a/web/docs/src/guide/project/test.md b/web/docs/src/guide/project/test.md
new file mode 100644
index 0000000..abf3c50
--- /dev/null
+++ b/web/docs/src/guide/project/test.md
@@ -0,0 +1,33 @@
+# 单元测试
+
+项目内置了 [Vitest](https://vitest.dev/) 作为单元测试工具。Vitest 是一个基于 Vite 的测试运行器,它提供了一套简单的 API 来编写测试用例。
+
+## 编写测试用例
+
+在项目中,我们约定将测试文件名以 `.test.ts` 结尾,或者存放到`__tests__`目录内。例如,创建一个 `utils.ts` 文件,然后同级目录`utils.test.ts` 文件,
+
+```ts
+// utils.test.ts
+import { expect, test } from 'vitest';
+import { sum } from './sum';
+
+test('adds 1 + 2 to equal 3', () => {
+ expect(sum(1, 2)).toBe(3);
+});
+```
+
+## 运行测试
+
+在大仓根目录下运行以下命令即可:
+
+```bash
+pnpm test:unit
+```
+
+## 现有单元测试
+
+项目中已经有一些单元测试用例,可以搜索以`.test.ts`结尾的文件查看,在你更改到相关代码时,可以运行单元测试来保证代码的正确性,建议在开发过程中,保持单元测试的覆盖率,且同时在 CI/CD 流程中运行单元测试,保证测试通过在进行项目部署。
+
+现有单元测试情况:
+
+
diff --git a/web/docs/src/guide/project/vite.md b/web/docs/src/guide/project/vite.md
new file mode 100644
index 0000000..51b5a38
--- /dev/null
+++ b/web/docs/src/guide/project/vite.md
@@ -0,0 +1,33 @@
+# Vite Config
+
+项目封装了一层vite配置,并集成了一些插件,方便在多个包以及应用内复用,使用方式如下:
+
+## 应用
+
+```ts
+// vite.config.mts
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ // vite配置,参考vite官方文档进行覆盖
+ vite: {},
+ };
+});
+```
+
+## 包
+
+```ts
+// vite.config.mts
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ library: {},
+ // vite配置,参考vite官方文档进行覆盖
+ vite: {},
+ };
+});
+```
diff --git a/web/docs/src/index.md b/web/docs/src/index.md
new file mode 100644
index 0000000..3a346d0
--- /dev/null
+++ b/web/docs/src/index.md
@@ -0,0 +1,111 @@
+---
+# https://vitepress.dev/reference/default-theme-home-page
+layout: home
+sidebar: false
+
+hero:
+ name: Vben Admin
+ text: 企业级管理系统框架
+ tagline: 全新升级,开箱即用,简单高效
+ image:
+ src: https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp
+ alt: Vben Admin
+ actions:
+ - theme: brand
+ text: 快速开始 ->
+ link: /guide/introduction/vben
+ - theme: alt
+ text: 在线预览
+ link: https://www.vben.pro
+ - theme: alt
+ text: 在 GitHub 查看
+ link: https://github.com/vbenjs/vue-vben-admin
+ - theme: alt
+ text: DeepWiki 文档
+ link: https://deepwiki.com/vbenjs/vue-vben-admin
+
+features:
+ - icon: 🚀
+ title: 最新技术栈
+ details: 基于 Vue3、Pinia、Vue Router、TypeScript、等最新技术栈。
+ link: /guide/introduction/quick-start
+ linkText: 快速开始
+ - icon: 🦄
+ title: 丰富的配置
+ details: 企业级中后台前端解决方案,提供丰富的组件和模板以及 N 种偏好设置组合方案。
+ link: /guide/essentials/settings
+ linkText: 配置文档
+ - icon: 🎨
+ title: 主题定制
+ details: 通过简单的配置,即可实现各种主题切换,满足个性化需求。
+ link: /guide/in-depth/theme
+ linkText: 主题文档
+ - icon: 🌐
+ title: 国际化
+ details: 内置国际化方案,支持多语言切换,满足国际化需求。
+ link: /guide/in-depth/locale
+ linkText: 国际化文档
+ - icon: 🔐
+ title: 权限管理
+ details: 内置权限管理方案,支持多种权限控制方式,满足各种权限需求。
+ link: /guide/in-depth/access
+ linkText: 权限文档
+ - title: Vite
+ icon:
+ src: /logos/vite.svg
+ details: 现代化的前端构建工具,快速冷启动,瞬间热更新。
+ link: https://vitejs.dev/
+ linkText: 官方站点
+ - title: Shadcn UI
+ icon:
+ src: /logos/shadcn-ui.svg
+ details: 核心基于 Shadcn UI + Tailwindcss,业务可支持任意的 UI 框架。
+ link: https://www.shadcn-vue.com/
+ linkText: 官方站点
+ - title: Turbo Repo
+ icon:
+ src: /logos/turborepo.svg
+ details: 规范且标准的大仓架构,使用 pnpm + monorepo + turbo 工程管理模式,提供企业级开发规范。
+ link: https://turbo.build/
+ linkText: 官方站点
+ - title: Nitro Mock Server
+ icon:
+ src: /logos/nitro.svg
+ details: 内置 Nitro Mock 服务,让你的 mock 服务更加强大。
+ link: https://nitro.unjs.io/
+ linkText: 官方站点
+---
+
+
+
+
diff --git a/web/docs/src/public/favicon.ico b/web/docs/src/public/favicon.ico
new file mode 100644
index 0000000..fcf9818
Binary files /dev/null and b/web/docs/src/public/favicon.ico differ
diff --git a/web/docs/src/public/guide/devtools.png b/web/docs/src/public/guide/devtools.png
new file mode 100644
index 0000000..b34a685
Binary files /dev/null and b/web/docs/src/public/guide/devtools.png differ
diff --git a/web/docs/src/public/guide/loading.png b/web/docs/src/public/guide/loading.png
new file mode 100644
index 0000000..e1d0046
Binary files /dev/null and b/web/docs/src/public/guide/loading.png differ
diff --git a/web/docs/src/public/guide/locale.png b/web/docs/src/public/guide/locale.png
new file mode 100644
index 0000000..e9c79d0
Binary files /dev/null and b/web/docs/src/public/guide/locale.png differ
diff --git a/web/docs/src/public/guide/login-expired.png b/web/docs/src/public/guide/login-expired.png
new file mode 100644
index 0000000..bc82ba5
Binary files /dev/null and b/web/docs/src/public/guide/login-expired.png differ
diff --git a/web/docs/src/public/guide/login.png b/web/docs/src/public/guide/login.png
new file mode 100644
index 0000000..3774b0b
Binary files /dev/null and b/web/docs/src/public/guide/login.png differ
diff --git a/web/docs/src/public/guide/preferences.png b/web/docs/src/public/guide/preferences.png
new file mode 100644
index 0000000..9b1582f
Binary files /dev/null and b/web/docs/src/public/guide/preferences.png differ
diff --git a/web/docs/src/public/guide/qq.png b/web/docs/src/public/guide/qq.png
new file mode 100644
index 0000000..cc76cf7
Binary files /dev/null and b/web/docs/src/public/guide/qq.png differ
diff --git a/web/docs/src/public/guide/qq_channel.png b/web/docs/src/public/guide/qq_channel.png
new file mode 100644
index 0000000..5fa807b
Binary files /dev/null and b/web/docs/src/public/guide/qq_channel.png differ
diff --git a/web/docs/src/public/guide/report.png b/web/docs/src/public/guide/report.png
new file mode 100644
index 0000000..fbff280
Binary files /dev/null and b/web/docs/src/public/guide/report.png differ
diff --git a/web/docs/src/public/guide/test.png b/web/docs/src/public/guide/test.png
new file mode 100644
index 0000000..cbd4dc6
Binary files /dev/null and b/web/docs/src/public/guide/test.png differ
diff --git a/web/docs/src/public/guide/update-notice.png b/web/docs/src/public/guide/update-notice.png
new file mode 100644
index 0000000..a4436cf
Binary files /dev/null and b/web/docs/src/public/guide/update-notice.png differ
diff --git a/web/docs/src/public/logos/nitro.svg b/web/docs/src/public/logos/nitro.svg
new file mode 100644
index 0000000..e5564f2
--- /dev/null
+++ b/web/docs/src/public/logos/nitro.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/docs/src/public/logos/shadcn-ui.svg b/web/docs/src/public/logos/shadcn-ui.svg
new file mode 100644
index 0000000..2d584b9
--- /dev/null
+++ b/web/docs/src/public/logos/shadcn-ui.svg
@@ -0,0 +1 @@
+
diff --git a/web/docs/src/public/logos/turborepo.svg b/web/docs/src/public/logos/turborepo.svg
new file mode 100644
index 0000000..9c7035a
--- /dev/null
+++ b/web/docs/src/public/logos/turborepo.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/docs/src/public/logos/vite.svg b/web/docs/src/public/logos/vite.svg
new file mode 100644
index 0000000..de4aedd
--- /dev/null
+++ b/web/docs/src/public/logos/vite.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/docs/src/sponsor/personal.md b/web/docs/src/sponsor/personal.md
new file mode 100644
index 0000000..fa2fa23
--- /dev/null
+++ b/web/docs/src/sponsor/personal.md
@@ -0,0 +1,12 @@
+# 赞助
+
+如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
+
+
+
+您的赞助将帮助我们:
+
+- 维持项目的基础设施,如服务器、域名、社群费用。
+- 支持开发者的贡献和加快新功能的开发。
+
+感谢所有现有的和未来的赞助者,您的支持对我们来说至关重要,让我们一起推动项目继续前行。
diff --git a/web/docs/tailwind.config.mjs b/web/docs/tailwind.config.mjs
new file mode 100644
index 0000000..9dbc8c3
--- /dev/null
+++ b/web/docs/tailwind.config.mjs
@@ -0,0 +1,11 @@
+import tailwindcssConfig from '@vben/tailwind-config';
+
+export default {
+ ...tailwindcssConfig,
+ content: [
+ ...tailwindcssConfig.content,
+ '.vitepress/**/*.{js,mts,ts,vue}',
+ 'src/demos/**/*.{js,mts,ts,vue}',
+ 'src/**/*.md',
+ ],
+};
diff --git a/web/docs/tsconfig.json b/web/docs/tsconfig.json
new file mode 100644
index 0000000..05147a9
--- /dev/null
+++ b/web/docs/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "#/*": ["./src/_env/*"]
+ }
+ },
+ "include": [
+ ".vitepress/*.mts",
+ ".vitepress/**/*.ts",
+ ".vitepress/**/*.vue",
+ "src/*.mts",
+ "src/**/*.ts",
+ "src/**/*.vue"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs
new file mode 100644
index 0000000..b29b567
--- /dev/null
+++ b/web/eslint.config.mjs
@@ -0,0 +1,5 @@
+// @ts-check
+
+import { defineConfig } from '@vben/eslint-config';
+
+export default defineConfig();
diff --git a/web/internal/lint-configs/commitlint-config/index.mjs b/web/internal/lint-configs/commitlint-config/index.mjs
new file mode 100644
index 0000000..3d85439
--- /dev/null
+++ b/web/internal/lint-configs/commitlint-config/index.mjs
@@ -0,0 +1,153 @@
+import { execSync } from 'node:child_process';
+
+import { getPackagesSync } from '@vben/node-utils';
+
+const { packages } = getPackagesSync();
+
+const allowedScopes = [
+ ...packages.map((pkg) => pkg.packageJson.name),
+ 'project',
+ 'style',
+ 'lint',
+ 'ci',
+ 'dev',
+ 'deploy',
+ 'other',
+];
+
+// precomputed scope
+const scopeComplete = execSync('git status --porcelain || true')
+ .toString()
+ .trim()
+ .split('\n')
+ .find((r) => ~r.indexOf('M src'))
+ ?.replace(/(\/)/g, '%%')
+ ?.match(/src%%((\w|-)*)/)?.[1]
+ ?.replace(/s$/, '');
+
+/**
+ * @type {import('cz-git').UserConfig}
+ */
+const userConfig = {
+ extends: ['@commitlint/config-conventional'],
+ plugins: ['commitlint-plugin-function-rules'],
+ prompt: {
+ /** @use `pnpm commit :f` */
+ alias: {
+ b: 'build: bump dependencies',
+ c: 'chore: update config',
+ f: 'docs: fix typos',
+ r: 'docs: update README',
+ s: 'style: update code format',
+ },
+ allowCustomIssuePrefixs: false,
+ // scopes: [...scopes, 'mock'],
+ allowEmptyIssuePrefixs: false,
+ customScopesAlign: scopeComplete ? 'bottom' : 'top',
+ defaultScope: scopeComplete,
+ // English
+ typesAppend: [
+ { name: 'workflow: workflow improvements', value: 'workflow' },
+ { name: 'types: type definition file changes', value: 'types' },
+ ],
+
+ // 中英文对照版
+ // messages: {
+ // type: '选择你要提交的类型 :',
+ // scope: '选择一个提交范围 (可选):',
+ // customScope: '请输入自定义的提交范围 :',
+ // subject: '填写简短精炼的变更描述 :\n',
+ // body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
+ // breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
+ // footerPrefixsSelect: '选择关联issue前缀 (可选):',
+ // customFooterPrefixs: '输入自定义issue前缀 :',
+ // footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
+ // confirmCommit: '是否提交或修改commit ?',
+ // },
+ // types: [
+ // { value: 'feat', name: 'feat: 新增功能' },
+ // { value: 'fix', name: 'fix: 修复缺陷' },
+ // { value: 'docs', name: 'docs: 文档变更' },
+ // { value: 'style', name: 'style: 代码格式' },
+ // { value: 'refactor', name: 'refactor: 代码重构' },
+ // { value: 'perf', name: 'perf: 性能优化' },
+ // { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
+ // { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
+ // { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
+ // { value: 'revert', name: 'revert: 回滚 commit' },
+ // { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
+ // { value: 'wip', name: 'wip: 正在开发中' },
+ // { value: 'workflow', name: 'workflow: 工作流程改进' },
+ // { value: 'types', name: 'types: 类型定义文件修改' },
+ // ],
+ // emptyScopesAlias: 'empty: 不填写',
+ // customScopesAlias: 'custom: 自定义',
+ },
+ rules: {
+ /**
+ * type[scope]: [function] description
+ *
+ * ^^^^^^^^^^^^^^ empty line.
+ * - Something here
+ */
+ 'body-leading-blank': [2, 'always'],
+ /**
+ * type[scope]: [function] description
+ *
+ * - something here
+ *
+ * ^^^^^^^^^^^^^^
+ */
+ 'footer-leading-blank': [1, 'always'],
+ /**
+ * type[scope]: [function] description
+ * ^^^^^
+ */
+ 'function-rules/scope-enum': [
+ 2, // level: error
+ 'always',
+ (parsed) => {
+ if (!parsed.scope || allowedScopes.includes(parsed.scope)) {
+ return [true];
+ }
+
+ return [false, `scope must be one of ${allowedScopes.join(', ')}`];
+ },
+ ],
+ /**
+ * type[scope]: [function] description [No more than 108 characters]
+ * ^^^^^
+ */
+ 'header-max-length': [2, 'always', 108],
+
+ 'scope-enum': [0],
+ 'subject-case': [0],
+ 'subject-empty': [2, 'never'],
+ 'type-empty': [2, 'never'],
+ /**
+ * type[scope]: [function] description
+ * ^^^^
+ */
+ 'type-enum': [
+ 2,
+ 'always',
+ [
+ 'feat',
+ 'fix',
+ 'perf',
+ 'style',
+ 'docs',
+ 'test',
+ 'refactor',
+ 'build',
+ 'ci',
+ 'chore',
+ 'revert',
+ 'types',
+ 'release',
+ ],
+ ],
+ },
+};
+
+export default userConfig;
diff --git a/web/internal/lint-configs/commitlint-config/package.json b/web/internal/lint-configs/commitlint-config/package.json
new file mode 100644
index 0000000..a137f94
--- /dev/null
+++ b/web/internal/lint-configs/commitlint-config/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@vben/commitlint-config",
+ "version": "5.5.7",
+ "private": true,
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "internal/lint-configs/commitlint-config"
+ },
+ "license": "MIT",
+ "type": "module",
+ "files": [
+ "dist"
+ ],
+ "main": "./index.mjs",
+ "module": "./index.mjs",
+ "exports": {
+ ".": {
+ "import": "./index.mjs",
+ "default": "./index.mjs"
+ }
+ },
+ "dependencies": {
+ "@commitlint/cli": "catalog:",
+ "@commitlint/config-conventional": "catalog:",
+ "@vben/node-utils": "workspace:*",
+ "commitlint-plugin-function-rules": "catalog:",
+ "cz-git": "catalog:",
+ "czg": "catalog:"
+ }
+}
diff --git a/web/internal/lint-configs/eslint-config/build.config.ts b/web/internal/lint-configs/eslint-config/build.config.ts
new file mode 100644
index 0000000..97e572c
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/build.config.ts
@@ -0,0 +1,7 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+ clean: true,
+ declaration: true,
+ entries: ['src/index'],
+});
diff --git a/web/internal/lint-configs/eslint-config/package.json b/web/internal/lint-configs/eslint-config/package.json
new file mode 100644
index 0000000..12556ec
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@vben/eslint-config",
+ "version": "5.0.0",
+ "private": true,
+ "homepage": "https://github.com/vbenjs/vue-vben-admin",
+ "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+ "directory": "internal/lint-configs/eslint-config"
+ },
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "stub": "pnpm unbuild --stub"
+ },
+ "files": [
+ "dist"
+ ],
+ "main": "./dist/index.mjs",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs"
+ }
+ },
+ "dependencies": {
+ "eslint-config-turbo": "catalog:",
+ "eslint-plugin-command": "catalog:",
+ "eslint-plugin-import-x": "catalog:"
+ },
+ "devDependencies": {
+ "@eslint/js": "catalog:",
+ "@types/eslint": "catalog:",
+ "@typescript-eslint/eslint-plugin": "catalog:",
+ "@typescript-eslint/parser": "catalog:",
+ "eslint": "catalog:",
+ "eslint-plugin-eslint-comments": "catalog:",
+ "eslint-plugin-jsdoc": "catalog:",
+ "eslint-plugin-jsonc": "catalog:",
+ "eslint-plugin-n": "catalog:",
+ "eslint-plugin-no-only-tests": "catalog:",
+ "eslint-plugin-perfectionist": "catalog:",
+ "eslint-plugin-prettier": "catalog:",
+ "eslint-plugin-regexp": "catalog:",
+ "eslint-plugin-unicorn": "catalog:",
+ "eslint-plugin-unused-imports": "catalog:",
+ "eslint-plugin-vitest": "catalog:",
+ "eslint-plugin-vue": "catalog:",
+ "globals": "catalog:",
+ "jsonc-eslint-parser": "catalog:",
+ "vue-eslint-parser": "catalog:"
+ }
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/command.ts b/web/internal/lint-configs/eslint-config/src/configs/command.ts
new file mode 100644
index 0000000..67651b2
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/command.ts
@@ -0,0 +1,10 @@
+import createCommand from 'eslint-plugin-command/config';
+
+export async function command() {
+ return [
+ {
+ // @ts-expect-error - no types
+ ...createCommand(),
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/comments.ts b/web/internal/lint-configs/eslint-config/src/configs/comments.ts
new file mode 100644
index 0000000..77ccd5d
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/comments.ts
@@ -0,0 +1,24 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function comments(): Promise {
+ const [pluginComments] = await Promise.all([
+ // @ts-expect-error - no types
+ interopDefault(import('eslint-plugin-eslint-comments')),
+ ] as const);
+
+ return [
+ {
+ plugins: {
+ 'eslint-comments': pluginComments,
+ },
+ rules: {
+ 'eslint-comments/no-aggregating-enable': 'error',
+ 'eslint-comments/no-duplicate-disable': 'error',
+ 'eslint-comments/no-unlimited-disable': 'error',
+ 'eslint-comments/no-unused-enable': 'error',
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/disableds.ts b/web/internal/lint-configs/eslint-config/src/configs/disableds.ts
new file mode 100644
index 0000000..152b84c
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/disableds.ts
@@ -0,0 +1,28 @@
+import type { Linter } from 'eslint';
+
+export async function disableds(): Promise {
+ return [
+ {
+ files: ['**/__tests__/**/*.?([cm])[jt]s?(x)'],
+ name: 'disables/test',
+ rules: {
+ '@typescript-eslint/ban-ts-comment': 'off',
+ 'no-console': 'off',
+ },
+ },
+ {
+ files: ['**/*.d.ts'],
+ name: 'disables/dts',
+ rules: {
+ '@typescript-eslint/triple-slash-reference': 'off',
+ },
+ },
+ {
+ files: ['**/*.js', '**/*.mjs', '**/*.cjs'],
+ name: 'disables/js',
+ rules: {
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/ignores.ts b/web/internal/lint-configs/eslint-config/src/configs/ignores.ts
new file mode 100644
index 0000000..136c956
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/ignores.ts
@@ -0,0 +1,52 @@
+import type { Linter } from 'eslint';
+
+export async function ignores(): Promise {
+ return [
+ {
+ ignores: [
+ '**/node_modules',
+ '**/dist',
+ '**/dist-*',
+ '**/*-dist',
+ '**/.husky',
+ '**/.nitro',
+ '**/.output',
+ '**/Dockerfile',
+ '**/package-lock.json',
+ '**/yarn.lock',
+ '**/pnpm-lock.yaml',
+ '**/bun.lockb',
+ '**/output',
+ '**/coverage',
+ '**/temp',
+ '**/.temp',
+ '**/tmp',
+ '**/.tmp',
+ '**/.history',
+ '**/.turbo',
+ '**/.nuxt',
+ '**/.next',
+ '**/.vercel',
+ '**/.changeset',
+ '**/.idea',
+ '**/.cache',
+ '**/.output',
+ '**/.vite-inspect',
+
+ '**/CHANGELOG*.md',
+ '**/*.min.*',
+ '**/LICENSE*',
+ '**/__snapshots__',
+ '**/*.snap',
+ '**/fixtures/**',
+ '**/.vitepress/cache/**',
+ '**/auto-import?(s).d.ts',
+ '**/components.d.ts',
+ '**/vite.config.mts.*',
+ '**/*.sh',
+ '**/*.ttf',
+ '**/*.woff',
+ ],
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/import.ts b/web/internal/lint-configs/eslint-config/src/configs/import.ts
new file mode 100644
index 0000000..ce6cf65
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/import.ts
@@ -0,0 +1,25 @@
+import type { Linter } from 'eslint';
+
+import * as pluginImport from 'eslint-plugin-import-x';
+
+export async function importPluginConfig(): Promise {
+ return [
+ {
+ plugins: {
+ // @ts-expect-error - This is a dynamic import
+ import: pluginImport,
+ },
+ rules: {
+ 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
+ 'import/first': 'error',
+ 'import/newline-after-import': 'error',
+ 'import/no-duplicates': 'error',
+ 'import/no-mutable-exports': 'error',
+ 'import/no-named-default': 'error',
+ 'import/no-self-import': 'error',
+ 'import/no-unresolved': 'off',
+ 'import/no-webpack-loader-syntax': 'error',
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/index.ts b/web/internal/lint-configs/eslint-config/src/configs/index.ts
new file mode 100644
index 0000000..c0284ef
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/index.ts
@@ -0,0 +1,17 @@
+export * from './command';
+export * from './comments';
+export * from './disableds';
+export * from './ignores';
+export * from './import';
+export * from './javascript';
+export * from './jsdoc';
+export * from './jsonc';
+export * from './node';
+export * from './perfectionist';
+export * from './prettier';
+export * from './regexp';
+export * from './test';
+export * from './turbo';
+export * from './typescript';
+export * from './unicorn';
+export * from './vue';
diff --git a/web/internal/lint-configs/eslint-config/src/configs/javascript.ts b/web/internal/lint-configs/eslint-config/src/configs/javascript.ts
new file mode 100644
index 0000000..44cf5b6
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/javascript.ts
@@ -0,0 +1,241 @@
+import type { Linter } from 'eslint';
+
+import js from '@eslint/js';
+import pluginUnusedImports from 'eslint-plugin-unused-imports';
+import globals from 'globals';
+
+export async function javascript(): Promise {
+ return [
+ {
+ languageOptions: {
+ ecmaVersion: 'latest',
+ globals: {
+ ...globals.browser,
+ ...globals.es2021,
+ ...globals.node,
+ document: 'readonly',
+ navigator: 'readonly',
+ window: 'readonly',
+ },
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ sourceType: 'module',
+ },
+ linterOptions: {
+ reportUnusedDisableDirectives: true,
+ },
+ plugins: {
+ 'unused-imports': pluginUnusedImports,
+ },
+ rules: {
+ ...js.configs.recommended.rules,
+ 'accessor-pairs': [
+ 'error',
+ { enforceForClassMembers: true, setWithoutGet: true },
+ ],
+ 'array-callback-return': 'error',
+ 'block-scoped-var': 'error',
+ 'constructor-super': 'error',
+ 'default-case-last': 'error',
+ 'dot-notation': ['error', { allowKeywords: true }],
+ eqeqeq: ['error', 'always'],
+ 'keyword-spacing': 'off',
+
+ 'new-cap': [
+ 'error',
+ { capIsNew: false, newIsCap: true, properties: true },
+ ],
+ 'no-alert': 'error',
+ 'no-array-constructor': 'error',
+ 'no-async-promise-executor': 'error',
+ 'no-caller': 'error',
+ 'no-case-declarations': 'error',
+ 'no-class-assign': 'error',
+ 'no-compare-neg-zero': 'error',
+ 'no-cond-assign': ['error', 'always'],
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
+ 'no-const-assign': 'error',
+ 'no-control-regex': 'error',
+ 'no-debugger': 'error',
+ 'no-delete-var': 'error',
+ 'no-dupe-args': 'error',
+ 'no-dupe-class-members': 'error',
+ 'no-dupe-keys': 'error',
+ 'no-duplicate-case': 'error',
+ 'no-empty': ['error', { allowEmptyCatch: true }],
+ 'no-empty-character-class': 'error',
+ 'no-empty-function': 'off',
+ 'no-empty-pattern': 'error',
+ 'no-eval': 'error',
+ 'no-ex-assign': 'error',
+ 'no-extend-native': 'error',
+ 'no-extra-bind': 'error',
+ 'no-extra-boolean-cast': 'error',
+ 'no-fallthrough': 'error',
+ 'no-func-assign': 'error',
+ 'no-global-assign': 'error',
+ 'no-implied-eval': 'error',
+ 'no-import-assign': 'error',
+ 'no-invalid-regexp': 'error',
+ 'no-irregular-whitespace': 'error',
+ 'no-iterator': 'error',
+ 'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
+ 'no-lone-blocks': 'error',
+ 'no-loss-of-precision': 'error',
+ 'no-misleading-character-class': 'error',
+ 'no-multi-str': 'error',
+ 'no-new': 'error',
+ 'no-new-func': 'error',
+ 'no-new-object': 'error',
+ 'no-new-symbol': 'error',
+ 'no-new-wrappers': 'error',
+ 'no-obj-calls': 'error',
+ 'no-octal': 'error',
+ 'no-octal-escape': 'error',
+ 'no-proto': 'error',
+ 'no-prototype-builtins': 'error',
+ 'no-redeclare': ['error', { builtinGlobals: false }],
+ 'no-regex-spaces': 'error',
+ 'no-restricted-globals': [
+ 'error',
+ { message: 'Use `globalThis` instead.', name: 'global' },
+ { message: 'Use `globalThis` instead.', name: 'self' },
+ ],
+ 'no-restricted-properties': [
+ 'error',
+ {
+ message:
+ 'Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead.',
+ property: '__proto__',
+ },
+ {
+ message: 'Use `Object.defineProperty` instead.',
+ property: '__defineGetter__',
+ },
+ {
+ message: 'Use `Object.defineProperty` instead.',
+ property: '__defineSetter__',
+ },
+ {
+ message: 'Use `Object.getOwnPropertyDescriptor` instead.',
+ property: '__lookupGetter__',
+ },
+ {
+ message: 'Use `Object.getOwnPropertyDescriptor` instead.',
+ property: '__lookupSetter__',
+ },
+ ],
+ 'no-restricted-syntax': [
+ 'error',
+ 'DebuggerStatement',
+ 'LabeledStatement',
+ 'WithStatement',
+ 'TSEnumDeclaration[const=true]',
+ 'TSExportAssignment',
+ ],
+ 'no-self-assign': ['error', { props: true }],
+ 'no-self-compare': 'error',
+ 'no-sequences': 'error',
+ 'no-shadow-restricted-names': 'error',
+ 'no-sparse-arrays': 'error',
+ 'no-template-curly-in-string': 'error',
+ 'no-this-before-super': 'error',
+ 'no-throw-literal': 'error',
+ 'no-undef': 'off',
+ 'no-undef-init': 'error',
+ 'no-unexpected-multiline': 'error',
+ 'no-unmodified-loop-condition': 'error',
+ 'no-unneeded-ternary': ['error', { defaultAssignment: false }],
+ 'no-unreachable': 'error',
+ 'no-unreachable-loop': 'error',
+ 'no-unsafe-finally': 'error',
+ 'no-unsafe-negation': 'error',
+ 'no-unused-expressions': [
+ 'error',
+ {
+ allowShortCircuit: true,
+ allowTaggedTemplates: true,
+ allowTernary: true,
+ },
+ ],
+ 'no-unused-vars': [
+ 'error',
+ {
+ args: 'none',
+ caughtErrors: 'none',
+ ignoreRestSiblings: true,
+ vars: 'all',
+ },
+ ],
+ 'no-use-before-define': [
+ 'error',
+ { classes: false, functions: false, variables: false },
+ ],
+ 'no-useless-backreference': 'error',
+ 'no-useless-call': 'error',
+ 'no-useless-catch': 'error',
+ 'no-useless-computed-key': 'error',
+ 'no-useless-constructor': 'error',
+ 'no-useless-rename': 'error',
+ 'no-useless-return': 'error',
+ 'no-var': 'error',
+ 'no-with': 'error',
+ 'object-shorthand': [
+ 'error',
+ 'always',
+ { avoidQuotes: true, ignoreConstructors: false },
+ ],
+ 'one-var': ['error', { initialized: 'never' }],
+ 'prefer-arrow-callback': [
+ 'error',
+ {
+ allowNamedFunctions: false,
+ allowUnboundThis: true,
+ },
+ ],
+ 'prefer-const': [
+ 'error',
+ {
+ destructuring: 'all',
+ ignoreReadBeforeAssign: true,
+ },
+ ],
+ 'prefer-exponentiation-operator': 'error',
+
+ 'prefer-promise-reject-errors': 'error',
+ 'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }],
+ 'prefer-rest-params': 'error',
+ 'prefer-spread': 'error',
+ 'prefer-template': 'error',
+ 'space-before-function-paren': 'off',
+ 'spaced-comment': 'error',
+ 'symbol-description': 'error',
+ 'unicode-bom': ['error', 'never'],
+
+ 'unused-imports/no-unused-imports': 'error',
+ 'unused-imports/no-unused-vars': [
+ 'error',
+ {
+ args: 'after-used',
+ argsIgnorePattern: '^_',
+ vars: 'all',
+ varsIgnorePattern: '^_',
+ },
+ ],
+ 'use-isnan': [
+ 'error',
+ { enforceForIndexOf: true, enforceForSwitchCase: true },
+ ],
+ 'valid-typeof': ['error', { requireStringLiterals: true }],
+
+ 'vars-on-top': 'error',
+ yoda: ['error', 'never'],
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/jsdoc.ts b/web/internal/lint-configs/eslint-config/src/configs/jsdoc.ts
new file mode 100644
index 0000000..1368197
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/jsdoc.ts
@@ -0,0 +1,34 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function jsdoc(): Promise {
+ const [pluginJsdoc] = await Promise.all([
+ interopDefault(import('eslint-plugin-jsdoc')),
+ ] as const);
+
+ return [
+ {
+ plugins: {
+ jsdoc: pluginJsdoc,
+ },
+ rules: {
+ 'jsdoc/check-access': 'warn',
+ 'jsdoc/check-param-names': 'warn',
+ 'jsdoc/check-property-names': 'warn',
+ 'jsdoc/check-types': 'warn',
+ 'jsdoc/empty-tags': 'warn',
+ 'jsdoc/implements-on-classes': 'warn',
+ 'jsdoc/no-defaults': 'warn',
+ 'jsdoc/no-multi-asterisks': 'warn',
+ 'jsdoc/require-param-name': 'warn',
+ 'jsdoc/require-property': 'warn',
+ 'jsdoc/require-property-description': 'warn',
+ 'jsdoc/require-property-name': 'warn',
+ 'jsdoc/require-returns-check': 'warn',
+ 'jsdoc/require-returns-description': 'warn',
+ 'jsdoc/require-yields-check': 'warn',
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/jsonc.ts b/web/internal/lint-configs/eslint-config/src/configs/jsonc.ts
new file mode 100644
index 0000000..4072e4c
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/jsonc.ts
@@ -0,0 +1,258 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function jsonc(): Promise {
+ const [pluginJsonc, parserJsonc] = await Promise.all([
+ interopDefault(import('eslint-plugin-jsonc')),
+ interopDefault(import('jsonc-eslint-parser')),
+ ] as const);
+
+ return [
+ {
+ files: ['**/*.json', '**/*.json5', '**/*.jsonc', '*.code-workspace'],
+ languageOptions: {
+ parser: parserJsonc as any,
+ },
+ plugins: {
+ jsonc: pluginJsonc as any,
+ },
+ rules: {
+ 'jsonc/no-bigint-literals': 'error',
+ 'jsonc/no-binary-expression': 'error',
+ 'jsonc/no-binary-numeric-literals': 'error',
+ 'jsonc/no-dupe-keys': 'error',
+ 'jsonc/no-escape-sequence-in-identifier': 'error',
+ 'jsonc/no-floating-decimal': 'error',
+ 'jsonc/no-hexadecimal-numeric-literals': 'error',
+ 'jsonc/no-infinity': 'error',
+ 'jsonc/no-multi-str': 'error',
+ 'jsonc/no-nan': 'error',
+ 'jsonc/no-number-props': 'error',
+ 'jsonc/no-numeric-separators': 'error',
+ 'jsonc/no-octal': 'error',
+ 'jsonc/no-octal-escape': 'error',
+ 'jsonc/no-octal-numeric-literals': 'error',
+ 'jsonc/no-parenthesized': 'error',
+ 'jsonc/no-plus-sign': 'error',
+ 'jsonc/no-regexp-literals': 'error',
+ 'jsonc/no-sparse-arrays': 'error',
+ 'jsonc/no-template-literals': 'error',
+ 'jsonc/no-undefined-value': 'error',
+ 'jsonc/no-unicode-codepoint-escapes': 'error',
+ 'jsonc/no-useless-escape': 'error',
+ 'jsonc/space-unary-ops': 'error',
+ 'jsonc/valid-json-number': 'error',
+ 'jsonc/vue-custom-block/no-parsing-error': 'error',
+ },
+ },
+ sortTsconfig(),
+ sortPackageJson(),
+ ];
+}
+
+function sortPackageJson(): Linter.Config {
+ return {
+ files: ['**/package.json'],
+ rules: {
+ 'jsonc/sort-array-values': [
+ 'error',
+ {
+ order: { type: 'asc' },
+ pathPattern: '^files$|^pnpm.neverBuiltDependencies$',
+ },
+ ],
+ 'jsonc/sort-keys': [
+ 'error',
+ {
+ order: [
+ 'name',
+ 'version',
+ 'description',
+ 'private',
+ 'keywords',
+ 'homepage',
+ 'bugs',
+ 'repository',
+ 'license',
+ 'author',
+ 'contributors',
+ 'categories',
+ 'funding',
+ 'type',
+ 'scripts',
+ 'files',
+ 'sideEffects',
+ 'bin',
+ 'main',
+ 'module',
+ 'unpkg',
+ 'jsdelivr',
+ 'types',
+ 'typesVersions',
+ 'imports',
+ 'exports',
+ 'publishConfig',
+ 'icon',
+ 'activationEvents',
+ 'contributes',
+ 'peerDependencies',
+ 'peerDependenciesMeta',
+ 'dependencies',
+ 'optionalDependencies',
+ 'devDependencies',
+ 'engines',
+ 'packageManager',
+ 'pnpm',
+ 'overrides',
+ 'resolutions',
+ 'husky',
+ 'simple-git-hooks',
+ 'lint-staged',
+ 'eslintConfig',
+ ],
+ pathPattern: '^$',
+ },
+ {
+ order: { type: 'asc' },
+ pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies(Meta)?$',
+ },
+ {
+ order: { type: 'asc' },
+ pathPattern: '^(?:resolutions|overrides|pnpm.overrides)$',
+ },
+ {
+ order: ['types', 'import', 'require', 'default'],
+ pathPattern: '^exports.*$',
+ },
+ ],
+ },
+ };
+}
+
+function sortTsconfig(): Linter.Config {
+ return {
+ files: [
+ '**/tsconfig.json',
+ '**/tsconfig.*.json',
+ 'internal/tsconfig/*.json',
+ ],
+ rules: {
+ 'jsonc/sort-keys': [
+ 'error',
+ {
+ order: [
+ 'extends',
+ 'compilerOptions',
+ 'references',
+ 'files',
+ 'include',
+ 'exclude',
+ ],
+ pathPattern: '^$',
+ },
+ {
+ order: [
+ /* Projects */
+ 'incremental',
+ 'composite',
+ 'tsBuildInfoFile',
+ 'disableSourceOfProjectReferenceRedirect',
+ 'disableSolutionSearching',
+ 'disableReferencedProjectLoad',
+ /* Language and Environment */
+ 'target',
+ 'jsx',
+ 'jsxFactory',
+ 'jsxFragmentFactory',
+ 'jsxImportSource',
+ 'lib',
+ 'moduleDetection',
+ 'noLib',
+ 'reactNamespace',
+ 'useDefineForClassFields',
+ 'emitDecoratorMetadata',
+ 'experimentalDecorators',
+ /* Modules */
+ 'baseUrl',
+ 'rootDir',
+ 'rootDirs',
+ 'customConditions',
+ 'module',
+ 'moduleResolution',
+ 'moduleSuffixes',
+ 'noResolve',
+ 'paths',
+ 'resolveJsonModule',
+ 'resolvePackageJsonExports',
+ 'resolvePackageJsonImports',
+ 'typeRoots',
+ 'types',
+ 'allowArbitraryExtensions',
+ 'allowImportingTsExtensions',
+ 'allowUmdGlobalAccess',
+ /* JavaScript Support */
+ 'allowJs',
+ 'checkJs',
+ 'maxNodeModuleJsDepth',
+ /* Type Checking */
+ 'strict',
+ 'strictBindCallApply',
+ 'strictFunctionTypes',
+ 'strictNullChecks',
+ 'strictPropertyInitialization',
+ 'allowUnreachableCode',
+ 'allowUnusedLabels',
+ 'alwaysStrict',
+ 'exactOptionalPropertyTypes',
+ 'noFallthroughCasesInSwitch',
+ 'noImplicitAny',
+ 'noImplicitOverride',
+ 'noImplicitReturns',
+ 'noImplicitThis',
+ 'noPropertyAccessFromIndexSignature',
+ 'noUncheckedIndexedAccess',
+ 'noUnusedLocals',
+ 'noUnusedParameters',
+ 'useUnknownInCatchVariables',
+ /* Emit */
+ 'declaration',
+ 'declarationDir',
+ 'declarationMap',
+ 'downlevelIteration',
+ 'emitBOM',
+ 'emitDeclarationOnly',
+ 'importHelpers',
+ 'importsNotUsedAsValues',
+ 'inlineSourceMap',
+ 'inlineSources',
+ 'mapRoot',
+ 'newLine',
+ 'noEmit',
+ 'noEmitHelpers',
+ 'noEmitOnError',
+ 'outDir',
+ 'outFile',
+ 'preserveConstEnums',
+ 'preserveValueImports',
+ 'removeComments',
+ 'sourceMap',
+ 'sourceRoot',
+ 'stripInternal',
+ /* Interop Constraints */
+ 'allowSyntheticDefaultImports',
+ 'esModuleInterop',
+ 'forceConsistentCasingInFileNames',
+ 'isolatedModules',
+ 'preserveSymlinks',
+ 'verbatimModuleSyntax',
+ /* Completeness */
+ 'skipDefaultLibCheck',
+ 'skipLibCheck',
+ ],
+ pathPattern: '^compilerOptions$',
+ },
+ ],
+ },
+ };
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/node.ts b/web/internal/lint-configs/eslint-config/src/configs/node.ts
new file mode 100644
index 0000000..fa960d8
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/node.ts
@@ -0,0 +1,57 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function node(): Promise {
+ const pluginNode = await interopDefault(import('eslint-plugin-n'));
+
+ return [
+ {
+ plugins: {
+ n: pluginNode,
+ },
+ rules: {
+ 'n/handle-callback-err': ['error', '^(err|error)$'],
+ 'n/no-deprecated-api': 'error',
+ 'n/no-exports-assign': 'error',
+ 'n/no-extraneous-import': [
+ 'error',
+ {
+ allowModules: [
+ 'unbuild',
+ '@vben/vite-config',
+ 'vitest',
+ 'vite',
+ '@vue/test-utils',
+ '@vben/tailwind-config',
+ '@playwright/test',
+ ],
+ },
+ ],
+ 'n/no-new-require': 'error',
+ 'n/no-path-concat': 'error',
+ // 'n/no-unpublished-import': 'off',
+ 'n/no-unsupported-features/es-syntax': [
+ 'error',
+ {
+ ignores: [],
+ version: '>=18.0.0',
+ },
+ ],
+ 'n/prefer-global/buffer': ['error', 'never'],
+ // 'n/no-missing-import': 'off',
+ 'n/prefer-global/process': ['error', 'never'],
+ 'n/process-exit-as-throw': 'error',
+ },
+ },
+ {
+ files: [
+ 'scripts/**/*.?([cm])[jt]s?(x)',
+ 'internal/**/*.?([cm])[jt]s?(x)',
+ ],
+ rules: {
+ 'n/prefer-global/process': 'off',
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/perfectionist.ts b/web/internal/lint-configs/eslint-config/src/configs/perfectionist.ts
new file mode 100644
index 0000000..136bfa3
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/perfectionist.ts
@@ -0,0 +1,89 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function perfectionist(): Promise {
+ const perfectionistPlugin = await interopDefault(
+ // @ts-expect-error - no types
+ import('eslint-plugin-perfectionist'),
+ );
+
+ return [
+ perfectionistPlugin.configs['recommended-natural'],
+ {
+ rules: {
+ 'perfectionist/sort-exports': [
+ 'error',
+ {
+ order: 'asc',
+ type: 'natural',
+ },
+ ],
+ 'perfectionist/sort-imports': [
+ 'error',
+ {
+ customGroups: {
+ type: {
+ 'vben-core-type': ['^@vben-core/.+'],
+ 'vben-type': ['^@vben/.+'],
+ 'vue-type': ['^vue$', '^vue-.+', '^@vue/.+'],
+ },
+ value: {
+ vben: ['^@vben/.+'],
+ 'vben-core': ['^@vben-core/.+'],
+ vue: ['^vue$', '^vue-.+', '^@vue/.+'],
+ },
+ },
+ environment: 'node',
+ groups: [
+ ['external-type', 'builtin-type', 'type'],
+ 'vue-type',
+ 'vben-type',
+ 'vben-core-type',
+ ['parent-type', 'sibling-type', 'index-type'],
+ ['internal-type'],
+ 'builtin',
+ 'vue',
+ 'vben',
+ 'vben-core',
+ 'external',
+ 'internal',
+ ['parent', 'sibling', 'index'],
+ 'side-effect',
+ 'side-effect-style',
+ 'style',
+ 'object',
+ 'unknown',
+ ],
+ internalPattern: ['^#/.+'],
+ newlinesBetween: 'always',
+ order: 'asc',
+ type: 'natural',
+ },
+ ],
+ 'perfectionist/sort-modules': 'off',
+ 'perfectionist/sort-named-exports': [
+ 'error',
+ {
+ order: 'asc',
+ type: 'natural',
+ },
+ ],
+ 'perfectionist/sort-objects': [
+ 'off',
+ {
+ customGroups: {
+ items: 'items',
+ list: 'list',
+ children: 'children',
+ },
+ groups: ['unknown', 'items', 'list', 'children'],
+ ignorePattern: ['children'],
+ order: 'asc',
+ type: 'natural',
+ },
+ ],
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/prettier.ts b/web/internal/lint-configs/eslint-config/src/configs/prettier.ts
new file mode 100644
index 0000000..3cd7af4
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/prettier.ts
@@ -0,0 +1,19 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function prettier(): Promise {
+ const [pluginPrettier] = await Promise.all([
+ interopDefault(import('eslint-plugin-prettier')),
+ ] as const);
+ return [
+ {
+ plugins: {
+ prettier: pluginPrettier,
+ },
+ rules: {
+ 'prettier/prettier': 'error',
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/regexp.ts b/web/internal/lint-configs/eslint-config/src/configs/regexp.ts
new file mode 100644
index 0000000..c0f4c9f
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/regexp.ts
@@ -0,0 +1,20 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function regexp(): Promise {
+ const [pluginRegexp] = await Promise.all([
+ interopDefault(import('eslint-plugin-regexp')),
+ ] as const);
+
+ return [
+ {
+ plugins: {
+ regexp: pluginRegexp,
+ },
+ rules: {
+ ...pluginRegexp.configs.recommended.rules,
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/test.ts b/web/internal/lint-configs/eslint-config/src/configs/test.ts
new file mode 100644
index 0000000..ddfde2b
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/test.ts
@@ -0,0 +1,45 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function test(): Promise {
+ const [pluginTest, pluginNoOnlyTests] = await Promise.all([
+ interopDefault(import('eslint-plugin-vitest')),
+ // @ts-expect-error - no types
+ interopDefault(import('eslint-plugin-no-only-tests')),
+ ] as const);
+
+ return [
+ {
+ files: [
+ `**/__tests__/**/*.?([cm])[jt]s?(x)`,
+ `**/*.spec.?([cm])[jt]s?(x)`,
+ `**/*.test.?([cm])[jt]s?(x)`,
+ `**/*.bench.?([cm])[jt]s?(x)`,
+ `**/*.benchmark.?([cm])[jt]s?(x)`,
+ ],
+ plugins: {
+ test: {
+ ...pluginTest,
+ rules: {
+ ...pluginTest.rules,
+ ...pluginNoOnlyTests.rules,
+ },
+ },
+ },
+ rules: {
+ 'no-console': 'off',
+ 'node/prefer-global/process': 'off',
+ 'test/consistent-test-it': [
+ 'error',
+ { fn: 'it', withinDescribe: 'it' },
+ ],
+ 'test/no-identical-title': 'error',
+ 'test/no-import-node-test': 'error',
+ 'test/no-only-tests': 'error',
+ 'test/prefer-hooks-in-order': 'error',
+ 'test/prefer-lowercase-title': 'error',
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/turbo.ts b/web/internal/lint-configs/eslint-config/src/configs/turbo.ts
new file mode 100644
index 0000000..9f6bf75
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/turbo.ts
@@ -0,0 +1,18 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function turbo(): Promise {
+ const [pluginTurbo] = await Promise.all([
+ // @ts-expect-error - no types
+ interopDefault(import('eslint-config-turbo')),
+ ] as const);
+
+ return [
+ {
+ plugins: {
+ turbo: pluginTurbo,
+ },
+ },
+ ];
+}
diff --git a/web/internal/lint-configs/eslint-config/src/configs/typescript.ts b/web/internal/lint-configs/eslint-config/src/configs/typescript.ts
new file mode 100644
index 0000000..cff9aa4
--- /dev/null
+++ b/web/internal/lint-configs/eslint-config/src/configs/typescript.ts
@@ -0,0 +1,72 @@
+import type { Linter } from 'eslint';
+
+import { interopDefault } from '../util';
+
+export async function typescript(): Promise