chore(release): unify to wwjcloud across backend/frontend; routes, DTO/VO paths, docs/links; remove niucloud; naming fixes

This commit is contained in:
wanwu
2025-11-13 19:26:41 +08:00
parent 5c1647df7c
commit 3163f56894
1192 changed files with 89154 additions and 9118 deletions

View File

@@ -1,11 +1,6 @@
# NestJS后端API地址
VITE_APP_BASE_URL=http://localhost:3000
# 开发模式
NODE_ENV=development
# API请求超时毫秒
VITE_APP_TIMEOUT=30000
# 是否开启Mock数据
VITE_APP_MOCK=false
VITE_REQUEST_HEADER_TOKEN_KEY='token'
VITE_REQUEST_HEADER_SITEID_KEY='site-id'

View File

@@ -1,11 +1,6 @@
# NestJS后端API地址生产环境
VITE_APP_BASE_URL=http://localhost:3000
# 生产模式
NODE_ENV=production
# API请求超时毫秒
VITE_APP_TIMEOUT=30000
# 是否开启Mock数据
VITE_APP_MOCK=false
VITE_REQUEST_HEADER_TOKEN_KEY='token'
VITE_REQUEST_HEADER_SITEID_KEY='site-id'

View File

@@ -0,0 +1,160 @@
# Java Admin前端迁移到Vben框架 - 迁移指南
## 项目概述
本项目将基于Java + Vue3 + Element Plus的admin前端系统迁移到Vben框架Vue3 + Ant Design Vue + Vben组件库
## 迁移状态
### ✅ 已完成迁移
1. **登录认证系统**
- 迁移了登录页面 (`login-migrated.vue`)
- 适配了Java admin的登录逻辑和双端登录平台端/站点端)
- 创建了认证API接口 (`auth.ts`)
- 创建了适配的认证状态管理 (`auth-migrated.ts`)
2. **系统管理模块**
- 用户管理页面 (`system/user/index.vue`)
- 用户编辑模态框 (`system/user/components/user-edit-modal.vue`)
- 用户管理API接口 (`user.ts`)
- 创建了系统管理相关的中文翻译
3. **路由配置**
- 创建了迁移后的系统管理路由配置 (`system-migrated.ts`)
### 🚧 待完成迁移
1. **角色管理模块**
- 角色列表页面
- 角色权限配置
- 角色编辑功能
2. **菜单管理模块**
- 菜单列表页面
- 菜单编辑功能
- 菜单权限配置
3. **部门管理模块**
- 部门列表页面
- 部门编辑功能
4. **站点管理模块**
- 站点列表页面
- 站点分组管理
- 站点配置功能
5. **DIY装修模块**
- 页面编辑器
- 组件库管理
- 预览与发布功能
6. **渠道管理模块**
- 微信小程序配置
- 微信公众号配置
- APP配置
- H5配置
- PC配置
## 技术栈对比
| 功能 | Java Admin | Vben |
|------|-----------|------|
| UI框架 | Element Plus | Ant Design Vue |
| 状态管理 | Pinia | Pinia + @vben/stores |
| 路由 | Vue Router | Vue Router + 动态路由 |
| 请求库 | Axios | Axios + @vben/request |
| 国际化 | vue-i18n | @vben/locales |
| 表单 | Element Plus Form | Vben Form + Ant Design Form |
| 表格 | Element Plus Table | Ant Design Table + vxe-table |
## 迁移策略
### 1. 保持API兼容性
- 所有API接口保持与Java后端一致
- 请求参数和响应数据结构不变
- 错误处理机制保持一致
### 2. UI组件替换
- Element Plus组件 → Ant Design Vue组件
- 保持相同的用户体验和交互逻辑
- 适配响应式设计
### 3. 状态管理适配
- 保持业务逻辑不变
- 适配Vben的状态管理架构
- 保持数据流的一致性
### 4. 路由配置
- 保持路由结构不变
- 适配Vben的动态路由系统
- 保持权限控制逻辑
## 文件结构
```
admin-vben/
├── apps/web-antd/src/
│ ├── api/
│ │ ├── core/
│ │ │ ├── auth.ts # 认证API
│ │ │ └── user.ts # 用户管理API
│ │ └── index.ts # API导出
│ ├── views/
│ │ ├── _core/authentication/
│ │ │ └── login-migrated.vue # 迁移后的登录页
│ │ └── system/
│ │ └── user/
│ │ ├── index.vue # 用户管理页面
│ │ └── components/
│ │ └── user-edit-modal.vue # 用户编辑模态框
│ ├── store/
│ │ └── auth-migrated.ts # 适配的认证状态管理
│ ├── locales/langs/zh-CN/
│ │ └── system.json # 系统管理中文翻译
│ └── router/routes/modules/
│ └── system-migrated.ts # 迁移后的系统管理路由
```
## 下一步计划
1. **完成核心模块迁移**
- 角色管理
- 菜单管理
- 部门管理
2. **业务模块迁移**
- 站点管理
- DIY装修
- 渠道管理
3. **测试与优化**
- 功能测试
- 性能优化
- 用户体验优化
4. **部署与上线**
- 构建配置
- 部署脚本
- 监控配置
## 注意事项
1. **保持向后兼容**
- 不要修改后端API接口
- 保持数据格式一致
- 保持业务逻辑一致
2. **用户体验**
- 保持操作习惯一致
- 优化响应速度
- 改善界面美观度
3. **代码质量**
- 遵循Vben的开发规范
- 保持代码整洁
- 添加必要的注释
## 联系方式
如有问题或建议,请联系开发团队。

