first commit

This commit is contained in:
aaron 2025-01-08 08:32:49 +08:00
commit 765cb9c2e0
19 changed files with 10134 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
node_modules

9086
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "dm-admin",
"version": "0.1.0",
"private": true,
"dependencies": {
"ant-design-vue": "^3.2.20",
"axios": "^1.7.9",
"nprogress": "^0.2.0",
"vue": "^3.3.4",
"vue-router": "^4.5.0",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vue/cli-service": "^5.0.8",
"@vue/compiler-sfc": "^3.3.4",
"less": "^4.1.3",
"less-loader": "^7.3.0"
},
"scripts": {
"serve": "vue-cli-service serve --port 8080",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}
}

13
public/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>DM Admin</title>
<link rel="stylesheet" href="https://a.amap.com/jsapi_demos/static/demo-center/css/demo-center.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>

9
src/App.vue Normal file
View File

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'App'
}
</script>

9
src/api/community.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getCommunityList(params) {
return request({
url: '/api/community',
method: 'get',
params
})
}

17
src/api/user.js Normal file
View File

@ -0,0 +1,17 @@
import request from '@/utils/request'
export function login(data) {
return request({
url: '/api/user/password-login',
method: 'post',
data
})
}
export function getUserList(params) {
return request({
url: '/api/user/list',
method: 'get',
params
})
}

158
src/layouts/BasicLayout.vue Normal file
View File

@ -0,0 +1,158 @@
<template>
<a-layout class="layout">
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
<div class="logo" />
<a-menu
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
theme="dark"
mode="inline"
>
<a-sub-menu key="user">
<template #icon>
<user-outlined />
</template>
<template #title>用户管理</template>
<a-menu-item key="user-list">
<router-link to="/user/list">用户列表</router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="community">
<template #icon>
<home-outlined />
</template>
<template #title>小区管理</template>
<a-menu-item key="community-list">
<router-link to="/community/list">小区列表</router-link>
</a-menu-item>
<a-menu-item key="building-list">
<router-link to="/community/building">楼栋列表</router-link>
</a-menu-item>
<a-menu-item key="station-list">
<router-link to="/community/station">驿站列表</router-link>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0">
<div class="header-right">
<a-dropdown>
<a class="ant-dropdown-link" @click.prevent>
{{ userInfo.username }}
<down-outlined />
</a>
<template #overlay>
<a-menu>
<a-menu-item @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<menu-fold-outlined
v-else
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
</a-layout-header>
<a-layout-content
:style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }"
>
<router-view></router-view>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script>
import { ref, defineComponent } from 'vue'
import {
UserOutlined,
HomeOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
DownOutlined,
LogoutOutlined,
} from '@ant-design/icons-vue'
import { useRouter } from 'vue-router'
export default defineComponent({
name: 'BasicLayout',
components: {
UserOutlined,
HomeOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
DownOutlined,
LogoutOutlined,
},
setup() {
const router = useRouter()
const collapsed = ref(false)
const selectedKeys = ref(['user-list'])
const openKeys = ref(['user', 'community'])
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
window.addEventListener('storage', (e) => {
if (e.key === 'userInfo') {
userInfo.value = JSON.parse(e.newValue || '{}')
}
})
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
router.push('/login')
}
return {
collapsed,
selectedKeys,
openKeys,
userInfo,
handleLogout
}
},
})
</script>
<style scoped>
.layout {
min-height: 100vh;
}
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
.logo {
height: 32px;
background: rgba(255, 255, 255, 0.2);
margin: 16px;
}
.header-right {
float: right;
margin-right: 24px;
line-height: 64px;
}
.ant-dropdown-link {
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
padding: 0 8px;
}
</style>

41
src/main.js Normal file
View File

@ -0,0 +1,41 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import {
Button,
Layout,
Menu,
Card,
Form,
Input,
Table,
Dropdown,
SubMenu,
Space,
Popconfirm,
Divider,
Tag,
Modal
} from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
const app = createApp(App)
app.use(router)
// 注册需要的组件
app.use(Button)
app.use(Layout)
app.use(Menu)
app.use(Card)
app.use(Form)
app.use(Input)
app.use(Table)
app.use(Dropdown)
app.use(SubMenu)
app.use(Space)
app.use(Popconfirm)
app.use(Divider)
app.use(Tag)
app.use(Modal)
app.mount('#app')

69
src/router/index.js Normal file
View File

