整体布局调整

This commit is contained in:
aaron 2025-01-08 10:16:18 +08:00
parent 899f292b54
commit 4064c74f70
9 changed files with 678 additions and 232 deletions

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>DM Admin</title> <title>闪兔到家后台管理系统</title>
<link rel="stylesheet" href="https://a.amap.com/jsapi_demos/static/demo-center/css/demo-center.css" /> <link rel="stylesheet" href="https://a.amap.com/jsapi_demos/static/demo-center/css/demo-center.css" />
<script type="text/javascript"> <script type="text/javascript">
window._AMapSecurityConfig = { window._AMapSecurityConfig = {

View File

@ -0,0 +1,15 @@
<template>
<div class="page-container">
<slot></slot>
</div>
</template>
<style scoped>
.page-container {
background: #fff;
min-height: calc(100vh - 120px);
padding: 24px;
margin: 16px;
border-radius: 4px;
}
</style>

282
src/layouts/TabsLayout.vue Normal file
View File

@ -0,0 +1,282 @@
<template>
<a-layout class="tabs-layout">
<a-layout-header class="header">
<div class="logo">闪兔到家后台管理系统</div>
<div class="header-right">
<a-space>
<a-button type="link" icon><SearchOutlined /></a-button>
<a-button type="link" icon><QuestionCircleOutlined /></a-button>
<a-dropdown>
<a-button type="link">
{{ username }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="logout" @click="handleLogout">
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</div>
</a-layout-header>
<a-layout>
<a-layout-sider width="256" class="sider">
<a-menu
mode="inline"
:selectedKeys="[activeMenu]"
:openKeys="openKeys"
@select="handleMenuSelect"
@openChange="handleOpenChange"
>
<a-sub-menu key="user">
<template #title>
<span>
<UserOutlined />
<span>用户管理</span>
</span>
</template>
<a-menu-item key="/user/list">用户列表</a-menu-item>
</a-sub-menu>
<a-sub-menu key="community">
<template #title>
<span>
<HomeOutlined />
<span>小区管理</span>
</span>
</template>
<a-menu-item key="/community/list">小区列表</a-menu-item>
<a-menu-item key="/community/building">楼栋管理</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content class="content">
<div class="tabs-wrapper">
<a-tabs
v-model:activeKey="activeTab"
type="editable-card"
@edit="handleTabEdit"
@change="handleTabChange"
hide-add
>
<a-tab-pane
v-for="tab in tabs"
:key="tab.key"
:tab="tab.title"
:closable="tab.closable"
/>
</a-tabs>
</div>
<div class="content-wrapper">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script>
import { defineComponent, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
SearchOutlined,
QuestionCircleOutlined,
DownOutlined,
UserOutlined,
HomeOutlined
} from '@ant-design/icons-vue'
export default defineComponent({
components: {
SearchOutlined,
QuestionCircleOutlined,
DownOutlined,
UserOutlined,
HomeOutlined
},
setup() {
const router = useRouter()
const route = useRoute()
const activeTab = ref('')
const activeMenu = ref('')
const openKeys = ref(['user', 'community'])
const username = ref('')
const tabs = ref([
{
title: '工作台',
key: '/dashboard',
closable: false
}
])
//
const getUserInfo = () => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
username.value = userInfo.username || '未登录'
}
//
getUserInfo()
//
watch(
() => route.path,
(newPath) => {
activeMenu.value = newPath
const existTab = tabs.value.find(tab => tab.key === newPath)
if (!existTab) {
tabs.value.push({
title: route.meta.title,
key: newPath,
closable: true
})
}
activeTab.value = newPath
},
{ immediate: true }
)
//
const handleTabChange = (key) => {
router.push(key)
}
//
const handleTabEdit = (targetKey, action) => {
if (action === 'remove') {
const targetIndex = tabs.value.findIndex(tab => tab.key === targetKey)
if (targetIndex === -1) return
//
if (targetKey === activeTab.value) {
const newActiveKey = tabs.value[targetIndex - 1]?.key || tabs.value[targetIndex + 1]?.key
if (newActiveKey) {
router.push(newActiveKey)
}
}
//
tabs.value = tabs.value.filter(tab => tab.key !== targetKey)
}
}
//
const handleMenuSelect = ({ key }) => {
router.push(key)
}
// /
const handleOpenChange = (keys) => {
openKeys.value = keys
}
// 退
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
router.push('/login')
}
return {
activeTab,
activeMenu,
openKeys,
tabs,
username,
handleTabChange,
handleTabEdit,
handleMenuSelect,
handleOpenChange,
handleLogout
}
}
})
</script>
<style scoped>
.tabs-layout {
min-height: 100vh;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
z-index: 10;
}
.logo {
font-size: 16px;
color: #1890ff;
white-space: nowrap;
}
.header-right {
display: flex;
align-items: center;
}
.sider {
background: #fff;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
}
.content {
display: flex;
flex-direction: column;
padding: 0;
background: #f0f2f5;
}
.tabs-wrapper {
padding: 6px 6px 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
}
.content-wrapper {
flex: 1;
padding: 16px;
overflow: auto;
}
:deep(.ant-tabs-nav) {
margin: 0;
}
:deep(.ant-tabs-tab) {
border: none !important;
background: transparent !important;
padding: 8px 16px !important;
}
:deep(.ant-tabs-tab-active) {
background: #e6f7ff !important;
}
:deep(.ant-tabs-tab-btn) {
color: #666 !important;
}
:deep(.ant-tabs-tab-active .ant-tabs-tab-btn) {
color: #1890ff !important;
}
:deep(.ant-menu-item) {
margin: 0 !important;
}
</style>

View File

@ -18,7 +18,8 @@ import {
Modal, Modal,
Select, Select,
InputNumber, InputNumber,
AutoComplete AutoComplete,
Tabs
} from 'ant-design-vue' } from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css' import 'ant-design-vue/dist/antd.css'
@ -43,5 +44,6 @@ app.use(Modal)
app.use(Select) app.use(Select)
app.use(InputNumber) app.use(InputNumber)
app.use(AutoComplete) app.use(AutoComplete)
app.use(Tabs)
app.mount('#app') app.mount('#app')