View File

@@ -1,57 +1,50 @@
import { baseRequestClient, requestClient } from '#/api/request';
import type { AxiosResponse } from 'axios';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
import { requestClient } from '#/api/request';
/**
* 登录
* 登录接口
* @param params 登录参数
* @param loginType 登录类型: admin | site
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data, {
withCredentials: true,
});
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>(
'/auth/refresh',
null,
{
withCredentials: true,
},
);
export function loginApi(
params: { username: string; password: string; captcha_code?: string },
loginType: string,
): Promise<AxiosResponse<any>> {
return requestClient.get(`login/${loginType}`, { params });
}
/**
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', null, {
withCredentials: true,
});
export function logoutApi(): Promise<AxiosResponse<any>> {
return requestClient.put('auth/logout', {}, { showErrorMessage: false });
}
/**
* 获取用户权限
* 获取用户权限菜单
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
export function getAuthMenusApi(params?: Record<string, any>): Promise<AxiosResponse<any>> {
return requestClient.get('auth/authmenu', { params });
}
/**
* 获取站点信息
*/
export function getSiteInfoApi(): Promise<AxiosResponse<any>> {
return requestClient.get('auth/site');
}
/**
* 获取登录配置
*/
export function getLoginConfigApi(): Promise<AxiosResponse<any>> {
return requestClient.get('login/config');
}
/**
* 获取系统版本信息
*/
export function getVersionsApi(): Promise<AxiosResponse<any>> {
return requestClient.get('sys/info');
}

View File

@@ -1,10 +1,57 @@
import type { UserInfo } from '@vben/types';
import type { AxiosResponse } from 'axios';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
* 获取用户列表
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
export function getUserListApi(params: {
page: number;
limit: number;
username?: string;
user_type?: string;
}): Promise<AxiosResponse<any>> {
return requestClient.get('site/user', { params });
}
/**
* 获取用户详情
*/
export function getUserInfoApi(userId: number): Promise<AxiosResponse<any>> {
return requestClient.get(`site/user/${userId}`);
}
/**
* 添加用户
*/
export function addUserApi(params: Record<string, any>): Promise<AxiosResponse<any>> {
return requestClient.post('site/user', params, { showSuccessMessage: true });
}
/**
* 编辑用户
*/
export function editUserApi(params: Record<string, any>): Promise<AxiosResponse<any>> {
return requestClient.put(`site/user/${params.uid}`, params, { showSuccessMessage: true });
}
/**
* 锁定用户
*/
export function lockUserApi(userId: number): Promise<AxiosResponse<any>> {
return requestClient.put(`site/user/lock/${userId}`);
}
/**
* 解锁用户
*/
export function unlockUserApi(userId: number): Promise<AxiosResponse<any>> {
return requestClient.put(`site/user/unlock/${userId}`);
}
/**
* 删除用户
*/
export function deleteUserApi(userId: number): Promise<AxiosResponse<any>> {
return requestClient.delete(`site/user/${userId}`);
}

View File

@@ -1,3 +1,2 @@
export * from './core';
export * from './examples';
export * from './system';
export * from './core/auth';
export * from './core/user';

View File