@ -0,0 +1,69 @@
import { createRouter, createWebHistory } from 'vue-router'
import BasicLayout from '../layouts/BasicLayout.vue'
const routes = [
{
path: '/',
component: BasicLayout,
children: [
{
path: '',
redirect: '/user/list'
},
{
path: '/user/list',
name: 'userList',
component: () => import('../views/user/UserList.vue'),
meta: { title: '用户列表' }
},
{
path: '/community/list',
name: 'communityList',
component: () => import('../views/community/CommunityList.vue'),
meta: { title: '小区列表' }
},
{
path: '/community/building',
name: 'buildingList',
component: () => import('../views/community/BuildingList.vue'),
meta: { title: '楼栋列表' }
},
{
path: '/community/station',
name: 'stationList',
component: () => import('../views/community/StationList.vue'),
meta: { title: '驿站列表' }
}
]
},
{
path: '/login',
name: 'login',
component: () => import('../views/login/Login.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path === '/login') {
if (token) {
next('/')
} else {
next()
}
} else {
if (token) {
next()
} else {
next('/login')
}
}
})
export default router

20
src/utils/amap.js Normal file
View File

@ -0,0 +1,20 @@
export function initAMap() {
return new Promise((resolve, reject) => {
if (window.AMap) {
resolve(window.AMap);
return;
}
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = `https://webapi.amap.com/maps?v=2.0&key=fd47f3d4f54b675693c7d59dcd2a6c5f`;
script.onerror = reject;
script.onload = () => {
resolve(window.AMap);
};
document.head.appendChild(script);
});
}

34
src/utils/request.js Normal file
View File

@ -0,0 +1,34 @@
import axios from 'axios'
import { message } from 'ant-design-vue'
const request = axios.create({
baseURL: 'http://127.0.0.1:8000',
timeout: 5000
})
// 请求拦截器
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.authorization = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
return response.data
},
error => {
message.error(error.response?.data?.message || '请求失败')
return Promise.reject(error)
}
)
export default request

View File

@ -0,0 +1,45 @@
<template>
<div class="building-list">
<h1>楼栋列表</h1>
<a-table :columns="columns" :data-source="data">
<template #headerCell="{ column }">
<template v-if="column.key === 'name'">
<span>楼栋名称</span>
</template>
</template>
</a-table>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const columns = [
{
title: '楼栋名称',
dataIndex: 'name',
key: 'name',
},
{
title: '所属小区',
dataIndex: 'community',
key: 'community',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
]
const data = ref([])
return {
data,
columns
}
}
})
</script>

View File

@ -0,0 +1,239 @@
<template>
<div class="community-list">
<div class="table-header">
<h1>小区列表</h1>
</div>
<a-table
:columns="columns"
:data-source="tableData"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
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>
</div>
</template>
<script>
import { defineComponent, ref, onMounted, nextTick } from 'vue'
import { message, Tag } from 'ant-design-vue'
import { getCommunityList } from '@/api/community'
import { initAMap } from '@/utils/amap'
import dayjs from 'dayjs'
export default defineComponent({
components: {
[Tag.name]: Tag
},
setup() {
const loading = ref(false)
const tableData = ref([])
const mapVisible = ref(false)
const currentMap = ref(null)
const currentMarker = ref(null)
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
//
const getStatusText = (status) => {
const statusMap = {
'OPENING': '运营中',
'UNOPEN': '未启用'
}
return statusMap[status] || status
}
//
const getStatusColor = (status) => {
const colorMap = {
'OPENING': 'green',
'UNOPEN': 'orange'
}
return colorMap[status] || 'default'
}
//
const formatDateTime = (value) => {
if (!value) return ''
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
align: 'center',
},
{
title: '小区名称',
dataIndex: 'name',
key: 'name',
width: 150,
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
width: 200,
},
{
title: '位置',
key: 'location',
width: 100,
align: 'center',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'center',
}
]
//
const fetchData = async () => {
try {
loading.value = true
const params = {
skip: (pagination.value.current - 1) * pagination.value.pageSize,
limit: pagination.value.pageSize
}
const res = await getCommunityList(params)
if (res.code === 200) {
tableData.value = res.data.items
pagination.value.total = res.data.total
} else {
message.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取小区列表失败:', error)
message.error('获取数据失败')
} finally {
loading.value = false
}
}
//
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
fetchData()
}
//
const showMap = async (record) => {
mapVisible.value = true
// DOM
await nextTick()
try {
const AMap = await initAMap()
//
if (!currentMap.value) {
currentMap.value = new AMap.Map('map-container', {
zoom: 15,
viewMode: '3D'
})
}
//
const position = [record.longitude, record.latitude]
currentMap.value.setCenter(position)
//
if (currentMarker.value) {
currentMap.value.remove(currentMarker.value)
}
//
currentMarker.value = new AMap.Marker({
position,
title: record.name
})
currentMap.value.add(currentMarker.value)
} catch (error) {
console.error('加载地图失败:', error)
message.error('加载地图失败')
}
}
//
const closeMap = () => {
mapVisible.value = false
}
onMounted(() => {
fetchData()
})
return {
loading,
columns,
tableData,
pagination,
mapVisible,
handleTableChange,
showMap,
closeMap,
getStatusText,
getStatusColor,
formatDateTime
}
}
})
</script>
<style scoped>
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-header h1 {
margin: 0;
}
:deep(.ant-table-content) {
overflow-x: auto;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="station-list">
<h1>驿站列表</h1>
<a-table :columns="columns" :data-source="data">
<template #headerCell="{ column }">
<template v-if="column.key === 'name'">
<span>驿站名称</span>
</template>
</template>
</a-table>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const columns = [
{
title: '驿站名称',
dataIndex: 'name',
key: 'name',
},
{
title: '所属小区',
dataIndex: 'community',
key: 'community',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
]
const data = ref([])
return {
data,
columns
}
}
})
</script>

