添加对ele的支持,目前vben支持还不是特别完善

This commit is contained in:
xie7654
2025-07-03 10:17:33 +08:00
parent 78b9f9e832
commit 9a0d7846cf
61 changed files with 5424 additions and 29 deletions

View File

@@ -4,10 +4,10 @@ VITE_PORT=5777
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=/api
VITE_GLOB_API_URL=http://127.0.0.1:8000/api
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=true
VITE_NITRO_MOCK=false
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false

View File

@@ -1,10 +1,10 @@
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
VITE_GLOB_API_URL=http://127.0.0.1:8000/api
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=none
VITE_COMPRESS=gzip
# 是否开启 PWA
VITE_PWA=false

View File

@@ -7,6 +7,7 @@ import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { ElButton, ElImage } from 'element-plus';
import { useVbenForm } from './form';
import type {Recordable} from "@vben-core/typings";
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
@@ -66,5 +67,11 @@ setupVbenVxeTable({
});
export { useVbenVxeGrid };
export type OnActionClickParams<T = Recordable<any>> = {
code: string;
row: T;
};
export type OnActionClickFn<T = Recordable<any>> = (
params: OnActionClickParams<T>,
) => void;
export type * from '@vben/plugins/vxe-table';

View File

@@ -22,14 +22,14 @@ export namespace AuthApi {
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
return requestClient.post<AuthApi.LoginResult>('/system/login/', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/system/refresh/', {
withCredentials: true,
});
}
@@ -38,7 +38,7 @@ export async function refreshTokenApi() {
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
return baseRequestClient.post('/system/logout/', {
withCredentials: true,
});
}
@@ -47,5 +47,5 @@ export async function logoutApi() {
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
return requestClient.get<string[]>('/system/codes/');
}

View File

@@ -6,5 +6,7 @@ import { requestClient } from '#/api/request';
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
return requestClient.get<RouteRecordStringComponent[]>(
'/system/menu/user_menu',
);
}

View File

@@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
return requestClient.get<UserInfo>('/system/info/');
}

View File

@@ -0,0 +1,56 @@
import type { Recordable } from '@vben/types';
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(params?: Recordable<any>) {
return requestClient.get<Array<SystemDeptApi.SystemDept>>('/system/dept/', {
params,
});
}
/**
* 创建部门
* @param data 部门数据
*/
async function createDept(
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
) {
return requestClient.post('/system/dept/', data);
}
/**
* 更新部门
*
* @param id 部门 ID
* @param data 部门数据
*/
async function updateDept(
id: string,
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
) {
return requestClient.put(`/system/dept/${id}/`, data);
}
/**
* 删除部门
* @param id 部门 ID
*/
async function deleteDept(id: string) {
return requestClient.delete(`/system/dept/${id}/`);
}
export { createDept, deleteDept, getDeptList, updateDept };

View File

@@ -0,0 +1,74 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemDictDataApi {
export interface SystemDictData {
[key: string]: any;
id: string;
name: string;
}
}
/**
* 获取字典数据列表数据
*/
async function getDictDataList(params: Recordable<any>) {
return requestClient.get<Array<SystemDictDataApi.SystemDictData>>(
'/system/dict_data/',
{
params,
},
);
}
/**
* 创建字典数据
* @param data 字典数据数据
*/
async function createDictData(
data: Omit<SystemDictDataApi.SystemDictData, 'id'>,
) {
return requestClient.post('/system/dict_data/', data);
}
/**
* 更新字典数据
*
* @param id 字典数据 ID
* @param data 字典数据数据
*/
async function updateDictData(
id: string,
data: Omit<SystemDictDataApi.SystemDictData, 'id'>,
) {
return requestClient.put(`/system/dict_data/${id}/`, data);
}
/**
* 更新字典数据
*
* @param id 字典数据 ID
* @param data 字典数据数据
*/
async function patchDictData(
id: string,
data: Omit<SystemDictDataApi.SystemDictData, 'id'>,
) {
return requestClient.patch(`/system/dict_data/${id}/`, data);
}
/**
* 删除字典数据
* @param id 字典数据 ID
*/
async function deleteDictData(id: string) {
return requestClient.delete(`/system/dict_data/${id}/`);
}
export {
createDictData,
deleteDictData,
getDictDataList,
patchDictData,
updateDictData,
};

View File

@@ -0,0 +1,75 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemDictTypeApi {
export interface SystemDictType {
[key: string]: any;
id: string;
name: string;
type: string;
}
}
/**
* 获取字典类型列表数据
*/
async function getDictTypeList(params: Recordable<any>) {
return requestClient.get<Array<SystemDictTypeApi.SystemDictType>>(
'/system/dict_type/',
{
params,
},
);
}
/**
* 创建字典类型
* @param data 字典类型数据
*/
async function createDictType(
data: Omit<SystemDictTypeApi.SystemDictType, 'id'>,
) {
return requestClient.post('/system/dict_type/', data);
}
/**
* 更新字典类型
*
* @param id 字典类型 ID
* @param data 字典类型数据
*/
async function updateDictType(
id: string,
data: Omit<SystemDictTypeApi.SystemDictType, 'id'>,
) {
return requestClient.put(`/system/dict_type/${id}/`, data);
}
/**
* 更新字典类型
*
* @param id 字典类型 ID
* @param data 字典类型数据
*/
async function patchDictType(
id: string,
data: Omit<SystemDictTypeApi.SystemDictType, 'id'>,
) {
return requestClient.patch(`/system/dict_type/${id}/`, data);
}
/**
* 删除字典类型
* @param id 字典类型 ID
*/
async function deleteDictType(id: string) {
return requestClient.delete(`/system/dict_type/${id}/`);
}
export {
createDictType,
deleteDictType,
getDictTypeList,
patchDictType,
updateDictType,
};

View File

@@ -0,0 +1,3 @@
export * from './dept';
export * from './menu';
export * from './role';

View File

@@ -0,0 +1,168 @@
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;
/** 后端权限标识 */
auth_code: 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<any>;
/** 菜单标题 */
title?: string;
};
/** 菜单名称 */
name: string;
/** 路由路径 */
path: string;
/** 父级ID */
pid: string;
/** 重定向 */
redirect?: string;
/** 菜单类型 */
type: (typeof MenuTypes)[number];
}
}
/**
* 获取菜单数据列表
*/
async function getMenuList() {
return requestClient.get<Array<SystemMenuApi.SystemMenu>>('/system/menu/');
}
async function isMenuNameExists(
name: string,
id?: SystemMenuApi.SystemMenu['id'],
) {
const url = id ? `/system/menu/${id}/` : `/system/menu/`;
return requestClient.get<boolean>(url, {
params: { id, name },
});
}
async function isMenuSearchExists(
name: string,
id?: SystemMenuApi.SystemMenu['id'],
pid?: SystemMenuApi.SystemMenu['pid'],
) {
return requestClient.get<boolean>('/system/menu/name-search', {
params: { name, id, pid },
});
}
async function isMenuPathExists(
path: string,
id?: SystemMenuApi.SystemMenu['id'],
) {
return requestClient.get<boolean>('/system/menu/path-exists', {
params: { id, path },
});
}
/**
* 创建菜单
* @param data 菜单数据
*/
async function createMenu(
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
) {
return requestClient.post('/system/menu/', data);
}
/**
* 更新菜单
*
* @param id 菜单 ID
* @param data 菜单数据
*/
async function updateMenu(
id: string,
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
) {
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,
};