@@ -1,73 +1,86 @@
{
"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": "徽标类型"
"system": {
"title": "系统管理",
"user": {
"title": "用户管理",
"accountNumber": "账号",
"accountNumberPlaceholder": "请输入账号",
"accountNumberRequired": "请输入账号",
"realName": "真实姓名",
"realNamePlaceholder": "请输入真实姓名",
"realNameRequired": "请输入真实姓名",
"password": "密码",
"passwordPlaceholder": "请输入密码",
"passwordPlaceholderEdit": "留空则不修改密码",
"passwordRequired": "请输入密码",
"role": "角色",
"rolePlaceholder": "请选择角色",
"roleRequired": "请选择角色",
"mobile": "手机号",
"mobilePlaceholder": "请输入手机号",
"email": "邮箱",
"emailPlaceholder": "请输入邮箱",
"status": "状态",
"statusUnlock": "正常",
"statusLock": "锁定",
"headImg": "头像",
"roleName": "角色名称",
"lastLoginTime": "最后登录时间",
"lastLoginIP": "最后登录IP",
"addUser": "新增用户",
"editUser": "编辑用户",
"lock": "锁定",
"unlock": "解锁",
"delete": "删除",
"lockTips": "确定要锁定该用户吗?",
"unlockTips": "确定要解锁该用户吗?",
"deleteTips": "确定要删除该用户吗?",
"administrator": "超级管理员",
"adminDisabled": "系统管理员不可操作"
},
"component": "页面组件",
"hideChildrenInMenu": "隐藏子菜单",
"hideInBreadcrumb": "在面包屑中隐藏",
"hideInMenu": "隐藏菜单",
"hideInTab": "在标签栏中隐藏",
"icon": "图标",
"keepAlive": "缓存标签页",
"linkSrc": "链接地址",
"menuName": "菜单名称",
"menuTitle": "标题",
"name": "菜单",
"operation": "操作",
"parent": "上级菜单",
"path": "路由地址",
"status": "状态",
"title": "菜单管理",
"type": "类型",
"typeButton": "按钮",
"typeCatalog": "目录",
"typeEmbedded": "内嵌",
"typeLink": "外链",
"typeMenu": "菜单"
"role": {
"title": "角色管理"
},
"menu": {
"title": "菜单管理"
},
"dept": {
"title": "部门管理"
}
},
"role": {
"title": "角色管理",
"list": "角色列表",
"name": "角色",
"roleName": "角色名称",
"id": "角色ID",
"status": "状态",
"remark": "备注",
"createTime": "创建时间",
"common": {
"search": "搜索",
"reset": "重置",
"add": "新增",
"edit": "编辑",
"delete": "删除",
"lock": "锁定",
"unlock": "解锁",
"confirm": "确定",
"cancel": "取消",
"save": "保存",
"close": "关闭",
"operation": "操作",
"permissions": "权限",
"setPermissions": "授权"
"total": "共 {total} 条",
"enable": "启用",
"disable": "禁用",
"warning": "提示"
},
"title": "系统管理",
"layout": {
"header": "头部",
"sider": "侧边栏",
"footer": "底部",
"content": "内容"
"authentication": {
"username": "用户名",
"password": "密码",
"usernameTip": "请输入用户名",
"passwordTip": "请输入密码",
"platformLogin": "平台登录",
"siteLogin": "站点登录",
"welcome": "欢迎登录",
"welcomeLogin": "欢迎登录",
"platform": "管理后台",
"platformDesc": "专业的管理系统",
"siteTitle": "管理系统",
"loginSuccess": "登录成功",
"loginSuccessDesc": "欢迎回来",
"selectAccount": "选择账号",
"verifyRequiredTip": "请完成验证"
}
}
}

View File

@@ -0,0 +1,55 @@
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/user',
name: 'SystemUser',
meta: {
icon: 'mdi:account-circle-outline',
title: $t('system.user.title'),
},
component: () => import('#/views/system/user/index.vue'),
},
{
path: '/system/role',
name: 'SystemRole',
meta: {
icon: 'mdi:account-group',
title: $t('system.role.title'),
},
component: () => import('#/views/system/role/index.vue'),
},
{
path: '/system/menu',
name: 'SystemMenu',
meta: {
icon: 'mdi:menu',
title: $t('system.menu.title'),
},
component: () => import('#/views/system/menu/index.vue'),
},
{
path: '/system/dept',
name: 'SystemDept',
meta: {
icon: 'charm:organisation',
title: $t('system.dept.title'),
},
component: () => import('#/views/system/dept/index.vue'),
},
],
},
];
export default routes;

View File

@@ -0,0 +1,204 @@
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 { getAuthMenusApi, getSiteInfoApi, 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);
/**
* 异步处理登录操作适配Java admin逻辑
* @param params 登录表单数据 { username, password, loginType }
* @param onSuccess 成功之后的回调函数
*/
async function authLogin(
params: Recordable<any>,
onSuccess?: () => Promise<void> | void,
) {
let userInfo: null | UserInfo & { siteInfo?: any; userrole?: any[] } = null;
try {
loginLoading.value = true;
// 调用Java admin的登录API
const loginResponse = await loginApi(
{
username: params.username,
password: params.password,
captcha_code: params.captcha_code,
},
params.loginType || 'admin',
);
// Java admin返回的数据结构处理
const { data } = loginResponse;
if (data && data.token) {
// 设置访问令牌
accessStore.setAccessToken(data.token);
// 获取用户信息和权限信息
const [fetchUserInfoResult, authMenus, siteInfo] = await Promise.all([
getUserInfoApi(),
getAuthMenusApi(),
getSiteInfoApi(),
]);
userInfo = {
...fetchUserInfoResult,
siteInfo: siteInfo.data,
userrole: data.userrole || [],
};
// 存储用户信息
userStore.setUserInfo(userInfo);
// 存储权限信息到accessStore
if (authMenus.data) {
accessStore.setAccessCodes(authMenus.data);
}
// 处理登录过期状态
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
// 登录成功后的跳转逻辑
if (onSuccess) {
await onSuccess?.();
} else {
// Java admin的跳转逻辑
if (params.loginType === 'admin' && (!data.userrole || data.userrole.length === 0)) {
// 平台端登录且没有角色,跳转到首页
await router.push('/home/index');
} else {
// 根据登录类型跳转到对应首页
const homePath = params.loginType === 'admin' ? '/admin' : '/site';
await router.push(homePath);
}
}
}
// 登录成功提示
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} catch (error) {
console.error('登录失败:', error);
throw error;
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
/**
* 退出登录适配Java admin逻辑
*/
async function logout(redirect: boolean = true) {
try {
await logoutApi();
} catch {
// 不做任何处理
}
// 重置所有状态
resetAllStores();
accessStore.setLoginExpired(false);
// 清除本地存储的Java admin相关数据
localStorage.removeItem('admin.token');
localStorage.removeItem('admin.userinfo');
localStorage.removeItem('admin.siteInfo');
localStorage.removeItem('site.token');
localStorage.removeItem('site.userinfo');
localStorage.removeItem('site.siteInfo');
localStorage.removeItem('siteId');
localStorage.removeItem('comparisonSiteIdStorage');
localStorage.removeItem('comparisonTokenStorage');
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
/**
* 获取用户信息
*/
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
try {
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
} catch (error) {
console.error('获取用户信息失败:', error);
}
return userInfo;
}
/**
* 获取权限菜单适配Java admin逻辑
*/
async function fetchAuthMenus() {
try {
const response = await getAuthMenusApi();
return response.data;
} catch (error) {
console.error('获取权限菜单失败:', error);
return [];
}
}
/**
* 获取站点信息
*/
async function fetchSiteInfo() {
try {
const response = await getSiteInfoApi();
return response.data;
} catch (error) {
console.error('获取站点信息失败:', error);
return null;
}
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
fetchAuthMenus,
fetchSiteInfo,
loginLoading,
logout,
};
});

View File

