Files
im_manager/web/src/views/imUserPolicyGroup/index.vue
T

496 lines
16 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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"
/>
<n-alert type="info" :show-icon="true" class="whitelist-summary">
商户渠道 IP 白名单 APP:{{
whitelistSummary.appList.length
? `已配置 ${whitelistSummary.appList.length} 条`
: '未配置'
}} Web:{{
whitelistSummary.webList.length
? `已配置 ${whitelistSummary.webList.length} 条`
: '未配置'
}}
</n-alert>
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-space>
<n-button
type="primary"
:disabled="!hasPermission(['/imUserPolicyGroup/edit'])"
@click="openEdit()"
>新增分组</n-button
>
<n-button
:disabled="!hasPermission(['/imUserPolicyGroup/loginIPWhitelist'])"
@click="openLoginIPWhitelist"
>
<template #icon
><n-icon><ShieldCheckmarkOutline /></n-icon
></template>
IP 白名单
</n-button>
</n-space>
</template>
</BasicTable>
</n-card>
<n-modal
v-model:show="loginIPVisible"
preset="dialog"
title="商户渠道 IP 白名单"
:show-icon="false"
:style="{ width: whitelistDialogWidth }"
>
<n-form label-placement="top">
<n-form-item label="APP 登录白名单">
<n-input
v-model:value="loginIPForm.appText"
type="textarea"
:rows="7"
placeholder="每行一个 IPv4、IPv6 或 CIDR,例如 203.0.113.10"
/>
</n-form-item>
<n-form-item label="Web 登录白名单">
<n-input
v-model:value="loginIPForm.webText"
type="textarea"
:rows="7"
placeholder="每行一个 IPv4、IPv6 或 CIDR,例如 198.51.100.0/24"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="loginIPVisible = false">取消</n-button>
<n-button
type="primary"
:disabled="!hasPermission(['/imUserPolicyGroup/loginIPWhitelistSave'])"
:loading="submitting"
@click="saveLoginIPWhitelist"
>确定</n-button
>
</n-space>
</template>
</n-modal>
<n-modal
v-model:show="editVisible"
preset="dialog"
:show-icon="false"
:title="editForm.id ? '编辑用户分组' : '新增用户分组'"
:style="{ width: dialogWidth }"
>
<n-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-placement="left"
label-width="118"
>
<n-grid :cols="2" :x-gap="16">
<n-form-item-gi label="分组编码" path="groupCode"
><n-input v-model:value="editForm.groupCode" :disabled="!!editForm.id"
/></n-form-item-gi>
<n-form-item-gi label="分组名称" path="groupName"
><n-input v-model:value="editForm.groupName"
/></n-form-item-gi>
<n-form-item-gi label="优先级"
><n-input-number v-model:value="editForm.priority" :min="0"
/></n-form-item-gi>
<n-form-item-gi label="状态"
><n-select v-model:value="editForm.status" :options="statusOptions"
/></n-form-item-gi>
</n-grid>
<n-divider title-placement="left">好友权限</n-divider>
<n-grid :cols="2" :x-gap="16">
<n-form-item-gi label="允许添加好友"
><n-switch v-model:value="editForm.policy.allow_add_friend"
/></n-form-item-gi>
<n-form-item-gi label="直接成为好友"
><n-switch v-model:value="editForm.policy.direct_add_friend"
/></n-form-item-gi>
<n-form-item-gi label="允许二维码添加"
><n-switch v-model:value="editForm.policy.allow_qrcode_add_friend"
/></n-form-item-gi>
<n-form-item-gi label="群成员资料可见"
><n-switch v-model:value="editForm.policy.allow_group_member_profile"
/></n-form-item-gi>
<n-form-item-gi label="禁用时隐藏资料"
><n-switch v-model:value="editForm.policy.hide_profile_when_friend_disabled"
/></n-form-item-gi>
<n-form-item-gi label="好友数量上限"
><n-input-number v-model:value="editForm.policy.max_friend_count" :min="0"
/></n-form-item-gi>
</n-grid>
<n-divider title-placement="left">消息与群权限</n-divider>
<n-grid :cols="2" :x-gap="16">
<n-form-item-gi label="允许单聊"
><n-switch v-model:value="editForm.policy.allow_private_message"
/></n-form-item-gi>
<n-form-item-gi label="允许群聊"
><n-switch v-model:value="editForm.policy.allow_group_message"
/></n-form-item-gi>
<n-form-item-gi label="允许创建群"
><n-switch v-model:value="editForm.policy.allow_create_group"
/></n-form-item-gi>
<n-form-item-gi label="允许加入群"
><n-switch v-model:value="editForm.policy.allow_join_group"
/></n-form-item-gi>
<n-form-item-gi label="允许上传文件"
><n-switch v-model:value="editForm.policy.allow_upload_file"
/></n-form-item-gi>
</n-grid>
<n-form-item label="IP白名单"
><n-input
v-model:value="editForm.ipWhitelistText"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="每行一个IP或CIDR"
/></n-form-item>
<n-form-item label="IP黑名单"
><n-input
v-model:value="editForm.ipBlacklistText"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="每行一个IP或CIDR"
/></n-form-item>
<n-form-item label="备注"
><n-input v-model:value="editForm.remark" type="textarea"
/></n-form-item>
</n-form>
<template #action
><n-space
><n-button @click="editVisible = false">取消</n-button
><n-button type="primary" :loading="submitting" @click="submitEdit"
>保存</n-button
></n-space
></template
>
</n-modal>
<n-modal
v-model:show="memberVisible"
preset="dialog"
:show-icon="false"
title="分组成员"
:style="{ width: dialogWidth }"
>
<n-space class="member-toolbar">
<n-input-number v-model:value="memberUserId" :min="1" placeholder="输入用户ID" />
<n-button
type="primary"
:disabled="!hasPermission(['/imUserPolicyGroup/memberAdd'])"
@click="addMember"
>添加成员</n-button
>
</n-space>
<BasicTable
ref="memberTableRef"
:columns="memberColumns"
:request="loadMemberTable"
:row-key="(row) => row.id"
:pagination="{ pageSize: 10 }"
:resizeHeightOffset="-10000"
/>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicForm, useForm } from '@/components/Form';
import { BasicTable, TableAction } from '@/components/Table';
import { useDictStore } from '@/store/modules/dict';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { ShieldCheckmarkOutline } from '@vicons/ionicons5';
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
import {
Delete,
Edit,
List,
MemberAdd,
MemberList,
MemberRemove,
LoginIPWhitelist,
LoginIPWhitelistSave,
Status,
} from '@/api/imUserPolicyGroup';
import { columns, defaultPolicy, loadOptions, schemas } from './model';
import {
getDefaultTenantValue,
loadPolicyTenantOptions,
parseJSON,
policyTenantOptions,
} from '@/views/imPolicyShared';
const dict = useDictStore(),
message = useMessage(),
dialog = useDialog(),
userStore = useUserStore();
const { hasPermission } = usePermission();
const actionRef = ref(),
searchFormRef = ref<any>({}),
editFormRef = ref(),
memberTableRef = ref();
const editVisible = ref(false),
memberVisible = ref(false),
loginIPVisible = ref(false),
submitting = ref(false),
memberUserId = ref<number | null>(null),
activeGroupId = ref(0);
const dialogWidth = computed(() => adaModalWidth(820));
const whitelistDialogWidth = computed(() => adaModalWidth(760));
const whitelistSummary = reactive({
tenantId: 0,
appList: [] as string[],
webList: [] as string[],
});
const loginIPForm = reactive({ appText: '', webText: '' });
const statusOptions = dict.getOption('sys_normal_disable');
const emptyForm = () => ({
id: 0,
tenantId: Number(searchFormRef.value?.formModel?.tenantId || 0),
groupCode: '',
groupName: '',
priority: 0,
status: 1,
policy: { ...defaultPolicy },
ipWhitelistText: '',
ipBlacklistText: '',
remark: '',
});
const editForm = ref<any>(emptyForm());
const editRules = {
groupCode: { required: true, message: '请输入分组编码', trigger: 'blur' },
groupName: { required: true, message: '请输入分组名称', trigger: 'blur' },
};
const tenantSchema = schemas.value.find((v) => v.field === 'tenantId');
if (tenantSchema) tenantSchema.ifShow = () => true;
const [register, { setFieldsValue }] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4' },
labelWidth: 90,
schemas,
});
const loadDataTable = (res) => List({ ...searchFormRef.value?.formModel, ...res });
const currentTenantId = () => Number(searchFormRef.value?.formModel?.tenantId || 0);
const reloadTable = () => {
actionRef.value?.reload();
refreshLoginIPSummary();
};
const actionColumn = reactive({
width: 300,
title: '操作',
key: 'action',
fixed: 'right',
render: (row) =>
h(TableAction as any, {
style: 'button',
actions: [
{ label: '编辑', auth: ['/imUserPolicyGroup/edit'], onClick: () => openEdit(row) },
{
label: '成员',
auth: ['/imUserPolicyGroup/memberList'],
ifShow: () => row.tenantId > 0,
onClick: () => openMembers(row),
},
{
label: row.status === 1 ? '停用' : '启用',
auth: ['/imUserPolicyGroup/status'],
onClick: () => changeStatus(row),
},
{
label: '删除',
type: 'error',
auth: ['/imUserPolicyGroup/delete'],
onClick: () => removeGroup(row),
},
],
}),
});
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
function lines(value: string) {
return value
.split(/\r?\n/)
.map((v) => v.trim())
.filter(Boolean);
}
async function fetchLoginIPWhitelist() {
const tenantId = currentTenantId();
if (tenantId <= 0) {
return null;
}
return await LoginIPWhitelist({ tenantId });
}
async function refreshLoginIPSummary() {
const result = await fetchLoginIPWhitelist();
if (!result) {
Object.assign(whitelistSummary, { tenantId: 0, appList: [], webList: [] });
return;
}
Object.assign(whitelistSummary, {
tenantId: Number(result.tenantId || 0),
appList: result.appList || [],
webList: result.webList || [],
});
}
async function openLoginIPWhitelist() {
const result = await fetchLoginIPWhitelist();
if (!result) {
message.warning('请先选择具体商户');
return;
}
Object.assign(loginIPForm, {
appText: (result.appList || []).join('\n'),
webText: (result.webList || []).join('\n'),
});
loginIPVisible.value = true;
}
async function saveLoginIPWhitelist() {
submitting.value = true;
try {
await LoginIPWhitelistSave({
tenantId: currentTenantId(),
appList: lines(loginIPForm.appText),
webList: lines(loginIPForm.webText),
});
message.success('商户渠道 IP 白名单保存成功');
loginIPVisible.value = false;
await refreshLoginIPSummary();
} finally {
submitting.value = false;
}
}
function openEdit(row?: any) {
editForm.value = row
? {
...row,
policy: { ...defaultPolicy, ...parseJSON(row.policyJson, {}) },
ipWhitelistText: parseJSON(row.ipWhitelist, []).join('\n'),
ipBlacklistText: parseJSON(row.ipBlacklist, []).join('\n'),
}
: emptyForm();
editVisible.value = true;
}
async function submitEdit() {
submitting.value = true;
try {
await editFormRef.value?.validate();
const f = editForm.value;
await Edit({
...f,
policyJson: f.policy,
ipWhitelist: lines(f.ipWhitelistText),
ipBlacklist: lines(f.ipBlacklistText),
});
message.success('保存成功');
editVisible.value = false;
reloadTable();
} finally {
submitting.value = false;
}
}
function changeStatus(row) {
dialog.warning({
title: row.status === 1 ? '停用分组' : '启用分组',
content: `确认${row.status === 1 ? '停用' : '启用'}“${row.groupName}”?`,
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await Status({ id: row.id, status: row.status === 1 ? 2 : 1 });
message.success('操作成功');
reloadTable();
},
});
}
function removeGroup(row) {
dialog.error({
title: '删除分组',
content: `确认删除“${row.groupName}”?分组有成员时不可删除。`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
await Delete({ id: row.id });
message.success('删除成功');
reloadTable();
},
});
}
const memberColumns = [
{ title: '用户ID', key: 'userId', width: 140 },
{ title: '有效开始', key: 'validFrom', width: 180 },
{ title: '有效结束', key: 'validUntil', width: 180 },
{ title: '备注', key: 'remark', width: 180 },
{
title: '操作',
key: 'action',
width: 90,
render: (row) =>
h(TableAction as any, {
style: 'button',
actions: [
{
label: '移除',
type: 'error',
auth: ['/imUserPolicyGroup/memberRemove'],
onClick: () => removeMember(row),
},
],
}),
},
];
function openMembers(row) {
activeGroupId.value = row.id;
memberVisible.value = true;
setTimeout(() => memberTableRef.value?.reload(), 0);
}
const loadMemberTable = (res) => MemberList({ groupId: activeGroupId.value, ...res });
async function addMember() {
if (!memberUserId.value) return message.warning('请输入用户ID');
await MemberAdd({ groupId: activeGroupId.value, userId: memberUserId.value });
memberUserId.value = null;
message.success('添加成功');
memberTableRef.value?.reload();
reloadTable();
}
async function removeMember(row) {
await MemberRemove({ id: row.id });
message.success('移除成功');
memberTableRef.value?.reload();
reloadTable();
}
onMounted(async () => {
loadOptions();
await loadPolicyTenantOptions(true);
await setFieldsValue({ tenantId: getDefaultTenantValue(policyTenantOptions.value) });
reloadTable();
});
</script>
<style lang="less" scoped>
.member-toolbar {
margin-bottom: 16px;
}
.whitelist-summary {
margin-bottom: 16px;
}
</style>