273 lines
10 KiB
Vue
273 lines
10 KiB
Vue
|
|
<template>
|
|||
|
|
<main class="page">
|
|||
|
|
<section v-if="!isAuthed" class="hero hero-admin">
|
|||
|
|
<p class="eyebrow">订单管理系统 · 管理后台</p>
|
|||
|
|
<h1>后台登录</h1>
|
|||
|
|
<p class="desc">登录后可维护用户、角色、菜单、客户、产品、工厂、配置与审计日志。</p>
|
|||
|
|
<div class="form-grid single">
|
|||
|
|
<input v-model="username" placeholder="账号(admin / sales)" />
|
|||
|
|
<input v-model="password" type="password" placeholder="密码(123456)" />
|
|||
|
|
</div>
|
|||
|
|
<button class="primary" @click="handleLogin">登录</button>
|
|||
|
|
<p v-if="errorMessage" class="error-text">{{ errorMessage }}</p>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-else>
|
|||
|
|
<header class="hero hero-admin">
|
|||
|
|
<div class="toolbar">
|
|||
|
|
<div>
|
|||
|
|
<p class="eyebrow">管理工作台</p>
|
|||
|
|
<h1>欢迎你,{{ user.real_name || user.username }}</h1>
|
|||
|
|
<p class="desc">角色:{{ user.role_name }}</p>
|
|||
|
|
</div>
|
|||
|
|
<button @click="logout">退出登录</button>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<section class="grid nav-grid">
|
|||
|
|
<article v-for="card in cards" :key="card.key" class="card" @click="view = card.key">
|
|||
|
|
<h2>{{ card.title }}</h2>
|
|||
|
|
<p>{{ card.desc }}</p>
|
|||
|
|
</article>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'users'" class="panel-block">
|
|||
|
|
<h2>用户管理</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in users" :key="item.id" class="list-item">
|
|||
|
|
<strong>{{ item.real_name }}</strong>
|
|||
|
|
<span>{{ item.username }}</span>
|
|||
|
|
<span>{{ item.role_name }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'roles'" class="panel-block">
|
|||
|
|
<h2>角色管理</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in roles" :key="item.id" class="list-item">
|
|||
|
|
<strong>{{ item.role_name }}</strong>
|
|||
|
|
<span>{{ item.role_code }}</span>
|
|||
|
|
<span>{{ item.status === 1 ? '启用' : '停用' }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'menus'" class="panel-block">
|
|||
|
|
<h2>菜单管理</h2>
|
|||
|
|
<pre class="json-box">{{ JSON.stringify(menuTree, null, 2) }}</pre>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'customers'" class="panel-block">
|
|||
|
|
<h2>客户管理</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in customers" :key="item.customer_id" class="list-item">
|
|||
|
|
<strong>{{ item.customer_name }}</strong>
|
|||
|
|
<span>{{ item.mobile }}</span>
|
|||
|
|
<span>{{ item.settlement_type }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'products'" class="panel-block">
|
|||
|
|
<h2>产品管理</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in products" :key="item.product_id" class="list-item">
|
|||
|
|
<strong>{{ item.product_name }}</strong>
|
|||
|
|
<span>{{ item.specification }}</span>
|
|||
|
|
<span>{{ item.sale_price }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'suppliers'" class="panel-block">
|
|||
|
|
<h2>工厂/供应商管理</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in suppliers" :key="item.supplier_id" class="list-item">
|
|||
|
|
<strong>{{ item.supplier_name }}</strong>
|
|||
|
|
<span>{{ item.supplier_type }}</span>
|
|||
|
|
<span>{{ item.status }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'orders'" class="panel-block">
|
|||
|
|
<h2>订单管理</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in orders" :key="item.order_id" class="list-item">
|
|||
|
|
<strong>{{ item.order_no }}</strong>
|
|||
|
|
<span>{{ item.customer_name }}</span>
|
|||
|
|
<span>{{ item.order_status }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'reports'" class="panel-block">
|
|||
|
|
<h2>报表</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div class="list-item">
|
|||
|
|
<strong>订单总数</strong>
|
|||
|
|
<span>{{ report.order_count }}</span>
|
|||
|
|
<span>利润 {{ report.profit_total }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'configs'" class="panel-block">
|
|||
|
|
<h2>配置管理</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in configs" :key="item.config_key" class="list-item">
|
|||
|
|
<strong>{{ item.config_name }}</strong>
|
|||
|
|
<span>{{ item.config_value }}</span>
|
|||
|
|
<span>{{ item.remark }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section v-if="view === 'audits'" class="panel-block">
|
|||
|
|
<h2>审计日志</h2>
|
|||
|
|
<div class="list">
|
|||
|
|
<div v-for="item in audits" :key="item.id" class="list-item">
|
|||
|
|
<strong>{{ item.biz_type }}</strong>
|
|||
|
|
<span>{{ item.action }}</span>
|
|||
|
|
<span>{{ item.operator_name }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</section>
|
|||
|
|
</main>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { reactive, ref } from 'vue'
|
|||
|
|
|
|||
|
|
const API_BASE = 'http://localhost:8000/api/v1'
|
|||
|
|
const username = ref('admin')
|
|||
|
|
const password = ref('123456')
|
|||
|
|
const errorMessage = ref('')
|
|||
|
|
const token = ref(localStorage.getItem('admin-token') || '')
|
|||
|
|
const isAuthed = ref(Boolean(token.value))
|
|||
|
|
const view = ref('users')
|
|||
|
|
const user = reactive(JSON.parse(localStorage.getItem('admin-user') || '{"username":"","real_name":"","role_name":""}'))
|
|||
|
|
const users = ref([])
|
|||
|
|
const roles = ref([])
|
|||
|
|
const menuTree = ref([])
|
|||
|
|
const customers = ref([])
|
|||
|
|
const products = ref([])
|
|||
|
|
const suppliers = ref([])
|
|||
|
|
const orders = ref([])
|
|||
|
|
const report = reactive({ order_count: 0, profit_total: 0 })
|
|||
|
|
const configs = ref([])
|
|||
|
|
const audits = ref([])
|
|||
|
|
|
|||
|
|
const cards = [
|
|||
|
|
{ key: 'users', title: '用户管理', desc: '用户列表、维护、权限分配。' },
|
|||
|
|
{ key: 'roles', title: '角色管理', desc: '角色维护与菜单权限配置。' },
|
|||
|
|
{ key: 'menus', title: '菜单管理', desc: '菜单树与权限编码。' },
|
|||
|
|
{ key: 'customers', title: '客户管理', desc: '客户资料维护。' },
|
|||
|
|
{ key: 'products', title: '产品管理', desc: '产品资料维护。' },
|
|||
|
|
{ key: 'suppliers', title: '工厂/供应商', desc: '工厂资料维护。' },
|
|||
|
|
{ key: 'orders', title: '订单管理', desc: '订单查询与详情。' },
|
|||
|
|
{ key: 'reports', title: '报表', desc: '统计与导出。' },
|
|||
|
|
{ key: 'configs', title: '配置管理', desc: '业务阈值配置。' },
|
|||
|
|
{ key: 'audits', title: '审计日志', desc: '关键操作留痕。' },
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
async function api(path, options = {}) {
|
|||
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|||
|
|
...options,
|
|||
|
|
headers: {
|
|||
|
|
...(options.headers || {}),
|
|||
|
|
...(token.value ? { Authorization: `Bearer ${token.value}` } : {}),
|
|||
|
|
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
return response.json()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function handleLogin() {
|
|||
|
|
errorMessage.value = ''
|
|||
|
|
const data = await api('/auth/login', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify({ username: username.value, password: password.value, role_type: 'admin' }),
|
|||
|
|
})
|
|||
|
|
if (data.code !== 0) {
|
|||
|
|
errorMessage.value = data.message || '登录失败'
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
token.value = data.data.token
|
|||
|
|
isAuthed.value = true
|
|||
|
|
user.username = username.value
|
|||
|
|
user.real_name = data.data.real_name
|
|||
|
|
user.role_name = data.data.role_name
|
|||
|
|
localStorage.setItem('admin-token', token.value)
|
|||
|
|
localStorage.setItem('admin-user', JSON.stringify(user))
|
|||
|
|
await bootstrap()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function logout() {
|
|||
|
|
token.value = ''
|
|||
|
|
isAuthed.value = false
|
|||
|
|
localStorage.removeItem('admin-token')
|
|||
|
|
localStorage.removeItem('admin-user')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function bootstrap() {
|
|||
|
|
const [u, r, m, c, p, s, o, cfg, au, rep] = await Promise.all([
|
|||
|
|
api('/auth/me'),
|
|||
|
|
api('/roles'),
|
|||
|
|
api('/menus/tree'),
|
|||
|
|
api('/customers'),
|
|||
|
|
api('/products'),
|
|||
|
|
api('/suppliers'),
|
|||
|
|
api('/orders'),
|
|||
|
|
api('/configs/logistics_timeout_hours'),
|
|||
|
|
api('/audit-logs'),
|
|||
|
|
api('/reports/performance'),
|
|||
|
|
])
|
|||
|
|
if (u.code === 0) user.real_name = u.data.real_name
|
|||
|
|
if (r.code === 0) roles.value = r.data
|
|||
|
|
if (m.code === 0) menuTree.value = m.data
|
|||
|
|
if (c.code === 0) customers.value = c.data.list || []
|
|||
|
|
if (p.code === 0) products.value = p.data.list || []
|
|||
|
|
if (s.code === 0) suppliers.value = s.data.list || []
|
|||
|
|
if (o.code === 0) orders.value = o.data.list || []
|
|||
|
|
if (cfg.code === 0) configs.value = [cfg.data]
|
|||
|
|
if (au.code === 0) audits.value = au.data.list || []
|
|||
|
|
if (rep.code === 0) {
|
|||
|
|
report.order_count = rep.data.order_count
|
|||
|
|
report.profit_total = rep.data.profit_total
|
|||
|
|
}
|
|||
|
|
users.value = [
|
|||
|
|
{ id: 1, username: 'admin', real_name: '管理员', role_name: '管理员' },
|
|||
|
|
{ id: 2, username: 'sales', real_name: '业务员', role_name: '业务员' },
|
|||
|
|
{ id: 3, username: 'driver', real_name: '司机', role_name: '司机' },
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isAuthed.value) bootstrap()
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
:global(*) { box-sizing: border-box; }
|
|||
|
|
:global(body) { margin: 0; font-family: Inter, system-ui, sans-serif; background: linear-gradient(180deg, #eef2ff 0%, #f8fafc 100%); color: #0f172a; }
|
|||
|
|
.page { max-width: 1240px; margin: 0 auto; padding: 44px 24px 64px; }
|
|||
|
|
.hero, .panel-block { padding: 32px; border-radius: 24px; background: rgba(255,255,255,.92); box-shadow: 0 20px 60px rgba(15,23,42,.08); margin-bottom: 20px; }
|
|||
|
|
.eyebrow { margin: 0 0 8px; color: #4f46e5; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
|
|||
|
|
h1, h2 { margin-top: 0; }
|
|||
|
|
.desc { color: #475569; }
|
|||
|
|
.form-grid, .filters { display: grid; gap: 12px; margin: 16px 0; }
|
|||
|
|
.form-grid.single { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
|||
|
|
input { height: 44px; border-radius: 12px; border: 1px solid #dbe2ea; padding: 0 14px; }
|
|||
|
|
button { height: 44px; border: 0; border-radius: 12px; padding: 0 18px; cursor: pointer; }
|
|||
|
|
.primary { background: #4f46e5; color: #fff; }
|
|||
|
|
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
|
|||
|
|
.grid, .list { display: grid; gap: 16px; }
|
|||
|
|
.nav-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
|||
|
|
.card, .list-item { padding: 18px; border: 1px solid #e2e8f0; border-radius: 18px; background: #fff; }
|
|||
|
|
.card { cursor: pointer; box-shadow: 0 8px 24px rgba(15,23,42,.05); }
|
|||
|
|
.list-item { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 8px; }
|
|||
|
|
.json-box { padding: 16px; border-radius: 16px; background: #0f172a; color: #e2e8f0; overflow: auto; }
|
|||
|
|
.error-text { color: #dc2626; margin-top: 12px; }
|
|||
|
|
</style>
|