2
0

fix:portal门户界面

This commit is contained in:
zhongzm
2025-02-24 19:45:38 +08:00
parent ec83482e99
commit 10e21ea516
6 changed files with 951 additions and 6 deletions

View File

@@ -1,6 +1,669 @@
<script>
</script>
<template>
<SimpleScrollbar>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ACard
:title="t('page.portal.title')"
:bordered="false"
:body-style="{ flex: 1, overflow: 'hidden' }"
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
>
<template #extra>
<div class="flex items-center gap-4">
<!-- 站点选择 -->
<a-select
v-model:value="selectedSiteId"
:loading="siteLoading"
:placeholder="t('page.portal.selectSite')"
style="width: 200px"
@change="handleSiteChange"
>
<a-select-option
v-for="site in siteList"
:key="site.siteId"
:value="site.siteId"
>
{{ site.name }}
</a-select-option>
</a-select>
<!-- 表格操作组件 -->
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="tableLoading"
:show-delete="false"
@add="handleAdd"
@refresh="getData"
/>
</div>
</template>
<!-- 表格 -->
<ATable
:columns="columns"
:data-source="tableData"
:loading="tableLoading"
:pagination="{
...mobilePagination,
total: mobilePagination.total,
current: searchParams.pageNum,
pageSize: searchParams.pageSize,
showTotal: (total: number) => `${t('page.portal.total')} ${total} `
}"
@change="(pagination) => {
searchParams.pageNum = pagination.current;
searchParams.pageSize = pagination.pageSize;
getData();
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'operate'">
<div class="flex items-center justify-center gap-2">
<a-tooltip :title="t('common.edit')">
<a-button
type="link"
size="small"
@click="() => handleEdit(record)"
>
<template #icon>
<EditOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip :title="t('common.delete')">
<a-button
type="link"
size="small"
class="text-red-500"
@click="() => handleDelete(record)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</div>
</template>
</template>
</ATable>
</ACard>
<!-- 添加门户对话框 -->
<a-modal
v-model:visible="addVisible"
:title="t('page.portal.addPortal')"
@ok="handleAddConfirm"
width="600px"
>
<a-form
:model="addForm"
:rules="addRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
layout="horizontal"
ref="formRef"
>
<a-form-item name="name" :label="t('page.portal.name')">
<a-input v-model:value="addForm.name" />
</a-form-item>
<a-form-item name="enable" :label="t('page.portal.enable')">
<a-switch v-model:checked="addForm.enable" />
</a-form-item>
<a-form-item name="ssidList" :label="t('page.portal.ssid')">
<a-tree-select
v-model:value="addForm.ssidList"
:tree-data="treeData"
:loading="ssidLoading"
:placeholder="t('page.portal.selectSsid')"
multiple
tree-checkable
:tree-default-expand-all="true"
:show-checked-strategy="TreeSelect.SHOW_CHILD"
style="width: 100%"
/>
</a-form-item>
<!-- 外部Portal服务器配置 -->
<a-form-item name="externalPortal" :label="t('page.portal.externalPortal')">
<div class="flex-col gap-2">
<!-- URL输入框 -->
<a-form-item
:name="['externalPortal', 'serverUrl']"
:label="t('page.portal.url')"
>
<a-input-group compact>
<a-select
v-model:value="addForm.externalPortal.serverUrlScheme"
style="width: 30%"
>
<a-select-option value="http">http://</a-select-option>
<a-select-option value="https">https://</a-select-option>
</a-select>
<a-input
v-model:value="addForm.externalPortal.serverUrl"
style="width: 70%"
:placeholder="t('page.portal.enterUrl')"
/>
</a-input-group>
</a-form-item>
</div>
</a-form-item>
<!-- 身份验证时间配置项仅在非外部Portal认证时显示 -->
<a-form-item
v-if="addForm.authType !== 4"
name="authTimeout"
:label="t('page.portal.authTimeout')"
>
<a-input-group compact>
<a-input-number
v-model:value="addForm.authTimeout.customTimeout"
style="width: 60%"
:min="1"
:max="getMaxTimeout(addForm.authTimeout.customTimeoutUnit)"
/>
<a-select
v-model:value="addForm.authTimeout.customTimeoutUnit"
style="width: 40%"
>
<a-select-option :value="1">{{ t('page.portal.timeUnit.min') }}</a-select-option>
<a-select-option :value="2">{{ t('page.portal.timeUnit.hour') }}</a-select-option>
<a-select-option :value="3">{{ t('page.portal.timeUnit.day') }}</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item name="noAuth" :label="t('page.portal.dailyLimit')">
<a-checkbox v-model:checked="addForm.noAuth.dailyLimitEnable">
{{ t('page.portal.enableDailyLimit') }}
</a-checkbox>
</a-form-item>
<a-form-item name="httpsRedirectEnable" :label="t('page.portal.httpsRedirect')">
<a-checkbox v-model:checked="addForm.httpsRedirectEnable">
{{ t('page.portal.enableHttpsRedirect') }}
</a-checkbox>
</a-form-item>
</a-form>
</a-modal>
<!-- 添加编辑门户对话框 -->
<a-modal
v-model:visible="editVisible"
:title="t('page.portal.editPortal')"
@ok="handleEditConfirm"
width="600px"
>
<a-form
:model="editForm"
:rules="addRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
layout="horizontal"
ref="editFormRef"
>
<a-form-item name="name" :label="t('page.portal.name')">
<a-input v-model:value="editForm.name" />
</a-form-item>
<a-form-item name="enable" :label="t('page.portal.enable')">
<a-switch v-model:checked="editForm.enable" />
</a-form-item>
<a-form-item name="ssidList" :label="t('page.portal.ssid')">
<a-tree-select
v-model:value="editForm.ssidList"
:tree-data="treeData"
:loading="ssidLoading"
:placeholder="t('page.portal.selectSsid')"
multiple
tree-checkable
:tree-default-expand-all="true"
:show-checked-strategy="TreeSelect.SHOW_CHILD"
style="width: 100%"
/>
</a-form-item>
<!-- 外部Portal服务器配置 -->
<a-form-item name="externalPortal" :label="t('page.portal.externalPortal')">
<div class="flex-col gap-2">
<!-- URL输入框 -->
<a-form-item
:name="['externalPortal', 'serverUrl']"
:label="t('page.portal.url')"
>
<a-input-group compact>
<a-select
v-model:value="editForm.externalPortal.serverUrlScheme"
style="width: 30%"
>
<a-select-option value="http">http://</a-select-option>
<a-select-option value="https">https://</a-select-option>
</a-select>
<a-input
v-model:value="editForm.externalPortal.serverUrl"
style="width: 70%"
:placeholder="t('page.portal.enterUrl')"
/>
</a-input-group>
</a-form-item>
</div>
</a-form-item>
<!-- 身份验证时间配置项仅在非外部Portal认证时显示 -->
<a-form-item
v-if="editForm.authType !== 4"
name="authTimeout"
:label="t('page.portal.authTimeout')"
>
<a-input-group compact>
<a-input-number
v-model:value="editForm.authTimeout.customTimeout"
style="width: 60%"
:min="1"
:max="getMaxTimeout(editForm.authTimeout.customTimeoutUnit)"
/>
<a-select
v-model:value="editForm.authTimeout.customTimeoutUnit"
style="width: 40%"
>
<a-select-option :value="1">{{ t('page.portal.timeUnit.min') }}</a-select-option>
<a-select-option :value="2">{{ t('page.portal.timeUnit.hour') }}</a-select-option>
<a-select-option :value="3">{{ t('page.portal.timeUnit.day') }}</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item name="httpsRedirectEnable" :label="t('page.portal.httpsRedirect')">
<a-checkbox v-model:checked="editForm.httpsRedirectEnable">
{{ t('page.portal.enableHttpsRedirect') }}
</a-checkbox>
</a-form-item>
</a-form>
</a-modal>
</div>
</SimpleScrollbar>
</template>
<script setup lang="ts">
import { SimpleScrollbar } from '~/packages/materials/src';
import { ref, onMounted, watch } from 'vue';
import { Card as ACard, Table as ATable, message, TreeSelect, Modal } from 'ant-design-vue';
import { useI18n } from 'vue-i18n';
import {
fetchSiteList,
fetchPortalList,
fetchSsidList,
addPortal,
getPortalConfig,
updatePortalConfig,
deletePortal
} from '@/service/api/auth';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import { useTable } from '@/hooks/common/table';
import type { TreeSelectProps } from 'ant-design-vue';
const { t } = useI18n();
// 站点相关
const siteList = ref<Api.Site.SiteInfo[]>([]);
const selectedSiteId = ref<string>('');
const siteLoading = ref(false);
// 使用 useTable hook
const {
columns,
columnChecks,
data: tableData,
loading: tableLoading,
getData,
mobilePagination,
searchParams,
} = useTable<Api.Portal.Portal>({
apiFn: async (params) => {
if (!selectedSiteId.value) {
return {
data: {
rows: [],
total: 0
}
};
}
return fetchPortalList(selectedSiteId.value, {
pageNum: params.pageNum || 1,
pageSize: params.pageSize || 10
});
},
rowKey: 'id',
columns: () => [
{
title: t('page.portal.name'),
dataIndex: 'name',
key: 'name',
width: 150
},
{
title: 'SSID',
dataIndex: 'ssidNames',
key: 'ssidNames',
width: 200,
customRender: ({ text }: { text: string[] }) => {
return text ? text.join(', ') : '-';
}
},
{
title: t('page.portal.authType'),
dataIndex: 'authType',
key: 'authType',
width: 150,
customRender: ({ text }: { text: number }) => {
const authTypeMap: Record<number, string> = {
0: t('page.portal.auth.none'),
1: t('page.portal.auth.simplePassword'),
2: t('page.portal.auth.externalRadius'),
4: t('page.portal.auth.externalPortal'),
11: t('page.portal.auth.hotspot'),
15: t('page.portal.auth.ldap')
};
return authTypeMap[text] || text;
}
},
{
title: t('common.operate'),
key: 'operate',
width: 120,
fixed: 'right',
align: 'center'
}
]
});
// 获取站点列表
const getSiteList = async () => {
siteLoading.value = true;
const { data, error } = await fetchSiteList({
pageNum: 1,
pageSize: 100
});
if (!error) {
siteList.value = data.rows || [];
// 如果有站点数据,默认选择第一个
if (siteList.value.length > 0) {
selectedSiteId.value = siteList.value[0].siteId;
// 获取第一个站点的数据
await getData();
}
}
siteLoading.value = false;
};
// 处理站点变更
const handleSiteChange = async (value: string) => {
selectedSiteId.value = value;
await getData();
};
// 组件挂载时获取站点列表
onMounted(() => {
getSiteList();
});
// 添加表单相关
const addVisible = ref(false);
const formRef = ref();
const ssidList = ref<Api.Portal.SsidInfo[]>([]);
const ssidLoading = ref(false);
const treeData = ref<TreeSelectProps['treeData']>([]);
// URL 格式校验正则
const URL_PATTERN = new RegExp(
'^(([-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,63})|(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.)){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))' +
'((:([1-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5]))?)' +
'(/[-a-zA-Z0-9@:%_+.~#?&//=]*)?$'
);
const addForm = ref({
name: '',
enable: true,
ssidList: [],
authType: 4, // 固定为外部 Portal 认证
authTimeout: {
customTimeout: 8,
customTimeoutUnit: 2
},
noAuth: {
dailyLimitEnable: false
},
httpsRedirectEnable: false,
landingPage: 1,
pageType: 2,
importedPortalPage: {
id: 'test'
},
externalPortal: {
hostType: 2,
serverUrl: '',
serverUrlScheme: 'http'
}
});
const addRules = {
name: [
{ required: true, message: t('page.portal.nameRequired'), trigger: 'blur' }
],
ssidList: [
{
validator: (_rule: any, value: Record<string, string>) => {
if (Object.keys(value).length === 0) {
return Promise.reject(t('page.portal.ssidRequired'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
externalPortal: [
{
validator: (_rule: any, value: Api.Portal.ExternalPortal) => {
if (addForm.value.authType === 4) {
if (!value.serverUrl) {
return Promise.reject(t('page.portal.urlRequired'));
}
// URL 格式校验
if (!URL_PATTERN.test(value.serverUrl)) {
return Promise.reject(t('page.portal.invalidUrl'));
}
}
return Promise.resolve();
}
}
]
};
// 获取 SSID 列表
const getSsidList = async () => {
if (!selectedSiteId.value) return;
ssidLoading.value = true;
const { data, error } = await fetchSsidList(selectedSiteId.value);
if (!error) {
// 转换数据为树形结构
treeData.value = data.map((group: Api.Portal.WlanGroup) => ({
title: group.wlanName,
value: group.wlanId, // 这个值不会被选中,只是为了符合树结构
key: group.wlanId,
selectable: false, // 禁止选择父节点
children: group.ssidList.map((ssid: Api.Portal.SsidInfo) => ({
title: ssid.ssidName,
value: ssid.ssidId,
key: ssid.ssidId,
isLeaf: true
}))
}));
} else {
message.error(t('common.error'));
}
ssidLoading.value = false;
};
// 根据时间单位获取最大超时时间
const getMaxTimeout = (unit: number) => {
switch (unit) {
case 1: return 1000000; // 分钟
case 2: return 10000; // 小时
case 3: return 1000; // 天
default: return 1000000;
}
};
// 处理添加
const handleAdd = async () => {
addVisible.value = true;
await getSsidList();
};
// 修改添加确认处理函数
const handleAddConfirm = async () => {
await formRef.value?.validate();
const hide = message.loading(t('common.loading'), 0);
// 创建一个新对象来存储要提交的数据
const submitData = { ...addForm.value };
// 设置默认参数
submitData.authTimeout = {
customTimeout: 8,
customTimeoutUnit: 2
};
submitData.externalPortal = {
hostType: 2,
serverUrl: submitData.externalPortal.serverUrl,
serverUrlScheme: submitData.externalPortal.serverUrlScheme
};
const { error } = await addPortal(selectedSiteId.value, submitData);
hide();
if (!error) {
message.success(t('page.portal.addSuccess'));
addVisible.value = false;
getData(); // 刷新列表
}
};
// 添加响应式变量
const currentPortalId = ref<string>('');
const editVisible = ref(false);
const editFormRef = ref();
const editForm = ref({
name: '',
enable: true,
ssidList: [],
authType: 4, // 固定为外部 Portal 认证
authTimeout: {
customTimeout: 8,
customTimeoutUnit: 2
},
httpsRedirectEnable: false,
landingPage: 1,
pageType: 2,
importedPortalPage: {
id: 'test'
},
externalPortal: {
hostType: 2,
serverUrl: '',
serverUrlScheme: 'http'
}
});
// 处理编辑按钮点击
const handleEdit = async (record: Api.Portal.Portal) => {
currentPortalId.value = record.id;
editVisible.value = true;
// 获取 SSID 列表
await getSsidList();
// 获取当前门户的配置
const hide = message.loading(t('common.loading'), 0);
const { data, error } = await getPortalConfig(selectedSiteId.value, currentPortalId.value);
hide();
if (!error && data) {
// 移除 noAuth 字段后再赋值
const { noAuth, ...configWithoutNoAuth } = data;
editForm.value = configWithoutNoAuth;
}
};
// 修改编辑确认处理函数
const handleEditConfirm = async () => {
await editFormRef.value?.validate();
const hide = message.loading(t('common.loading'), 0);
const submitData = {
name: editForm.value.name,
enable: editForm.value.enable,
ssidList: editForm.value.ssidList,
authType: 4, // 固定为外部 Portal 认证
httpsRedirectEnable: editForm.value.httpsRedirectEnable,
landingPage: 1,
pageType: 2,
importedPortalPage: {
id: 'test'
},
authTimeout: {
customTimeout: 8,
customTimeoutUnit: 2
},
externalPortal: {
hostType: 2,
serverUrl: editForm.value.externalPortal.serverUrl,
serverUrlScheme: editForm.value.externalPortal.serverUrlScheme
}
};
const { error } = await updatePortalConfig(selectedSiteId.value, currentPortalId.value, submitData);
hide();
if (!error) {
message.success(t('page.portal.updateSuccess'));
editVisible.value = false;
getData(); // 刷新列表
}
};
// 添加删除门户的 API 函数
const handleDelete = (record: Api.Portal.Portal) => {
Modal.confirm({
title: t('page.portal.confirmDelete'),
content: t('page.portal.deleteConfirmContent', { name: record.name }),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
const hide = message.loading(t('common.loading'), 0);
const { error } = await deletePortal(selectedSiteId.value, record.id);
hide();
if (!error) {
message.success(t('page.portal.deleteSuccess'));
getData(); // 刷新列表
}
}
});
};
</script>
<style scoped>
.card-wrapper {
margin-top: 16px;
}
</style>