This commit is contained in:
suguo 2026-03-18 17:18:17 +08:00
parent 51b58fdf7d
commit e95168c529
6 changed files with 313 additions and 32 deletions

View File

@ -0,0 +1,70 @@
<script setup>
import { onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import { useMenuStore } from '../stores/menu'
const menuStore = useMenuStore()
onMounted(() => {
menuStore.loadMenus()
})
</script>
<template>
<nav class="menu">
<RouterLink v-for="menu in menuStore.menus" :key="menu.id" class="item" active-class="is-active" :to="menu.path">
{{ menu.name }}
</RouterLink>
</nav>
</template>
<style scoped>
.menu {
margin-top: 12px;
display: grid;
gap: 6px;
}
.item {
height: 36px;
padding: 0 12px;
border-radius: 10px;
display: flex;
align-items: center;
text-decoration: none;
color: var(--muted);
border: 1px solid transparent;
}
.item:hover {
color: var(--text);
border-color: rgba(180, 128, 64, 0.25);
background: rgba(180, 128, 64, 0.08);
}
.item.is-active {
color: var(--text);
border-color: rgba(32, 92, 64, 0.28);
background: rgba(32, 92, 64, 0.1);
font-weight: 800;
}
@media (max-width: 980px) {
.item {
justify-content: center;
padding: 0;
}
}
@media (max-width: 720px) {
.menu {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.item {
justify-content: center;
padding: 0 8px;
text-align: center;
}
}
</style>

View File

@ -2,10 +2,12 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { RouterLink, RouterView, useRouter } from 'vue-router'
import { apiFetch } from '../../lib/api'
import { clearAuth, getToken, getUser, setUser } from '../../lib/auth'
import { clearAuth, getToken } from '../../lib/auth'
import { useUserStore } from '../../stores/user'
import DynamicMenu from '../../components/DynamicMenu.vue'
const router = useRouter()
const user = ref(getUser())
const userStore = useUserStore()
const menuOpen = ref(false)
const showUserModal = ref(false)
const showPasswordModal = ref(false)
@ -16,34 +18,20 @@ const passwordError = ref('')
const passwordLoading = ref(false)
const menuRoot = ref(null)
const avatarText = computed(() => {
const name = user.value?.userName || '用户'
const t = String(name).trim()
return t ? t.slice(0, 1).toUpperCase() : 'U'
})
const avatarText = computed(() => userStore.userAvatarText)
async function logout() {
try {
await apiFetch('/api/logout', { method: 'POST' })
} catch {
} finally {
clearAuth()
userStore.logout()
}
router.push('/')
}
async function loadMe() {
if (!getToken()) return
try {
const res = await apiFetch('/api/me')
if (res?.user) {
user.value = res.user
setUser(res.user)
}
} catch {
clearAuth()
router.push('/login')
}
await userStore.loadUser()
}
function closeMenu() {
@ -127,12 +115,7 @@ onBeforeUnmount(() => {
</div>
</RouterLink>
<nav class="menu">
<RouterLink class="item" active-class="is-active" to="/admin/archives">资源档案</RouterLink>
<RouterLink class="item" active-class="is-active" to="/admin/inspections">巡查监测</RouterLink>
<RouterLink class="item" active-class="is-active" to="/admin/projects">修缮项目</RouterLink>
<RouterLink class="item" active-class="is-active" to="/admin/analytics">统计分析</RouterLink>
</nav>
<DynamicMenu />
</aside>
<div class="main">
@ -142,7 +125,7 @@ onBeforeUnmount(() => {
<div ref="menuRoot" class="userbox">
<button class="userbtn" type="button" @click.stop="menuOpen = !menuOpen">
<span class="avatar">{{ avatarText }}</span>
<span class="username">{{ user?.userName || '管理员' }}</span>
<span class="username">{{ userStore.user?.userName || '管理员' }}</span>
<span class="chev" :class="{ 'is-open': menuOpen }"></span>
</button>
<div v-if="menuOpen" class="menu2">
@ -170,15 +153,15 @@ onBeforeUnmount(() => {
<div class="dialog__body">
<div class="kv">
<div class="k">用户名</div>
<div class="v">{{ user?.userName || '-' }}</div>
<div class="v">{{ userStore.user?.userName || '-' }}</div>
</div>
<div class="kv">
<div class="k">用户ID</div>
<div class="v mono">{{ user?.id || '-' }}</div>
<div class="v mono">{{ userStore.user?.id || '-' }}</div>
</div>
<div class="kv">
<div class="k">角色ID</div>
<div class="v mono">{{ user?.roleId || '-' }}</div>
<div class="v mono">{{ userStore.user?.roleId || '-' }}</div>
</div>
</div>
<div class="dialog__foot">

135
src/pages/org/list.vue Normal file
View File

@ -0,0 +1,135 @@
<script setup>
import { ref, onMounted } from 'vue'
import { apiFetch } from '../../lib/api'
const orgs = ref([])
const loading = ref(false)
const error = ref('')
async function loadOrgs() {
loading.value = true
error.value = ''
try {
const res = await apiFetch('/api/orgs')
if (res?.items) {
orgs.value = res.items
}
} catch (err) {
error.value = err.message || '加载组织机构失败'
} finally {
loading.value = false
}
}
onMounted(() => {
loadOrgs()
})
</script>
<template>
<div class="org-page">
<div class="page-header">
<h1>组织机构管理</h1>
<button class="btn2 btn2--primary">添加组织</button>
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else class="org-list">
<div v-for="org in orgs" :key="org.id" class="org-item">
<div class="org-name">{{ org.name }}</div>
<div class="org-code">{{ org.code }}</div>
<div class="org-actions">
<button class="btn2">编辑</button>
<button class="btn2">删除</button>
</div>
</div>
<div v-if="orgs.length === 0" class="empty">暂无组织机构</div>
</div>
</div>
</template>
<style scoped>
.org-page {
padding: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-header h1 {
font-size: 20px;
font-weight: 800;
margin: 0;
}
.loading,
.error,
.empty {
padding: 40px;
text-align: center;
color: var(--muted);
}
.error {
color: rgba(180, 44, 44, 0.95);
}
.org-list {
display: grid;
gap: 12px;
}
.org-item {
padding: 16px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface);
display: flex;
justify-content: space-between;
align-items: center;
}
.org-name {
font-weight: 800;
flex: 1;
}
.org-code {
color: var(--muted);
margin: 0 16px;
}
.org-actions {
display: flex;
gap: 8px;
}
.btn2 {
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid rgba(31, 35, 40, 0.12);
background: rgba(255, 255, 255, 0.7);
cursor: pointer;
font-weight: 700;
color: rgba(31, 35, 40, 0.85);
}
.btn2:hover {
border-color: rgba(180, 128, 64, 0.35);
}
.btn2--primary {
border-color: rgba(180, 128, 64, 0.35);
background: rgba(180, 128, 64, 0.16);
}
.btn2--primary:hover {
background: rgba(180, 128, 64, 0.22);
}
</style>

View File

@ -6,7 +6,6 @@ import Projects from '../pages/Projects.vue'
import Analytics from '../pages/Analytics.vue'
import QrLogin from '../pages/QrLogin.vue'
import AdminLayout from '../pages/admin/AdminLayout.vue'
import AdminDashboard from '../pages/admin/Dashboard.vue'
import { getToken } from '../lib/auth'
const router = createRouter({
@ -46,12 +45,13 @@ const router = createRouter({
path: '/admin',
component: AdminLayout,
children: [
{ path: '', name: 'admin-home', component: AdminDashboard },
{ path: 'dashboard', redirect: '/admin' },
{ path: '', redirect: '/admin/archives' },
{ path: 'dashboard', component: Home },
{ path: 'archives', name: 'admin-archives', component: Archives },
{ path: 'inspections', name: 'admin-inspections', component: Inspections },
{ path: 'projects', name: 'admin-projects', component: Projects },
{ path: 'analytics', name: 'admin-analytics', component: Analytics },
{ path: 'org/list', name: 'admin-org', component: () => import('../pages/org/list.vue') },
],
},
],

37
src/stores/menu.js Normal file
View File

@ -0,0 +1,37 @@
import { defineStore } from 'pinia'
import { apiFetch } from '../lib/api'
import { getToken } from '../lib/auth'
// 定义菜单 store
export const useMenuStore = defineStore('menu', {
state: () => ({
menus: [],
loading: false,
error: null
}),
getters: {
hasMenus: (state) => state.menus.length > 0
},
actions: {
async loadMenus() {
if (!getToken()) return
this.loading = true
this.error = null
try {
const res = await apiFetch('/api/menus')
if (res?.items) {
this.menus = res.items
}
} catch (err) {
this.error = err.message || '加载菜单失败'
console.error('加载菜单失败:', err)
} finally {
this.loading = false
}
}
}
})

56
src/stores/user.js Normal file
View File

@ -0,0 +1,56 @@
import { defineStore } from 'pinia'
import { apiFetch } from '../lib/api'
import { clearAuth, getToken, getUser, setUser } from '../lib/auth'
// 定义用户 store
export const useUserStore = defineStore('user', {
state: () => ({
user: getUser() || null,
loading: false,
error: null
}),
getters: {
isLoggedIn: (state) => !!state.user,
userAvatarText: (state) => {
if (!state.user) return 'U'
const name = state.user.userName || '用户'
const t = String(name).trim()
return t ? t.slice(0, 1).toUpperCase() : 'U'
}
},
actions: {
async loadUser() {
if (!getToken()) return
this.loading = true
this.error = null
try {
const res = await apiFetch('/api/me')
if (res?.user) {
this.user = res.user
setUser(res.user)
}
} catch (err) {
this.error = err.message || '加载用户信息失败'
clearAuth()
// 这里可以添加路由跳转逻辑,跳转到登录页
} finally {
this.loading = false
}
},
logout() {
clearAuth()
this.user = null
this.error = null
},
setUser(user) {
this.user = user
setUser(user)
}
}
})