初始化项目

This commit is contained in:
zengqiyang 2025-04-07 10:08:08 +08:00
parent 848be42621
commit f7b923539a
27 changed files with 5546 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules/
.DS_Store
.idea/
.vscode/
.env
.env.local
package-lock.json
yarn.lock

10
pc/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules/
.DS_Store
.idea/
.vscode/
.env
.env.local
package-lock.json
yarn.lock

77
pc/package.json Normal file
View File

@ -0,0 +1,77 @@
{
"name": "yoaf-web",
"version": "1.1.2",
"description": "智慧教育平台",
"author": "front-end",
"license": "MIT",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview"
},
"keywords": [
"vue",
"admin",
"dashboard",
"element-ui",
"boilerplate",
"admin-template",
"management-system"
],
"dependencies": {
"@riophae/vue-treeselect": "^0.4.0",
"axios": "0.24.0",
"clipboard": "2.0.8",
"core-js": "3.19.1",
"dayjs": "^1.11.10",
"echarts": "4.9.0",
"element-ui": "^2.15.6",
"file-saver": "2.0.5",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
"html2canvas": "^1.4.1",
"js-beautify": "1.13.0",
"js-cookie": "3.0.1",
"jszip": "^3.10.1",
"moment": "^2.30.1",
"nprogress": "0.2.0",
"qrcode": "^1.5.3",
"quill": "1.3.7",
"screenfull": "5.0.2",
"sm-crypto": "^0.3.12",
"sortablejs": "1.10.2",
"tui-image-editor": "^3.15.3",
"vue": "^2.7.14",
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-meta": "2.4.0",
"vue-router": "3.4.9",
"vuedraggable": "^2.24.3",
"vuex": "3.6.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vue/cli-plugin-babel": "4.4.6",
"@vue/cli-service": "4.4.6",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "4.1.0",
"compression-webpack-plugin": "5.0.2",
"connect": "3.6.6",
"mockjs": "^1.0.1-beta3",
"runjs": "4.4.2",
"sass": "1.32.13",
"sass-loader": "10.1.1",
"script-ext-html-webpack-plugin": "2.1.5",
"svg-sprite-loader": "5.1.1",
"vue-template-compiler": "2.6.14"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
">1%",
"last 2 versions"
]
}

17
pc/public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>智慧教育平台</title>
</head>
<body>
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

20
pc/src/App.vue Normal file
View File

@ -0,0 +1,20 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
height: 100vh;
}
</style>

View File

@ -0,0 +1,96 @@
import request from '@/utils/request'
// 查询资产分类列表
export function listClassification(query) {
return request({
url: '/asset/classification/list',
method: 'get',
params: query
})
}
// 查询资产分类详情
export function getClassification(id) {
return request({
url: '/asset/classification/' + id,
method: 'get'
})
}
// 新增资产分类
export function addClassification(data) {
return request({
url: '/asset/classification',
method: 'post',
data: data
})
}
// 修改资产分类
export function updateClassification(data) {
return request({
url: '/asset/classification',
method: 'put',
data: data
})
}
// 删除资产分类
export function delClassification(id) {
return request({
url: '/asset/classification/' + id,
method: 'delete'
})
}
// 禁用资产分类
export function disableClassification(id, lastModUserId) {
return request({
url: '/asset/classification/disable/' + id,
method: 'put',
params: { lastModUserId }
})
}
// 启用资产分类
export function enableClassification(id, lastModUserId) {
return request({
url: '/asset/classification/enable/' + id,
method: 'put',
params: { lastModUserId }
})
}
// 批量删除资产分类
export function delClassificationBatch(ids, lastModUserId) {
return request({
url: '/asset/classification/batch/' + ids,
method: 'delete',
params: { lastModUserId }
})
}
// 获取资产分类树形结构
export function getClassificationTree() {
return request({
url: '/asset/classification/tree',
method: 'get'
})
}
// 检查分类是否被使用
export function checkClassificationInUse(id) {
return request({
url: '/asset/classification/' + id + '/check',
method: 'get'
})
}
// 导出资产分类
export function exportClassification(query) {
return request({
url: '/asset/classification/export',
method: 'get',
params: query
})
}

View File

@ -0,0 +1,96 @@
import request from '@/utils/request'
// 查询资产位置列表
export function listLocation(query) {
return request({
url: '/asset/location/list',
method: 'get',
params: query
})
}
// 查询资产位置详情
export function getLocation(id) {
return request({
url: '/asset/location/' + id,
method: 'get'
})
}
// 新增资产位置
export function addLocation(data) {
return request({
url: '/asset/location',
method: 'post',
data: data
})
}
// 修改资产位置
export function updateLocation(data) {
return request({
url: '/asset/location',
method: 'put',
data: data
})
}
// 删除资产位置
export function delLocation(id) {
return request({
url: '/asset/location/' + id,
method: 'delete'
})
}
// 禁用资产位置
export function disableLocation(id, lastModUserId) {
return request({
url: '/asset/location/disable/' + id,
method: 'put',
params: { lastModUserId }
})
}
// 启用资产位置
export function enableLocation(id, lastModUserId) {
return request({
url: '/asset/location/enable/' + id,
method: 'put',
params: { lastModUserId }
})
}
// 批量删除资产位置
export function delLocationBatch(ids, lastModUserId) {
return request({
url: '/asset/location/batch/' + ids,
method: 'delete',
params: { lastModUserId }
})
}
// 获取资产位置树形结构
export function getLocationTree() {
return request({
url: '/asset/location/tree',
method: 'get'
})
}
// 检查位置是否被使用
export function checkLocationInUse(id) {
return request({
url: '/asset/location/' + id + '/check',
method: 'get'
})
}
// 导出资产位置
export function exportLocation(query) {
return request({
url: '/asset/location/export',
method: 'get',
params: query
})
}

133
pc/src/api/building.js Normal file
View File

@ -0,0 +1,133 @@
import request from '@/utils/request'
// 查询楼宇列表
export function getBuildingList(query) {
return request({
url: '/room/building/list',
method: 'get',
params: query
})
}
// 查询楼宇详细
export function getBuildingDetail(id) {
return request({
url: `/room/building/${id}`,
method: 'get'
})
}
// 新增楼宇
export function addBuilding(data) {
// 处理字段名映射,确保前端驼峰命名与后端下划线命名正确对应
const processedData = {
...data,
buildingTags: data.buildingTags,
isHot: data.isHot,
imageUrl: data.buildingImage,
}
// 删除undefined的属性
Object.keys(processedData).forEach(key => {
if (processedData[key] === undefined) {
delete processedData[key]
}
})
return request({
url: '/room/building',
method: 'post',
data: processedData
})
}
// 修改楼宇
export function updateBuilding(data) {
// 处理字段名映射,确保前端驼峰命名与后端下划线命名正确对应
const processedData = {
...data,
buildingTags: data.buildingTags,
isHot: data.isHot,
imageUrl: data.buildingImage
}
// 删除undefined的属性
Object.keys(processedData).forEach(key => {
if (processedData[key] === undefined) {
delete processedData[key]
}
})
return request({
url: '/room/building',
method: 'put',
data: processedData
})
}
// 删除楼宇
export function deleteBuilding(ids) {
return request({
url: `/room/building/${ids}`,
method: 'delete'
})
}
// 获取楼宇统计信息
export function getBuildingStatistics(id) {
return request({
url: `/room/building/${id}/statistics`,
method: 'get'
})
}
// 查询楼层列表
export function getFloorList(query) {
return request({
url: '/room/floor/list',
method: 'get',
params: query
})
}
// 获取楼层详情
export function getFloorDetail(id) {
return request({
url: `/room/floor/${id}`,
method: 'get'
})
}
// 新增楼层
export function addFloor(data) {
return request({
url: '/room/floor',
method: 'post',
data: data
})
}
// 修改楼层
export function updateFloor(data) {
return request({
url: '/room/floor',
method: 'put',
data: data
})
}
// 删除楼层
export function deleteFloor(ids) {
return request({
url: `/room/floor/${ids}`,
method: 'delete'
})
}
// 根据楼宇ID获取楼层列表
export function getFloorListByBuilding(buildingId) {
return request({
url: `/room/floor/building/${buildingId}`,
method: 'get'
})
}

61
pc/src/api/project.js Normal file
View File

@ -0,0 +1,61 @@
import request from '@/utils/request'
// 获取项目列表
export function getProjectList(query) {
return request({
url: '/room/project/list',
method: 'get',
params: query
})
}
// 获取项目详情
export function getProjectDetail(id) {
return request({
url: `/room/project/${id}`,
method: 'get'
})
}
// 新增项目
export function addProject(data) {
return request({
url: '/room/project',
method: 'post',
data: data
})
}
// 修改项目
export function updateProject(id, data) {
const updateData = { ...data, id }
return request({
url: '/room/project',
method: 'put',
data: updateData
})
}
// 删除项目
export function deleteProject(id) {
return request({
url: `/room/project/${id}`,
method: 'delete'
})
}
// 获取项目统计数据
export function getProjectStatistics(id) {
return request({
url: `/room/project/${id}/statistics`,
method: 'get'
})
}
// 获取所有项目统计数据(整体概况)
export function getAllProjectStatistics() {
return request({
url: '/room/project/statistics',
method: 'get'
})
}

View File

@ -0,0 +1,24 @@
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: Avenir, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
}
.el-container {
height: 100%;
}
.el-header {
padding: 0;
height: 60px;
}
.el-main {
background-color: #f0f2f5;
padding: 20px;
}

112
pc/src/layout/index.vue Normal file
View File

@ -0,0 +1,112 @@
<template>
<el-container class="app-wrapper">
<el-aside width="200px">
<el-menu
:default-active="$route.path"
class="el-menu-vertical"
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
router
>
<template v-for="route in routes">
<el-submenu
v-if="route.children && route.children.length > 1"
:key="route.path"
:index="route.path"
>
<template slot="title">
<i :class="route.meta && route.meta.icon"></i>
<span>{{ route.meta && route.meta.title }}</span>
</template>
<el-menu-item
v-for="child in route.children"
:key="child.path"
:index="route.path + '/' + child.path"
>
<i :class="child.meta && child.meta.icon"></i>
<span slot="title">{{ child.meta && child.meta.title }}</span>
</el-menu-item>
</el-submenu>
<el-menu-item
v-else-if="!route.hidden"
:key="route.path"
:index="route.path"
>
<i :class="route.meta && route.meta.icon"></i>
<span slot="title">{{ route.meta && route.meta.title }}</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div class="header-right">
<el-dropdown>
<span class="el-dropdown-link">
管理员<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script>
export default {
name: 'Layout',
computed: {
routes() {
return this.$router.options.routes.filter(route => {
return route.path !== '/' && !route.hidden
})
}
}
}
</script>
<style scoped>
.app-wrapper {
height: 100vh;
}
.el-aside {
background-color: #304156;
}
.el-menu {
border-right: none;
}
.el-header {
background-color: #fff;
color: #333;
line-height: 60px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: flex-end;
padding: 0 20px;
}
.header-right {
color: #333;
}
.el-dropdown-link {
cursor: pointer;
color: #409EFF;
}
.el-main {
background-color: #f0f2f5;
padding: 20px;
}
</style>

16
pc/src/main.js Normal file
View File

@ -0,0 +1,16 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import './assets/styles/index.scss'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

59
pc/src/router/index.js Normal file
View File

@ -0,0 +1,59 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Layout from '@/layout/index'
import systemRoutes from './modules/system'
import projectRoutes from './modules/project'
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: () => import('@/layout/index'),
redirect: '/home',
hidden: true
},
{
path: '/home',
component: () => import('@/layout/index'),
children: [
{
path: '',
name: 'Home',
component: () => import('../views/Home.vue'),
meta: { title: '首页', icon: 'el-icon-s-home' }
}
],
meta: { title: '首页', icon: 'el-icon-s-home' }
},
systemRoutes,
projectRoutes,
{
path: '/asset',
component: Layout,
name: 'Asset',
meta: { title: '资产管理', icon: 'el-icon-s-management' },
children: [
{
path: 'location',
component: () => import('@/views/asset/location/index'),
name: 'AssetLocation',
meta: { title: '资产位置设置', icon: 'el-icon-location' }
},
{
path: 'classification',
component: () => import('@/views/asset/classification/index'),
name: 'AssetClassification',
meta: { title: '资产分类设置', icon: 'el-icon-folder' }
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

View File

@ -0,0 +1,21 @@
export default {
path: '/project',
component: () => import('@/layout/index'),
redirect: '/project/list',
name: 'Project',
meta: { title: '项目管理', icon: 'el-icon-office-building' },
children: [
{
path: 'list',
name: 'ProjectList',
component: () => import('@/views/project/index'),
meta: { title: '项目列表' }
},
{
path: 'building',
name: 'BuildingList',
component: () => import('@/views/project/building/index'),
meta: { title: '楼宇列表' }
}
]
}

View File

@ -0,0 +1,15 @@
export default {
path: '/system',
component: () => import('@/layout/index'),
redirect: '/system/department-member',
name: 'System',
meta: { title: '系统管理', icon: 'el-icon-s-tools' },
children: [
{
path: 'department-member',
component: () => import('@/views/system/department-member/index'),
name: 'DepartmentMember',
meta: { title: '部门成员管理', icon: 'el-icon-office-building' }
}
]
}

15
pc/src/store/index.js Normal file
View File

@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

51
pc/src/utils/request.js Normal file
View File

@ -0,0 +1,51 @@
import axios from 'axios'
import { Message } from 'element-ui'
// 创建axios实例
const service = axios.create({
// baseURL: '/api', // 修改为相对路径,使用代理
baseURL: 'http://192.168.137.38:8080/api', // 接口地址
timeout: 10000 // 请求超时时间
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 可以在这里添加请求头等信息
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
// 如果返回的状态码不是200则判断为错误
if (res.code !== '000000') {
Message({
message: res.message || '系统错误',
type: 'error',
duration: 5 * 1000
})
return Promise.reject(new Error(res.message || '系统错误'))
} else {
return res
}
},
error => {
console.log('err' + error)
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service

84
pc/src/views/Home.vue Normal file
View File

@ -0,0 +1,84 @@
<template>
<div class="home">
<el-row :gutter="20">
<el-col :span="6">
<el-card shadow="hover">
<div class="card-header">
<i class="el-icon-user"></i>
<span>总成员数</span>
</div>
<div class="card-body">
<span class="number">100</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="card-header">
<i class="el-icon-office-building"></i>
<span>部门数量</span>
</div>
<div class="card-body">
<span class="number">10</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="card-header">
<i class="el-icon-user-solid"></i>
<span>在职人数</span>
</div>
<div class="card-body">
<span class="number">95</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="card-header">
<i class="el-icon-user"></i>
<span>离职人数</span>
</div>
<div class="card-body">
<span class="number">5</span>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: 'Home'
}
</script>
<style scoped>
.home {
padding: 20px;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.card-header i {
font-size: 20px;
margin-right: 10px;
color: #409EFF;
}
.card-body {
text-align: center;
}
.number {
font-size: 24px;
font-weight: bold;
color: #303133;
}
</style>

View File

@ -0,0 +1,416 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 左侧树形结构 -->
<el-col :span="6">
<div class="tree-container">
<el-tree
ref="classTree"
:data="classTree"
:props="defaultProps"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
:default-expanded-keys="defaultExpandedKeys">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
</span>
</el-tree>
</div>
</el-col>
<!-- 右侧列表 -->
<el-col :span="18">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>分类列表</span>
<el-button
style="float: right;"
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd">新增分类</el-button>
</div>
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="分类编码">
<el-input v-model="queryParams.classificationCode" placeholder="请输入分类编码" clearable size="small" />
</el-form-item>
<el-form-item label="分类名称">
<el-input v-model="queryParams.classificationName" placeholder="请输入分类名称" clearable size="small" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="classList" border stripe style="width: 100%">
<el-table-column prop="classificationCode" label="分类编码" width="150" />
<el-table-column prop="classificationName" label="分类名称" width="200" />
<el-table-column prop="parentName" label="上级分类" width="200" />
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'info'">
{{ scope.row.status === '1' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleEdit(scope.row)">修改</el-button>
<el-button
size="mini"
type="text"
@click="handleStatusChange(scope.row)">
{{ scope.row.status === '1' ? '禁用' : '启用' }}
</el-button>
<el-button
size="mini"
type="text"
style="color: #F56C6C"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
class="pagination">
</el-pagination>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="分类编码" prop="classificationCode">
<el-input v-model="form.classificationCode" placeholder="请输入分类编码" />
</el-form-item>
<el-form-item label="分类名称" prop="classificationName">
<el-input v-model="form.classificationName" placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="上级分类" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="classTree"
:props="defaultProps"
placeholder="请选择上级分类"
clearable
filterable
check-strictly
default-expand-all
node-key="id"
:render-after-expand="false"
style="width: 100%">
</el-tree-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listClassification, getClassification, addClassification, updateClassification, delClassification, enableClassification, disableClassification, getClassificationTree, checkClassificationInUse } from '@/api/asset/classification'
export default {
name: 'AssetClassification',
data() {
return {
//
loading: false,
//
total: 0,
//
classList: [],
//
classTree: [],
//
defaultExpandedKeys: [],
//
defaultProps: {
children: 'children',
label: 'classificationName'
},
//
dialogTitle: '',
//
dialogVisible: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
classificationCode: undefined,
classificationName: undefined,
status: undefined,
parentId: undefined
},
//
form: {},
//
rules: {
classificationCode: [
{ required: true, message: '请输入分类编码', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
classificationName: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
parentId: [
{ required: false, message: '请选择上级分类', trigger: 'change' }
],
remark: [
{ max: 200, message: '长度不能超过200个字符', trigger: 'blur' }
]
}
}
},
created() {
this.getTree()
this.getList()
},
methods: {
/** 获取分类树形结构 */
getTree() {
getClassificationTree().then(res => {
if (res.code === 200) {
this.classTree = res.data
//
if (this.classTree.length > 0) {
this.defaultExpandedKeys = [this.classTree[0].id]
}
}
})
},
/** 查询分类列表 */
getList() {
this.loading = true
listClassification(this.queryParams).then(res => {
if (res.code === 200) {
this.classList = res.data.rows
this.total = res.data.total
}
this.loading = false
})
},
/** 节点单击事件 */
handleNodeClick(data) {
this.queryParams.parentId = data.id
this.getList()
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.queryParams.parentId = undefined
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
classificationCode: undefined,
classificationName: undefined,
status: undefined,
parentId: undefined
}
this.getList()
},
/** 新增按钮操作 */
handleAdd() {
this.dialogTitle = '新增分类'
this.form = {
classificationCode: '',
classificationName: '',
parentId: this.queryParams.parentId || '', //
status: '1',
remark: ''
}
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
/** 修改按钮操作 */
handleEdit(row) {
const id = row.id
getClassification(id).then(res => {
if (res.code === 200) {
this.form = res.data
// parentId
if (this.form.parentId && typeof this.form.parentId === 'object') {
this.form.parentId = this.form.parentId.id || ''
}
this.dialogTitle = '编辑分类'
this.dialogVisible = true
//
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
}
})
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
const method = this.form.id ? updateClassification : addClassification
method(this.form).then(res => {
if (res.code === 200) {
this.$message.success('操作成功')
this.dialogVisible = false
this.getTree()
this.getList()
}
})
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除该分类?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 使
checkClassificationInUse(row.id).then(res => {
if (res.code === 200 && res.data) {
this.$message.error('该分类已被使用,无法删除')
return
}
// 使
delClassification(row.id).then(res => {
if (res.code === 200) {
this.$message.success('删除成功')
this.getTree()
this.getList()
}
})
})
}).catch(() => {})
},
/** 状态修改按钮操作 */
handleStatusChange(row) {
const action = row.status === '1' ? '禁用' : '启用'
this.$confirm(`是否确认${action}该分类?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// ID
const lastModUserId = this.$store.getters.userId || '1'
const method = row.status === '1' ? disableClassification : enableClassification
method(row.id, lastModUserId).then(res => {
if (res.code === 200) {
this.$message.success(`${action}成功`)
this.getList()
}
})
}).catch(() => {})
},
/** 分页大小改变 */
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getList()
},
/** 分页页码改变 */
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getList()
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
.tree-container {
background-color: #fff;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
height: calc(100vh - 140px);
overflow-y: auto;
}
.box-card {
margin-bottom: 20px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
/* 让所有输入框和下拉框保持一致宽度 */
.el-input, .el-select, .el-textarea {
width: 100%;
}
/* 确保所有表单元素的高度和边距一致 */
::v-deep .el-form-item {
margin-bottom: 18px;
.el-form-item__content {
line-height: 36px;
}
}
/* 确保el-select和普通输入框宽度一致 */
::v-deep .el-select, ::v-deep .el-tree-select {
width: 100%;
}
/* 树形选择器样式优化 */
::v-deep .el-tree-select .el-select-dropdown__item {
height: auto;
padding: 0;
}
}
</style>

View File

@ -0,0 +1,416 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 左侧树形结构 -->
<el-col :span="6">
<div class="tree-container">
<el-tree
ref="locationTree"
:data="locationTree"
:props="defaultProps"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
:default-expanded-keys="defaultExpandedKeys">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
</span>
</el-tree>
</div>
</el-col>
<!-- 右侧列表 -->
<el-col :span="18">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>位置列表</span>
<el-button
style="float: right;"
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd">新增位置</el-button>
</div>
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="位置编码">
<el-input v-model="queryParams.locationCode" placeholder="请输入位置编码" clearable size="small" />
</el-form-item>
<el-form-item label="位置名称">
<el-input v-model="queryParams.locationName" placeholder="请输入位置名称" clearable size="small" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="locationList" border stripe style="width: 100%">
<el-table-column prop="locationCode" label="位置编码" width="150" />
<el-table-column prop="locationName" label="位置名称" width="200" />
<el-table-column prop="parentName" label="上级位置" width="200" />
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'info'">
{{ scope.row.status === '1' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleEdit(scope.row)">修改</el-button>
<el-button
size="mini"
type="text"
@click="handleStatusChange(scope.row)">
{{ scope.row.status === '1' ? '禁用' : '启用' }}
</el-button>
<el-button
size="mini"
type="text"
style="color: #F56C6C"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
class="pagination">
</el-pagination>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="位置编码" prop="locationCode">
<el-input v-model="form.locationCode" placeholder="请输入位置编码" />
</el-form-item>
<el-form-item label="位置名称" prop="locationName">
<el-input v-model="form.locationName" placeholder="请输入位置名称" />
</el-form-item>
<el-form-item label="上级位置" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="locationTree"
:props="defaultProps"
placeholder="请选择上级位置"
clearable
filterable
check-strictly
default-expand-all
node-key="id"
:render-after-expand="false"
style="width: 100%">
</el-tree-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listLocation, getLocation, addLocation, updateLocation, delLocation, enableLocation, disableLocation, getLocationTree, checkLocationInUse } from '@/api/asset/location'
export default {
name: 'AssetLocation',
data() {
return {
//
loading: false,
//
total: 0,
//
locationList: [],
//
locationTree: [],
//
defaultExpandedKeys: [],
//
defaultProps: {
children: 'children',
label: 'locationName'
},
//
dialogTitle: '',
//
dialogVisible: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
locationCode: undefined,
locationName: undefined,
status: undefined,
parentId: undefined
},
//
form: {},
//
rules: {
locationCode: [
{ required: true, message: '请输入位置编码', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
locationName: [
{ required: true, message: '请输入位置名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
parentId: [
{ required: false, message: '请选择上级位置', trigger: 'change' }
],
remark: [
{ max: 200, message: '长度不能超过200个字符', trigger: 'blur' }
]
}
}
},
created() {
this.getTree()
this.getList()
},
methods: {
/** 获取位置树形结构 */
getTree() {
getLocationTree().then(res => {
if (res.code === 200) {
this.locationTree = res.data
//
if (this.locationTree.length > 0) {
this.defaultExpandedKeys = [this.locationTree[0].id]
}
}
})
},
/** 查询位置列表 */
getList() {
this.loading = true
listLocation(this.queryParams).then(res => {
if (res.code === 200) {
this.locationList = res.data.rows
this.total = res.data.total
}
this.loading = false
})
},
/** 节点单击事件 */
handleNodeClick(data) {
this.queryParams.parentId = data.id
this.getList()
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.queryParams.parentId = undefined
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
locationCode: undefined,
locationName: undefined,
status: undefined,
parentId: undefined
}
this.getList()
},
/** 新增按钮操作 */
handleAdd() {
this.dialogTitle = '新增位置'
this.form = {
locationCode: '',
locationName: '',
parentId: this.queryParams.parentId || '', //
status: '1',
remark: ''
}
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
/** 修改按钮操作 */
handleEdit(row) {
const id = row.id
getLocation(id).then(res => {
if (res.code === 200) {
this.form = res.data
// parentId
if (this.form.parentId && typeof this.form.parentId === 'object') {
this.form.parentId = this.form.parentId.id || ''
}
this.dialogTitle = '编辑位置'
this.dialogVisible = true
//
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
}
})
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
const method = this.form.id ? updateLocation : addLocation
method(this.form).then(res => {
if (res.code === 200) {
this.$message.success('操作成功')
this.dialogVisible = false
this.getTree()
this.getList()
}
})
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除该位置?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 使
checkLocationInUse(row.id).then(res => {
if (res.code === 200 && res.data) {
this.$message.error('该位置已被使用,无法删除')
return
}
// 使
delLocation(row.id).then(res => {
if (res.code === 200) {
this.$message.success('删除成功')
this.getTree()
this.getList()
}
})
})
}).catch(() => {})
},
/** 状态修改按钮操作 */
handleStatusChange(row) {
const action = row.status === '1' ? '禁用' : '启用'
this.$confirm(`是否确认${action}该位置?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// ID
const lastModUserId = this.$store.getters.userId || '1'
const method = row.status === '1' ? disableLocation : enableLocation
method(row.id, lastModUserId).then(res => {
if (res.code === 200) {
this.$message.success(`${action}成功`)
this.getList()
}
})
}).catch(() => {})
},
/** 分页大小改变 */
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getList()
},
/** 分页页码改变 */
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getList()
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
.tree-container {
background-color: #fff;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
height: calc(100vh - 140px);
overflow-y: auto;
}
.box-card {
margin-bottom: 20px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
/* 让所有输入框和下拉框保持一致宽度 */
.el-input, .el-select, .el-textarea {
width: 100%;
}
/* 确保所有表单元素的高度和边距一致 */
::v-deep .el-form-item {
margin-bottom: 18px;
.el-form-item__content {
line-height: 36px;
}
}
/* 确保el-select和普通输入框宽度一致 */
::v-deep .el-select, ::v-deep .el-tree-select {
width: 100%;
}
/* 树形选择器样式优化 */
::v-deep .el-tree-select .el-select-dropdown__item {
height: auto;
padding: 0;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,537 @@
<template>
<div class="app-container">
<!-- 统计数据展示区域 -->
<!-- <el-row :gutter="20" class="statistics-row">
<el-col :span="4">
<div class="stat-card">
<div class="stat-label">管理面积</div>
<div class="stat-value">{{ statistics.managementArea }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-card">
<div class="stat-label">可招商面积</div>
<div class="stat-value">{{ statistics.availableArea }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-card">
<div class="stat-label">可招商占比</div>
<div class="stat-value">{{ statistics.availableRatio }}%</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-card">
<div class="stat-label">总房源数量</div>
<div class="stat-value">{{ statistics.totalRooms }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-card">
<div class="stat-label">可招商房源数量</div>
<div class="stat-value">{{ statistics.availableRooms }} </div>
</div>
</el-col>
</el-row> ..>
<!-- 搜索区域 -->
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>项目列表</span>
<el-button
style="float: right;"
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd">新增项目</el-button>
</div>
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="项目名称">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable size="small" />
</el-form-item>
<el-form-item label="项目类型">
<el-select v-model="queryParams.projectType" placeholder="请选择项目类型" clearable size="small">
<el-option label="写字楼" value="写字楼" />
<el-option label="产业园区" value="产业园区" />
<el-option label="商场" value="商场" />
<el-option label="联合办公" value="联合办公" />
<el-option label="公寓" value="公寓" />
<el-option label="小区" value="小区" />
<el-option label="社区养老" value="社区养老" />
</el-select>
</el-form-item>
<el-form-item label="所属地区">
<el-input v-model="queryParams.region" placeholder="请输入所属地区" clearable size="small" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格区域 -->
<el-table v-loading="loading" :data="projectList" border style="width: 100%">
<el-table-column prop="projectName" label="项目名称" />
<el-table-column prop="projectType" label="项目类型" />
<el-table-column prop="region" label="所属地区" />
<el-table-column prop="totalArea" label="管理面积(㎡)" />
<el-table-column prop="availableArea" label="可招商面积(㎡)" />
<el-table-column prop="totalBuildings" label="总房源数量" />
<el-table-column prop="availableBuildings" label="可招商房源数量" />
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="text" style="color: #F56C6C" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="50%" @open="onDialogOpen" @close="onDialogClose">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目类型" prop="projectType">
<el-select v-model="form.projectType" placeholder="请选择项目类型" :disabled="form.id !== undefined">
<el-option label="写字楼" value="写字楼" />
<el-option label="产业园区" value="产业园区" />
<el-option label="商场" value="商场" />
<el-option label="联合办公" value="联合办公" />
<el-option label="公寓" value="公寓" />
<el-option label="小区" value="小区" />
<el-option label="社区养老" value="社区养老" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目简称" prop="projectShortName">
<el-input v-model="form.projectShortName" placeholder="请输入项目简称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属地区" prop="region">
<el-input v-model="form.region" placeholder="请输入所属地区" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="详细地址" prop="address">
<el-input v-model="form.address" type="textarea" placeholder="请输入详细地址" />
</el-form-item>
<el-form-item label="项目标签" prop="projectTags">
<el-select v-model="form.projectTags" multiple placeholder="请选择项目标签">
<el-option
v-for="tag in tagOptions"
:key="tag.value"
:label="tag.label"
:value="tag.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="项目简介" prop="projectDesc">
<el-input v-model="form.projectDesc" type="textarea" :rows="4" placeholder="请输入项目简介" />
</el-form-item>
<el-form-item label="项目图片" prop="projectImage">
<el-upload
class="avatar-uploader"
action="/api/upload"
:show-file-list="false"
:on-success="handleImageSuccess"
:before-upload="beforeImageUpload">
<img v-if="form.projectImage" :src="form.projectImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="项目详情" :visible.sync="detailVisible" width="50%">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目名称">{{ detail.projectName }}</el-descriptions-item>
<el-descriptions-item label="项目简称">{{ detail.projectShortName }}</el-descriptions-item>
<el-descriptions-item label="项目类型">{{ detail.projectType }}</el-descriptions-item>
<el-descriptions-item label="所属地区">{{ detail.region }}</el-descriptions-item>
<el-descriptions-item label="详细地址">{{ detail.address }}</el-descriptions-item>
<el-descriptions-item label="管理面积">{{ detail.totalArea }}</el-descriptions-item>
<el-descriptions-item label="可招商面积">{{ detail.availableArea }}</el-descriptions-item>
<el-descriptions-item label="总房源数量">{{ detail.totalBuildings }}</el-descriptions-item>
<el-descriptions-item label="可招商房源数量">{{ detail.availableBuildings }}</el-descriptions-item>
<el-descriptions-item label="排序值">{{ detail.sort }}</el-descriptions-item>
<el-descriptions-item label="竣工时间">{{ detail.completionTime }}</el-descriptions-item>
<el-descriptions-item label="标准层高">{{ detail.standardHeight }}m</el-descriptions-item>
<el-descriptions-item label="物业">{{ detail.property }}</el-descriptions-item>
<el-descriptions-item label="物业费">{{ detail.propertyFee }}//</el-descriptions-item>
<el-descriptions-item label="车位数量">{{ detail.parkingSpaces }}</el-descriptions-item>
<el-descriptions-item label="车位月租金">{{ detail.parkingFee }}/</el-descriptions-item>
<el-descriptions-item label="空调">{{ detail.airConditioning }}</el-descriptions-item>
<el-descriptions-item label="空调费">{{ detail.airConditioningFee }}//</el-descriptions-item>
<el-descriptions-item label="空调开放时间">{{ detail.airConditioningTime }}</el-descriptions-item>
<el-descriptions-item label="电梯">{{ detail.elevator }}</el-descriptions-item>
<el-descriptions-item label="网络">{{ detail.network }}</el-descriptions-item>
<el-descriptions-item label="入驻企业">{{ detail.enterprises }}</el-descriptions-item>
<el-descriptions-item label="招商岗位">{{ detail.recruitmentPosition }}</el-descriptions-item>
<el-descriptions-item label="招商部门">{{ detail.recruitmentDepartment }}</el-descriptions-item>
<el-descriptions-item label="VR链接">{{ detail.vrLink }}</el-descriptions-item>
<el-descriptions-item label="招商时间">{{ detail.recruitmentTime }}</el-descriptions-item>
<el-descriptions-item label="项目图片" :span="2">
<el-image
v-if="detail.projectImage"
:src="detail.projectImage"
:preview-src-list="[detail.projectImage]"
fit="cover"
style="width: 200px; height: 200px">
</el-image>
</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getProjectList, getProjectDetail, addProject, updateProject, deleteProject, getAllProjectStatistics } from '@/api/project'
export default {
name: 'ProjectList',
data() {
return {
//
loading: false,
//
total: 0,
//
projectList: [],
//
dialogTitle: '',
//
dialogVisible: false,
//
detailVisible: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
projectName: undefined,
projectType: undefined,
region: undefined
},
//
form: {},
//
detail: {},
//
tagOptions: [],
//
rules: {
projectType: [
{ required: true, message: '请选择项目类型', trigger: 'change' }
],
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' }
],
projectShortName: [
{ required: true, message: '请输入项目简称', trigger: 'blur' }
],
region: [
{ required: true, message: '请输入所属地区', trigger: 'blur' }
],
address: [
{ required: true, message: '请输入详细地址', trigger: 'blur' }
]
},
//
statistics: {
managementArea: '37,234.52',
availableArea: '20,916.2',
availableRatio: '56.17',
totalRooms: '637',
availableRooms: '51'
}
}
},
created() {
this.getList()
this.getTagOptions()
this.fetchStatistics()
},
methods: {
/** 查询项目列表 */
getList() {
this.loading = true
const params = {
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize,
projectName: this.queryParams.projectName,
projectType: this.queryParams.projectType,
region: this.queryParams.region
}
getProjectList(params).then(res => {
this.projectList = res.data.list
this.total = res.data.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** 获取标签选项 */
getTagOptions() {
// TODO:
this.tagOptions = [
{ value: '1', label: '标签1' },
{ value: '2', label: '标签2' }
]
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
projectName: undefined,
projectType: undefined,
region: undefined
}
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.dialogTitle = '新增项目'
//
this.form = {}
this.dialogVisible = true
},
/** 修改按钮操作 */
handleEdit(row) {
this.dialogTitle = '编辑项目'
getProjectDetail(row.id).then(res => {
//
const projectData = { ...res.data };
if (typeof projectData.projectTags === 'string' && projectData.projectTags) {
projectData.projectTags = projectData.projectTags.split(',');
} else if (!projectData.projectTags) {
projectData.projectTags = [];
}
this.form = projectData;
this.dialogVisible = true;
})
},
/** 详情按钮操作 */
handleDetail(row) {
getProjectDetail(row.id).then(res => {
//
const detailData = { ...res.data };
//
if (typeof detailData.projectTags === 'string' && detailData.projectTags) {
//
} else if (Array.isArray(detailData.projectTags)) {
//
detailData.projectTags = detailData.projectTags.join(',');
}
this.detail = detailData;
this.detailVisible = true;
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除该项目?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteProject(row.id).then(() => {
this.getList()
})
}).catch(() => {})
},
/** 表单提交 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
//
const formData = { ...this.form };
if (Array.isArray(formData.projectTags)) {
formData.projectTags = formData.projectTags.join(',');
}
// ID
const isEdit = formData.id !== undefined;
const method = isEdit ? updateProject(formData.id, formData) : addProject(formData);
method.then(() => {
this.dialogVisible = false;
this.getList();
});
}
});
},
/** 图片上传成功处理 */
handleImageSuccess(res, file) {
// URL
this.form.projectImage = res.data
},
/** 图片上传前处理 */
beforeImageUpload(file) {
const isJPG = file.type === 'image/jpeg'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
this.$message.error('上传图片只能是 JPG 格式!')
}
if (!isLt2M) {
this.$message.error('上传图片大小不能超过 2MB!')
}
return isJPG && isLt2M
},
/** 分页大小改变 */
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getList()
},
/** 分页页码改变 */
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getList()
},
/** 获取统计数据 */
async fetchStatistics() {
try {
const res = await getAllProjectStatistics()
this.statistics = {
managementArea: res.data.totalArea || '0',
availableArea: res.data.availableArea || '0',
availableRatio: res.data.availableArea && res.data.totalArea
? ((res.data.availableArea / res.data.totalArea) * 100).toFixed(2)
: '0',
totalRooms: res.data.totalBuildings || '0',
availableRooms: res.data.availableBuildings || '0'
}
} catch (error) {
console.error('获取统计数据失败:', error)
// 使
this.statistics = {
managementArea: '37,234.52',
availableArea: '20,916.2',
availableRatio: '56.17',
totalRooms: '637',
availableRooms: '51'
}
}
},
/** 对话框打开时处理 */
onDialogOpen() {
//
this.$nextTick(() => {
this.$refs['form'] && this.$refs['form'].clearValidate()
})
},
/** 对话框关闭时处理 */
onDialogClose() {
//
this.form = {}
this.$refs['form'].clearValidate()
}
}
}
</script>
<style lang="scss" scoped>
.statistics-row {
margin-bottom: 20px;
.stat-card {
background: #fff;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
text-align: center;
.stat-label {
color: #606266;
font-size: 14px;
margin-bottom: 10px;
}
.stat-value {
color: #303133;
font-size: 20px;
font-weight: bold;
}
}
}
/* 确保下拉框和输入框宽度一致 */
::v-deep .el-form-item {
.el-select {
width: 100%;
}
.el-input {
width: 100%;
}
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>

View File

@ -0,0 +1,946 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab" type="border-card">
<!-- 部门管理 tab -->
<el-tab-pane label="部门管理" name="department">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>部门管理</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="handleAddDepartment">新增部门</el-button>
</div>
<el-table
v-loading="departmentLoading"
:data="departmentList"
row-key="id"
border
default-expand-all
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="部门名称" />
<el-table-column prop="level" label="部门层级" />
<el-table-column prop="memberCount" label="部门人数" sortable />
<el-table-column prop="leader" label="部门负责人" />
<el-table-column label="操作" width="220">
<template slot-scope="scope">
<el-button type="text" @click="handleEditDepartment(scope.row)">编辑</el-button>
<el-button type="text" @click="handleAddSubDepartment(scope.row)">添加子部门</el-button>
<el-button type="text" class="delete-btn" @click="handleDeleteDepartment(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-tab-pane>
<!-- 成员管理 tab -->
<el-tab-pane label="成员管理" name="member">
<el-card class="box-card">
<div class="member-container">
<!-- 左侧部门树 -->
<div class="department-tree">
<div class="tree-header">
<el-input
v-model="deptSearchValue"
placeholder="搜索部门"
size="small"
prefix-icon="el-icon-search"
clearable
/>
</div>
<div class="tree-content">
<el-tree
ref="deptTree"
:data="departmentList"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
:filter-node-method="filterNode"
@node-click="handleNodeClick"
>
<span slot-scope="{ node, data }" class="custom-tree-node">
<span>{{ node.label }}</span>
<span class="member-count">({{ data.memberCount || 0 }})</span>
</span>
</el-tree>
</div>
</div>
<!-- 右侧成员列表 -->
<div class="member-list">
<div class="member-header">
<div class="header-title">
<span>{{ selectedDeptName || '全部' }}</span>
<span class="member-count">{{ total }}</span>
</div>
<div class="header-actions">
<el-button type="primary" size="small" @click="handleAddMember">添加成员</el-button>
<el-button type="primary" plain size="small" @click="handleImportMember">申请入列表</el-button>
<el-dropdown>
<el-button type="text" size="small">
更多<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>导出Excel</el-dropdown-item>
<el-dropdown-item>打印</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div class="member-filter">
<el-radio-group v-model="queryParams.status" size="small" @change="handleStatusChange">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="1">正常</el-radio-button>
<el-radio-button label="0">未激活</el-radio-button>
</el-radio-group>
</div>
<el-table
v-loading="memberLoading"
:data="memberList"
row-key="id"
border
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="status" label="账号状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'">
{{ scope.row.status === '1' ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="employeeId" label="工号" width="100" />
<el-table-column prop="position" label="职位" width="120" />
<el-table-column prop="phone" label="手机" width="120" />
<el-table-column prop="deptName" label="部门" width="120" />
<el-table-column prop="gender" label="性别" width="80">
<template #default="scope">
{{ scope.row.gender === '1' ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="entryDate" label="入职时间" width="120" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEditMember(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDeleteMember(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 30, 40]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</el-card>
</el-tab-pane>
</el-tabs>
<!-- 部门对话框 -->
<el-dialog :title="departmentTitle" :visible.sync="departmentOpen" width="500px" append-to-body>
<el-form ref="departmentForm" :model="departmentForm" :rules="departmentRules" label-width="80px">
<el-form-item label="上级部门" prop="parentId">
<treeselect
v-model="departmentForm.parentId"
:options="departmentOptions"
:normalizer="normalizer"
placeholder="选择上级部门"
/>
</el-form-item>
<el-form-item label="部门名称" prop="name">
<el-input v-model="departmentForm.name" placeholder="请输入部门名称" />
</el-form-item>
<el-form-item label="部门层级" prop="level">
<el-input v-model="departmentForm.level" placeholder="请输入部门层级" disabled />
</el-form-item>
<el-form-item label="负责人" prop="leader">
<el-input v-model="departmentForm.leader" placeholder="请输入负责人" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitDepartmentForm"> </el-button>
<el-button @click="cancelDepartment"> </el-button>
</div>
</el-dialog>
<!-- 成员对话框 -->
<el-dialog :title="memberTitle" :visible.sync="memberOpen" width="500px" append-to-body>
<el-form ref="memberForm" :model="memberForm" :rules="memberRules" label-width="80px">
<el-form-item label="姓名" prop="name">
<el-input v-model="memberForm.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<treeselect
v-model="memberForm.deptId"
:options="departmentOptions"
:normalizer="normalizer"
placeholder="选择部门"
/>
</el-form-item>
<el-form-item label="职位" prop="position">
<el-input v-model="memberForm.position" placeholder="请输入职位" />
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input v-model="memberForm.phone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="memberForm.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="memberForm.status">
<el-radio label="1">在职</el-radio>
<el-radio label="0">离职</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitMemberForm"> </el-button>
<el-button @click="cancelMember"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
name: 'DepartmentMember',
components: { Treeselect },
data() {
return {
// tab
activeTab: 'department',
//
departmentLoading: true,
departmentList: [],
departmentOptions: [],
departmentTitle: '',
departmentOpen: false,
departmentForm: {},
departmentRules: {
name: [
{ required: true, message: '部门名称不能为空', trigger: 'blur' }
],
leader: [
{ required: true, message: '负责人不能为空', trigger: 'blur' }
]
},
//
memberLoading: true,
memberList: [],
total: 0,
memberTitle: '',
memberOpen: false,
memberForm: {},
memberRules: {
name: [
{ required: true, message: '姓名不能为空', trigger: 'blur' }
],
deptId: [
{ required: true, message: '所属部门不能为空', trigger: 'change' }
]
},
//
queryParams: {
pageNum: 1,
pageSize: 10,
name: undefined,
deptId: undefined
},
deptSearchValue: '',
selectedDeptName: ''
}
},
mounted() {
this.getDepartmentList()
this.getMemberList()
},
methods: {
/** 获取部门列表 */
getDepartmentList() {
this.departmentLoading = true
// API
setTimeout(() => {
this.departmentList = [
{
id: 1,
name: '小红马演示园区',
level: '分公司',
memberCount: 19,
leader: '张三',
children: [
{
id: 2,
name: '技术部',
level: '分公司',
memberCount: 8,
leader: '李四',
children: [
{
id: 21,
name: '技术1组',
level: '分公司',
memberCount: 4,
leader: '王五'
},
{
id: 22,
name: '技术2组',
level: '分公司',
memberCount: 4,
leader: '赵六'
}
]
},
{
id: 3,
name: '成都分公司',
level: '分公司',
memberCount: 5,
leader: '王七',
children: [
{
id: 31,
name: '管理部门',
level: '分公司',
memberCount: 5,
leader: '刘八'
}
]
},
{
id: 4,
name: '安防部',
level: '分公司',
memberCount: 3,
leader: '钱九',
children: [
{
id: 41,
name: '保安',
level: '分公司',
memberCount: 3,
leader: '孙十'
}
]
},
{
id: 5,
name: '保洁部',
level: '分公司',
memberCount: 2,
leader: '周十一',
children: [
{
id: 51,
name: '电梯保洁',
level: '分公司',
memberCount: 1,
leader: '吴十二'
},
{
id: 52,
name: '楼道保洁',
level: '分公司',
memberCount: 1,
leader: '郑十三'
}
]
},
{
id: 6,
name: '巡检部门',
level: '分公司',
memberCount: 1,
leader: '冯十四'
},
{
id: 7,
name: '客服部',
level: '分公司',
memberCount: 1,
leader: '陈十五'
},
{
id: 8,
name: '维修部',
level: '分公司',
memberCount: 1,
leader: '褚十六'
}
]
}
]
this.departmentOptions = this.departmentList
this.departmentLoading = false
}, 500)
},
/** 获取成员列表 */
getMemberList() {
this.memberLoading = true
// API
setTimeout(() => {
// ID
let filteredList = [
{
id: 1,
name: '小马',
deptId: 21,
deptName: '技术1组',
status: '1',
phone: '13800138001',
gender: '1',
employeeId: 'EMP001',
position: '高级工程师',
entryDate: '2023-01-15'
},
{
id: 2,
name: '罗卜',
deptId: 21,
deptName: '技术1组',
status: '1',
phone: '13800138002',
gender: '2',
employeeId: 'EMP002',
position: '前端工程师',
entryDate: '2023-02-20'
},
{
id: 3,
name: '车轮',
deptId: 21,
deptName: '技术1组',
status: '1',
phone: '13800138003',
gender: '1',
employeeId: 'EMP003',
position: '后端工程师',
entryDate: '2023-03-10'
},
{
id: 4,
name: '刘斌',
deptId: 21,
deptName: '技术1组',
status: '1',
phone: '13800138004',
gender: '1',
employeeId: 'EMP004',
position: '测试工程师',
entryDate: '2023-04-05'
},
{
id: 5,
name: '王师傅',
deptId: 22,
deptName: '技术2组',
status: '1',
phone: '13800138005',
gender: '1',
employeeId: 'EMP005',
position: '技术主管',
entryDate: '2023-01-01'
},
{
id: 6,
name: '王鹏飞',
deptId: 22,
deptName: '技术2组',
status: '1',
phone: '13800138006',
gender: '1',
employeeId: 'EMP006',
position: '高级工程师',
entryDate: '2023-02-15'
},
{
id: 7,
name: '正容',
deptId: 22,
deptName: '技术2组',
status: '1',
phone: '13800138007',
gender: '2',
employeeId: 'EMP007',
position: 'UI设计师',
entryDate: '2023-03-20'
},
{
id: 8,
name: '王小二',
deptId: 22,
deptName: '技术2组',
status: '1',
phone: '13800138008',
gender: '1',
employeeId: 'EMP008',
position: '运维工程师',
entryDate: '2023-04-10'
},
{
id: 9,
name: '张三',
deptId: 31,
deptName: '管理部门',
status: '1',
phone: '13800138009',
gender: '1',
employeeId: 'EMP009',
position: '部门经理',
entryDate: '2023-01-05'
},
{
id: 10,
name: '李四',
deptId: 31,
deptName: '管理部门',
status: '1',
phone: '13800138010',
gender: '1',
employeeId: 'EMP010',
position: '行政主管',
entryDate: '2023-02-01'
},
{
id: 11,
name: '王五',
deptId: 31,
deptName: '管理部门',
status: '1',
phone: '13800138011',
gender: '1',
employeeId: 'EMP011',
position: '人力资源',
entryDate: '2023-03-15'
},
{
id: 12,
name: '赵六',
deptId: 31,
deptName: '管理部门',
status: '1',
phone: '13800138012',
gender: '2',
employeeId: 'EMP012',
position: '财务主管',
entryDate: '2023-04-01'
},
{
id: 13,
name: '钱七',
deptId: 31,
deptName: '管理部门',
status: '1',
phone: '13800138013',
gender: '2',
employeeId: 'EMP013',
position: '会计',
entryDate: '2023-05-10'
},
{
id: 14,
name: '孙八',
deptId: 41,
deptName: '保安',
status: '1',
phone: '13800138014',
gender: '1',
employeeId: 'EMP014',
position: '保安队长',
entryDate: '2023-01-10'
},
{
id: 15,
name: '周九',
deptId: 41,
deptName: '保安',
status: '1',
phone: '13800138015',
gender: '1',
employeeId: 'EMP015',
position: '保安',
entryDate: '2023-02-15'
},
{
id: 16,
name: '吴十',
deptId: 41,
deptName: '保安',
status: '1',
phone: '13800138016',
gender: '1',
employeeId: 'EMP016',
position: '保安',
entryDate: '2023-03-20'
},
{
id: 17,
name: '郑十一',
deptId: 51,
deptName: '电梯保洁',
status: '1',
phone: '13800138017',
gender: '2',
employeeId: 'EMP017',
position: '保洁员',
entryDate: '2023-01-20'
},
{
id: 18,
name: '王十二',
deptId: 52,
deptName: '楼道保洁',
status: '1',
phone: '13800138018',
gender: '2',
employeeId: 'EMP018',
position: '保洁员',
entryDate: '2023-02-25'
},
{
id: 19,
name: '冯十三',
deptId: 6,
deptName: '巡检部门',
status: '0',
phone: '13800138019',
gender: '1',
employeeId: 'EMP019',
position: '巡检员',
entryDate: '2023-03-30'
}
]
// ID
if (this.queryParams.deptId) {
// ID
const deptIds = this.getChildDeptIds(this.queryParams.deptId)
filteredList = filteredList.filter(item => deptIds.includes(item.deptId))
}
//
if (this.queryParams.status) {
filteredList = filteredList.filter(item => item.status === this.queryParams.status)
}
this.memberList = filteredList
this.total = filteredList.length
this.memberLoading = false
}, 500)
},
/** 获取部门及其所有子部门的ID */
getChildDeptIds(deptId) {
const ids = []
const collectIds = (dept) => {
ids.push(dept.id)
if (dept.children) {
dept.children.forEach(child => {
collectIds(child)
})
}
}
const findDepartment = (deptList) => {
for (const dept of deptList) {
if (dept.id === deptId) {
collectIds(dept)
return true
}
if (dept.children && findDepartment(dept.children)) {
return true
}
}
return false
}
findDepartment(this.departmentList)
return ids
},
/** 转换部门数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.id,
label: node.name,
children: node.children
}
},
//
handleAddDepartment() {
this.departmentForm = {
id: undefined,
parentId: undefined,
name: undefined,
level: undefined,
leader: undefined
}
this.departmentTitle = '添加部门'
this.departmentOpen = true
},
handleEditDepartment(row) {
this.departmentForm = row
this.departmentTitle = '修改部门'
this.departmentOpen = true
},
handleDeleteDepartment(row) {
this.$confirm('是否确认删除名称为"' + row.name + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// API
this.$message.success('删除成功')
this.getDepartmentList()
})
},
submitDepartmentForm() {
this.$refs['departmentForm'].validate(valid => {
if (valid) {
// API
this.$message.success('操作成功')
this.departmentOpen = false
this.getDepartmentList()
}
})
},
cancelDepartment() {
this.departmentOpen = false
this.$refs['departmentForm'].resetFields()
},
handleAddSubDepartment(row) {
this.departmentForm = {
id: undefined,
parentId: row.id,
name: undefined,
level: row.level + 1,
leader: undefined
}
this.departmentTitle = '添加子部门'
this.departmentOpen = true
},
//
handleAddMember() {
this.memberForm = {
id: undefined,
name: undefined,
deptId: undefined,
position: undefined,
phone: undefined,
email: undefined,
status: '1'
}
this.memberTitle = '添加成员'
this.memberOpen = true
},
handleEditMember(row) {
this.memberForm = row
this.memberTitle = '修改成员'
this.memberOpen = true
},
handleDeleteMember(row) {
this.$confirm('是否确认删除姓名为"' + row.name + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// API
this.$message.success('删除成功')
this.getMemberList()
})
},
submitMemberForm() {
this.$refs['memberForm'].validate(valid => {
if (valid) {
// API
this.$message.success('操作成功')
this.memberOpen = false
this.getMemberList()
}
})
},
cancelMember() {
this.memberOpen = false
this.$refs['memberForm'].resetFields()
},
//
handleQuery() {
this.queryParams.pageNum = 1
this.getMemberList()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
name: undefined,
deptId: undefined
}
this.getMemberList()
},
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getMemberList()
},
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getMemberList()
},
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
handleNodeClick(node) {
this.selectedDeptName = node.label;
this.queryParams.deptId = node.id;
this.getMemberList();
},
handleStatusChange() {
this.getMemberList();
},
handleDetail(row) {
//
},
handleImportMember() {
//
}
}
}
</script>
<style scoped>
.app-container {
padding: 20px;
height: 100%;
}
.el-tabs {
margin-bottom: 20px;
height: calc(100vh - 60px);
display: flex;
flex-direction: column;
}
.el-tabs ::v-deep .el-tabs__content {
flex: 1;
overflow: hidden;
}
.el-tabs ::v-deep .el-tab-pane {
height: 100%;
}
.el-card {
height: 100%;
}
.delete-btn {
color: #F56C6C;
}
.el-button + .el-button {
margin-left: 6px;
}
.member-container {
display: flex;
height: calc(100vh - 180px);
min-height: 500px;
}
.department-tree {
width: 280px;
border-right: 1px solid #e6e6e6;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tree-header {
padding: 10px;
border-bottom: 1px solid #e6e6e6;
}
.tree-content {
flex: 1;
overflow: auto;
padding: 10px;
}
.member-list {
flex: 1;
padding: 0 20px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.member-header {
padding: 16px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 16px;
font-weight: bold;
}
.header-title .member-count {
font-size: 14px;
color: #909399;
margin-left: 8px;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.member-filter {
margin-bottom: 16px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.custom-tree-node .member-count {
font-size: 12px;
color: #909399;
}
.el-table {
flex: 1;
overflow: hidden;
}
.el-table ::v-deep .el-table__body-wrapper {
overflow-y: auto;
height: calc(100% - 40px);
}
.el-pagination {
margin-top: 20px;
padding: 10px 0;
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>部门管理</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="handleAdd">新增部门</el-button>
</div>
<el-table
v-loading="loading"
:data="departmentList"
row-key="id"
border
default-expand-all
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table-column prop="name" label="部门名称" />
<el-table-column prop="code" label="部门编码" />
<el-table-column prop="leader" label="负责人" />
<el-table-column prop="phone" label="联系电话" />
<el-table-column prop="email" label="邮箱" />
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加或修改部门对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="上级部门" prop="parentId">
<treeselect
v-model="form.parentId"
:options="departmentOptions"
:normalizer="normalizer"
placeholder="选择上级部门"
/>
</el-form-item>
<el-form-item label="部门名称" prop="name">
<el-input v-model="form.name" placeholder="请输入部门名称" />
</el-form-item>
<el-form-item label="部门编码" prop="code">
<el-input v-model="form.code" placeholder="请输入部门编码" />
</el-form-item>
<el-form-item label="负责人" prop="leader">
<el-input v-model="form.leader" placeholder="请输入负责人" />
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
name: 'Department',
components: { Treeselect },
data() {
return {
//
loading: true,
//
departmentList: [],
//
departmentOptions: [],
//
title: '',
//
open: false,
//
form: {},
//
rules: {
name: [
{ required: true, message: '部门名称不能为空', trigger: 'blur' }
],
code: [
{ required: true, message: '部门编码不能为空', trigger: 'blur' }
]
}
}
},
mounted() {
this.getList()
},
methods: {
/** 查询部门列表 */
getList() {
this.loading = true
// API
setTimeout(() => {
this.departmentList = [
{
id: 1,
name: '总公司',
code: 'HQ',
leader: '张三',
phone: '13800138000',
email: 'zhangsan@example.com',
children: [
{
id: 2,
name: '技术部',
code: 'TECH',
leader: '李四',
phone: '13800138001',
email: 'lisi@example.com'
}
]
}
]
this.loading = false
}, 500)
},
/** 转换部门数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.id,
label: node.name,
children: node.children
}
},
/** 取消按钮 */
cancel() {
this.open = false
this.reset()
},
/** 表单重置 */
reset() {
this.form = {
id: undefined,
parentId: undefined,
name: undefined,
code: undefined,
leader: undefined,
phone: undefined,
email: undefined
}
this.resetForm('form')
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加部门'
},
/** 修改按钮操作 */
handleEdit(row) {
this.reset()
this.form = row
this.open = true
this.title = '修改部门'
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除名称为"' + row.name + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// API
this.$message.success('删除成功')
this.getList()
})
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
// API
this.$message.success('操作成功')
this.open = false
this.getList()
}
})
}
}
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
</style>

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,285 @@
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>成员管理</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="handleAdd">新增成员</el-button>
</div>
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="部门">
<treeselect
v-model="queryParams.deptId"
:options="departmentOptions"
:normalizer="normalizer"
placeholder="选择部门"
clearable
/>
</el-form-item>
<el-form-item label="姓名">
<el-input
v-model="queryParams.name"
placeholder="请输入姓名"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="handleQuery">查询</el-button>
<el-button size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="memberList"
row-key="id"
border
>
<el-table-column type="index" label="序号" width="50" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="deptName" label="所属部门" />
<el-table-column prop="position" label="职位" />
<el-table-column prop="phone" label="联系电话" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'info'">
{{ scope.row.status === '1' ? '在职' : '离职' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 30, 40]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<!-- 添加或修改成员对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<treeselect
v-model="form.deptId"
:options="departmentOptions"
:normalizer="normalizer"
placeholder="选择部门"
/>
</el-form-item>
<el-form-item label="职位" prop="position">
<el-input v-model="form.position" placeholder="请输入职位" />
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="1">在职</el-radio>
<el-radio label="0">离职</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
name: 'Member',
components: { Treeselect },
data() {
return {
//
loading: true,
//
total: 0,
//
memberList: [],
//
departmentOptions: [],
//
title: '',
//
open: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
name: undefined,
deptId: undefined
},
//
form: {},
//
rules: {
name: [
{ required: true, message: '姓名不能为空', trigger: 'blur' }
],
deptId: [
{ required: true, message: '所属部门不能为空', trigger: 'change' }
]
}
}
},
mounted() {
this.getList()
this.getDepartmentOptions()
},
methods: {
/** 查询成员列表 */
getList() {
this.loading = true
// API
setTimeout(() => {
this.memberList = [
{
id: 1,
name: '张三',
deptName: '技术部',
position: '工程师',
phone: '13800138000',
email: 'zhangsan@example.com',
status: '1'
}
]
this.total = 1
this.loading = false
}, 500)
},
/** 获取部门选项 */
getDepartmentOptions() {
// API
this.departmentOptions = [
{
id: 1,
name: '总公司',
children: [
{
id: 2,
name: '技术部'
}
]
}
]
},
/** 转换部门数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.id,
label: node.name,
children: node.children
}
},
/** 取消按钮 */
cancel() {
this.open = false
this.reset()
},
/** 表单重置 */
reset() {
this.form = {
id: undefined,
name: undefined,
deptId: undefined,
position: undefined,
phone: undefined,
email: undefined,
status: '1'
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加成员'
},
/** 修改按钮操作 */
handleEdit(row) {
this.reset()
this.form = row
this.open = true
this.title = '修改成员'
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除姓名为"' + row.name + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// API
this.$message.success('删除成功')
this.getList()
})
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
// API
this.$message.success('操作成功')
this.open = false
this.getList()
}
})
},
/** 分页大小改变 */
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getList()
},
/** 分页页码改变 */
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getList()
}
}
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
</style>

23
pc/vue.config.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
productionSourceMap: false,
devServer: {
port: 8080,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
}
}