View File

@@ -0,0 +1,70 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemRoleApi {
export interface SystemRole {
[key: string]: any;
id: string;
name: string;
permissions: [];
profile: {
create_time: string;
permissions: [];
remark?: string;
status: 0 | 1;
};
}
}
/**
* 获取角色列表数据
*/
async function getRoleList(params: Recordable<any>) {
return requestClient.get<Array<SystemRoleApi.SystemRole>>('/system/role/', {
params,
});
}
/**
* 创建角色
* @param data 角色数据
*/
async function createRole(data: Omit<SystemRoleApi.SystemRole, 'id'>) {
return requestClient.post('/system/role/', data);
}
/**
* 更新角色
*
* @param id 角色 ID
* @param data 角色数据
*/
async function updateRole(
id: string,
data: Omit<SystemRoleApi.SystemRole, 'id'>,
) {
return requestClient.put(`/system/role/${id}/`, data);
}
/**
* 更新角色
*
* @param id 角色 ID
* @param data 角色数据
*/
async function patchRole(
id: string,
data: Omit<SystemRoleApi.SystemRole, 'id'>,
) {
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 };

View File

@@ -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<any>) {
return requestClient.get<Array<SystemTenantPackageApi.SystemTenantPackage>>(
'/system/tenant_package/',
{
params,
},
);
}
/**
* 创建租户
* @param data 租户数据
*/
async function createTenantPackage(
data: Omit<SystemTenantPackageApi.SystemTenantPackage, 'id'>,
) {
return requestClient.post('/system/tenant_package/', data);
}
/**
* 更新租户
*
* @param id 租户 ID
* @param data 租户数据
*/
async function updateTenantPackage(
id: string,
data: Omit<SystemTenantPackageApi.SystemTenantPackage, 'id'>,
) {
return requestClient.put(`/system/tenant_package/${id}/`, data);
}
/**
* 更新租户
*
* @param id 租户 ID
* @param data 租户数据
*/
async function patchTenantPackage(
id: string,
data: Omit<SystemTenantPackageApi.SystemTenantPackage, 'id'>,
) {
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,
};

View File

@@ -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<any>) {
return requestClient.get<Array<SystemTenantsApi.SystemTenants>>(
'/system/tenants/',
{
params,
});
}
/**
* 创建租户
* @param data 租户数据
*/
async function createTenants(data: Omit<SystemTenantsApi.SystemTenants, 'id'>) {
return requestClient.post('/system/tenants/', data);
}
/**
* 更新租户
*
* @param id 租户 ID
* @param data 租户数据
*/
async function updateTenants(
id: string,
data: Omit<SystemTenantsApi.SystemTenants, 'id'>,
) {
return requestClient.put(`/system/tenants/${id}/`, data);
}
/**
* 更新租户
*
* @param id 租户 ID
* @param data 租户数据
*/
async function patchTenants(
id: string,
data: Omit<SystemTenantsApi.SystemTenants, 'id'>,
) {
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,
};

View File

@@ -6,11 +6,13 @@ import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/ele';
import ElementPlus from 'element-plus'
import { useTitle } from '@vueuse/core';
import { ElLoading } from 'element-plus';
import { $t, setupI18n } from '#/locales';
import { registerPermissionDirective } from '#/utils/permission';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
@@ -51,7 +53,8 @@ async function bootstrap(namespace: string) {
// 安装权限指令
registerAccessDirective(app);
// 注册自定义v-permission指令
registerPermissionDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
@@ -62,6 +65,7 @@ async function bootstrap(namespace: string) {
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
app.use(ElementPlus);
// 动态更新标题
watchEffect(() => {

View File

@@ -0,0 +1,113 @@
{
"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",
"sort": "sort",
"auth_code": "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 doesnt 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"
},
"post": {
"name": "Post",
"title": "Post Management"
},
"user": {
"name": "User",
"title": "User Management"
},
"login_log": {
"name": "login log",
"title": "login log"
},
"status": "Status",
"remark": "Remarks",
"creator": "creator",
"modifier": "modifier",
"createTime": "Created At",
"operation": "Actions",
"updateTime": "Updated At"
}

View File

@@ -0,0 +1,114 @@
{
"title": "系统管理",
"dept": {
"list": "部门列表",
"createTime": "创建时间",
"deptName": "部门名称",
"name": "部门",
"operation": "操作",
"parentDept": "上级部门",
"remark": "备注",
"status": "状态",
"title": "部门管理"
},
"menu": {
"list": "菜单列表",
"activeIcon": "激活图标",
"sort": "排序",
"activePath": "激活路径",
"activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时需要指定激活路径",
"activePathMustExist": "该路径未能找到有效的菜单",
"advancedSettings": "其它设置",
"affixTab": "固定在标签",
"auth_code": "权限标识",
"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": "字典键值"
},
"post": {
"name": "岗位",
"title": "岗位管理"
},
"user": {
"name": "用户",
"title": "用户管理"
},
"login_log": {
"name": "登录日志",
"title": "登录日志"
},
"status": "状态",
"remark": "备注",
"creator": "创建人",
"modifier": "修改人",
"createTime": "创建时间",
"operation": "操作",
"updateTime": "更新时间"
}

View File

@@ -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<T, any>,
UpdateData = Partial<CreateData>,
> {
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<T> = {}) {
return requestClient.get(`${this.baseUrl}export/`, {
params,
responseType: 'blob', // 二进制流
});
}
/**
* 获取列表数据
*/
async list(params: Recordable<T> = {}) {
return requestClient.get<Array<T>>(this.baseUrl, { params });
}
/**
* 部分更新记录
*/
async patch(id: number, data: Partial<UpdateData>) {
return requestClient.patch(`${this.baseUrl}${id}/`, data);
}
/**
* 获取单条记录
*/
async retrieve(id: number) {
return requestClient.get<T>(`${this.baseUrl}${id}/`);
}
/**
* 全量更新记录
*/
async update(id: number, data: UpdateData) {
return requestClient.put(`${this.baseUrl}${id}/`, data);
}
}
//
// // 字典类型专用Model
// export class SystemDictTypeModel extends BaseModel<SystemDictTypeApi.SystemDictType> {
// constructor() {
// super('/system/dict_type/');
// }
// }
//
// // AmazonListingModel示例
// export class AmazonListingModel extends BaseModel<AmazonListing> {
// 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: '更新后的字典' });

View File

@@ -0,0 +1 @@
export * from './base';

View File

@@ -0,0 +1,23 @@
import { BaseModel } from '#/models/base';
export namespace SystemLoginLogApi {
export interface SystemLoginLog {
id: number;
remark: string;
creator: string;
modifier: string;
update_time: string;
create_time: string;
is_deleted: boolean;
username: string;
result: number;
user_ip: string;
user_agent: string;
}
}
export class SystemLoginLogModel extends BaseModel<SystemLoginLogApi.SystemLoginLog> {
constructor() {
super('/system/login_log/');
}
}

View File

@@ -0,0 +1,23 @@
import { BaseModel } from '#/models/base';
export namespace SystemPostApi {
export interface SystemPost {
id: number;
remark: string;
creator: string;
modifier: string;
update_time: string;
create_time: string;
is_deleted: boolean;
code: string;
name: string;
sort: number;
status: number;
}
}
export class SystemPostModel extends BaseModel<SystemPostApi.SystemPost> {
constructor() {
super('/system/post/');
}
}

View File

@@ -0,0 +1,40 @@
import { BaseModel } from '#/models/base';
export namespace SystemUserApi {
export interface SystemUser {
id: number;
password: string;
last_login: string;
is_superuser: boolean;
username: string;
first_name: string;
last_name: string;
email: string;
is_staff: boolean;
is_active: boolean;
date_joined: string;
remark: string;
creator: string;
modifier: string;
update_time: string;
create_time: string;
is_deleted: boolean;
mobile: string;
nickname: string;
gender: number;
language: string;
city: string;
province: string;
country: string;
avatar_url: string;
status: number;
login_date: string;
login_ip: any;
}
}
export class SystemUserModel extends BaseModel<SystemUserApi.SystemUser> {
constructor() {
super('/system/user/');
}
}

View File

@@ -9,5 +9,6 @@ export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
accessMode: 'backend', // 或 'frontend'
},
});

