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

View File

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

View File

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

View File

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

View File

@ -26,8 +26,8 @@ const {
} = useTable({
apiFn: fetchGet${BusinessName}List,
apiParams: {
current: 1,
size: 10,
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
#foreach ($column in $columns)
@ -109,7 +109,7 @@ async function handleDelete(#foreach($column in $columns)#if($column.isPk == '1'
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);
}
</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">
import { ref, useAttrs } from 'vue';
import { useLoading } from '@sa/hooks';
import type { SelectOption, SelectProps } from 'naive-ui';
import { fetchGetDictTypeOption } from '@/service/api/system';
<script setup lang="ts">
import { useAttrs } from 'vue';
import type { SelectProps } from 'naive-ui';
import { useDict } from '@/hooks/business/dict';
defineOptions({ name: 'DictSelect' });
interface Props {
dictCode: string;
immediate?: boolean;
[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 options = ref<SelectOption[]>([]);
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();
const { options } = useDict(props.dictCode, props.immediate);
</script>
<template>
<NSelect
v-model:value="value"
:loading="loading"
:loading="!options.length"
:options="options"
:clear-filter-after-select="false"
v-bind="attrs"

View File

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

View File

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

View File

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

View File

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

View File

@ -22,5 +22,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
login: () => import("@/views/_builtin/login/index.vue"),
home: () => import("@/views/home/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"),
};

View File

@ -96,6 +96,15 @@ export const generatedRoutes: GeneratedRoute[] = [
localIcon: 'menu-tree-table',
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)?",
"system": "/system",
"system_menu": "/system/menu",
"system_user": "/system/user",
"tool": "/tool",
"tool_gen": "/tool/gen"
};

View File

@ -1,2 +1,3 @@
export * from './menu';
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 */
type CommonSearchParams<T = any> = Pick<Common.PaginatingCommonParams, 'pageNum' | 'pageSize'> &
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'pageNum' | 'pageSize'> &
CommonType.RecordNullable<{
orderByColumn: keyof T;
orderByColumn: string;
isAsc: 'asc' | 'desc';
params: { [key: string]: any };
}>;
@ -69,8 +69,7 @@ declare namespace Api {
type CommonTenantRecord<T = any> = {
/** record tenant id */
tenantId: string;
} & CommonRecord &
T;
} & CommonRecord<T>;
}
/**

View File

@ -13,32 +13,32 @@ declare namespace Api {
/** role */
type Role = Common.CommonRecord<{
/** 数据范围1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限 */
dataScope?: string;
dataScope: string;
/** 部门树选择项是否关联显示 */
deptCheckStrictly?: boolean;
deptCheckStrictly: boolean;
/** 用户是否存在此角色标识 默认不存在 */
flag?: boolean;
flag: boolean;
/** 菜单树选择项是否关联显示 */
menuCheckStrictly?: boolean;
menuCheckStrictly: boolean;
/** 备注 */
remark?: string;
/** 角色ID */
roleId?: number;
roleId: number;
/** 角色权限字符串 */
roleKey?: string;
roleKey: string;
/** 角色名称 */
roleName?: string;
roleName: string;
/** 显示顺序 */
roleSort?: number;
roleSort: number;
/** 角色状态0正常 1停用 */
status?: string;
status: string;
/** 是否管理员 */
superAdmin?: boolean;
superAdmin: boolean;
}>;
/** role search params */
type RoleSearchParams = CommonType.RecordNullable<
Pick<Role, 'roleName' | 'roleKey' | 'status'> & Common.CommonSearchParams<Role>
Pick<Role, 'roleName' | 'roleKey' | 'status'> & Common.CommonSearchParams
>;
/** role list */
@ -58,40 +58,59 @@ declare namespace Api {
/** user */
type User = Common.CommonTenantRecord<{
/** 用户ID */
userId?: CommonType.IdType;
userId: CommonType.IdType;
/** 部门ID */
deptId?: CommonType.IdType;
deptId: CommonType.IdType;
/** 部门名称 */
deptName: string;
/** 用户账号 */
userName?: string;
userName: string;
/** 用户昵称 */
nickName?: string;
nickName: string;
/** 用户类型sys_user系统用户 */
userType?: string;
userType: string;
/** 用户邮箱 */
email?: string;
email: string;
/** 手机号码 */
phonenumber?: string;
phonenumber: string;
/** 用户性别0男 1女 2未知 */
sex?: string;
sex: string;
/** 头像地址 */
avatar?: number;
avatar: string;
/** 密码 */
password?: string;
password: string;
/** 帐号状态0正常 1停用 */
status?: string;
status: string;
/** 删除标志0代表存在 2代表删除 */
delFlag?: string;
delFlag: string;
/** 最后登录IP */
loginIp?: string;
loginIp: string;
/** 最后登录时间 */
loginDate?: Date;
loginDate: Date;
/** 备注 */
remark?: string;
}>;
/** user search params */
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 */
@ -100,33 +119,33 @@ declare namespace Api {
/** tenant */
interface Tenant {
/** 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;
/** 租户套餐编号 */
packageId?: number;
packageId: number;
/** 过期时间 */
expireTime?: Date;
expireTime: Date;
/** 用户数量(-1不限制 */
accountCount?: number;
accountCount: number;
/** 租户状态0正常 1停用 */
status?: string;
status: string;
/** 删除标志0代表存在 2代表删除 */
delFlag: string;
}
@ -159,39 +178,39 @@ declare namespace Api {
type Menu = Common.CommonRecord<{
/** 菜单 ID */
menuId?: CommonType.IdType;
menuId: CommonType.IdType;
/** 父菜单 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 */
isFrame?: IsMenuFrame;
isFrame: IsMenuFrame;
/** 是否缓存0缓存 1不缓存 */
isCache?: Common.YesOrNoStatus;
isCache: Common.YesOrNoStatus;
/** 菜单类型M目录 C菜单 F按钮 */
menuType?: MenuType;
menuType: MenuType;
/** 显示状态0显示 1隐藏 */
visible?: Common.VisibleStatus;
visible: Common.VisibleStatus;
/** 菜单状态0正常 1停用 */
status?: Common.EnableStatus;
status: Common.EnableStatus;
/** 权限标识 */
perms?: string;
perms: string;
/** 菜单图标 */
icon?: string;
icon: string;
/** 备注 */
remark?: string;
/** 父菜单名称 */
parentName?: string;
parentName: string;
/** 子菜单 */
children?: MenuList;
children: MenuList;
}>;
/** menu list */
@ -201,57 +220,59 @@ declare namespace Api {
type MenuSearchParams = CommonType.RecordNullable<Pick<Menu, 'menuName' | 'status' | 'menuType' | 'parentId'>>;
/** menu operate params */
type MenuOperateParams = Pick<
Menu,
| 'menuId'
| 'menuName'
| 'parentId'
| 'orderNum'
| 'path'
| 'component'
| 'queryParam'
| 'isFrame'
| 'isCache'
| 'menuType'
| 'visible'
| 'status'
| 'perms'
| 'icon'
| 'remark'
type MenuOperateParams = CommonType.RecordNullable<
Pick<
Menu,
| 'menuId'
| 'menuName'
| 'parentId'
| 'orderNum'
| 'path'
| 'component'
| 'queryParam'
| 'isFrame'
| 'isCache'
| 'menuType'
| 'visible'
| 'status'
| 'perms'
| 'icon'
| 'remark'
>
>;
/** 字典类型 */
type DictType = Common.CommonRecord<{
/** 字典主键 */
dictId?: number;
dictId: number;
/** 字典名称 */
dictName?: string;
dictName: string;
/** 字典类型 */
dictType?: string;
dictType: string;
/** 备注 */
remark?: string;
remark: string;
}>;
/** 字典数据 */
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否 */
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']
BooleanTag: typeof import('./../components/custom/boolean-tag.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']
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']
ExceptionBase: typeof import('./../components/common/exception-base.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)?";
"system": "/system";
"system_menu": "/system/menu";
"system_user": "/system/user";
"tool": "/tool";
"tool_gen": "/tool/gen";
};
@ -89,6 +90,7 @@ declare module "@elegant-router/types" {
| "login"
| "home"
| "system_menu"
| "system_user"
| "tool_gen"
>;

View File

@ -31,7 +31,7 @@ interface Props {
const props = defineProps<Props>();
interface Emits {
(e: 'submitted', menuType?: Api.System.MenuType): void;
(e: 'submitted', menuType: Api.System.MenuType): void;
}
const emit = defineEmits<Emits>();
@ -45,7 +45,7 @@ const { options: enableStatusOptions } = useDict('sys_normal_disable');
const iconType = ref<Api.System.IconType>('1');
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const { createRequiredRule } = useFormRules();
const queryList = ref<{ key: string; value: string }[]>([]);
const drawerTitle = computed(() => {
@ -82,10 +82,10 @@ function createDefaultModel(): Model {
type RuleKey = Extract<keyof Model, 'menuName' | 'orderNum' | 'path' | 'component'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
menuName: { ...defaultRequiredRule, message: '菜单名称不能为空' },
orderNum: { ...defaultRequiredRule, type: 'number', message: '菜单排序不能为空' },
path: { ...defaultRequiredRule, message: '路由地址不能为空' },
component: { ...defaultRequiredRule, message: '组件路径不能为空' }
menuName: createRequiredRule('菜单名称不能为空'),
orderNum: createRequiredRule('菜单排序不能为空'),
path: createRequiredRule('路由地址不能为空'),
component: createRequiredRule('组件路径不能为空')
};
const isBtn = computed(() => model.menuType === 'F');
@ -210,7 +210,7 @@ async function handleSubmit() {
}
closeDrawer();
emit('submitted', menuType);
emit('submitted', menuType!);
}
watch(visible, () => {
@ -344,7 +344,7 @@ const FormTipComponent = defineComponent({
ignore-path-change
:show-label="false"
: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 />
</NFormItem>
@ -354,7 +354,7 @@ const FormTipComponent = defineComponent({
ignore-path-change
:show-label="false"
: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 />
</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;
window.$message?.success('生成成功');
} 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}
render-label={renderLabel}
render-tag={renderTag}
clearable
/>
);
}