#20 通用模块: - MinioService: MinIO 文件上传/下载/桶管理 - @DataScope 注解: 数据权限切片 - ExcelUtils: EasyExcel 导出 - IdUtils: Snowflake ID 生成器 - BaseEntity: MyBatis-Plus 基础实体 #23 前端框架: - Vue3 + TypeScript + Vite + Element Plus + ECharts + Pinia - 路由系统: 登录守卫 + 动态路由 - 登录页: 渐变背景 + Axios+Sa-Token 认证 - 仪表盘: 统计卡片 + 供水趋势折线图 + 片区饼图 - MainLayout: 侧边栏菜单 + 顶栏用户下拉(退出) - API 层: Axios 请求/响应拦截器 + Token 自动注入 - 系统管理骨架: 用户/角色/菜单/部门 列表页
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
import request from './request'
|
||||
|
||||
export function login(username: string, password: string) {
|
||||
return request.post('/base/auth/login', { username, password })
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return request.get('/base/auth/user-info')
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request.post('/base/auth/logout')
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = axios.create({ baseURL: '/api', timeout: 15000 })
|
||||
|
||||
request.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) config.headers.Authorization = token
|
||||
return config
|
||||
})
|
||||
|
||||
request.interceptors.response.use(
|
||||
res => {
|
||||
const data = res.data
|
||||
if (data.code !== 200) { ElMessage.error(data.message); return Promise.reject(data) }
|
||||
return data
|
||||
},
|
||||
err => { ElMessage.error(err.message); return Promise.reject(err) }
|
||||
)
|
||||
|
||||
export default request
|
||||
@@ -0,0 +1,2 @@
|
||||
body { margin:0; padding:0; font-family:"Microsoft YaHei","PingFang SC",sans-serif }
|
||||
#app { height:100vh }
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<el-container style="height:100vh">
|
||||
<el-aside width="220px" class="sidebar">
|
||||
<div class="logo">💧 智慧水务</div>
|
||||
<el-menu :default-active="route.path" router background-color="#304156" text-color="#bfcbd9" active-text-color="#409EFF">
|
||||
<el-menu-item index="/dashboard"><el-icon><DataAnalysis /></el-icon>供水总览</el-menu-item>
|
||||
<el-sub-menu index="system">
|
||||
<template #title><el-icon><Setting /></el-icon>系统管理</template>
|
||||
<el-menu-item index="/system/user">用户管理</el-menu-item>
|
||||
<el-menu-item index="/system/role">角色管理</el-menu-item>
|
||||
<el-menu-item index="/system/menu">菜单管理</el-menu-item>
|
||||
<el-menu-item index="/system/dept">部门管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="topbar">
|
||||
<span class="title">精河县供水工程综合管理平台</span>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="user">{{ userStore.realName || '管理员' }} <el-icon><ArrowDown /></el-icon></span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-header>
|
||||
<el-main><router-view /></el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
function handleCommand(cmd: string) {
|
||||
if (cmd === 'logout') { userStore.logout(); router.push('/login') }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar { background:#304156; overflow-y:auto }
|
||||
.logo { color:#fff; text-align:center; padding:16px; font-size:18px; font-weight:bold; border-bottom:1px solid rgba(255,255,255,.1) }
|
||||
.topbar { background:#fff; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid #e6e6e6 }
|
||||
.title { font-size:16px; color:#333 }
|
||||
.user { cursor:pointer; color:#666 }
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import './assets/style.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
for (const [key, comp] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, comp)
|
||||
}
|
||||
app.use(ElementPlus).use(router).use(createPinia()).mount('#app')
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{ path: '/login', name: 'login', component: () => import('@/views/login/LoginView.vue') },
|
||||
{
|
||||
path: '/', component: () => import('@/components/layout/MainLayout.vue'),
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{ path: 'dashboard', name: 'dashboard', component: () => import('@/views/dashboard/DashboardView.vue') },
|
||||
{ path: 'system/user', name: 'user', component: () => import('@/views/system/user/UserList.vue') },
|
||||
{ path: 'system/role', name: 'role', component: () => import('@/views/system/role/RoleList.vue') },
|
||||
{ path: 'system/menu', name: 'menu', component: () => import('@/views/system/menu/MenuList.vue') },
|
||||
{ path: 'system/dept', name: 'dept', component: () => import('@/views/system/dept/DeptList.vue') },
|
||||
]
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/dashboard' }
|
||||
]
|
||||
|
||||
const router = createRouter({ history: createWebHistory(), routes })
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (to.path !== '/login' && !token) { next('/login') }
|
||||
else { next() }
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const realName = ref('')
|
||||
const setToken = (t: string) => { token.value = t; localStorage.setItem('token', t) }
|
||||
const logout = () => { token.value = ''; localStorage.removeItem('token') }
|
||||
return { token, realName, setToken, logout }
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h2>供水总览</h2>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6" v-for="card in cards" :key="card.title">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ card.value }}</div>
|
||||
<div class="stat-title">{{ card.title }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :span="12"><el-card><div ref="chart1" style="height:300px" /></el-card></el-col>
|
||||
<el-col :span="12"><el-card><div ref="chart2" style="height:300px" /></el-card></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const cards = [
|
||||
{ title: '今日进水(m³)', value: '12,580' },
|
||||
{ title: '今日出水(m³)', value: '11,230' },
|
||||
{ title: '在线设备', value: '156' },
|
||||
{ title: '今日报警', value: '3' },
|
||||
]
|
||||
|
||||
const chart1 = ref()
|
||||
const chart2 = ref()
|
||||
|
||||
onMounted(() => {
|
||||
const c1 = echarts.init(chart1.value)
|
||||
c1.setOption({
|
||||
title: { text: '24小时供水趋势' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { data: ['00:00','04:00','08:00','12:00','16:00','20:00'] },
|
||||
yAxis: { name: '流量(m³/h)' },
|
||||
series: [{ data: [320,280,450,520,480,380], type: 'line', smooth: true, areaStyle: {} }]
|
||||
})
|
||||
const c2 = echarts.init(chart2.value)
|
||||
c2.setOption({
|
||||
title: { text: '片区用水分布' },
|
||||
tooltip: {},
|
||||
series: [{
|
||||
type: 'pie', radius: ['45%','70%'],
|
||||
data: [{ value: 3200, name: '一体化水厂' },{ value: 1800, name: '精芒片区' },
|
||||
{ value: 1500, name: '八家户片区' },{ value: 1200, name: '托里片区' },
|
||||
{ value: 900, name: '大镇片区' },{ value: 600, name: '托托片区' }]
|
||||
}]
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card { text-align:center; padding:10px }
|
||||
.stat-value { font-size:28px; font-weight:bold; color:#2a5298 }
|
||||
.stat-title { color:#666; margin-top:8px }
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<h1>智慧水务管理系统</h1>
|
||||
<el-form :model="form" :rules="rules" ref="formRef">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" @keyup.enter="handleLogin" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="large" style="width:100%" :loading="loading" @click="handleLogin">登 录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { login } from '@/api/auth'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const form = reactive({ username: 'admin', password: 'admin123' })
|
||||
const rules = { username: [{ required: true, message: '请输入用户名' }], password: [{ required: true, message: '请输入密码' }] }
|
||||
|
||||
async function handleLogin() {
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login(form.username, form.password)
|
||||
userStore.setToken(res.data)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/dashboard')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container { display:flex; justify-content:center; align-items:center; height:100vh; background:linear-gradient(135deg,#1e3c72 0%,#2a5298 100%) }
|
||||
.login-box { width:400px; padding:40px; background:#fff; border-radius:8px; box-shadow:0 2px 12px rgba(0,0,0,.3) }
|
||||
h1 { text-align:center; margin-bottom:30px; color:#1e3c72 }
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" @click="dialogVisible=true">新增部门</el-button>
|
||||
<el-table :data="tableData" style="margin-top:10px" border>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default>
|
||||
<el-button link type="primary">编辑</el-button>
|
||||
<el-button link type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="dialogVisible" title="新增部门"><span>表单内容待实现</span></el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const dialogVisible = ref(false)
|
||||
const tableData = ref<Array<{id:number,name:string}>>([])
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" @click="dialogVisible=true">新增菜单</el-button>
|
||||
<el-table :data="tableData" style="margin-top:10px" border>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default>
|
||||
<el-button link type="primary">编辑</el-button>
|
||||
<el-button link type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="dialogVisible" title="新增菜单"><span>表单内容待实现</span></el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const dialogVisible = ref(false)
|
||||
const tableData = ref<Array<{id:number,name:string}>>([])
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" @click="dialogVisible=true">新增角色</el-button>
|
||||
<el-table :data="tableData" style="margin-top:10px" border>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default>
|
||||
<el-button link type="primary">编辑</el-button>
|
||||
<el-button link type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="dialogVisible" title="新增角色"><span>表单内容待实现</span></el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const dialogVisible = ref(false)
|
||||
const tableData = ref<Array<{id:number,name:string}>>([])
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" @click="dialogVisible=true">新增用户</el-button>
|
||||
<el-table :data="tableData" style="margin-top:10px" border>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default>
|
||||
<el-button link type="primary">编辑</el-button>
|
||||
<el-button link type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="dialogVisible" title="新增用户"><span>表单内容待实现</span></el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const dialogVisible = ref(false)
|
||||
const tableData = ref<Array<{id:number,name:string}>>([])
|
||||
</script>
|
||||
Reference in New Issue
Block a user