refactor(admin): 重构管理员界面并优化用户体验并添加应用图标
部署开发环境 / deploy-dev (push) Failing after 14s

- 更新 AdminView.vue 组件结构,使用新的布局和导航设计
- 集成 Element Plus 图标组件,提升界面美观度
- 添加响应式设计支持,适配移动端设备
- 重构 HomeView.vue 组件,改进 Token 查询功能
- 实现自动刷新机制,每5秒更新 Token 信息
- 优化 TokenDetailView.vue 组件,增强数据管理功能
- 添加确认对话框,防止误删操作
- 在 App.vue 中引入全局 CSS 变量和主题系统
- 创建通用组件样式类,统一页面外观
- 优化数据加载逻辑,提升页面性能和用户体验
This commit is contained in:
2026-04-23 23:51:24 +08:00
parent 819a2eb8ec
commit 650c480416
7 changed files with 1574 additions and 505 deletions
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

+151 -5
View File
@@ -1,15 +1,161 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
const themeMode = ref('light')
</script> </script>
<template> <template>
<router-view></router-view> <div :class="themeMode" class="app-container">
<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;
} }
</style>
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>
+204 -30
View File
@@ -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>
<el-menu-item index="TokenManage">管理Token</el-menu-item> </div>
<el-menu-item index="TokenDetail">Token详细信息</el-menu-item> </div>
<el-menu-item v-if="useCounterStore().isAdmin">
<el-button type="danger" plain @click="useCounterStore().isAdmin=false">退出管理员</el-button>
</el-menu-item>
</el-menu>
</el-header> <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-icon>
<component :is="item.icon"/>
</el-icon>
<span>{{ item.label }}</span>
</div>
</nav>
<el-container> <div class="header-right">
<el-main> <div v-if="store.isAdmin" class="admin-badge">
<router-view></router-view> <el-icon>
</el-main> <User/>
</el-container> </el-icon>
</el-container> <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>
<main class="admin-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component"/>
</transition>
</router-view>
</main>
</div>
</template> </template>
<style scoped> <style scoped>
.admin-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-page);
}
</style> .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>
+258 -60
View File
@@ -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 updateTime = () => {
const now = new Date()
lastUpdate.value = now.toLocaleTimeString()
}
const fetchInfo = async () => {
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
token.value = input.value
updateTime()
// ElMessage({message: '刷新成功', type: 'success', duration: 1500})
} catch {
ElMessage({message: 'Token输入错误', type: 'error', duration: 2000})
} finally {
loading.value = false
}
}
const inputChange = () => { const inputChange = () => {
if (input.value != null && input.value != '') { if (input.value) fetchInfo()
axios.get('/api/token/info', {
params: {
token: input.value
}
}).then(res => {
result.value = res.data.result
token.value = input.value
ElMessage({
message: '更改成功',
type: 'success',
})
}).catch(error => {
ElMessage({
message: 'Token输入错误',
type: 'error',
})
})
}
} }
const getInfo = () => { let timer: number
if (token.value != null && token.value != '') { onMounted(() => {
axios.get('/api/token/info', { if (token.value) fetchInfo()
params: { timer = window.setInterval(fetchInfo, 5000)
token: token.value })
}
}).then(res => {
result.value = res.data.result
})
}
}
getInfo() onUnmounted(() => {
setInterval(getInfo, 5000) 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">
<el-input <div class="content-card" style="max-width: 1600px;">
v-model="input" <div class="page-header">
style="width: 240px" <div class="page-title">Token 信息查询</div>
placeholder="输入Token" <div class="header-subtitle">输入 Token 以查看去重和缓存信息</div>
clearable </div>
@change="inputChange"
/>
<el-divider/> <div class="search-box">
<b>Token信息每5秒刷新</b> <el-input
<el-button type="primary" plain @click="getInfo">手动刷新</el-button> v-model="input"
<el-descriptions placeholder="输入 Token"
direction="vertical" clearable
:column="4" size="large"
border class="token-input"
> @change="inputChange"
<el-descriptions-item label="去重对象">{{ result?.dedup_object }}</el-descriptions-item> @keyup.enter="fetchInfo"
<el-descriptions-item label="上传数据格式">{{ result?.data_format }}</el-descriptions-item> >
<el-descriptions-item label="去重记录值">{{ result?.dedup_items_number }}</el-descriptions-item> <template #prefix>
<el-descriptions-item label="Redis中数据条数">{{ result?.cache_list_number }}</el-descriptions-item> <el-icon>
</el-descriptions> <Search/>
</el-icon>
</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>
+492 -157
View File
@@ -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()
}
watch(value, (newValue) => { const deleteSpecifyRaw = async () => {
useCounterStore().token = value.value await axios.delete('/api/token/info', {
getInfo() 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()
}
axios.get('/api/token').then(res => { let timer: number
if (res.status == 200) { onMounted(() => {
res.data.result.forEach((item: any) => { fetchTokens()
options.value.push(item.token) if (store.token) {
}) value.value = store.token
} }
}) })
const deleteSpecifyData = () => { watch(value, (newValue) => {
axios.delete('/api/token/info', { store.token = newValue
params: { if (newValue) fetchInfo()
token: value.value, })
both_number: inputSpecifyData.value,
} onMounted(() => {
}).then(res => { if (value.value) fetchInfo()
getInfo() timer = window.setInterval(fetchInfo, 5000)
deleteSpecifyDataVisible.value = false })
})
} onUnmounted(() => {
const deleteSpecifyDedup = () => { clearInterval(timer)
axios.delete('/api/token/info', { })
params: {
token: value.value, const statCards = [
dedup_bf: inputSpecifyDedup.value, {label: '去重对象', key: 'dedup_object', icon: DataAnalysis, color: '#409eff'},
} {label: '上传数据格式', key: 'data_format', icon: Document, color: '#67c23a'},
}).then(res => { {label: '去重参考值数量', key: 'dedup_items_number', icon: Key, color: '#e6a23c'},
getInfo() {label: '原始数据数量', key: 'cache_list_number', icon: Warning, color: '#909399'},
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> <div v-if="!store.isAdmin" class="no-permission">
<div class="no-permission-card">
<el-icon size="64" color="#f56c6c">
<div v-if="useCounterStore().isAdmin"> <InfoFilled/>
<b>当前Token</b> </el-icon>
<el-select v-model="value" placeholder="选择Token" style="width: 240px"> <div class="no-permission-title">无权限访问</div>
<el-option <div class="no-permission-text">您没有权限访问此页面</div>
v-for="item in options" </div>
:key="item"
:value="item"
/>
</el-select>
<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="去重对象" 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>
</div> </div>
<!-- 管理页面 -->
<div v-else class="detail-page">
<div class="content-card">
<div class="page-title">Token 详情</div>
<!--弹窗输入--> <!-- 选择 Token -->
<el-dialog v-model="deleteSpecifyDedupVisible" title="删除指定数量去重参考值" width="400"> <div class="token-select">
<el-input v-model="inputSpecifyDedup" style="width: 200px" placeholder="请输入删除数量"/> <el-select v-model="value" placeholder="选择 Token" clearable style="width: 100%; max-width: 300px">
<template #footer> <el-option v-for="item in options" :key="item" :value="item" :label="item"/>
<el-button type="primary" @click="deleteSpecifyDedup"> </el-select>
确定 <el-button type="primary" @click="refresh" :loading="loading">
</el-button> <el-icon>
</template> <Refresh/>
</el-dialog> </el-icon>
<el-dialog v-model="deleteSpecifyRawVisible" title="删除指定数量原始数据" width="400"> 刷新
<el-input v-model="inputSpecifyRaw" style="width: 200px" placeholder="请输入删除数量"/> </el-button>
<template #footer> <span v-if="lastUpdate" class="update-time">上次更新: {{ lastUpdate }}</span>
<el-button type="primary" @click="deleteSpecifyRaw"> </div>
确定
</el-button> <!-- Token 信息 -->
</template> <div v-if="result && value" class="info-section">
</el-dialog> <div class="section-title">Token 信息</div>
<el-dialog v-model="deleteSpecifyDataVisible" title="删除指定数量的数据" width="400">
<el-input v-model="inputSpecifyData" style="width: 200px" placeholder="请输入删除数量"/> <div class="stat-cards">
<template #footer> <div v-for="card in statCards" :key="card.key" class="stat-card">
<el-button type="primary" @click="deleteSpecifyData"> <div class="stat-icon" :style="{ background: card.color + '20', color: card.color }">
确定 <el-icon size="24">
</el-button> <component :is="card.icon"/>
</template> </el-icon>
</el-dialog> </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-input v-model="inputSpecifyDedup" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyDedupVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyDedup">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="deleteSpecifyRawVisible" title="删除指定数量原始数据" width="400">
<el-input v-model="inputSpecifyRaw" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyRawVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyRaw">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="deleteSpecifyDataVisible" title="删除指定数量数据" width="400">
<el-input v-model="inputSpecifyData" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyDataVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyData">确定</el-button>
</template>
</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>
+469 -253
View File
@@ -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") {
params: { ElMessage({message: '登录成功', type: 'success'})
token: input.value, store.isAdmin = true
dedup_object: value.value, passwordVisible.value = false
data_format: dataFormat.value, fetchTokens()
notes: inputNotes.value, } else {
} ElMessage.error('密码错误')
}).then(response => { }
if (response.data.result == "ok") { }
ElMessage({
message: '添加成功', const addToken = async () => {
type: 'success', if (!input.value || !value.value || !dataFormat.value) {
}) ElMessage.warning('请填写完整信息')
axios.get('/api/token').then(res => { return
tableData.value = res.data.result }
}) try {
} await axios.post('/api/token', {}, {
}).catch(error => { params: {
ElMessage.error(error.response?.data?.error) token: input.value,
}) dedup_object: value.value,
data_format: dataFormat.value,
notes: inputNotes.value,
}
})
ElMessage({message: '添加成功', type: 'success'})
input.value = ''
value.value = ''
dataFormat.value = ''
inputNotes.value = ''
fetchTokens()
} catch (error: any) {
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: inputNotes.value notes: rowOut.value.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 deleteToken = (row: any) => {
axios.delete('/api/token', {
params: {
token: row.token
}
}).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 checkPassword = () => {
if (inputPassWord.value == "haha") {
ElMessage({
message: '密码正确',
type: 'success',
}) })
useCounterStore().isAdmin = true ElMessage({message: '更新成功', type: 'success'})
} else { dedupObjectVisible.value = false
ElMessage.error('密码错误') 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
}
})
ElMessage({message: '更新成功', type: 'success'})
inputNotesVisible.value = false
fetchTokens()
} catch (error: any) {
ElMessage.error(error.response?.data?.error || '更新失败')
}
}
const deleteToken = async (row: any) => {
try {
await axios.delete('/api/token', {
params: {token: row.token}
})
ElMessage({message: '删除成功', type: 'success'})
fetchTokens()
} catch (error: any) {
ElMessage.error(error.response?.data?.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/>
</div> </el-icon>
</div>
<div class="login-title">管理员登录</div>
<div class="login-subtitle">请输入管理员密码访问管理后台</div>
<!--管理员--> <el-input
<div v-if="useCounterStore().isAdmin"> v-model="inputPassWord"
<!--添加Token--> type="password"
<el-input v-model="input" style="width: 150px" placeholder="请输入Token名称"/> placeholder="请输入管理员密码"
<el-select v-model="value" placeholder="选择去重对象" style="width: 150px"> size="large"
<el-option show-password
v-for="item in options" @keyup.enter="checkPassword"
:key="item.value" >
:value="item.value" <template #prefix>
/> <el-icon>
</el-select> <Lock/>
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 280px"> </el-icon>
<el-option </template>
v-for="item in dataFormatOptions" </el-input>
: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-button type="primary" size="large" class="login-btn" @click="checkPassword">
<el-table :data="tableData" stripe style="width: 100%"> 登录
<el-table-column prop="token" label="Token" width="150"/> </el-button>
<el-table-column prop="dedup_object" label="去重对象" width="150"/> </div>
<el-table-column prop="data_format" label="上传数据格式" width="280"/> </div>
<el-table-column prop="notes" label="备注" width="200"/>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="viewDetails(scope.row)">查看详细</el-button>
<el-button @click="dialogDedupObjectVisible(scope.row)" type="primary">更改去重对象</el-button>
<el-button @click="dialogDataFormatVisible(scope.row)" type="primary">更改数据格式</el-button>
<el-button @click="dialogNotesVisible(scope.row)" type="primary">更改备注</el-button>
<el-popconfirm
width="180"
title="确认删除此Token吗"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="deleteToken(scope.row)"
>
<template #reference>
<el-button type="danger">删除此Token</el-button>
</template>
</el-popconfirm>
<!-- 管理页面 -->
<div v-else class="manage-page">
<div class="content-card">
<div class="page-title">Token 管理</div>
<!-- 添加表单 -->
<div class="add-form">
<div class="form-title">添加新 Token</div>
<div class="form-grid">
<el-input v-model="input" placeholder="Token 名称" clearable>
<template #prefix>
<el-icon>
<Key/>
</el-icon>
</template>
</el-input>
<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
title="确认删除此 Token 吗?"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="deleteToken(row)"
>
<template #reference>
<el-button type="danger" size="small" text>
<el-icon>
<Delete/>
</el-icon>
删除
</el-button>
</template>
</el-popconfirm>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dedupObjectVisible" title="更改去重对象" width="400">
<el-select v-model="value" placeholder="选择去重对象" style="width: 100%">
<el-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<template #footer>
<el-button @click="dedupObjectVisible = false">取消</el-button>
<el-button type="primary" @click="updateDedupObject">确定</el-button>
</template> </template>
</el-table-column> </el-dialog>
</el-table>
<el-dialog v-model="dedupObjectVisible" title="更改去重对象" width="400"> <el-dialog v-model="dataFormatVisible" title="更改数据格式" width="400">
<el-select v-model="value" placeholder="选择去重对象" style="width: 200px"> <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 options" </el-select>
:key="item.value" <template #footer>
:value="item.value" <el-button @click="dataFormatVisible = false">取消</el-button>
/> <el-button type="primary" @click="updateDataFormat">确定</el-button>
</el-select> </template>
<template #footer> </el-dialog>
<el-button type="primary" @click="updateDedupObject">
确定
</el-button>
</template>
</el-dialog>
<el-dialog v-model="dataFormatVisible" title="更改数据格式" width="400"> <el-dialog v-model="inputNotesVisible" title="更改备注" width="400">
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 280px"> <el-input v-model="inputNotes" placeholder="请输入备注"/>
<el-option <template #footer>
v-for="item in dataFormatOptions" <el-button @click="inputNotesVisible = false">取消</el-button>
:key="item.value" <el-button type="primary" @click="updateNotes">确定</el-button>
:value="item.value" </template>
/> </el-dialog>
</el-select> </div>
<template #footer>
<el-button type="primary" @click="updateDataFormat">
确定
</el-button>
</template>
</el-dialog>
<el-dialog v-model="NotesVisible" title="更改备注" width="400">
<el-input v-model="inputNotes" style="width: 200px" placeholder="请输入备注"/>
<template #footer>
<el-button type="primary" @click="updateNotes">
确定
</el-button>
</template>
</el-dialog>
</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>