@@ -0,0 +1,256 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useRouter } from 'vue-router';
import { useAuthStore } from '#/store';
import { getLoginConfig } from '#/api';
// 定义组件名称
defineOptions({ name: 'LoginMigrated' });
const authStore = useAuthStore();
const router = useRouter();
// 登录类型admin(平台) 或 site(站点)
const loginType = ref<'admin' | 'site'>('admin');
const loading = ref(false);
const loginConfig = ref<any>(null);
// 获取登录配置
const getLoginConfigFn = async () => {
try {
const res = await getLoginConfig();
loginConfig.value = res.data;
} catch (error) {
console.error('获取登录配置失败:', error);
}
};
// 组件挂载时获取配置
getLoginConfigFn();
// 动态背景样式
const backgroundStyle = computed(() => {
if (loginType.value === 'site' && loginConfig.value?.site_login_bg_img) {
return {
backgroundImage: `url(${loginConfig.value.site_login_bg_img})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
};
}
return {};
});
// 登录表单配置
const formSchema = computed((): VbenFormSchema[] => [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.username'),
size: 'large',
allowClear: true,
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
size: 'large',
allowClear: true,
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
]);
// 登录提交处理
async function onSubmit(params: Recordable<any>) {
try {
loading.value = true;
// 检查是否需要验证码
const needCaptcha = loginType.value === 'admin'
? loginConfig.value?.is_captcha
: loginConfig.value?.is_site_captcha;
if (needCaptcha) {
// TODO: 集成验证码组件
console.log('需要验证码验证');
}
// 调用登录API
await authStore.authLogin({
username: params.username,
password: params.password,
loginType: loginType.value,
});
// 登录成功后的跳转逻辑
const redirect = router.currentRoute.value.query.redirect as string;
if (redirect) {
router.push(redirect);
} else {
// 根据登录类型跳转到对应首页
if (loginType.value === 'admin') {
router.push('/admin');
} else {
router.push('/site');
}
}
} catch (error) {
console.error('登录失败:', error);
} finally {
loading.value = false;
}
}
// 切换登录类型
const toggleLoginType = (type: 'admin' | 'site') => {
loginType.value = type;
};
</script>
<template>
<div class="min-h-screen flex items-center justify-center relative" :style="backgroundStyle">
<!-- 登录类型切换 -->
<div class="absolute top-4 right-4">
<a-space>
<a-button
:type="loginType === 'admin' ? 'primary' : 'default'"
@click="toggleLoginType('admin')"
>
{{ $t('authentication.platformLogin') }}
</a-button>
<a-button
:type="loginType === 'site' ? 'primary' : 'default'"
@click="toggleLoginType('site')"
>
{{ $t('authentication.siteLogin') }}
</a-button>
</a-space>
</div>
<!-- 平台端登录 -->
<div v-if="loginType === 'admin'" class="w-full max-w-4xl mx-auto">
<div class="bg-white rounded-lg shadow-lg overflow-hidden">
<div class="flex">
<!-- 左侧图片区域 -->
<div class="w-1/2 hidden md:block">
<img
v-if="loginConfig?.bg"
:src="loginConfig.bg"
alt="Login Background"
class="w-full h-96 object-cover"
/>
<div v-else class="w-full h-96 bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<div class="text-white text-center">
<h2 class="text-2xl font-bold mb-2">{{ $t('authentication.welcome') }}</h2>
<p class="text-blue-100">{{ $t('authentication.platformDesc') }}</p>
</div>
</div>
</div>
<!-- 右侧登录表单 -->
<div class="w-full md:w-1/2 p-8">
<div class="max-w-md mx-auto">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">
{{ loginConfig?.site_name || $t('authentication.siteTitle') }}
</h1>
<p class="text-gray-600">{{ $t('authentication.platform') }}</p>
</div>
<AuthenticationLogin
:form-schema="formSchema"
:loading="loading"
:submit-button-props="{ size: 'large', block: true }"
@submit="onSubmit"
/>
</div>
</div>
</div>
</div>
</div>
<!-- 站点端登录 -->
<div v-else class="w-full max-w-md mx-auto">
<div class="bg-white rounded-lg shadow-lg p-8">
<!-- Logo区域 -->
<div class="text-center mb-8">
<div v-if="loginConfig?.site_login_logo" class="w-32 h-12 mx-auto mb-4">
<img
:src="loginConfig.site_login_logo"
alt="Site Logo"
class="w-full h-full object-contain"
/>
</div>
<h1 class="text-2xl font-bold text-gray-900">
{{ loginConfig?.site_name || $t('authentication.siteTitle') }}
</h1>
<p class="text-gray-600 mt-2">{{ $t('authentication.welcomeLogin') }}</p>
</div>
<AuthenticationLogin
:form-schema="formSchema"
:loading="loading"
:submit-button-props="{ size: 'large', block: true }"
@submit="onSubmit"
/>
</div>
</div>
<!-- 版权信息 -->
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-center text-sm text-gray-500">
<div v-if="loginConfig?.copyright" class="space-x-4">
<a v-if="loginConfig.copyright.copyright_link"
:href="loginConfig.copyright.copyright_link"
target="_blank"
class="hover:text-blue-600"
>
{{ loginConfig.copyright.copyright_desc }}
</a>
<span v-if="loginConfig.copyright.company_name">
{{ loginConfig.copyright.company_name }}
</span>
<a v-if="loginConfig.copyright.icp"
href="https://beian.miit.gov.cn/"
target="_blank"
class="hover:text-blue-600"
>
{{ loginConfig.copyright.icp }}
</a>
<a v-if="loginConfig.copyright.gov_record"
:href="loginConfig.copyright.gov_url"
target="_blank"
class="hover:text-blue-600"
>
{{ loginConfig.copyright.gov_record }}
</a>
</div>
</div>
</div>
</template>
<style scoped>
/* 响应式样式 */
@media (max-width: 768px) {
.md\:w-1\/2 {
width: 100% !important;
}
.md\:block {
display: none !important;
}
}
</style>

View File

@@ -0,0 +1,231 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import { computed, ref, watch } from 'vue';
import { Button, Form, Input, Modal, Select, Switch } from 'ant-design-vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { addUserApi, editUserApi } from '#/api';
// 表单数据
const formState = ref({
uid: undefined as number | undefined,
username: '',
real_name: '',
password: '',
role_ids: [] as number[],
status: 1,
head_img: '',
mobile: '',
email: '',
});
const loading = ref(false);
const isEdit = computed(() => !!formState.value.uid);
// 角色选项(需要从后端获取)
const roleOptions = ref([
{ label: '管理员', value: 1 },
{ label: '运营', value: 2 },
{ label: '客服', value: 3 },
]);
// 表单配置
const formSchema: VbenFormSchema[] = [
{
fieldName: 'username',
label: $t('sys.user.accountNumber'),
component: 'Input',
componentProps: {
placeholder: $t('sys.user.accountNumberPlaceholder'),
disabled: isEdit,
},
rules: [{ required: true, message: $t('sys.user.accountNumberRequired') }],
},
{
fieldName: 'real_name',
label: $t('sys.user.realName'),
component: 'Input',
componentProps: {
placeholder: $t('sys.user.realNamePlaceholder'),
},
rules: [{ required: true, message: $t('sys.user.realNameRequired') }],
},
{
fieldName: 'password',
label: $t('sys.user.password'),
component: 'InputPassword',
componentProps: {
placeholder: isEdit ? $t('sys.user.passwordPlaceholderEdit') : $t('sys.user.passwordPlaceholder'),
},
rules: isEdit.value ? [] : [{ required: true, message: $t('sys.user.passwordRequired') }],
},
{
fieldName: 'role_ids',
label: $t('sys.user.role'),
component: 'Select',
componentProps: {
mode: 'multiple',
placeholder: $t('sys.user.rolePlaceholder'),
options: roleOptions.value,
},
rules: [{ required: true, message: $t('sys.user.roleRequired') }],
},
{
fieldName: 'mobile',
label: $t('sys.user.mobile'),
component: 'Input',
componentProps: {
placeholder: $t('sys.user.mobilePlaceholder'),
},
},
{
fieldName: 'email',
label: $t('sys.user.email'),
component: 'Input',
componentProps: {
placeholder: $t('sys.user.emailPlaceholder'),
},
},
{
fieldName: 'status',
label: $t('sys.user.status'),
component: 'Switch',
componentProps: {
checkedChildren: $t('common.enable'),
unCheckedChildren: $t('common.disable'),
},
},
];
// 模态框API
const [Modal, modalApi] = useVbenModal({
draggable: true,
title: computed(() => (isEdit.value ? $t('sys.user.editUser') : $t('sys.user.addUser'))),
onConfirm: handleSubmit,
onOpenChange: handleOpenChange,
});
// 处理模态框打开
function handleOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData();
if (data) {
// 编辑模式,填充表单数据
Object.keys(formState.value).forEach((key) => {
if (data[key] !== undefined) {
(formState.value as any)[key] = data[key];
}
});
} else {
// 新增模式,重置表单
resetForm();
}
}
}
// 重置表单
function resetForm() {
formState.value = {
uid: undefined,
username: '',
real_name: '',
password: '',
role_ids: [],
status: 1,
head_img: '',
mobile: '',
email: '',
};
}
// 提交表单
async function handleSubmit() {
try {
loading.value = true;
const params = {
...formState.value,
role_ids: formState.value.role_ids.join(','),
};
if (isEdit.value) {
// 编辑用户
await editUserApi(params);
} else {
// 新增用户
await addUserApi(params);
}
modalApi.close();
// 通知父组件刷新数据
const callback = modalApi.getData()?.callback;
if (callback) {
callback();
}
} catch (error) {
console.error('保存用户失败:', error);
throw error;
} finally {
loading.value = false;
}
}
// 暴露给父组件的方法
defineExpose({
open: modalApi.open,
});
</script>
<template>
<Modal>
<VbenForm
:schema="formSchema"
:model="formState"
:loading="loading"
label-col="{ span: 6 }"
wrapper-col="{ span: 18 }"
>
<template #default="{ form }">
<Form.Item
v-for="field in formSchema"
:key="field.fieldName"
:label="field.label"
:name="field.fieldName"
:rules="field.rules"
>
<template v-if="field.component === 'Input'">
<Input
v-model:value="formState[field.fieldName as keyof typeof formState]"
v-bind="field.componentProps"
/>
</template>
<template v-else-if="field.component === 'InputPassword'">
<Input.Password
v-model:value="formState[field.fieldName as keyof typeof formState]"
v-bind="field.componentProps"
/>
</template>
<template v-else-if="field.component === 'Select'">
<Select
v-model:value="formState[field.fieldName as keyof typeof formState]"
v-bind="field.componentProps"
/>
</template>
<template v-else-if="field.component === 'Switch'">
<Switch
v-model:checked="formState[field.fieldName as keyof typeof formState]"
v-bind="field.componentProps"
/>
</template>
</Form.Item>
</template>
</VbenForm>
</Modal>
</template>

View File

@@ -0,0 +1,320 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import { computed, h, onMounted, ref } from 'vue';
import { Avatar, Button, Card, Modal, Space, Table, Tag } from 'ant-design-vue';
import { DeleteOutlined, EditOutlined, LockOutlined, UnlockOutlined, UserAddOutlined } from '@ant-design/icons-vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { addUserApi, deleteUserApi, editUserApi, getUserListApi, lockUserApi, unlockUserApi } from '#/api';
import UserEditModal from './components/user-edit-modal.vue';
// 用户数据
const loading = ref(false);
const dataSource = ref<any[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const searchValue = ref('');
// 搜索表单配置
const searchFormSchema: VbenFormSchema[] = [
{
fieldName: 'search',
label: $t('sys.user.accountNumber'),
component: 'Input',
componentProps: {
placeholder: $t('sys.user.accountNumberPlaceholder'),
allowClear: true,
},
},
];
// 表格列配置
const columns = computed(() => [
{
title: $t('sys.user.headImg'),
dataIndex: 'head_img',
width: 80,
align: 'center',
customRender: ({ record }: any) => {
return h(Avatar, {
src: record.head_img || '/src/assets/images/member_head.png',
size: 40,
});
},
},
{
title: $t('sys.user.accountNumber'),
dataIndex: 'username',
width: 120,
},
{
title: $t('sys.user.realName'),
dataIndex: 'real_name',
width: 120,
customRender: ({ text }: any) => text || '--',
},
{
title: $t('sys.user.roleName'),
dataIndex: 'role_array',
width: 150,
customRender: ({ record }: any) => {
if (record.is_admin) {
return h(Tag, { color: 'red' }, () => $t('sys.user.administrator'));
}
if (record.role_array && record.role_array.length > 0) {
return record.role_array.join(' | ');
}
return '--';
},
},
{
title: $t('sys.user.status'),
dataIndex: 'status',
width: 100,
align: 'center',
customRender: ({ record }: any) => {
return record.status === 1
? h(Tag, { color: 'success' }, () => $t('sys.user.statusUnlock'))
: h(Tag, { color: 'error' }, () => $t('sys.user.statusLock'));
},
},
{
title: $t('sys.user.lastLoginTime'),
dataIndex: 'last_time',
width: 180,
align: 'center',
},
{
title: $t('sys.user.lastLoginIP'),
dataIndex: 'last_ip',
width: 150,
align: 'center',
},
{
title: $t('common.operation'),
dataIndex: 'operation',
width: 200,
align: 'right',
fixed: 'right',
customRender: ({ record }: any) => {
if (record.is_admin) {
return h('span', { style: { color: '#999' } }, $t('sys.user.adminDisabled'));
}
return h(Space, null, () => [
h(Button, {
type: 'link',
size: 'small',
icon: h(EditOutlined),
onClick: () => handleEdit(record),
}, () => $t('common.edit')),
record.status === 1
? h(Button, {
type: 'link',
size: 'small',
icon: h(LockOutlined),
danger: true,
onClick: () => handleLock(record.uid),
}, () => $t('common.lock'))
: h(Button, {
type: 'link',
size: 'small',
icon: h(UnlockOutlined),
onClick: () => handleUnlock(record.uid),
}, () => $t('common.unlock')),
h(Button, {
type: 'link',
size: 'small',
icon: h(DeleteOutlined),
danger: true,
onClick: () => handleDelete(record.uid),
}, () => $t('common.delete')),
]);
},
},
]);
// 用户编辑模态框
const [UserEditModalApi, userEditModalRef] = useVbenModal({
connectedComponent: UserEditModal,
});
// 加载用户列表
const loadUserList = async (page = 1) => {
try {
loading.value = true;
const params = {
page,
limit: pageSize.value,
username: searchValue.value,
};
const response = await getUserListApi(params);
dataSource.value = response.data.data;
total.value = response.data.total;
currentPage.value = page;
} catch (error) {
console.error('加载用户列表失败:', error);
} finally {
loading.value = false;
}
};
// 搜索处理
const handleSearch = (values: any) => {
searchValue.value = values?.search || '';
loadUserList(1);
};
// 重置搜索
const handleReset = () => {
searchValue.value = '';
loadUserList(1);
};
// 添加用户
const handleAdd = () => {
UserEditModalApi.open({
title: $t('sys.user.addUser'),
});
};
// 编辑用户
const handleEdit = (record: any) => {
UserEditModalApi.open({
title: $t('sys.user.editUser'),
data: record,
});
};
// 锁定用户
const handleLock = async (userId: number) => {
Modal.confirm({
title: $t('common.warning'),
content: $t('sys.user.lockTips'),
onOk: async () => {
try {
await lockUserApi(userId);
loadUserList(currentPage.value);
} catch (error) {
console.error('锁定用户失败:', error);
}
},
});
};
// 解锁用户
const handleUnlock = async (userId: number) => {
Modal.confirm({
title: $t('common.warning'),
content: $t('sys.user.unlockTips'),
onOk: async () => {
try {
await unlockUserApi(userId);
loadUserList(currentPage.value);
} catch (error) {
console.error('解锁用户失败:', error);
}
},
});
};
// 删除用户
const handleDelete = async (userId: number) => {
Modal.confirm({
title: $t('common.warning'),
content: $t('sys.user.deleteTips'),
onOk: async () => {
try {
await deleteUserApi(userId);
loadUserList(currentPage.value);
} catch (error) {
console.error('删除用户失败:', error);
}
},
});
};
// 表格分页变化
const handleTableChange = (pagination: any) => {
currentPage.value = pagination.current;
pageSize.value = pagination.pageSize;
loadUserList(pagination.current);
};
// 模态框操作完成
const handleModalComplete = () => {
loadUserList(currentPage.value);
};
onMounted(() => {
loadUserList();
});
</script>
<template>
<div class="container mx-auto p-4">
<Card>
<template #title>
<div class="flex items-center justify-between">
<span>{{ $t('sys.user.title') }}</span>
<Button type="primary" @click="handleAdd">
<template #icon>
<UserAddOutlined />
</template>
{{ $t('sys.user.addUser') }}
</Button>
</div>
</template>
<!-- 搜索表单 -->
<div class="mb-4">
<VbenForm
:schema="searchFormSchema"
:show-default-actions="false"
@submit="handleSearch"
@reset="handleReset"
>
<template #actions="{ reset, submit }">
<Space>
<Button type="primary" @click="submit">
{{ $t('common.search') }}
</Button>
<Button @click="reset">
{{ $t('common.reset') }}
</Button>
</Space>
</template>
</VbenForm>
</div>
<!-- 用户表格 -->
<Table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => $t('common.total', { total }),
}"
row-key="uid"
@change="handleTableChange"
/>
</Card>
<!-- 用户编辑模态框 -->
<user-edit-modal ref="userEditModalRef" @complete="handleModalComplete" />
</div>
</template>

View File

@@ -9,7 +9,7 @@
<el-button type="primary" class="w-[100px]" @click="addEvent">
{{ t('addMenu') }}
</el-button>
<el-button class="w-[100px]" @click="refreshMenu">
<el-button class="w-[100px]" :loading="refreshLoading" @click="refreshMenu">
{{ t('initializeMenu') }}
</el-button>
</div>
@@ -82,6 +82,7 @@ const getMenuList = () => {
}
getMenuList()
// 重置菜单
const refreshLoading = ref(false)
const refreshMenu = () => {
ElMessageBox.confirm(h('div', null, [
h('p', null, t('initializeMenuTipsOne')),
@@ -93,9 +94,12 @@ const refreshMenu = () => {
// type: 'warning'
}
).then(() => {
refreshLoading.value = true
menuRefresh({}).then(res => {
location.reload()
refreshLoading.value = false
}).catch(() => {
refreshLoading.value = false
})
}).catch(() => {
})