feat:白名单界面
This commit is contained in:
523
src/views/device/access/index.vue
Normal file
523
src/views/device/access/index.vue
Normal file
@@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<SimpleScrollbar>
|
||||
<a-card
|
||||
:bordered="false"
|
||||
style="box-shadow: 0 2px 8px #f0f1f2; border-radius: 8px; padding: 0 24px; margin: 24px;"
|
||||
>
|
||||
<div>
|
||||
<!-- 标题 -->
|
||||
<h2 style="font-weight: bold; font-size: 22px; margin-bottom: 8px;">Access Control</h2>
|
||||
<!-- 站点选择框 -->
|
||||
<div style="display: flex; align-items: center; margin-bottom: 24px;">
|
||||
<a-select
|
||||
v-model:value="selectedSiteId"
|
||||
:loading="siteLoading"
|
||||
placeholder="请选择站点"
|
||||
style="width: 200px; margin-right: 16px;"
|
||||
@change="handleSiteChange"
|
||||
>
|
||||
<a-select-option v-for="site in siteList" :key="site.siteId" :value="site.siteId">
|
||||
{{ site.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- Pre-Authentication Access -->
|
||||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px;">
|
||||
<span>Pre-Authentication Access</span>
|
||||
<a-switch v-model:checked="preAuthEnabled" />
|
||||
<span>Enable</span>
|
||||
<a-tooltip title="Pre-Authentication Access 说明">
|
||||
<i class="anticon anticon-info-circle" style="color: #13c2c2; margin-left: 4px;"></i>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div v-if="preAuthEnabled">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
||||
<span>Pre-Authentication Access List</span>
|
||||
<a-button type="link" style="color: #13c2c2; font-weight: bold;" @click="onAddPreAuth"><i class="anticon anticon-plus-circle" /> Add</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
:columns="preAuthColumns"
|
||||
:dataSource="preAuthList"
|
||||
:pagination="false"
|
||||
bordered
|
||||
size="middle"
|
||||
rowKey="key"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-tooltip title="删除">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
style="color: #ff4d4f"
|
||||
@click="() => handleDeletePreAuth(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- Authentication-Free Client -->
|
||||
<div style="margin: 32px 0 16px 0; display: flex; align-items: center; gap: 12px;">
|
||||
<span>Authentication-Free Client</span>
|
||||
<a-switch v-model:checked="freeClientEnabled" />
|
||||
<span>Enable</span>
|
||||
<a-tooltip title="Authentication-Free Client 说明">
|
||||
<i class="anticon anticon-info-circle" style="color: #13c2c2; margin-left: 4px;"></i>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div v-if="freeClientEnabled">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
||||
<span>Authentication-Free Client List</span>
|
||||
<a-button type="link" style="color: #13c2c2; font-weight: bold;" @click="onAddFreeClient"><i class="anticon anticon-plus-circle" /> Add</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
:columns="freeClientColumns"
|
||||
:dataSource="freeClientList"
|
||||
:pagination="false"
|
||||
bordered
|
||||
size="middle"
|
||||
rowKey="key"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-tooltip title="删除">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
style="color: #ff4d4f"
|
||||
@click="() => handleDeleteFreeClient(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div style="margin-top: 32px; display: flex; gap: 16px;">
|
||||
<a-button type="primary" @click="handleApply">Apply</a-button>
|
||||
<a-button @click="handleCancel">Cancel</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
<!-- 新增:添加预认证白名单弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="addPreAuthVisible"
|
||||
title="Add Pre-Authentication Access"
|
||||
@ok="handleAddPreAuthOk"
|
||||
@cancel="() => addPreAuthVisible = false"
|
||||
destroyOnClose
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="TYPE">
|
||||
<a-select v-model:value="addPreAuthType" style="width: 100%;">
|
||||
<a-select-option :value="1">IP</a-select-option>
|
||||
<a-select-option :value="2">URL</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="addPreAuthType === 1" label="IP">
|
||||
<div style="display: flex; align-items: center; gap: 4px;">
|
||||
<a-input v-model:value="addPreAuthIpParts[0]" style="width: 56px;" :maxlength="3" />
|
||||
<span>.</span>
|
||||
<a-input v-model:value="addPreAuthIpParts[1]" style="width: 56px;" :maxlength="3" />
|
||||
<span>.</span>
|
||||
<a-input v-model:value="addPreAuthIpParts[2]" style="width: 56px;" :maxlength="3" />
|
||||
<span>.</span>
|
||||
<a-input v-model:value="addPreAuthIpParts[3]" style="width: 56px;" :maxlength="3" />
|
||||
<span>/</span>
|
||||
<a-input v-model:value="addPreAuthIpParts[4]" style="width: 56px;" :maxlength="2" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="addPreAuthType === 2" label="URL">
|
||||
<a-input-group compact>
|
||||
<span style="display: inline-block; width: 90px; text-align: center; background: #f5f5f5; border: 1px solid #d9d9d9; border-right: none; border-radius: 6px 0 0 6px; line-height: 32px;">http(s)://</span>
|
||||
<a-input v-model:value="addPreAuthValue" style="width: calc(100% - 90px); border-radius: 0 6px 6px 0;" placeholder="请输入URL" />
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<!-- 新增:添加免认证白名单弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="addFreeClientVisible"
|
||||
title="Add Authentication-Free Client"
|
||||
@ok="handleAddFreeClientOk"
|
||||
@cancel="() => addFreeClientVisible = false"
|
||||
destroyOnClose
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="TYPE">
|
||||
<a-select v-model:value="addFreeClientType" style="width: 100%;">
|
||||
<a-select-option :value="3">IP</a-select-option>
|
||||
<a-select-option :value="4">MAC</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="addFreeClientType === 3" label="IP">
|
||||
<div style="display: flex; align-items: center; gap: 4px;">
|
||||
<a-input v-model:value="addFreeClientIpParts[0]" style="width: 56px;" :maxlength="3" />
|
||||
<span>.</span>
|
||||
<a-input v-model:value="addFreeClientIpParts[1]" style="width: 56px;" :maxlength="3" />
|
||||
<span>.</span>
|
||||
<a-input v-model:value="addFreeClientIpParts[2]" style="width: 56px;" :maxlength="3" />
|
||||
<span>.</span>
|
||||
<a-input v-model:value="addFreeClientIpParts[3]" style="width: 56px;" :maxlength="3" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="addFreeClientType === 4" label="MAC">
|
||||
<div style="display: flex; align-items: center; gap: 4px;">
|
||||
<a-input v-model:value="addFreeClientMacParts[0]" style="width: 40px;" :maxlength="2" />
|
||||
<span>-</span>
|
||||
<a-input v-model:value="addFreeClientMacParts[1]" style="width: 40px;" :maxlength="2" />
|
||||
<span>-</span>
|
||||
<a-input v-model:value="addFreeClientMacParts[2]" style="width: 40px;" :maxlength="2" />
|
||||
<span>-</span>
|
||||
<a-input v-model:value="addFreeClientMacParts[3]" style="width: 40px;" :maxlength="2" />
|
||||
<span>-</span>
|
||||
<a-input v-model:value="addFreeClientMacParts[4]" style="width: 40px;" :maxlength="2" />
|
||||
<span>-</span>
|
||||
<a-input v-model:value="addFreeClientMacParts[5]" style="width: 40px;" :maxlength="2" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</SimpleScrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, h } from 'vue'
|
||||
import { fetchSiteList, fetchAccessControl, updateAccessControl } from '@/service/api/auth'
|
||||
import { DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||
const preAuthEnabled = ref(true)
|
||||
const freeClientEnabled = ref(true)
|
||||
const preAuthList = ref([]) // 预认证白名单列表
|
||||
const freeClientList = ref([]) // 免认证白名单列表
|
||||
|
||||
const siteList = ref<{ siteId: string, name: string }[]>([])
|
||||
const selectedSiteId = ref('')
|
||||
const siteLoading = ref(false)
|
||||
|
||||
// 新增:添加预认证白名单弹窗相关变量
|
||||
const addPreAuthVisible = ref(false)
|
||||
const addPreAuthType = ref(1) // 1: IP, 2: URL
|
||||
const addPreAuthValue = ref('')
|
||||
const addPreAuthIpParts = ref(['', '', '', '', '']) // [ip1, ip2, ip3, ip4, mask]
|
||||
|
||||
// 新增:添加免认证白名单弹窗相关变量
|
||||
const addFreeClientVisible = ref(false)
|
||||
const addFreeClientType = ref(3) // 3: IP, 4: MAC
|
||||
const addFreeClientIpParts = ref(['', '', '', ''])
|
||||
const addFreeClientMacParts = ref(['', '', '', '', '', ''])
|
||||
|
||||
// 定义表格列配置
|
||||
const preAuthColumns = [
|
||||
{
|
||||
title: 'TYPE',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
customRender: ({ record }: { record: any }) => {
|
||||
if (record.type === 1) return 'IP'
|
||||
if (record.type === 2) return 'URL'
|
||||
return record.type
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'INFORMATION',
|
||||
dataIndex: 'url',
|
||||
key: 'url',
|
||||
customRender: ({ record }: { record: any }) => {
|
||||
if (record.type === 1) return record.ip
|
||||
if (record.type === 2) return record.url
|
||||
return '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'ACTION',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
customRender: ({ record }: { record: any }) => {
|
||||
return h(
|
||||
'a-button',
|
||||
{
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
style: { color: '#ff4d4f' },
|
||||
onClick: () => handleDeletePreAuth(record)
|
||||
},
|
||||
{
|
||||
icon: () => h(DeleteOutlined)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const freeClientColumns = [
|
||||
{
|
||||
title: 'TYPE',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
customRender: ({ record }: { record: any }) => {
|
||||
if (record.type === 3) return 'IP'
|
||||
if (record.type === 4) return 'MAC'
|
||||
return record.type
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'INFORMATION',
|
||||
dataIndex: 'clientIp',
|
||||
key: 'clientIp',
|
||||
customRender: ({ record }: { record: any }) => {
|
||||
if (record.type === 3) return record.clientIp
|
||||
if (record.type === 4) return record.clientMac
|
||||
return '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'ACTION',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
customRender: ({ record }: { record: any }) => {
|
||||
return h(
|
||||
'a-button',
|
||||
{
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
style: { color: '#ff4d4f' },
|
||||
onClick: () => handleDeleteFreeClient(record)
|
||||
},
|
||||
{
|
||||
icon: () => h(DeleteOutlined)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const getSiteList = async () => {
|
||||
siteLoading.value = true
|
||||
const { data, error } = await fetchSiteList({ pageNum: 1, pageSize: 100 })
|
||||
console.log('siteList接口返回', data, error)
|
||||
if (!error) {
|
||||
siteList.value = data.rows || []
|
||||
console.log('siteList.value', siteList.value)
|
||||
if (siteList.value.length > 0) {
|
||||
selectedSiteId.value = siteList.value[0].siteId
|
||||
console.log('getSiteList设置selectedSiteId', selectedSiteId.value)
|
||||
await getAccessControl()
|
||||
}
|
||||
}
|
||||
siteLoading.value = false
|
||||
}
|
||||
|
||||
const getAccessControl = async () => {
|
||||
console.log('getAccessControl调用,selectedSiteId', selectedSiteId.value)
|
||||
if (!selectedSiteId.value) return
|
||||
const { data, error } = await fetchAccessControl(selectedSiteId.value)
|
||||
console.log('accessControl接口返回', data, error)
|
||||
if (!error && data) {
|
||||
preAuthEnabled.value = !!data.preAuthAccessEnable
|
||||
freeClientEnabled.value = !!data.freeAuthClientEnable
|
||||
preAuthList.value = (data.preAuthAccessPolicies || []).map((item: any) => ({
|
||||
key: String(item.idInt),
|
||||
type: item.type,
|
||||
ip: item.ip,
|
||||
url: item.url,
|
||||
...item
|
||||
}))
|
||||
freeClientList.value = (data.freeAuthClientPolicies || []).map((item: any) => ({
|
||||
key: String(item.idInt),
|
||||
type: item.type,
|
||||
clientIp: item.clientIp,
|
||||
clientMac: item.clientMac,
|
||||
...item
|
||||
}))
|
||||
console.log('preAuthList', preAuthList.value)
|
||||
console.log('freeClientList', freeClientList.value)
|
||||
} else {
|
||||
console.log('getAccessControl未进入赋值分支', { error, data })
|
||||
}
|
||||
console.log('getAccessControl结束', preAuthList.value, freeClientList.value)
|
||||
}
|
||||
|
||||
const handleSiteChange = async (value: string) => {
|
||||
selectedSiteId.value = value
|
||||
// 不需要手动调用 getAccessControl,watch 会自动触发
|
||||
}
|
||||
|
||||
const onAddPreAuth = () => {
|
||||
addPreAuthType.value = 1
|
||||
addPreAuthValue.value = ''
|
||||
addPreAuthIpParts.value = ['', '', '', '', '']
|
||||
addPreAuthVisible.value = true
|
||||
}
|
||||
|
||||
const handleAddPreAuthOk = () => {
|
||||
if (addPreAuthType.value === 1) {
|
||||
// 拼接IP
|
||||
const ip = addPreAuthIpParts.value.slice(0, 4).join('.')
|
||||
const mask = addPreAuthIpParts.value[4]
|
||||
const ipValue = mask ? `${ip}/${mask}` : ip
|
||||
preAuthList.value.push({
|
||||
key: Date.now() + Math.random(), // 简单唯一key
|
||||
type: 1,
|
||||
ip: ipValue,
|
||||
url: null
|
||||
})
|
||||
} else {
|
||||
// 只保存输入框内容,不带http(s)://
|
||||
preAuthList.value.push({
|
||||
key: Date.now() + Math.random(),
|
||||
type: 2,
|
||||
ip: null,
|
||||
url: addPreAuthValue.value
|
||||
})
|
||||
}
|
||||
addPreAuthVisible.value = false
|
||||
}
|
||||
|
||||
const onAddFreeClient = () => {
|
||||
addFreeClientType.value = 3
|
||||
addFreeClientIpParts.value = ['', '', '', '']
|
||||
addFreeClientMacParts.value = ['', '', '', '', '', '']
|
||||
addFreeClientVisible.value = true
|
||||
}
|
||||
|
||||
const handleAddFreeClientOk = () => {
|
||||
if (addFreeClientType.value === 3) {
|
||||
// 拼接IP
|
||||
const ip = addFreeClientIpParts.value.join('.')
|
||||
freeClientList.value.push({
|
||||
key: Date.now() + Math.random(),
|
||||
type: 3,
|
||||
clientIp: ip,
|
||||
clientMac: null
|
||||
})
|
||||
} else {
|
||||
// 拼接MAC
|
||||
const mac = addFreeClientMacParts.value.map(x => x.padStart(2, '0')).join('-')
|
||||
freeClientList.value.push({
|
||||
key: Date.now() + Math.random(),
|
||||
type: 4,
|
||||
clientIp: null,
|
||||
clientMac: mac
|
||||
})
|
||||
}
|
||||
addFreeClientVisible.value = false
|
||||
}
|
||||
|
||||
const handleDeletePreAuth = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该条记录吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
preAuthList.value = preAuthList.value.filter(item => item.key !== record.key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteFreeClient = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该条记录吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
freeClientList.value = freeClientList.value.filter(item => item.key !== record.key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听 selectedSiteId,变化时自动拉取白名单数据
|
||||
watch(selectedSiteId, (val) => {
|
||||
console.log('watch触发,selectedSiteId变为', val)
|
||||
if (val) getAccessControl()
|
||||
})
|
||||
|
||||
// 在 script setup 中添加提交函数
|
||||
const handleApply = async () => {
|
||||
if (!selectedSiteId.value) {
|
||||
message.warning('请先选择站点')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 组装提交数据
|
||||
const submitData = {
|
||||
preAuthAccessEnable: preAuthEnabled.value,
|
||||
freeAuthClientEnable: freeClientEnabled.value,
|
||||
preAuthAccessPolicies: preAuthList.value.map(item => ({
|
||||
type: item.type,
|
||||
ip: item.type === 1 ? item.ip : undefined,
|
||||
url: item.type === 2 ? item.url : undefined
|
||||
})),
|
||||
freeAuthClientPolicies: freeClientList.value.map(item => ({
|
||||
type: item.type,
|
||||
clientIp: item.type === 3 ? item.clientIp : undefined,
|
||||
clientMac: item.type === 4 ? item.clientMac : undefined
|
||||
}))
|
||||
}
|
||||
|
||||
// 调用更新接口
|
||||
const { error } = await updateAccessControl(selectedSiteId.value, submitData)
|
||||
if (error) {
|
||||
message.error('提交失败')
|
||||
return
|
||||
}
|
||||
|
||||
message.success('提交成功')
|
||||
// 重新获取最新数据
|
||||
await getAccessControl()
|
||||
} catch (e) {
|
||||
console.error('提交出错:', e)
|
||||
message.error('提交失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 在 script setup 中添加 Cancel 处理函数
|
||||
const handleCancel = async () => {
|
||||
if (!selectedSiteId.value) {
|
||||
message.warning('请先选择站点')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await getAccessControl()
|
||||
message.success('已刷新数据')
|
||||
} catch (e) {
|
||||
console.error('刷新数据出错:', e)
|
||||
message.error('刷新数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('onMounted执行')
|
||||
await getSiteList()
|
||||
console.log('onMounted后selectedSiteId', selectedSiteId.value)
|
||||
if (selectedSiteId.value) {
|
||||
await getAccessControl()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.anticon-info-circle {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user