feat: 测试代码生成

This commit is contained in:
xlsea
2024-09-09 15:40:38 +08:00
parent 74529144aa
commit 07e30bd591
26 changed files with 673 additions and 143 deletions

View File

@ -71,10 +71,10 @@ public class VelocityUtils {
velocityContext.put("pkColumn", genTable.getPkColumn()); velocityContext.put("pkColumn", genTable.getPkColumn());
velocityContext.put("importList", getImportList(genTable)); velocityContext.put("importList", getImportList(genTable));
velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
velocityContext.put("columns", getColumns(genTable));
velocityContext.put("table", genTable);
velocityContext.put("dicts", getDicts(genTable)); velocityContext.put("dicts", getDicts(genTable));
velocityContext.put("dictList", getDictList(genTable)); velocityContext.put("dictList", getDictList(genTable));
velocityContext.put("columns", getColumns(genTable));
velocityContext.put("table", genTable);
setMenuVelocityContext(velocityContext, genTable); setMenuVelocityContext(velocityContext, genTable);
if (GenConstants.TPL_TREE.equals(tplCategory)) { if (GenConstants.TPL_TREE.equals(tplCategory)) {
setTreeVelocityContext(velocityContext, genTable); setTreeVelocityContext(velocityContext, genTable);
@ -178,7 +178,7 @@ public class VelocityUtils {
if (template.contains("soy.index.vue.vm")) { if (template.contains("soy.index.vue.vm")) {
fileName = StringUtils.format("soybean/views/{}/{}/index.vue", moduleName, businessName); fileName = StringUtils.format("soybean/views/{}/{}/index.vue", moduleName, businessName);
} else if (template.contains("soy.api.d.ts.vm")) { } else if (template.contains("soy.api.d.ts.vm")) {
fileName = StringUtils.format("soybean/typings/api/{}.d.ts", moduleName); fileName = StringUtils.format("soybean/typings/api/{}.api.d.ts", moduleName);
} else if (template.contains("soy.api.ts.vm")) { } else if (template.contains("soy.api.ts.vm")) {
fileName = StringUtils.format("soybean/api/{}/{}.ts", moduleName, businessName); fileName = StringUtils.format("soybean/api/{}/{}.ts", moduleName, businessName);
} else if (template.contains("soy.search.vue.vm")) { } else if (template.contains("soy.search.vue.vm")) {

View File

@ -28,7 +28,7 @@ export function fetchUpdate${BusinessName} (data: Api.${ModuleName}.${BusinessNa
} }
/** 批量删除${functionName} */ /** 批量删除${functionName} */
export function fetchDelete${BusinessName} (${pkColumn.javaField}s: CommonType.IdType[]) { export function fetchBatchDelete${BusinessName} (${pkColumn.javaField}s: CommonType.IdType[]) {
return request<boolean>({ return request<boolean>({
url: `/${moduleName}/${businessName}/${${pkColumn.javaField}s.join(',')}`, url: `/${moduleName}/${businessName}/${${pkColumn.javaField}s.join(',')}`,
method: 'delete' method: 'delete'

View File

@ -3,6 +3,7 @@ import { computed, reactive, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form'; import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { fetchCreate${BusinessName}, fetchUpdate${BusinessName} } from '@/service/api/${moduleName}/${businessName}'; import { fetchCreate${BusinessName}, fetchUpdate${BusinessName} } from '@/service/api/${moduleName}/${businessName}';
#if($dictList && $dictList.size() > 0)import { useDict } from '@/hooks/business/dict';#end
defineOptions({ defineOptions({
name: '${BusinessName}OperateDrawer' name: '${BusinessName}OperateDrawer'

View File

@ -3,6 +3,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { useNaiveForm } from '@/hooks/common/form'; import { useNaiveForm } from '@/hooks/common/form';
#if($dictList && $dictList.size() > 0)import { useDict } from '@/hooks/business/dict';#end
defineOptions({ defineOptions({
name: '${BusinessName}Search' name: '${BusinessName}Search'
@ -27,14 +28,14 @@ const model = defineModel<Api.$ModuleName.${BusinessName}SearchParams>('model',
#if($dictList && $dictList.size() > 0) #if($dictList && $dictList.size() > 0)
#foreach($dict in $dictList) #foreach($dict in $dictList)
const { options: ${dict.name}Options } = useDict(${dict.type}#if($dict.immediate), false#end); const { options: ${dict.name}Options } = useDict('${dict.type}#if($dict.immediate)', false#end);
#end#end #end#end
async function reset() { async function reset() {
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
const dateRange${AttrName}.value = undefined; dateRange${AttrName}.value = undefined;
#end #end
#end #end
await restoreValidation(); await restoreValidation();

View File

@ -26,8 +26,8 @@ const {
} = useTable({ } = useTable({
apiFn: fetchGet${BusinessName}List, apiFn: fetchGet${BusinessName}List,
apiParams: { apiParams: {
current: 1, pageNum: 1,
size: 10, pageSize: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null // if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive // the value can not be undefined, otherwise the property in Form will not be reactive
#foreach ($column in $columns) #foreach ($column in $columns)
@ -109,7 +109,7 @@ async function handleDelete(#foreach($column in $columns)#if($column.isPk == '1'
onDeleted(); onDeleted();
} }
async function edit(id: CommonType.IdType) { async function edit(#foreach($column in $columns)#if($column.isPk == '1')$column.javaField#end#end: CommonType.IdType) {
handleEdit('#foreach($column in $columns)#if($column.isPk == '1')$column.javaField#end#end', #foreach($column in $columns)#if($column.isPk == '1')$column.javaField#end#end); handleEdit('#foreach($column in $columns)#if($column.isPk == '1')$column.javaField#end#end', #foreach($column in $columns)#if($column.isPk == '1')$column.javaField#end#end);
} }
</script> </script>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { useDict } from '@/hooks/business/dict';
defineOptions({ name: 'DictRadio' });
interface Props {
dictCode: string;
immediate?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
immediate: true
});
const value = defineModel<string | null>('value', { required: false });
const { options } = useDict(props.dictCode, props.immediate);
</script>
<template>
<NRadioGroup v-model:value="value">
<NSpace>
<NRadio v-for="option in options" :key="option.value" :value="option.value" :label="option.label" />
</NSpace>
</NRadioGroup>
</template>
<style scoped></style>

View File

@ -1,46 +1,30 @@
<script setup lang="tsx"> <script setup lang="ts">
import { ref, useAttrs } from 'vue'; import { useAttrs } from 'vue';
import { useLoading } from '@sa/hooks'; import type { SelectProps } from 'naive-ui';
import type { SelectOption, SelectProps } from 'naive-ui'; import { useDict } from '@/hooks/business/dict';
import { fetchGetDictTypeOption } from '@/service/api/system';
defineOptions({ name: 'DictSelect' }); defineOptions({ name: 'DictSelect' });
interface Props { interface Props {
dictCode: string;
immediate?: boolean;
[key: string]: any; [key: string]: any;
} }
defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
immediate: true
});
const value = defineModel<string>('value', { required: true }); const value = defineModel<string | null>('value', { required: true });
const attrs: SelectProps = useAttrs(); const attrs: SelectProps = useAttrs();
const options = ref<SelectOption[]>([]); const { options } = useDict(props.dictCode, props.immediate);
const { loading, startLoading, endLoading } = useLoading();
async function getDeptOptions() {
startLoading();
const { error, data } = await fetchGetDictTypeOption();
if (error) return;
options.value = data.map(dict => ({
value: dict.dictType!,
label: () => (
<div class="w-520px flex justify-between">
<span>{dict.dictType}</span>
<span>{dict.dictName}</span>
</div>
)
}));
endLoading();
}
getDeptOptions();
</script> </script>
<template> <template>
<NSelect <NSelect
v-model:value="value" v-model:value="value"
:loading="loading" :loading="!options.length"
:options="options" :options="options"
:clear-filter-after-select="false" :clear-filter-after-select="false"
v-bind="attrs" v-bind="attrs"

View File

@ -19,8 +19,8 @@ export const menuTypeOptions = transformRecordToOption(menuTypeRecord);
/** menu is frame */ /** menu is frame */
export const menuIsFrameRecord: Record<Api.System.IsMenuFrame, string> = { export const menuIsFrameRecord: Record<Api.System.IsMenuFrame, string> = {
'0': '缓存', '0': '',
'1': '不缓存', '1': '',
'2': 'iframe' '2': 'iframe'
}; };

View File

@ -28,7 +28,7 @@ export function useDownload() {
window.$loading?.startLoading('正在下载数据,请稍候...'); window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token'); const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID; const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const now = new Date().getTime(); const now = Date.now();
const formData = new FormData(); const formData = new FormData();
Object.keys(params).forEach(key => formData.append(key, params[key])); Object.keys(params).forEach(key => formData.append(key, params[key]));
fetch(`${baseURL}${url}?t=${now}`, { fetch(`${baseURL}${url}?t=${now}`, {
@ -58,7 +58,7 @@ export function useDownload() {
const token = localStg.get('token'); const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID; const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const url = `/resource/oss/download/${ossId}`; const url = `/resource/oss/download/${ossId}`;
const now = new Date().getTime(); const now = Date.now();
let fileName = String(`${ossId}-${now}`); let fileName = String(`${ossId}-${now}`);
fetch(`${baseURL}${url}?t=${now}`, { fetch(`${baseURL}${url}?t=${now}`, {
method: 'get', method: 'get',
@ -80,7 +80,7 @@ export function useDownload() {
window.$loading?.startLoading('正在下载数据,请稍候...'); window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token'); const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID; const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const now = new Date().getTime(); const now = Date.now();
fetch(`${baseURL}${url}${url.includes('?') ? '&' : '?'}t=${now}`, { fetch(`${baseURL}${url}${url.includes('?') ? '&' : '?'}t=${now}`, {
method: 'get', method: 'get',
headers: { headers: {

View File

@ -160,7 +160,8 @@ const local: App.I18n.Schema = {
system: 'System Management', system: 'System Management',
system_menu: 'Menu Management', system_menu: 'Menu Management',
tool: 'System Tools', tool: 'System Tools',
tool_gen: 'Code Generation' tool_gen: 'Code Generation',
system_user: 'User Management'
}, },
page: { page: {
login: { login: {

View File

@ -160,7 +160,8 @@ const local: App.I18n.Schema = {
system: '系统管理', system: '系统管理',
system_menu: '菜单管理', system_menu: '菜单管理',
tool: '系统工具', tool: '系统工具',
tool_gen: '代码生成' tool_gen: '代码生成',
system_user: '用户管理'
}, },
page: { page: {
login: { login: {

View File

@ -22,5 +22,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
login: () => import("@/views/_builtin/login/index.vue"), login: () => import("@/views/_builtin/login/index.vue"),
home: () => import("@/views/home/index.vue"), home: () => import("@/views/home/index.vue"),
system_menu: () => import("@/views/system/menu/index.vue"), system_menu: () => import("@/views/system/menu/index.vue"),
system_user: () => import("@/views/system/user/index.vue"),
tool_gen: () => import("@/views/tool/gen/index.vue"), tool_gen: () => import("@/views/tool/gen/index.vue"),
}; };

View File

@ -96,6 +96,15 @@ export const generatedRoutes: GeneratedRoute[] = [
localIcon: 'menu-tree-table', localIcon: 'menu-tree-table',
order: 3 order: 3
} }
},
{
name: 'system_user',
path: '/system/user',
component: 'view.system_user',
meta: {
title: 'system_user',
i18nKey: 'route.system_user'
}
} }
] ]
}, },

View File

@ -171,6 +171,7 @@ const routeMap: RouteMap = {
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"system": "/system", "system": "/system",
"system_menu": "/system/menu", "system_menu": "/system/menu",
"system_user": "/system/user",
"tool": "/tool", "tool": "/tool",
"tool_gen": "/tool/gen" "tool_gen": "/tool/gen"
}; };

View File

@ -1,2 +1,3 @@
export * from './menu'; export * from './menu';
export * from './dict'; export * from './dict';
export * from './user';

View File

@ -0,0 +1,36 @@
import { request } from '@/service/request';
/** 获取用户信息列表 */
export function fetchGetUserList(params?: Api.System.UserSearchParams) {
return request<Api.System.UserList>({
url: '/system/user/list',
method: 'get',
params
});
}
/** 新增用户信息 */
export function fetchCreateUser(data: Api.System.UserOperateParams) {
return request<boolean>({
url: '/system/user',
method: 'post',
data
});
}
/** 修改用户信息 */
export function fetchUpdateUser(data: Api.System.UserOperateParams) {
return request<boolean>({
url: '/system/user',
method: 'put',
data
});
}
/** 批量删除用户信息 */
export function fetchBatchDeleteUser(userIds: CommonType.IdType[]) {
return request<boolean>({
url: `/system/user/${userIds.join(',')}`,
method: 'delete'
});
}

View File

@ -21,9 +21,9 @@ declare namespace Api {
} }
/** common search params of table */ /** common search params of table */
type CommonSearchParams<T = any> = Pick<Common.PaginatingCommonParams, 'pageNum' | 'pageSize'> & type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'pageNum' | 'pageSize'> &
CommonType.RecordNullable<{ CommonType.RecordNullable<{
orderByColumn: keyof T; orderByColumn: string;
isAsc: 'asc' | 'desc'; isAsc: 'asc' | 'desc';
params: { [key: string]: any }; params: { [key: string]: any };
}>; }>;
@ -69,8 +69,7 @@ declare namespace Api {
type CommonTenantRecord<T = any> = { type CommonTenantRecord<T = any> = {
/** record tenant id */ /** record tenant id */
tenantId: string; tenantId: string;
} & CommonRecord & } & CommonRecord<T>;
T;
} }
/** /**

View File

@ -13,32 +13,32 @@ declare namespace Api {
/** role */ /** role */
type Role = Common.CommonRecord<{ type Role = Common.CommonRecord<{
/** 数据范围1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限 */ /** 数据范围1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限 */
dataScope?: string; dataScope: string;
/** 部门树选择项是否关联显示 */ /** 部门树选择项是否关联显示 */
deptCheckStrictly?: boolean; deptCheckStrictly: boolean;
/** 用户是否存在此角色标识 默认不存在 */ /** 用户是否存在此角色标识 默认不存在 */
flag?: boolean; flag: boolean;
/** 菜单树选择项是否关联显示 */ /** 菜单树选择项是否关联显示 */
menuCheckStrictly?: boolean; menuCheckStrictly: boolean;
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 角色ID */ /** 角色ID */
roleId?: number; roleId: number;
/** 角色权限字符串 */ /** 角色权限字符串 */
roleKey?: string; roleKey: string;
/** 角色名称 */ /** 角色名称 */
roleName?: string; roleName: string;
/** 显示顺序 */ /** 显示顺序 */
roleSort?: number; roleSort: number;
/** 角色状态0正常 1停用 */ /** 角色状态0正常 1停用 */
status?: string; status: string;
/** 是否管理员 */ /** 是否管理员 */
superAdmin?: boolean; superAdmin: boolean;
}>; }>;
/** role search params */ /** role search params */
type RoleSearchParams = CommonType.RecordNullable< type RoleSearchParams = CommonType.RecordNullable<
Pick<Role, 'roleName' | 'roleKey' | 'status'> & Common.CommonSearchParams<Role> Pick<Role, 'roleName' | 'roleKey' | 'status'> & Common.CommonSearchParams
>; >;
/** role list */ /** role list */
@ -58,40 +58,59 @@ declare namespace Api {
/** user */ /** user */
type User = Common.CommonTenantRecord<{ type User = Common.CommonTenantRecord<{
/** 用户ID */ /** 用户ID */
userId?: CommonType.IdType; userId: CommonType.IdType;
/** 部门ID */ /** 部门ID */
deptId?: CommonType.IdType; deptId: CommonType.IdType;
/** 部门名称 */
deptName: string;
/** 用户账号 */ /** 用户账号 */
userName?: string; userName: string;
/** 用户昵称 */ /** 用户昵称 */
nickName?: string; nickName: string;
/** 用户类型sys_user系统用户 */ /** 用户类型sys_user系统用户 */
userType?: string; userType: string;
/** 用户邮箱 */ /** 用户邮箱 */
email?: string; email: string;
/** 手机号码 */ /** 手机号码 */
phonenumber?: string; phonenumber: string;
/** 用户性别0男 1女 2未知 */ /** 用户性别0男 1女 2未知 */
sex?: string; sex: string;
/** 头像地址 */ /** 头像地址 */
avatar?: number; avatar: string;
/** 密码 */ /** 密码 */
password?: string; password: string;
/** 帐号状态0正常 1停用 */ /** 帐号状态0正常 1停用 */
status?: string; status: string;
/** 删除标志0代表存在 2代表删除 */ /** 删除标志0代表存在 2代表删除 */
delFlag?: string; delFlag: string;
/** 最后登录IP */ /** 最后登录IP */
loginIp?: string; loginIp: string;
/** 最后登录时间 */ /** 最后登录时间 */
loginDate?: Date; loginDate: Date;
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
}>; }>;
/** user search params */ /** user search params */
type UserSearchParams = CommonType.RecordNullable< type UserSearchParams = CommonType.RecordNullable<
Pick<User, 'userName' | 'sex' | 'nickName' | 'phonenumber' | 'email' | 'status'> & Common.CommonSearchParams<User> Pick<User, 'deptId' | 'userName' | 'nickName' | 'phonenumber' | 'status'> & Common.CommonSearchParams
>;
/** user operate params */
type UserOperateParams = CommonType.RecordNullable<
Pick<
User,
| 'userId'
| 'deptId'
| 'userName'
| 'nickName'
| 'email'
| 'phonenumber'
| 'sex'
| 'password'
| 'status'
| 'remark'
> & { roleIds: string[] }
>; >;
/** user list */ /** user list */
@ -100,33 +119,33 @@ declare namespace Api {
/** tenant */ /** tenant */
interface Tenant { interface Tenant {
/** id */ /** id */
id?: CommonType.IdType; id: CommonType.IdType;
/** 租户编号 */ /** 租户编号 */
tenantId?: string; tenantId: string;
/** 联系人 */ /** 联系人 */
contactUserName?: string; contactUserName: string;
/** 联系电话 */ /** 联系电话 */
contactPhone?: string; contactPhone: string;
/** 企业名称 */ /** 企业名称 */
companyName?: string; companyName: string;
/** 统一社会信用代码 */ /** 统一社会信用代码 */
licenseNumber?: string; licenseNumber: string;
/** 地址 */ /** 地址 */
address?: string; address: string;
/** 域名 */ /** 域名 */
domain?: string; domain: string;
/** 企业简介 */ /** 企业简介 */
intro?: string; intro: string;
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 租户套餐编号 */ /** 租户套餐编号 */
packageId?: number; packageId: number;
/** 过期时间 */ /** 过期时间 */
expireTime?: Date; expireTime: Date;
/** 用户数量(-1不限制 */ /** 用户数量(-1不限制 */
accountCount?: number; accountCount: number;
/** 租户状态0正常 1停用 */ /** 租户状态0正常 1停用 */
status?: string; status: string;
/** 删除标志0代表存在 2代表删除 */ /** 删除标志0代表存在 2代表删除 */
delFlag: string; delFlag: string;
} }
@ -159,39 +178,39 @@ declare namespace Api {
type Menu = Common.CommonRecord<{ type Menu = Common.CommonRecord<{
/** 菜单 ID */ /** 菜单 ID */
menuId?: CommonType.IdType; menuId: CommonType.IdType;
/** 父菜单 ID */ /** 父菜单 ID */
parentId?: CommonType.IdType; parentId: CommonType.IdType;
/** 菜单名称 */ /** 菜单名称 */
menuName?: string; menuName: string;
/** 显示顺序 */ /** 显示顺序 */
orderNum?: number; orderNum: number;
/** 路由地址 */ /** 路由地址 */
path?: string; path: string;
/** 组件路径 */ /** 组件路径 */
component?: string; component: string;
/** 路由参数 */ /** 路由参数 */
queryParam?: string; queryParam: string;
/** 是否为外链0是 1否 2iframe */ /** 是否为外链0是 1否 2iframe */
isFrame?: IsMenuFrame; isFrame: IsMenuFrame;
/** 是否缓存0缓存 1不缓存 */ /** 是否缓存0缓存 1不缓存 */
isCache?: Common.YesOrNoStatus; isCache: Common.YesOrNoStatus;
/** 菜单类型M目录 C菜单 F按钮 */ /** 菜单类型M目录 C菜单 F按钮 */
menuType?: MenuType; menuType: MenuType;
/** 显示状态0显示 1隐藏 */ /** 显示状态0显示 1隐藏 */
visible?: Common.VisibleStatus; visible: Common.VisibleStatus;
/** 菜单状态0正常 1停用 */ /** 菜单状态0正常 1停用 */
status?: Common.EnableStatus; status: Common.EnableStatus;
/** 权限标识 */ /** 权限标识 */
perms?: string; perms: string;
/** 菜单图标 */ /** 菜单图标 */
icon?: string; icon: string;
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 父菜单名称 */ /** 父菜单名称 */
parentName?: string; parentName: string;
/** 子菜单 */ /** 子菜单 */
children?: MenuList; children: MenuList;
}>; }>;
/** menu list */ /** menu list */
@ -201,57 +220,59 @@ declare namespace Api {
type MenuSearchParams = CommonType.RecordNullable<Pick<Menu, 'menuName' | 'status' | 'menuType' | 'parentId'>>; type MenuSearchParams = CommonType.RecordNullable<Pick<Menu, 'menuName' | 'status' | 'menuType' | 'parentId'>>;
/** menu operate params */ /** menu operate params */
type MenuOperateParams = Pick< type MenuOperateParams = CommonType.RecordNullable<
Menu, Pick<
| 'menuId' Menu,
| 'menuName' | 'menuId'
| 'parentId' | 'menuName'
| 'orderNum' | 'parentId'
| 'path' | 'orderNum'
| 'component' | 'path'
| 'queryParam' | 'component'
| 'isFrame' | 'queryParam'
| 'isCache' | 'isFrame'
| 'menuType' | 'isCache'
| 'visible' | 'menuType'
| 'status' | 'visible'
| 'perms' | 'status'
| 'icon' | 'perms'
| 'remark' | 'icon'
| 'remark'
>
>; >;
/** 字典类型 */ /** 字典类型 */
type DictType = Common.CommonRecord<{ type DictType = Common.CommonRecord<{
/** 字典主键 */ /** 字典主键 */
dictId?: number; dictId: number;
/** 字典名称 */ /** 字典名称 */
dictName?: string; dictName: string;
/** 字典类型 */ /** 字典类型 */
dictType?: string; dictType: string;
/** 备注 */ /** 备注 */
remark?: string; remark: string;
}>; }>;
/** 字典数据 */ /** 字典数据 */
type DictData = Common.CommonRecord<{ type DictData = Common.CommonRecord<{
/** 样式属性(其他样式扩展) */ /** 样式属性(其他样式扩展) */
cssClass?: string; cssClass: string;
/** 字典编码 */ /** 字典编码 */
dictCode?: number; dictCode: number;
/** 字典标签 */ /** 字典标签 */
dictLabel?: string; dictLabel: string;
/** 字典排序 */ /** 字典排序 */
dictSort?: number; dictSort: number;
/** 字典类型 */ /** 字典类型 */
dictType?: string; dictType: string;
/** 字典键值 */ /** 字典键值 */
dictValue?: string; dictValue: string;
/** 是否默认Y是 N否 */ /** 是否默认Y是 N否 */
isDefault?: string; isDefault: string;
/** 表格回显样式 */ /** 表格回显样式 */
listClass?: string; listClass: string;
/** 备注 */ /** 备注 */
remark?: string; remark: string;
}>; }>;
} }
} }

View File

@ -11,8 +11,10 @@ declare module 'vue' {
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default'] BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
BooleanTag: typeof import('./../components/custom/boolean-tag.vue')['default'] BooleanTag: typeof import('./../components/custom/boolean-tag.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
copy: typeof import('./../components/custom/dict-select copy.vue')['default']
CountTo: typeof import('./../components/custom/count-to.vue')['default'] CountTo: typeof import('./../components/custom/count-to.vue')['default']
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default'] DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
DictRadio: typeof import('./../components/custom/dict-radio.vue')['default']
DictSelect: typeof import('./../components/custom/dict-select.vue')['default'] DictSelect: typeof import('./../components/custom/dict-select.vue')['default']
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default'] ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
FullScreen: typeof import('./../components/common/full-screen.vue')['default'] FullScreen: typeof import('./../components/common/full-screen.vue')['default']

View File

@ -25,6 +25,7 @@ declare module "@elegant-router/types" {
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"system": "/system"; "system": "/system";
"system_menu": "/system/menu"; "system_menu": "/system/menu";
"system_user": "/system/user";
"tool": "/tool"; "tool": "/tool";
"tool_gen": "/tool/gen"; "tool_gen": "/tool/gen";
}; };
@ -89,6 +90,7 @@ declare module "@elegant-router/types" {
| "login" | "login"
| "home" | "home"
| "system_menu" | "system_menu"
| "system_user"
| "tool_gen" | "tool_gen"
>; >;

View File

@ -31,7 +31,7 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
interface Emits { interface Emits {
(e: 'submitted', menuType?: Api.System.MenuType): void; (e: 'submitted', menuType: Api.System.MenuType): void;
} }
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
@ -45,7 +45,7 @@ const { options: enableStatusOptions } = useDict('sys_normal_disable');
const iconType = ref<Api.System.IconType>('1'); const iconType = ref<Api.System.IconType>('1');
const { formRef, validate, restoreValidation } = useNaiveForm(); const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules(); const { createRequiredRule } = useFormRules();
const queryList = ref<{ key: string; value: string }[]>([]); const queryList = ref<{ key: string; value: string }[]>([]);
const drawerTitle = computed(() => { const drawerTitle = computed(() => {
@ -82,10 +82,10 @@ function createDefaultModel(): Model {
type RuleKey = Extract<keyof Model, 'menuName' | 'orderNum' | 'path' | 'component'>; type RuleKey = Extract<keyof Model, 'menuName' | 'orderNum' | 'path' | 'component'>;
const rules: Record<RuleKey, App.Global.FormRule> = { const rules: Record<RuleKey, App.Global.FormRule> = {
menuName: { ...defaultRequiredRule, message: '菜单名称不能为空' }, menuName: createRequiredRule('菜单名称不能为空'),
orderNum: { ...defaultRequiredRule, type: 'number', message: '菜单排序不能为空' }, orderNum: createRequiredRule('菜单排序不能为空'),
path: { ...defaultRequiredRule, message: '路由地址不能为空' }, path: createRequiredRule('路由地址不能为空'),
component: { ...defaultRequiredRule, message: '组件路径不能为空' } component: createRequiredRule('组件路径不能为空')
}; };
const isBtn = computed(() => model.menuType === 'F'); const isBtn = computed(() => model.menuType === 'F');
@ -210,7 +210,7 @@ async function handleSubmit() {
} }
closeDrawer(); closeDrawer();
emit('submitted', menuType); emit('submitted', menuType!);
} }
watch(visible, () => { watch(visible, () => {
@ -344,7 +344,7 @@ const FormTipComponent = defineComponent({
ignore-path-change ignore-path-change
:show-label="false" :show-label="false"
:path="`query[${index}].key`" :path="`query[${index}].key`"
:rule="{ ...defaultRequiredRule, validator: value => isNotNull(value) }" :rule="{ ...createRequiredRule('请输入 Key'), validator: value => isNotNull(value) }"
> >
<NInput v-model:value="queryList[index].key" placeholder="Key" @keydown.enter.prevent /> <NInput v-model:value="queryList[index].key" placeholder="Key" @keydown.enter.prevent />
</NFormItem> </NFormItem>
@ -354,7 +354,7 @@ const FormTipComponent = defineComponent({
ignore-path-change ignore-path-change
:show-label="false" :show-label="false"
:path="`query[${index}].value`" :path="`query[${index}].value`"
:rule="{ ...defaultRequiredRule, validator: value => isNotNull(value) }" :rule="{ ...createRequiredRule('请输入 Value'), validator: value => isNotNull(value) }"
> >
<NInput v-model:value="queryList[index].value" placeholder="Value" @keydown.enter.prevent /> <NInput v-model:value="queryList[index].value" placeholder="Value" @keydown.enter.prevent />
</NFormItem> </NFormItem>

View File

@ -0,0 +1,179 @@
<script setup lang="tsx">
import { NButton, NPopconfirm } from 'naive-ui';
import { fetchBatchDeleteUser, fetchGetUserList } from '@/service/api/system';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserSearch from './modules/user-search.vue';
defineOptions({
name: 'UserList'
});
const appStore = useAppStore();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetUserList,
apiParams: {
pageNum: 1,
pageSize: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
deptId: null,
userName: null,
nickName: null,
phonenumber: null,
status: null,
params: {}
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'deptId',
title: '部门',
align: 'center',
minWidth: 120
},
{
key: 'userName',
title: '用户名称',
align: 'center',
minWidth: 120
},
{
key: 'nickName',
title: '用户昵称',
align: 'center',
minWidth: 120
},
{
key: 'phonenumber',
title: '手机号码',
align: 'center',
minWidth: 120
},
{
key: 'status',
title: '帐号状态',
align: 'center',
minWidth: 120
},
{
key: 'createTime',
title: '创建时间',
align: 'center',
minWidth: 120
},
{
key: 'remark',
title: '备注',
align: 'center',
minWidth: 120
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.userId!)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.userId!)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteUser(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(userId: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteUser([userId]);
if (error) return;
onDeleted();
}
async function edit(userId: CommonType.IdType) {
handleEdit('userId', userId);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<UserSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="用户信息列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.userId"
:pagination="mobilePagination"
class="sm:h-full"
/>
<UserOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,175 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { fetchCreateUser, fetchUpdateUser } from '@/service/api/system';
defineOptions({
name: 'UserOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.System.User | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增用户信息',
edit: '编辑用户信息'
};
return titles[props.operateType];
});
type Model = Api.System.UserOperateParams;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
deptId: null,
userName: '',
nickName: '',
email: '',
phonenumber: '',
sex: '',
password: '',
status: '',
remark: ''
};
}
type RuleKey = Extract<keyof Model, 'userName' | 'nickName' | 'password' | 'status'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
userName: createRequiredRule('用户名称不能为空'),
nickName: createRequiredRule('用户昵称不能为空'),
password: createRequiredRule('密码不能为空'),
status: createRequiredRule('帐号状态不能为空')
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const { deptId, userName, nickName, email, phonenumber, sex, password, status, remark } = model;
const { error } = await fetchCreateUser({
deptId,
userName,
nickName,
email,
phonenumber,
sex,
password,
status,
remark
});
if (error) return;
}
if (props.operateType === 'edit') {
const { userId, deptId, userName, nickName, email, phonenumber, sex, password, status, remark } = model;
const { error } = await fetchUpdateUser({
userId,
deptId,
userName,
nickName,
email,
phonenumber,
sex,
password,
status,
remark
});
if (error) return;
}
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem label="部门" path="deptId">
<!-- <NInput v-model:value="model.deptId" placeholder="请输入部门" /> -->
</NFormItem>
<NFormItem label="用户名称" path="userName">
<NInput v-model:value="model.userName" placeholder="请输入用户名称" />
</NFormItem>
<NFormItem label="用户昵称" path="nickName">
<NInput v-model:value="model.nickName" placeholder="请输入用户昵称" />
</NFormItem>
<NFormItem label="用户邮箱" path="email">
<NInput v-model:value="model.email" placeholder="请输入用户邮箱" />
</NFormItem>
<NFormItem label="手机号码" path="phonenumber">
<NInput v-model:value="model.phonenumber" placeholder="请输入手机号码" />
</NFormItem>
<NFormItem label="用户性别" path="sex">
<DictRadio v-model:value="model.sex" dict-code="sys_user_sex" />
</NFormItem>
<NFormItem label="密码" path="password">
<NInput v-model:value="model.password" placeholder="请输入密码" />
</NFormItem>
<NFormItem label="帐号状态" path="status">
<NInput v-model:value="model.status" placeholder="请输入帐号状态" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput v-model:value="model.remark" placeholder="请输入备注" />
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,87 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@/locales';
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'UserSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const dateRangeCreateTime = ref<[string, string]>();
const model = defineModel<Api.System.UserSearchParams>('model', { required: true });
async function reset() {
dateRangeCreateTime.value = undefined;
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
if (dateRangeCreateTime.value?.length) {
model.value.params!.beginCreateTime = dateRangeCreateTime.value[0];
model.value.params!.endCreateTime = dateRangeCreateTime.value[0];
}
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="用户名称" path="userName" class="pr-24px">
<NInput v-model:value="model.userName" placeholder="请输入用户名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="用户昵称" path="nickName" class="pr-24px">
<NInput v-model:value="model.nickName" placeholder="请输入用户昵称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="手机号码" path="phonenumber" class="pr-24px">
<NInput v-model:value="model.phonenumber" placeholder="请输入手机号码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="帐号状态" path="status" class="pr-24px">
<NSelect v-model:value="model.status" placeholder="请选择帐号状态" :options="[]" clearable />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="创建时间" path="createTime" class="pr-24px">
<NDatePicker
v-model:formatted-value="dateRangeCreateTime"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>

View File

@ -218,7 +218,7 @@ async function handleGenCode(row?: Api.Tool.GenTable) {
if (error) return; if (error) return;
window.$message?.success('生成成功'); window.$message?.success('生成成功');
} else { } else {
zip(`/tool/gen/batchGenCode?tableIdStr=${tableIds}`, `ruoyi-${new Date().getTime()}.zip`); zip(`/tool/gen/batchGenCode?tableIdStr=${tableIds}`, `RuoYi-${row?.tableId ? `${row.className}` : Date.now()}.zip`);
} }
} }

View File

@ -269,6 +269,7 @@ const columns: NaiveUI.TableColumn<Api.Tool.GenTableColumn>[] = [
consistent-menu-width={false} consistent-menu-width={false}
render-label={renderLabel} render-label={renderLabel}
render-tag={renderTag} render-tag={renderTag}
clearable
/> />
); );
} }