feat(admin): 实现管理员登录认证和页面权限控制

- 添加管理员密码验证登录功能
- 实现登录状态管理和用户界面显示
- 集成权限检查确保页面访问安全
- 添加登录/登出流程处理
- 重构AdminView组件结构和样式
- 集成Element Plus图标和UI组件
- 添加设置页面路由配置
- 优化Token管理页面折叠表单设计
- 移除旧的响应式布局相关代码
- 更新应用标题动态渲染逻辑
This commit is contained in:
2026-06-19 17:36:50 +08:00
parent 4100a51eb8
commit fc640407d9
10 changed files with 246 additions and 308 deletions
+3
View File
@@ -13,7 +13,10 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AddDataDialog: typeof import('./src/components/AddDataDialog.vue')['default'] AddDataDialog: typeof import('./src/components/AddDataDialog.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
-12
View File
@@ -57,16 +57,4 @@ html, body {
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
margin: 20px 0 16px; margin: 20px 0 16px;
} }
/* 响应式 */
@media (max-width: 768px) {
.content-card {
padding: 16px;
border-radius: 0;
}
.hide-on-mobile {
display: none;
}
}
</style> </style>
+16 -8
View File
@@ -1,21 +1,29 @@
import {createApp} from 'vue' import {createApp} from 'vue'
import {createPinia} from 'pinia' import {createPinia} from 'pinia'
import persistedState from 'pinia-plugin-persistedstate'; import persistedState from 'pinia-plugin-persistedstate';
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import './assets/main.css' import './assets/main.css'
const pinia = createPinia()
pinia.use(persistedState)
const app = createApp(App) const app = createApp(App)
app.use(pinia)
if (import.meta.env.DEV) {
document.title = '抖音数据去重 - dev'
}
app.use(createPinia().use(persistedState))
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)
app.mount('#app') app.mount('#app')
router.beforeEach((to, from) => {
var postfix: string = ''
if (import.meta.env.DEV) {
postfix = '-dev'
}
if (to.meta.title) {
document.title = to.meta.title + postfix
}
})
+8
View File
@@ -4,17 +4,20 @@ import TokenManageView from '@/views/admin/TokenManageView.vue'
import TokenDetailView from '@/views/admin/TokenDetailView.vue' import TokenDetailView from '@/views/admin/TokenDetailView.vue'
import AdminView from '@/views/AdminView.vue' import AdminView from '@/views/AdminView.vue'
import HomeView from '@/views/HomeView.vue' import HomeView from '@/views/HomeView.vue'
import SettingsView from '@/views/admin/SettingsView.vue'
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: "Home", name: "Home",
component: HomeView, component: HomeView,
meta: {title: "抖音数据去重",}
}, },
{ {
path: '/haha', path: '/haha',
name: "Admin", name: "Admin",
component: AdminView, component: AdminView,
meta: {title: "抖音数据去重 | 管理后台",},
children: [ children: [
{ {
path: '', path: '',
@@ -26,6 +29,11 @@ const routes = [
name: "TokenDetail", name: "TokenDetail",
component: TokenDetailView component: TokenDetailView
}, },
{
path: "settings",
name: "AdminSettings",
component: SettingsView,
}
], ],
}, },
-4
View File
@@ -2,13 +2,9 @@ import {ref} from 'vue'
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
export const useCounterStore = defineStore('counter', () => { export const useCounterStore = defineStore('counter', () => {
const homeToken = ref("")
const token = ref("") const token = ref("")
const isAdmin = ref(false) const isAdmin = ref(false)
return {token, isAdmin} return {token, isAdmin}
}, { }, {
persist: true persist: true
+133 -24
View File
@@ -1,28 +1,49 @@
<script setup lang="ts"> <script setup lang="ts">
import {useCounterStore} from "@/stores/counter.ts" import {useCounterStore} from "@/stores/counter.ts"
import {CloseBold, Document, Edit, User} from '@element-plus/icons-vue' import {CloseBold, Document, Edit, Lock, Setting, User} from '@element-plus/icons-vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const store = useCounterStore() const store = useCounterStore()
const activeIndex = ref(route.name?.toString() || "TokenManage") const activeIndex = ref(route.name?.toString() || "TokenManage")
const _userName = ref("未登录")
const _isAdmin = ref(store.isAdmin)
const inputPassWord = ref('')
if (_isAdmin.value) {
_userName.value = "管理员"
} else {
_userName.value = "未登录"
}
watch(_isAdmin, (newValue) => {
store.isAdmin = newValue
if (newValue) {
_userName.value = "管理员"
} else {
_userName.value = "未登录"
}
})
watch(route, (newRoute) => { watch(route, (newRoute) => {
activeIndex.value = newRoute.name?.toString() || "TokenManage" activeIndex.value = newRoute.name?.toString() || "TokenManage"
}) })
const handleSelect = (key: string) => { const handleSelect = (name: string) => {
router.push({name: key}) router.push({name: name})
} }
const menuItems = [
{key: 'TokenManage', label: '管理 Token', icon: Edit},
{key: 'TokenDetail', label: 'Token 详情', icon: Document},
]
const logout = () => { const logout = () => {
store.isAdmin = false _isAdmin.value = false
}
const checkPassword = () => {
if (inputPassWord.value === "haha") {
ElMessage({message: '登录成功', type: 'success'})
_isAdmin.value = true
} else {
ElMessage.error('密码错误')
}
} }
</script> </script>
@@ -30,27 +51,42 @@ const logout = () => {
<div class="admin-layout"> <div class="admin-layout">
<el-header class="admin-header"> <el-header class="admin-header">
<div class="header-left"> <div class="header-left">
<div class="logo"> <div class="logo">DYPID 管理后台</div>
DYPID 管理后台
</div>
</div> </div>
<nav class="header-nav"> <nav class="header-nav">
<div v-for="item in menuItems" class="nav-item" <div class="nav-item"
:key="item.key" :class="{active:activeIndex=='TokenManage'}"
:class="{ active: activeIndex === item.key }" @click="handleSelect('TokenManage')"
@click="handleSelect(item.key)"
> >
<el-icon> <el-icon>
<component :is="item.icon"/> <Edit/>
</el-icon> </el-icon>
<span>{{ item.label }}</span> <span>管理 Token</span>
</div>
<div class="nav-item"
:class="{active:activeIndex=='TokenDetail'}"
@click="handleSelect('TokenDetail')"
>
<el-icon>
<Document/>
</el-icon>
<span>Token 详情</span>
</div> </div>
</nav> </nav>
<div class="header-right"> <div class="header-right">
<el-dropdown v-if="store.isAdmin"> <el-button v-if="_isAdmin" circle link
<el-button :icon="User">管理员</el-button> @click="handleSelect('AdminSettings')"
>
<el-icon size="16">
<Setting/>
</el-icon>
</el-button>
<el-divider direction="vertical"/>
<el-dropdown :disabled="!_isAdmin">
<el-button :icon="User" link>{{ _userName }}</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
@@ -69,10 +105,40 @@ const logout = () => {
</el-header> </el-header>
<el-main> <el-main>
<router-view v-slot="{ Component }"> <!-- 登录页面 -->
<transition name="fade" mode="out-in"> <div v-if="!_isAdmin" class="login-page">
<div class="login-card">
<div class="login-icon">
<el-icon size="48" color="#409eff">
<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>
<router-view v-else v-slot="{ Component }">
<component :is="Component"/> <component :is="Component"/>
</transition>
</router-view> </router-view>
</el-main> </el-main>
</div> </div>
@@ -141,7 +207,50 @@ const logout = () => {
.header-right { .header-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; }
/* 登录页面 */
.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(--el-text-color-primary);
margin-bottom: 8px;
}
.login-subtitle {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-bottom: 32px;
}
.login-card :deep(.el-input) {
margin-bottom: 16px;
}
.login-btn {
width: 100%;
margin-top: 8px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
+34 -96
View File
@@ -66,34 +66,43 @@ const statCards = [
</script> </script>
<template> <template>
<div class="home-page"> <div class="p-6 h-dvh">
<div class="content-card" style="max-width: 1600px"> <div class="content-card h-full">
<div class="page-header"> <div class="text-center mb-8">
<div class="page-title">Token 信息查询</div> <div class="page-title">Token 信息查询</div>
<div class="header-subtitle">输入 Token 以查看去重和缓存信息</div> <div class="header-subtitle">输入 Token 以查看去重和缓存信息</div>
</div> </div>
<div class="search-box"> <div class="flex justify-center mb-8">
<el-input placeholder="输入 Token" size="large" clearable class="token-input" <el-input placeholder="输入要查询数据的Token..." size="large" clearable
v-model="inputToken" @change="inputChange"/> class="max-w-120"
v-model="inputToken" @change="inputChange"
>
<template #prepend>
Token
</template>
</el-input>
<el-button type="primary" size="large" <el-button type="primary" size="large"
@click="refresh" :loading="loading" :icon="Search"> class="ml-3"
@click="refresh" :loading="loading" :icon="Search"
>
查询 查询
</el-button> </el-button>
<el-button type="success" size="large" class=""
@click="showAddDataDialog = true" :icon="Plus">
增加数据
</el-button>
</div> </div>
<AddDataDialog v-model="showAddDataDialog" :token="inputToken"/> <AddDataDialog v-model="showAddDataDialog" :token="inputToken"/>
<div v-if="result" class="result-section"> <div v-if="result" class="result-section">
<div class="result-header"> <el-button type="primary" size=""
class=""
@click="showAddDataDialog = true" :icon="Plus"
>
增加数据
</el-button>
<div class="flex ">
<span class="section-title">Token 信息</span> <span class="section-title">Token 信息</span>
<span class="auto-refresh">每5秒自动刷新</span>
</div> </div>
<div class="stat-cards"> <div class="stat-cards">
@@ -103,55 +112,38 @@ const statCards = [
<component :is="card.icon"/> <component :is="card.icon"/>
</el-icon> </el-icon>
</div> </div>
<div class="stat-content"> <div class="flex-1">
<div class="stat-label">{{ card.label }}</div> <div class="stat-label">{{ card.label }}</div>
<div class="stat-value">{{ result[card.key] || '-' }}</div> <div class="stat-value">{{ result[card.key] || '-' }}</div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="lastUpdate" class="update-time"> <el-text v-if="lastUpdate" type="info"
最后更新: {{ lastUpdate }} class="block w-full mt-5 text-center"
</div> >
每5秒更新一次数据 最后更新: {{ lastUpdate }}
</el-text>
</div> </div>
<div v-else class="empty-state"> <el-space v-else direction="vertical"
class="flex p-16"
>
<el-icon size="64" color="#dcdfe6"> <el-icon size="64" color="#dcdfe6">
<Search/> <Search/>
</el-icon> </el-icon>
<div class="empty-text">请输入 Token 进行查询</div> <el-text type="info" size="large" class="">请输入 Token 进行查询</el-text>
</div> </el-space>
</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 { .header-subtitle {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
margin-top: 8px; margin-top: 8px;
} }
.search-box {
display: flex;
gap: 12px;
margin-bottom: 32px;
}
.token-input {
max-width: 400px;
}
.result-section { .result-section {
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
} }
@@ -174,11 +166,6 @@ const statCards = [
margin-bottom: 20px; margin-bottom: 20px;
} }
.auto-refresh {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.stat-cards { .stat-cards {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@@ -209,11 +196,6 @@ const statCards = [
justify-content: center; justify-content: center;
} }
.stat-content {
flex: 1;
min-width: 0;
}
.stat-label { .stat-label {
font-size: 13px; font-size: 13px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
@@ -228,48 +210,4 @@ const statCards = [
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.update-time {
text-align: center;
margin-top: 20px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 64px;
color: var(--el-text-color-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>
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
TODO
</template>
<style scoped>
</style>
+1 -36
View File
@@ -143,19 +143,8 @@ const statCards = [
<template> <template>
<div class="token-detail"> <div class="token-detail">
<!-- 检查权限 -->
<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 v-else class="detail-page"> <div class="detail-page">
<div class="content-card"> <div class="content-card">
<div class="page-title">Token 详情</div> <div class="page-title">Token 详情</div>
@@ -321,30 +310,6 @@ const statCards = [
padding: 0; 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(--el-text-color-secondary);
}
/* Token 选择 */ /* Token 选择 */
.token-select { .token-select {
display: flex; display: flex;
+10 -98
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import api from "@/api" import api from "@/api"
import {useCounterStore} from "@/stores/counter.ts" import {useCounterStore} from "@/stores/counter.ts"
import {Delete, Edit, Key, Lock, Memo, Plus, View} from '@element-plus/icons-vue' import {Delete, Edit, Key, Memo, Plus, View} from '@element-plus/icons-vue'
const store = useCounterStore() const store = useCounterStore()
const router = useRouter() const router = useRouter()
@@ -12,8 +12,7 @@ const input = ref('')
const value = ref('') const value = ref('')
const dataFormat = ref('') const dataFormat = ref('')
const inputNotes = ref('') const inputNotes = ref('')
const inputPassWord = ref('') const activeNames = ref(['1'])
const passwordVisible = ref(true)
const rowOut = ref<any>(null) const rowOut = ref<any>(null)
const addDataDialogVisible = ref(false) const addDataDialogVisible = ref(false)
@@ -46,24 +45,11 @@ const fetchTokens = async () => {
} }
onMounted(() => { onMounted(() => {
if (!store.isAdmin) { if (store.isAdmin) {
passwordVisible.value = true
} else {
fetchTokens() fetchTokens()
} }
}) })
const checkPassword = () => {
if (inputPassWord.value === "haha") {
ElMessage({message: '登录成功', type: 'success'})
store.isAdmin = true
passwordVisible.value = false
fetchTokens()
} else {
ElMessage.error('密码错误')
}
}
const addToken = async () => { const addToken = async () => {
if (!input.value || !value.value || !dataFormat.value) { if (!input.value || !value.value || !dataFormat.value) {
ElMessage.warning('请填写完整信息') ElMessage.warning('请填写完整信息')
@@ -176,46 +162,14 @@ const deleteToken = async (row: any) => {
<template> <template>
<div class="token-manage"> <div class="token-manage">
<!-- 登录页面 -->
<div v-if="!store.isAdmin" class="login-page">
<div class="login-card">
<div class="login-icon">
<el-icon size="48" color="#409eff">
<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 v-else class="manage-page"> <div class="manage-page">
<div class="content-card"> <div class="content-card">
<div class="page-title">Token 管理</div> <div class="page-title">Token 管理</div>
<!-- 添加表单 --> <!-- 添加表单 -->
<div class="add-form"> <el-collapse class="add-form" v-model="activeNames">
<div class="form-title">添加新 Token</div> <el-collapse-item title="添加新 Token" name="1">
<div class="form-grid"> <div class="form-grid">
<el-input v-model="input" placeholder="Token 名称" clearable> <el-input v-model="input" placeholder="Token 名称" clearable>
<template #prefix> <template #prefix>
@@ -230,7 +184,8 @@ const deleteToken = async (row: any) => {
</el-select> </el-select>
<el-select v-model="dataFormat" placeholder="数据格式" clearable> <el-select v-model="dataFormat" placeholder="数据格式" clearable>
<el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value" :label="item.label"/> <el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value"
:label="item.label"/>
</el-select> </el-select>
<el-input v-model="inputNotes" placeholder="备注(可选)" clearable> <el-input v-model="inputNotes" placeholder="备注(可选)" clearable>
@@ -248,7 +203,8 @@ const deleteToken = async (row: any) => {
添加 添加
</el-button> </el-button>
</div> </div>
</div> </el-collapse-item>
</el-collapse>
<!-- Token 列表 --> <!-- Token 列表 -->
<div class="table-section"> <div class="table-section">
@@ -387,50 +343,6 @@ const deleteToken = async (row: any) => {
padding: 0; 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(--el-text-color-primary);
margin-bottom: 8px;
}
.login-subtitle {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-bottom: 32px;
}
.login-card :deep(.el-input) {
margin-bottom: 16px;
}
.login-btn {
width: 100%;
margin-top: 8px;
}
/* 管理页面 */ /* 管理页面 */
.add-form { .add-form {
margin-bottom: 32px; margin-bottom: 32px;