实现效果
PC端 表格显示效果
PE端 表格显示效果
展开信息
编辑和更新对话框
重置密码对话框
删除用户
查询
代码实现
用户视图组件
<!--
* 用户管理 视图组件
*
* @author tuuuuuun
* @version v1.0
* @since 2024/2/2 3:43
-->
<script lang="ts" setup>
import { useTableSharedMeta } from './useTableMeta';
import UserModal from './components/UserModal.vue';
import DelModal from './components/DelUserModal.vue';
import UserTable from './components/UserTable.vue';
import { useRequest } from '#imports';
import { getUserInfoApi } from '~/api/user';
defineOptions({
name: 'sys-user',
});
// 布局响应
const { isTablet, isDesktop } = useLayout();
// 用户更新,创建对话框是否显示
const userModalVisible = ref(false);
// 用户删除对话框是否显示
const delUserModalVisible = ref(false);
// 用户表格
const userTableRef = ref<InstanceType<typeof UserTable>>();
// 关系 store , 随着组件销毁而销毁
const { tableLoading, tableRefresh, tableSize, tableMap, selectedKeys } =
useTableSharedMeta();
// 用户更新,创建对话框
const userModalRef = ref<InstanceType<typeof UserModal>>();
const currentElement = useCurrentElement<HTMLElement>();
const { isFullscreen, toggle: toggleFullscreen } =
useFullscreen(currentElement);
/**
* [button] 创建按钮创建用户
*/
const onCreateUser = () => {
userModalVisible.value = true;
};
/**
* 更新用户信息
*/
// 获取 用户信息请求
const { runAsync: userInfoRunAsync, loading: userInfoLoading } =
useRequest(getUserInfoApi);
const onEditUser = async (uid: string) => {
const userEntity = await userInfoRunAsync(uid);
// 加载用户信息
userModalRef.value!.loadUserInfo(userEntity);
};
</script>
<template>
<a-card
:body-style="{
padding: isTablet ? '0.75rem 0.7rem' : '1rem',
}"
:bordered="false"
class="drop-shadow-sm! rounded-lg!"
>
<UserModal
ref="userModalRef"
v-model:visible="userModalVisible"
class="z-100"
/>
<DelModal v-model="delUserModalVisible" />
<a-space class="pb-2.5 justify-between" fill>
<a-space>
<a-button
:class="{ 'arco-btn-only-icon': isTablet }"
:size="'small'"
type="primary"
@click="onCreateUser"
>
<span v-if="!isTablet">创建用户</span>
<template #icon>
<i class="i-gg:user-add" />
</template>
</a-button>
<a-button
v-if="selectedKeys.length > 0"
:class="{ 'arco-btn-only-icon': isTablet }"
:size="'small'"
:status="'danger'"
@click="delUserModalVisible = true"
>
<span v-if="!isTablet">删除用户({{ selectedKeys.length }})</span>
<template #icon>
<i class="i-tabler:trash" />
</template>
</a-button>
</a-space>
<a-space>
<a-button
:class="{ 'arco-btn-only-icon': isTablet }"
:loading="tableLoading"
:size="'small'"
@click="tableRefresh"
>
<span v-if="!isTablet">刷新</span>
<template #icon>
<i class="i-tabler:refresh" />
</template>
</a-button>
<a-select v-model="tableSize" :size="'small'">
<template #label="{ data }">
<a-space>
<i class="i-tabler:arrow-autofit-height" />
<span>{{ data.label }}</span>
</a-space>
</template>
<a-option v-for="(value, key) in tableMap" :key="key" :value="key">
{{ value }}
</a-option>
</a-select>
<a-button
:class="{ 'arco-btn-only-icon': isTablet }"
:size="'small'"
:type="isFullscreen ? 'primary' : 'secondary'"
@click="toggleFullscreen"
>
<span v-if="!isTablet">全屏</span>
<template #icon>
<i class="i-tabler-arrows-maximize" />
</template>
</a-button>
</a-space>
</a-space>
<UserTable
ref="userTableRef"
:edit-loading="userInfoLoading"
@on-edit-user="onEditUser"
/>
</a-card>
</template>
<style lang="scss" scoped>
:deep(.arco-tag-size-medium) {
font-size: 0.8rem;
}
:deep(.arco-table-cell-fixed-expand) {
padding: 0 !important;
}
:deep(.arco-select-view-single) {
width: 7rem;
}
:deep(.arco-table-cell) {
@apply px-2 lg:px-3;
}
</style>
表格渲染
定义 useTableMeta [SharedComposable]
- pages/sys/user/useTableMeta.ts
使用 vueuse createSharedComposable 持久在视图组件生命周期中,随着组件销毁而销毁。 便于组件向下传递使用。
import type { PaginationProps, TableColumnData } from '@arco-design/web-vue';
import { toNumber } from 'lodash-es';
import type { TableRowSelection } from '@arco-design/web-vue/es/table/interface';
import { usePagination } from '#imports';
import { getUserPageApi } from '~/api/user';
export const useTableMeta = () => {
const tableColumns = ref<TableColumnData[]>([
{
title: '头像',
dataIndex: 'avatarUrl',
slotName: 'avatarUrl',
width: 40,
align: 'center',
},
{
title: '用户账号',
dataIndex: 'username',
slotName: 'username',
width: 120,
align: 'left',
},
{
title: '用户名称',
dataIndex: 'nickName',
slotName: 'nickName',
width: 120,
align: 'center',
},
{
title: '邮件地址',
dataIndex: 'email',
slotName: 'email',
align: 'center',
width: 180,
},
{
title: '性别',
dataIndex: 'gender.label',
slotName: 'gender',
width: 80,
align: 'center',
},
{
title: '地址名称',
dataIndex: 'address',
slotName: 'address',
width: 160,
align: 'center',
},
{
title: '状态',
dataIndex: 'status.label',
slotName: 'status',
align: 'center',
width: 50,
},
{
title: '创建于',
dataIndex: 'createAt',
slotName: 'createAt',
width: 100,
align: 'center',
},
{
title: '操作',
dataIndex: 'operation',
slotName: 'operation',
width: 50,
align: 'center',
fixed: 'right',
},
]);
const { isTablet } = useLayout();
const {
data,
loading,
refresh,
pageSize,
total,
current,
changePageSize,
changeCurrent,
} = usePagination(getUserPageApi, {
manual: false,
defaultParams: [
{
current: 1,
size: 8,
},
],
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
totalKey: 'total',
},
});
const rowSelection: TableRowSelection = {
type: 'checkbox',
showCheckedAll: true,
width: 36,
};
// 选择用户 user id
const selectedKeys = ref<string[]>([]);
// 选择更新用户状态 [那个 button 需要显示加载状态]
const selectLoadingKey = ref('');
const tableData = computed(() => {
return data.value?.records || [];
});
const tablePagination = computed(() => {
return <PaginationProps>{
current: current.value,
pageSize: pageSize.value,
total: toNumber(total.value),
showTotal: true,
showJumper: true,
showPageSize: true,
pageSizeOptions: [5, 8, 10, 15, 20],
simple: isTablet.value,
};
});
const tableSize = ref<'mini' | 'medium' | 'large' | 'small'>('small');
const tableMap = {
large: '宽松',
medium: '中等',
small: '紧凑',
mini: '迷你',
};
return {
tableColumns,
tableData,
tableLoading: loading,
tableRefresh: refresh,
tablePagination,
changePageSize,
changeCurrent,
tableSize,
tableMap,
rowSelection,
selectedKeys,
selectLoadingKey,
};
};
export const useTableSharedMeta = createSharedComposable(useTableMeta);
表格组件
<!--
* 用户表格
*
* @author tuuuuuun
* @version v1.0
* @since 2024/2/2 13:43
-->
<script lang="ts" setup>
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface';
import { useTableSharedMeta } from '../useTableMeta';
import PasswordModal from './RestUserPasswordModal.vue';
import { SYS_STATUS } from '~/key/dict';
import { patchUserStatusApi } from '~/api/user';
import { useRequest } from '#imports';
defineProps<{
editLoading: boolean;
}>();
// 响应布局
const { isTablet } = useLayout();
const emits = defineEmits<{
/**
* 点击编辑用户
*
* @param e 事件名称
* @param userId 用户 id
*/
(e: 'onEditUser', userId: string): void;
}>();
const {
tableColumns,
tableData,
tableLoading,
tableRefresh,
tablePagination,
changePageSize,
changeCurrent,
tableSize,
rowSelection,
selectedKeys,
selectLoadingKey,
} = useTableSharedMeta();
const expandedKeys = ref<string[]>([]);
/**
* 更新用户信息
*/
const { run: patchUserStatus, loading: patchUserStatusLoading } = useRequest(
patchUserStatusApi,
{
onSuccess: () => {
// 刷新表格
tableRefresh();
Message.success('修改用户状态成功');
},
onError: (error) => {
Message.error(`修改用户状态失败: ${error.message}`);
},
onAfter() {
// 重置选择更新用户状态的key
selectLoadingKey.value = '';
},
}
);
/**
* 用户状态修改
*
* @param userId 用户 id
* @param status 用户状态
*/
const onChangeUserStatus = (userId: string, status: string) => {
selectLoadingKey.value = userId;
patchUserStatus(userId, status);
};
/**
* 展开用户信息
*
* @param userId 用户 id
*/
const onExpand = (userId: string) => {
const findIndex = expandedKeys.value.findIndex((item) => item === userId);
if (findIndex !== -1) {
expandedKeys.value = [];
return;
}
expandedKeys.value = [userId];
};
/**
* 用户描述
*
* @param user
*/
const toDescriptions = (user: Record<string, any>): DescData[] => {
return Object.keys(user).map((key) => {
const element = user[key];
return {
value: (element.label ? element.label : element) as string,
label: key + '',
};
});
};
/**
* 编辑用户
*
* @param uid 用户 id
*/
const onEditUser = (uid: string) => {
selectLoadingKey.value = uid;
emits('onEditUser', uid);
};
</script>
<template>
<a-table
v-model:selected-keys="selectedKeys"
:bordered="false"
:columns="tableColumns"
:data="tableData"
:expanded-keys="expandedKeys"
:loading="tableLoading"
:page-position="'bottom'"
:pagination="tablePagination"
:row-selection="rowSelection"
:size="tableSize"
default-expand-all-rows
row-key="userId"
scrollbar
@page-change="changeCurrent"
@page-size-change="changePageSize"
>
<template #expand-row="{ record }">
<a-descriptions
:bordered="true"
:column="isTablet ? 1 : 4"
:data="toDescriptions(record)"
:layout="isTablet ? 'horizontal' : 'inline-vertical'"
:size="'mini'"
:table-layout="'fixed'"
class="py-2.5 px-0"
style="background-color: var(--color-bg-2)"
/>
</template>
<template #avatarUrl="{ record }">
<a-avatar
:image-url="record.avatarUrl"
:shape="'square'"
:size="isTablet ? 34 : 36"
@click="onExpand(record.userId)"
/>
</template>
<template #status="{ record }">
<a-spin
:loading="patchUserStatusLoading && selectLoadingKey === record.userId"
>
<SysDictSelect
:dict-type="SYS_STATUS"
:disabled="record.userId <= 1"
:model-value="record.status?.code"
:size="tableSize"
@change="onChangeUserStatus(record.userId, $event as string)"
/>
</a-spin>
</template>
<template #username="{ record }">
<NuxtLink v-if="record.userId" :to="`/sys/user/${record.userId}`">
<a-tag color="arcoblue">
{{ record.username }}
</a-tag>
</NuxtLink>
</template>
<template #createAt="{ record }">
<span>
{{ $dayjs(record.createAt).fromNow() }}
</span>
</template>
<template #gender="{ record }">
<a-tag v-if="record.gender?.label">
{{ record.gender.label }}
</a-tag>
</template>
<template #address="{ record }">
<a-tooltip v-if="record.address" :content="record.address">
<span>
{{ record.address.split(' ')[0] }}
</span>
</a-tooltip>
</template>
<template #operation="{ record }">
<a-button-group :size="tableSize">
<a-space :size="'mini'">
<a-button
:loading="selectLoadingKey === record.userId && editLoading"
@click="onEditUser(record.userId)"
>
<template #icon>
<i class="i-tabler:pencil-minus" />
</template>
</a-button>
<a-tooltip content="重置密码">
<PasswordModal
:user-id="record.userId"
:user-name="record.username"
/>
</a-tooltip>
</a-space>
</a-button-group>
</template>
</a-table>
</template>
<style scoped></style>
删除用户组件
<!--
* 删除用户对话框
*
* @author tuuuuuun
* @version v1.0
* @since 2024/2/2 13:43
-->
<script lang="ts" setup>
import { useRequest } from '#imports';
import { delUserByIdsApi } from '~/api/user';
import { useTableSharedMeta } from '~/pages/sys/user/useTableMeta';
const props = defineProps<{
modelValue?: boolean;
}>();
// 当前弹窗是否可见
const visible = useVModel(props, 'modelValue');
// 共享表格数据
const { tableRefresh, selectedKeys, tableData } = useTableSharedMeta();
/**
* 删除用户请求
*/
const { run: delUserByIdsRun, loading: patchUserPasswordLoading } =
useRequest(delUserByIdsApi, {
onSuccess: () => {
Message.success('删除用户成功');
visible.value = false;
// 清空选择keys
selectedKeys.value = [];
// 刷新表格数据
tableRefresh();
},
onError: ({ message }) => {
Message.error(`删除用户失败: ${message}`);
visible.value = false;
// 清空选择keys
selectedKeys.value = [];
},
});
/**
* 确认删除
*/
const onConfirm = () => {
delUserByIdsRun(unref(selectedKeys));
};
/**
* 删除用户名称
*/
const delUserNamesContent = computed(() => {
return tableData.value
.filter((item) => selectedKeys.value.includes(item.userId))
.map((item) => item.nickName)
.join(', ');
});
</script>
<template>
<a-modal
:cancel-button-props="{
size: 'small',
}"
:footer="true"
:message-type="'warning'"
:ok-button-props="{
size: 'small',
}"
:ok-loading="patchUserPasswordLoading"
:title-align="'start'"
:visible="visible"
:width="300"
class=""
draggable
simple
title="删除用户"
unmount-on-close
@cancel="visible = false"
@ok="onConfirm"
>
确定删除 "{{ delUserNamesContent }}" 吗?
</a-modal>
</template>
<style scoped></style>
重置密码组件
<!--
* 重置用户密码对话框
*
* @author tuuuuuun
* @version v1.0
* @since 2024/2/2 13:43
-->
<script lang="ts" setup>
import type { FormInstance } from '@arco-design/web-vue';
import { formRules } from '~/pages/sys/formRules';
import { useRequest } from '#imports';
import { patchUserPasswordApi } from '~/api/user';
defineProps<{
userId?: string;
userName?: string;
}>();
// 窗口是否可见
const visible = ref(false);
// 表格
const formRef = ref<FormInstance>();
// 响应布局
const { isTablet } = useLayout();
// 表单数据
const formData = reactive({
password: '',
});
// 重置密码请求
const { loading: patchUserPasswordLoading, run: patchUserPasswordRun } =
useRequest(patchUserPasswordApi, {
onSuccess: () => {
onModalCancel();
Message.success('重置密码成功');
},
onError: ({ message }) => {
onModalCancel();
Message.error(`重置密码失败: ${message}`);
},
});
/**
* 关闭弹窗
*/
const onModalCancel = () => {
visible.value = false;
formRef.value!.resetFields();
};
/**
* 重置密码按钮事件
*/
const onResetPassword = () => {
visible.value = true;
};
/**
* 前往用户详情页面
*/
const onToUserDetail = async (userId: string) => {
await navigateTo(`/sys/user/${userId}`);
// visible.value = false;
};
/**
* 确认重置密码请求
*/
const onRestPasswordConfirm = async (userId: string) => {
const validate = await formRef.value!.validate();
if (validate) {
Message.warning('请填写完整信息');
return;
}
// 发送请求
patchUserPasswordRun(userId, formData.password);
};
</script>
<template>
<div>
<a-button @click="onResetPassword">
<template #icon>
<i class="i-tabler:password" />
</template>
</a-button>
<a-modal
v-if="userId"
:footer="true"
:fullscreen="isTablet"
:ok-loading="patchUserPasswordLoading"
:title-align="'start'"
:visible="visible"
:width="360"
class=""
draggable
modal-class="py-4! px-4! lg:px-8!"
simple
title="重置密码"
unmount-on-close
@cancel="onModalCancel"
@ok="onRestPasswordConfirm(userId)"
>
<a-alert :type="'warning'" banner center class="mb-4" closable>
重置密码将会强制使
<a-link v-if="userId" @click="onToUserDetail(userId)">
{{ userName }}
</a-link>
账号下线
</a-alert>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:size="isTablet ? 'large' : 'medium'"
layout="vertical"
>
<a-form-item
:label="`重置 ${userName} 账号密码`"
:validate-trigger="'input'"
field="password"
hide-asterisk
>
<a-input-password
v-model="formData.password"
placeholder="请输入新密码"
>
<template #prefix>
<i class="i-tabler-lock" />
</template>
</a-input-password>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped></style>
创建、更新用户组件
<!--
* 用户更新,创建 对话框
*
* @author tuuuuuun
* @version v1.0
* @since 2024/2/2 13:53
-->
<script lang="ts" setup>
import type { FormInstance } from '@arco-design/web-vue';
import { useTableSharedMeta } from '../useTableMeta';
import { SYS_GENDER } from '~/key/dict';
import type { UserDto } from '~/types/dto/user';
import { formRulesNullable } from '~/pages/sys/rulesNullable';
import { formRules } from '~/pages/sys/formRules';
import { useRequest } from '#imports';
import { postCreateUserApi, postUserInfoApi } from '~/api/user';
const props = defineProps<{
visible?: boolean;
}>();
// 响应布局
const { isTablet } = useLayout();
// 共享 table 数据
const { tableRefresh } = useTableSharedMeta();
// 窗口是否可见
const visible = useVModel(props, 'visible');
// 表单
const formRef = ref<FormInstance>();
// 表单数据
const formData = ref<Partial<UserDto>>({});
/**
* 创建用户请求
*/
const { loading: createUserLoading, run: createUserRun } = useRequest(
postCreateUserApi,
{
onSuccess: () => {
// 创建成功 关闭窗口
onModalCancel();
// 提示消息
Message.success('创建成功');
tableRefresh();
},
onError: ({ message }) => {
Message.error(`创建用户失败: ${message}`);
},
}
);
/**
* 更新用户信息请求
*/
const { run: updateUserInfoRun, loading: updateUserInfoLoading } = useRequest(
postUserInfoApi,
{
onSuccess: () => {
// 提示消息
Message.success('更新用户信息成功');
visible.value = false;
// 刷新表格数据
tableRefresh();
},
onError: ({ message }) => {
Message.error(`更新用户信息失败: ${message}`);
},
}
);
/**
* 新建用户
*/
const onCreateUserSubmit = async () => {
// 校验表单
const validate = await formRef.value?.validate();
if (validate) {
Message.info({
id: 'create-user-submit',
content: '请完成表单填写',
});
return;
}
// 发送请求
createUserRun({ ...unref(formData) });
};
/**
* 关闭弹窗, 重置表单数据以及校验结果
*/
const onModalCancel = () => {
// 关闭弹窗,
visible.value = false;
// 重置表单数据以及校验结果
formRef.value!.resetFields();
formData.value = {};
};
/**
* 是否是新建用户
*/
const isCreateMode = computed<boolean>(() => {
return !formData.value?.userId;
});
/**
* 更新用户信息
*/
const onUpdateUserSubmit = () => {
updateUserInfoRun(formData.value?.userId as string, { ...unref(formData) });
};
/**
* 加载用户信息
*
* @param userEntity 用户信息
*/
const loadUserInfo = (userEntity: UserEntity) => {
const {
userId,
username,
avatarUrl,
email,
phoneNumber,
addressCode,
remark,
nickName,
gender,
birthday,
} = userEntity;
formData.value = {
userId,
username,
avatarUrl,
email,
phoneNumber,
addressCode,
remark,
nickName,
birthday,
gender: gender.code,
};
// 显示窗口
visible.value = true;
};
defineExpose({
/**
* 加载用户信息
*/
loadUserInfo,
});
</script>
<template>
<a-modal
:footer="true"
:fullscreen="isTablet"
:title="isCreateMode ? '新建用户' : '更新用户'"
:title-align="'start'"
:visible="visible"
:width="680"
draggable
modal-class="py-6! px-6! lg:px-10!"
simple
@cancel="onModalCancel"
>
<!-- {{ form }}-->
<a-form
ref="formRef"
:layout="'vertical'"
:model="formData"
:rules="formRulesNullable"
>
<div class="overflow-y-auto box-border overflow-x-hidden">
<a-row :gutter="isTablet ? 0 : 24">
<a-col :span="isTablet ? 24 : 12">
<a-form-item
class="relative"
extra="仅支持 image/png, image/jpeg 格式"
field="avatarUrl"
label="账号头像"
>
<i
class="i-banner-undraw_personal_info_re_ur1n w-[12rem] h-[6rem]"
/>
<UploadAvatar
v-model="formData.avatarUrl"
action-url="http://localhost:7000/my/upload/avatar"
class="absolute right-6"
/>
</a-form-item>
</a-col>
<a-col :span="isTablet ? 24 : 12">
<a-form-item
:field="isCreateMode ? 'username' : ''"
:hide-asterisk="!isCreateMode"
:rules="formRules.username"
label="登录账号"
>
<a-input
v-model="formData.username"
:disabled="!isCreateMode"
placeholder="用户登录账号"
>
<template #prefix>
<i class="i-tabler:user-bolt" />
</template>
</a-input>
</a-form-item>
<a-form-item
:field="isCreateMode ? 'password' : ''"
:hide-asterisk="!isCreateMode"
:rules="formRules.password"
label="用户密码"
>
<a-input-password
v-model="formData.password"
:disabled="!isCreateMode"
:placeholder="isCreateMode ? '用户密码' : '******'"
>
<template #prefix>
<i class="i-tabler:lock" />
</template>
</a-input-password>
</a-form-item>
</a-col>
<a-col :span="isTablet ? 24 : 12">
<a-form-item field="nickName" hide-asterisk label="用户名称">
<a-input v-model="formData.nickName" placeholder="用户名称">
<template #prefix>
<i class="i-tabler:pencil-check" />
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="isTablet ? 24 : 12">
<a-form-item field="birthday" label="出生日期">
<a-date-picker
v-model="formData.birthday"
:disabled-date="
(current: Date | undefined) =>
$dayjs(current).isAfter($dayjs())
"
:show-now-btn="false"
class="w-full"
placeholder="请输入出生日期"
>
<template #prefix>
<i class="i-tabler:calendar" />
</template>
<template #suffix-icon>
<span />
</template>
</a-date-picker>
</a-form-item>
</a-col>
<a-col :span="isTablet ? 24 : 12">
<a-form-item field="email" hide-asterisk label="用户邮件">
<EmailInput v-model="formData.email" />
</a-form-item>
</a-col>
<a-col :span="isTablet ? 24 : 12">
<a-form-item field="gender" label="性别">
<SysDictSelect
v-model="formData.gender"
:dict-type="SYS_GENDER"
all
other-name="未知"
placeholder="请选择性别"
/>
</a-form-item>
</a-col>
<a-col :span="isTablet ? 24 : 12">
<a-form-item field="addressCode" label="所在地区">
<AreaCascader
v-model="formData.addressCode"
placeholder="请选择所在地区"
/>
</a-form-item>
<a-form-item field="phoneNumber" label="手机号">
<a-input
v-model="formData.phoneNumber"
placeholder="请输入手机号码"
>
<template #prefix>
<i class="i-tabler:phone" />
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="isTablet ? 24 : 12">
<a-form-item field="remark" label="个人简介">
<a-textarea
v-model="formData.remark"
:auto-size="{
minRows: 4,
maxRows: 4,
}"
:max-length="120"
placeholder="请输入个人简介"
show-word-limit
/>
</a-form-item>
</a-col>
</a-row>
</div>
</a-form>
<template #footer>
<a-space class="w-full justify-end pt-4">
<a-space>
<a-button @click="onModalCancel">取消</a-button>
<a-button v-if="isCreateMode" @click="formRef?.resetFields()">
重置表单
</a-button>
<a-button
v-if="isCreateMode"
:loading="createUserLoading"
:type="'primary'"
@click="onCreateUserSubmit"
>
<span>创建用户</span>
<template #icon>
<i class="i-tabler:users-plus" />
</template>
</a-button>
<a-button
v-else
:loading="updateUserInfoLoading"
:type="'primary'"
@click="onUpdateUserSubmit"
>
<span>更新用户</span>
<template #icon>
<i class="i-tabler:users-plus" />
</template>
</a-button>
</a-space>
</a-space>
</template>
</a-modal>
</template>
<style lang="scss" scoped>
:deep(.custom-upload-avatar img),
:deep(.arco-upload-picture-card),
:deep(.arco-upload-list-picture-mask) {
@apply rounded-full;
}
</style>
查询组件
<!--
* 搜索用户表单
*
* @author tuuuuuun
* @version v1.0
* @since 2024/2/7 13:52
-->
<script lang="ts" setup>
import type { FormInstance } from '@arco-design/web-vue';
import { useTableSharedMeta } from '../useTableMeta';
import type { UserQuery } from '~/types/query/user';
import { SYS_GENDER, SYS_STATUS } from '~/key/dict';
const { isDesktop, isTablet } = useLayout();
const { loadTableRun, tableLoading } = useTableSharedMeta();
const formRef = ref<FormInstance>();
const formData = ref<Partial<UserQuery>>({});
const activeKey = ref<string[]>([]);
/**
* 触发查询
*/
const onSearch = () => {
loadTableRun({ ...unref(formData) });
};
/**
* 折叠/展开
*/
const onCollapse = () => {
if (activeKey.value.length > 0) {
activeKey.value = [];
return;
}
activeKey.value = ['1'];
};
</script>
<template>
<a-collapse
:active-key="activeKey"
:bordered="false"
:show-expand-icon="false"
>
<a-collapse-item key="1">
<template #header>
<a-space class="cursor-pointer" @click="onCollapse">
<i class="i-tabler:list-search text-1.1rem" />
<span>用户查询</span>
</a-space>
</template>
<template #extra>
<a-space>
<a-button
:class="{ 'arco-btn-only-icon': !isDesktop }"
:loading="tableLoading"
:shape="'round'"
:size="'small'"
type="primary"
@click.stop="onSearch"
>
<span v-if="isDesktop">搜索用户</span>
<template #icon>
<i class="i-tabler:input-search" />
</template>
</a-button>
<a-button
:class="{ 'arco-btn-only-icon': !isDesktop }"
:shape="'round'"
:size="'small'"
@click.stop="formRef?.resetFields() || onSearch()"
>
<span v-if="isDesktop">重置表单</span>
<template #icon>
<i class="i-tabler:refresh" />
</template>
</a-button>
</a-space>
</template>
<a-form
ref="formRef"
:label-col-props="{ span: 6 }"
:layout="isTablet ? 'vertical' : 'horizontal'"
:model="formData"
:wrapper-col-props="{ span: 18 }"
>
<a-row :gutter="15">
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="username" label="用户账号">
<a-input
v-model="formData.username"
placeholder="输入查询用户账号"
/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="nickName" label="用户名称">
<a-input
v-model="formData.nickName"
placeholder="输入查询用户名称"
/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="gender" label="用户性别">
<SysDictSelect
v-model="formData.gender"
:dict-type="SYS_GENDER"
all
placeholder="输入查询用户性别"
/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="email" label="用户邮箱">
<EmailInput
v-model="formData.email"
placeholder="输入查询用户邮箱"
/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="addressCode" label="所在地区">
<AreaCascader
v-model="formData.addressCode"
placeholder="输入查询用户所在地区"
/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="isStatus" label="用户状态">
<SysDictSelect
v-model="formData.isStatus"
:dict-type="SYS_STATUS"
all
placeholder="输入查询用户状态"
/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="startTime" label="创建日期">
<a-date-picker
v-model="formData.startTime"
class="w-full"
format="YYYY-MM-DD"
placeholder="输入查询开始日期"
/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xl="6">
<a-form-item field="endTime" label="结束日期">
<a-date-picker
v-model="formData.endTime"
class="w-full"
format="YYYY-MM-DD"
placeholder="输入查询结束日期"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-collapse-item>
</a-collapse>
</template>
<style lang="scss" scoped>
// 展开容器-头部
:deep(.arco-collapse-item-header-left) {
@apply mb-1.5 pl-2 cursor-default;
}
// 展开容器-内容
:deep(.arco-collapse-item-content) {
background-color: var(--color-bg-2);
@apply px-1 lg:px-2;
}
// 小屏 单列为撑满
:deep(.arco-col) {
width: 100%;
}
</style>