2
0
Files
fe.wfc/src/views/device/portal/index.vue
2025-02-27 16:12:55 +08:00

853 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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>
<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, // 默认不启用 HTTPS 重定向
landingPage: 1, // 默认登录页面
pageType: 2, // 默认页面类型
importedPortalPage: {
id: 'test' // 默认导入的门户页面 ID
},
externalPortal: {
hostType: 2, // 默认外部 Portal 服务器类型
serverUrl: '', // 默认服务器 URL
serverUrlScheme: 'http' // 默认协议
},
portalCustomize: { // 新增的默认参数
defaultLanguage: 1,
backgroundPictureId: '',
logoPictureId: '',
logoDisplay: true,
inputBoxColor: '',
inputBoxOpacity: 0,
inputTextColor: '',
inputTextOpacity: 0,
buttonColor: '',
buttonOpacity: 0,
buttonTextColor: '',
buttonTextOpacity: 0,
buttonText: '',
// formAuthButtonText: '',
welcomeEnable: true,
// welcomeInformation: '',
// welcomeTextColor: '',
welcomeTextOpacity: 0,
welcomeTextFontSize: 0,
termsOfServiceEnable: true,
termsOfServiceText: '',
termsOfServiceFontSize: 0,
termsOfServiceUrlTexts: [
{
content: '',
text: ''
}
],
copyrightEnable: false,
copyright: '',
// copyrightTextColor: '',
// copyrightTextOpacity: 0,
copyrightTextFontSize: 12,
redirectionCountDownEnable: true,
advertisement: {
enable: false,
pictureIds: [],
// totalDuration: 0,
// pictureInterval: 0,
// skipEnable: true
}
}
});
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,
// 这里可以直接使用默认值
authType: 4, // 固定为外部 Portal 认证
authTimeout: {
customTimeout: 8,
customTimeoutUnit: 2
},
noAuth: {
dailyLimitEnable: false
},
httpsRedirectEnable: false,
landingPage: 1,
pageType: 2,
importedPortalPage: {
id: 'test'
},
externalPortal: {
hostType: 2,
serverUrl: addForm.value.externalPortal.serverUrl, // 确保从 addForm 中获取 serverUrl
serverUrlScheme: addForm.value.externalPortal.serverUrlScheme
},
portalCustomize: { // 新增的默认参数
defaultLanguage: 1,
backgroundPictureId: '',
logoPictureId: '',
logoDisplay: true,
inputBoxColor: "#ffffff",
inputBoxOpacity: 100,
inputTextColor: "#2B2B2B",
inputTextOpacity: 100,
buttonColor: "#00778C",
buttonOpacity: 100,
buttonTextColor: "#FFFFFF",
buttonTextOpacity: 100,
buttonText: "Log In",
// formAuthButtonText: '',
welcomeEnable: false,
// welcomeInformation: '',
// welcomeTextColor: '',
welcomeTextOpacity: 100,
welcomeTextFontSize: 12,
termsOfServiceEnable: false,
termsOfServiceText: "I agree to Terms of Service",
termsOfServiceFontSize: 12,
termsOfServiceUrlTexts: [
{
content: "By accepting this agreement and accessing the wireless network, you acknowledge that you are of legal age, you have read and understood, and agree to be bound by this agreement.",
text: "Terms of Service"
}
],
copyrightEnable: false,
copyright: '',
// copyrightTextColor: '',
// copyrightTextOpacity: 0,
copyrightTextFontSize: 12,
redirectionCountDownEnable: true,
advertisement: {
enable: false,
pictureIds: [],
// totalDuration: 0,
// pictureInterval: 0,
// skipEnable: true
}
}
};
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, // 默认不启用 HTTPS 重定向
landingPage: 1, // 默认登录页面
pageType: 2, // 默认页面类型
importedPortalPage: {
id: 'test' // 默认导入的门户页面 ID
},
externalPortal: {
hostType: 2, // 默认外部 Portal 服务器类型
serverUrl: '', // 默认服务器 URL
serverUrlScheme: 'http' // 默认协议
},
portalCustomize: { // 新增的默认参数
defaultLanguage: 1,
backgroundPictureId: '',
logoPictureId: '',
logoDisplay: true,
inputBoxColor: "#ffffff",
inputBoxOpacity: 100,
inputTextColor: "#2B2B2B",
inputTextOpacity: 100,
buttonColor: "#00778C",
buttonOpacity: 100,
buttonTextColor: "#FFFFFF",
buttonTextOpacity: 100,
buttonText: "Log In",
// formAuthButtonText: '',
welcomeEnable: false,
// welcomeInformation: '',
// welcomeTextColor: '',
welcomeTextOpacity: 100,
welcomeTextFontSize: 12,
termsOfServiceEnable: false,
termsOfServiceText: "I agree to Terms of Service",
termsOfServiceFontSize: 12,
termsOfServiceUrlTexts: [
{
content: "By accepting this agreement and accessing the wireless network, you acknowledge that you are of legal age, you have read and understood, and agree to be bound by this agreement.",
text: "Terms of Service"
}
],
copyrightEnable: false,
copyright: '',
// copyrightTextColor: '',
// copyrightTextOpacity: 0,
copyrightTextFontSize: 12,
redirectionCountDownEnable: true,
advertisement: {
enable: false,
pictureIds: [],
// totalDuration: 0,
// pictureInterval: 0,
// skipEnable: true
}
}
});
// 处理编辑按钮点击
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
},
portalCustomize: { // 新增的默认参数
defaultLanguage: 1,
backgroundPictureId: '',
logoPictureId: '',
logoDisplay: true,
inputBoxColor: "#ffffff",
inputBoxOpacity: 100,
inputTextColor: "#2B2B2B",
inputTextOpacity: 100,
buttonColor: "#00778C",
buttonOpacity: 100,
buttonTextColor: "#FFFFFF",
buttonTextOpacity: 100,
buttonText: "Log In",
// formAuthButtonText: '',
welcomeEnable: false,
// welcomeInformation: '',
// welcomeTextColor: '',
welcomeTextOpacity: 100,
welcomeTextFontSize: 12,
termsOfServiceEnable: false,
termsOfServiceText: "I agree to Terms of Service",
termsOfServiceFontSize: 12,
termsOfServiceUrlTexts: [
{
content: "By accepting this agreement and accessing the wireless network, you acknowledge that you are of legal age, you have read and understood, and agree to be bound by this agreement.",
text: "Terms of Service"
}
],
copyrightEnable: false,
copyright: '',
// copyrightTextColor: '',
// copyrightTextOpacity: 0,
copyrightTextFontSize: 12,
redirectionCountDownEnable: true,
advertisement: {
enable: false,
pictureIds: [],
// totalDuration: 0,
// pictureInterval: 0,
// skipEnable: true
}
}
};
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>