110 lines
4.8 KiB
Vue
110 lines
4.8 KiB
Vue
<template>
|
|
<div v-if="!token" class="login-wrap">
|
|
<div class="login-card">
|
|
<div class="mark">✦ AI维度 · 管理后台</div>
|
|
<el-input v-model="username" placeholder="管理员用户名" @keyup.enter="login" />
|
|
<el-input v-model="password" type="password" placeholder="密码" show-password @keyup.enter="login" />
|
|
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
|
<el-button type="primary" :loading="busy" style="width:100%" @click="login">登 录</el-button>
|
|
</div>
|
|
</div>
|
|
<div v-else class="layout">
|
|
<aside class="side">
|
|
<div class="side-title">✦ AI维度</div>
|
|
<div v-for="m in menus" :key="m.key" class="menu-item" :class="{ active: view === m.key }" @click="view = m.key">
|
|
<span>{{ m.icon }}</span>{{ m.label }}
|
|
</div>
|
|
<div class="side-foot"><el-button size="small" @click="logout">退出</el-button></div>
|
|
</aside>
|
|
<main class="content"><component :is="menus.find(m => m.key === view).comp" /></main>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, ref } from 'vue'
|
|
import { getToken, setToken, clearToken, apiPost } from './api'
|
|
import Dashboard from './views/Dashboard.vue'
|
|
import Users from './views/Users.vue'
|
|
import Knowledge from './views/Knowledge.vue'
|
|
import Trends from './views/Trends.vue'
|
|
import Quotas from './views/Quotas.vue'
|
|
import Feedbacks from './views/Feedbacks.vue'
|
|
import Logs from './views/Logs.vue'
|
|
import VpayOrders from './views/VpayOrders.vue'
|
|
|
|
const token = ref(getToken())
|
|
const view = ref('dashboard')
|
|
const username = ref(''), password = ref(''), busy = ref(false), error = ref('')
|
|
|
|
const menus = [
|
|
{ key: 'dashboard', label: '数据看板', icon: '📊', comp: Dashboard },
|
|
{ key: 'users', label: '用户管理', icon: '👥', comp: Users },
|
|
{ key: 'knowledge', label: '知识管理', icon: '📚', comp: Knowledge },
|
|
{ key: 'trends', label: '趋势管理', icon: '📈', comp: Trends },
|
|
{ key: 'quotas', label: '问答额度', icon: '💬', comp: Quotas },
|
|
{ key: 'feedbacks', label: '反馈管理', icon: '📝', comp: Feedbacks },
|
|
{ key: 'vpay', label: '订单/余额', icon: '💰', comp: VpayOrders },
|
|
{ key: 'logs', label: '访问日志', icon: '🗂️', comp: Logs },
|
|
]
|
|
|
|
async function login() {
|
|
busy.value = true; error.value = ''
|
|
try {
|
|
const r = await apiPost('/api/auth/admin/login', { username: username.value, password: password.value })
|
|
setToken(r.data?.token || r.token); token.value = r.data?.token || r.token
|
|
} catch (e) { error.value = e.message }
|
|
finally { busy.value = false }
|
|
}
|
|
function logout() { clearToken(); token.value = '' }
|
|
</script>
|
|
|
|
<style>
|
|
body { margin: 0; font-family: -apple-system, 'PingFang SC', sans-serif; background: #f5f6fa; }
|
|
.login-wrap { min-height: 100vh; display: grid; place-items: center; }
|
|
.login-card { width: 340px; background: #fff; border-radius: 16px; padding: 32px 28px; display: flex; flex-direction: column; gap: 14px; box-shadow: 0 8px 30px rgba(0,0,0,.1); }
|
|
.mark { text-align: center; font-size: 18px; font-weight: 700; color: #1f2333; margin-bottom: 6px; }
|
|
.hint { margin: 0; font-size: 11px; color: #b6bac9; text-align: center; }
|
|
.layout { display: grid; grid-template-columns: 200px 1fr; min-height: 100vh; }
|
|
.side { background: #1f2333; color: #cfd3e4; display: flex; flex-direction: column; padding: 16px 10px; position: sticky; top: 0; height: 100vh; }
|
|
.side-title { font-weight: 700; color: #fff; padding: 6px 12px 18px; font-size: 16px; }
|
|
.menu-item { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-radius: 8px; cursor: pointer; font-size: 14px; margin-bottom: 2px; }
|
|
.menu-item:hover { background: rgba(255,255,255,.07); }
|
|
.menu-item.active { background: #6366f1; color: #fff; }
|
|
.side-foot { margin-top: auto; padding: 10px; }
|
|
.content { padding: 20px 24px; }
|
|
@media (max-width: 768px) {
|
|
.layout { grid-template-columns: 1fr !important; grid-template-rows: auto 1fr; }
|
|
.side {
|
|
position: sticky !important; top: 0; z-index: 30;
|
|
height: 52px !important; max-height: 52px !important;
|
|
flex-direction: row !important; align-items: center;
|
|
overflow-x: auto; overflow-y: hidden;
|
|
padding: 0 10px; gap: 6px;
|
|
-webkit-overflow-scrolling: touch;
|
|
white-space: nowrap;
|
|
}
|
|
.side-title {
|
|
font-size: 14px; padding: 0 8px;
|
|
white-space: nowrap; flex-shrink: 0;
|
|
}
|
|
.menu-item {
|
|
height: 36px; padding: 0 14px;
|
|
font-size: 13px; margin-bottom: 0;
|
|
white-space: nowrap; flex-shrink: 0;
|
|
border-radius: 999px;
|
|
}
|
|
.side-foot {
|
|
margin: 0 0 0 auto; padding: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.content { padding: 12px; min-width: 0; overflow-x: hidden; }
|
|
}
|
|
|
|
</style>
|
|
<style>
|
|
/* 全局:管理后台所有列表在窄屏给足表格最小宽,左右滑动查看 */
|
|
.el-table { width: 100%; }
|
|
.el-table__inner-wrapper { overflow-x: auto; }
|
|
.page .el-table table { min-width: 640px; }
|
|
</style>
|