- 更新 AdminView.vue 组件结构,使用新的布局和导航设计 - 集成 Element Plus 图标组件,提升界面美观度 - 添加响应式设计支持,适配移动端设备 - 重构 HomeView.vue 组件,改进 Token 查询功能 - 实现自动刷新机制,每5秒更新 Token 信息 - 优化 TokenDetailView.vue 组件,增强数据管理功能 - 添加确认对话框,防止误删操作 - 在 App.vue 中引入全局 CSS 变量和主题系统 - 创建通用组件样式类,统一页面外观 - 优化数据加载逻辑,提升页面性能和用户体验
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 9.8 KiB |
+149
-3
@@ -1,15 +1,161 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const themeMode = ref('light')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div :class="themeMode" class="app-container">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
html, body, #app {
|
:root {
|
||||||
height: 100%;
|
--primary-color: #409eff;
|
||||||
|
--primary-light: #79bbff;
|
||||||
|
--success-color: #67c23a;
|
||||||
|
--warning-color: #e6a23c;
|
||||||
|
--danger-color: #f56c6c;
|
||||||
|
--info-color: #909399;
|
||||||
|
--text-primary: #303133;
|
||||||
|
--text-regular: #606266;
|
||||||
|
--text-secondary: #909399;
|
||||||
|
--border-color: #dcdfe6;
|
||||||
|
--bg-page: #f5f7fa;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--bg-header: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app {
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-page);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片通用样式 */
|
||||||
|
.content-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面标题样式 */
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section 标题 */
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 20px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组样式 */
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格工具栏 */
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flex 布局工具类 */
|
||||||
|
.flex-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gap 间距 */
|
||||||
|
.gap-sm { gap: 8px; }
|
||||||
|
.gap-md { gap: 12px; }
|
||||||
|
.gap-lg { gap: 16px; }
|
||||||
|
|
||||||
|
/* 间距工具类 */
|
||||||
|
.mt-sm { margin-top: 8px; }
|
||||||
|
.mt-md { margin-top: 16px; }
|
||||||
|
.mt-lg { margin-top: 24px; }
|
||||||
|
.mb-sm { margin-bottom: 8px; }
|
||||||
|
.mb-md { margin-bottom: 16px; }
|
||||||
|
.mb-lg { margin-bottom: 24px; }
|
||||||
|
.ml-sm { margin-left: 8px; }
|
||||||
|
.ml-md { margin-left: 12px; }
|
||||||
|
.ml-lg { margin-left: 16px; }
|
||||||
|
.mr-sm { margin-right: 8px; }
|
||||||
|
.mr-md { margin-right: 12px; }
|
||||||
|
.mr-lg { margin-right: 16px; }
|
||||||
|
|
||||||
|
/* 文本工具类 */
|
||||||
|
.text-primary { color: var(--text-primary); }
|
||||||
|
.text-success { color: var(--success-color); }
|
||||||
|
.text-warning { color: var(--warning-color); }
|
||||||
|
.text-danger { color: var(--danger-color); }
|
||||||
|
.text-info { color: var(--info-color); }
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 48px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content-card {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-on-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+202
-28
@@ -1,50 +1,224 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, watch} from 'vue'
|
import {ref, watch, computed} from 'vue'
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router"
|
||||||
import {useCounterStore} from "@/stores/counter.ts";
|
import {useCounterStore} from "@/stores/counter.ts"
|
||||||
|
import {Edit, Delete, Clock, User, Document} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const store = useCounterStore()
|
||||||
|
|
||||||
const activeIndex = ref(route.name?.toString() || "TokenList")
|
const activeIndex = ref(route.name?.toString() || "TokenManage")
|
||||||
|
|
||||||
watch(route, (newRoute) => {
|
watch(route, (newRoute) => {
|
||||||
activeIndex.value = newRoute.name?.toString() || "TokenList"
|
activeIndex.value = newRoute.name?.toString() || "TokenManage"
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSelect = (key: string) => {
|
const handleSelect = (key: string) => {
|
||||||
router.push({
|
router.push({name: key})
|
||||||
name: key
|
}
|
||||||
})
|
|
||||||
|
const menuItems = [
|
||||||
|
{key: 'TokenManage', label: '管理 Token', icon: Edit},
|
||||||
|
{key: 'TokenDetail', label: 'Token 详情', icon: Document},
|
||||||
|
]
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
store.isAdmin = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div class="admin-layout">
|
||||||
<el-header>
|
<header class="admin-header">
|
||||||
<el-menu
|
<div class="header-left">
|
||||||
:default-active="activeIndex"
|
<div class="logo">
|
||||||
class="el-menu-demo"
|
<el-icon size="24">
|
||||||
mode="horizontal"
|
<Edit/>
|
||||||
@select="handleSelect"
|
</el-icon>
|
||||||
|
<span class="logo-text">DYPID 管理后台</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="header-nav">
|
||||||
|
<div
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.key"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeIndex === item.key }"
|
||||||
|
@click="handleSelect(item.key)"
|
||||||
>
|
>
|
||||||
<el-menu-item index="TokenManage">管理Token</el-menu-item>
|
<el-icon>
|
||||||
<el-menu-item index="TokenDetail">Token详细信息</el-menu-item>
|
<component :is="item.icon"/>
|
||||||
<el-menu-item v-if="useCounterStore().isAdmin">
|
</el-icon>
|
||||||
<el-button type="danger" plain @click="useCounterStore().isAdmin=false">退出管理员</el-button>
|
<span>{{ item.label }}</span>
|
||||||
</el-menu-item>
|
</div>
|
||||||
</el-menu>
|
</nav>
|
||||||
|
|
||||||
</el-header>
|
<div class="header-right">
|
||||||
|
<div v-if="store.isAdmin" class="admin-badge">
|
||||||
|
<el-icon>
|
||||||
|
<User/>
|
||||||
|
</el-icon>
|
||||||
|
<span>管理员</span>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
v-if="store.isAdmin"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
@click="logout"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<Delete/>
|
||||||
|
</el-icon>
|
||||||
|
退出
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<el-container>
|
<main class="admin-main">
|
||||||
<el-main>
|
<router-view v-slot="{ Component }">
|
||||||
<router-view></router-view>
|
<transition name="fade" mode="out-in">
|
||||||
</el-main>
|
<component :is="Component"/>
|
||||||
</el-container>
|
</transition>
|
||||||
</el-container>
|
</router-view>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.admin-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
height: 60px;
|
||||||
|
background: var(--bg-header);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.logo-text {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-regular);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: var(--bg-page);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--danger-color);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px 48px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-header {
|
||||||
|
padding: 0 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav {
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+251
-53
@@ -1,77 +1,275 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref} from 'vue'
|
import {ref, onMounted, onUnmounted} from 'vue'
|
||||||
import axios from "@/axios.ts";
|
import axios from "@/axios.ts"
|
||||||
import {useRoute} from "vue-router"
|
import {useRoute} from "vue-router"
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus"
|
||||||
|
import {
|
||||||
|
Key, Refresh, Delete, DataAnalysis,
|
||||||
|
Document, Warning, Search, InfoFilled
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const token = ref(useRoute().query.token)
|
const route = useRoute()
|
||||||
const input = ref(useRoute().query.token)
|
const token = ref(route.query.token as string || '')
|
||||||
const result = ref()
|
const input = ref(route.query.token as string || '')
|
||||||
|
const result = ref<any>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const lastUpdate = ref('')
|
||||||
|
|
||||||
const inputChange = () => {
|
const updateTime = () => {
|
||||||
if (input.value != null && input.value != '') {
|
const now = new Date()
|
||||||
axios.get('/api/token/info', {
|
lastUpdate.value = now.toLocaleTimeString()
|
||||||
params: {
|
}
|
||||||
token: input.value
|
|
||||||
}
|
const fetchInfo = async () => {
|
||||||
}).then(res => {
|
if (!input.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/token/info', {
|
||||||
|
params: {token: input.value}
|
||||||
|
})
|
||||||
result.value = res.data.result
|
result.value = res.data.result
|
||||||
token.value = input.value
|
token.value = input.value
|
||||||
ElMessage({
|
updateTime()
|
||||||
message: '更改成功',
|
// ElMessage({message: '刷新成功', type: 'success', duration: 1500})
|
||||||
type: 'success',
|
} catch {
|
||||||
})
|
ElMessage({message: 'Token输入错误', type: 'error', duration: 2000})
|
||||||
}).catch(error => {
|
} finally {
|
||||||
ElMessage({
|
loading.value = false
|
||||||
message: 'Token输入错误',
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInfo = () => {
|
const inputChange = () => {
|
||||||
if (token.value != null && token.value != '') {
|
if (input.value) fetchInfo()
|
||||||
axios.get('/api/token/info', {
|
|
||||||
params: {
|
|
||||||
token: token.value
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
result.value = res.data.result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getInfo()
|
let timer: number
|
||||||
setInterval(getInfo, 5000)
|
onMounted(() => {
|
||||||
|
if (token.value) fetchInfo()
|
||||||
|
timer = window.setInterval(fetchInfo, 5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timer)
|
||||||
|
})
|
||||||
|
|
||||||
|
const statCards = [
|
||||||
|
{label: '去重对象', key: 'dedup_object', icon: DataAnalysis, color: '#409eff'},
|
||||||
|
{label: '上传数据格式', key: 'data_format', icon: Document, color: '#67c23a'},
|
||||||
|
{label: '去重记录值', key: 'dedup_items_number', icon: Key, color: '#e6a23c'},
|
||||||
|
{label: 'Redis中数据条数', key: 'cache_list_number', icon: Warning, color: '#909399'},
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<b>当前Token:</b>
|
<div class="home-page">
|
||||||
|
<div class="content-card" style="max-width: 1600px;">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-title">Token 信息查询</div>
|
||||||
|
<div class="header-subtitle">输入 Token 以查看去重和缓存信息</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-box">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="input"
|
v-model="input"
|
||||||
style="width: 240px"
|
placeholder="输入 Token"
|
||||||
placeholder="输入Token"
|
|
||||||
clearable
|
clearable
|
||||||
|
size="large"
|
||||||
|
class="token-input"
|
||||||
@change="inputChange"
|
@change="inputChange"
|
||||||
/>
|
@keyup.enter="fetchInfo"
|
||||||
|
|
||||||
<el-divider/>
|
|
||||||
<b>Token信息(每5秒刷新)</b>
|
|
||||||
<el-button type="primary" plain @click="getInfo">手动刷新</el-button>
|
|
||||||
<el-descriptions
|
|
||||||
direction="vertical"
|
|
||||||
:column="4"
|
|
||||||
border
|
|
||||||
>
|
>
|
||||||
<el-descriptions-item label="去重对象">{{ result?.dedup_object }}</el-descriptions-item>
|
<template #prefix>
|
||||||
<el-descriptions-item label="上传数据格式">{{ result?.data_format }}</el-descriptions-item>
|
<el-icon>
|
||||||
<el-descriptions-item label="去重记录值">{{ result?.dedup_items_number }}</el-descriptions-item>
|
<Search/>
|
||||||
<el-descriptions-item label="Redis中数据条数">{{ result?.cache_list_number }}</el-descriptions-item>
|
</el-icon>
|
||||||
</el-descriptions>
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" size="large" @click="fetchInfo" :loading="loading">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="result" class="result-section">
|
||||||
|
<div class="result-header">
|
||||||
|
<span class="section-title">Token 信息</span>
|
||||||
|
<span class="auto-refresh">每5秒自动刷新</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-cards">
|
||||||
|
<div v-for="card in statCards" :key="card.key" class="stat-card">
|
||||||
|
<div class="stat-icon" :style="{ background: card.color + '20', color: card.color }">
|
||||||
|
<el-icon size="24">
|
||||||
|
<component :is="card.icon"/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-label">{{ card.label }}</div>
|
||||||
|
<div class="stat-value">{{ result[card.key] || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="lastUpdate" class="update-time">
|
||||||
|
最后更新: {{ lastUpdate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<el-icon size="64" color="#dcdfe6">
|
||||||
|
<Search/>
|
||||||
|
</el-icon>
|
||||||
|
<div class="empty-text">请输入 Token 进行查询</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
padding: 24px 48px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-input {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-refresh {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-time {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 64px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.stat-cards {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.home-page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-input {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+476
-141
@@ -1,198 +1,533 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useCounterStore} from "@/stores/counter.ts";
|
import {ref, onMounted, onUnmounted, watch} from 'vue'
|
||||||
import {ref, watch} from 'vue'
|
import {useCounterStore} from "@/stores/counter.ts"
|
||||||
import axios from "@/axios.ts";
|
import axios from "@/axios.ts"
|
||||||
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
|
import {
|
||||||
|
Key, Refresh, Delete, DataAnalysis,
|
||||||
|
Document, Warning, Search, InfoFilled
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const store = useCounterStore()
|
||||||
// 创建响应式引用,用于存储API请求结果
|
const result = ref<any>(null)
|
||||||
const result = ref()
|
|
||||||
|
|
||||||
// 创建响应式引用,用于存储当前选中的token值
|
|
||||||
const value = ref('')
|
const value = ref('')
|
||||||
value.value = useCounterStore().token
|
const options = ref<string[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const lastUpdate = ref('')
|
||||||
|
|
||||||
// 创建响应式引用,用于存储下拉选项列表
|
|
||||||
const options = ref([] as string[])
|
|
||||||
|
|
||||||
// 控制删除指定Redis键的确认对话框的显示状态
|
|
||||||
const deleteSpecifyDataVisible = ref(false)
|
const deleteSpecifyDataVisible = ref(false)
|
||||||
const inputSpecifyData = ref('')
|
const inputSpecifyData = ref('')
|
||||||
|
|
||||||
const deleteSpecifyDedupVisible = ref(false)
|
const deleteSpecifyDedupVisible = ref(false)
|
||||||
const inputSpecifyDedup = ref('')
|
const inputSpecifyDedup = ref('')
|
||||||
const deleteSpecifyRawVisible = ref(false)
|
const deleteSpecifyRawVisible = ref(false)
|
||||||
const inputSpecifyRaw = ref('')
|
const inputSpecifyRaw = ref('')
|
||||||
|
|
||||||
const getInfo = () => {
|
const fetchInfo = async () => {
|
||||||
if (value.value != '') {
|
if (!value.value) return
|
||||||
axios.get('/api/token/info', {
|
loading.value = true
|
||||||
params: {
|
try {
|
||||||
token: value.value
|
const res = await axios.get('/api/token/info', {
|
||||||
}
|
params: {token: value.value}
|
||||||
}).then(res => {
|
|
||||||
if (res.status == 200) {
|
|
||||||
result.value = res.data.result
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
if (res.status === 200) {
|
||||||
|
result.value = res.data.result
|
||||||
|
lastUpdate.value = new Date().toLocaleTimeString()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteDedup = () => {
|
const fetchTokens = async () => {
|
||||||
axios.delete('/api/token/info', {
|
try {
|
||||||
params: {
|
const res = await axios.get('/api/token')
|
||||||
token: value.value,
|
if (res.status === 200) {
|
||||||
dedup_bf: "all"
|
options.value = res.data.result?.map((item: any) => item.token) || []
|
||||||
}
|
}
|
||||||
}).then(res => {
|
} catch (error) {
|
||||||
getInfo()
|
console.error('Failed to fetch tokens:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
fetchInfo()
|
||||||
|
ElMessage({message: '刷新成功', type: 'success', duration: 1500})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDedup = () => {
|
||||||
|
ElMessageBox.confirm('确定要删除全部去重参考值吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}).then(async () => {
|
||||||
|
await axios.delete('/api/token/info', {
|
||||||
|
params: {token: value.value, dedup_bf: "all"}
|
||||||
|
})
|
||||||
|
ElMessage({message: '删除成功', type: 'success'})
|
||||||
|
fetchInfo()
|
||||||
|
}).catch(() => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRedis = () => {
|
const deleteRedis = () => {
|
||||||
axios.delete('/api/token/info', {
|
ElMessageBox.confirm('确定要删除全部原始数据吗?', '警告', {
|
||||||
params: {
|
confirmButtonText: '确定',
|
||||||
token: value.value,
|
cancelButtonText: '取消',
|
||||||
cache_list: "all",
|
type: 'warning',
|
||||||
}
|
}).then(async () => {
|
||||||
}).then(res => {
|
await axios.delete('/api/token/info', {
|
||||||
getInfo()
|
params: {token: value.value, cache_list: "all"}
|
||||||
|
})
|
||||||
|
ElMessage({message: '删除成功', type: 'success'})
|
||||||
|
fetchInfo()
|
||||||
|
}).catch(() => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getInfo()
|
const deleteSpecifyDedup = async () => {
|
||||||
setInterval(getInfo, 5000)
|
await axios.delete('/api/token/info', {
|
||||||
|
params: {token: value.value, dedup_bf: inputSpecifyDedup.value}
|
||||||
|
})
|
||||||
|
ElMessage({message: '删除成功', type: 'success'})
|
||||||
|
deleteSpecifyDedupVisible.value = false
|
||||||
|
inputSpecifyDedup.value = ''
|
||||||
|
fetchInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSpecifyRaw = async () => {
|
||||||
|
await axios.delete('/api/token/info', {
|
||||||
|
params: {token: value.value, cache_list: inputSpecifyRaw.value}
|
||||||
|
})
|
||||||
|
ElMessage({message: '删除成功', type: 'success'})
|
||||||
|
deleteSpecifyRawVisible.value = false
|
||||||
|
inputSpecifyRaw.value = ''
|
||||||
|
fetchInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSpecifyData = async () => {
|
||||||
|
await axios.delete('/api/token/info', {
|
||||||
|
params: {token: value.value, both_number: inputSpecifyData.value}
|
||||||
|
})
|
||||||
|
ElMessage({message: '删除成功', type: 'success'})
|
||||||
|
deleteSpecifyDataVisible.value = false
|
||||||
|
inputSpecifyData.value = ''
|
||||||
|
fetchInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer: number
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTokens()
|
||||||
|
if (store.token) {
|
||||||
|
value.value = store.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(value, (newValue) => {
|
watch(value, (newValue) => {
|
||||||
useCounterStore().token = value.value
|
store.token = newValue
|
||||||
getInfo()
|
if (newValue) fetchInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
axios.get('/api/token').then(res => {
|
if (value.value) fetchInfo()
|
||||||
if (res.status == 200) {
|
timer = window.setInterval(fetchInfo, 5000)
|
||||||
res.data.result.forEach((item: any) => {
|
|
||||||
options.value.push(item.token)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteSpecifyData = () => {
|
onUnmounted(() => {
|
||||||
axios.delete('/api/token/info', {
|
clearInterval(timer)
|
||||||
params: {
|
})
|
||||||
token: value.value,
|
|
||||||
both_number: inputSpecifyData.value,
|
const statCards = [
|
||||||
}
|
{label: '去重对象', key: 'dedup_object', icon: DataAnalysis, color: '#409eff'},
|
||||||
}).then(res => {
|
{label: '上传数据格式', key: 'data_format', icon: Document, color: '#67c23a'},
|
||||||
getInfo()
|
{label: '去重参考值数量', key: 'dedup_items_number', icon: Key, color: '#e6a23c'},
|
||||||
deleteSpecifyDataVisible.value = false
|
{label: '原始数据数量', key: 'cache_list_number', icon: Warning, color: '#909399'},
|
||||||
})
|
]
|
||||||
}
|
|
||||||
const deleteSpecifyDedup = () => {
|
|
||||||
axios.delete('/api/token/info', {
|
|
||||||
params: {
|
|
||||||
token: value.value,
|
|
||||||
dedup_bf: inputSpecifyDedup.value,
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
getInfo()
|
|
||||||
deleteSpecifyDedupVisible.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const deleteSpecifyRaw = () => {
|
|
||||||
axios.delete('/api/token/info', {
|
|
||||||
params: {
|
|
||||||
token: value.value,
|
|
||||||
cache_list: inputSpecifyRaw.value,
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
getInfo()
|
|
||||||
deleteSpecifyRawVisible.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!useCounterStore().isAdmin">
|
<div class="token-detail">
|
||||||
<el-alert title="您没有权限访问此页面" type="error" center show-icon/>
|
<!-- 检查权限 -->
|
||||||
|
<div v-if="!store.isAdmin" class="no-permission">
|
||||||
|
<div class="no-permission-card">
|
||||||
|
<el-icon size="64" color="#f56c6c">
|
||||||
|
<InfoFilled/>
|
||||||
|
</el-icon>
|
||||||
|
<div class="no-permission-title">无权限访问</div>
|
||||||
|
<div class="no-permission-text">您没有权限访问此页面</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 管理页面 -->
|
||||||
|
<div v-else class="detail-page">
|
||||||
|
<div class="content-card">
|
||||||
|
<div class="page-title">Token 详情</div>
|
||||||
|
|
||||||
<div v-if="useCounterStore().isAdmin">
|
<!-- 选择 Token -->
|
||||||
<b>当前Token:</b>
|
<div class="token-select">
|
||||||
<el-select v-model="value" placeholder="选择Token" style="width: 240px">
|
<el-select v-model="value" placeholder="选择 Token" clearable style="width: 100%; max-width: 300px">
|
||||||
<el-option
|
<el-option v-for="item in options" :key="item" :value="item" :label="item"/>
|
||||||
v-for="item in options"
|
|
||||||
:key="item"
|
|
||||||
:value="item"
|
|
||||||
/>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-button type="primary" @click="refresh" :loading="loading">
|
||||||
<el-divider/>
|
<el-icon>
|
||||||
|
<Refresh/>
|
||||||
<b>Token信息(每5秒刷新)</b>
|
</el-icon>
|
||||||
<el-button type="primary" plain @click="getInfo">手动刷新</el-button>
|
刷新
|
||||||
<el-descriptions
|
|
||||||
direction="vertical"
|
|
||||||
:column="4"
|
|
||||||
border
|
|
||||||
>
|
|
||||||
<el-descriptions-item label="去重对象" label-width="150px">
|
|
||||||
<el-tag>{{ result?.dedup_object }}</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="上传数据格式" label-width="150px">
|
|
||||||
{{ result?.data_format }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="去重参考值数量" label-width="150px">
|
|
||||||
{{ result?.dedup_items_number }} 条
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="原始数据数量" label-width="150px">
|
|
||||||
{{ result?.cache_list_number }} 条
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<p><b>管理</b></p>
|
|
||||||
<el-button type="danger" @click="deleteDedup">删除全部去重参考值</el-button>
|
|
||||||
<el-button type="danger" @click="deleteRedis">删除全部原始数据</el-button>
|
|
||||||
<div style="margin-top: 10px">
|
|
||||||
<el-button type="danger" @click="deleteSpecifyDedupVisible=true">删除指定数量去重参考值</el-button>
|
|
||||||
<el-button type="danger" @click="deleteSpecifyRawVisible=true">删除指定数量原始数据</el-button>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 10px">
|
|
||||||
<el-button type="danger" @click="deleteSpecifyDataVisible=true">
|
|
||||||
删除指定数量的数据(去重参考值+原始数据)
|
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<span v-if="lastUpdate" class="update-time">上次更新: {{ lastUpdate }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Token 信息 -->
|
||||||
|
<div v-if="result && value" class="info-section">
|
||||||
|
<div class="section-title">Token 信息</div>
|
||||||
|
|
||||||
<!--弹窗输入-->
|
<div class="stat-cards">
|
||||||
|
<div v-for="card in statCards" :key="card.key" class="stat-card">
|
||||||
|
<div class="stat-icon" :style="{ background: card.color + '20', color: card.color }">
|
||||||
|
<el-icon size="24">
|
||||||
|
<component :is="card.icon"/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-label">{{ card.label }}</div>
|
||||||
|
<div class="stat-value">{{ result[card.key] || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 管理操作 -->
|
||||||
|
<div class="manage-section">
|
||||||
|
<div class="section-title">数据管理</div>
|
||||||
|
|
||||||
|
<div class="action-grid">
|
||||||
|
<div class="action-card danger">
|
||||||
|
<div class="action-icon">
|
||||||
|
<el-icon size="28">
|
||||||
|
<Delete/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action-info">
|
||||||
|
<div class="action-label">删除全部去重参考值</div>
|
||||||
|
<div class="action-desc">清除该 Token 的所有去重布隆过滤器</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="danger" @click="deleteDedup">执行</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-card danger">
|
||||||
|
<div class="action-icon">
|
||||||
|
<el-icon size="28">
|
||||||
|
<Delete/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action-info">
|
||||||
|
<div class="action-label">删除全部原始数据</div>
|
||||||
|
<div class="action-desc">清除该 Token 的所有原始缓存数据</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="danger" @click="deleteRedis">执行</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-subtitle">按数量删除</div>
|
||||||
|
|
||||||
|
<div class="action-grid">
|
||||||
|
<div class="action-card">
|
||||||
|
<div class="action-icon warning">
|
||||||
|
<el-icon size="28">
|
||||||
|
<Search/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action-info">
|
||||||
|
<div class="action-label">删除指定数量去重参考值</div>
|
||||||
|
<div class="action-desc">指定要删除的去重参考值数量</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="warning" @click="deleteSpecifyDedupVisible = true">指定</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-card">
|
||||||
|
<div class="action-icon warning">
|
||||||
|
<el-icon size="28">
|
||||||
|
<Search/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action-info">
|
||||||
|
<div class="action-label">删除指定数量原始数据</div>
|
||||||
|
<div class="action-desc">指定要删除的原始数据数量</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="warning" @click="deleteSpecifyRawVisible = true">指定</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-card">
|
||||||
|
<div class="action-icon warning">
|
||||||
|
<el-icon size="28">
|
||||||
|
<Search/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action-info">
|
||||||
|
<div class="action-label">删除指定数量数据</div>
|
||||||
|
<div class="action-desc">同时删除去重参考值和原始数据</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="warning" @click="deleteSpecifyDataVisible = true">指定</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else-if="value" class="empty-state">
|
||||||
|
<el-icon size="64" color="#dcdfe6">
|
||||||
|
<InfoFilled/>
|
||||||
|
</el-icon>
|
||||||
|
<div class="empty-text">暂无数据</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未选择 -->
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<el-icon size="64" color="#dcdfe6">
|
||||||
|
<Key/>
|
||||||
|
</el-icon>
|
||||||
|
<div class="empty-text">请选择 Token 查看详情</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
<el-dialog v-model="deleteSpecifyDedupVisible" title="删除指定数量去重参考值" width="400">
|
<el-dialog v-model="deleteSpecifyDedupVisible" title="删除指定数量去重参考值" width="400">
|
||||||
<el-input v-model="inputSpecifyDedup" style="width: 200px" placeholder="请输入删除数量"/>
|
<el-input v-model="inputSpecifyDedup" placeholder="请输入删除数量" type="number"/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" @click="deleteSpecifyDedup">
|
<el-button @click="deleteSpecifyDedupVisible = false">取消</el-button>
|
||||||
确定
|
<el-button type="primary" @click="deleteSpecifyDedup">确定</el-button>
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="deleteSpecifyRawVisible" title="删除指定数量原始数据" width="400">
|
<el-dialog v-model="deleteSpecifyRawVisible" title="删除指定数量原始数据" width="400">
|
||||||
<el-input v-model="inputSpecifyRaw" style="width: 200px" placeholder="请输入删除数量"/>
|
<el-input v-model="inputSpecifyRaw" placeholder="请输入删除数量" type="number"/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" @click="deleteSpecifyRaw">
|
<el-button @click="deleteSpecifyRawVisible = false">取消</el-button>
|
||||||
确定
|
<el-button type="primary" @click="deleteSpecifyRaw">确定</el-button>
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<el-dialog v-model="deleteSpecifyDataVisible" title="删除指定数量的数据" width="400">
|
|
||||||
<el-input v-model="inputSpecifyData" style="width: 200px" placeholder="请输入删除数量"/>
|
<el-dialog v-model="deleteSpecifyDataVisible" title="删除指定数量数据" width="400">
|
||||||
|
<el-input v-model="inputSpecifyData" placeholder="请输入删除数量" type="number"/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" @click="deleteSpecifyData">
|
<el-button @click="deleteSpecifyDataVisible = false">取消</el-button>
|
||||||
确定
|
<el-button type="primary" @click="deleteSpecifyData">确定</el-button>
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.token-detail {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无权限 */
|
||||||
|
.no-permission {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-permission-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-permission-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-permission-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Token 选择 */
|
||||||
|
.token-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-time {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息卡片 */
|
||||||
|
.info-section {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 管理操作 */
|
||||||
|
.manage-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 24px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon.warning {
|
||||||
|
background: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 64px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.stat-cards {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.token-select {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+431
-215
@@ -1,304 +1,520 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref} from "vue";
|
import {ref, onMounted} from "vue"
|
||||||
import axios from "@/axios.ts";
|
import axios from "@/axios.ts"
|
||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
import {useCounterStore} from "@/stores/counter.ts";
|
import {useCounterStore} from "@/stores/counter.ts"
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router"
|
||||||
|
import {Plus, View, Edit, Delete, Key, Document, Memo, Search, Lock} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const tableData = ref([])
|
const store = useCounterStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
const input = ref('')
|
const input = ref('')
|
||||||
const value = ref('')
|
const value = ref('')
|
||||||
const dataFormat = ref('')
|
const dataFormat = ref('')
|
||||||
|
const inputNotes = ref('')
|
||||||
const inputPassWord = ref('')
|
const inputPassWord = ref('')
|
||||||
const router = useRouter()
|
const passwordVisible = ref(true)
|
||||||
var rowOut: any
|
|
||||||
|
const rowOut = ref<any>(null)
|
||||||
const dedupObjectVisible = ref(false)
|
const dedupObjectVisible = ref(false)
|
||||||
const dataFormatVisible = ref(false)
|
const dataFormatVisible = ref(false)
|
||||||
const inputNotes = ref("")
|
const inputNotesVisible = ref(false)
|
||||||
const NotesVisible = ref(false)
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{value: 'uid', label: 'uid'},
|
||||||
value: 'uid',
|
{value: 'secid', label: 'secid'},
|
||||||
},
|
{value: 'pid', label: 'pid'},
|
||||||
{
|
{value: 'comment_id', label: 'comment_id'},
|
||||||
value: 'secid',
|
{value: 'dyid', label: 'dyid'}
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'pid',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'comment_id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'dyid',
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const dataFormatOptions = [
|
const dataFormatOptions = [
|
||||||
{
|
{value: 'uid----secid----pid----comment_id', label: 'uid----secid----pid----comment_id'},
|
||||||
value: 'uid----secid----pid----comment_id',
|
{value: 'uid----secid', label: 'uid----secid'},
|
||||||
}, {
|
{value: 'dyid', label: 'dyid'}
|
||||||
value: 'uid----secid',
|
|
||||||
}, {
|
|
||||||
value: 'dyid',
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const fetchTokens = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await axios.get("/api/token")
|
||||||
|
tableData.value = res.data.result || []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
axios.get("/api/token").then(res => {
|
onMounted(() => {
|
||||||
tableData.value = res.data.result
|
if (!store.isAdmin) {
|
||||||
|
passwordVisible.value = true
|
||||||
|
} else {
|
||||||
|
fetchTokens()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const addToken = () => {
|
const checkPassword = () => {
|
||||||
axios.post('/api/token', {}, {
|
if (inputPassWord.value === "haha") {
|
||||||
|
ElMessage({message: '登录成功', type: 'success'})
|
||||||
|
store.isAdmin = true
|
||||||
|
passwordVisible.value = false
|
||||||
|
fetchTokens()
|
||||||
|
} else {
|
||||||
|
ElMessage.error('密码错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToken = async () => {
|
||||||
|
if (!input.value || !value.value || !dataFormat.value) {
|
||||||
|
ElMessage.warning('请填写完整信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await axios.post('/api/token', {}, {
|
||||||
params: {
|
params: {
|
||||||
token: input.value,
|
token: input.value,
|
||||||
dedup_object: value.value,
|
dedup_object: value.value,
|
||||||
data_format: dataFormat.value,
|
data_format: dataFormat.value,
|
||||||
notes: inputNotes.value,
|
notes: inputNotes.value,
|
||||||
}
|
}
|
||||||
}).then(response => {
|
|
||||||
if (response.data.result == "ok") {
|
|
||||||
ElMessage({
|
|
||||||
message: '添加成功',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
axios.get('/api/token').then(res => {
|
|
||||||
tableData.value = res.data.result
|
|
||||||
})
|
})
|
||||||
|
ElMessage({message: '添加成功', type: 'success'})
|
||||||
|
input.value = ''
|
||||||
|
value.value = ''
|
||||||
|
dataFormat.value = ''
|
||||||
|
inputNotes.value = ''
|
||||||
|
fetchTokens()
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.response?.data?.error || '添加失败')
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
|
||||||
ElMessage.error(error.response?.data?.error)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewDetails = (row: any) => {
|
const viewDetails = (row: any) => {
|
||||||
useCounterStore().token = row.token
|
store.token = row.token
|
||||||
router.push({
|
router.push({name: "TokenDetail"})
|
||||||
name: "TokenDetail",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogDedupObjectVisible = (row: any) => {
|
const dialogDedupObjectVisible = (row: any) => {
|
||||||
rowOut = row
|
rowOut.value = row
|
||||||
|
value.value = row.dedup_object
|
||||||
dedupObjectVisible.value = true
|
dedupObjectVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDedupObject = () => {
|
|
||||||
dedupObjectVisible.value = false
|
|
||||||
axios.put('/api/token', {}, {
|
|
||||||
params: {
|
|
||||||
token: rowOut.token,
|
|
||||||
dedup_object: value.value,
|
|
||||||
data_format: rowOut.data_format,
|
|
||||||
notes: rowOut.notes,
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.result == "ok") {
|
|
||||||
ElMessage({
|
|
||||||
message: '更改成功',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
axios.get('/api/token').then(res => {
|
|
||||||
tableData.value = res.data.result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
ElMessage.error(error.response?.data?.error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialogDataFormatVisible = (row: any) => {
|
const dialogDataFormatVisible = (row: any) => {
|
||||||
rowOut = row
|
rowOut.value = row
|
||||||
|
dataFormat.value = row.data_format
|
||||||
dataFormatVisible.value = true
|
dataFormatVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDataFormat = () => {
|
|
||||||
dataFormatVisible.value = false
|
|
||||||
axios.put('/api/token', {}, {
|
|
||||||
params: {
|
|
||||||
token: rowOut.token,
|
|
||||||
dedup_object: rowOut.dedup_object,
|
|
||||||
data_format: dataFormat.value,
|
|
||||||
notes: rowOut.notes,
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.result == "ok") {
|
|
||||||
ElMessage({
|
|
||||||
message: '更改成功',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
axios.get('/api/token').then(res => {
|
|
||||||
tableData.value = res.data.result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
ElMessage.error(error.response?.data?.error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialogNotesVisible = (row: any) => {
|
const dialogNotesVisible = (row: any) => {
|
||||||
rowOut = row
|
rowOut.value = row
|
||||||
NotesVisible.value = true
|
inputNotes.value = row.notes
|
||||||
|
inputNotesVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateNotes = () => {
|
const updateDedupObject = async () => {
|
||||||
NotesVisible.value = false
|
try {
|
||||||
axios.put('/api/token', {}, {
|
await axios.put('/api/token', {}, {
|
||||||
params: {
|
params: {
|
||||||
token: rowOut.token,
|
token: rowOut.value.token,
|
||||||
dedup_object: rowOut.dedup_object,
|
dedup_object: value.value,
|
||||||
data_format: rowOut.data_format,
|
data_format: rowOut.value.data_format,
|
||||||
|
notes: rowOut.value.notes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ElMessage({message: '更新成功', type: 'success'})
|
||||||
|
dedupObjectVisible.value = false
|
||||||
|
fetchTokens()
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.response?.data?.error || '更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDataFormat = async () => {
|
||||||
|
try {
|
||||||
|
await axios.put('/api/token', {}, {
|
||||||
|
params: {
|
||||||
|
token: rowOut.value.token,
|
||||||
|
dedup_object: rowOut.value.dedup_object,
|
||||||
|
data_format: dataFormat.value,
|
||||||
|
notes: rowOut.value.notes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ElMessage({message: '更新成功', type: 'success'})
|
||||||
|
dataFormatVisible.value = false
|
||||||
|
fetchTokens()
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.response?.data?.error || '更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateNotes = async () => {
|
||||||
|
try {
|
||||||
|
await axios.put('/api/token', {}, {
|
||||||
|
params: {
|
||||||
|
token: rowOut.value.token,
|
||||||
|
dedup_object: rowOut.value.dedup_object,
|
||||||
|
data_format: rowOut.value.data_format,
|
||||||
notes: inputNotes.value
|
notes: inputNotes.value
|
||||||
}
|
}
|
||||||
}).then(res => {
|
|
||||||
if (res.data.result == "ok") {
|
|
||||||
ElMessage({
|
|
||||||
message: '更改成功',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
axios.get('/api/token').then(res => {
|
|
||||||
tableData.value = res.data.result
|
|
||||||
})
|
})
|
||||||
|
ElMessage({message: '更新成功', type: 'success'})
|
||||||
|
inputNotesVisible.value = false
|
||||||
|
fetchTokens()
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.response?.data?.error || '更新失败')
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
|
||||||
ElMessage.error(error.response?.data?.error)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteToken = (row: any) => {
|
const deleteToken = async (row: any) => {
|
||||||
axios.delete('/api/token', {
|
try {
|
||||||
params: {
|
await axios.delete('/api/token', {
|
||||||
token: row.token
|
params: {token: row.token}
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.result == "ok") {
|
|
||||||
ElMessage({
|
|
||||||
message: '删除成功',
|
|
||||||
type: 'success',
|
|
||||||
})
|
})
|
||||||
axios.get('/api/token').then(res => {
|
ElMessage({message: '删除成功', type: 'success'})
|
||||||
tableData.value = res.data.result
|
fetchTokens()
|
||||||
})
|
} catch (error: any) {
|
||||||
}
|
ElMessage.error(error.response?.data?.error || '删除失败')
|
||||||
}).catch(error => {
|
|
||||||
ElMessage.error(error.response?.data?.error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkPassword = () => {
|
|
||||||
if (inputPassWord.value == "haha") {
|
|
||||||
ElMessage({
|
|
||||||
message: '密码正确',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
useCounterStore().isAdmin = true
|
|
||||||
} else {
|
|
||||||
ElMessage.error('密码错误')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!--非管理员-->
|
<div class="token-manage">
|
||||||
<div v-if="!useCounterStore().isAdmin" style="margin: auto auto; max-width: 400px">
|
<!-- 登录页面 -->
|
||||||
<el-alert title="您没有权限访问此页面,输入管理员密码" type="error" :closable="false" show-icon/>
|
<div v-if="!store.isAdmin" class="login-page">
|
||||||
<p></p>
|
<div class="login-card">
|
||||||
<el-input v-model="inputPassWord" style="width: 400px" placeholder="请输入管理员密码" @change="checkPassword"/>
|
<div class="login-icon">
|
||||||
<p></p>
|
<el-icon size="48" color="#409eff">
|
||||||
<el-button type="primary" @click="checkPassword">确认</el-button>
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="login-title">管理员登录</div>
|
||||||
|
<div class="login-subtitle">请输入管理员密码访问管理后台</div>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-model="inputPassWord"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入管理员密码"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="checkPassword"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<el-button type="primary" size="large" class="login-btn" @click="checkPassword">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--管理员-->
|
<!-- 管理页面 -->
|
||||||
<div v-if="useCounterStore().isAdmin">
|
<div v-else class="manage-page">
|
||||||
<!--添加Token-->
|
<div class="content-card">
|
||||||
<el-input v-model="input" style="width: 150px" placeholder="请输入Token名称"/>
|
<div class="page-title">Token 管理</div>
|
||||||
<el-select v-model="value" placeholder="选择去重对象" style="width: 150px">
|
|
||||||
<el-option
|
|
||||||
v-for="item in options"
|
|
||||||
:key="item.value"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 280px">
|
|
||||||
<el-option
|
|
||||||
v-for="item in dataFormatOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-input v-model="inputNotes" style="width: 200px" placeholder="请输入备注"/>
|
|
||||||
<el-button type="primary" @click="addToken">添加Token</el-button>
|
|
||||||
|
|
||||||
<!--Token列表-->
|
<!-- 添加表单 -->
|
||||||
<el-table :data="tableData" stripe style="width: 100%">
|
<div class="add-form">
|
||||||
<el-table-column prop="token" label="Token" width="150"/>
|
<div class="form-title">添加新 Token</div>
|
||||||
<el-table-column prop="dedup_object" label="去重对象" width="150"/>
|
<div class="form-grid">
|
||||||
<el-table-column prop="data_format" label="上传数据格式" width="280"/>
|
<el-input v-model="input" placeholder="Token 名称" clearable>
|
||||||
<el-table-column prop="notes" label="备注" width="200"/>
|
<template #prefix>
|
||||||
<el-table-column label="操作">
|
<el-icon>
|
||||||
<template #default="scope">
|
<Key/>
|
||||||
<el-button @click="viewDetails(scope.row)">查看详细</el-button>
|
</el-icon>
|
||||||
<el-button @click="dialogDedupObjectVisible(scope.row)" type="primary">更改去重对象</el-button>
|
</template>
|
||||||
<el-button @click="dialogDataFormatVisible(scope.row)" type="primary">更改数据格式</el-button>
|
</el-input>
|
||||||
<el-button @click="dialogNotesVisible(scope.row)" type="primary">更改备注</el-button>
|
|
||||||
|
<el-select v-model="value" placeholder="去重对象" clearable>
|
||||||
|
<el-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"/>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-select v-model="dataFormat" placeholder="数据格式" clearable>
|
||||||
|
<el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value" :label="item.label"/>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-input v-model="inputNotes" placeholder="备注(可选)" clearable>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Memo/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<el-button type="primary" @click="addToken">
|
||||||
|
<el-icon>
|
||||||
|
<Plus/>
|
||||||
|
</el-icon>
|
||||||
|
添加
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token 列表 -->
|
||||||
|
<div class="table-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">Token 列表</span>
|
||||||
|
<span class="table-count">共 {{ tableData.length }} 条</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="tableData" v-loading="loading" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="token" label="Token" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="token-cell">
|
||||||
|
<el-icon>
|
||||||
|
<Key/>
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ row.token }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="dedup_object" label="去重对象" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="primary" effect="plain">{{ row.dedup_object }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="data_format" label="数据格式" min-width="350" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="format-cell">{{ row.data_format }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="notes" label="备注" min-width="250" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="notes-text">{{ row.notes || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" size="small" text @click="viewDetails(row)">
|
||||||
|
<el-icon>
|
||||||
|
<View/>
|
||||||
|
</el-icon>
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" text @click="dialogDedupObjectVisible(row)">
|
||||||
|
<el-icon>
|
||||||
|
<Edit/>
|
||||||
|
</el-icon>
|
||||||
|
去重对象
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" text @click="dialogDataFormatVisible(row)">
|
||||||
|
<el-icon>
|
||||||
|
<Edit/>
|
||||||
|
</el-icon>
|
||||||
|
数据格式
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" text @click="dialogNotesVisible(row)">
|
||||||
|
<el-icon>
|
||||||
|
<Edit/>
|
||||||
|
</el-icon>
|
||||||
|
备注
|
||||||
|
</el-button>
|
||||||
<el-popconfirm
|
<el-popconfirm
|
||||||
width="180"
|
title="确认删除此 Token 吗?"
|
||||||
title="确认删除此Token吗"
|
|
||||||
confirm-button-text="确认"
|
confirm-button-text="确认"
|
||||||
cancel-button-text="取消"
|
cancel-button-text="取消"
|
||||||
@confirm="deleteToken(scope.row)"
|
@confirm="deleteToken(row)"
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button type="danger">删除此Token</el-button>
|
<el-button type="danger" size="small" text>
|
||||||
|
<el-icon>
|
||||||
|
<Delete/>
|
||||||
|
</el-icon>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-popconfirm>
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑对话框 -->
|
||||||
<el-dialog v-model="dedupObjectVisible" title="更改去重对象" width="400">
|
<el-dialog v-model="dedupObjectVisible" title="更改去重对象" width="400">
|
||||||
<el-select v-model="value" placeholder="选择去重对象" style="width: 200px">
|
<el-select v-model="value" placeholder="选择去重对象" style="width: 100%">
|
||||||
<el-option
|
<el-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"/>
|
||||||
v-for="item in options"
|
|
||||||
:key="item.value"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" @click="updateDedupObject">
|
<el-button @click="dedupObjectVisible = false">取消</el-button>
|
||||||
确定
|
<el-button type="primary" @click="updateDedupObject">确定</el-button>
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="dataFormatVisible" title="更改数据格式" width="400">
|
<el-dialog v-model="dataFormatVisible" title="更改数据格式" width="400">
|
||||||
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 280px">
|
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 100%">
|
||||||
<el-option
|
<el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value" :label="item.label"/>
|
||||||
v-for="item in dataFormatOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" @click="updateDataFormat">
|
<el-button @click="dataFormatVisible = false">取消</el-button>
|
||||||
确定
|
<el-button type="primary" @click="updateDataFormat">确定</el-button>
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="NotesVisible" title="更改备注" width="400">
|
<el-dialog v-model="inputNotesVisible" title="更改备注" width="400">
|
||||||
<el-input v-model="inputNotes" style="width: 200px" placeholder="请输入备注"/>
|
<el-input v-model="inputNotes" placeholder="请输入备注"/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="primary" @click="updateNotes">
|
<el-button @click="inputNotesVisible = false">取消</el-button>
|
||||||
确定
|
<el-button type="primary" @click="updateNotes">确定</el-button>
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.token-manage {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录页面 */
|
||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 40px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-icon {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card :deep(.el-input) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 管理页面 */
|
||||||
|
.add-form {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid .el-input {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid .el-select {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格 */
|
||||||
|
.table-section {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-count {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-cell {
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid .el-input,
|
||||||
|
.form-grid .el-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid .el-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user