jcloud/dashboard/src2/components/JsiteServerFirewallRules.vue

606 lines
18 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>
<div class="space-y-6">
<!-- 标题和操作按钮 -->
<div class="flex items-center justify-between">
<div>
</div>
<div class="flex gap-2">
<Button
@click="refreshRules"
:loading="$resources.firewallRules.loading"
variant="outline"
size="sm"
>
<RefreshCwIcon class="h-4 w-4 mr-1" />
刷新
</Button>
<Button
v-if="selectedRules.length > 0"
@click="batchDeleteRules"
variant="outline"
size="sm"
class="text-red-600 hover:text-red-700"
>
<Trash2Icon class="h-4 w-4 mr-1" />
删除选中 ({{ selectedRules.length }})
</Button>
<Button
@click="addNewRow"
variant="solid"
size="sm"
>
<PlusIcon class="h-4 w-4 mr-1" />
添加规则
</Button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="$resources.firewallRules.loading" class="flex items-center justify-center py-12">
<div class="flex items-center space-x-2">
<Loader2Icon class="h-5 w-5 animate-spin text-gray-500" />
<span class="text-gray-500">正在加载防火墙规则...</span>
</div>
</div>
<!-- 错误状态 -->
<div v-else-if="$resources.firewallRules.error" class="rounded-md border border-red-200 bg-red-50 p-4">
<div class="flex items-center">
<AlertCircleIcon class="h-5 w-5 text-red-400 mr-2" />
<div>
<h3 class="text-sm font-medium text-red-800">加载失败</h3>
<p class="text-sm text-red-700 mt-1">{{ $resources.firewallRules.error }}</p>
</div>
</div>
</div>
<!-- 防火墙规则列表 -->
<div v-else-if="!$resources.firewallRules.loading && !$resources.firewallRules.error" class="space-y-4">
<div class="overflow-hidden rounded-md border border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<input
type="checkbox"
class="rounded border-gray-300"
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
/>
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
规则ID
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
协议
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
端口范围
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
来源IP
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
备注
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<!-- 现有规则 -->
<tr v-for="(rule, index) in firewallRules" :key="rule.rule_id || `temp-${index}`" class="hover:bg-gray-50">
<td class="px-4 py-4 whitespace-nowrap">
<input
type="checkbox"
class="rounded border-gray-300"
:checked="selectedRules.includes(String(rule.rule_id))"
@change="toggleRuleSelection(rule.rule_id)"
:disabled="rule.isNew"
/>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
{{ rule.rule_id || '-' }}
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 协议编辑 -->
<div v-if="rule.editing" class="flex items-center space-x-2">
<select
v-model="rule.rule_protocol"
class="w-20 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="TCP">TCP</option>
<option value="UDP">UDP</option>
<option value="ICMP">ICMP</option>
</select>
</div>
<div v-else>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-blue-100 text-blue-800': rule.rule_protocol === 'TCP',
'bg-green-100 text-green-800': rule.rule_protocol === 'UDP',
'bg-yellow-100 text-yellow-800': rule.rule_protocol === 'ICMP',
'bg-gray-100 text-gray-800': !rule.rule_protocol
}">
{{ rule.rule_protocol || '未知' }}
</span>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 端口范围编辑 -->
<div v-if="rule.editing" class="flex items-center space-x-2">
<input
v-model="rule.port"
type="text"
class="w-24 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="端口范围"
/>
</div>
<div v-else class="text-sm text-gray-900">
{{ rule.port || '-' }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900 max-w-xs truncate" :title="rule.source_cidr_ip">
{{ rule.source_cidr_ip || '0.0.0.0/0' }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<!-- 备注编辑 -->
<div v-if="rule.editing" class="flex items-center space-x-2">
<input
v-model="rule.remark"
type="text"
class="w-32 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="备注"
/>
</div>
<div v-else class="text-sm text-gray-500 max-w-xs truncate" :title="rule.remark">
{{ rule.remark || '-' }}
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm">
<div class="flex items-center space-x-1">
<Button
v-if="rule.editing"
@click="saveRule(rule)"
variant="ghost"
size="sm"
class="text-green-600 hover:text-green-700"
>
保存
</Button>
<Button
v-if="rule.editing"
@click="cancelEdit(rule)"
variant="ghost"
size="sm"
class="text-gray-600 hover:text-gray-700"
>
取消
</Button>
<Button
v-if="!rule.editing && !rule.isNew"
@click="deleteRule(rule)"
variant="ghost"
size="sm"
class="text-red-600 hover:text-red-700"
>
删除
</Button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div v-if="pagination.total > pagination.limit" class="flex flex-col items-center space-y-4">
<div class="text-sm text-gray-700">
显示第 {{ (pagination.pageno - 1) * pagination.limit + 1 }} -
{{ Math.min(pagination.pageno * pagination.limit, pagination.total) }}
{{ pagination.total }} 条记录
</div>
<div class="flex items-center space-x-2">
<Button
@click="changePage(pagination.pageno - 1)"
:disabled="pagination.pageno <= 1"
variant="outline"
size="sm"
class="px-3"
>
</Button>
<div class="flex items-center space-x-1">
<Button
v-for="page in getVisiblePages()"
:key="page"
@click="changePage(page)"
:class="[
'px-3 py-1 text-sm rounded',
page === pagination.pageno
? '!bg-[#1fc76f] text-white'
: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
]"
:disabled="page === pagination.pageno"
>
{{ page }}
</Button>
</div>
<Button
@click="changePage(pagination.pageno + 1)"
:disabled="pagination.pageno >= pagination.pagecount"
variant="outline"
size="sm"
class="px-3"
>
</Button>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="!$resources.firewallRules.loading && !$resources.firewallRules.error && firewallRules.length === 0" class="text-center py-12">
<ShieldIcon class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900">暂无防火墙规则</h3>
<p class="mt-1 text-sm text-gray-500">开始添加您的第一个防火墙规则</p>
<div class="mt-6">
<Button @click="addNewRow" variant="solid">
<PlusIcon class="h-4 w-4 mr-1" />
添加规则
</Button>
</div>
</div>
</div>
</template>
<script>
import { getCachedDocumentResource, Badge, Button, createResource } from 'jingrow-ui';
import { h, defineAsyncComponent } from 'vue';
import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../utils/toast';
import { renderDialog, confirmDialog } from '../utils/components';
import RefreshCwIcon from '~icons/lucide/refresh-cw';
import PlusIcon from '~icons/lucide/plus';
import Loader2Icon from '~icons/lucide/loader-2';
import AlertCircleIcon from '~icons/lucide/alert-circle';
import EditIcon from '~icons/lucide/edit';
import Trash2Icon from '~icons/lucide/trash-2';
import ShieldIcon from '~icons/lucide/shield';
import SettingsIcon from '~icons/lucide/settings';
export default {
name: 'JsiteServerFirewallRules',
props: ['server'],
data() {
return {
firewallRules: [],
pagination: {
pageno: 1,
limit: 10,
total: 0,
pagecount: 0
},
selectedRules: [] // 用于存储选中的规则ID
};
},
resources: {
firewallRules() {
return {
url: 'jcloud.api.aliyun_server_light.get_aliyun_firewall_rules',
params: {
instance_id: this.$jsiteServer.pg?.instance_id,
region_id: this.$jsiteServer.pg?.region
},
auto: true,
onSuccess: (response) => {
if (response.success && response.data) {
// 直接使用API返回的防火墙规则数据
const firewallRules = response.data.firewall_rules || [];
this.firewallRules = firewallRules.map(rule => ({
rule_id: rule.rule_id,
rule_protocol: rule.rule_protocol,
port: rule.port,
source_cidr_ip: rule.source_cidr_ip,
remark: rule.remark,
editing: false,
isNew: false
}));
// 简单分页处理
const total = this.firewallRules.length;
const limit = this.pagination.limit;
const pageno = this.pagination.pageno;
const start = (pageno - 1) * limit;
const end = start + limit;
this.pagination = {
pageno: pageno,
limit: limit,
total: total,
pagecount: Math.ceil(total / limit)
};
// 对当前页的数据进行切片
this.firewallRules = this.firewallRules.slice(start, end);
} else {
this.error = response.message || '获取防火墙规则失败';
}
},
onError: (error) => {
this.error = getToastErrorMessage(error);
}
};
}
},
methods: {
// 刷新规则
refreshRules() {
this.$resources.firewallRules.reload();
},
// 切换页面
changePage(page) {
if (page >= 1 && page <= this.pagination.pagecount) {
this.pagination.pageno = page;
this.$resources.firewallRules.reload();
}
},
// 添加新行
addNewRow() {
const newRule = {
rule_id: null,
rule_protocol: 'TCP',
port: '',
source_cidr_ip: '0.0.0.0/0',
remark: '',
editing: true,
isNew: true
};
// 添加新规则到最前面
this.firewallRules.unshift(newRule);
},
// 保存规则
async saveRule(rule) {
if (!rule.rule_protocol || !rule.port) {
toast.error('协议和端口不能为空');
return;
}
try {
const request = createResource({
url: 'jcloud.api.aliyun_server_light.create_aliyun_firewall_rule',
params: {
instance_id: this.$jsiteServer.pg.instance_id,
rule_protocol: rule.rule_protocol,
port: rule.port,
remark: rule.remark,
region_id: this.$jsiteServer.pg.region
},
onSuccess: (response) => {
if (response.success) {
toast.success('防火墙规则保存成功');
rule.editing = false;
rule.isNew = false;
// 重新加载规则列表
setTimeout(() => {
this.$resources.firewallRules.reload();
}, 1000);
} else {
toast.error(response.message || '保存防火墙规则失败');
}
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
toast.error('保存防火墙规则失败');
console.error('保存防火墙规则失败:', error);
}
},
// 取消编辑
cancelEdit(rule) {
if (rule.isNew) {
// 如果是新增规则,直接删除
const index = this.firewallRules.indexOf(rule);
if (index > -1) {
this.firewallRules.splice(index, 1);
}
} else {
// 如果是编辑中的规则,恢复其原始值
if (rule._original) {
Object.assign(rule, rule._original);
delete rule._original;
}
rule.editing = false;
}
},
// 删除规则
deleteRule(rule) {
confirmDialog({
title: '删除防火墙规则',
message: `确定要删除这条防火墙规则吗?\n协议: ${rule.rule_protocol}\n端口: ${rule.port}`,
primaryAction: {
label: '确定',
variant: 'solid',
class: 'bg-black text-white hover:bg-gray-800',
onClick: ({ hide }) => {
this.performDeleteRule(rule, hide);
}
}
});
},
// 执行删除规则
async performDeleteRule(rule, hide) {
try {
const request = createResource({
url: 'jcloud.api.aliyun_server_light.delete_aliyun_firewall_rules',
params: {
instance_id: this.$jsiteServer.pg.instance_id,
region_id: this.$jsiteServer.pg.region,
rule_ids: [rule.rule_id]
},
onSuccess: (response) => {
if (response.success) {
toast.success('防火墙规则删除成功');
hide();
setTimeout(() => {
this.$resources.firewallRules.reload();
}, 1000);
} else {
toast.error(response.message || '删除防火墙规则失败');
}
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
toast.error('删除防火墙规则失败');
console.error('删除防火墙规则失败:', error);
}
},
// 批量删除规则
async batchDeleteRules() {
if (this.selectedRules.length === 0) {
toast.info('请选择要删除的规则');
return;
}
confirmDialog({
title: '批量删除防火墙规则',
message: `确定要删除选中的 ${this.selectedRules.length} 条防火墙规则吗?`,
primaryAction: {
label: '确定',
variant: 'solid',
class: 'bg-black text-white hover:bg-gray-800',
onClick: ({ hide }) => {
this.performBatchDeleteRules(this.selectedRules, hide);
}
}
});
},
// 执行批量删除规则
async performBatchDeleteRules(ruleIds, hide) {
const validRuleIds = ruleIds.filter(id => id);
if (validRuleIds.length === 0) {
toast.error('没有有效的规则可以删除');
return;
}
try {
const request = createResource({
url: 'jcloud.api.aliyun_server_light.delete_aliyun_firewall_rules',
params: {
instance_id: this.$jsiteServer.pg.instance_id,
region_id: this.$jsiteServer.pg.region,
rule_ids: validRuleIds
},
onSuccess: (response) => {
if (response.success) {
toast.success(response.message || `批量删除 ${validRuleIds.length} 条防火墙规则成功`);
hide();
setTimeout(() => {
this.$resources.firewallRules.reload();
}, 1000);
this.selectedRules = []; // 清空选中
} else {
toast.error(response.message || '批量删除防火墙规则失败');
}
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
}
});
request.submit();
} catch (error) {
toast.error('批量删除防火墙规则失败');
console.error('批量删除防火墙规则失败:', error);
}
},
// 切换规则选中状态
toggleRuleSelection(ruleId) {
if (!ruleId) return;
const ruleIdStr = String(ruleId);
const index = this.selectedRules.indexOf(ruleIdStr);
if (index > -1) {
this.selectedRules.splice(index, 1);
} else {
this.selectedRules.push(ruleIdStr);
}
},
// 全选/取消全选
toggleSelectAll(event) {
const checkbox = event.target;
if (checkbox.checked) {
this.selectedRules = this.firewallRules
.filter(rule => rule.rule_id && !rule.isNew)
.map(rule => String(rule.rule_id));
} else {
this.selectedRules = [];
}
},
// 获取可见的页码
getVisiblePages() {
const current = this.pagination.pageno;
const total = this.pagination.pagecount;
const pages = [];
if (total <= 5) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
const start = Math.max(1, current - 2);
const end = Math.min(total, current + 2);
for (let i = start; i <= end; i++) {
pages.push(i);
}
}
return pages;
}
},
computed: {
$jsiteServer() {
return getCachedDocumentResource('Jsite Server', this.server);
},
// 判断是否全选
isAllSelected() {
const validRules = this.firewallRules.filter(rule => rule.rule_id && !rule.isNew);
return validRules.length > 0 && this.selectedRules.length === validRules.length;
},
// 判断是否半选
isIndeterminate() {
const validRules = this.firewallRules.filter(rule => rule.rule_id && !rule.isNew);
return this.selectedRules.length > 0 && this.selectedRules.length < validRules.length;
}
},
mounted() {
// 组件会自动加载防火墙规则因为resources设置了auto: true
}
};
</script>