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

551 lines
15 KiB
Vue

<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 } from '@/service/api/auth';
import { useI18n } from 'vue-i18n';
import { Form, Modal } 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: '',
});
// 当前编辑的站点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') }]
});
// 处理编辑按钮点击
const handleEdit = async (record: Api.DashboardSite) => {
try {
currentEditSiteId.value = record.siteId;
const response = await getSiteConfig(record.siteId);
if (response.data) {
editFormData.value = {
name: response.data.name,
region: response.data.region,
timeZone: response.data.timeZone,
scenario: response.data.scenario
};
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, editFormData.value);
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"
>
<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>
</AForm>
</AModal>
</ACard>
</template>
<style scoped>
.card-wrapper {
margin-bottom: 16px;
}
.cursor-pointer {
cursor: pointer;
}
.w-240px {
width: 240px;
}
</style>