View File

@@ -0,0 +1,102 @@
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-group',
title: $t('system.user.title'),
},
component: () => import('#/views/system/user/list.vue'),
},
{
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/post',
name: 'SystemPost',
meta: {
icon: 'charm:organisation',
title: $t('system.post.title'),
},
component: () => import('#/views/system/post/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;

View File

@@ -13,9 +13,13 @@ import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
import { usePermissionStore } from './permission';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const permissionStore = usePermissionStore();
const router = useRouter();
const loginLoading = ref(false);
@@ -63,7 +67,7 @@ export const useAuthStore = defineStore('auth', () => {
if (userInfo?.realName) {
ElNotification({
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.username}`,
title: $t('authentication.loginSuccess'),
type: 'success',
});
@@ -102,6 +106,11 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
// 设置权限
if (userInfo && Array.isArray(userInfo.permissions)) {
permissionStore.setPermissions(userInfo.permissions);
}
return userInfo;
}

View File

@@ -0,0 +1,30 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
export const usePermissionStore = defineStore('permission', () => {
// 权限码列表
const permissions = ref<string[]>([]);
// 设置权限码
function setPermissions(perms: string[]) {
permissions.value = perms || [];
}
// 判断是否有某个权限
function hasPermission(code: string): boolean {
return permissions.value.includes(code);
}
// 重置
function $reset() {
permissions.value = [];
}
return {
permissions,
setPermissions,
hasPermission,
$reset,
};
})

View File

@@ -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);
}

View File

@@ -0,0 +1,5 @@
export enum DICT_TYPE {
USER_TYPE = 'user_type',
// TEMU_ORDER_STATUS = 'temu_order_status',
}

View File

@@ -0,0 +1,48 @@
import type { App, DirectiveBinding } from 'vue';
import { usePermissionStore } from '#/store/permission';
/**
* 权限按钮option生成工具
* @param code 权限码
* @param option 按钮option或字符串
* @returns 有权限返回option无权限返回false
*/
export function op(code: string, option: any) {
const permissionStore = usePermissionStore();
return permissionStore.hasPermission(code) ? option : false;
}
/**
* 全局权限判断函数适用于模板v-if
*/
export function hasPermission(code: string): boolean {
const permissionStore = usePermissionStore();
return permissionStore.hasPermission(code);
}
/**
* v-permission 自定义指令
* 用法v-permission="'system:user:create'"
* 或 v-permission="['system:user:create', 'system:user:update']"
*/
const permissionDirective = {
mounted(el: HTMLElement, binding: DirectiveBinding<string | string[]>) {
const codes = Array.isArray(binding.value)
? binding.value
: [binding.value];
const permissionStore = usePermissionStore();
const has = codes.some((code) => permissionStore.hasPermission(code));
if (!has) {
el.parentNode && el.remove();
}
},
};
/**
* 注册全局权限指令
* @param app Vue App 实例
*/
export function registerPermissionDirective(app: App) {
app.directive('permission', permissionDirective);
}

View File

@@ -2,9 +2,9 @@
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import { computed, markRaw } from 'vue';
import { computed } from 'vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { AuthenticationLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store';
@@ -24,7 +24,7 @@ const MOCK_USER_OPTIONS: BasicOption[] = [
},
{
label: 'User',
value: 'jack',
value: 'chenze',
},
];
@@ -57,7 +57,7 @@ const formSchema = computed((): VbenFormSchema[] => {
);
if (findUser) {
form.setValues({
password: '123456',
password: 'admin123',
username: findUser.value,
});
}
@@ -78,13 +78,13 @@ const formSchema = computed((): VbenFormSchema[] => {
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: markRaw(SliderCaptcha),
fieldName: 'captcha',
rules: z.boolean().refine((value) => value, {
message: $t('authentication.verifyRequiredTip'),
}),
},
// {
// component: markRaw(SliderCaptcha),
// fieldName: 'captcha',
// rules: z.boolean().refine((value) => value, {
// message: $t('authentication.verifyRequiredTip'),
// }),
// },
];
});
</script>

View File

@@ -0,0 +1,160 @@
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';
import { op } from '#/utils/permission';
/**
* 获取编辑表单的字段配置。如果没有使用多语言可以直接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<SystemDeptApi.SystemDept>,
): VxeTableGridOptions<SystemDeptApi.SystemDept>['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: [
op('system:dept:create', { code: 'append', text: '新增下级' }),
op('system:dept:edit', 'edit'),
op('system:dept:delete', {
code: 'delete',
disabled: (row: SystemDeptApi.SystemDept) => {
return !!(row.children && row.children.length > 0);
},
}),
].filter(Boolean),
},
field: 'operation',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
title: $t('system.dept.operation'),
width: 200,
},
];
}

View File

@@ -0,0 +1,147 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemDeptApi } from '#/api/system/dept';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDept, getDeptList } from '#/api/system/dept';
import { $t } from '#/locales';
import { useColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑部门
* @param row
*/
function onEdit(row: SystemDeptApi.SystemDept) {
formModalApi.setData(row).open();
}
/**
* 添加下级部门
* @param row
*/
function onAppend(row: SystemDeptApi.SystemDept) {
formModalApi.setData({ pid: row.id }).open();
}
/**
* 创建新部门
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除部门
* @param row
*/
function onDelete(row: SystemDeptApi.SystemDept) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deleteDept(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
refreshGrid();
})
.catch(() => {
hideLoading();
});
}
/**
* 表格操作按钮的回调函数
*/
function onActionClick({
code,
row,
}: OnActionClickParams<SystemDeptApi.SystemDept>) {
switch (code) {
case 'append': {
onAppend(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async (_params) => {
return await getDeptList();
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="部门列表">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-permission="'system:dept:create'"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.dept.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
import type { SystemDeptApi } from '#/api/system/dept';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDept, updateDept } from '#/api/system/dept';
import { $t } from '#/locales';
import { useSchema } from '../data';
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
const emit = defineEmits(['success']);
const formData = ref<SystemDeptApi.SystemDept>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.dept.name')])
: $t('ui.actionTitle.create', [$t('system.dept.name')]);
});
const breakpoints = useBreakpoints(breakpointsTailwind);
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateDept(formData.value.id, data)
: createDept(data));
modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemDeptApi.SystemDept>();
if (data) {
if (data.pid === 0) {
data.pid = undefined;
}
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,211 @@
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';
import {format_datetime} from "#/utils/date";
/**
* 获取编辑表单的字段配置。如果没有使用多语言可以直接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: 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<SystemDictDataApi.SystemDictData>,
): VxeTableGridOptions<SystemDictDataApi.SystemDictData>['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',
},
field: 'status',
title: '状态',
width: 100,
},
{
field: 'remark',
title: '备注',
},
{
field: 'create_time',
title: '创建时间',
width: 180,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
{
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,
},
];
}

View File

@@ -0,0 +1,143 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemDictDataApi } from '#/api/system/dict_data';
import { useRoute } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDictData, getDictDataList } from '#/api/system/dict_data';
import { $t } from '#/locales';
import { useColumns } from './data';
import Form from './modules/form.vue';
const route = useRoute();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑套餐
* @param row
*/
function onEdit(row: SystemDictDataApi.SystemDictData) {
if (row.menu_ids) {
row.menu_ids = row.menu_ids.split(',').map(Number);
}
formModalApi.setData(row).open();
}
/**
* 创建新套餐
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除套餐
* @param row
*/
function onDelete(row: SystemDictDataApi.SystemDictData) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deleteDictData(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
refreshGrid();
})
.catch(() => {
hideLoading();
});
}
/**
* 表格操作按钮的回调函数
*/
function onActionClick({
code,
row,
}: OnActionClickParams<SystemDictDataApi.SystemDictData>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const { dict_type } = route.query;
return await getDictDataList({
page: page.currentPage,
pageSize: page.pageSize,
dict_type,
...formValues,
});
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="字典数据">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.dict_data.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SystemDictDataApi } from '#/api/system/dict_data';
import { computed, ref } from 'vue';
import { useRoute } from "vue-router";
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDictData, updateDictData } from '#/api/system/dict_data';
import { $t } from '#/locales';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemDictDataApi.SystemDictData>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.dict_data.name')])
: $t('ui.actionTitle.create', [$t('system.dict_data.name')]);
});
const route = useRoute();
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
const { dict_type } = route.query;
data.dict_type = dict_type;
try {
await (formData.value?.id
? updateDictData(formData.value.id, data)
: createDictData(data));
await modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemDictDataApi.SystemDictData>();
if (data) {
if (data.pid === 0) {
data.pid = undefined;
}
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,147 @@
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';
import { format_datetime } from '#/utils/date';
/**
* 获取编辑表单的字段配置。如果没有使用多语言可以直接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: 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<SystemDictTypeApi.SystemDictType>,
): VxeTableGridOptions<SystemDictTypeApi.SystemDictType>['columns'] {
return [
{
align: 'left',
field: 'id',
fixed: 'left',
title: '字典编号',
treeNode: true,
width: 150,
},
{
field: 'name',
title: '字典名称',
},
{
field: 'type',
title: '字典类型',
width: 180,
},
{
cellRender: {
name: 'CellTag',
},
field: 'status',
title: '状态',
width: 100,
},
{
field: 'remark',
title: '备注',
},
{
field: 'create_time',
title: '创建时间',
width: 180,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
{
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,
},
];
}

View File

@@ -0,0 +1,150 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemDictTypeApi } from '#/api/system/dict_type';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDictType, getDictTypeList } from '#/api/system/dict_type';
import { $t } from '#/locales';
import { useColumns } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑字典
* @param row
*/
function onEdit(row: SystemDictTypeApi.SystemDictType) {
if (row.menu_ids) {
row.menu_ids = row.menu_ids.split(',').map(Number);
}
formModalApi.setData(row).open();
}
/**
* 创建新字典
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除字典
* @param row
*/
function onDelete(row: SystemDictTypeApi.SystemDictType) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deleteDictType(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
refreshGrid();
})
.catch(() => {
hideLoading();
});
}
const handleViewDetail = (row: SystemDictTypeApi.SystemDictType) => {
router.push({
path: '/system/dict_data/', // 目标页面路径
query: { dict_type: row.id }, // 传递查询参数
});
};
/**
* 表格操作按钮的回调函数
*/
function onActionClick({
code,
row,
}: OnActionClickParams<SystemDictTypeApi.SystemDictType>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'view': {
handleViewDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDictTypeList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="字典列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.dict_type.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,79 @@
<script lang="ts" setup>
import type { SystemDictTypeApi } from '#/api/system/dict_type';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDictType, updateDictType } from '#/api/system/dict_type';
import { $t } from '#/locales';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemDictTypeApi.SystemDictType>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.dict_type.name')])
: $t('ui.actionTitle.create', [$t('system.dict_type.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateDictType(formData.value.id, data)
: createDictType(data));
await modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemDictTypeApi.SystemDictType>();
if (data) {
if (data.pid === 0) {
data.pid = undefined;
}
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,95 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { SystemLoginLogApi } from '#/models/system/login_log';
import { z } from '#/adapter/form';
import { $t } from '#/locales';
import { format_datetime } from '#/utils/date';
/**
* 获取编辑表单的字段配置
*/
export function useSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'username',
label: 'username',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['username']))
.max(100, $t('ui.formRules.maxLength', ['username', 100])),
},
{
component: 'InputNumber',
fieldName: 'result',
label: 'result',
},
{
component: 'Input',
fieldName: 'user_ip',
label: 'user ip',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['user ip']))
.max(100, $t('ui.formRules.maxLength', ['user ip', 100])),
},
{
component: 'Input',
fieldName: 'user_agent',
label: 'user agent',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['user agent']))
.max(100, $t('ui.formRules.maxLength', ['user agent', 100])),
},
{
component: 'Input',
fieldName: 'remark',
label: 'remark',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['remark']))
.max(100, $t('ui.formRules.maxLength', ['remark', 100])),
},
];
}
/**
* 获取表格列配置
* @description 使用函数的形式返回列数据而不是直接export一个Array常量是为了响应语言切换时重新翻译表头
*/
export function useColumns(): VxeTableGridOptions<SystemLoginLogApi.SystemLoginLog>['columns'] {
return [
{
field: 'id',
title: 'ID',
},
{
field: 'username',
title: '用户名',
},
{
cellRender: {
name: 'CellTag',
},
field: 'result_text',
title: '登录结果',
},
{
field: 'user_ip',
title: '登录地址',
},
{
field: 'user_agent',
title: '浏览器',
},
{
field: 'create_time',
title: $t('system.createTime'),
width: 150,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
];
}

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { SystemLoginLogModel } from '#/models/system/login_log';
import { useColumns } from './data';
const formModel = new SystemLoginLogModel();
const [Grid] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await formModel.list({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
} as VxeTableGridOptions,
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="系统访问记录" />
</Page>
</template>

View File

@@ -0,0 +1,79 @@
<script lang="ts" setup>
import type { SystemLoginLogApi } from '#/models/system/login_log';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { $t } from '#/locales';
import { SystemLoginLogModel } from '#/models/system/login_log';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formModel = new SystemLoginLogModel();
const formData = ref<SystemLoginLogApi.SystemLoginLog>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.login_log.name')])
: $t('ui.actionTitle.create', [$t('system.login_log.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? formModel.update(formData.value.id, data)
: formModel.create(data));
await modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemLoginLogApi.SystemLoginLog>();
if (data) {
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,114 @@
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<SystemMenuApi.SystemMenu>,
): VxeTableGridOptions<SystemMenuApi.SystemMenu>['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: 'auth_code',
title: $t('system.menu.auth_code'),
width: 200,
},
{
field: 'sort',
title: $t('system.menu.sort'),
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,
},
];
}

View File

@@ -0,0 +1,162 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon, Plus } from '@vben/icons';
import { $t } from '@vben/locales';
import { MenuBadge } from '@vben-core/menu-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteMenu, getMenuList, SystemMenuApi } from '#/api/system/menu';
import { useColumns } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async (_params) => {
return await getMenuList();
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
function onActionClick({
code,
row,
}: OnActionClickParams<SystemMenuApi.SystemMenu>) {
switch (code) {
case 'append': {
onAppend(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
default: {
break;
}
}
}
function onRefresh() {
gridApi.query();
}
function onEdit(row: SystemMenuApi.SystemMenu) {
formDrawerApi.setData(row).open();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
function onAppend(row: SystemMenuApi.SystemMenu) {
formDrawerApi.setData({ pid: row.id }).open();
}
function onDelete(row: SystemMenuApi.SystemMenu) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deleteMenu(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid>
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.menu.name')]) }}
</Button>
</template>
<template #title="{ row }">
<div class="flex w-full items-center gap-1">
<div class="size-5 flex-shrink-0">
<IconifyIcon
v-if="row.type === 'button'"
icon="carbon:security"
class="size-full"
/>
<IconifyIcon
v-else-if="row.meta?.icon"
:icon="row.meta?.icon || 'carbon:circle-dash'"
class="size-full"
/>
</div>
<span class="flex-auto">{{ $t(row.meta?.title) }}</span>
<div class="items-center justify-end"></div>
</div>
<MenuBadge
v-if="row.meta?.badgeType"
class="menu-badge"
:badge="row.meta.badge"
:badge-type="row.meta.badgeType"
:badge-variants="row.meta.badgeVariants"
/>
</template>
</Grid>
</Page>
</template>
<style lang="scss" scoped>
.menu-badge {
top: 50%;
right: 0;
transform: translateY(-50%);
& > :deep(div) {
padding-top: 0;
padding-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,524 @@
<script lang="ts" setup>
import type { ChangeEvent } from 'ant-design-vue/es/_util/EventInterface';
import type { Recordable } from '@vben/types';
import type { VbenFormSchema } from '#/adapter/form';
import { computed, h, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $te } from '@vben/locales';
import { getPopupContainer } from '@vben/utils';
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
import { useVbenForm, z } from '#/adapter/form';
import {
createMenu,
getMenuList,
isMenuPathExists,
isMenuSearchExists,
SystemMenuApi,
updateMenu,
} from '#/api/system/menu';
import { $t } from '#/locales';
import { componentKeys } from '#/router/routes';
import { getMenuTypeOptions } from '../data';
const emit = defineEmits<{
success: [];
}>();
const formData = ref<SystemMenuApi.SystemMenu>();
const titleSuffix = ref<string>();
const schema: VbenFormSchema[] = [
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: getMenuTypeOptions(),
optionType: 'button',
},
defaultValue: 'menu',
fieldName: 'type',
formItemClass: 'col-span-2 md:col-span-2',
label: $t('system.menu.type'),
},
{
component: 'Input',
fieldName: 'name',
label: $t('system.menu.menuName'),
rules: z
.string()
.min(2, $t('ui.formRules.minLength', [$t('system.menu.menuName'), 2]))
.max(30, $t('ui.formRules.maxLength', [$t('system.menu.menuName'), 30]))
.refine(
async (value: string) => {
return !(await isMenuSearchExists(
value,
formData.value?.id,
formData.value?.pid,
));
},
(value) => ({
message: $t('ui.formRules.alreadyExists', [
$t('system.menu.menuName'),
value,
]),
}),
),
},
{
component: 'ApiTreeSelect',
componentProps: {
api: getMenuList,
class: 'w-full',
filterTreeNode(input: string, node: Recordable<any>) {
if (!input || input.length === 0) {
return true;
}
const title: string = node.meta?.title ?? '';
if (!title) return false;
return title.includes(input) || $t(title).includes(input);
},
getPopupContainer,
resultField: 'items',
labelField: 'meta.title',
showSearch: true,
treeDefaultExpandAll: true,
valueField: 'id',
childrenField: 'children',
},
fieldName: 'pid',
label: $t('system.menu.parent'),
renderComponentContent() {
return {
title({ label, meta }: { label: string; meta: Recordable<any> }) {
const coms = [];
if (!label) return '';
if (meta?.icon) {
coms.push(h(IconifyIcon, { class: 'size-4', icon: meta.icon }));
}
coms.push(h('span', { class: '' }, $t(label || '')));
return h('div', { class: 'flex items-center gap-1' }, coms);
},
};
},
},
{
component: 'Input',
componentProps() {
// 不需要处理多语言时就无需这么做
return {
addonAfter: titleSuffix.value,
onChange({ target: { value } }: ChangeEvent) {
titleSuffix.value = value && $te(value) ? $t(value) : undefined;
},
};
},
fieldName: 'meta.title',
label: $t('system.menu.menuTitle'),
rules: 'required',
},
{
component: 'Input',
dependencies: {
show: (values) => {
return ['catalog', 'embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'path',
label: $t('system.menu.path'),
rules: z
.string()
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
.refine(
(value: string) => {
return value.startsWith('/');
},
$t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
)
.refine(
async (value: string) => {
return !(await isMenuPathExists(value, formData.value?.id));
},
(value) => ({
message: $t('ui.formRules.alreadyExists', [
$t('system.menu.path'),
value,
]),
}),
),
},
{
component: 'Input',
dependencies: {
show: (values) => {
return ['embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'activePath',
help: $t('system.menu.activePathHelp'),
label: $t('system.menu.activePath'),
rules: z
.string()
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
.refine(
(value: string) => {
return value.startsWith('/');
},
$t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
)
.refine(async (value: string) => {
return await isMenuPathExists(value, formData.value?.id);
}, $t('system.menu.activePathMustExist'))
.optional(),
},
{
component: 'IconPicker',
componentProps: {
prefix: 'carbon',
},
dependencies: {
show: (values) => {
return ['catalog', 'embedded', 'link', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.icon',
label: $t('system.menu.icon'),
},
{
component: 'IconPicker',
componentProps: {
prefix: 'carbon',
},
dependencies: {
show: (values) => {
return ['catalog', 'embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.activeIcon',
label: $t('system.menu.activeIcon'),
},
{
component: 'AutoComplete',
componentProps: {
allowClear: true,
class: 'w-full',
filterOption(input: string, option: { value: string }) {
return option.value.toLowerCase().includes(input.toLowerCase());
},
options: componentKeys.map((v) => ({ value: v })),
},
dependencies: {
rules: (values) => {
return values.type === 'menu' ? 'required' : null;
},
show: (values) => {
return values.type === 'menu';
},
triggerFields: ['type'],
},
fieldName: 'component',
label: $t('system.menu.component'),
},
{
component: 'Input',
dependencies: {
show: (values) => {
return ['embedded', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'linkSrc',
label: $t('system.menu.linkSrc'),
rules: z.string().url($t('ui.formRules.invalidURL')),
},
{
component: 'Input',
dependencies: {
rules: (values) => {
return values.type === 'action' ? 'required' : null;
},
show: (values) => {
return ['action', 'button', 'catalog', 'embedded', 'menu'].includes(
values.type,
);
},
triggerFields: ['type'],
},
fieldName: 'auth_code',
label: $t('system.menu.auth_code'),
},
{
component: 'InputNumber',
dependencies: {
rules: () => {
return 'required';
},
triggerFields: ['type'],
},
fieldName: 'sort',
label: $t('system.menu.sort'),
},
{
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.menu.status'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: [
{ label: $t('system.menu.badgeType.dot'), value: 'dot' },
{ label: $t('system.menu.badgeType.normal'), value: 'normal' },
],
},
dependencies: {
show: (values) => {
// return values.type !== 'action';
return !['action', 'button'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.badgeType',
label: $t('system.menu.badgeType.title'),
},
{
component: 'Input',
componentProps: (values) => {
return {
allowClear: true,
class: 'w-full',
disabled: values.meta?.badgeType !== 'normal',
};
},
dependencies: {
show: (values) => {
return !['action', 'button'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.badge',
label: $t('system.menu.badge'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: SystemMenuApi.BadgeVariants.map((v) => ({
label: v,
value: v,
})),
},
dependencies: {
show: (values) => {
return !['action', 'button'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.badgeVariants',
label: $t('system.menu.badgeVariants'),
},
{
component: 'Divider',
dependencies: {
show: (values) => {
return !['action', 'button', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'divider1',
formItemClass: 'col-span-2 md:col-span-2 pb-0',
hideLabel: true,
renderComponentContent() {
return {
default: () => $t('system.menu.advancedSettings'),
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return ['menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.keepAlive',
renderComponentContent() {
return {
default: () => $t('system.menu.keepAlive'),
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return ['embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.affixTab',
renderComponentContent() {
return {
default: () => $t('system.menu.affixTab'),
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['action', 'button'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hide_in_menu',
renderComponentContent() {
return {
default: () => $t('system.menu.hideInMenu'),
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return ['catalog', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hide_children_in_menu',
renderComponentContent() {
return {
default: () => $t('system.menu.hideChildrenInMenu'),
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['action', 'button', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hideInBreadcrumb',
renderComponentContent() {
return {
default: () => $t('system.menu.hideInBreadcrumb'),
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['action', 'button', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hideInTab',
renderComponentContent() {
return {
default: () => $t('system.menu.hideInTab'),
};
},
},
];
const breakpoints = useBreakpoints(breakpointsTailwind);
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
const [Form, formApi] = useVbenForm({
commonConfig: {
colon: true,
formItemClass: 'col-span-2 md:col-span-1',
},
schema,
showDefaultActions: false,
wrapperClass: 'grid-cols-2 gap-x-4',
});
const [Drawer, drawerApi] = useVbenDrawer({
onConfirm: onSubmit,
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemMenuApi.SystemMenu>();
if (data?.type === 'link') {
data.linkSrc = data.meta?.link;
} else if (data?.type === 'embedded') {
data.linkSrc = data.meta?.iframeSrc;
}
if (data) {
formData.value = data;
formApi.setValues(formData.value);
titleSuffix.value = formData.value.meta?.title
? $t(formData.value.meta.title)
: '';
} else {
formApi.resetForm();
titleSuffix.value = '';
}
}
},
});
async function onSubmit() {
const { valid } = await formApi.validate();
if (valid) {
drawerApi.lock();
const data =
await formApi.getValues<
Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>
>();
if (data.type === 'link') {
data.meta = { ...data.meta, link: data.linkSrc };
} else if (data.type === 'embedded') {
data.meta = { ...data.meta, iframeSrc: data.linkSrc };
}
delete data.linkSrc;
try {
await (formData.value?.id
? updateMenu(formData.value.id, data)
: createMenu(data));
drawerApi.close();
emit('success');
} finally {
drawerApi.unlock();
}
}
}
const getDrawerTitle = computed(() =>
formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.menu.name')])
: $t('ui.actionTitle.create', [$t('system.menu.name')]),
);
</script>
<template>
<Drawer class="w-full max-w-[800px]" :title="getDrawerTitle">
<Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" />
</Drawer>
</template>

View File

@@ -0,0 +1,124 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { SystemPostApi } from '#/models/system/post';
import { z } from '#/adapter/form';
import { $t } from '#/locales';
import { format_datetime } from '#/utils/date';
/**
* 获取编辑表单的字段配置
*/
export function useSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '名称',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['名称']))
.max(100, $t('ui.formRules.maxLength', ['名称', 100])),
},
{
component: 'Input',
fieldName: 'code',
label: '编码',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['编码']))
.max(100, $t('ui.formRules.maxLength', ['编码', 100])),
},
{
component: 'InputNumber',
fieldName: 'sort',
label: '排序',
rules: z.number(),
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '开启', value: true },
{ label: '关闭', value: false },
],
optionType: 'button',
},
defaultValue: true,
fieldName: 'status',
label: '是否启用',
},
{
component: 'Input',
fieldName: 'remark',
label: '备注',
},
];
}
export function useColumns(
onActionClick?: OnActionClickFn<SystemPostApi.SystemPost>,
): VxeTableGridOptions<SystemPostApi.SystemPost>['columns'] {
return [
{
field: 'id',
title: 'id',
},
{
field: 'name',
title: '岗位名称',
},
{
field: 'code',
title: '岗位编码',
},
{
field: 'sort',
title: '排序',
},
{
cellRender: {
name: 'CellTag',
},
field: 'status',
title: '状态',
width: 100,
},
{
field: 'remark',
title: '备注',
},
{
field: 'create_time',
title: '创建时间',
width: 180,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
{
align: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: $t('system.post.name'),
onClick: onActionClick,
},
name: 'CellOperation',
options: [
'edit', // 默认的编辑按钮
{
code: 'delete', // 默认的删除按钮
},
],
},
field: 'operation',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
title: '操作',
width: 200,
},
];
}

View File

@@ -0,0 +1,132 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemPostApi } from '#/models/system/post';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { SystemPostModel } from '#/models/system/post';
import { useColumns } from './data';
import Form from './modules/form.vue';
const formModel = new SystemPostModel();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑岗位信息表
*/
function onEdit(row: SystemPostApi.SystemPost) {
formModalApi.setData(row).open();
}
/**
* 创建新岗位信息表
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除岗位信息表
*/
function onDelete(row: SystemPostApi.SystemPost) {
const hideLoading = message.loading({
content: '删除岗位信息表',
duration: 0,
key: 'action_process_msg',
});
formModel
.delete(row.id)
.then(() => {
message.success({
content: '删除成功',
key: 'action_process_msg',
});
refreshGrid();
})
.catch(() => {
hideLoading();
});
}
/**
* 表格操作按钮的回调函数
*/
function onActionClick({
code,
row,
}: OnActionClickParams<SystemPostApi.SystemPost>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await formModel.list({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="岗位信息表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.post.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,79 @@
<script lang="ts" setup>
import type { SystemPostApi } from '#/models/system/post';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { $t } from '#/locales';
import { SystemPostModel } from '#/models/system/post';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formModel = new SystemPostModel();
const formData = ref<SystemPostApi.SystemPost>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.post.name')])
: $t('ui.actionTitle.create', [$t('system.post.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? formModel.update(formData.value.id, data)
: formModel.create(data));
await modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemPostApi.SystemPost>();
if (data) {
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,118 @@
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: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: $t('common.enabled'), value: 1 },
{ label: $t('common.disabled'), value: 0 },
],
},
fieldName: 'status',
label: $t('system.role.status'),
},
];
}
export function useColumns<T = SystemRoleApi.SystemRole>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): 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,
},
];
}

View File

@@ -0,0 +1,166 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemRoleApi } from '#/api/system/role';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteRole, getRoleList, patchRole } from '#/api/system/role';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getRoleList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
pagerConfig: {
enabled: true,
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<SystemRoleApi.SystemRole>,
});
function onActionClick(e: OnActionClickParams<SystemRoleApi.SystemRole>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
/**
* 将Antd的Modal.confirm封装为promise方便在异步函数中调用。
* @param content 提示内容
* @param title 提示标题
*/
function confirm(content: string, title: string) {
return new Promise((reslove, reject) => {
Modal.confirm({
content,
onCancel() {
reject(new Error('已取消'));
},
onOk() {
reslove(true);
},
title,
});
});
}
/**
* 状态开关即将改变
* @param newStatus 期望改变的状态值
* @param row 行数据
* @returns 返回false则中止改变返回其他值undefined、true则允许改变
*/
async function onStatusChange(
newStatus: number,
row: SystemRoleApi.SystemRole,
) {
const status: Recordable<string> = {
0: '禁用',
1: '启用',
};
try {
await confirm(
`你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
`切换状态`,
);
await patchRole(row.id, { status: newStatus });
return true;
} catch {
return false;
}
}
function onEdit(row: SystemRoleApi.SystemRole) {
formDrawerApi.setData(row).open();
}
function onDelete(row: SystemRoleApi.SystemRole) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deleteRole(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh"/>
<Grid :table-title="$t('system.role.list')">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.role.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import type { DataNode } from 'ant-design-vue/es/tree';
import type { Recordable } from '@vben/types';
import type { SystemRoleApi } from '#/api/system/role';
import { computed, ref } from 'vue';
import { useVbenDrawer, VbenTree } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Spin } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getMenuList } from '#/api/system/menu';
import { createRole, updateRole } from '#/api/system/role';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emits = defineEmits(['success']);
const formData = ref<SystemRoleApi.SystemRole>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const permissions = ref<DataNode[]>([]);
const loadingPermissions = ref(false);
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
(id.value ? updateRole(id.value, values) : createRole(values))
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemRoleApi.SystemRole>();
formApi.resetForm();
if (data) {
formData.value = data;
id.value = data.id;
formApi.setValues(data);
} else {
id.value = undefined;
}
if (permissions.value.length === 0) {
loadPermissions();
}
}
},
});
async function loadPermissions() {
loadingPermissions.value = true;
try {
const res = await getMenuList();
permissions.value = res as unknown as DataNode[];
} finally {
loadingPermissions.value = false;
}
}
const getDrawerTitle = computed(() => {
return formData.value?.id
? $t('common.edit', $t('system.role.name'))
: $t('common.create', $t('system.role.name'));
});
function getNodeClass(node: Recordable<any>) {
const classes: string[] = [];
if (node.value?.type === 'button') {
classes.push('inline-flex');
if (node.index % 3 >= 1) {
classes.push('!pl-0');
}
}
return classes.join(' ');
}
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form>
<template #permissions="slotProps">
<Spin v-if="permissions.length" :spinning="loadingPermissions" wrapper-class-name="w-full">
<VbenTree
:tree-data="permissions"
multiple
bordered
:default-expanded-level="2"
:get-node-class="getNodeClass"
v-bind="slotProps"
value-field="id"
label-field="meta.title"
icon-field="meta.icon"
>
<template #node="{ value }">
<IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />
{{ $t(value.meta.title) }}
</template>
</VbenTree>
</Spin>
</template>
</Form>
</Drawer>
</template>
<style lang="css" scoped>
:deep(.ant-tree-title) {
.tree-actions {
display: none;
margin-left: 20px;
}
}
:deep(.ant-tree-title:hover) {
.tree-actions {
display: flex;
flex: auto;
justify-content: flex-end;
margin-left: 20px;
}
}
</style>

