feat: 添加新插件支持及多项功能改进
- 新增磁盘加密、打印审计和剪贴板管控插件支持 - 优化水印插件显示效果,支持中文及更多Unicode字符 - 改进硬件资产收集逻辑,更准确获取磁盘和显卡信息 - 增强API错误处理,添加详细日志记录 - 完善前端界面,新增插件管理页面 - 修复多个UI问题,优化页面过渡效果 - 添加环境变量覆盖配置功能 - 实现插件状态管理API - 更新文档和变更日志 - 添加安装程序脚本支持
This commit is contained in:
2
web/auto-imports.d.ts
vendored
2
web/auto-imports.d.ts
vendored
@@ -5,5 +5,5 @@
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
const vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.5.6",
|
||||
"pinia": "^2.1.7",
|
||||
|
||||
@@ -178,13 +178,13 @@ html, body, #app {
|
||||
}
|
||||
|
||||
/* ---- Page Transition ---- */
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.page-enter-from,
|
||||
.page-leave-to {
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,43 @@ function clearAuth() {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
let refreshPromise: Promise<boolean> | null = null
|
||||
|
||||
async function tryRefresh(): Promise<boolean> {
|
||||
// Coalesce concurrent refresh attempts
|
||||
if (refreshPromise) return refreshPromise
|
||||
|
||||
refreshPromise = (async () => {
|
||||
const refreshToken = localStorage.getItem('refresh_token')
|
||||
if (!refreshToken || refreshToken.trim() === '') return false
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
})
|
||||
|
||||
if (!response.ok) return false
|
||||
|
||||
const result = await response.json()
|
||||
if (!result.success || !result.data?.access_token) return false
|
||||
|
||||
localStorage.setItem('token', result.data.access_token)
|
||||
if (result.data.refresh_token) {
|
||||
localStorage.setItem('refresh_token', result.data.refresh_token)
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
} finally {
|
||||
refreshPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
@@ -53,8 +90,28 @@ async function request<T>(
|
||||
headers,
|
||||
})
|
||||
|
||||
// Handle 401 - token expired or invalid
|
||||
// Handle 401 - try refresh before giving up
|
||||
if (response.status === 401) {
|
||||
const refreshed = await tryRefresh()
|
||||
if (refreshed) {
|
||||
// Retry the original request with new token
|
||||
const newToken = getToken()
|
||||
headers.set('Authorization', `Bearer ${newToken}`)
|
||||
const retryResponse = await fetch(`${API_BASE}${path}`, { ...options, headers })
|
||||
if (retryResponse.status === 401) {
|
||||
clearAuth()
|
||||
throw new ApiError(401, 'UNAUTHORIZED', 'Session expired')
|
||||
}
|
||||
const retryContentType = retryResponse.headers.get('content-type')
|
||||
if (!retryContentType || !retryContentType.includes('application/json')) {
|
||||
throw new ApiError(retryResponse.status, 'NON_JSON_RESPONSE', `Server returned ${retryResponse.status}`)
|
||||
}
|
||||
const retryResult: ApiResult<T> = await retryResponse.json()
|
||||
if (!retryResult.success) {
|
||||
throw new ApiError(retryResponse.status, 'API_ERROR', retryResult.error || 'Unknown error')
|
||||
}
|
||||
return retryResult.data as T
|
||||
}
|
||||
clearAuth()
|
||||
throw new ApiError(401, 'UNAUTHORIZED', 'Session expired')
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ const router = createRouter({
|
||||
{ path: 'plugins/popup-blocker', name: 'PopupBlocker', component: () => import('../views/plugins/PopupBlocker.vue') },
|
||||
{ path: 'plugins/usb-file-audit', name: 'UsbFileAudit', component: () => import('../views/plugins/UsbFileAudit.vue') },
|
||||
{ path: 'plugins/watermark', name: 'Watermark', component: () => import('../views/plugins/Watermark.vue') },
|
||||
{ path: 'plugins/disk-encryption', name: 'DiskEncryption', component: () => import('../views/plugins/DiskEncryption.vue') },
|
||||
{ path: 'plugins/print-audit', name: 'PrintAudit', component: () => import('../views/plugins/PrintAudit.vue') },
|
||||
{ path: 'plugins/clipboard-control', name: 'ClipboardControl', component: () => import('../views/plugins/ClipboardControl.vue') },
|
||||
{ path: 'plugins/plugin-control', name: 'PluginControl', component: () => import('../views/plugins/PluginControl.vue') },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
export interface Device {
|
||||
id: number
|
||||
@@ -35,9 +19,11 @@ export interface Device {
|
||||
export interface DeviceStatusDetail {
|
||||
cpu_usage: number
|
||||
memory_usage: number
|
||||
memory_total: number
|
||||
memory_total_mb: number
|
||||
disk_usage: number
|
||||
disk_total: number
|
||||
disk_total_mb: number
|
||||
network_rx_rate: number
|
||||
network_tx_rate: number
|
||||
running_procs: number
|
||||
top_processes: Array<{ name: string; pid: number; cpu_usage: number; memory_mb: number }>
|
||||
}
|
||||
@@ -50,28 +36,36 @@ export const useDeviceStore = defineStore('devices', () => {
|
||||
async function fetchDevices(params?: Record<string, string>) {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await api.get('/devices', { params })
|
||||
if (data.success) {
|
||||
devices.value = data.data.devices
|
||||
total.value = data.data.total ?? devices.value.length
|
||||
}
|
||||
const query = params ? '?' + new URLSearchParams(params).toString() : ''
|
||||
const result = await api.get<{ devices: Device[]; total: number }>(`/api/devices${query}`)
|
||||
devices.value = result.devices
|
||||
total.value = result.total ?? devices.value.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDeviceStatus(uid: string): Promise<DeviceStatusDetail | null> {
|
||||
const { data } = await api.get(`/devices/${uid}/status`)
|
||||
return data.success ? data.data : null
|
||||
try {
|
||||
return await api.get<DeviceStatusDetail>(`/api/devices/${uid}/status`)
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch device status', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDeviceHistory(uid: string, params?: Record<string, string>) {
|
||||
const { data } = await api.get(`/devices/${uid}/history`, { params })
|
||||
return data.success ? data.data : null
|
||||
try {
|
||||
const query = params ? '?' + new URLSearchParams(params).toString() : ''
|
||||
return await api.get(`/api/devices/${uid}/history${query}`)
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch device history', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDevice(uid: string) {
|
||||
await api.delete(`/devices/${uid}`)
|
||||
await api.delete(`/api/devices/${uid}`)
|
||||
devices.value = devices.value.filter((d) => d.device_uid !== uid)
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,10 @@ async function fetchRecords() {
|
||||
if (handledFilter.value) params.set('handled', handledFilter.value)
|
||||
const data = await api.get<any>(`/api/alerts/records?${params}`)
|
||||
records.value = data.records || []
|
||||
} catch { /* api.ts handles 401 */ } finally { recLoading.value = false }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch alert records', e)
|
||||
ElMessage.warning('加载告警记录失败')
|
||||
} finally { recLoading.value = false }
|
||||
}
|
||||
|
||||
async function handleRecord(id: number) {
|
||||
@@ -167,7 +170,10 @@ async function fetchRules() {
|
||||
try {
|
||||
const data = await api.get<any>('/api/alerts/rules')
|
||||
rules.value = data.rules || []
|
||||
} catch { /* api.ts handles 401 */ } finally { ruleLoading.value = false }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch alert rules', e)
|
||||
ElMessage.warning('加载告警规则失败')
|
||||
} finally { ruleLoading.value = false }
|
||||
}
|
||||
|
||||
function showRuleDialog(row?: any) {
|
||||
@@ -204,7 +210,7 @@ async function toggleRule(row: any) {
|
||||
try {
|
||||
await api.put(`/api/alerts/rules/${row.id}`, { enabled: !row.enabled ? 1 : 0 })
|
||||
fetchRules()
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error('Failed to toggle alert rule', e) }
|
||||
}
|
||||
|
||||
async function deleteRule(id: number) {
|
||||
|
||||
@@ -179,8 +179,8 @@ async function fetchDashboard() {
|
||||
const events = usbData.events || []
|
||||
stats.value.usbEvents = events.length
|
||||
recentUsbEvents.value = events.slice(0, 8)
|
||||
} catch {
|
||||
// Silently fail - dashboard gracefully shows zeros
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch dashboard data', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
@@ -207,7 +208,10 @@ async function fetchHardware() {
|
||||
try {
|
||||
const data = await api.get<any>(`/api/assets/hardware?device_uid=${deviceUid}`)
|
||||
hardware.value = data.hardware || []
|
||||
} catch { /* ignore */ } finally { hwLoading.value = false }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch hardware data', e)
|
||||
ElMessage.warning('加载硬件资产失败')
|
||||
} finally { hwLoading.value = false }
|
||||
}
|
||||
|
||||
// --- Software data ---
|
||||
@@ -222,7 +226,10 @@ async function fetchSoftware() {
|
||||
if (swSearch.value) params.set('search', swSearch.value)
|
||||
const data = await api.get<any>(`/api/assets/software?${params}`)
|
||||
software.value = data.software || []
|
||||
} catch { /* ignore */ } finally { swLoading.value = false }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch software data', e)
|
||||
ElMessage.warning('加载软件资产失败')
|
||||
} finally { swLoading.value = false }
|
||||
}
|
||||
|
||||
// --- Changes data ---
|
||||
@@ -234,7 +241,10 @@ async function fetchChanges() {
|
||||
try {
|
||||
const data = await api.get<any>(`/api/assets/changes?device_uid=${deviceUid}`)
|
||||
changes.value = data.changes || []
|
||||
} catch { /* ignore */ } finally { chLoading.value = false }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch change records', e)
|
||||
ElMessage.warning('加载变更记录失败')
|
||||
} finally { chLoading.value = false }
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
@@ -276,7 +286,10 @@ onMounted(async () => {
|
||||
])
|
||||
device.value = devData
|
||||
status.value = statData
|
||||
} catch { /* api.ts handles 401 */ } finally {
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch device detail', e)
|
||||
ElMessage.warning('加载设备详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -242,23 +242,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Search, Monitor, FolderOpened, Folder, Delete, MoreFilled, Plus
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDeviceStore } from '../stores/devices'
|
||||
import type { Device } from '../stores/devices'
|
||||
|
||||
const apiBase = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
apiBase.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
return config
|
||||
})
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const router = useRouter()
|
||||
const deviceStore = useDeviceStore()
|
||||
@@ -416,12 +406,12 @@ async function showCreateGroup() {
|
||||
inputErrorMessage: '名称长度为1-50个字符',
|
||||
})
|
||||
if (!name) return
|
||||
const { data } = await apiBase.post('/groups', { name: name.trim() })
|
||||
if (data.success) {
|
||||
try {
|
||||
await api.post('/api/groups', { name: name.trim() })
|
||||
ElMessage.success('分组创建成功')
|
||||
doSearch() // refresh device list to reflect new group
|
||||
} else {
|
||||
ElMessage.error(data.error || '创建失败')
|
||||
doSearch()
|
||||
} catch {
|
||||
ElMessage.error('创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,13 +427,13 @@ async function renameGroup() {
|
||||
inputErrorMessage: '名称长度为1-50个字符',
|
||||
})
|
||||
if (!newName || newName.trim() === g.name) return
|
||||
const { data } = await apiBase.put(`/groups/${encodeURIComponent(g.name)}`, { new_name: newName.trim() })
|
||||
if (data.success) {
|
||||
try {
|
||||
await api.put(`/api/groups/${encodeURIComponent(g.name)}`, { new_name: newName.trim() })
|
||||
ElMessage.success('重命名成功')
|
||||
if (activeGroup.value === g.name) activeGroup.value = newName.trim()
|
||||
doSearch()
|
||||
} else {
|
||||
ElMessage.error(data.error || '重命名失败')
|
||||
} catch {
|
||||
ElMessage.error('重命名失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,13 +446,13 @@ async function deleteGroup() {
|
||||
'删除分组',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
||||
)
|
||||
const { data } = await apiBase.delete(`/groups/${encodeURIComponent(g.name)}`)
|
||||
if (data.success) {
|
||||
try {
|
||||
await api.delete(`/api/groups/${encodeURIComponent(g.name)}`)
|
||||
ElMessage.success('分组已删除')
|
||||
if (activeGroup.value === g.name) activeGroup.value = ''
|
||||
doSearch()
|
||||
} else {
|
||||
ElMessage.error(data.error || '删除失败')
|
||||
} catch {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,22 +473,22 @@ async function handleMoveSubmit() {
|
||||
if (target === '__new__') {
|
||||
target = newGroupName.value.trim()
|
||||
if (!target) return
|
||||
// Create group first
|
||||
const { data: createData } = await apiBase.post('/groups', { name: target })
|
||||
if (!createData.success) {
|
||||
ElMessage.error(createData.error || '创建分组失败')
|
||||
try {
|
||||
await api.post('/api/groups', { name: target })
|
||||
} catch {
|
||||
ElMessage.error('创建分组失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = await apiBase.put(`/devices/${d.device_uid}/group`, { group_name: target })
|
||||
if (data.success) {
|
||||
try {
|
||||
await api.put(`/api/devices/${d.device_uid}/group`, { group_name: target })
|
||||
ElMessage.success('已移动到分组')
|
||||
moveDialog.value = { visible: false, device: null, target: '' }
|
||||
newGroupName.value = ''
|
||||
doSearch()
|
||||
} else {
|
||||
ElMessage.error(data.error || '移动失败')
|
||||
} catch {
|
||||
ElMessage.error('移动失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,6 +64,18 @@
|
||||
<el-menu-item index="/plugins/watermark">
|
||||
<template #title><span>水印管理</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/plugins/disk-encryption">
|
||||
<template #title><span>磁盘加密</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/plugins/print-audit">
|
||||
<template #title><span>打印审计</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/plugins/clipboard-control">
|
||||
<template #title><span>剪贴板管控</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/plugins/plugin-control">
|
||||
<template #title><span>插件控制</span></template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item index="/settings">
|
||||
@@ -116,10 +128,8 @@
|
||||
</el-header>
|
||||
|
||||
<el-main class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<component :is="Component" :key="route.path" />
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
@@ -158,8 +168,8 @@ async function fetchUnreadAlerts() {
|
||||
try {
|
||||
const data = await api.get<any>('/api/alerts/records?handled=0&page_size=1')
|
||||
unreadAlerts.value = data.records?.length || 0
|
||||
} catch {
|
||||
// Silently fail
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch unread alert count', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +185,10 @@ const pageTitles: Record<string, string> = {
|
||||
'/plugins/popup-blocker': '弹窗拦截',
|
||||
'/plugins/usb-file-audit': 'U盘审计',
|
||||
'/plugins/watermark': '水印管理',
|
||||
'/plugins/disk-encryption': '磁盘加密',
|
||||
'/plugins/print-audit': '打印审计',
|
||||
'/plugins/clipboard-control': '剪贴板管控',
|
||||
'/plugins/plugin-control': '插件控制',
|
||||
}
|
||||
|
||||
const pageTitle = computed(() => pageTitles[route.path] || '仪表盘')
|
||||
|
||||
@@ -103,7 +103,7 @@ onMounted(() => {
|
||||
user.username = payload.username || 'admin'
|
||||
user.role = payload.role || 'admin'
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error('Failed to decode token for username', e) }
|
||||
|
||||
api.get<any>('/health')
|
||||
.then((data: any) => {
|
||||
@@ -112,7 +112,7 @@ onMounted(() => {
|
||||
const bytes = data.db_size_bytes || 0
|
||||
dbInfo.value = `SQLite (WAL) - ${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||||
})
|
||||
.catch(() => { /* ignore */ })
|
||||
.catch((e) => { console.error('Failed to fetch health status', e) })
|
||||
})
|
||||
|
||||
async function changePassword() {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<el-table-column prop="device_name" label="USB设备" width="150" />
|
||||
<el-table-column label="事件类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.event_type === 'Inserted' ? 'success' : row.event_type === 'Blocked' ? 'danger' : 'info'" size="small" effect="light">
|
||||
<el-tag :type="row.event_type.toLowerCase() === 'inserted' ? 'success' : row.event_type.toLowerCase() === 'blocked' ? 'danger' : 'info'" size="small" effect="light">
|
||||
{{ eventTypeLabel(row.event_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -128,7 +128,10 @@ async function fetchPolicies() {
|
||||
try {
|
||||
const data = await api.get<any>('/api/usb/policies')
|
||||
policies.value = data.policies || []
|
||||
} catch { /* api.ts handles 401 */ } finally { loading.value = false }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch USB policies', e)
|
||||
ElMessage.warning('加载USB策略失败')
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function showPolicyDialog(row?: any) {
|
||||
@@ -166,7 +169,7 @@ async function togglePolicy(row: any) {
|
||||
try {
|
||||
await api.put(`/api/usb/policies/${row.id}`, { enabled: !row.enabled ? 1 : 0 })
|
||||
fetchPolicies()
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error('Failed to toggle USB policy', e) }
|
||||
}
|
||||
|
||||
async function deletePolicy(id: number) {
|
||||
@@ -199,12 +202,16 @@ async function fetchEvents() {
|
||||
if (eventFilter.value) params.set('event_type', eventFilter.value)
|
||||
const data = await api.get<any>(`/api/usb/events?${params}`)
|
||||
events.value = data.events || []
|
||||
} catch { /* api.ts handles 401 */ } finally { evLoading.value = false }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch USB events', e)
|
||||
ElMessage.warning('加载USB事件失败')
|
||||
} finally { evLoading.value = false }
|
||||
}
|
||||
|
||||
function eventTypeLabel(type: string) {
|
||||
const map: Record<string, string> = { Inserted: '插入', Removed: '拔出', Blocked: '拦截' }
|
||||
return map[type] || type
|
||||
const lower = type.toLowerCase()
|
||||
const map: Record<string, string> = { inserted: '插入', removed: '拔出', blocked: '拦截' }
|
||||
return map[lower] || type
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
153
web/src/views/plugins/ClipboardControl.vue
Normal file
153
web/src/views/plugins/ClipboardControl.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-tabs v-model="activeTab" class="page-tabs">
|
||||
<el-tab-pane label="管控规则" name="rules">
|
||||
<div class="page-toolbar">
|
||||
<el-button type="primary" @click="showRuleDialog()">
|
||||
<el-icon><Plus /></el-icon>新建规则
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="csm-card">
|
||||
<el-table :data="rules" v-loading="loading" style="width:100%">
|
||||
<el-table-column prop="rule_type" label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.rule_type === 'block' ? 'danger' : 'success'" size="small">{{ row.rule_type === 'block' ? '阻止' : '放行' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="direction" label="方向" width="80">
|
||||
<template #default="{ row }">{{ ({ out: '外发', 'in': '接收', both: '双向' } as Record<string, string>)[row.direction] || row.direction }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="source_process" label="源进程" min-width="130">
|
||||
<template #default="{ row }">{{ row.source_process || '*' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_process" label="目标进程" min-width="130">
|
||||
<template #default="{ row }">{{ row.target_process || '*' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content_pattern" label="内容匹配" min-width="150">
|
||||
<template #default="{ row }">{{ row.content_pattern || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enabled" label="启用" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="showRuleDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteRule(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="违规记录" name="violations">
|
||||
<div class="csm-card">
|
||||
<el-table :data="violations" v-loading="vioLoading" style="width:100%">
|
||||
<el-table-column prop="device_uid" label="设备" min-width="120">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.device_uid?.substring(0, 8) }}...</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="source_process" label="源进程" min-width="130">
|
||||
<template #default="{ row }">{{ row.source_process || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content_preview" label="内容预览" min-width="200">
|
||||
<template #default="{ row }">{{ row.content_preview || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="action_taken" label="动作" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.action_taken === 'blocked' ? 'danger' : 'success'" size="small">{{ row.action_taken === 'blocked' ? '已阻止' : '已放行' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="timestamp" label="时间" width="180" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editing ? '编辑规则' : '新建规则'" width="500">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="form.rule_type"><el-option label="阻止" value="block" /><el-option label="放行" value="allow" /></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="方向">
|
||||
<el-select v-model="form.direction"><el-option label="外发" value="out" /><el-option label="接收" value="in" /><el-option label="双向" value="both" /></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="源进程"><el-input v-model="form.source_process" placeholder="进程名匹配模式,留空=全部" /></el-form-item>
|
||||
<el-form-item label="目标进程"><el-input v-model="form.target_process" placeholder="进程名匹配模式,留空=全部" /></el-form-item>
|
||||
<el-form-item label="内容匹配"><el-input v-model="form.content_pattern" placeholder="可选,内容关键词" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="form.enabled" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveRule">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const activeTab = ref('rules')
|
||||
const rules = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const violations = ref<any[]>([])
|
||||
const vioLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref<any>(null)
|
||||
const form = reactive({ rule_type: 'block', direction: 'out', source_process: '', target_process: '', content_pattern: '', enabled: true })
|
||||
|
||||
async function fetchRules() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/clipboard-control/rules')
|
||||
rules.value = data.rules || []
|
||||
} catch (e) { console.error('Failed to load clipboard control rules', e); ElMessage.warning('加载剪贴板规则失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchViolations() {
|
||||
vioLoading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/clipboard-control/violations')
|
||||
violations.value = data.violations || []
|
||||
} catch (e) { console.error('Failed to load clipboard violations', e); ElMessage.warning('加载违规记录失败') } finally { vioLoading.value = false }
|
||||
}
|
||||
|
||||
function showRuleDialog(row?: any) {
|
||||
if (row) {
|
||||
editing.value = row
|
||||
Object.assign(form, { rule_type: row.rule_type, direction: row.direction, source_process: row.source_process || '', target_process: row.target_process || '', content_pattern: row.content_pattern || '', enabled: row.enabled })
|
||||
} else {
|
||||
editing.value = null
|
||||
Object.assign(form, { rule_type: 'block', direction: 'out', source_process: '', target_process: '', content_pattern: '', enabled: true })
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveRule() {
|
||||
try {
|
||||
if (editing.value) {
|
||||
await api.put(`/api/plugins/clipboard-control/rules/${editing.value.id}`, form)
|
||||
ElMessage.success('规则已更新')
|
||||
} else {
|
||||
await api.post('/api/plugins/clipboard-control/rules', form)
|
||||
ElMessage.success('规则已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchRules()
|
||||
} catch (e: any) { ElMessage.error(e.message || '保存失败') }
|
||||
}
|
||||
|
||||
async function deleteRule(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除此规则?', '删除确认', { type: 'warning' })
|
||||
await api.delete(`/api/plugins/clipboard-control/rules/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
fetchRules()
|
||||
} catch (e) { console.error('Failed to delete clipboard rule', e) }
|
||||
}
|
||||
|
||||
onMounted(() => { fetchRules(); fetchViolations() })
|
||||
</script>
|
||||
86
web/src/views/plugins/DiskEncryption.vue
Normal file
86
web/src/views/plugins/DiskEncryption.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-tabs v-model="activeTab" class="page-tabs">
|
||||
<el-tab-pane label="加密状态" name="status">
|
||||
<div class="csm-card">
|
||||
<el-table :data="statusList" v-loading="loading" style="width:100%">
|
||||
<el-table-column prop="device_uid" label="设备" min-width="120">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.device_uid?.substring(0, 8) }}...</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="drive_letter" label="驱动器" width="100" />
|
||||
<el-table-column prop="volume_name" label="卷标" width="120">
|
||||
<template #default="{ row }">{{ row.volume_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="encryption_method" label="加密方式" width="140">
|
||||
<template #default="{ row }">{{ row.encryption_method || '未知' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="protection_status" label="保护状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.protection_status === 'On' ? 'success' : row.protection_status === 'Off' ? 'danger' : 'info'" size="small" effect="light">
|
||||
{{ row.protection_status === 'On' ? '已开启' : row.protection_status === 'Off' ? '未开启' : '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="encryption_percentage" label="加密进度" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="Number(row.encryption_percentage)" :status="row.encryption_percentage >= 100 ? 'success' : ''" :stroke-width="6" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lock_status" label="锁定状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.lock_status === 'Unlocked' ? 'success' : 'warning'" size="small">{{ row.lock_status === 'Unlocked' ? '已解锁' : row.lock_status === 'Locked' ? '已锁定' : '未知' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="告警记录" name="alerts">
|
||||
<div class="csm-card">
|
||||
<el-table :data="alerts" v-loading="alertLoading" style="width:100%">
|
||||
<el-table-column prop="device_uid" label="设备" min-width="120">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.device_uid?.substring(0, 8) }}...</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="alert_type" label="告警类型" width="150" />
|
||||
<el-table-column prop="message" label="消息" min-width="200" />
|
||||
<el-table-column prop="severity" label="级别" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.severity === 'critical' ? 'danger' : 'warning'" size="small">{{ row.severity }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="时间" width="180" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const activeTab = ref('status')
|
||||
const statusList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const alerts = ref<any[]>([])
|
||||
const alertLoading = ref(false)
|
||||
|
||||
async function fetchStatus() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/disk-encryption/status')
|
||||
statusList.value = data.entries || data.drives || data.statuses || []
|
||||
} catch (e) { console.error('Failed to load disk encryption status', e); ElMessage.warning('加载磁盘加密状态失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchAlerts() {
|
||||
alertLoading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/disk-encryption/alerts')
|
||||
alerts.value = data.alerts || []
|
||||
} catch (e) { console.error('Failed to load disk encryption alerts', e); ElMessage.warning('加载加密告警失败') } finally { alertLoading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { fetchStatus(); fetchAlerts() })
|
||||
</script>
|
||||
71
web/src/views/plugins/PluginControl.vue
Normal file
71
web/src/views/plugins/PluginControl.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-toolbar">
|
||||
<span style="font-weight:600;font-size:16px">插件管理</span>
|
||||
</div>
|
||||
<div class="csm-card">
|
||||
<el-table :data="plugins" v-loading="loading" style="width:100%">
|
||||
<el-table-column prop="plugin_name" label="插件名称" min-width="200">
|
||||
<template #default="{ row }">{{ pluginLabel(row.plugin_name) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enabled" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '已启用' : '已禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_type" label="作用范围" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ ({ global: '全局', group: '分组', device: '设备' } as Record<string, string>)[row.target_type] || row.target_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_id" label="目标" min-width="120">
|
||||
<template #default="{ row }">{{ row.target_id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.enabled" link type="success" size="small" @click="togglePlugin(row, true)">启用</el-button>
|
||||
<el-button v-else link type="warning" size="small" @click="togglePlugin(row, false)">禁用</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const plugins = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
web_filter: '上网行为管理', usage_timer: '使用时长统计', software_blocker: '软件黑名单',
|
||||
popup_blocker: '弹窗拦截', usb_file_audit: 'U盘文件审计', watermark: '屏幕水印',
|
||||
disk_encryption: '磁盘加密检测', print_audit: '打印审计', clipboard_control: '剪贴板管控',
|
||||
}
|
||||
|
||||
function pluginLabel(name: string) { return labelMap[name] || name }
|
||||
|
||||
async function fetchPlugins() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/control')
|
||||
plugins.value = data.plugins || []
|
||||
} catch (e) { console.error('Failed to load plugin list', e); ElMessage.warning('加载插件列表失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function togglePlugin(row: any, enabled: boolean) {
|
||||
const action = enabled ? '启用' : '禁用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${action}插件「${pluginLabel(row.plugin_name)}」?`, `${action}确认`, { type: 'warning' })
|
||||
await api.put(`/api/plugins/control/${row.plugin_name}`, { enabled })
|
||||
ElMessage.success(`已${action}`)
|
||||
fetchPlugins()
|
||||
} catch (e) { console.error('Failed to toggle plugin state', e) }
|
||||
}
|
||||
|
||||
onMounted(fetchPlugins)
|
||||
</script>
|
||||
@@ -2,16 +2,26 @@
|
||||
<div class="plugin-page">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="拦截规则" name="rules">
|
||||
<div class="toolbar"><el-button type="primary" @click="showDialog()">新建规则</el-button></div>
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="showDialog()">新建规则</el-button>
|
||||
</div>
|
||||
<el-table :data="rules" v-loading="loading" stripe size="small">
|
||||
<el-table-column prop="rule_type" label="类型" width="80">
|
||||
<template #default="{ row }"><el-tag :type="row.rule_type==='block'?'danger':'success'" size="small">{{ row.rule_type==='block'?'拦截':'放行' }}</el-tag></template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.rule_type === 'block' ? 'danger' : 'success'" size="small">
|
||||
{{ row.rule_type === 'block' ? '拦截' : '放行' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="window_title" label="窗口标题" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="window_class" label="窗口类" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="process_name" label="进程" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="enabled" label="启用" width="70">
|
||||
<template #default="{ row }"><el-tag :type="row.enabled?'success':'info'" size="small">{{ row.enabled?'是':'否' }}</el-tag></template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
||||
{{ row.enabled ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
@@ -29,28 +39,136 @@
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-dialog v-model="visible" :title="editing?'编辑规则':'新建规则'" width="480px">
|
||||
|
||||
<el-dialog v-model="visible" :title="editing ? '编辑规则' : '新建规则'" width="480px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="类型"><el-select v-model="form.rule_type"><el-option label="拦截" value="block" /><el-option label="放行" value="allow" /></el-select></el-form-item>
|
||||
<el-form-item label="窗口标题"><el-input v-model="form.window_title" placeholder="匹配模式(支持*通配符)" /></el-form-item>
|
||||
<el-form-item label="窗口类"><el-input v-model="form.window_class" /></el-form-item>
|
||||
<el-form-item label="进程名"><el-input v-model="form.process_name" /></el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="form.rule_type">
|
||||
<el-option label="拦截" value="block" />
|
||||
<el-option label="放行" value="allow" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="窗口标题">
|
||||
<el-input v-model="form.window_title" placeholder="匹配模式(支持*通配符)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="窗口类">
|
||||
<el-input v-model="form.window_class" />
|
||||
</el-form-item>
|
||||
<el-form-item label="进程名">
|
||||
<el-input v-model="form.process_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="visible=false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
const activeTab=ref('rules'),auth=()=>({headers:{Authorization:`Bearer ${localStorage.getItem('token')}`,'Content-Type':'application/json'}})
|
||||
const rules=ref<any[]>([]),loading=ref(false),stats=ref<any[]>([]),sLoading=ref(false),visible=ref(false),editing=ref<any>(null)
|
||||
const form=reactive({rule_type:'block',window_title:'',window_class:'',process_name:''})
|
||||
async function fetchRules(){loading.value=true;try{const r=await fetch('/api/plugins/popup-blocker/rules',auth()).then(r=>r.json());if(r.success)rules.value=r.data.rules||[]}finally{loading.value=false}}
|
||||
async function fetchStats(){sLoading.value=true;try{const r=await fetch('/api/plugins/popup-blocker/stats',auth()).then(r=>r.json());if(r.success)stats.value=r.data.stats||[]}finally{sLoading.value=false}}
|
||||
function showDialog(row?:any){if(row){editing.value=row;Object.assign(form,{rule_type:row.rule_type,window_title:row.window_title||'',window_class:row.window_class||'',process_name:row.process_name||''})}else{editing.value=null;Object.assign(form,{rule_type:'block',window_title:'',window_class:'',process_name:''})}visible.value=true}
|
||||
async function save(){const url=editing.value?`/api/plugins/popup-blocker/rules/${editing.value.id}`:'/api/plugins/popup-blocker/rules';const m=editing.value?'PUT':'POST';const r=await fetch(url,{method:m,...auth(),body:JSON.stringify(form)}).then(r=>r.json());if(r.success){ElMessage.success('已保存');visible.value=false;fetchRules()}else{ElMessage.error(r.error)}}
|
||||
async function remove(id:number){await ElMessageBox.confirm('确定删除?','确认',{type:'warning'});await fetch(`/api/plugins/popup-blocker/rules/${id}`,{method:'DELETE',...auth()});ElMessage.success('已删除');fetchRules()}
|
||||
onMounted(()=>{fetchRules();fetchStats()})
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const activeTab = ref('rules')
|
||||
const rules = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const stats = ref<any[]>([])
|
||||
const sLoading = ref(false)
|
||||
const visible = ref(false)
|
||||
const editing = ref<any>(null)
|
||||
|
||||
const form = reactive({
|
||||
rule_type: 'block',
|
||||
window_title: '',
|
||||
window_class: '',
|
||||
process_name: '',
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
async function fetchRules() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/popup-blocker/rules')
|
||||
rules.value = data?.rules || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
sLoading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/popup-blocker/stats')
|
||||
stats.value = data?.stats || []
|
||||
} finally {
|
||||
sLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showDialog(row?: any) {
|
||||
if (row) {
|
||||
editing.value = row
|
||||
Object.assign(form, {
|
||||
rule_type: row.rule_type,
|
||||
window_title: row.window_title || '',
|
||||
window_class: row.window_class || '',
|
||||
process_name: row.process_name || '',
|
||||
enabled: row.enabled ?? true,
|
||||
})
|
||||
} else {
|
||||
editing.value = null
|
||||
Object.assign(form, {
|
||||
rule_type: 'block',
|
||||
window_title: '',
|
||||
window_class: '',
|
||||
process_name: '',
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
if (editing.value) {
|
||||
await api.put(`/api/plugins/popup-blocker/rules/${editing.value.id}`, form)
|
||||
} else {
|
||||
await api.post('/api/plugins/popup-blocker/rules', form)
|
||||
}
|
||||
ElMessage.success('已保存')
|
||||
visible.value = false
|
||||
fetchRules()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: number) {
|
||||
await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' })
|
||||
await api.delete(`/api/plugins/popup-blocker/rules/${id}`)
|
||||
ElMessage.success('已删除')
|
||||
fetchRules()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRules()
|
||||
fetchStats()
|
||||
})
|
||||
</script>
|
||||
<style scoped>.plugin-page{padding:20px}.toolbar{display:flex;gap:12px;margin-bottom:16px}</style>
|
||||
|
||||
<style scoped>
|
||||
.plugin-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
61
web/src/views/plugins/PrintAudit.vue
Normal file
61
web/src/views/plugins/PrintAudit.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-toolbar">
|
||||
<el-select v-model="filterDevice" placeholder="筛选设备" clearable style="width:220px;margin-right:12px" @change="fetchEvents">
|
||||
<el-option v-for="d in deviceList" :key="d.device_uid" :label="d.hostname" :value="d.device_uid" />
|
||||
</el-select>
|
||||
<el-button @click="fetchEvents">刷新</el-button>
|
||||
</div>
|
||||
<div class="csm-card">
|
||||
<el-table :data="events" v-loading="loading" style="width:100%">
|
||||
<el-table-column prop="device_uid" label="设备" min-width="120">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.device_uid?.substring(0, 8) }}...</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="document_name" label="文档名称" min-width="200">
|
||||
<template #default="{ row }">{{ row.document_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="printer_name" label="打印机" width="160">
|
||||
<template #default="{ row }">{{ row.printer_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pages" label="页数" width="80" />
|
||||
<el-table-column prop="copies" label="份数" width="80" />
|
||||
<el-table-column prop="user_name" label="用户" width="120">
|
||||
<template #default="{ row }">{{ row.user_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="file_size_bytes" label="文件大小" width="110">
|
||||
<template #default="{ row }">{{ row.file_size_bytes ? (row.file_size_bytes / 1024).toFixed(1) + ' KB' : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="timestamp" label="时间" width="180" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const events = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const filterDevice = ref('')
|
||||
const deviceList = ref<any[]>([])
|
||||
|
||||
async function fetchDevices() {
|
||||
try {
|
||||
const data = await api.get<any>('/api/devices')
|
||||
deviceList.value = data.devices || []
|
||||
} catch (e) { console.error('Failed to load device list', e); ElMessage.warning('加载设备列表失败') }
|
||||
}
|
||||
|
||||
async function fetchEvents() {
|
||||
loading.value = true
|
||||
try {
|
||||
const query = filterDevice.value ? `?device_uid=${filterDevice.value}` : ''
|
||||
const data = await api.get<any>(`/api/plugins/print-audit/events${query}`)
|
||||
events.value = data.events || []
|
||||
} catch (e) { console.error('Failed to load print audit events', e); ElMessage.warning('加载打印审计记录失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { fetchDevices(); fetchEvents() })
|
||||
</script>
|
||||
@@ -1,71 +1,184 @@
|
||||
<template>
|
||||
<div class="plugin-page">
|
||||
<el-tabs v-model="activeTab">
|
||||
<div class="page-container">
|
||||
<el-tabs v-model="activeTab" class="page-tabs">
|
||||
<el-tab-pane label="软件黑名单" name="blacklist">
|
||||
<div class="toolbar">
|
||||
<div class="page-toolbar">
|
||||
<el-button type="primary" @click="showDialog()">添加规则</el-button>
|
||||
</div>
|
||||
<el-table :data="blacklist" v-loading="loading" stripe size="small">
|
||||
<el-table-column prop="name_pattern" label="软件名称匹配" min-width="200" />
|
||||
<el-table-column prop="category" label="分类" width="100">
|
||||
<template #default="{ row }"><el-tag size="small">{{ catLabel(row.category) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="action" label="动作" width="100">
|
||||
<template #default="{ row }"><el-tag :type="row.action==='block'?'danger':'warning'" size="small">{{ row.action==='block'?'阻止':'告警' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_type" label="范围" width="80" />
|
||||
<el-table-column prop="enabled" label="启用" width="70">
|
||||
<template #default="{ row }"><el-tag :type="row.enabled?'success':'info'" size="small">{{ row.enabled?'是':'否' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" size="small" @click="remove(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="csm-card">
|
||||
<el-table :data="blacklist" v-loading="loading" style="width:100%">
|
||||
<el-table-column prop="name_pattern" label="软件名称匹配" min-width="200">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.name_pattern }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="category" label="分类" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="light">{{ catLabel(row.category) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="action" label="动作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.action==='block'?'danger':'warning'" size="small" effect="light">{{ row.action==='block'?'阻止':'告警' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_type" label="范围" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ { global:'全局', group:'分组', device:'设备' }[row.target_type as string] || row.target_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enabled" label="启用" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled?'success':'info'" size="small" effect="light">{{ row.enabled?'启用':'停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" size="small" @click="remove(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="违规记录" name="violations">
|
||||
<div class="toolbar"><el-input v-model="vFilter" placeholder="终端UID" style="width:200px" clearable @input="fetchViolations" /></div>
|
||||
<el-table :data="violations" v-loading="vLoading" stripe size="small">
|
||||
<el-table-column prop="device_uid" label="终端" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="software_name" label="软件" min-width="200" />
|
||||
<el-table-column prop="action_taken" label="处理动作" width="150">
|
||||
<template #default="{ row }"><el-tag :type="actionTag(row.action_taken)" size="small">{{ actionLabel(row.action_taken) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="timestamp" label="时间" width="170" />
|
||||
</el-table>
|
||||
<div class="page-toolbar">
|
||||
<el-input v-model="vFilter" placeholder="终端UID" style="width:200px" clearable @input="fetchViolations" />
|
||||
</div>
|
||||
<div class="csm-card">
|
||||
<el-table :data="violations" v-loading="violationsLoading" style="width:100%">
|
||||
<el-table-column prop="device_uid" label="终端" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.device_uid }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="software_name" label="软件" min-width="200" />
|
||||
<el-table-column prop="action_taken" label="处理动作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="actionTag(row.action_taken)" size="small" effect="light">{{ actionLabel(row.action_taken) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="timestamp" label="时间" width="170">
|
||||
<template #default="{ row }"><span class="secondary-text">{{ row.timestamp }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-dialog v-model="dialogVisible" title="添加黑名单规则" width="480px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="软件名称"><el-input v-model="form.name_pattern" placeholder="*游戏* / *.exe" /></el-form-item>
|
||||
<el-form-item label="分类"><el-select v-model="form.category"><el-option label="游戏" value="game" /><el-option label="社交" value="social" /><el-option label="VPN" value="vpn" /><el-option label="挖矿" value="mining" /><el-option label="自定义" value="custom" /></el-select></el-form-item>
|
||||
<el-form-item label="动作"><el-select v-model="form.action"><el-option label="阻止安装" value="block" /><el-option label="仅告警" value="alert" /></el-select></el-form-item>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="添加黑名单规则" width="480px" destroy-on-close>
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="软件名称">
|
||||
<el-input v-model="form.name_pattern" placeholder="支持通配符" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="form.category" style="width:100%">
|
||||
<el-option label="游戏" value="game" />
|
||||
<el-option label="社交" value="social" />
|
||||
<el-option label="VPN" value="vpn" />
|
||||
<el-option label="挖矿" value="mining" />
|
||||
<el-option label="自定义" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="动作">
|
||||
<el-select v-model="form.action" style="width:100%">
|
||||
<el-option label="阻止安装" value="block" />
|
||||
<el-option label="仅告警" value="alert" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const activeTab = ref('blacklist')
|
||||
const auth = () => ({ headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json' } })
|
||||
const blacklist = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const violations = ref<any[]>([])
|
||||
const vLoading = ref(false)
|
||||
const violationsLoading = ref(false)
|
||||
const vFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const form = reactive({ name_pattern: '', category: 'custom', action: 'block' })
|
||||
function catLabel(c: string) { return { game:'游戏',social:'社交',vpn:'VPN',mining:'挖矿',custom:'自定义' }[c]||c }
|
||||
function actionLabel(a: string) { return { blocked_install:'已阻止',auto_uninstalled:'已卸载',alerted:'已告警' }[a]||a }
|
||||
function actionTag(a: string) { return { blocked_install:'danger',auto_uninstalled:'warning',alerted:'' }[a]||'info' }
|
||||
async function fetchBlacklist() { loading.value=true; try{const r=await fetch('/api/plugins/software-blocker/blacklist',auth()).then(r=>r.json());if(r.success)blacklist.value=r.data.blacklist||[]}finally{loading.value=false} }
|
||||
async function fetchViolations() { vLoading.value=true; try{const p=new URLSearchParams();if(vFilter.value)p.set('device_uid',vFilter.value);const r=await fetch(`/api/plugins/software-blocker/violations?${p}`,auth()).then(r=>r.json());if(r.success)violations.value=r.data.violations||[]}finally{vLoading.value=false} }
|
||||
function showDialog(){Object.assign(form,{name_pattern:'',category:'custom',action:'block'});dialogVisible.value=true}
|
||||
async function save(){const r=await fetch('/api/plugins/software-blocker/blacklist',{method:'POST',...auth(),body:JSON.stringify(form)}).then(r=>r.json());if(r.success){ElMessage.success('已添加');dialogVisible.value=false;fetchBlacklist()}else{ElMessage.error(r.error)}}
|
||||
async function remove(id:number){await ElMessageBox.confirm('确定删除?','确认',{type:'warning'});await fetch(`/api/plugins/software-blocker/blacklist/${id}`,{method:'DELETE',...auth()});ElMessage.success('已删除');fetchBlacklist()}
|
||||
onMounted(()=>{fetchBlacklist();fetchViolations()})
|
||||
|
||||
function catLabel(c: string | undefined) {
|
||||
if (!c) return '未知'
|
||||
return { game: '游戏', social: '社交', vpn: 'VPN', mining: '挖矿', custom: '自定义' }[c] || c
|
||||
}
|
||||
|
||||
function actionLabel(a: string | undefined) {
|
||||
if (!a) return '未知'
|
||||
return { blocked_install: '已阻止', auto_uninstalled: '已卸载', alerted: '已告警' }[a] || '已告警'
|
||||
}
|
||||
|
||||
function actionTag(a: string) {
|
||||
return { blocked_install: 'danger', auto_uninstalled: 'warning', alerted: '' }[a] || 'info'
|
||||
}
|
||||
|
||||
async function fetchBlacklist() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/software-blocker/blacklist')
|
||||
blacklist.value = data.blacklist || []
|
||||
} catch (e) { console.error('Failed to load software blacklist', e); ElMessage.warning('加载软件黑名单失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchViolations() {
|
||||
violationsLoading.value = true
|
||||
try {
|
||||
const p = new URLSearchParams()
|
||||
if (vFilter.value) p.set('device_uid', vFilter.value)
|
||||
const data = await api.get<any>(`/api/plugins/software-blocker/violations?${p}`)
|
||||
violations.value = data.violations || []
|
||||
} catch (e) { console.error('Failed to load software violations', e); ElMessage.warning('加载违规记录失败') } finally { violationsLoading.value = false }
|
||||
}
|
||||
|
||||
function showDialog() {
|
||||
Object.assign(form, { name_pattern: '', category: 'custom', action: 'block' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
await api.post('/api/plugins/software-blocker/blacklist', form)
|
||||
ElMessage.success('已添加')
|
||||
dialogVisible.value = false
|
||||
fetchBlacklist()
|
||||
} catch (e: any) { ElMessage.error(e.message || '添加失败') }
|
||||
}
|
||||
|
||||
async function remove(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' })
|
||||
await api.delete(`/api/plugins/software-blocker/blacklist/${id}`)
|
||||
ElMessage.success('已删除')
|
||||
fetchBlacklist()
|
||||
} catch (e) { console.error('Failed to remove software rule', e) }
|
||||
}
|
||||
|
||||
onMounted(() => { fetchBlacklist(); fetchViolations() })
|
||||
</script>
|
||||
<style scoped>.plugin-page{padding:20px}.toolbar{display:flex;gap:12px;margin-bottom:16px}</style>
|
||||
|
||||
<style scoped>
|
||||
.mono-text {
|
||||
font-family: var(--csm-font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--csm-text-secondary);
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
font-size: 12px;
|
||||
color: var(--csm-text-tertiary);
|
||||
}
|
||||
|
||||
.page-tabs :deep(.el-tabs__header) {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--csm-border-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { api } from '@/lib/api'
|
||||
const activeTab = ref('daily')
|
||||
const auth = () => ({ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } })
|
||||
const uidFilter = ref('')
|
||||
const dailyData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -58,16 +58,16 @@ function formatMinutes(m: number) { if(m>=60) return `${Math.floor(m/60)}h${m%60
|
||||
async function fetchDaily() {
|
||||
loading.value=true
|
||||
try{const params=new URLSearchParams();if(uidFilter.value)params.set('device_uid',uidFilter.value)
|
||||
const r=await fetch(`/api/plugins/usage-timer/daily?${params}`,auth()).then(r=>r.json());if(r.success)dailyData.value=r.data.daily||[]}finally{loading.value=false}
|
||||
const data=await api.get<any>(`/api/plugins/usage-timer/daily?${params}`);dailyData.value=data?.daily||[]}finally{loading.value=false}
|
||||
}
|
||||
async function fetchApps() {
|
||||
appLoading.value=true
|
||||
try{const params=new URLSearchParams();if(appUid.value)params.set('device_uid',appUid.value)
|
||||
const r=await fetch(`/api/plugins/usage-timer/app-usage?${params}`,auth()).then(r=>r.json());if(r.success)appData.value=r.data.app_usage||[]}finally{appLoading.value=false}
|
||||
const data=await api.get<any>(`/api/plugins/usage-timer/app-usage?${params}`);appData.value=data?.app_usage||[]}finally{appLoading.value=false}
|
||||
}
|
||||
async function fetchBoard() {
|
||||
boardLoading.value=true
|
||||
try{const r=await fetch('/api/plugins/usage-timer/leaderboard',auth()).then(r=>r.json());if(r.success)board.value=r.data.leaderboard||[]}finally{boardLoading.value=false}
|
||||
try{const data=await api.get<any>('/api/plugins/usage-timer/leaderboard');board.value=data?.leaderboard||[]}finally{boardLoading.value=false}
|
||||
}
|
||||
onMounted(()=>{fetchDaily();fetchApps();fetchBoard()})
|
||||
</script>
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { api } from '@/lib/api'
|
||||
const activeTab = ref('log')
|
||||
const auth = () => ({ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } })
|
||||
const filters = reactive({ device_uid: '', operation: '' })
|
||||
const log = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -46,8 +46,8 @@ const sLoading = ref(false)
|
||||
function opTag(o:string){return{create:'success',delete:'danger',modify:'warning',rename:'info'}[o]||'info'}
|
||||
function opLabel(o:string){return{create:'创建',delete:'删除',modify:'修改',rename:'重命名'}[o]||o}
|
||||
function formatSize(b:number){if(b>=1073741824)return`${(b/1073741824).toFixed(1)}GB`;if(b>=1048576)return`${(b/1048576).toFixed(1)}MB`;if(b>=1024)return`${(b/1024).toFixed(1)}KB`;return`${b}B`}
|
||||
async function fetchLog(){loading.value=true;try{const p=new URLSearchParams();if(filters.device_uid)p.set('device_uid',filters.device_uid);if(filters.operation)p.set('operation',filters.operation);const r=await fetch(`/api/plugins/usb-file-audit/log?${p}`,auth()).then(r=>r.json());if(r.success)log.value=r.data.operations||[]}finally{loading.value=false}}
|
||||
async function fetchSummary(){sLoading.value=true;try{const r=await fetch('/api/plugins/usb-file-audit/summary',auth()).then(r=>r.json());if(r.success)summaryData.value=r.data.summary||[]}finally{sLoading.value=false}}
|
||||
async function fetchLog(){loading.value=true;try{const p=new URLSearchParams();if(filters.device_uid)p.set('device_uid',filters.device_uid);if(filters.operation)p.set('operation',filters.operation);const data=await api.get<any>(`/api/plugins/usb-file-audit/log?${p}`);log.value=data?.operations||[]}finally{loading.value=false}}
|
||||
async function fetchSummary(){sLoading.value=true;try{const data=await api.get<any>('/api/plugins/usb-file-audit/summary');summaryData.value=data?.summary||[]}finally{sLoading.value=false}}
|
||||
onMounted(()=>{fetchLog();fetchSummary()})
|
||||
</script>
|
||||
<style scoped>.plugin-page{padding:20px}.toolbar{display:flex;gap:12px;margin-bottom:16px}</style>
|
||||
|
||||
@@ -141,7 +141,7 @@ async function fetchConfigs() {
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/watermark/config')
|
||||
configs.value = data.configs || []
|
||||
} catch { /* api.ts handles 401 */ } finally { loading.value = false }
|
||||
} catch (e) { console.error('Failed to load watermark configs', e); ElMessage.warning('加载水印配置失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
function showDialog(row?: any) {
|
||||
|
||||
@@ -121,7 +121,7 @@ async function fetchRules() {
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/web-filter/rules')
|
||||
rules.value = data.rules || []
|
||||
} catch { /* api.ts handles 401 */ } finally { loading.value = false }
|
||||
} catch (e) { console.error('Failed to load web filter rules', e); ElMessage.warning('加载过滤规则失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchLog() {
|
||||
@@ -129,7 +129,7 @@ async function fetchLog() {
|
||||
try {
|
||||
const data = await api.get<any>('/api/plugins/web-filter/log')
|
||||
accessLog.value = data.log || []
|
||||
} catch { /* api.ts handles 401 */ } finally { logLoading.value = false }
|
||||
} catch (e) { console.error('Failed to load web filter access log', e); ElMessage.warning('加载访问日志失败') } finally { logLoading.value = false }
|
||||
}
|
||||
|
||||
function showRuleDialog(row?: any) {
|
||||
|
||||
Reference in New Issue
Block a user