591 lines
18 KiB
Vue
591 lines
18 KiB
Vue
<template>
|
||
<div>
|
||
<div class="n-layout-page-header">
|
||
<n-card :bordered="false" title="用户管理" />
|
||
</div>
|
||
<n-card :bordered="false" class="proCard">
|
||
<BasicForm
|
||
ref="searchFormRef"
|
||
@register="register"
|
||
@submit="reloadTable"
|
||
@reset="reloadTable"
|
||
@keyup.enter="reloadTable"
|
||
/>
|
||
<BasicTable
|
||
ref="actionRef"
|
||
:columns="columns"
|
||
:request="loadDataTable"
|
||
:row-key="(row) => row.tenantId + '-' + row.userId"
|
||
:actionColumn="actionColumn"
|
||
:scroll-x="scrollX"
|
||
:resizeHeightOffset="-10000"
|
||
>
|
||
<template #tableTitle>
|
||
<n-space>
|
||
<n-button type="primary" v-if="hasPermission(['/imUser/create'])" @click="openCreate">
|
||
<template #icon
|
||
><n-icon><PlusOutlined /></n-icon
|
||
></template>
|
||
创建用户
|
||
</n-button>
|
||
<n-button v-if="hasPermission(['/imUser/batchCreate'])" @click="openBatchCreate">
|
||
批量创建
|
||
</n-button>
|
||
<n-button v-if="hasPermission(['/imUser/import'])" @click="openImport">
|
||
<template #icon
|
||
><n-icon><UploadOutlined /></n-icon
|
||
></template>
|
||
导入Excel
|
||
</n-button>
|
||
<n-button v-if="hasPermission(['/imUser/downloadTemplate'])" @click="DownloadTemplate">
|
||
<template #icon
|
||
><n-icon><DownloadOutlined /></n-icon
|
||
></template>
|
||
下载模板
|
||
</n-button>
|
||
</n-space>
|
||
</template>
|
||
</BasicTable>
|
||
</n-card>
|
||
<ViewModal ref="viewRef" />
|
||
<ResetPasswordModal ref="resetPasswordRef" @reload="reloadTable" />
|
||
|
||
<n-modal
|
||
v-model:show="ipVisible"
|
||
preset="dialog"
|
||
title="IP白名单"
|
||
:show-icon="false"
|
||
class="user-dialog"
|
||
>
|
||
<n-form label-placement="left" label-width="90">
|
||
<n-form-item label="用户">
|
||
{{ operationUser?.username }}({{ operationUser?.displayNo || operationUser?.userId }})
|
||
</n-form-item>
|
||
<n-form-item label="IP/CIDR">
|
||
<n-input
|
||
v-model:value="ipWhitelistText"
|
||
type="textarea"
|
||
:rows="7"
|
||
placeholder="每行一个 IP 或 CIDR,例如 192.168.1.10 或 10.0.0.0/8"
|
||
/>
|
||
</n-form-item>
|
||
</n-form>
|
||
<template #action>
|
||
<n-button @click="ipVisible = false">取消</n-button>
|
||
<n-button
|
||
v-if="hasPermission(['/imUser/ipWhitelistSave'])"
|
||
type="primary"
|
||
:loading="submitting"
|
||
@click="submitIPWhitelist"
|
||
>保存</n-button
|
||
>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<n-modal
|
||
v-model:show="groupVisible"
|
||
preset="dialog"
|
||
title="调整分组"
|
||
:show-icon="false"
|
||
class="user-dialog"
|
||
>
|
||
<n-form label-placement="left" label-width="90">
|
||
<n-form-item label="用户">
|
||
{{ operationUser?.username }}({{ operationUser?.displayNo || operationUser?.userId }})
|
||
</n-form-item>
|
||
<n-form-item label="用户分组" required>
|
||
<n-select
|
||
v-model:value="adjustGroupId"
|
||
:options="policyGroupOptions"
|
||
placeholder="请选择用户分组"
|
||
/>
|
||
</n-form-item>
|
||
</n-form>
|
||
<template #action>
|
||
<n-button @click="groupVisible = false">取消</n-button>
|
||
<n-button type="primary" :loading="submitting" @click="submitAdjustGroup">保存</n-button>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<n-modal
|
||
v-model:show="createVisible"
|
||
preset="dialog"
|
||
title="创建用户"
|
||
:show-icon="false"
|
||
class="user-dialog"
|
||
>
|
||
<n-form
|
||
ref="createFormRef"
|
||
:model="createForm"
|
||
:rules="createRules"
|
||
label-placement="left"
|
||
label-width="90"
|
||
>
|
||
<n-form-item label="用户名" path="username"
|
||
><n-input v-model:value="createForm.username" placeholder="可空,系统自动生成"
|
||
/></n-form-item>
|
||
<n-form-item label="昵称" path="nickname"
|
||
><n-input v-model:value="createForm.nickname" placeholder="请输入昵称"
|
||
/></n-form-item>
|
||
<n-form-item label="手机号" path="phone"
|
||
><n-input v-model:value="createForm.phone" placeholder="可选"
|
||
/></n-form-item>
|
||
<n-form-item label="密码" path="password"
|
||
><n-input v-model:value="createForm.password" type="password" show-password-on="click"
|
||
/></n-form-item>
|
||
<n-form-item label="用户分组"
|
||
><n-select
|
||
v-model:value="createForm.groupId"
|
||
:options="policyGroupOptions"
|
||
clearable
|
||
placeholder="请选择用户分组"
|
||
/></n-form-item>
|
||
</n-form>
|
||
<template #action
|
||
><n-button @click="createVisible = false">取消</n-button
|
||
><n-button type="primary" :loading="submitting" @click="submitCreate"
|
||
>确定</n-button
|
||
></template
|
||
>
|
||
</n-modal>
|
||
|
||
<n-modal
|
||
v-model:show="batchVisible"
|
||
preset="dialog"
|
||
title="批量创建用户"
|
||
:show-icon="false"
|
||
class="user-dialog"
|
||
>
|
||
<n-form
|
||
ref="batchFormRef"
|
||
:model="batchForm"
|
||
:rules="batchRules"
|
||
label-placement="left"
|
||
label-width="100"
|
||
>
|
||
<n-form-item label="创建数量" path="count"
|
||
><n-input-number v-model:value="batchForm.count" :min="1" :max="100"
|
||
/></n-form-item>
|
||
<n-form-item label="昵称前缀"
|
||
><n-input v-model:value="batchForm.nicknamePrefix" placeholder="默认:用户"
|
||
/></n-form-item>
|
||
<n-form-item label="默认密码" path="password"
|
||
><n-input v-model:value="batchForm.password" type="password" show-password-on="click"
|
||
/></n-form-item>
|
||
<n-form-item label="默认分组"
|
||
><n-select v-model:value="batchForm.groupId" :options="policyGroupOptions" clearable
|
||
/></n-form-item>
|
||
</n-form>
|
||
<template #action
|
||
><n-button @click="batchVisible = false">取消</n-button
|
||
><n-button type="primary" :loading="submitting" @click="submitBatchCreate"
|
||
>确定</n-button
|
||
></template
|
||
>
|
||
</n-modal>
|
||
|
||
<n-modal
|
||
v-model:show="importVisible"
|
||
preset="dialog"
|
||
title="导入Excel"
|
||
:show-icon="false"
|
||
class="user-dialog"
|
||
>
|
||
<n-form label-placement="left" label-width="90">
|
||
<n-form-item label="默认分组"
|
||
><n-select v-model:value="importGroupId" :options="policyGroupOptions" clearable
|
||
/></n-form-item>
|
||
<n-form-item label="Excel文件">
|
||
<n-upload :max="1" accept=".xlsx" :custom-request="handleImport" :show-file-list="true">
|
||
<n-upload-dragger><n-text>点击或拖动 .xlsx 文件到此处</n-text></n-upload-dragger>
|
||
</n-upload>
|
||
</n-form-item>
|
||
</n-form>
|
||
</n-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, h, onMounted, reactive, ref, watch } from 'vue';
|
||
import { UploadCustomRequestOptions, useDialog, useMessage } from 'naive-ui';
|
||
import { BasicTable, TableAction } from '@/components/Table';
|
||
import { BasicForm, useForm } from '@/components/Form/index';
|
||
import { usePermission } from '@/hooks/web/usePermission';
|
||
import { useUserStore } from '@/store/modules/user';
|
||
import {
|
||
AccountCancel,
|
||
AdjustGroup,
|
||
Ban,
|
||
BatchCreate,
|
||
Create,
|
||
DownloadTemplate,
|
||
ImportUsers,
|
||
IPWhitelist,
|
||
IPWhitelistSave,
|
||
List,
|
||
LogoutAll,
|
||
Unban,
|
||
} from '@/api/imUser';
|
||
import {
|
||
columns,
|
||
loadOptions,
|
||
loadPolicyGroupOptions,
|
||
loadTenantOptions,
|
||
policyGroupOptions,
|
||
schemas,
|
||
State,
|
||
tenantOptions,
|
||
} from './model';
|
||
import { DownloadOutlined, PlusOutlined, UploadOutlined } from '@vicons/antd';
|
||
import { adaTableScrollX } from '@/utils/hotgo';
|
||
import ViewModal from './view.vue';
|
||
import ResetPasswordModal from './ResetPasswordModal.vue';
|
||
|
||
const message = useMessage();
|
||
const dialog = useDialog();
|
||
const { hasPermission } = usePermission();
|
||
const userStore = useUserStore();
|
||
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
|
||
const actionRef = ref();
|
||
const searchFormRef = ref<any>({});
|
||
const viewRef = ref();
|
||
const resetPasswordRef = ref();
|
||
const createFormRef = ref();
|
||
const batchFormRef = ref();
|
||
const createVisible = ref(false);
|
||
const batchVisible = ref(false);
|
||
const importVisible = ref(false);
|
||
const submitting = ref(false);
|
||
const ipVisible = ref(false);
|
||
const groupVisible = ref(false);
|
||
const operationUser = ref<State | null>(null);
|
||
const ipWhitelistText = ref('');
|
||
const adjustGroupId = ref<number | null>(null);
|
||
const importGroupId = ref<number | null>(null);
|
||
const createForm = reactive({
|
||
username: '',
|
||
nickname: '',
|
||
phone: '',
|
||
password: '',
|
||
groupId: null as number | null,
|
||
});
|
||
const batchForm = reactive({
|
||
count: 10,
|
||
nicknamePrefix: '用户',
|
||
password: '',
|
||
groupId: null as number | null,
|
||
});
|
||
const createRules = {
|
||
nickname: { required: true, message: '请输入昵称', trigger: 'blur' },
|
||
password: { required: true, min: 3, message: '密码至少3位', trigger: 'blur' },
|
||
};
|
||
const batchRules = {
|
||
count: { required: true, type: 'number', message: '请输入创建数量', trigger: 'change' },
|
||
password: { required: true, min: 3, message: '密码至少3位', trigger: 'blur' },
|
||
};
|
||
|
||
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
|
||
if (tenantSchema) {
|
||
tenantSchema.ifShow = () => isPlatformAdmin.value;
|
||
}
|
||
|
||
const actionColumn = reactive({
|
||
width: 600,
|
||
title: '操作',
|
||
key: 'action',
|
||
fixed: 'right',
|
||
render(record: State) {
|
||
return h(TableAction as any, {
|
||
style: 'button',
|
||
actions: [
|
||
{
|
||
label: '详情',
|
||
type: 'default',
|
||
onClick: handleView.bind(null, record),
|
||
auth: ['/imUser/view'],
|
||
},
|
||
{
|
||
label: '重置密码',
|
||
type: 'default',
|
||
onClick: handleResetPassword.bind(null, record),
|
||
auth: ['/imUser/resetPassword'],
|
||
ifShow: record.state !== 20,
|
||
},
|
||
{
|
||
label: 'IP白名单',
|
||
type: 'default',
|
||
onClick: openIPWhitelist.bind(null, record),
|
||
auth: ['/imUser/ipWhitelist'],
|
||
ifShow: record.state !== 20,
|
||
},
|
||
{
|
||
label: '调整分组',
|
||
type: 'default',
|
||
onClick: openAdjustGroup.bind(null, record),
|
||
auth: ['/imUser/adjustGroup'],
|
||
ifShow: record.state !== 20,
|
||
},
|
||
{
|
||
label: record.state === 0 ? '封禁' : '解封',
|
||
type: record.state === 0 ? 'error' : 'default',
|
||
onClick: handleBanToggle.bind(null, record),
|
||
auth: [record.state === 0 ? '/imUser/ban' : '/imUser/unban'],
|
||
ifShow: record.state === 0 || record.state === 10,
|
||
},
|
||
{
|
||
label: '强制下线',
|
||
type: 'error',
|
||
onClick: handleForceOffline.bind(null, record),
|
||
auth: ['/imUser/logoutAll'],
|
||
ifShow: record.state !== 20,
|
||
},
|
||
{
|
||
label: '强制注销',
|
||
type: 'error',
|
||
onClick: handleForceCancel.bind(null, record),
|
||
auth: ['/imUser/accountCancel'],
|
||
ifShow: record.state !== 20,
|
||
},
|
||
],
|
||
});
|
||
},
|
||
});
|
||
|
||
const scrollX = computed(() => {
|
||
return adaTableScrollX(columns, actionColumn.width);
|
||
});
|
||
|
||
const [register, { setFieldsValue }] = useForm({
|
||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||
labelWidth: 90,
|
||
schemas,
|
||
});
|
||
|
||
// 加载表格数据
|
||
const loadDataTable = async (res) => {
|
||
const params = { ...searchFormRef.value?.formModel, ...res };
|
||
if (isPlatformAdmin.value && !params.tenantId) {
|
||
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||
}
|
||
return await List(params);
|
||
};
|
||
|
||
// 重新加载表格数据
|
||
function reloadTable() {
|
||
actionRef.value?.reload();
|
||
}
|
||
|
||
// 查看详情
|
||
function handleView(record: Recordable) {
|
||
viewRef.value.openModal(record);
|
||
}
|
||
|
||
function handleForceOffline(record: State) {
|
||
dialog.warning({
|
||
title: '强制下线',
|
||
content: `确定强制用户“${record.username}”的全部设备下线吗?`,
|
||
positiveText: '确认',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
await LogoutAll({
|
||
tenantId: record.tenantId,
|
||
userId: Number(record.userId),
|
||
reason: '后台强制下线',
|
||
});
|
||
message.success('强制下线已完成');
|
||
reloadTable();
|
||
},
|
||
});
|
||
}
|
||
|
||
function handleResetPassword(record: State) {
|
||
resetPasswordRef.value.openModal(record);
|
||
}
|
||
|
||
async function openIPWhitelist(record: State) {
|
||
operationUser.value = record;
|
||
const result = await IPWhitelist({ tenantId: record.tenantId, userId: Number(record.userId) });
|
||
ipWhitelistText.value = (result?.ipList || []).join('\n');
|
||
ipVisible.value = true;
|
||
}
|
||
|
||
async function submitIPWhitelist() {
|
||
if (!operationUser.value) return;
|
||
const ipList = ipWhitelistText.value
|
||
.split(/[\n,,]+/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
submitting.value = true;
|
||
try {
|
||
await IPWhitelistSave({
|
||
tenantId: operationUser.value.tenantId,
|
||
userId: Number(operationUser.value.userId),
|
||
ipList,
|
||
});
|
||
message.success('IP白名单保存成功');
|
||
ipVisible.value = false;
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
async function openAdjustGroup(record: State) {
|
||
operationUser.value = record;
|
||
await loadPolicyGroupOptions(record.tenantId);
|
||
adjustGroupId.value = record.policyGroupId || null;
|
||
groupVisible.value = true;
|
||
}
|
||
|
||
async function submitAdjustGroup() {
|
||
if (!operationUser.value || !adjustGroupId.value) {
|
||
message.warning('请选择用户分组');
|
||
return;
|
||
}
|
||
submitting.value = true;
|
||
try {
|
||
await AdjustGroup({
|
||
tenantId: operationUser.value.tenantId,
|
||
userId: Number(operationUser.value.userId),
|
||
groupId: adjustGroupId.value,
|
||
});
|
||
message.success('用户分组调整成功');
|
||
groupVisible.value = false;
|
||
reloadTable();
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
function handleBanToggle(record: State) {
|
||
const banning = record.state === 0;
|
||
dialog.warning({
|
||
title: banning ? '封禁用户' : '解封用户',
|
||
content: `确定${banning ? '封禁' : '解封'}用户“${record.username}”吗?`,
|
||
positiveText: '确认',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
const action = banning ? Ban : Unban;
|
||
await action({
|
||
tenantId: record.tenantId,
|
||
userId: Number(record.userId),
|
||
reason: banning ? '后台封禁' : '后台解封',
|
||
});
|
||
message.success(`${banning ? '封禁' : '解封'}已完成`);
|
||
reloadTable();
|
||
},
|
||
});
|
||
}
|
||
|
||
function handleForceCancel(record: State) {
|
||
dialog.error({
|
||
title: '强制注销',
|
||
content: `确定强制注销用户“${record.username}”吗?注销后账号不可继续登录。`,
|
||
positiveText: '确认注销',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
await AccountCancel({
|
||
tenantId: record.tenantId,
|
||
userId: Number(record.userId),
|
||
reason: '后台强制注销',
|
||
});
|
||
message.success('强制注销已完成');
|
||
reloadTable();
|
||
},
|
||
});
|
||
}
|
||
|
||
function currentTenantId() {
|
||
return Number(searchFormRef.value?.formModel?.tenantId || 0);
|
||
}
|
||
|
||
function openCreate() {
|
||
Object.assign(createForm, {
|
||
username: '',
|
||
nickname: '',
|
||
phone: '',
|
||
password: '',
|
||
groupId: null,
|
||
});
|
||
createVisible.value = true;
|
||
}
|
||
|
||
async function submitCreate() {
|
||
await createFormRef.value?.validate();
|
||
submitting.value = true;
|
||
try {
|
||
const result = await Create({ ...createForm, tenantId: currentTenantId() });
|
||
message.success(`用户 ${result.username} 创建成功`);
|
||
createVisible.value = false;
|
||
reloadTable();
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
function openBatchCreate() {
|
||
Object.assign(batchForm, { count: 10, nicknamePrefix: '用户', password: '', groupId: null });
|
||
batchVisible.value = true;
|
||
}
|
||
|
||
async function submitBatchCreate() {
|
||
await batchFormRef.value?.validate();
|
||
submitting.value = true;
|
||
try {
|
||
const result = await BatchCreate({ ...batchForm, tenantId: currentTenantId() });
|
||
message.success(`创建完成:成功 ${result.successCount},失败 ${result.failedCount}`);
|
||
batchVisible.value = false;
|
||
reloadTable();
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
function openImport() {
|
||
importGroupId.value = null;
|
||
importVisible.value = true;
|
||
}
|
||
|
||
async function handleImport(options: UploadCustomRequestOptions) {
|
||
try {
|
||
const result = await ImportUsers({
|
||
file: options.file.file as File,
|
||
tenantId: currentTenantId(),
|
||
groupId: importGroupId.value || 0,
|
||
});
|
||
options.onFinish();
|
||
message.success(`导入完成:成功 ${result.successCount},失败 ${result.failedCount}`);
|
||
importVisible.value = false;
|
||
reloadTable();
|
||
} catch {
|
||
options.onError();
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => searchFormRef.value?.formModel?.tenantId,
|
||
(tenantId) => loadPolicyGroupOptions(Number(tenantId || 0))
|
||
);
|
||
|
||
onMounted(async () => {
|
||
loadOptions();
|
||
let tenantId = currentTenantId();
|
||
if (isPlatformAdmin.value) {
|
||
await loadTenantOptions();
|
||
if (tenantOptions.value.length > 0) {
|
||
tenantId = Number(tenantOptions.value[0].value);
|
||
await setFieldsValue({ tenantId });
|
||
} else {
|
||
message.warning('暂无可用商户');
|
||
}
|
||
}
|
||
await loadPolicyGroupOptions(tenantId);
|
||
reloadTable();
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
:deep(.user-dialog) {
|
||
width: min(560px, calc(100vw - 32px));
|
||
}
|
||
</style>
|