片区部分优化

pull/150/head
ZHW 2 months ago
parent caaf265c92
commit 673b22b310

@ -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);
}
/**
*
*/

@ -50,6 +50,12 @@
<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)"
@ -65,7 +71,7 @@
</td>
</tr>
<tr v-if="filteredCampus.length === 0">
<td colspan="7" class="no-data">
<td colspan="8" class="no-data">
{{ loading ? '正在加载数据...' : '暂无校区数据' }}
</td>
</tr>
@ -184,10 +190,87 @@
</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>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
@ -223,6 +306,18 @@ 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[]>([]) //
@ -235,11 +330,14 @@ 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>({
@ -395,6 +493,7 @@ const fetchCityList = async () => {
}
//
// -
const fetchAdminList = async () => {
try {
const token = authStore.token
@ -404,34 +503,37 @@ 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) {
@ -562,14 +664,13 @@ const handleSave = async () => {
code: number
msg: string
data: Area
}>('/api/web/area/update', {
method: 'POST', // POST
}>(`/api/web/area/update/${formData.value.areaId}`, { // URLareaId
method: 'PUT', // 使PUT
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}` //
},
body: JSON.stringify({
areaId: formData.value.areaId,
areaName: formData.value.areaName,
areaType: 'campus',
parentAreaId: formData.value.parentAreaId,
@ -653,6 +754,7 @@ const handleSave = async () => {
}
}
// ID
const getManagerName = (managerId: string) => {
if (!managerId) return '未分配'
@ -660,6 +762,51 @@ const getManagerName = (managerId: string) => {
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 () => {
@ -791,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;
@ -971,6 +1134,103 @@ onMounted(async () => {
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) {
@ -986,5 +1246,9 @@ onMounted(async () => {
.campus-select, .area-select {
width: 100%;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

@ -344,7 +344,6 @@ const confirmDelete = async () => {
}
//
// handleSave
const handleSave = async () => {
saving.value = true
try {
@ -358,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) {
@ -392,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)
})
@ -418,6 +432,8 @@ const handleSave = async () => {
}
//
onMounted(() => {
console.log('Token:', authStore.token)

@ -17,17 +17,41 @@
>
<button class="search-btn" @click="handleSearch"></button>
</div>
<select
v-model="selectedArea"
class="filter-select"
@change="currentPage = 1"
>
<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
v-model="selectedStatus"
class="filter-select"
@ -266,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>
@ -317,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 //
@ -382,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' }
)
@ -491,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 {
@ -529,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)
@ -587,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
@ -616,16 +672,10 @@ const formatStatus = (status: DeviceStatus): string => {
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() //
}
//
@ -633,8 +683,6 @@ const viewDevice = (id: string) => {
router.push(`/home/equipment/water-maker/${id}`)
}
// 线
//
// 线
const confirmOffline = async () => {
try {
@ -709,7 +757,6 @@ const confirmFault = async () => {
}
}
// 线
//
const deleteDevice = async (deviceId: string) => {
if (!confirm(`确定要删除设备 ${deviceId} 吗?此操作不可恢复。`)) {
@ -916,8 +963,8 @@ const addDevice = async () => {
//
onMounted(async () => {
console.log('🚀 开始加载设备数据...')
await loadDevices()
await loadCityList()
await loadDevices() //
})
</script>
@ -971,6 +1018,7 @@ onMounted(async () => {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
@ -994,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 {
@ -1227,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,7 +795,8 @@ const deleteDevice = async () => {
}
//
onMounted(() => {
onMounted(async () => {
await loadCityList() //
loadWaterSuppliers()
})
</script>
@ -701,6 +851,7 @@ onMounted(() => {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
@ -724,12 +875,18 @@ onMounted(() => {
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 {
@ -957,14 +1114,17 @@ onMounted(() => {
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">
<!-- 页面标题和面包屑 -->
@ -37,14 +36,14 @@
</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.areas ? admin.areas.join(', ') : '未关联区域' }}
{{ admin.areaName || (admin.areaId ? getAreaNameById(admin.areaId) : '') || '未关联区域' }}
</span>
<span v-else>-</span>
</td>
@ -75,7 +74,7 @@
</button>
</td>
</tr>
<tr v-if="filteredAdmins.length === 0">
<tr v-if="paginatedAdmins.length === 0">
<td colspan="7" class="no-data">暂无管理员数据</td>
</tr>
</tbody>
@ -92,7 +91,7 @@
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredAdmins.length }} 条记录)
</span>
<button
class="page-btn"
@ -133,17 +132,15 @@
</select>
</div>
<!-- 区域选择 - 仅当选择区域管理员时显示 -->
<!-- 区域选择 - 仅当选择区域管理员时显示改为单选下拉菜单选填 -->
<div class="form-group" v-if="formData.role === 'ROLE_AREA_ADMIN'">
<label class="form-label required">关联区域</label>
<select v-model="selectedAreas" multiple required>
<option v-for="area in areaList" :key="area.areaId" :value="area.areaId">
<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>
<p v-if="!selectedAreas.length && formData.role === 'ROLE_AREA_ADMIN'" class="error-message">
请选择关联的区域
</p>
</div>
<div class="form-group">
@ -194,8 +191,13 @@
</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>
@ -246,7 +248,8 @@ interface Admin {
phone: string
role: string
status: AdminStatus
areas?: string[] //
areaId?: string
areaName?: string
}
//
@ -256,6 +259,7 @@ interface FormData {
phone: string
role: string
password?: string
areaId?: string
}
//
@ -266,6 +270,7 @@ interface EditFormData {
phone: string
role: string
password?: string
areaId?: string
}
const authStore = useAuthStore()
@ -280,11 +285,11 @@ const loading = ref(false)
const showAddModal = ref(false)
const showEditModal = ref(false)
//
const areaList = ref<Area[]>([])
//
const unassignedAreas = ref<Area[]>([])
// ID
const selectedAreas = ref<string[]>([])
// ID
const selectedAreaId = ref<string>('')
//
const originalAdminData = ref<Admin | null>(null)
@ -295,7 +300,8 @@ const formData = ref<FormData>({
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
})
//
@ -305,11 +311,12 @@ const editFormData = ref<EditFormData>({
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
})
//
const loadAreaList = async () => {
//
const loadUnassignedAreas = async () => {
try {
const token = authStore.token
@ -319,17 +326,17 @@ const loadAreaList = async () => {
return
}
const response = await request<ResultVO<Area[]>>('/api/web/area/list', {
const response = await request<ResultVO<Area[]>>('/api/web/area/without-manager', {
method: 'GET',
})
if (response.code === 200) {
areaList.value = response.data || []
unassignedAreas.value = response.data || []
} else {
console.error('获取列表失败:', response.message)
console.error('获取未设置负责人片区列表失败:', response.message)
}
} catch (error: any) {
console.error('请求列表异常:', error)
console.error('请求未设置负责人片区列表异常:', error)
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
@ -366,7 +373,8 @@ const fetchAdminList = async () => {
phone: admin.phone || '未知电话',
role: admin.role || '未知角色',
status: 'active' as AdminStatus,
areas: admin.areas || [] //
areaId: admin.areaId,
areaName: admin.areaName || undefined //
}))
} else {
const errorMsg = response.message || `获取失败(错误码:${response.code}`
@ -401,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 => {
@ -411,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)
})
@ -425,7 +449,7 @@ const handleSearch = () => {
// -
const handleRoleChange = () => {
if (formData.value.role !== 'ROLE_AREA_ADMIN') {
selectedAreas.value = []
selectedAreaId.value = '' //
}
}
@ -438,7 +462,7 @@ const handleEditRoleChange = () => {
//
onMounted(async () => {
await loadAreaList()
await loadUnassignedAreas()
fetchAdminList()
})
@ -483,7 +507,8 @@ const handleEdit = async (id: string) => {
account: adminToEdit.account,
phone: adminToEdit.phone,
role: adminToEdit.role,
password: ''
password: '',
areaId: adminToEdit.areaId
};
showEditModal.value = true;
@ -505,7 +530,7 @@ const handleEditSubmit = async () => {
phone: editFormData.value.phone,
role: editFormData.value.role,
password: editFormData.value.password || undefined,
// areaIds: undefined //
// areaId: undefined //
};
const response = await request<ResultVO>(`/api/web/admin/save`, {
@ -523,7 +548,8 @@ const handleEditSubmit = async () => {
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
};
originalAdminData.value = null;
//
@ -584,20 +610,29 @@ const performDelete = async (id: string) => {
}
}
//
const campusAreas = computed(() => {
return unassignedAreas.value.filter(area => area.areaType === '校园')
})
//
const handleSubmit = async () => {
try {
//
if (formData.value.role === 'ROLE_AREA_ADMIN' && (!selectedAreas.value || selectedAreas.value.length === 0)) {
alert('区域管理员必须关联至少一个区域')
return
}
//
// ID
const submitData = {
...formData.value,
adminName: formData.value.name, // name adminName
account: formData.value.account,
phone: formData.value.phone,
role: formData.value.role,
password: formData.value.password || '123456',
areaIds: formData.value.role === 'ROLE_AREA_ADMIN' ? selectedAreas.value : undefined
// 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`, {
@ -614,9 +649,10 @@ const handleSubmit = async () => {
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
password: '',
areaId: undefined
}
selectedAreas.value = [] //
selectedAreaId.value = '' //
//
fetchAdminList()
} else {
@ -627,6 +663,7 @@ const handleSubmit = async () => {
alert(`添加管理员失败:${error.message}`)
}
}
</script>
<style scoped>
@ -895,6 +932,14 @@ const handleSubmit = async () => {
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;

Loading…
Cancel
Save