feat: 用户管理新增导入
This commit is contained in:
parent
5de60e2a83
commit
62460bba90
@ -75,7 +75,7 @@ function handleExport() {
|
||||
</NPopconfirm>
|
||||
<NButton v-if="showExport" size="small" ghost @click="handleExport">
|
||||
<template #icon>
|
||||
<icon-ic-round-download class="text-icon" />
|
||||
<icon-material-symbols:download-2-rounded class="text-icon" />
|
||||
</template>
|
||||
导出
|
||||
</NButton>
|
||||
|
@ -11,6 +11,8 @@ defineOptions({
|
||||
|
||||
interface Props {
|
||||
action?: string;
|
||||
data?: Record<string, any>;
|
||||
defaultUpload?: boolean;
|
||||
showTip?: boolean;
|
||||
max?: number;
|
||||
accept?: string;
|
||||
@ -20,6 +22,8 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
action: `/resource/oss/upload`,
|
||||
data: undefined,
|
||||
defaultUpload: true,
|
||||
showTip: true,
|
||||
max: 5,
|
||||
accept: '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf',
|
||||
@ -29,6 +33,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const attrs: UploadProps = useAttrs();
|
||||
|
||||
const value = defineModel<CommonType.IdType[]>('value', { required: false, default: [] });
|
||||
|
||||
let fileNum = 0;
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
const needRelaodData = defineModel<boolean>('needRelaodData', {
|
||||
@ -39,6 +45,7 @@ watch(
|
||||
() => fileList.value,
|
||||
newValue => {
|
||||
needRelaodData.value = newValue.length > 0;
|
||||
value.value = newValue.map(item => item.id);
|
||||
}
|
||||
);
|
||||
|
||||
@ -126,11 +133,13 @@ async function handleRemove(file: UploadFileInfo) {
|
||||
v-bind="attrs"
|
||||
v-model:file-list="fileList"
|
||||
:action="`${baseURL}${action}`"
|
||||
:data="data"
|
||||
:headers="headers"
|
||||
:max="max"
|
||||
:accept="accept"
|
||||
multiple
|
||||
:multiple="max > 1"
|
||||
directory-dnd
|
||||
:default-upload="defaultUpload"
|
||||
:list-type="uploadType === 'image' ? 'image-card' : 'text'"
|
||||
:is-error-state="isErrorState"
|
||||
@finish="handleFinish"
|
||||
@ -145,12 +154,12 @@ async function handleRemove(file: UploadFileInfo) {
|
||||
<NText class="text-16px">点击或者拖动文件到该区域来上传</NText>
|
||||
<NP v-if="showTip" depth="3" class="mt-8px text-center">
|
||||
请上传
|
||||
<template v-if="max">
|
||||
<template v-if="fileSize">
|
||||
大小不超过
|
||||
<b class="text-red-500">{{ max }}MB</b>
|
||||
<b class="text-red-500">{{ fileSize }}MB</b>
|
||||
</template>
|
||||
<template v-if="accept">
|
||||
格式为
|
||||
,且格式为
|
||||
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
|
||||
</template>
|
||||
的文件
|
||||
@ -159,12 +168,12 @@ async function handleRemove(file: UploadFileInfo) {
|
||||
</NUpload>
|
||||
<NP v-if="showTip && uploadType === 'image'" depth="3" class="mt-12px">
|
||||
请上传
|
||||
<template v-if="max">
|
||||
<template v-if="fileSize">
|
||||
大小不超过
|
||||
<b class="text-red-500">{{ max }}MB</b>
|
||||
<b class="text-red-500">{{ fileSize }}MB</b>
|
||||
</template>
|
||||
<template v-if="accept">
|
||||
格式为
|
||||
,且格式为
|
||||
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
|
||||
</template>
|
||||
的文件
|
||||
|
@ -58,6 +58,32 @@ export function fetchGetDeptTree() {
|
||||
});
|
||||
}
|
||||
|
||||
/** 重置用户密码 */
|
||||
export function fetchResetUserPassword(userId: CommonType.IdType, password: string) {
|
||||
return request<boolean>({
|
||||
url: '/system/user/resetPwd',
|
||||
method: 'put',
|
||||
data: { userId, password }
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据用户编号获取授权角色 */
|
||||
export function fetchGetAuthRole(userId: CommonType.IdType) {
|
||||
return request<Api.System.AuthRole>({
|
||||
url: `/system/user/authRole/${userId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/** 用户授权角色 */
|
||||
export function fetchAuthUserRole(userId: CommonType.IdType, roleIds: CommonType.IdType[]) {
|
||||
return request<boolean>({
|
||||
url: '/system/user/authRole',
|
||||
method: 'put',
|
||||
data: { userId, roleIds }
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改用户基本信息 */
|
||||
export function fetchUpdateUserProfile(data: Api.System.UserProfileOperateParams) {
|
||||
return request<boolean>({
|
||||
|
@ -6,7 +6,6 @@ import { fetchGetUserInfo, fetchLogin, fetchLogout } from '@/service/api';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouteStore } from '../route';
|
||||
import { useTabStore } from '../tab';
|
||||
import { clearAuthStorage, getToken } from './shared';
|
||||
@ -81,11 +80,11 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
if (pass) {
|
||||
await redirectFromLogin(redirect);
|
||||
window.$notification?.success({
|
||||
title: $t('page.login.common.loginSuccess'),
|
||||
content: $t('page.login.common.welcomeBack', { userName: userInfo.user?.nickName }),
|
||||
duration: 4500
|
||||
});
|
||||
// window.$notification?.success({
|
||||
// title: $t('page.login.common.loginSuccess'),
|
||||
// content: $t('page.login.common.welcomeBack', { userName: userInfo.user?.nickName }),
|
||||
// duration: 4500
|
||||
// });
|
||||
}
|
||||
} else {
|
||||
resetStore();
|
||||
|
8
src/typings/api/system.api.d.ts
vendored
8
src/typings/api/system.api.d.ts
vendored
@ -23,7 +23,7 @@ declare namespace Api {
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
/** 角色ID */
|
||||
roleId: number;
|
||||
roleId: CommonType.IdType;
|
||||
/** 角色权限字符串 */
|
||||
roleKey: string;
|
||||
/** 角色名称 */
|
||||
@ -144,6 +144,12 @@ declare namespace Api {
|
||||
/** user list */
|
||||
type UserList = Common.PaginatingQueryRecord<User>;
|
||||
|
||||
/** auth role */
|
||||
type AuthRole = {
|
||||
user: User;
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
/** social */
|
||||
type Social = Common.CommonRecord<{
|
||||
/** 用户ID */
|
||||
|
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@ -47,7 +47,9 @@ declare module 'vue' {
|
||||
IconIcRoundUpload: typeof import('~icons/ic/round-upload')['default']
|
||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
IconLocalLogo: typeof import('~icons/local/logo')['default']
|
||||
'IconMaterialSymbols:download2Rounded': typeof import('~icons/material-symbols/download2-rounded')['default']
|
||||
'IconMaterialSymbols:syncRounded': typeof import('~icons/material-symbols/sync-rounded')['default']
|
||||
'IconMaterialSymbols:upload2Rounded': typeof import('~icons/material-symbols/upload2-rounded')['default']
|
||||
IconMaterialSymbolsHelpOutline: typeof import('~icons/material-symbols/help-outline')['default']
|
||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
|
@ -50,7 +50,7 @@ export function createServiceConfig(env: Env.ImportMeta) {
|
||||
* @param env - the current env
|
||||
* @param isProxy - if use proxy
|
||||
*/
|
||||
export function getServiceBaseURL(env: Env.ImportMeta, isProxy: boolean) {
|
||||
export function getServiceBaseURL(env: Env.ImportMeta, isProxy: boolean = env.DEV && env.VITE_HTTP_PROXY === 'Y') {
|
||||
const { baseURL, other, proxyPattern } = createServiceConfig(env);
|
||||
|
||||
const otherBaseURL = {} as Record<App.Service.OtherBaseURLKey, string>;
|
||||
|
@ -44,7 +44,13 @@ async function unbindSsoAccount(socialId: string) {
|
||||
endBtnLoading();
|
||||
}
|
||||
|
||||
const socialSources = [
|
||||
const socialSources: {
|
||||
key: Api.System.SocialSource;
|
||||
icon?: string;
|
||||
localIcon?: string;
|
||||
color: string;
|
||||
name: string;
|
||||
}[] = [
|
||||
{ key: 'wechat_open', icon: 'ic:outline-wechat', color: '#44b549', name: '微信' },
|
||||
{ key: 'topiam', localIcon: 'topiam', color: '', name: 'TopIAM' },
|
||||
{ key: 'maxkey', localIcon: 'maxkey', color: '', name: 'MaxKey' },
|
||||
@ -92,9 +98,7 @@ function getSocial(key: string) {
|
||||
:style="{ color: source.color }"
|
||||
/>
|
||||
<div class="text-16px font-medium">{{ source.name }}</div>
|
||||
<NButton type="primary" size="small" @click="bindSsoAccount(source.key as Api.System.SocialSource)">
|
||||
绑定
|
||||
</NButton>
|
||||
<NButton type="primary" size="small" @click="bindSsoAccount(source.key)">绑定</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NCard>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue';
|
||||
import { NButton } from 'naive-ui';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { useBoolean, useLoading } from '@sa/hooks';
|
||||
import { fetchBatchDeleteUser, fetchGetDeptTree, fetchGetUserList } from '@/service/api/system';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
@ -11,6 +11,7 @@ import ButtonIcon from '@/components/custom/button-icon.vue';
|
||||
import DictTag from '@/components/custom/dict-tag.vue';
|
||||
import { $t } from '@/locales';
|
||||
import UserOperateDrawer from './modules/user-operate-drawer.vue';
|
||||
import UserImportModal from './modules/user-import-modal.vue';
|
||||
import UserSearch from './modules/user-search.vue';
|
||||
|
||||
defineOptions({
|
||||
@ -23,6 +24,8 @@ useDict('sys_normal_disable');
|
||||
const { hasAuth } = useAuth();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { bool: importVisible, setTrue: openImportModal } = useBoolean();
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
@ -198,6 +201,10 @@ function handleResetTreeData() {
|
||||
deptPattern.value = undefined;
|
||||
getTreeData();
|
||||
}
|
||||
|
||||
function handleImport() {
|
||||
openImportModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -247,7 +254,16 @@ function handleResetTreeData() {
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
>
|
||||
<template #after>
|
||||
<NButton v-if="hasAuth('system:user:import')" size="small" ghost @click="handleImport">
|
||||
<template #icon>
|
||||
<icon-material-symbols:upload-2-rounded class="text-icon" />
|
||||
</template>
|
||||
导入
|
||||
</NButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</template>
|
||||
<NDataTable
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
@ -262,6 +278,7 @@ function handleResetTreeData() {
|
||||
:pagination="mobilePagination"
|
||||
class="h-full"
|
||||
/>
|
||||
<UserImportModal v-model:visible="importVisible" @submitted="getDataByPage" />
|
||||
<UserOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
|
153
src/views/system/user/modules/user-import-modal.vue
Normal file
153
src/views/system/user/modules/user-import-modal.vue
Normal file
@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
// import { fetchUserResetPwd } from '@/service/api/system';
|
||||
import { ref, watch } from 'vue';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import { getToken } from '@/store/modules/auth/shared';
|
||||
import { useDownload } from '@/hooks/business/download';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import type FileUpload from '@/components/custom/file-upload.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserImportModal'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const { download } = useDownload();
|
||||
|
||||
const { baseURL } = getServiceBaseURL(import.meta.env);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
clientid: import.meta.env.VITE_APP_CLIENT_ID!
|
||||
};
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const uploadRef = ref<typeof FileUpload>();
|
||||
const message = ref<string>('');
|
||||
const success = ref<boolean>(false);
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const data = ref<Record<string, any>>({
|
||||
updateSupport: false
|
||||
});
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
if (success.value) {
|
||||
emit('submitted');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
fileList.value.forEach(item => {
|
||||
item.status = 'pending';
|
||||
});
|
||||
uploadRef.value?.submit();
|
||||
}
|
||||
|
||||
function isErrorState(xhr: XMLHttpRequest) {
|
||||
const responseText = xhr?.responseText;
|
||||
const response = JSON.parse(responseText);
|
||||
return response.code !== 200;
|
||||
}
|
||||
|
||||
function handleFinish(options: { file: UploadFileInfo; event?: ProgressEvent }) {
|
||||
const { file, event } = options;
|
||||
// @ts-expect-error Ignore type errors
|
||||
const responseText = event?.target?.responseText;
|
||||
const response = JSON.parse(responseText);
|
||||
message.value = response.msg;
|
||||
window.$message?.success('导入成功');
|
||||
success.value = true;
|
||||
return file;
|
||||
}
|
||||
|
||||
function handleError(options: { file: UploadFileInfo; event?: ProgressEvent }) {
|
||||
const { event } = options;
|
||||
// @ts-expect-error Ignore type errors
|
||||
const responseText = event?.target?.responseText;
|
||||
const msg = JSON.parse(responseText).msg;
|
||||
message.value = msg;
|
||||
window.$message?.error(msg || '导入失败');
|
||||
success.value = false;
|
||||
}
|
||||
|
||||
function handleDownloadTemplate() {
|
||||
download('/system/user/importTemplate', {}, `user_template_${new Date().getTime()}.xlsx`);
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
fileList.value = [];
|
||||
success.value = false;
|
||||
message.value = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal
|
||||
v-model:show="visible"
|
||||
title="导入用户"
|
||||
preset="card"
|
||||
:bordered="false"
|
||||
display-directive="show"
|
||||
class="max-w-90% w-600px"
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<NUpload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="fileList"
|
||||
:action="`${baseURL}/system/user/importData`"
|
||||
:headers="headers"
|
||||
:data="data"
|
||||
:max="1"
|
||||
:file-size="50"
|
||||
accept=".xls,.xlsx"
|
||||
:multiple="false"
|
||||
directory-dnd
|
||||
:default-upload="false"
|
||||
list-type="text"
|
||||
:is-error-state="isErrorState"
|
||||
@finish="handleFinish"
|
||||
@error="handleError"
|
||||
>
|
||||
<NUploadDragger>
|
||||
<div class="mb-12px flex-center">
|
||||
<SvgIcon icon="material-symbols:unarchive-outline" class="text-58px color-#d8d8db dark:color-#a1a1a2" />
|
||||
</div>
|
||||
<NText class="text-16px">点击或者拖动文件到该区域来上传</NText>
|
||||
<NP depth="3" class="mt-8px text-center">
|
||||
请上传大小不超过
|
||||
<b class="text-red-500">50MB</b>
|
||||
,且格式为
|
||||
<b class="text-red-500">xls/xlsx</b>
|
||||
的文件
|
||||
</NP>
|
||||
</NUploadDragger>
|
||||
</NUpload>
|
||||
<div class="flex-center">
|
||||
<NCheckbox v-model="data.updateSupport">是否更新已经存在的用户数据</NCheckbox>
|
||||
</div>
|
||||
<NAlert v-if="message" title="导入结果" :type="success ? 'success' : 'error'" :bordered="false">
|
||||
{{ message }}
|
||||
</NAlert>
|
||||
<template #footer>
|
||||
<NSpace justify="end" :size="16">
|
||||
<NButton @click="handleDownloadTemplate">下载模板</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">导入用户</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -70,7 +70,7 @@ type RuleKey = Extract<keyof Model, 'userName' | 'nickName' | 'password' | 'stat
|
||||
const rules: Record<RuleKey, App.Global.FormRule[]> = {
|
||||
userName: [createRequiredRule('用户名称不能为空')],
|
||||
nickName: [createRequiredRule('用户昵称不能为空')],
|
||||
password: [{ ...patternRules.pwd, required: true }],
|
||||
password: [{ ...patternRules.pwd, required: props.operateType === 'add' }],
|
||||
phonenumber: [patternRules.phone],
|
||||
status: [createRequiredRule('帐号状态不能为空')]
|
||||
};
|
||||
@ -95,6 +95,7 @@ function handleUpdateModelWhenEdit() {
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
startDeptLoading();
|
||||
Object.assign(model, props.rowData);
|
||||
model.password = '';
|
||||
getUserInfo();
|
||||
endDeptLoading();
|
||||
}
|
||||
@ -107,9 +108,10 @@ function closeDrawer() {
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const { userId, deptId, userName, nickName, email, phonenumber, sex, password, status, remark } = model;
|
||||
|
||||
// request
|
||||
if (props.operateType === 'add') {
|
||||
const { deptId, userName, nickName, email, phonenumber, sex, password, status, remark } = model;
|
||||
const { error } = await fetchCreateUser({
|
||||
deptId,
|
||||
userName,
|
||||
@ -125,7 +127,6 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit') {
|
||||
const { userId, deptId, userName, nickName, email, phonenumber, sex, password, status, remark } = model;
|
||||
const { error } = await fetchUpdateUser({
|
||||
userId,
|
||||
deptId,
|
||||
@ -183,7 +184,7 @@ watch(visible, () => {
|
||||
<NFormItem v-if="operateType === 'add'" label="用户名称" path="userName">
|
||||
<NInput v-model:value="model.userName" placeholder="请输入用户名称" />
|
||||
</NFormItem>
|
||||
<NFormItem v-if="operateType === 'add'" label="用户密码" path="password">
|
||||
<NFormItem label="用户密码" path="password">
|
||||
<NInput
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
|
Loading…
Reference in New Issue
Block a user