This commit is contained in:
2025-03-18 07:43:46 +08:00
commit d2e93a2736
26 changed files with 918 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<template>
<div class="cf-turnstile">
<div ref="turnstileWidget"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const turnstileWidget = ref(null)
const emit = defineEmits(['verify'])
onMounted(() => {
if (window.turnstile) {
window.turnstile.render(turnstileWidget.value, {
sitekey: 'your-site-key',
callback: (token) => {
emit('verify', token)
},
})
}
})
</script>
<style scoped>
.cf-turnstile {
margin: 1rem 0;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<nav class="navbar">
<router-link to="/">首页</router-link>
<router-link to="/user/dashboard">用户面板</router-link>
<router-link to="/admin/dashboard" v-if="isAdmin">管理面板</router-link>
<button @click="logout">退出</button>
</nav>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const router = useRouter()
const isAdmin = computed(() => authStore.user?.role === 'admin')
const logout = () => {
authStore.logout()
router.push('/login')
}
</script>
<style scoped>
.navbar {
display: flex;
gap: 1rem;
padding: 1rem;
background: #f0f0f0;
}
</style>

9
frontend/src/main.js Normal file
View File

@@ -0,0 +1,9 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

18
frontend/src/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "firewall-frontend",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.3.4",
"pinia": "^2.0.33",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
},
"devDependencies": {
"vite": "^4.1.4"
}
}

View File

@@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/user/dashboard'
},
{
path: '/user/dashboard',
component: () => import('../views/User/Dashboard.vue')
},
{
path: '/admin/dashboard',
component: () => import('../views/Admin/Dashboard.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@@ -0,0 +1,44 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const router = useRouter()
// 登录
const login = async (username, password) => {
const response = await axios.post('/api/login', {
username,
password
})
user.value = response.data.user
localStorage.setItem('token', response.data.token)
router.push('/user/dashboard')
}
// 登出
const logout = () => {
user.value = null
localStorage.removeItem('token')
router.push('/login')
}
// 检查登录状态
const checkAuth = async () => {
try {
const response = await axios.get('/api/user/me')
user.value = response.data
} catch (error) {
logout()
}
}
return {
user,
login,
logout,
checkAuth
}
})

View File

@@ -0,0 +1,27 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'
export const useRequestsStore = defineStore('requests', () => {
const pendingRequests = ref([])
const approvedRequests = ref([])
// 获取待审批请求
const fetchPendingRequests = async () => {
const response = await axios.get('/api/admin/requests')
pendingRequests.value = response.data
}
// 获取已批准请求
const fetchApprovedRequests = async () => {
const response = await axios.get('/api/user/requests')
approvedRequests.value = response.data
}
return {
pendingRequests,
approvedRequests,
fetchPendingRequests,
fetchApprovedRequests
}
})

View File

@@ -0,0 +1,43 @@
<template>
<div class="blacklist">
<h2>黑名单管理</h2>
<div v-if="blacklist.length > 0">
<ul>
<li v-for="item in blacklist" :key="item.id">
{{ item.target }} - {{ item.type }}
<button @click="removeFromBlacklist(item.id)">移除</button>
</li>
</ul>
</div>
<div>
<input v-model="newTarget" placeholder="输入IP或用户名">
<button @click="addToBlacklist">加入黑名单</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const blacklist = ref([])
const newTarget = ref('')
const fetchBlacklist = async () => {
const response = await axios.get('/api/admin/blacklist')
blacklist.value = response.data
}
const addToBlacklist = async () => {
await axios.post('/api/admin/blacklist', { target: newTarget.value })
newTarget.value = ''
await fetchBlacklist()
}
const removeFromBlacklist = async (id) => {
await axios.delete(`/api/admin/blacklist/${id}`)
await fetchBlacklist()
}
onMounted(fetchBlacklist)
</script>

View File

@@ -0,0 +1,139 @@
<template>
<div class="admin-dashboard">
<h2>管理员面板</h2>
<div v-if="pendingRequests.length > 0">
<h3>待审批请求</h3>
<table class="requests-table">
<thead>
<tr>
<th>用户</th>
<th>IP 地址</th>
<th>申请时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="req in pendingRequests" :key="req.id">
<td>{{ req.username }}</td>
<td>{{ req.ip }}</td>
<td>{{ formatDate(req.created_at) }}</td>
<td>
<button @click="approveRequest(req.id)" class="approve-btn">批准</button>
<button @click="rejectRequest(req.id)" class="reject-btn">拒绝</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<p>没有待审批的请求</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
// 待审批请求列表
const pendingRequests = ref([])
// 获取待审批请求
const fetchPendingRequests = async () => {
try {
const response = await axios.get('/api/admin/requests')
pendingRequests.value = response.data
} catch (error) {
console.error('获取待审批请求失败:', error)
}
}
// 批准请求
const approveRequest = async (requestId) => {
try {
await axios.post('/api/admin/approve', { request_id: requestId })
await fetchPendingRequests()
alert('请求已批准')
} catch (error) {
console.error('批准请求失败:', error)
alert('操作失败,请重试')
}
}
// 拒绝请求
const rejectRequest = async (requestId) => {
try {
await axios.post('/api/admin/reject', { request_id: requestId })
await fetchPendingRequests()
alert('请求已拒绝')
} catch (error) {
console.error('拒绝请求失败:', error)
alert('操作失败,请重试')
}
}
// 格式化日期
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString()
}
// 组件加载时获取数据
onMounted(fetchPendingRequests)
</script>
<style scoped>
.admin-dashboard {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.requests-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.requests-table th,
.requests-table td {
padding: 12px;
border: 1px solid #ddd;
text-align: left;
}
.requests-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.requests-table tr:hover {
background-color: #f9f9f9;
}
.approve-btn {
background-color: #4caf50;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
margin-right: 8px;
}
.approve-btn:hover {
background-color: #45a049;
}
.reject-btn {
background-color: #f44336;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
}
.reject-btn:hover {
background-color: #e53935;
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="dashboard">
<h2>用户面板</h2>
<div v-if="requests.length > 0">
<h3>我的请求</h3>
<ul>
<li v-for="req in requests" :key="req.id">
{{ req.ip }} - {{ req.status }}
</li>
</ul>
</div>
<button @click="requestNewIP">申请新IP</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRequestsStore } from '../stores/requests'
const requestsStore = useRequestsStore()
const requests = ref([])
const requestNewIP = async () => {
await axios.post('/api/user/request')
await requestsStore.fetchApprovedRequests()
}
onMounted(async () => {
await requestsStore.fetchApprovedRequests()
requests.value = requestsStore.approvedRequests
})
</script>

View File

@@ -0,0 +1,26 @@
<template>
<div class="request-form">
<h3>申请新IP</h3>
<form @submit.prevent="submitRequest">
<CfTurnstile @verify="setToken" />
<button type="submit" :disabled="!token">提交申请</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
import CfTurnstile from '../components/CfTurnstile.vue'
const token = ref('')
const setToken = (newToken) => {
token.value = newToken
}
const submitRequest = async () => {
await axios.post('/api/user/request', { token: token.value })
alert('申请已提交')
}
</script>

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})