init
This commit is contained in:
parent
51b58fdf7d
commit
e95168c529
|
|
@ -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>
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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') },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue