校区添加 #150

Merged
pc8xi2fbj merged 9 commits from zhanghongwei_branch into develop 2 weeks ago

@ -69,42 +69,43 @@ public class AdminService {
}
/**
* /
*
*/
public Admin saveAdmin(Admin admin) {
admin.setUpdatedTime(LocalDateTime.now());
if (admin.getCreatedTime() == null) {
admin.setCreatedTime(LocalDateTime.now());
}
* /
*
*/
public Admin saveAdmin(Admin admin) {
admin.setUpdatedTime(LocalDateTime.now());
if (admin.getCreatedTime() == null) {
admin.setCreatedTime(LocalDateTime.now());
}
// 区域管理员ROLE_AREA_ADMIN的专属校验逻辑
if (admin.getRole() == Admin.AdminRole.ROLE_AREA_ADMIN) {
// 1. 若未填写区域IDnull/空字符串),直接放行(支持先创建管理员,后续补填)
if (admin.getAreaId() == null || admin.getAreaId().trim().isEmpty()) {
admin.setAreaId(null); // 统一置为null避免空字符串冗余数据
// 无需校验,直接允许保存
} else {
// 2. 若填写了区域ID进行严格校验区域存在 + 类型为校区(禁止市区)
String areaId = admin.getAreaId().trim();
// 校验区域是否存在
Area targetArea = areaRepository.findById(areaId)
.orElseThrow(() -> new RuntimeException("关联的区域不存在:" + areaId));
// 核心校验:仅允许关联校区,禁止关联市区
if (Area.AreaType.zone.equals(targetArea.getAreaType())) {
throw new RuntimeException("区域管理员仅允许关联校区,不能关联市区,请重新选择");
}
// 校验通过保留填写的合法校区ID
admin.setAreaId(areaId);
}
// 区域管理员ROLE_AREA_ADMIN的专属校验逻辑
if (admin.getRole() == Admin.AdminRole.ROLE_AREA_ADMIN) {
// 1. 若未填写区域IDnull或空字符串直接放行支持先创建管理员后续补填
if (admin.getAreaId() == null || admin.getAreaId().trim().isEmpty()) {
admin.setAreaId(null); // 统一置为null避免空字符串冗余数据
// 无需校验,直接允许保存
} else {
// 非区域管理员清空区域ID避免冗余数据
admin.setAreaId(null);
// 2. 若填写了区域ID进行严格校验区域存在 + 类型为校区(禁止市区)
String areaId = admin.getAreaId().trim();
// 校验区域是否存在
Area targetArea = areaRepository.findById(areaId)
.orElseThrow(() -> new RuntimeException("关联的区域不存在:" + areaId));
// 核心校验:仅允许关联校区,禁止关联市区
if (Area.AreaType.zone.equals(targetArea.getAreaType())) {
throw new RuntimeException("区域管理员仅允许关联校区,不能关联市区,请重新选择");
}
// 校验通过保留填写的合法校区ID
admin.setAreaId(areaId);
}
return adminRepository.save(admin);
} else {
// 非区域管理员清空区域ID避免冗余数据
admin.setAreaId(null);
}
return adminRepository.save(admin);
}
/**
*
*/

@ -1,4 +1,3 @@
<!-- src/views/area/Campus.vue -->
<template>
<div class="campus-page">
<!-- 页面标题和面包屑 -->
@ -12,6 +11,20 @@
<!-- 新增校区按钮 -->
<button class="btn-add" @click="handleAddCampus"></button>
<!-- 父片区筛选 -->
<div class="filter-box">
<label>所属市区</label>
<select
v-model="selectedCityFilter"
class="area-select"
@change="handleCityFilterChange"
>
<option value="">全部市区</option>
<option v-for="city in cityList" :key="city.areaId" :value="city.areaId">
{{ city.areaName }}
</option>
</select>
</div>
</div>
<!-- 校区表格 -->
@ -20,6 +33,7 @@
<thead>
<tr>
<th>校区名称</th>
<th>所属市区</th> <!-- 新增列显示所属市区 -->
<th>地址</th>
<th>负责人</th>
<th>联系电话</th>
@ -30,11 +44,18 @@
<tbody>
<tr v-for="campus in filteredCampus" :key="campus.areaId">
<td>{{ campus.areaName }}</td>
<td>{{ getCityName(campus.parentAreaId) }}</td> <!-- 显示所属市区名称 -->
<td>{{ campus.address }}</td>
<td>{{ campus.manager }}</td>
<td>{{ getManagerName(campus.manager) }}</td>
<td>{{ campus.managerPhone }}</td>
<td>{{ formatDate(campus.createdTime) }}</td>
<td class="operation-buttons">
<button
class="btn-stats"
@click="handleViewStats(campus.areaId, campus.areaName)"
>
统计
</button>
<button
class="btn-edit"
@click="handleEdit(campus)"
@ -50,7 +71,7 @@
</td>
</tr>
<tr v-if="filteredCampus.length === 0">
<td colspan="6" class="no-data">
<td colspan="8" class="no-data">
{{ loading ? '正在加载数据...' : '暂无校区数据' }}
</td>
</tr>
@ -169,6 +190,84 @@
</div>
</div>
</div>
<!-- 设备统计弹窗 -->
<div class="modal-mask" v-if="showStatsModal">
<div class="modal-container stats-modal">
<div class="modal-header">
<h3>设备统计信息</h3>
<button class="close-btn" @click="showStatsModal = false">×</button>
</div>
<div class="modal-body">
<div v-if="loadingStats" class="loading-stats">
正在加载统计信息...
</div>
<div v-else-if="currentAreaStats" class="stats-content">
<div class="stats-header">
<h4>{{ currentAreaStats.areaName }}设备统计</h4>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ currentAreaStats.totalDeviceCount || 0 }}</div>
<div class="stat-label">总设备数</div>
</div>
<div class="stat-card">
<div class="stat-value online">{{ currentAreaStats.onlineDeviceCount || 0 }}</div>
<div class="stat-label">在线设备</div>
</div>
<div class="stat-card">
<div class="stat-value offline">{{ currentAreaStats.offlineDeviceCount || 0 }}</div>
<div class="stat-label">离线设备</div>
</div>
<div class="stat-card">
<div class="stat-value fault">{{ currentAreaStats.faultDeviceCount || 0 }}</div>
<div class="stat-label">故障设备</div>
</div>
<div class="stat-card">
<div class="stat-value maker">{{ currentAreaStats.waterMakerCount || 0 }}</div>
<div class="stat-label">制水机</div>
</div>
<div class="stat-card">
<div class="stat-value supply">{{ currentAreaStats.waterSupplyCount || 0 }}</div>
<div class="stat-label">供水机</div>
</div>
</div>
<div class="stats-details">
<h5>详细统计</h5>
<div class="detail-item">
<span>在线率:</span>
<span class="detail-value">
{{
currentAreaStats.totalDeviceCount > 0
? ((currentAreaStats.onlineDeviceCount / currentAreaStats.totalDeviceCount) * 100).toFixed(2) + '%'
: '0%'
}}
</span>
</div>
<div class="detail-item">
<span>故障率:</span>
<span class="detail-value">
{{
currentAreaStats.totalDeviceCount > 0
? ((currentAreaStats.faultDeviceCount / currentAreaStats.totalDeviceCount) * 100).toFixed(2) + '%'
: '0%'
}}
</span>
</div>
</div>
</div>
<div v-else class="no-stats-data">
暂无统计信息
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="showStatsModal = false">关闭</button>
</div>
</div>
</div>
</div>
</div>
</template>
@ -207,22 +306,38 @@ interface Area {
updatedTime?: Date
}
//
interface AreaDeviceStatsVO {
areaId: string
areaName: string
totalDeviceCount: number
onlineDeviceCount: number
offlineDeviceCount: number
faultDeviceCount: number
waterMakerCount: number
waterSupplyCount: number
}
//
const campusList = ref<Area[]>([])
const cityList = ref<Area[]>([])
const campusList = ref<Area[]>([]) //
const cityList = ref<Area[]>([]) //
const adminList = ref<Admin[]>([])
const selectedManager = ref<Admin | null>(null)
const selectedCityFilter = ref('') // ID
const selectedCampus = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const showModal = ref(false)
const isEdit = ref(false)
const showDeleteConfirm = ref(false)
const showStatsModal = ref(false)
const currentAreaStats = ref<AreaDeviceStatsVO | null>(null)
const deleteCampusId = ref('')
const deleteCampusName = ref('')
const loading = ref(false) //
const saving = ref(false) //
const deleting = ref(false) //
const loadingStats = ref(false) //
//
const formData = ref<Area>({
@ -244,13 +359,20 @@ const formatDate = (date: Date | undefined) => {
return d.toLocaleDateString('zh-CN')
}
//
//
const getCityName = (cityId: string | null) => {
if (!cityId) return '未知市区'
const city = cityList.value.find(c => c.areaId === cityId)
return city ? city.areaName : '未知市区'
}
// -
const filteredCampus = computed(() => {
let filtered = [...campusList.value]
//
if (selectedCampus.value) {
filtered = filtered.filter(campus => campus.areaId === selectedCampus.value)
//
if (selectedCityFilter.value) {
filtered = filtered.filter(campus => campus.parentAreaId === selectedCityFilter.value)
}
//
@ -261,13 +383,16 @@ const filteredCampus = computed(() => {
//
const totalPages = computed(() => {
const filteredCount = selectedCampus.value
? campusList.value.filter(campus => campus.areaId === selectedCampus.value).length
: campusList.value.length
let filteredCount = campusList.value.length
if (selectedCityFilter.value) {
filteredCount = campusList.value.filter(campus => campus.parentAreaId === selectedCityFilter.value).length
}
return Math.ceil(filteredCount / pageSize.value)
})
//
//
const fetchCampusList = async () => {
loading.value = true
try {
@ -278,8 +403,7 @@ const fetchCampusList = async () => {
return
}
//
// 1:
//
const citiesResponse = await request<{
code: number
msg: string
@ -294,11 +418,12 @@ const fetchCampusList = async () => {
return
}
const cities = citiesResponse.data
const allCampuses: Area[] = []
//
cityList.value = citiesResponse.data
//
for (const city of cities) {
const allCampuses: Area[] = []
for (const city of citiesResponse.data) {
try {
const campusResponse = await request<{
code: number
@ -332,7 +457,7 @@ const fetchCampusList = async () => {
}
}
//
// fetchCampusList
const fetchCityList = async () => {
try {
const token = authStore.token
@ -368,7 +493,7 @@ const fetchCityList = async () => {
}
//
//
// -
const fetchAdminList = async () => {
try {
const token = authStore.token
@ -378,41 +503,41 @@ const fetchAdminList = async () => {
return
}
//
// 使
const response = await request<{
code: number
msg: string
data: Admin[]
}>('/api/web/admin/list', {
}>('/api/web/admin/available-area-admins', {
method: 'GET',
})
if (response?.code === 200 && response?.data) {
//
//
const areaAdmins = response.data.filter(admin =>
admin.role === 'AREA_ADMIN' || admin.role === 'ROLE_AREA_ADMIN'
)
adminList.value = areaAdmins
} else {
console.error('获取管理员列表失败:', response?.msg || '未知错误')
alert(`获取管理员列表失败:${response?.msg || '未知错误'}`)
console.error('获取可分配校区的区域管理员失败:', response?.msg || '未知错误')
alert(`获取可分配校区的区域管理员失败:${response?.msg || '未知错误'}`)
}
} catch (error: any) {
console.error('获取管理员列表异常:', error)
console.error('获取可分配校区的区域管理员异常:', error)
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取管理员列表失败,请稍后重试'
alert(`获取管理员列表失败:${errorMsg}`)
: error.message || '获取可分配校区的区域管理员失败,请稍后重试'
alert(`获取可分配校区的区域管理员失败:${errorMsg}`)
}
}
//
// - ID
const onManagerChange = () => {
if (selectedManager.value) {
formData.value.manager = selectedManager.value.adminName
formData.value.manager = selectedManager.value.adminId // ID
formData.value.managerPhone = selectedManager.value.phone
} else {
formData.value.manager = ''
@ -420,6 +545,11 @@ const onManagerChange = () => {
}
}
//
const handleCityFilterChange = () => {
currentPage.value = 1 //
}
//
const handleAddCampus = () => {
isEdit.value = false
@ -428,7 +558,7 @@ const handleAddCampus = () => {
areaId: '',
areaName: '',
areaType: 'campus',
parentAreaId: null, // null
parentAreaId: null,
address: '',
manager: '',
managerPhone: '',
@ -445,9 +575,15 @@ const handleEdit = (campus: Area) => {
formData.value = { ...campus }
//
const matchedAdmin = adminList.value.find(admin => admin.adminName === campus.manager)
const matchedAdmin = adminList.value.find(admin => admin.adminId === campus.manager)
selectedManager.value = matchedAdmin || null
//
if (!selectedManager.value) {
const matchedByName = adminList.value.find(admin => admin.adminName === campus.manager)
selectedManager.value = matchedByName || null
}
showModal.value = true
}
@ -497,7 +633,6 @@ const confirmDelete = async () => {
}
}
//
//
const handleSave = async () => {
saving.value = true
@ -515,6 +650,12 @@ const handleSave = async () => {
return
}
//
if (selectedManager.value.role !== 'ROLE_AREA_ADMIN' && selectedManager.value.role !== 'AREA_ADMIN') {
alert('所选负责人必须是区域管理员角色');
return;
}
let response
if (isEdit.value) {
@ -523,20 +664,19 @@ const handleSave = async () => {
code: number
msg: string
data: Area
}>('/api/web/area/update', {
method: 'PUT',
}>(`/api/web/area/update/${formData.value.areaId}`, { // URLareaId
method: 'PUT', // 使PUT
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}`
'Authorization': `Bearer ${authStore.token}` //
},
body: JSON.stringify({
areaId: formData.value.areaId,
areaName: formData.value.areaName,
areaType: 'campus',
parentAreaId: formData.value.parentAreaId,
address: formData.value.address,
manager: formData.value.manager,
managerPhone: formData.value.managerPhone
manager: selectedManager.value.adminId, // 使ID
managerPhone: selectedManager.value.phone
})
})
@ -571,11 +711,11 @@ const handleSave = async () => {
areaType: 'campus',
parentAreaId: formData.value.parentAreaId,
address: formData.value.address,
manager: formData.value.manager,
managerPhone: formData.value.managerPhone
manager: selectedManager.value.adminId, // 使ID
managerPhone: selectedManager.value.phone
}
console.log('发送的校区数据:', newCampus) //
console.log('发送的校区数据:', newCampus)
response = await request<{
code: number
@ -585,7 +725,7 @@ const handleSave = async () => {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}`
'Authorization': `Bearer ${authStore.token}` //
},
body: JSON.stringify(newCampus)
})
@ -605,19 +745,75 @@ const handleSave = async () => {
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '保存失败,请稍后重试'
: error.message.includes('400')
? '请求参数错误,请检查输入数据'
: error.message || '保存失败,请稍后重试'
alert(`保存校区失败:${ errorMsg}`)
} finally {
saving.value = false
}
}
// ID
const getManagerName = (managerId: string) => {
if (!managerId) return '未分配'
const admin = adminList.value.find(admin => admin.adminId === managerId)
return admin ? admin.adminName : '未知负责人'
}
//
const fetchAreaDeviceStats = async (areaId: string) => {
loadingStats.value = true
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const response = await request<{
code: number
msg: string
data: AreaDeviceStatsVO
}>(`/api/web/area/device-stats/${areaId}`, {
method: 'GET',
})
if (response?.code === 200 && response?.data) {
currentAreaStats.value = response.data
showStatsModal.value = true
} else {
const errorMsg = response?.msg || `获取统计信息失败(错误码:${response?.code || '未知'}`
console.error('获取统计信息失败:', errorMsg)
alert(`获取统计信息失败:${errorMsg}`)
}
} catch (error: any) {
console.error('获取统计信息异常:', error)
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取统计信息失败,请稍后重试'
alert(`获取统计信息失败:${errorMsg}`)
} finally {
loadingStats.value = false
}
}
//
const handleViewStats = (areaId: string, areaName: string) => {
document.title = `查看${areaName}统计信息`
fetchAreaDeviceStats(areaId)
}
//
onMounted(async () => {
console.log('Token:', authStore.token)
await fetchCityList() //
await fetchAdminList() //
fetchCampusList()
fetchCampusList() //
})
</script>
@ -742,6 +938,22 @@ onMounted(async () => {
opacity: 0.9;
}
/* 统计按钮样式 */
.btn-stats {
background-color: #e6f7ff;
color: #1890ff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
border: none;
transition: opacity 0.3s;
}
.btn-stats:hover {
opacity: 0.9;
}
.no-data {
text-align: center;
padding: 40px 0;
@ -902,6 +1114,124 @@ onMounted(async () => {
cursor: not-allowed;
}
/* 在现有样式基础上添加筛选框样式 */
.filter-box {
display: flex;
align-items: center;
gap: 8px;
color: #666;
}
.area-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 180px;
font-size: 14px;
}
.filter-box label {
white-space: nowrap;
}
/* 统计弹窗样式 */
.stats-modal {
width: 600px;
max-width: 90%;
}
.stats-content {
padding: 16px 0;
}
.stats-header {
margin-bottom: 20px;
text-align: center;
}
.stats-header h4 {
margin: 0;
color: #333;
font-size: 18px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
text-align: center;
padding: 16px;
border: 1px solid #f0f0f0;
border-radius: 8px;
background-color: #fafafa;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.stat-value.online {
color: #52c41a;
}
.stat-value.offline {
color: #faad14;
}
.stat-value.fault {
color: #ff4d4f;
}
.stat-value.maker {
color: #722ed1;
}
.stat-value.supply {
color: #13c2c2;
}
.stat-label {
font-size: 14px;
color: #666;
}
.stats-details {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
.stats-details h5 {
margin: 0 0 12px 0;
color: #333;
font-size: 16px;
}
.detail-item {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f8f8f8;
}
.detail-value {
font-weight: 500;
color: #1890ff;
}
.loading-stats,
.no-stats-data {
text-align: center;
padding: 40px 0;
color: #8c8c8c;
}
/* 响应式调整 */
@media (max-width: 768px) {
.action-bar {
@ -916,5 +1246,9 @@ onMounted(async () => {
.campus-select, .area-select {
width: 100%;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

@ -11,16 +11,6 @@
<!-- 新增片区按钮 -->
<button class="btn-add" @click="handleAddArea"></button>
<!-- 片区下拉筛选 -->
<div class="filter-box">
<label>选择片区</label>
<select v-model="selectedArea" @change="handleAreaChange" class="area-select">
<option value="">全部片区</option>
<option v-for="area in areaList" :key="area.areaId" :value="area.areaId">
{{ area.areaName }}
</option>
</select>
</div>
</div>
<!-- 片区表格 -->
@ -354,7 +344,6 @@ const confirmDelete = async () => {
}
//
// handleSave
const handleSave = async () => {
saving.value = true
try {
@ -368,14 +357,25 @@ const handleSave = async () => {
let response
if (isEdit.value) {
// -
// - URLareaId
response = await request<{
code: number
msg: string
data: Area
}>('/api/web/area/update', {
}>(`/api/web/area/update/${formData.value.areaId}`, { // URL
method: 'PUT',
body: JSON.stringify(formData.value)
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}` //
},
body: JSON.stringify({
areaName: formData.value.areaName,
areaType: formData.value.areaType,
parentAreaId: formData.value.parentAreaId,
address: formData.value.address,
manager: formData.value.manager,
managerPhone: formData.value.managerPhone
})
})
if (response.code === 200) {
@ -402,6 +402,10 @@ const handleSave = async () => {
data: Area
}>('/api/web/area/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}` //
},
body: JSON.stringify(newArea)
})
@ -428,6 +432,8 @@ const handleSave = async () => {
}
//
onMounted(() => {
console.log('Token:', authStore.token)

@ -1,43 +1,61 @@
<template>
<div class="water-maker-page">
<!-- 页面标题和面包屑 -->
<div class="page-header">
<h2>制水机管理</h2>
<div class="breadcrumb">校园矿化水平台 / 设备监控 / 制水机</div>
</div>
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add" @click="showAddModal = true">添加制水机</button>
<div class="filters">
<!-- 搜索框 -->
<div class="search-box">
<input
type="text"
placeholder="搜索设备ID或位置..."
v-model="searchKeyword"
@input="handleSearch"
v-model="searchKeyword"
type="text"
placeholder="搜索设备ID或位置..."
@input="handleSearch"
>
<button class="search-btn" @click="handleSearch"></button>
</div>
<!-- 片区筛选 -->
<select
v-model="selectedArea"
<!-- 两层筛选市区选择影响校区列表 -->
<div class="area-filter">
<select
v-model="selectedCity"
class="filter-select"
@change="handleSearch"
>
<option value="">全部片区</option>
<option value="A">A</option>
<option value="B">B</option>
</select>
@change="onCityChange"
>
<option value="">选择市区</option>
<option
v-for="city in cityList"
:key="city.areaId"
:value="city.areaId"
>
{{ city.areaName }}
</option>
</select>
<!-- 状态筛选 -->
<select
v-model="selectedStatus"
<select
v-model="selectedCampus"
class="filter-select"
@change="handleSearch"
:disabled="!selectedCity"
@change="onCampusChange"
>
<option value="">选择校区</option>
<option
v-for="campus in campusList"
:key="campus.areaId"
:value="campus.areaId"
>
{{ campus.areaName }}
</option>
</select>
</div>
<select
v-model="selectedStatus"
class="filter-select"
@change="currentPage = 1"
>
<option value="">全部状态</option>
<option value="online">在线</option>
@ -48,56 +66,55 @@
</div>
</div>
<!-- 设备表格 - 新增设备机型列 -->
<div class="card">
<table class="equipment-table">
<thead>
<tr>
<th>设备ID</th>
<th>设备机型</th> <!-- 新增机型列 -->
<th>所属片区</th>
<th>详细位置</th>
<th>状态</th>
<th>最后上传时间</th>
<th>操作</th>
</tr>
<tr>
<th>设备ID</th>
<th>设备名称</th>
<th>设备类型</th>
<th>所属片区</th>
<th>安装位置</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="device in paginatedDevices" :key="device.deviceId">
<td>{{ device.deviceId }}</td>
<td>{{ device.deviceType === 'water_maker' ? '制水机' : device.deviceType }}</td>
<td>{{ device.areaId }}</td>
<td>{{ device.installLocation }}</td>
<td>
<span :class="`status-tag ${device.status}`">
{{ formatStatus(device.status) }}
</span>
</td>
<td>{{ formatDate(device.lastHeartbeatTime) }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewDevice(device.deviceId)"></button>
<button class="btn-edit" @click="openEditModal(device)"></button>
<button
<tr v-for="device in paginatedDevices" :key="device.deviceId">
<td>{{ device.deviceId }}</td>
<td>{{ device.deviceName }}</td>
<td>{{ device.deviceType }}</td>
<td>{{ device.areaId }}</td>
<td>{{ device.installLocation }}</td>
<td>
<span :class="`status-tag ${device.status}`">
{{ formatStatus(device.status) }}
</span>
</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewDevice(device.deviceId)"></button>
<button class="btn-edit" @click="openEditModal(device)"></button>
<button
class="btn-delete"
@click="deleteDevice(device.deviceId)"
>
删除
</button>
</td>
</tr>
<tr v-if="paginatedDevices.length === 0">
<td colspan="7" class="no-data">暂无设备数据</td>
</tr>
:disabled="device.status === 'online'"
>
删除
</button>
</td>
</tr>
<tr v-if="paginatedDevices.length === 0">
<td colspan="7" class="no-data">暂无设备数据</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
@ -105,9 +122,9 @@
{{ currentPage }} / {{ totalPages }} ( {{ filteredDevices.length }} 条记录)
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
@ -273,7 +290,7 @@
<form @submit.prevent="confirmFault">
<div class="form-group">
<label>故障类型:</label>
<input v-model="faultInfo.faultType" type="text" placeholder="请输入故障类型" required>
<textarea v-model="faultInfo.faultType" placeholder="请输入故障类型" required></textarea>
</div>
<div class="form-group">
<label>故障描述:</label>
@ -297,7 +314,7 @@ import { request } from '@/api/request'
import type { ResultVO } from '@/api/types/auth'
//
type DeviceStatus = 'online' | 'offline' | 'fault'
type DeviceStatus = 'online' | 'offline' | 'warning' | 'fault'
//
interface WaterMakerDevice {
@ -307,8 +324,7 @@ interface WaterMakerDevice {
areaId: string
installLocation: string
status: DeviceStatus
lastHeartbeatTime?: string
createTime?: string
// lastHeartbeatTime
}
//
@ -325,7 +341,8 @@ interface Area {
//
const devices = ref<WaterMakerDevice[]>([])
const searchKeyword = ref('')
const selectedArea = ref('') //
const selectedCity = ref('') //
const selectedCampus = ref('') //
const selectedStatus = ref('') //
const currentPage = ref(1)
const pageSize = 10 //
@ -390,9 +407,25 @@ const loadDevices = async (): Promise<void> => {
console.log('开始加载制水机设备数据...')
//
const params = new URLSearchParams()
if (selectedStatus.value && selectedStatus.value !== '') {
params.append('status', selectedStatus.value)
}
//
if (selectedCampus.value && selectedCampus.value !== '') {
params.append('areaId', selectedCampus.value)
} else if (selectedCity.value && selectedCity.value !== '') {
params.append('areaId', selectedCity.value)
}
params.append('deviceType', 'water_maker')
const queryString = params.toString()
const url = `/api/web/device-status/by-type${queryString ? `?${queryString}` : ''}`
//
const result = await request<ResultVO<WaterMakerDevice[]>>(
`/api/web/device-status/by-type?deviceType=water_maker`,
url,
{ method: 'GET' }
)
@ -406,8 +439,8 @@ const loadDevices = async (): Promise<void> => {
deviceType: item.deviceType,
areaId: item.areaId,
installLocation: item.installLocation,
status: item.status,
lastHeartbeatTime: item.lastHeartbeatTime
status: item.status
// lastHeartbeatTime
}))
}
@ -499,6 +532,20 @@ const loadCampusListByCity = async (cityId: string): Promise<void> => {
}
}
//
const onCityChange = async () => {
//
selectedCampus.value = ''
campusList.value = []
if (selectedCity.value) {
await loadCampusListByCity(selectedCity.value)
} else {
//
campusList.value = []
}
}
// ID
const loadEditCampusListByCity = async (cityId: string): Promise<void> => {
try {
@ -537,18 +584,8 @@ const loadEditCampusListByCity = async (cityId: string): Promise<void> => {
}
}
//
const onCityChange = async () => {
// ID
selectedCampusId.value = ''
campusList.value = []
if (selectedCityId.value) {
await loadCampusListByCity(selectedCityId.value)
}
}
//
//
const onCampusChange = () => {
// areaIdareaNameareaId
const selectedCampus = campusList.value.find(campus => campus.areaId === selectedCampusId.value)
@ -595,7 +632,18 @@ const filteredDevices = computed(() => {
device.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
device.installLocation.toLowerCase().includes(searchKeyword.value.toLowerCase())
const areaMatch = selectedArea.value === '' || device.areaId === selectedArea.value
//
let areaMatch = true
if (selectedCampus.value && selectedCampus.value !== '') {
areaMatch = device.areaId === selectedCampus.value ||
device.areaId === campusList.value.find(c => c.areaId === selectedCampus.value)?.areaName
} else if (selectedCity.value && selectedCity.value !== '') {
//
areaMatch = campusList.value.some(campus =>
device.areaId === campus.areaId || device.areaId === campus.areaName
) || device.areaId === selectedCity.value
}
const statusMatch = selectedStatus.value === '' || device.status === selectedStatus.value
return keywordMatch && areaMatch && statusMatch
@ -618,21 +666,16 @@ const formatStatus = (status: DeviceStatus): string => {
const statusMap: Record<string, string> = {
online: '在线',
offline: '离线',
warning: '警告',
fault: '故障'
}
return statusMap[status] || status
}
//
const formatDate = (dateString?: string): string => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
//
const handleSearch = () => {
currentPage.value = 1 //
loadDevices() //
}
//
@ -640,8 +683,6 @@ const viewDevice = (id: string) => {
router.push(`/home/equipment/water-maker/${id}`)
}
// 线
//
// 线
const confirmOffline = async () => {
try {
@ -716,7 +757,6 @@ const confirmFault = async () => {
}
}
// 线
//
const deleteDevice = async (deviceId: string) => {
if (!confirm(`确定要删除设备 ${deviceId} 吗?此操作不可恢复。`)) {
@ -923,13 +963,13 @@ const addDevice = async () => {
//
onMounted(async () => {
console.log('🚀 开始加载设备数据...')
await loadDevices()
await loadCityList()
await loadDevices() //
})
</script>
<style scoped>
/* 样式与供水机页面保持一致 */
/* 样式与终端机页面保持一致 */
.water-maker-page {
padding: 20px;
}
@ -978,6 +1018,7 @@ onMounted(async () => {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
@ -1001,12 +1042,18 @@ onMounted(async () => {
cursor: pointer;
}
.area-filter {
display: flex;
gap: 8px;
}
.filter-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
min-width: 120px;
}
.equipment-table {
@ -1032,6 +1079,34 @@ onMounted(async () => {
background-color: #f8f9fa;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.online {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.offline {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.fault {
background-color: #ffebe6;
color: #cf1322;
}
.operation-buttons {
display: flex;
gap: 8px;
@ -1062,13 +1137,13 @@ onMounted(async () => {
}
.btn-edit {
background-color: #e6f7ff;
color: #1890ff;
background-color: #faad14;
color: white;
}
.btn-delete {
background-color: #ffebe6;
color: #ff4d4f;
background-color: #ff4d4f;
color: white;
}
.no-data {
@ -1206,10 +1281,14 @@ onMounted(async () => {
width: 100%;
}
.search-box, .filter-select {
.search-box, .area-filter, .filter-select {
width: 100%;
}
.area-filter {
flex-direction: column;
}
.modal-content {
width: 90%;
min-width: auto;

@ -22,18 +22,39 @@
<button class="search-btn">搜索</button>
</div>
<!-- 片区筛选 -->
<select
v-model="selectedArea"
class="filter-select"
@change="handleSearch"
>
<option value="">全部片区</option>
<option value="A">A区</option>
<option value="B">B区</option>
<option value="C">C区</option>
<option value="D">D区</option>
</select>
<!-- 两层筛选市区选择影响校区列表 -->
<div class="area-filter">
<select
v-model="selectedCity"
class="filter-select"
@change="onCityChange"
>
<option value="">选择市区</option>
<option
v-for="city in cityList"
:key="city.areaId"
:value="city.areaId"
>
{{ city.areaName }}
</option>
</select>
<select
v-model="selectedCampus"
class="filter-select"
:disabled="!selectedCity"
@change="onCampusChange"
>
<option value="">选择校区</option>
<option
v-for="campus in campusList"
:key="campus.areaId"
:value="campus.areaId"
>
{{ campus.areaName }}
</option>
</select>
</div>
<!-- 状态筛选 -->
<select
@ -127,10 +148,13 @@
<label>所属片区:</label>
<select v-model="newDevice.areaId" @change="loadAvailableMakers" required>
<option value="">请选择片区</option>
<option value="A">A区</option>
<option value="B">B区</option>
<option value="C">C区</option>
<option value="D">D区</option>
<option
v-for="area in cityList"
:key="area.areaId"
:value="area.areaId"
>
{{ area.areaName }}
</option>
</select>
</div>
<div class="form-group">
@ -172,10 +196,13 @@
<label>所属片区:</label>
<select v-model="editingDevice.areaId" @change="loadAvailableMakersForEdit" required>
<option value="">请选择片区</option>
<option value="A">A区</option>
<option value="B">B区</option>
<option value="C">C区</option>
<option value="D">D区</option>
<option
v-for="area in cityList"
:key="area.areaId"
:value="area.areaId"
>
{{ area.areaName }}
</option>
</select>
</div>
<div class="form-group">
@ -243,16 +270,32 @@ interface DeviceDetail {
parentMakerId?: string
}
//
interface Area {
areaId: string
areaName: string
areaType: string
parentAreaId: string | null
address: string
manager: string
managerPhone: string
}
//
const devices = ref<WaterSupplierDevice[]>([])
const searchKeyword = ref('')
const selectedArea = ref('') //
const selectedCity = ref('') //
const selectedCampus = ref('') //
const selectedStatus = ref('') //
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore()
//
const cityList = ref<Area[]>([]) //
const campusList = ref<Area[]>([]) //
//
const showAddModal = ref(false)
const newDevice = ref({
@ -283,6 +326,103 @@ const availableMakersForEdit = ref<{id: string, name: string}[]>([])
const showDeleteModal = ref(false)
const currentDeviceId = ref('')
//
const loadCityList = async (): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
await router.push('/login')
return
}
console.log('开始加载市区列表...')
const result = await request<any>('/api/web/area/cities', { method: 'GET' })
if (result && typeof result === 'object' && 'code' in result) {
if (result.code === 200 && result.data && Array.isArray(result.data)) {
cityList.value = result.data
console.log(`获取到${cityList.value.length}个市区`)
} else {
console.warn('API响应非成功状态或数据格式错误:', result)
cityList.value = []
}
} else if (Array.isArray(result)) {
cityList.value = result
} else {
console.warn('API响应数据格式错误:', result)
cityList.value = []
}
} catch (error) {
console.error('加载市区列表失败:', error)
cityList.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
await router.push('/login')
}
}
}
// ID
const loadCampusListByCity = async (cityId: string): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
await router.push('/login')
return
}
console.log(`开始加载市区 ${cityId} 的校区列表...`)
const result = await request<any>(`/api/web/area/campuses/${cityId}`, { method: 'GET' })
if (result && typeof result === 'object' && 'code' in result) {
if (result.code === 200 && result.data && Array.isArray(result.data)) {
campusList.value = result.data
console.log(`获取到${campusList.value.length}个校区`)
} else {
console.warn('API响应非成功状态或数据格式错误:', result)
campusList.value = []
}
} else if (Array.isArray(result)) {
campusList.value = result
} else {
console.warn('API响应数据格式错误:', result)
campusList.value = []
}
} catch (error) {
console.error('加载校区列表失败:', error)
campusList.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
await router.push('/login')
}
}
}
//
const onCityChange = async () => {
//
selectedCampus.value = ''
campusList.value = []
if (selectedCity.value) {
await loadCampusListByCity(selectedCity.value)
} else {
//
campusList.value = []
}
}
//
const onCampusChange = () => {
//
currentPage.value = 1
loadWaterSuppliers()
}
//
const loadWaterSuppliers = async () => {
try {
@ -299,8 +439,9 @@ const loadWaterSuppliers = async () => {
if (selectedStatus.value && selectedStatus.value !== '') {
params.append('status', selectedStatus.value);
}
if (selectedArea.value && selectedArea.value !== '') {
params.append('areaId', selectedArea.value);
//
if (selectedCampus.value && selectedCampus.value !== '') {
params.append('areaId', selectedCampus.value)
}
params.append('deviceType', 'water_supply'); //
@ -421,13 +562,21 @@ const loadAvailableMakersForEdit = async () => {
}
//
// filteredDevices
const filteredDevices = computed(() => {
return devices.value.filter(device => {
const keywordMatch = searchKeyword.value.trim() === '' ||
device.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
device.location.toLowerCase().includes(searchKeyword.value.toLowerCase())
const areaMatch = selectedArea.value === '' || device.area === selectedArea.value
//
let areaMatch = true
if (selectedCampus.value && selectedCampus.value !== '') {
//
areaMatch = device.area === selectedCampus.value
}
//
const statusMatch = selectedStatus.value === '' || device.status === selectedStatus.value
return keywordMatch && areaMatch && statusMatch
@ -646,13 +795,14 @@ const deleteDevice = async () => {
}
//
onMounted(() => {
onMounted(async () => {
await loadCityList() //
loadWaterSuppliers()
})
</script>
<style scoped>
/* 样式与制水机页面保持一致 */
/* 样式与终端机页面保持一致 */
.water-supplier-page {
padding: 20px;
}
@ -678,7 +828,7 @@ onMounted(() => {
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap:wrap;
flex-wrap: wrap;
gap: 16px;
}
@ -697,40 +847,11 @@ onMounted(() => {
background: #359e75;
}
.btn-edit {
background: #e6f7ff;
color: #1890ff;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.3s;
}
.btn-edit:hover {
background: #bae7ff;
}
.btn-delete {
background: #cf1322;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn-delete:hover {
background: #b80c1a;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
@ -751,7 +872,12 @@ onMounted(() => {
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor:pointer;
cursor: pointer;
}
.area-filter {
display: flex;
gap: 8px;
}
.filter-select {
@ -760,11 +886,12 @@ onMounted(() => {
border-radius: 4px;
background: white;
cursor: pointer;
min-width: 120px;
}
.equipment-table {
width: 100%;
border-collapse:collapse;
border-collapse: collapse;
}
.equipment-table th,
@ -785,32 +912,71 @@ onMounted(() => {
background-color: #f8f9fa;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.online {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.offline {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.error {
background-color: #ffebe6;
color: #cf1322;
}
.operation-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.operation-buttons button {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor:pointer;
border:none;
cursor: pointer;
border: none;
transition: opacity 0.3s;
}
.operation-buttons button:hover {
.operation-buttons button:hover:not(:disabled) {
opacity: 0.9;
}
.operation-buttons button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-view {
background-color: #e6f7ff;
color: #1890ff;
}
.btn-edit {
background-color: #faad14;
color: white;
}
.btn-delete {
background-color: #ffebe6;
color: #cf1322;
background-color: #ff4d4f;
color: white;
}
.no-data {
@ -834,7 +1000,7 @@ onMounted(() => {
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor:pointer;
cursor: pointer;
}
.page-btn:disabled {
@ -862,13 +1028,11 @@ onMounted(() => {
border-radius: 8px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
}
.form-group {
@ -877,12 +1041,13 @@ onMounted(() => {
.form-group label {
display: block;
margin-bottom: 8px;
margin-bottom: 4px;
font-weight: 500;
}
.form-group input,
.form-group select {
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
@ -890,6 +1055,11 @@ onMounted(() => {
box-sizing: border-box;
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.form-actions {
display: flex;
justify-content: flex-end;
@ -901,37 +1071,60 @@ onMounted(() => {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
border: 1px solid #ddd;
}
.form-actions button[type="button"] {
background: #f5f5f5;
border: 1px solid #ddd;
color: #333;
}
.form-actions button[type="submit"] {
background: #42b983;
border: none;
color: white;
border: none;
}
.help-text {
font-size: 12px;
/* 新增:下拉框样式 */
.select-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
background: white;
cursor: pointer;
}
.select-input:focus {
outline: none;
border-color: #42b983;
}
/* 新增:无数据提示样式 */
.no-data-message {
margin-top: 8px;
color: #8c8c8c;
margin-top: 4px;
margin-bottom: 0;
font-size: 12px;
}
/* 响应式调整 */
@media (max-width: 768px) {
@media (max-width: 768px) {
.filters {
flex-direction: column;
width: 100%;
}
.search-box, .filter-select {
.search-box, .area-filter, .filter-select {
width: 100%;
}
.area-filter {
flex-direction: column;
}
.modal-content {
width: 90%;
min-width: auto;
}
}
</style>

@ -1,4 +1,3 @@
<!-- src/views/personnel/Admin.vue -->
<template>
<div class="admin-page">
<!-- 页面标题和面包屑 -->
@ -31,16 +30,23 @@
<th>账号</th>
<th>联系电话</th>
<th>身份</th>
<th>关联区域</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="admin in filteredAdmins" :key="admin.adminId">
<tr v-for="admin in paginatedAdmins" :key="admin.adminId">
<td>{{ admin.name }}</td>
<td>{{ admin.account }}</td>
<td>{{ admin.phone }}</td>
<td>{{ formatRole(admin.role) }}</td>
<td>
<span v-if="admin.role === 'ROLE_AREA_ADMIN'" class="area-list">
{{ admin.areaName || (admin.areaId ? getAreaNameById(admin.areaId) : '') || '未关联区域' }}
</span>
<span v-else>-</span>
</td>
<td>
<span :class="`status-tag ${admin.status}`">
{{ admin.status === 'active' ? '启用' : '禁用' }}
@ -68,8 +74,8 @@
</button>
</td>
</tr>
<tr v-if="filteredAdmins.length === 0">
<td colspan="6" class="no-data">暂无管理员数据</td>
<tr v-if="paginatedAdmins.length === 0">
<td colspan="7" class="no-data">暂无管理员数据</td>
</tr>
</tbody>
</table>
@ -85,7 +91,7 @@
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredAdmins.length }} 条记录)
</span>
<button
class="page-btn"
@ -119,12 +125,24 @@
</div>
<div class="form-group">
<label for="role" class="form-label required">身份</label>
<select id="role" v-model="formData.role" required>
<select id="role" v-model="formData.role" @change="handleRoleChange" required>
<option value="ROLE_SUPER_ADMIN">超级管理员</option>
<option value="ROLE_AREA_ADMIN">区域管理员</option>
<option value="ROLE_VIEWER">查看者</option>
</select>
</div>
<!-- 区域选择 - 仅当选择区域管理员时显示改为单选下拉菜单选填 -->
<div class="form-group" v-if="formData.role === 'ROLE_AREA_ADMIN'">
<label class="form-label">关联片区选填</label>
<select v-model="selectedAreaId">
<option value="">请选择片区可选</option>
<option v-for="area in unassignedAreas" :key="area.areaId" :value="area.areaId">
{{ area.areaName }}
</option>
</select>
</div>
<div class="form-group">
<label for="password" class="form-label">初始密码</label>
<input
@ -166,12 +184,21 @@
</div>
<div class="form-group">
<label for="edit-role" class="form-label required">身份</label>
<select id="edit-role" v-model="editFormData.role" required>
<select id="edit-role" v-model="editFormData.role" @change="handleEditRoleChange" required>
<option value="ROLE_SUPER_ADMIN">超级管理员</option>
<option value="ROLE_AREA_ADMIN">区域管理员</option>
<option value="ROLE_VIEWER">查看者</option>
</select>
</div>
<!-- 编辑表单中显示关联区域只读 -->
<div class="form-group" v-if="editFormData.role === 'ROLE_AREA_ADMIN' && originalAdminData?.areaId">
<label class="form-label">关联区域</label>
<div class="readonly-area">
{{ getAreaNameById(originalAdminData.areaId) }}
</div>
</div>
<div class="form-group">
<label for="edit-password" class="form-label">重置密码</label>
<input
@ -193,7 +220,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
@ -202,6 +229,17 @@ import type { ResultVO } from '@/api/types/auth'
//
type AdminStatus = 'active' | 'disabled'
//
interface Area {
areaId: string
areaName: string
areaType: string
parentAreaId: string | null
address: string
manager: string
managerPhone: string
}
//
interface Admin {
adminId: string
@ -210,6 +248,8 @@ interface Admin {
phone: string
role: string
status: AdminStatus
areaId?: string
areaName?: string
}
//
@ -219,6 +259,7 @@ interface FormData {
phone: string
role: string
password?: string
areaId?: string
}
//
@ -229,6 +270,7 @@ interface EditFormData {
phone: string
role: string
password?: string
areaId?: string
}
const authStore = useAuthStore()
@ -243,13 +285,23 @@ const loading = ref(false)
const showAddModal = ref(false)
const showEditModal = ref(false)
//
const unassignedAreas = ref<Area[]>([])
// ID
const selectedAreaId = ref<string>('')
//
const originalAdminData = ref<Admin | null>(null)
//
const formData = ref<FormData>({
name: '',
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
})
//
@ -259,9 +311,39 @@ const editFormData = ref<EditFormData>({
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
})
//
const loadUnassignedAreas = async () => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const response = await request<ResultVO<Area[]>>('/api/web/area/without-manager', {
method: 'GET',
})
if (response.code === 200) {
unassignedAreas.value = response.data || []
} else {
console.error('获取未设置负责人片区列表失败:', response.message)
}
} catch (error: any) {
console.error('请求未设置负责人片区列表异常:', error)
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const fetchAdminList = async () => {
loading.value = true
@ -290,12 +372,14 @@ const fetchAdminList = async () => {
account: admin.adminId || '',
phone: admin.phone || '未知电话',
role: admin.role || '未知角色',
status: 'active' as AdminStatus
status: 'active' as AdminStatus,
areaId: admin.areaId,
areaName: admin.areaName || undefined //
}))
} else {
const errorMsg = response.message || `获取失败(错误码:${response.code}`
console.error('获取管理员列表失败:', errorMsg)
alert(`获取管理员列表失败:${errorMsg}`)
alert(`获取管理员列表失败:${ errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
@ -304,7 +388,7 @@ const fetchAdminList = async () => {
: error.message.includes('Net')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取管理员列表失败:${errorMsg}`)
alert(`获取管理员列表失败:${ errorMsg}`)
if (error.message.includes('401')) {
authStore.logout()
@ -325,6 +409,15 @@ const formatRole = (role: string): string => {
return roleMap[role] || role
}
// ID
// script
const getAreaNameById = (areaId: string | undefined): string => {
if (!areaId) return '未知区域'
const area = unassignedAreas.value.find(a => a.areaId === areaId)
return area ? area.areaName : '未知区域'
}
//
const filteredAdmins = computed(() => {
return admins.value.filter(admin => {
@ -335,7 +428,14 @@ const filteredAdmins = computed(() => {
})
})
//
// -
const paginatedAdmins = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredAdmins.value.slice(start, end)
})
//
const totalPages = computed(() => {
return Math.ceil(filteredAdmins.value.length / pageSize)
})
@ -346,8 +446,23 @@ const handleSearch = () => {
fetchAdminList()
}
// -
const handleRoleChange = () => {
if (formData.value.role !== 'ROLE_AREA_ADMIN') {
selectedAreaId.value = '' //
}
}
// -
const handleEditRoleChange = () => {
if (editFormData.value.role !== 'ROLE_AREA_ADMIN') {
//
}
}
//
onMounted(() => {
onMounted(async () => {
await loadUnassignedAreas()
fetchAdminList()
})
@ -382,6 +497,9 @@ const handleEdit = async (id: string) => {
//
const adminToEdit = admins.value.find(admin => admin.adminId === id);
if (adminToEdit) {
//
originalAdminData.value = { ...adminToEdit };
//
editFormData.value = {
adminId: adminToEdit.adminId,
@ -389,8 +507,10 @@ const handleEdit = async (id: string) => {
account: adminToEdit.account,
phone: adminToEdit.phone,
role: adminToEdit.role,
password: ''
password: '',
areaId: adminToEdit.areaId
};
showEditModal.value = true;
}
} catch (error: any) {
@ -402,13 +522,15 @@ const handleEdit = async (id: string) => {
//
const handleEditSubmit = async () => {
try {
//
const submitData = {
adminId: editFormData.value.adminId,
adminName: editFormData.value.name,
account: editFormData.value.account,
phone: editFormData.value.phone,
role: editFormData.value.role,
password: editFormData.value.password || undefined //
password: editFormData.value.password || undefined,
// areaId: undefined //
};
const response = await request<ResultVO>(`/api/web/admin/save`, {
@ -426,8 +548,10 @@ const handleEditSubmit = async () => {
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
};
originalAdminData.value = null;
//
fetchAdminList();
} else {
@ -468,7 +592,7 @@ const performDelete = async (id: string) => {
} else {
const errorMsg = response.message || `删除失败(错误码:${response.code}`;
console.error('删除管理员失败:', errorMsg);
alert(`删除管理员失败:${errorMsg}`);
alert(`删除管理员失败:${ errorMsg}`);
}
} catch (error: any) {
console.error('请求异常:', error);
@ -477,7 +601,7 @@ const performDelete = async (id: string) => {
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '删除失败,请稍后重试';
alert(`删除管理员失败:${errorMsg}`);
alert(`删除管理员失败:${ errorMsg}`);
if (error.message.includes('401')) {
authStore.logout();
@ -486,13 +610,29 @@ const performDelete = async (id: string) => {
}
}
//
const campusAreas = computed(() => {
return unassignedAreas.value.filter(area => area.areaType === '校园')
})
//
const handleSubmit = async () => {
try {
//
// ID
const submitData = {
...formData.value,
password: formData.value.password || '123456'
adminName: formData.value.name, // name adminName
account: formData.value.account,
phone: formData.value.phone,
role: formData.value.role,
password: formData.value.password || '123456',
// areaId
areaId: selectedAreaId.value ? selectedAreaId.value : undefined
}
//
if (!formData.value.name || !formData.value.account || !formData.value.phone) {
alert('请填写必填项:姓名、账号、联系电话')
return
}
const response = await request<ResultVO>(`/api/web/admin/save`, {
@ -509,8 +649,10 @@ const handleSubmit = async () => {
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
}
selectedAreaId.value = '' //
//
fetchAdminList()
} else {
@ -521,6 +663,7 @@ const handleSubmit = async () => {
alert(`添加管理员失败:${error.message}`)
}
}
</script>
<style scoped>
@ -630,6 +773,11 @@ const handleSubmit = async () => {
color: #8c8c8c;
}
.area-list {
color: #666;
font-size: 12px;
}
.operation-buttons {
display: flex;
gap: 8px;
@ -780,6 +928,25 @@ const handleSubmit = async () => {
cursor: not-allowed;
}
.form-group select[multiple] {
height: 120px;
}
.readonly-area {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f5f5f5;
font-size: 14px;
}
.error-message {
color: #cf1322;
font-size: 12px;
margin-top: 4px;
margin-bottom: 0;
}
.form-actions {
display: flex;
gap: 16px;
@ -829,5 +996,11 @@ const handleSubmit = async () => {
.search-box input {
width: 100%;
}
.admin-table th,
.admin-table td {
padding: 8px 10px;
font-size: 12px;
}
}
</style>

@ -7,6 +7,14 @@
<div class="breadcrumb">校园矿化水平台 / 人员管理 / 维修人员 / 维修记录</div>
</div>
<!-- 返回按钮 -->
<div class="back-button">
<button @click="goBack" class="btn-back">
返回维修人员列表
</button>
</div>
<!-- 维修人员信息 -->
<div class="repairman-info card">
<h3>维修人员信息</h3>
@ -305,6 +313,12 @@ const viewOrderDetail = (orderId: string) => {
router.push(`/home/work-order/detail/${orderId}`)
}
//
const goBack = () => {
router.back()
}
//
// MaintenanceRecord.vue
onMounted(() => {
@ -565,5 +579,23 @@ onMounted(() => {
.order-tabs {
flex-direction: column;
}
.btn-back {
background: #f0f0f0;
color: #333;
border: 1px solid #ddd;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.btn-back:hover {
background: #e0e0e0;
border-color: #bbb;
}
}
</style>

Loading…
Cancel
Save