View File

@@ -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<SystemTenantPackageApi.SystemTenantPackage>,
): VxeTableGridOptions<SystemTenantPackageApi.SystemTenantPackage>['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,
},
];
}

View File

@@ -0,0 +1,135 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemTenantPackageApi } from '#/api/system/tenant_package';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteTenantPackage,
getTenantPackageList,
} from '#/api/system/tenant_package';
import { $t } from '#/locales';
import { useColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑套餐
* @param row
*/
function onEdit(row: SystemTenantPackageApi.SystemTenantPackage) {
formModalApi.setData(row).open();
}
/**
* 创建新套餐
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除套餐
* @param row
*/
function onDelete(row: SystemTenantPackageApi.SystemTenantPackage) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deleteTenantPackage(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
refreshGrid();
})
.catch(() => {
hideLoading();
});
}
/**
* 表格操作按钮的回调函数
*/
function onActionClick({
code,
row,
}: OnActionClickParams<SystemTenantPackageApi.SystemTenantPackage>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async (_params) => {
return await getTenantPackageList();
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="租户套餐">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.tenants_package.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,78 @@
<script lang="ts" setup>
import type { SystemTenantPackageApi } from '#/api/system/tenant_package';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createTenantPackage, updateTenantPackage } from '#/api/system/tenant_package';
import { $t } from '#/locales';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemTenantPackageApi.SystemTenantPackage>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.dept.name')])
: $t('ui.actionTitle.create', [$t('system.dept.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateTenantPackage(formData.value.id, data)
: createTenantPackage(data));
modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemTenantPackagestApi.SystemTenantPackage>();
if (data) {
if (data.pid === 0) {
data.pid = undefined;
}
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -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<SystemTenantsApi.SystemTenants>,
): VxeTableGridOptions<SystemTenantsApi.SystemTenants>['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,
},
];
}

View File

@@ -0,0 +1,131 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemTenantsApi } from '#/api/system/tenants';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteTenants, getTenantsList } from '#/api/system/tenants';
import { $t } from '#/locales';
import { useColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑租户
* @param row
*/
function onEdit(row: SystemTenantsApi.SystemTenants) {
formModalApi.setData(row).open();
}
/**
* 创建新租户
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除租户
* @param row
*/
function onDelete(row: SystemTenantsApi.SystemTenants) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deleteTenants(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
refreshGrid();
})
.catch(() => {
hideLoading();
});
}
/**
* 表格操作按钮的回调函数
*/
function onActionClick({
code,
row,
}: OnActionClickParams<SystemTenantsApi.SystemTenants>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async (_params) => {
return await getTenantsList();
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="租户列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.tenants.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,78 @@
<script lang="ts" setup>
import type { SystemTenantsApi } from '#/api/system/tenants';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createTenants, updateTenants } from '#/api/system/tenants';
import { $t } from '#/locales';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemTenantsApi.SystemTenants>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.dept.name')])
: $t('ui.actionTitle.create', [$t('system.dept.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateTenants(formData.value.id, data)
: createTenants(data));
modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemTenantstApi.SystemTenants>();
if (data) {
if (data.pid === 0) {
data.pid = undefined;
}
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,232 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/models/system/user';
import { z } from '#/adapter/form';
import { getDeptList, getRoleList } from '#/api/system';
import { $t } from '#/locales';
import { SystemPostModel } from '#/models/system/post';
import { format_datetime } from '#/utils/date';
const systemPost = new SystemPostModel();
/**
* 获取编辑表单的字段配置
*/
export function useSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'username',
label: '用户名',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['用户名']))
.max(100, $t('ui.formRules.maxLength', ['用户名', 100])),
},
{
component: 'Input',
fieldName: 'email',
label: '电子邮件地址',
rules: z
.string()
.min(1, $t('ui.formRules.required', ['电子邮件地址']))
.max(100, $t('ui.formRules.maxLength', ['电子邮件地址', 100])),
},
{
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
multiple: true, // 允许多选
api: getDeptList,
class: 'w-full',
resultField: 'items',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
fieldName: 'dept',
label: $t('system.dept.name'),
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
multiple: true, // 允许多选
mode: 'multiple', // 允许多选
api: getRoleList,
class: 'w-full',
resultField: 'items',
labelField: 'name',
valueField: 'id',
},
fieldName: 'role',
label: $t('system.role.name'),
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
multiple: true, // 允许多选
mode: 'multiple', // 允许多选
api: () => systemPost.list(),
class: 'w-full',
resultField: 'items',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
fieldName: 'post',
label: $t('system.post.name'),
},
{
component: 'Input',
fieldName: 'mobile',
label: '手机号',
},
{
component: 'Input',
fieldName: 'nickname',
label: '昵称',
},
{
component: 'InputPassword',
fieldName: 'password',
label: '密码',
},
{
component: 'Input',
fieldName: 'city',
label: '城市',
},
{
component: 'Input',
fieldName: 'province',
label: '省份',
},
{
component: 'Input',
fieldName: 'country',
label: '国家',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '开启', value: 1 },
{ label: '关闭', value: 0 },
],
optionType: 'button',
},
defaultValue: 1,
fieldName: 'status',
label: $t('system.status'),
},
{
component: 'Input',
fieldName: 'remark',
label: $t('system.remark'),
},
];
}
/**
* 获取表格列配置
* @description 使用函数的形式返回列数据而不是直接export一个Array常量是为了响应语言切换时重新翻译表头
* @param onActionClick 表格操作按钮点击事件
*/
export function useColumns(
onActionClick?: OnActionClickFn<SystemUserApi.SystemUser>,
): VxeTableGridOptions<SystemUserApi.SystemUser>['columns'] {
return [
{
field: 'id',
title: 'ID',
},
{
field: 'username',
title: '用户名',
width: 100,
},
{
field: 'is_superuser',
title: '超级用户状态',
},
{
field: 'date_joined',
title: '加入日期',
width: 150,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
{
field: 'mobile',
title: 'mobile',
},
{
cellRender: {
name: 'CellTag',
},
field: 'status',
title: $t('system.status'),
width: 100,
},
{
field: 'login_ip',
title: 'login ip',
width: 150,
},
{
field: 'last_login',
title: '最后登录',
width: 150,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
{
field: 'remark',
title: $t('system.remark'),
},
{
field: 'creator',
title: $t('system.creator'),
width: 80,
},
{
field: 'modifier',
title: $t('system.modifier'),
width: 80,
},
{
field: 'update_time',
title: $t('system.updateTime'),
width: 150,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
{
field: 'create_time',
title: $t('system.createTime'),
width: 150,
formatter: ({ cellValue }) => format_datetime(cellValue),
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: $t('system.user.name'),
onClick: onActionClick,
},
name: 'CellOperation',
options: ['edit', 'delete'],
},
field: 'action',
fixed: 'right',
title: '操作',
width: 120,
},
];
}

View File

@@ -0,0 +1,129 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/models/system/user';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { ElLoading, ElMessage } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { SystemUserModel } from '#/models/system/user';
import { useColumns } from './data';
import Form from './modules/form.vue';
const formModel = new SystemUserModel();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑用户数据
*/
function onEdit(row: SystemUserApi.SystemUser) {
formModalApi.setData(row).open();
}
/**
* 创建新用户数据
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除用户数据
*/
function onDelete(row: SystemUserApi.SystemUser) {
const loading = ElLoading.service({
lock: true,
text: '删除用户数据',
background: 'rgba(0, 0, 0, 0.7)',
});
formModel
.delete(row.id)
.then(() => {
ElMessage.success('删除成功');
refreshGrid();
})
.catch(() => {
loading.close();
});
}
/**
* 表格操作按钮的回调函数
*/
function onActionClick({
code,
row,
}: OnActionClickParams<SystemUserApi.SystemUser>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await formModel.list({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="用户数据">
<template #toolbar-tools>
<el-button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.user.name')]) }}
</el-button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SystemUserApi } from '#/models/system/user';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { $t } from '#/locales';
import { SystemUserModel } from '#/models/system/user';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formModel = new SystemUserModel();
const formData = ref<SystemUserApi.SystemUser>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.user.name')])
: $t('ui.actionTitle.create', [$t('system.user.name')]);
});
const [Form, formApi] = useVbenForm({
layout: 'horizontal',
commonConfig: {
colon: true,
formItemClass: 'col-span-2 md:col-span-1',
},
wrapperClass: 'grid-cols-2 gap-x-4',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? formModel.update(formData.value.id, data)
: formModel.create(data));
await modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<SystemUserApi.SystemUser>();
if (data) {
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-full max-w-[800px]">
<Form />
<template #prepend-footer>
<div class="flex-auto">
<el-button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</el-button>
</div>
</template>
</Modal>
</template>
<style lang="css" scoped></style>