2
0
Files
fe.wfc/src/views/dashboard/modules/card-data.vue

649 lines
20 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.
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { TableColumnType } from 'ant-design-vue';
import {
EnvironmentOutlined,
SearchOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import { getDashboardSiteList, addSite, deleteSite, getSiteConfig, updateSite, getMeshConfig, updateMeshConfig, getRoamingConfig, updateRoamingConfig } from '@/service/api/auth';
import { useI18n } from 'vue-i18n';
import { Form, Modal, Divider, Checkbox, Select, Input, Button } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { regionOptions, timeZoneOptions } from '@/constants/site-options';
const { t } = useI18n();
defineOptions({
name: 'CardData'
});
// 表单实例
const formRef = ref();
const useForm = Form.useForm;
// 弹窗控制
const showAddDialog = ref(false);
// 表单数据
const formData = ref({
name: '',
region: '',
timeZone: '',
scenario: '',
deviceAccountSetting: {
username: '',
password: ''
}
});
// 表单验证规则
const { validate, validateInfos } = useForm(formData, {
name: [
{ required: true, message: t('page.carddata.nameRequired') },
{
pattern: /^[^ \+\-\@\=]$|^[^ \+\-\@\=].{0,62}[^ ]$/,
message: t('page.carddata.nameInvalid')
}
],
region: [{ required: true, message: t('page.carddata.regionRequired') }],
timeZone: [{ required: true, message: t('page.carddata.timeZoneRequired') }],
scenario: [{ required: true, message: t('page.carddata.scenarioRequired') }],
'deviceAccountSetting.username': [
{ required: true, message: t('page.carddata.usernameRequired') },
{
pattern: /^[\x21-\x7E]{1,64}$/,
message: t('page.carddata.usernameInvalid')
}
],
'deviceAccountSetting.password': [
{ required: true, message: t('page.carddata.passwordRequired') },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\!\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\@\[\\\]\^\_\`\{\|\}\~])(?!.*[\00-\040\042\077\0177]).{8,64}$/,
message: t('page.carddata.passwordInvalid')
}
]
});
// 使用场景选项
const scenarioOptions = [
{ label: t('page.carddata.office'), value: 'office' },
{ label: t('page.carddata.hotel'), value: 'hotel' },
{ label: t('page.carddata.education'), value: 'education' },
{ label: t('page.carddata.retail'), value: 'retail' },
{ label: t('page.carddata.other'), value: 'other' }
];
// 处理添加站点
const handleAddSite = async () => {
try {
await validate();
const response = await addSite(formData.value);
if (response) {
message.success(t('page.carddata.addSuccess'));
showAddDialog.value = false;
fetchSiteList(); // 刷新列表
}
} catch (error) {
console.error('Add site failed:', error);
message.error(t('page.carddata.addFailed'));
}
};
// 打开添加对话框
const openAddDialog = () => {
formData.value = {
name: '',
region: '',
timeZone: '',
scenario: '',
deviceAccountSetting: {
username: '',
password: ''
}
};
showAddDialog.value = true;
};
// 搜索和分页状态
const searchValue = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const loading = ref(false);
const total = ref(0);
const siteData = ref<Api.DashboardSite[]>([]);
const fetchSiteList = async () => {
try {
loading.value = true;
const { data } = await getDashboardSiteList({
pageNum: currentPage.value,
pageSize: pageSize.value,
searchKey: searchValue.value
});
console.log('API Response:', data);
if (data) {
siteData.value = data.rows || [];
total.value = data.total || 0;
console.log('Processed Site Data:', siteData.value);
console.log('Total:', total.value);
} else {
siteData.value = [];
total.value = 0;
}
}catch (error){
console.error('Failed to fetch site list:', error);
siteData.value = [];
total.value = 0;
}finally {
loading.value = false;
}
};
// 修改监听方式
watch([currentPage, pageSize, searchValue], () => {
fetchSiteList();
}, { immediate: true });
// 表格列定义
const columns: TableColumnType<Api.DashboardSite>[] = [
{
title: t('page.carddata.sitename'),
key: 'name',
dataIndex: 'name'
},
{
title: t('page.carddata.country'),
key: 'region',
dataIndex: 'region'
},
{
title: t('page.carddata.alert'),
key: 'alerts',
width: 100
},
// {
// title: t('page.carddata.gateway'),
// key: 'gateway',
// width: 100
// },
// {
// title: t('page.carddata.switches'),
// key: 'switches',
// width: 100
// },
// {
// title: 'OLTS',
// key: 'olts',
// width: 100
// },
{
title: 'EAPS',
key: 'eaps',
width: 100
},
{
title: t('page.carddata.clients'),
key: 'clients',
width: 150
},
{
title: 'ACTION',
key: 'action',
width: 100,
fixed: 'right'
}
];
// 按钮操作处理函数
//
// const handleCopy = (record: Api.DashboardSite) => {
// console.log('Copy:', record);
// };
//
const handleDelete = (record: Api.DashboardSite) => {
Modal.confirm({
title: t('page.carddata.deleteConfirmTitle'),
content: t('page.carddata.deleteConfirmContent', { name: record.name }),
okText: t('page.carddata.confirm'),
cancelText: t('page.carddata.cancel'),
okType: 'danger',
async onOk() {
try {
await deleteSite(record.siteId);
message.success(t('page.carddata.deleteSuccess'));
fetchSiteList(); // 刷新列表
} catch (error) {
}
}
});
};
//
// const handleHome = (record: Api.DashboardSite) => {
// console.log('Home:', record);
// };
// 分页处理函数
const handlePageChange = (page: number, pageSize: number) => {
currentPage.value = page;
if (pageSize !== undefined) {
handlePageSizeChange(pageSize);
}
};
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
};
// 编辑弹窗控制
const showEditDialog = ref(false);
// 编辑表单数据
const editFormData = ref({
name: '',
region: '',
timeZone: '',
scenario: '',
meshEnable: false,
autoFailoverEnable: false,
defGatewayEnable: true,
gateway: '',
fastRoamingEnable: false,
nonStickRoamingEnable: false,
aiRoamingEnable: false
});
// 当前编辑的站点ID
const currentEditSiteId = ref('');
// 编辑表单验证规则
const { validate: validateEdit, validateInfos: validateEditInfos } = useForm(editFormData, {
name: [
{ required: true, message: t('page.carddata.nameRequired') },
{
pattern: /^[^ \+\-\@\=]$|^[^ \+\-\@\=].{0,62}[^ ]$/,
message: t('page.carddata.nameInvalid')
}
],
region: [{ required: true, message: t('page.carddata.regionRequired') }],
timeZone: [{ required: true, message: t('page.carddata.timeZoneRequired') }],
scenario: [{ required: true, message: t('page.carddata.scenarioRequired') }]
});
// IP分段输入
const gatewayIpParts = ref(['', '', '', '']);
// 拆分IP到4段
function splitGatewayIp(ip: string) {
if (!ip) return ['', '', '', ''];
const parts = ip.split('.');
return [parts[0] || '', parts[1] || '', parts[2] || '', parts[3] || ''];
}
// 拼接4段为IP
function joinGatewayIp(parts: string[]) {
return parts.map(p => p.trim()).join('.');
}
// 弹窗打开时同步分段
watch(() => editFormData.value.gateway, (val) => {
gatewayIpParts.value = splitGatewayIp(val);
}, { immediate: true });
// 分段输入时同步到gateway
watch(gatewayIpParts, (val) => {
editFormData.value.gateway = joinGatewayIp(val);
}, { deep: true });
// 处理编辑按钮点击
const handleEdit = async (record: Api.DashboardSite) => {
try {
currentEditSiteId.value = record.siteId;
const [siteRes, meshRes, roamingRes] = await Promise.all([
getSiteConfig(record.siteId),
getMeshConfig(record.siteId),
getRoamingConfig(record.siteId)
]);
if (siteRes.data) {
editFormData.value = {
name: siteRes.data.name,
region: siteRes.data.region,
timeZone: siteRes.data.timeZone,
scenario: siteRes.data.scenario,
meshEnable: meshRes.data?.mesh?.meshEnable ?? false,
autoFailoverEnable: meshRes.data?.mesh?.autoFailoverEnable ?? false,
defGatewayEnable: meshRes.data?.mesh?.defGatewayEnable ?? true,
gateway: meshRes.data?.mesh?.gateway ?? '',
fastRoamingEnable: roamingRes.data?.roaming?.fastRoamingEnable ?? false,
nonStickRoamingEnable: roamingRes.data?.roaming?.nonStickRoamingEnable ?? false,
aiRoamingEnable: roamingRes.data?.roaming?.aiRoamingEnable ?? false
};
gatewayIpParts.value = splitGatewayIp(editFormData.value.gateway);
showEditDialog.value = true;
}
} catch (error) {
console.error('Get site config failed:', error);
message.error(t('page.carddata.getConfigFailed'));
}
};
// 处理更新站点
const handleUpdateSite = async () => {
try {
await validateEdit();
// 先保存基础配置
await updateSite(currentEditSiteId.value, {
name: editFormData.value.name,
region: editFormData.value.region,
timeZone: editFormData.value.timeZone,
scenario: editFormData.value.scenario
});
// mesh 配置为嵌套结构meshEnable为true时才携带defGatewayEnable和gateway
const meshData: any = { mesh: { meshEnable: editFormData.value.meshEnable } };
if (editFormData.value.meshEnable) {
meshData.mesh.autoFailoverEnable = editFormData.value.autoFailoverEnable;
meshData.mesh.defGatewayEnable = editFormData.value.defGatewayEnable;
if (editFormData.value.defGatewayEnable === false) {
meshData.mesh.gateway = editFormData.value.gateway;
}
}
// roaming 配置为嵌套结构
const roamingData: any = { roaming: { fastRoamingEnable: editFormData.value.fastRoamingEnable } };
if (editFormData.value.fastRoamingEnable) {
roamingData.roaming.nonStickRoamingEnable = editFormData.value.nonStickRoamingEnable;
roamingData.roaming.aiRoamingEnable = editFormData.value.aiRoamingEnable;
}
await Promise.all([
updateMeshConfig(currentEditSiteId.value, meshData),
updateRoamingConfig(currentEditSiteId.value, roamingData)
]);
message.success(t('page.carddata.updateSuccess'));
showEditDialog.value = false;
fetchSiteList(); // 刷新列表
} catch (error) {
console.error('Update site failed:', error);
message.error(t('page.carddata.updateFailed'));
}
};
</script>
<template>
<ACard :bordered="false" size="small" class="card-wrapper">
<div class="flex justify-between items-center mb-16px">
<div class="flex items-center gap-8px">
<span class="text-16px font-medium">{{ t('page.carddata.sitelist') }}</span>
</div>
<div class="flex items-center gap-16px">
<AInput
v-model:value="searchValue"
:placeholder="t('page.carddata.search')"
class="w-240px"
allow-clear
>
<template #prefix>
<search-outlined />
</template>
</AInput>
<AButton type="primary" @click="openAddDialog">
<template #icon>
<plus-outlined />
</template>
{{ t('page.carddata.addsite') }}
</AButton>
</div>
</div>
<ATable
:columns="columns"
:data-source="siteData"
:pagination="{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${t('page.carddata.total')} ${total} `,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange
}"
row-key="siteId"
:scroll="{ x: 1200 }"
:loading="loading"
>
<!-- 添加一个调试信息 -->
<template #headerCell="{ column }">
<span>{{ column.title }}</span>
</template>
<template #bodyCell="{ column, record }">
<!-- 添加调试信息 -->
{{ console.log('Rendering cell:', column.key, record) }}
<template v-if="column.key === 'name'">
<div class="flex items-center gap-8px">
<environment-outlined class="text-16px" />
<span>{{ record.name }}</span>
</div>
</template>
<template v-else-if="column.key === 'region'">
<span>{{ record.region }}</span>
</template>
<template v-else-if="column.key === 'alerts'">
<span>0</span>
</template>
<template v-else-if="column.key === 'gateway'">
<span>{{ record.connectedGatewayNum }}/{{ record.disconnectedGatewayNum }}</span>
</template>
<template v-else-if="column.key === 'switches'">
<span>{{ record.connectedSwitchNum }}/{{ record.disconnectedSwitchNum }}</span>
</template>
<template v-else-if="column.key === 'olts'">
<span>0/0</span>
</template>
<template v-else-if="column.key === 'eaps'">
<span>{{ record.connectedApNum }}/{{ record.disconnectedApNum }}/{{ record.isolatedApNum }}</span>
</template>
<template v-else-if="column.key === 'clients'">
<span>{{ record.wiredClientNum }}/{{ record.wirelessClientNum }}/{{ record.guestNum }}</span>
</template>
<template v-else-if="column.key === 'action'">
<div class="flex items-center gap-8px">
<edit-outlined class="cursor-pointer text-primary" @click="handleEdit(record)" />
<!-- <copy-outlined class="cursor-pointer text-primary" @click="handleCopy(record)" />-->
<delete-outlined class="cursor-pointer text-red-500" @click="handleDelete(record)" />
<!-- <home-outlined class="cursor-pointer text-primary" @click="handleHome(record)" />-->
</div>
</template>
</template>
</ATable>
<!-- 添加站点对话框 -->
<AModal
v-model:visible="showAddDialog"
:title="t('page.carddata.addsite')"
@ok="handleAddSite"
@cancel="showAddDialog = false"
:maskClosable="false"
>
<AForm
ref="formRef"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<AFormItem
name="name"
:label="t('page.carddata.sitename')"
v-bind="validateInfos.name"
>
<AInput v-model:value="formData.name" />
</AFormItem>
<AFormItem
name="region"
:label="t('page.carddata.region')"
v-bind="validateInfos.region"
>
<ASelect
v-model:value="formData.region"
:options="regionOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="timeZone"
:label="t('page.carddata.timezone')"
v-bind="validateInfos.timeZone"
>
<ASelect
v-model:value="formData.timeZone"
:options="timeZoneOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="scenario"
:label="t('page.carddata.scenario')"
v-bind="validateInfos.scenario"
>
<ASelect
v-model:value="formData.scenario"
:options="scenarioOptions"
/>
</AFormItem>
<AFormItem
name="deviceAccountSetting.username"
:label="t('page.carddata.username')"
v-bind="validateInfos['deviceAccountSetting.username']"
>
<AInput v-model:value="formData.deviceAccountSetting.username" />
</AFormItem>
<AFormItem
name="deviceAccountSetting.password"
:label="t('page.carddata.password')"
v-bind="validateInfos['deviceAccountSetting.password']"
>
<AInputPassword v-model:value="formData.deviceAccountSetting.password" />
</AFormItem>
</AForm>
</AModal>
<!-- 编辑站点对话框 -->
<AModal
v-model:visible="showEditDialog"
:title="t('page.carddata.editsite')"
@ok="handleUpdateSite"
@cancel="showEditDialog = false"
:maskClosable="false"
:width="700"
>
<AForm
:model="editFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<AFormItem
name="name"
:label="t('page.carddata.sitename')"
v-bind="validateEditInfos.name"
>
<AInput v-model:value="editFormData.name" />
</AFormItem>
<AFormItem
name="region"
:label="t('page.carddata.region')"
v-bind="validateEditInfos.region"
>
<ASelect
v-model:value="editFormData.region"
:options="regionOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="timeZone"
:label="t('page.carddata.timezone')"
v-bind="validateEditInfos.timeZone"
>
<ASelect
v-model:value="editFormData.timeZone"
:options="timeZoneOptions"
show-search
:filter-option="(input, option) =>
option?.label?.toLowerCase().includes(input.toLowerCase())"
/>
</AFormItem>
<AFormItem
name="scenario"
:label="t('page.carddata.scenario')"
v-bind="validateEditInfos.scenario"
>
<ASelect
v-model:value="editFormData.scenario"
:options="scenarioOptions"
/>
</AFormItem>
<a-divider />
<div style="margin-bottom: 8px; font-weight: bold;">组网配置</div>
<AFormItem>
<a-checkbox v-model:checked="editFormData.meshEnable">Mesh</a-checkbox>
</AFormItem>
<AFormItem v-if="editFormData.meshEnable" style="margin-left: 24px;">
<ASelect v-model:value="editFormData.defGatewayEnable" style="width: 200px;">
<ASelectOption :value="true">AutoRecommended</ASelectOption>
<ASelectOption :value="false">Custom</ASelectOption>
</ASelect>
<template v-if="editFormData.defGatewayEnable === false">
<div style="display: inline-flex; align-items: center; margin-left: 12px;">
<AInput v-for="(part, idx) in gatewayIpParts" :key="idx" v-model:value="gatewayIpParts[idx]" maxlength="3" style="width: 48px; text-align: center; margin-right: 4px;" />
<span v-if="idx < 3" v-for="idx in 3" :key="'dot'+idx">.</span>
</div>
</template>
</AFormItem>
<AFormItem v-if="editFormData.meshEnable" style="margin-left: 24px;">
<a-checkbox v-model:checked="editFormData.autoFailoverEnable">Auto Failover</a-checkbox>
</AFormItem>
<AFormItem>
<a-checkbox v-model:checked="editFormData.fastRoamingEnable">Fast Roaming</a-checkbox>
</AFormItem>
<AFormItem v-if="editFormData.fastRoamingEnable" style="margin-left: 24px;">
<a-checkbox v-model:checked="editFormData.nonStickRoamingEnable">Non-Stick Roaming</a-checkbox>
</AFormItem>
<AFormItem v-if="editFormData.fastRoamingEnable" style="margin-left: 24px;">
<a-checkbox v-model:checked="editFormData.aiRoamingEnable">AI Roaming</a-checkbox>
</AFormItem>
</AForm>
</AModal>
</ACard>
</template>
<style scoped>
.card-wrapper {
margin-bottom: 16px;
}
.cursor-pointer {
cursor: pointer;
}
.w-240px {
width: 240px;
}
</style>