107
src/views/login/Login.vue Normal file
View File

@ -0,0 +1,107 @@
<template>
<div class="login-container">
<a-card class="login-card" title="系统登录">
<a-form
:model="formState"
name="login"
@finish="onFinish"
autocomplete="off"
>
<a-form-item
name="phone"
:rules="[{ required: true, message: '请输入手机号码!' }]"
>
<a-input v-model:value="formState.phone" placeholder="手机号码">
<template #prefix>
<mobile-outlined />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[{ required: true, message: '请输入密码!' }]"
>
<a-input-password v-model:value="formState.password" placeholder="密码">
<template #prefix>
<lock-outlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
class="login-button"
:loading="loading"
>
登录
</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script>
import { defineComponent, reactive, ref } from 'vue'
import { MobileOutlined, LockOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { login } from '@/api/user'
export default defineComponent({
components: {
MobileOutlined,
LockOutlined
},
setup() {
const router = useRouter()
const loading = ref(false)
const formState = reactive({
phone: '',
password: ''
})
const onFinish = async values => {
try {
loading.value = true
const res = await login(values)
localStorage.setItem('token', res.data.access_token)
localStorage.setItem('userInfo', JSON.stringify(res.data.user))
message.success('登录成功')
router.push('/')
} catch (error) {
console.error('登录失败:', error)
} finally {
loading.value = false
}
}
return {
formState,
loading,
onFinish
}
}
})
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
}
.login-card {
width: 368px;
}
.login-button {
width: 100%;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="settings">
<h1>系统设置</h1>
<a-form
:model="formState"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
>
<a-form-item
label="系统名称"
name="systemName"
>
<a-input v-model:value="formState.systemName" />
</a-form-item>
<a-form-item
label="系统描述"
name="description"
>
<a-textarea v-model:value="formState.description" />
</a-form-item>
</a-form>
</div>
</template>
<script>
import { defineComponent, reactive } from 'vue'
export default defineComponent({
setup() {
const formState = reactive({
systemName: '',
description: ''
})
return {
formState
}
}
})
</script>

152
src/views/user/UserList.vue Normal file
View File

@ -0,0 +1,152 @@
<template>
<div class="user-list">
<div class="table-header">
<h1>用户列表</h1>
</div>
<a-table
:columns="columns"
:data-source="tableData"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
row-key="userid"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'phone'">
{{ formatPhone(record.phone) }}
</template>
<template v-if="column.key === 'points'">
{{ record.points || 0 }}
</template>
</template>
</a-table>
</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { getUserList } from '@/api/user'
export default defineComponent({
setup() {
const loading = ref(false)
const tableData = ref([])
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
const columns = [
{
title: '用户ID',
dataIndex: 'userid',
key: 'userid',
width: 80,
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 120,
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
width: 120,
},
{
title: '用户编号',
dataIndex: 'user_code',
key: 'user_code',
width: 120,
},
{
title: '积分',
dataIndex: 'points',
key: 'points',
width: 80,
align: 'right',
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: 180,
}
]
// 4*
const formatPhone = (phone) => {
if (!phone) return ''
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
//
const fetchData = async () => {
try {
loading.value = true
const params = {
skip: (pagination.value.current - 1) * pagination.value.pageSize,
limit: pagination.value.pageSize
}
const res = await getUserList(params)
if (res.code === 200) {
tableData.value = res.data.items
pagination.value.total = res.data.total
} else {
message.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取用户列表失败:', error)
message.error('获取数据失败')
} finally {
loading.value = false
}
}
//
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
fetchData()
}
onMounted(() => {
fetchData()
})
return {
loading,
columns,
tableData,
pagination,
handleTableChange,
formatPhone
}
}
})
</script>
<style scoped>
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-header h1 {
margin: 0;
}
:deep(.ant-table-content) {
overflow-x: auto;
}
</style>

21
vue.config.js Normal file
View File

@ -0,0 +1,21 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
css: {
loaderOptions: {
less: {
lessOptions: {
javascriptEnabled: true,
},
},
},
},
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true
}
}
}
})