View File

@ -1,24 +1,30 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import BasicLayout from '../layouts/BasicLayout.vue' import TabsLayout from '../layouts/TabsLayout.vue'
const routes = [ const routes = [
{ {
path: '/', path: '/',
component: BasicLayout, component: TabsLayout,
children: [ children: [
{ {
path: '', path: '',
redirect: '/user/list' redirect: '/dashboard'
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/dashboard/Dashboard.vue'),
meta: { title: '工作台' }
}, },
{ {
path: '/user/list', path: '/user/list',
name: 'userList', name: 'UserList',
component: () => import('../views/user/UserList.vue'), component: () => import('../views/user/UserList.vue'),
meta: { title: '用户列表' } meta: { title: '用户列表' }
}, },
{ {
path: '/community/list', path: '/community/list',
name: 'communityList', name: 'CommunityList',
component: () => import('../views/community/CommunityList.vue'), component: () => import('../views/community/CommunityList.vue'),
meta: { title: '小区列表' } meta: { title: '小区列表' }
}, },
@ -27,12 +33,6 @@ const routes = [
name: 'BuildingList', name: 'BuildingList',
component: () => import('../views/community/BuildingList.vue'), component: () => import('../views/community/BuildingList.vue'),
meta: { title: '楼栋管理' } meta: { title: '楼栋管理' }
},
{
path: '/community/station',
name: 'stationList',
component: () => import('../views/community/StationList.vue'),
meta: { title: '驿站列表' }
} }
] ]
}, },

View File

@ -1,92 +1,94 @@
<template> <template>
<div class="building-list"> <page-container>
<div class="table-header"> <div class="building-list">
<h1>楼栋列表</h1> <div class="table-header">
<a-button type="primary" @click="showAddModal">添加楼栋</a-button> <h1>楼栋列表</h1>
</div> <a-button type="primary" @click="showAddModal">添加楼栋</a-button>
</div>
<!-- 添加筛选区域 --> <!-- 添加筛选区域 -->
<div class="table-filter"> <div class="table-filter">
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="所属小区"> <a-form-item label="所属小区">
<a-select <a-select
v-model:value="filterForm.community_id" v-model:value="filterForm.community_id"
placeholder="请选择小区" placeholder="请选择小区"
style="width: 200px" style="width: 200px"
allowClear allowClear
@change="handleFilter" @change="handleFilter"
>
<a-select-option :value="undefined">全部</a-select-option>
<a-select-option
v-for="item in communityOptions"
:key="item.id"
:value="item.id"
> >
{{ item.name }} <a-select-option :value="undefined">全部</a-select-option>
</a-select-option> <a-select-option
</a-select> v-for="item in communityOptions"
</a-form-item> :key="item.id"
</a-form> :value="item.id"
</div> >
{{ item.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</div>
<a-table <a-table
:columns="columns" :columns="columns"
:data-source="tableData" :data-source="tableData"
:pagination="pagination" :pagination="pagination"
:loading="loading" :loading="loading"
@change="handleTableChange" @change="handleTableChange"
row-key="id" row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'create_time'">
{{ formatDateTime(record.create_time) }}
</template>
</template>
</a-table>
<!-- 添加楼栋模态框 -->
<a-modal
v-model:visible="addModalVisible"
title="添加楼栋"
@cancel="handleCancel"
:confirmLoading="confirmLoading"
width="500px"
>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleAdd">
保存
</a-button>
</a-space>
</template>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
> >
<a-form-item label="所属小区" name="community_id" required> <template #bodyCell="{ column, record }">
<a-select <template v-if="column.key === 'create_time'">
v-model:value="formState.community_id" {{ formatDateTime(record.create_time) }}
placeholder="请选择小区" </template>
:options="communityOptions" </template>
:field-names="{ label: 'name', value: 'id' }" </a-table>
/>
</a-form-item>
<a-form-item label="楼栋名称" name="building_name" required> <!-- 添加楼栋模态框 -->
<a-input v-model:value="formState.building_name" placeholder="请输入楼栋名称" /> <a-modal
</a-form-item> v-model:visible="addModalVisible"
title="添加楼栋"
@cancel="handleCancel"
:confirmLoading="confirmLoading"
width="500px"
>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleAdd">
保存
</a-button>
</a-space>
</template>
<a-form-item label="楼栋编号" name="building_number" required> <a-form
<a-input v-model:value="formState.building_number" placeholder="请输入楼栋编号" /> ref="formRef"
</a-form-item> :model="formState"
</a-form> :rules="rules"
</a-modal> :label-col="{ span: 6 }"
</div> :wrapper-col="{ span: 16 }"
>
<a-form-item label="所属小区" name="community_id" required>
<a-select
v-model:value="formState.community_id"
placeholder="请选择小区"
:options="communityOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</a-form-item>
<a-form-item label="楼栋名称" name="building_name" required>
<a-input v-model:value="formState.building_name" placeholder="请输入楼栋名称" />
</a-form-item>
<a-form-item label="楼栋编号" name="building_number" required>
<a-input v-model:value="formState.building_number" placeholder="请输入楼栋编号" />
</a-form-item>
</a-form>
</a-modal>
</div>
</page-container>
</template> </template>
<script> <script>
@ -94,8 +96,12 @@ import { defineComponent, ref, onMounted } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { getBuildingList, getCommunityList, createBuilding } from '@/api/community' import { getBuildingList, getCommunityList, createBuilding } from '@/api/community'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import PageContainer from '@/components/PageContainer.vue'
export default defineComponent({ export default defineComponent({
components: {
PageContainer
},
setup() { setup() {
const loading = ref(false) const loading = ref(false)
const tableData = ref([]) const tableData = ref([])

View File

@ -1,122 +1,124 @@
<template> <template>
<div class="community-list"> <page-container>
<div class="table-header"> <div class="community-list">
<h1>小区列表</h1> <div class="table-header">
<a-button type="primary" @click="showAddModal">添加小区</a-button> <h1>小区列表</h1>
</div> <a-button type="primary" @click="showAddModal">添加小区</a-button>
</div>
<a-table <a-table
:columns="columns" :columns="columns"
:data-source="tableData" :data-source="tableData"
:pagination="pagination" :pagination="pagination"
:loading="loading" :loading="loading"
@change="handleTableChange" @change="handleTableChange"
row-key="id" row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-if="column.key === 'location'">
<a @click="showMap(record)">查看位置</a>
</template>
<template v-if="column.key === 'create_time'">
{{ formatDateTime(record.create_time) }}
</template>
</template>
</a-table>
<a-modal
v-model:visible="mapVisible"
title="小区位置"
:footer="null"
width="800px"
@cancel="closeMap"
>
<div id="map-container" style="height: 500px;"></div>
</a-modal>
<a-modal
v-model:visible="addModalVisible"
title="添加小区"
@ok="handleAdd"
@cancel="handleCancel"
:confirmLoading="confirmLoading"
width="680px"
>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleAdd">
保存
</a-button>
</a-space>
</template>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
> >
<a-form-item label="地址搜索"> <template #bodyCell="{ column, record }">
<a-auto-complete <template v-if="column.key === 'status'">
v-model:value="searchAddress" <a-tag :color="getStatusColor(record.status)">
:options="searchOptions" {{ getStatusText(record.status) }}
placeholder="输入地址搜索" </a-tag>
@change="handleSearch" </template>
@select="handleSelect" <template v-if="column.key === 'location'">
:loading="searchLoading" <a @click="showMap(record)">查看位置</a>
allow-clear </template>
> <template v-if="column.key === 'create_time'">
<template #option="{ value: val, label }"> {{ formatDateTime(record.create_time) }}
<div>{{ label }}</div> </template>
</template> </template>
</a-auto-complete> </a-table>
</a-form-item>
<a-form-item label=" " :colon="false"> <a-modal
<div class="map-container"> v-model:visible="mapVisible"
<div id="add-map-container" style="height: 240px;"></div> title="小区位置"
</div> :footer="null"
</a-form-item> width="800px"
@cancel="closeMap"
>
<div id="map-container" style="height: 500px;"></div>
</a-modal>
<a-form-item label="小区名称" name="name" required> <a-modal
<a-input v-model:value="formState.name" placeholder="请输入小区名称" /> v-model:visible="addModalVisible"
</a-form-item> title="添加小区"
@ok="handleAdd"
@cancel="handleCancel"
:confirmLoading="confirmLoading"
width="680px"
>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleAdd">
保存
</a-button>
</a-space>
</template>
<a-form-item label="详细地址" name="address" required> <a-form
<a-input v-model:value="formState.address" placeholder="请输入详细地址" /> ref="formRef"
</a-form-item> :model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="地址搜索">
<a-auto-complete
v-model:value="searchAddress"
:options="searchOptions"
placeholder="输入地址搜索"
@change="handleSearch"
@select="handleSelect"
:loading="searchLoading"
allow-clear
>
<template #option="{ value: val, label }">
<div>{{ label }}</div>
</template>
</a-auto-complete>
</a-form-item>
<a-form-item label="经纬度" required> <a-form-item label=" " :colon="false">
<a-input-group compact> <div class="map-container">
<a-input-number <div id="add-map-container" style="height: 240px;"></div>
v-model:value="formState.longitude" </div>
:min="-180" </a-form-item>
:max="180"
:controls="false" <a-form-item label="小区名称" name="name" required>
disabled <a-input v-model:value="formState.name" placeholder="请输入小区名称" />
style="width: 50%" </a-form-item>
placeholder="经度"
/> <a-form-item label="详细地址" name="address" required>
<a-input-number <a-input v-model:value="formState.address" placeholder="请输入详细地址" />
v-model:value="formState.latitude" </a-form-item>
:min="-90"
:max="90" <a-form-item label="经纬度" required>
:controls="false" <a-input-group compact>
disabled <a-input-number
style="width: 50%" v-model:value="formState.longitude"
placeholder="纬度" :min="-180"
/> :max="180"
</a-input-group> :controls="false"
</a-form-item> disabled
</a-form> style="width: 50%"
</a-modal> placeholder="经度"
</div> />
<a-input-number
v-model:value="formState.latitude"
:min="-90"
:max="90"
:controls="false"
disabled
style="width: 50%"
placeholder="纬度"
/>
</a-input-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</page-container>
</template> </template>
<script> <script>
@ -125,9 +127,11 @@ import { message, Tag } from 'ant-design-vue'
import { getCommunityList, createCommunity } from '@/api/community' import { getCommunityList, createCommunity } from '@/api/community'
import { initAMap, createMap } from '@/utils/amap' import { initAMap, createMap } from '@/utils/amap'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import PageContainer from '@/components/PageContainer.vue'
export default defineComponent({ export default defineComponent({
components: { components: {
PageContainer,
[Tag.name]: Tag [Tag.name]: Tag
}, },
setup() { setup() {

View File

@ -0,0 +1,127 @@
<template>
<page-container>
<h2>欢迎使用后台管理系统</h2>
<p class="time">{{ currentTime }}</p>
<div class="stats-section">
<div class="stat-item">
<div class="stat-label">小区总数</div>
<div class="stat-value">{{ stats.communityCount }}</div>
</div>
<div class="stat-item">
<div class="stat-label">楼栋总数</div>
<div class="stat-value">{{ stats.buildingCount }}</div>
</div>
<div class="stat-item">
<div class="stat-label">用户总数</div>
<div class="stat-value">{{ stats.userCount }}</div>
</div>
<div class="stat-item">
<div class="stat-label">今日活跃</div>
<div class="stat-value">{{ stats.activeCount }}</div>
</div>
</div>
</page-container>
</template>
<script>
import { defineComponent, ref, onMounted, onUnmounted } from 'vue'
import dayjs from 'dayjs'
import PageContainer from '@/components/PageContainer.vue'
export default defineComponent({
components: {
PageContainer
},
setup() {
const currentTime = ref('')
const stats = ref({
communityCount: 0,
buildingCount: 0,
userCount: 0,
activeCount: 0
})
//
const updateTime = () => {
currentTime.value = dayjs().format('YYYY年MM月DD日 HH:mm:ss')
}
//
let timer = null
onMounted(() => {
//
updateTime()
//
timer = setInterval(updateTime, 1000)
// TODO: API
// 使
stats.value = {
communityCount: 25,
buildingCount: 168,
userCount: 1280,
activeCount: 368
}
})
onUnmounted(() => {
//
if (timer) {
clearInterval(timer)
}
})
return {
currentTime,
stats
}
}
})
</script>
<style scoped>
.dashboard {
/* 移除 background、padding、margin 等样式 */
}
.dashboard h2 {
margin: 0;
font-size: 20px;
font-weight: normal;
color: #000;
}
.time {
margin: 8px 0 24px;
font-size: 13px;
color: #999;
}
.stats-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
background: #fff;
}
.stat-item {
background: #fff;
padding: 20px;
border: 1px solid #eee;
border-radius: 2px;
}
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
color: #1890ff;
font-weight: normal;
}
</style>

View File

@ -1,35 +1,41 @@
<template> <template>
<div class="user-list"> <page-container>
<div class="table-header"> <div class="user-list">
<h1>用户列表</h1> <div class="table-header">
</div> <h1>用户列表</h1>
</div>
<a-table <a-table
:columns="columns" :columns="columns"
:data-source="tableData" :data-source="tableData"
:pagination="pagination" :pagination="pagination"
:loading="loading" :loading="loading"
@change="handleTableChange" @change="handleTableChange"
row-key="userid" row-key="userid"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'phone'"> <template v-if="column.key === 'phone'">
{{ formatPhone(record.phone) }} {{ formatPhone(record.phone) }}
</template>
<template v-if="column.key === 'points'">
{{ record.points || 0 }}
</template>
</template> </template>
<template v-if="column.key === 'points'"> </a-table>
{{ record.points || 0 }} </div>
</template> </page-container>
</template>
</a-table>
</div>
</template> </template>
<script> <script>
import { defineComponent, ref, onMounted } from 'vue' import { defineComponent, ref, onMounted } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { getUserList } from '@/api/user' import { getUserList } from '@/api/user'
import PageContainer from '@/components/PageContainer.vue'
export default defineComponent({ export default defineComponent({
components: {
PageContainer
},
setup() { setup() {
const loading = ref(false) const loading = ref(false)
const tableData = ref([]) const tableData = ref([])
@ -149,4 +155,8 @@ export default defineComponent({
:deep(.ant-table-content) { :deep(.ant-table-content) {
overflow-x: auto; overflow-x: auto;
} }
.user-list {
/* 保留其他必要样式 */
}
</style> </style>