部分优化 #127

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

@ -79,7 +79,7 @@ public class WorkOrderController {
private String orderId;
private boolean approved;
}
// 审核工单
// 2. 修改接口接收方式
@PostMapping("/review")
@PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')")

@ -82,6 +82,11 @@ public class AdminController {
public ResponseEntity<ResultVO<Admin>> saveAdmin(@RequestBody Admin admin) {
// 实现保持不变
try {
if (admin.getAdminName() == null || admin.getAdminName().trim().isEmpty()) {
return ResponseEntity.ok(ResultVO.error(400, "管理员姓名不能为空"));
}
Admin savedAdmin = adminService.saveAdmin(admin);
return ResponseEntity.ok(ResultVO.success(savedAdmin));
} catch (Exception e) {
@ -121,6 +126,8 @@ public class AdminController {
}
}
/**
*
* /

@ -94,23 +94,30 @@ public class TerminalServiceImpl implements TerminalService {
}
@Override
@Transactional
public void deleteTerminal(String terminalId) {
// 1. 校验终端是否已绑定设备使用新增的existsByTerminalId方法
if (mappingRepository.existsByTerminalId(terminalId)) {
@Transactional
public void deleteTerminal(String terminalId) {
// 1. 检查终端映射记录是否存在
Optional<DeviceTerminalMapping> mappingOpt = mappingRepository.findByTerminalId(terminalId);
if (mappingOpt.isPresent()) {
// 检查是否实际绑定了设备deviceId不为null
DeviceTerminalMapping mapping = mappingOpt.get();
if (mapping.getDeviceId() != null && !mapping.getDeviceId().isEmpty()) {
throw new RuntimeException("终端已绑定设备,无法删除,请先解除设备关联");
}
}
// 2. 校验终端是否存在复用原有existsById方法
if (!locationRepository.existsById(terminalId)) {
throw new RuntimeException("终端不存在,无需删除:" + terminalId);
}
// 3. 级联删除数据(先删映射表,再删位置表,保证数据一致性)
mappingRepository.deleteByTerminalId(terminalId); // 新增的批量删除方法
locationRepository.deleteById(terminalId); // 复用原有删除方法
// 2. 校验终端是否存在
if (!locationRepository.existsById(terminalId)) {
throw new RuntimeException("终端不存在,无需删除:" + terminalId);
}
// 3. 级联删除数据
mappingRepository.deleteByTerminalId(terminalId);
locationRepository.deleteById(terminalId);
}
@Override
public TerminalManageVO getTerminalById(String terminalId) {
// 1. 查询位置信息复用原有findById方法

@ -55,7 +55,8 @@ const menuItems: MenuItem[] = [
route: '/home/equipment',
children: [
{ name: '制水机', route: '/home/equipment/water-maker' },
{ name: '供水机', route: '/home/equipment/water-supplier' }
{ name: '供水机', route: '/home/equipment/water-supplier' },
{ name: '终端机', route: '/home/equipment/terminal' } //
]
},
{

@ -70,6 +70,17 @@ const router = createRouter({
title: '供水设备'
}
},
{
path: '/home/equipment/terminal',
component: () => import('@/views/equipment/Terminal.vue'),
meta: { requiresAuth: true }
},
{
path: '/home/equipment/terminal/:id',
component: () => import('@/views/equipment/TerminalDetail.vue'), // 如果需要详情页
meta: { requiresAuth: true }
},
// 工单管理相关路由
{
path: 'work-order',

@ -74,7 +74,7 @@
<!-- 告警列表 -->
<div v-else class="alert-list">
<!-- 只显示前10条告警 -->
<!-- 只显示前10条告警按时间倒序排列 -->
<div v-for="(alert, index) in recentAlerts.slice(0, 10)" :key="alert.alertId" class="alert-item">
<div class="alert-text">{{ alert.deviceId }}{{ alert.alertMessage }}</div>
<div class="alert-time">{{ formatDateTime(alert.timestamp) }}</div>
@ -244,6 +244,7 @@ const fetchStatsData = async () => {
}
}
//
//
const fetchAlertData = async () => {
loadingAlerts.value = true
@ -271,24 +272,25 @@ const fetchAlertData = async () => {
//
const alerts = Array.isArray(alertResult.data) ? alertResult.data : []
//
const sortedAlerts = [...alerts].sort((a, b) => {
// Date
const timeA = new Date(a.timestamp).getTime()
const timeB = new Date(b.timestamp).getTime()
return timeB - timeA //
})
// 10
recentAlerts.value = alerts.slice(0, 10)
//
if (recentAlerts.value.length > 0) {
const sortedAlerts = [...recentAlerts.value].sort((a, b) => {
const priorityMap: Record<string, number> = {
'critical': 4,
'error': 3,
'warning': 2,
'info': 1
};
return (priorityMap[b.alertLevel?.toLowerCase()] || 0) - (priorityMap[a.alertLevel?.toLowerCase()] || 0);
});
latestAlert.value = sortedAlerts[0] ?? null;
recentAlerts.value = sortedAlerts.slice(0, 10)
//
// 222
if (recentAlerts.value.length > 0 && recentAlerts.value[0]) {
latestAlert.value = recentAlerts.value[0] //
} else {
latestAlert.value = null;
latestAlert.value = null
}
}
} catch (error: any) {
console.error('获取告警数据失败:', error)
@ -318,6 +320,7 @@ const fetchAlertData = async () => {
}
}
//
onMounted(() => {
fetchStatsData()

@ -11,8 +11,7 @@
<div class="action-bar">
<!-- 新增校区按钮 -->
<button class="btn-add" @click="handleAddCampus"></button>
<!-- 导航到片区管理按钮 -->
<button class="btn-nav" @click="goToZoneManagement"></button>
</div>
<!-- 校区表格 -->
@ -89,6 +88,18 @@
</div>
<div class="modal-body">
<form @submit.prevent="handleSave">
<div class="form-item" v-if="!isEdit">
<label>所属市区</label>
<select
v-model="formData.parentAreaId"
required
>
<option value="">请选择所属市区</option>
<option v-for="city in cityList" :key="city.areaId" :value="city.areaId">
{{ city.areaName }}
</option>
</select>
</div>
<div class="form-item">
<label>校区名称</label>
<input
@ -109,20 +120,24 @@
</div>
<div class="form-item">
<label>负责人</label>
<input
type="text"
v-model="formData.manager"
placeholder="请输入负责人姓名"
<select
v-model="selectedManager"
@change="onManagerChange"
required
>
<option value="">请选择负责人</option>
<option v-for="admin in adminList" :key="admin.adminId" :value="admin">
{{ admin.adminName }}
</option>
</select>
</div>
<div class="form-item">
<label>联系电话</label>
<input
type="text"
v-model="formData.managerPhone"
placeholder="请输入负责人联系电话"
required
placeholder="联系电话会自动填充"
readonly
>
</div>
<div class="form-actions">
@ -167,6 +182,18 @@ import { useAuthStore } from '@/stores/auth' // 导入 authStore
const router = useRouter()
const authStore = useAuthStore()
//
interface Admin {
adminId: string
adminName: string
password: string
phone: string
role: string
areaId: string | null
createdTime?: Date
updatedTime?: Date
}
// Area
interface Area {
areaId: string
@ -182,6 +209,9 @@ interface Area {
//
const campusList = ref<Area[]>([])
const cityList = ref<Area[]>([])
const adminList = ref<Admin[]>([])
const selectedManager = ref<Admin | null>(null)
const selectedCampus = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
@ -248,21 +278,47 @@ const fetchCampusList = async () => {
return
}
const response = await request<{
//
// 1:
const citiesResponse = await request<{
code: number
msg: string
data: Area[]
}>('/api/web/area/list-campus', {
}>('/api/web/area/cities', {
method: 'GET',
})
if (response.code === 200) {
campusList.value = response.data
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取校区列表失败:', errorMsg)
alert(`获取校区列表失败:${errorMsg}`)
if (citiesResponse?.code !== 200 || !citiesResponse?.data) {
console.error('获取市区列表失败:', citiesResponse?.msg || '未知错误')
alert(`获取市区列表失败:${citiesResponse?.msg || '未知错误'}`)
return
}
const cities = citiesResponse.data
const allCampuses: Area[] = []
//
for (const city of cities) {
try {
const campusResponse = await request<{
code: number
msg: string
data: Area[]
}>(`/api/web/area/campuses/${city.areaId}`, {
method: 'GET',
})
if (campusResponse?.code === 200 && campusResponse?.data) {
allCampuses.push(...campusResponse.data)
} else {
console.warn(`获取市区 ${city.areaName} 的校区失败:`, campusResponse?.msg || '未知错误')
}
} catch (error) {
console.warn(`获取市区 ${city.areaName} 的校区时出错:`, error)
}
}
campusList.value = allCampuses
} catch (error: any) {
console.error('请求异常:', error)
const errorMsg = error.message.includes('401') || error.message.includes('403')
@ -276,6 +332,94 @@ const fetchCampusList = async () => {
}
}
//
const fetchCityList = async () => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const response = await request<{
code: number
msg: string
data: Area[]
}>('/api/web/area/cities', {
method: 'GET',
})
if (response?.code === 200 && response?.data) {
cityList.value = response.data
} else {
console.error('获取市区列表失败:', response?.msg || '未知错误')
alert(`获取市区列表失败:${response?.msg || '未知错误'}`)
}
} catch (error: any) {
console.error('获取市区列表异常:', error)
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取市区列表失败,请稍后重试'
alert(`获取市区列表失败:${errorMsg}`)
}
}
//
//
const fetchAdminList = async () => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
const response = await request<{
code: number
msg: string
data: Admin[]
}>('/api/web/admin/list', {
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 || '未知错误'}`)
}
} catch (error: any) {
console.error('获取管理员列表异常:', error)
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取管理员列表失败,请稍后重试'
alert(`获取管理员列表失败:${errorMsg}`)
}
}
//
const onManagerChange = () => {
if (selectedManager.value) {
formData.value.manager = selectedManager.value.adminName
formData.value.managerPhone = selectedManager.value.phone
} else {
formData.value.manager = ''
formData.value.managerPhone = ''
}
}
//
const handleAddCampus = () => {
isEdit.value = false
@ -284,13 +428,14 @@ const handleAddCampus = () => {
areaId: '',
areaName: '',
areaType: 'campus',
parentAreaId: null,
parentAreaId: null, // null
address: '',
manager: '',
managerPhone: '',
createdTime: undefined,
updatedTime: undefined
}
selectedManager.value = null
showModal.value = true
}
@ -298,6 +443,11 @@ const handleAddCampus = () => {
const handleEdit = (campus: Area) => {
isEdit.value = true
formData.value = { ...campus }
//
const matchedAdmin = adminList.value.find(admin => admin.adminName === campus.manager)
selectedManager.value = matchedAdmin || null
showModal.value = true
}
@ -326,11 +476,11 @@ const confirmDelete = async () => {
method: 'DELETE',
})
if (response.code === 200) {
if (response?.code === 200) {
fetchCampusList() //
showDeleteConfirm.value = false
} else {
const errorMsg = response.msg || `删除失败(错误码:${response.code}`
const errorMsg = response?.msg || `删除失败(错误码:${response?.code || '未知'}`
console.error('删除校区失败:', errorMsg)
alert(`删除校区失败:${errorMsg}`)
}
@ -358,8 +508,19 @@ const handleSave = async () => {
return
}
// ID
if (!isEdit.value && (!formData.value.parentAreaId || formData.value.parentAreaId.trim() === '')) {
alert('新增校区时必须选择所属市区')
return
}
//
if (!selectedManager.value) {
alert('请选择负责人')
return
}
let response
let result
if (isEdit.value) {
//
@ -372,11 +533,11 @@ const handleSave = async () => {
body: JSON.stringify(formData.value)
})
if (response.code === 200) {
if (response?.code === 200 && response?.data) {
fetchCampusList() //
showModal.value = false
} else {
const errorMsg = response.msg || `更新失败(错误码:${response.code}`
const errorMsg = response?.msg || `更新失败(错误码:${response?.code || '未知'}`
console.error('更新校区失败:', errorMsg)
alert(`更新校区失败:${errorMsg}`)
}
@ -385,6 +546,7 @@ const handleSave = async () => {
const newCampus = {
areaName: formData.value.areaName,
areaType: 'campus' as const,
parentAreaId: formData.value.parentAreaId, // ID
address: formData.value.address,
manager: formData.value.manager,
managerPhone: formData.value.managerPhone
@ -399,11 +561,11 @@ const handleSave = async () => {
body: JSON.stringify(newCampus)
})
if (response.code === 200) {
if (response?.code === 200 && response?.data) {
fetchCampusList() //
showModal.value = false
} else {
const errorMsg = response.msg || `新增失败(错误码:${response.code}`
const errorMsg = response?.msg || `新增失败(错误码:${response?.code || '未知'}`
console.error('新增校区失败:', errorMsg)
alert(`新增校区失败:${errorMsg}`)
}
@ -421,14 +583,11 @@ const handleSave = async () => {
}
}
//
const goToZoneManagement = () => {
router.push('/area/zone')
}
//
onMounted(() => {
onMounted(async () => {
console.log('Token:', authStore.token)
await fetchCityList() //
await fetchAdminList() //
fetchCampusList()
})
</script>

@ -27,39 +27,39 @@
<div class="card">
<table class="area-table">
<thead>
<tr>
<th>片区</th>
<th>设备数量</th>
<th>范围</th>
<th>操作</th>
</tr>
<tr>
<th>片区</th>
<th>设备数量</th>
<th>范围</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="area in filteredAreas" :key="area.areaId">
<td>{{ area.areaName }}</td>
<td>{{ area.deviceCount || 0 }}</td>
<td>{{ area.address || '未设置' }}</td>
<td class="operation-buttons">
<button
<tr v-for="area in filteredAreas" :key="area.areaId">
<td>{{ area.areaName }}</td>
<td>{{ area.deviceCount || 0 }}</td>
<td>{{ area.address || '未设置' }}</td>
<td class="operation-buttons">
<button
class="btn-edit"
@click="handleEdit(area)"
>
编辑
</button>
<button
>
编辑
</button>
<button
class="btn-delete"
@click="handleDelete(area.areaId, area.areaName)"
>
删除
</button>
</td>
</tr>
<tr v-if="loading">
<td colspan="4" class="no-data">正在加载数据...</td>
</tr>
<tr v-else-if="filteredAreas.length === 0">
<td colspan="4" class="no-data">暂无片区数据</td>
</tr>
>
删除
</button>
</td>
</tr>
<tr v-if="loading">
<td colspan="4" class="no-data">正在加载数据...</td>
</tr>
<tr v-else-if="filteredAreas.length === 0">
<td colspan="4" class="no-data">暂无片区数据</td>
</tr>
</tbody>
</table>
</div>
@ -67,9 +67,9 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
:disabled="currentPage === 1 || loading"
@click="currentPage--"
class="page-btn"
:disabled="currentPage === 1 || loading"
@click="currentPage--"
>
上一页
</button>
@ -77,9 +77,9 @@
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages || loading"
@click="currentPage++"
class="page-btn"
:disabled="currentPage === totalPages || loading"
@click="currentPage++"
>
下一页
</button>
@ -97,34 +97,34 @@
<div class="form-item">
<label>片区名称</label>
<input
type="text"
v-model="formData.areaName"
placeholder="请输入片区名称"
required
type="text"
v-model="formData.areaName"
placeholder="请输入片区名称"
required
>
</div>
<div class="form-item">
<label>片区范围</label>
<textarea
v-model="formData.address"
placeholder="请输入片区范围描述"
rows="3"
v-model="formData.address"
placeholder="请输入片区范围描述"
rows="3"
></textarea>
</div>
<div class="form-item">
<label>负责人</label>
<input
type="text"
v-model="formData.manager"
placeholder="请输入负责人姓名"
type="text"
v-model="formData.manager"
placeholder="请输入负责人姓名"
>
</div>
<div class="form-item">
<label>联系电话</label>
<input
type="text"
v-model="formData.managerPhone"
placeholder="请输入负责人联系电话"
type="text"
v-model="formData.managerPhone"
placeholder="请输入负责人联系电话"
>
</div>
<div class="form-actions">
@ -227,8 +227,8 @@ const filteredAreas = computed(() => {
//
const totalPages = computed(() => {
const filteredCount = selectedArea.value
? areaList.value.filter(area => area.areaId === selectedArea.value).length
: areaList.value.length
? areaList.value.filter(area => area.areaId === selectedArea.value).length
: areaList.value.length
return Math.ceil(filteredCount / pageSize.value)
})
@ -245,7 +245,7 @@ const fetchAreaList = async () => {
}
// URL
const url = `/api/web/area/list?areaType=zone`
const url = `/api/web/area/cities`
const response = await request<{
code: number
msg: string

@ -0,0 +1,807 @@
<template>
<div class="terminal-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"
>
<button class="search-btn" @click="handleSearch"></button>
</div>
<!-- 状态筛选 -->
<select
v-model="selectedStatus"
class="filter-select"
@change="handleSearch"
>
<option value="">全部状态</option>
<option value="active">在线</option>
<option value="inactive">离线</option>
<option value="warning">警告</option>
<option value="fault">故障</option>
</select>
</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>关联设备ID</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="terminal in paginatedTerminals" :key="terminal.terminalId">
<td>{{ terminal.terminalId }}</td>
<td>{{ terminal.terminalName }}</td>
<td>{{ terminal.longitude }}</td>
<td>{{ terminal.latitude }}</td>
<td>
<span :class="`status-tag ${terminal.terminalStatus}`">
{{ formatStatus(terminal.terminalStatus) }}
</span>
</td>
<td>{{ formatDate(terminal.installDate) }}</td>
<td>{{ terminal.deviceId || '-' }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewTerminal(terminal.terminalId)"></button>
<button
class="btn-edit"
@click="editTerminal(terminal)"
>
编辑
</button>
<button
class="btn-delete"
@click="deleteTerminal(terminal.terminalId)"
>
删除
</button>
</td>
</tr>
<tr v-if="paginatedTerminals.length === 0">
<td colspan="8" class="no-data">暂无终端数据</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }} ( {{ filteredTerminals.length }} 条记录)
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
</div>
<!-- 添加/编辑终端模态框 -->
<div v-if="showAddModal" class="modal-overlay" @click="showAddModal = false">
<div class="modal-content" @click.stop>
<h3>{{ isEditing ? '编辑终端' : '添加终端机' }}</h3>
<form @submit.prevent="saveTerminal">
<div class="form-group">
<label>终端ID:</label>
<input v-model="currentTerminal.terminalId" type="text" :disabled="isEditing" required>
</div>
<div class="form-group">
<label>终端名称:</label>
<input v-model="currentTerminal.terminalName" type="text" required>
</div>
<div class="form-group">
<label>经度:</label>
<input v-model="currentTerminal.longitude" type="number" step="any" required>
</div>
<div class="form-group">
<label>纬度:</label>
<input v-model="currentTerminal.latitude" type="number" step="any" required>
</div>
<div class="form-group">
<label>状态:</label>
<select v-model="currentTerminal.terminalStatus">
<option value="active">在线</option>
<option value="inactive">离线</option>
<option value="warning">警告</option>
<option value="fault">故障</option>
</select>
</div>
<div class="form-group">
<label>安装日期:</label>
<input v-model="currentTerminal.installDate" type="date">
</div>
<div class="form-group">
<label>关联设备ID:</label>
<input v-model="currentTerminal.deviceId" type="text">
</div>
<div class="form-actions">
<button type="button" @click="showAddModal = false">取消</button>
<button type="submit">{{ isEditing ? '更新' : '添加' }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { request } from '@/api/request'
import type { ResultVO } from '@/api/types/auth'
//
const isTerminalManageVO = (obj: any): obj is TerminalManageVO => {
return obj &&
typeof obj === 'object' &&
typeof obj.terminalId === 'string' &&
typeof obj.terminalName === 'string' &&
typeof obj.longitude === 'number' &&
typeof obj.latitude === 'number' &&
['active', 'inactive', 'warning', 'fault'].includes(obj.terminalStatus)
}
//
type TerminalStatus = 'active' | 'inactive' | 'fault' | 'warning'
// - TerminalManageVO
interface TerminalManageVO {
terminalId: string
terminalName: string
longitude: number
latitude: number
terminalStatus: TerminalStatus
installDate?: string
deviceId?: string
}
//
const terminals = ref<TerminalManageVO[]>([])
const searchKeyword = ref('')
const selectedStatus = ref('') //
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore() // auth store
//
const showAddModal = ref(false)
const isEditing = ref(false)
//
const currentTerminal = ref<TerminalManageVO>({
terminalId: '',
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
})
//
//
const loadTerminals = async (): Promise<void> => {
try {
// tokenisLoggedIn
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
console.log('开始加载终端机数据...')
//
const result = await request<ResultVO<TerminalManageVO[]>>(
`/api/web/terminal/list`,
{ method: 'GET' }
)
console.log('终端机请求结果:', result)
// ResultVO
if (result && typeof result === 'object' && 'code' in result) {
// ResultVO
if (result.code === 200 && result.data && Array.isArray(result.data)) {
console.log(`获取到${result.data.length}个终端`)
//
terminals.value = result.data.map(item => ({
terminalId: item.terminalId,
terminalName: item.terminalName,
longitude: item.longitude,
latitude: item.latitude,
terminalStatus: item.terminalStatus,
installDate: item.installDate,
deviceId: item.deviceId
}))
} else {
console.warn('API响应非成功状态或数据格式错误:', result)
terminals.value = []
}
} else {
// ResultVO
if (Array.isArray(result)) {
console.log(`获取到${(result as TerminalManageVO[]).length}个终端`)
terminals.value = (result as TerminalManageVO[]).map(item => ({
terminalId: item.terminalId,
terminalName: item.terminalName,
longitude: item.longitude,
latitude: item.latitude,
terminalStatus: item.terminalStatus,
installDate: item.installDate,
deviceId: item.deviceId
}))
} else {
console.warn('API响应数据格式错误:', result)
terminals.value = []
}
}
if (terminals.value.length === 0) {
console.log('提示:未找到任何终端,请确认是否已添加终端')
}
} catch (error) {
console.error('加载终端数据失败:', error)
terminals.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const filteredTerminals = computed(() => {
return terminals.value.filter(terminal => {
const keywordMatch = searchKeyword.value.trim() === '' ||
terminal.terminalId.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
terminal.terminalName.toLowerCase().includes(searchKeyword.value.toLowerCase())
const statusMatch = selectedStatus.value === '' || terminal.terminalStatus === selectedStatus.value
return keywordMatch && statusMatch
})
})
//
const paginatedTerminals = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredTerminals.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(filteredTerminals.value.length / pageSize)
})
//
const formatStatus = (status: TerminalStatus): string => {
const statusMap: Record<string, string> = {
active: '在线',
inactive: '离线',
warning: '警告',
fault: '故障'
}
return statusMap[status] || status
}
//
const formatDate = (dateString?: string): string => {
if (!dateString) return '-'
//
return dateString
}
//
const handleSearch = () => {
currentPage.value = 1 //
}
//
const viewTerminal = (id: string) => {
router.push(`/home/equipment/terminal/${id}`)
}
//
const editTerminal = (terminal: TerminalManageVO) => {
currentTerminal.value = { ...terminal }
isEditing.value = true
showAddModal.value = true
}
//
//
const deleteTerminal = async (terminalId: string) => {
if (!confirm(`确定要删除终端 ${terminalId} 吗?`)) {
return
}
try {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const result = await request<any>(`/api/web/terminal/delete/${terminalId}`, {
method: 'DELETE'
})
// Map
if (result && typeof result === 'object' && result.message) {
if (result.message.includes('成功')) {
//
terminals.value = terminals.value.filter(t => t.terminalId !== terminalId)
alert('终端删除成功')
} else {
alert(`删除终端失败: ${result.message}`)
}
} else {
//
//
terminals.value = terminals.value.filter(t => t.terminalId !== terminalId)
alert('终端删除成功')
}
} catch (error) {
console.error('删除终端失败:', error)
alert('删除终端失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const saveTerminal = async () => {
try {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
let result: ResultVO<TerminalManageVO> | TerminalManageVO
if (isEditing.value) {
//
result = await request<ResultVO<TerminalManageVO> | TerminalManageVO>('/api/web/terminal/update', {
method: 'PUT',
body: JSON.stringify(currentTerminal.value)
})
} else {
//
result = await request<ResultVO<TerminalManageVO> | TerminalManageVO>('/api/web/terminal/add', {
method: 'POST',
body: JSON.stringify(currentTerminal.value)
})
}
// ResultVO
if (result && typeof result === 'object' && 'code' in result) {
// ResultVO
if (result.code === 200 && result.data && isTerminalManageVO(result.data)) {
//
await loadTerminals()
//
showAddModal.value = false
isEditing.value = false
currentTerminal.value = {
terminalId: '',
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
}
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
} else if (result.code === 200) {
// code200data
//
await loadTerminals()
//
showAddModal.value = false
isEditing.value = false
currentTerminal.value = {
terminalId: '',
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
}
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
} else {
alert(`${isEditing.value ? '更新' : '添加'}终端失败: ${result.message}`)
}
}
//
else if (isTerminalManageVO(result)) {
//
await loadTerminals()
//
showAddModal.value = false
isEditing.value = false
currentTerminal.value = {
terminalId: '',
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
}
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
}
else {
//
//
await loadTerminals()
//
showAddModal.value = false
isEditing.value = false
currentTerminal.value = {
terminalId: '',
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
}
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
}
} catch (error) {
console.error(`${isEditing.value ? '更新' : '添加'}终端失败:`, error)
alert(`${isEditing.value ? '更新' : '添加'}终端失败`)
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
onMounted(async () => {
console.log('🚀 开始加载终端数据...')
await loadTerminals()
})
</script>
<style scoped>
/* 样式与制水机页面保持一致 */
.terminal-page {
padding: 20px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h2 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.breadcrumb {
color: #666;
font-size: 14px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 16px;
}
.btn-add {
background: #42b983;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn-add:hover {
background: #359e75;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
}
.search-box {
display: flex;
gap: 8px;
}
.search-box input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 240px;
}
.search-btn {
background: #667eea;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.filter-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
.equipment-table {
width: 100%;
border-collapse: collapse;
}
.equipment-table th,
.equipment-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
}
.equipment-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #4e5969;
font-size: 14px;
}
.equipment-table tbody tr:hover {
background-color: #f8f9fa;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.active {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.inactive {
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;
flex-wrap: wrap;
}
.operation-buttons button {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
border: none;
transition: opacity 0.3s;
}
.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: #ff4d4f;
color: white;
}
.no-data {
text-align: center;
padding: 40px 0;
color: #8c8c8c;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 24px;
color: #666;
font-size: 14px;
}
.page-btn {
padding: 4px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.page-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
min-width: 400px;
max-width: 500px;
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.form-actions button {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
border: 1px solid #ddd;
}
.form-actions button[type="button"] {
background: #f5f5f5;
}
.form-actions button[type="submit"] {
background: #42b983;
color: white;
border: none;
}
/* 响应式调整 */
@media (max-width: 768px) {
.filters {
flex-direction: column;
width: 100%;
}
.search-box, .filter-select {
width: 100%;
}
.modal-content {
width: 90%;
min-width: auto;
}
}
</style>

@ -0,0 +1,572 @@
<template>
<div class="terminal-detail-page">
<!-- 页面标题和面包屑 -->
<div class="page-header">
<h2>终端详情</h2>
<div class="breadcrumb">校园矿化水平台 / 设备监控 / 终端机 / 详情</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">...</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error">{{ error }}</div>
<!-- 终端详情内容 -->
<div v-else-if="terminal" class="terminal-detail-content">
<!-- 终端基本信息卡片 -->
<div class="card">
<h3>终端基本信息</h3>
<div class="detail-grid">
<div class="detail-item">
<label>终端ID:</label>
<span>{{ terminal.terminalId }}</span>
</div>
<div class="detail-item">
<label>终端名称:</label>
<span>{{ terminal.terminalName }}</span>
</div>
<div class="detail-item">
<label>经度:</label>
<span>{{ terminal.longitude }}</span>
</div>
<div class="detail-item">
<label>纬度:</label>
<span>{{ terminal.latitude }}</span>
</div>
<div class="detail-item">
<label>状态:</label>
<span :class="`status-tag ${terminal.terminalStatus}`">
{{ formatStatus(terminal.terminalStatus) }}
</span>
</div>
<div class="detail-item">
<label>安装日期:</label>
<span>{{ formatDate(terminal.installDate) }}</span>
</div>
<div class="detail-item">
<label>关联设备ID:</label>
<span>{{ terminal.deviceId || '-' }}</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button class="btn-back" @click="goBack"></button>
</div>
</div>
<!-- 无数据状态 -->
<div v-else class="no-data">未找到终端信息</div>
<!-- 编辑模态框 -->
<div v-if="showEditModal" class="modal-overlay" @click="showEditModal = false">
<div class="modal-content" @click.stop>
<h3>编辑终端</h3>
<form @submit.prevent="saveTerminal">
<div class="form-group">
<label>终端ID:</label>
<input v-model="editingTerminal.terminalId" type="text" disabled>
</div>
<div class="form-group">
<label>终端名称:</label>
<input v-model="editingTerminal.terminalName" type="text" required>
</div>
<div class="form-group">
<label>经度:</label>
<input v-model="editingTerminal.longitude" type="number" step="any" required>
</div>
<div class="form-group">
<label>纬度:</label>
<input v-model="editingTerminal.latitude" type="number" step="any" required>
</div>
<div class="form-group">
<label>状态:</label>
<select v-model="editingTerminal.terminalStatus">
<option value="active">在线</option>
<option value="inactive">离线</option>
<option value="warning">警告</option>
<option value="fault">故障</option>
</select>
</div>
<div class="form-group">
<label>安装日期:</label>
<input v-model="editingTerminal.installDate" type="date">
</div>
<div class="form-group">
<label>关联设备ID:</label>
<input v-model="editingTerminal.deviceId" type="text">
</div>
<div class="form-actions">
<button type="button" @click="showEditModal = false">取消</button>
<button type="submit">保存</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { request } from '@/api/request'
import type { ResultVO } from '@/api/types/auth'
//
type TerminalStatus = 'active' | 'inactive' | 'fault' | 'warning'
// - TerminalManageVO
interface TerminalManageVO {
terminalId: string
terminalName: string
longitude: number
latitude: number
terminalStatus: TerminalStatus
installDate?: string
deviceId?: string
}
//
function isTerminalManageVO(obj: any): obj is TerminalManageVO {
return obj &&
typeof obj === 'object' &&
typeof obj.terminalId === 'string' &&
typeof obj.terminalName === 'string' &&
typeof obj.longitude === 'number' &&
typeof obj.latitude === 'number' &&
['active', 'inactive', 'warning', 'fault'].includes(obj.terminalStatus)
}
//
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
const error = ref('')
const terminal = ref<TerminalManageVO | null>(null)
const showEditModal = ref(false)
const editingTerminal = ref<TerminalManageVO>({
terminalId: '',
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
})
// ID
const terminalId = route.params.id as string
//
const formatStatus = (status: TerminalStatus): string => {
const statusMap: Record<string, string> = {
active: '在线',
inactive: '离线',
warning: '警告',
fault: '故障'
}
return statusMap[status] || status
}
//
const formatDate = (dateString?: string): string => {
if (!dateString) return '-'
//
return dateString
}
//
const loadTerminal = async () => {
if (!terminalId) {
error.value = '缺少终端ID'
return
}
try {
loading.value = true
error.value = ''
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const result = await request<ResultVO<TerminalManageVO> | TerminalManageVO>(`/api/web/terminal/${terminalId}`, {
method: 'GET'
})
console.log('终端详情请求结果:', result)
// ResultVO
if (result && typeof result === 'object' && 'code' in result) {
// ResultVO
if (result.code === 200 && result.data && isTerminalManageVO(result.data)) {
//
terminal.value = {
terminalId: result.data.terminalId,
terminalName: result.data.terminalName,
longitude: result.data.longitude,
latitude: result.data.latitude,
terminalStatus: result.data.terminalStatus,
installDate: result.data.installDate,
deviceId: result.data.deviceId
}
//
editingTerminal.value = { ...terminal.value }
} else {
error.value = result.message || '获取终端信息失败'
console.warn('API响应非成功状态或数据格式错误:', result)
}
} else if (isTerminalManageVO(result)) {
// ResultVO
terminal.value = {
terminalId: result.terminalId,
terminalName: result.terminalName,
longitude: result.longitude,
latitude: result.latitude,
terminalStatus: result.terminalStatus,
installDate: result.installDate,
deviceId: result.deviceId
}
//
editingTerminal.value = { ...terminal.value }
} else {
error.value = '获取终端信息失败'
console.warn('API响应数据格式错误:', result)
}
} catch (err) {
console.error('加载终端详情失败:', err)
error.value = '加载终端详情失败'
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
} finally {
loading.value = false
}
}
//
const editTerminal = () => {
if (terminal.value) {
editingTerminal.value = { ...terminal.value }
showEditModal.value = true
}
}
//
//
const saveTerminal = async () => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const result = await request<ResultVO<TerminalManageVO> | TerminalManageVO>(`/api/web/terminal/update`, {
method: 'PUT',
body: JSON.stringify(editingTerminal.value)
})
// ResultVO
if (result && typeof result === 'object' && 'code' in result) {
// ResultVO
if (result.code === 200 && result.data && isTerminalManageVO(result.data)) {
//
if (terminal.value) {
Object.assign(terminal.value, result.data)
}
showEditModal.value = false
alert('终端信息更新成功')
} else if (result.code === 200) {
// code200data
if (terminal.value) {
Object.assign(terminal.value, editingTerminal.value)
}
showEditModal.value = false
alert('终端信息更新成功')
} else {
//
const errorMessage = result.message || '更新终端信息失败'
alert(`更新终端信息失败: ${errorMessage}`)
}
}
//
else if (isTerminalManageVO(result)) {
//
if (terminal.value) {
Object.assign(terminal.value, result)
}
showEditModal.value = false
alert('终端信息更新成功')
}
else {
// ResultVO
if (terminal.value) {
Object.assign(terminal.value, editingTerminal.value)
}
showEditModal.value = false
alert('终端信息更新成功')
}
} catch (err) {
console.error('更新终端信息失败:', err)
alert('更新终端信息失败')
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const deleteTerminal = async () => {
if (!confirm(`确定要删除终端 ${terminal.value?.terminalId} 吗?`)) {
return
}
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const result = await request<ResultVO<boolean>>(`/api/web/terminal/delete/${terminal.value?.terminalId}`, {
method: 'DELETE'
})
if (result.code === 200 || result.code === 201 || result.code === 204) {
alert('终端删除成功')
router.push('/home/equipment/terminal')
} else {
alert(`删除终端失败: ${result.message}`)
}
} catch (err) {
console.error('删除终端失败:', err)
alert('删除终端失败')
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const goBack = () => {
router.push('/home/equipment/terminal')
}
//
onMounted(() => {
loadTerminal()
})
</script>
<style scoped>
.terminal-detail-page {
padding: 20px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h2 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.breadcrumb {
color: #666;
font-size: 14px;
}
.loading, .error, .no-data {
text-align: center;
padding: 40px 0;
color: #666;
}
.error {
color: #f5222d;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
.card h3 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
font-size: 18px;
font-weight: 600;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.detail-item {
display: flex;
flex-direction: column;
}
.detail-item label {
font-weight: 500;
color: #666;
margin-bottom: 4px;
font-size: 14px;
}
.detail-item span {
color: #333;
font-size: 15px;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
align-self: flex-start;
max-width: fit-content;
}
.status-tag.active {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.inactive {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.fault {
background-color: #ffebe6;
color: #cf1322;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
.action-buttons button {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
border: 1px solid #ddd;
font-size: 14px;
}
.btn-edit {
background: #42b983;
color: white;
border: none;
}
.btn-delete {
background: #ff4d4f;
color: white;
border: none;
}
.btn-back {
background: #f5f5f5;
color: #666;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
min-width: 400px;
max-width: 500px;
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.form-actions button {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
border: 1px solid #ddd;
}
.form-actions button[type="button"] {
background: #f5f5f5;
}
.form-actions button[type="submit"] {
background: #42b983;
color: white;
border: none;
}
</style>

@ -10,6 +10,8 @@
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add" @click="showAddModal = true">添加制水机</button>
<!-- 在操作按钮列中添加删除按钮 -->
<div class="filters">
<!-- 搜索框 -->
@ -99,6 +101,12 @@
设为故障
</button>
<button
class="btn-delete"
@click="deleteDevice(device.deviceId)"
>
删除
</button>
</td>
</tr>
<tr v-if="paginatedDevices.length === 0">
@ -495,6 +503,41 @@ const updateDeviceStatus = async (deviceId: string, status: string) => {
}
}
//
const deleteDevice = async (deviceId: string) => {
if (!confirm(`确定要删除设备 ${deviceId} 吗?此操作不可恢复。`)) {
return
}
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const result = await request<ResultVO<boolean>>(`/api/web/device/delete/${deviceId}`, {
method: 'DELETE'
})
if (result.code === 200) {
//
devices.value = devices.value.filter(d => d.deviceId !== deviceId)
alert('设备删除成功')
} else {
alert(`删除设备失败: ${result.message}`)
}
} catch (error) {
console.error('删除设备失败:', error)
alert('删除设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const addDevice = async () => {
try {
@ -852,5 +895,10 @@ onMounted(async () => {
width: 90%;
min-width: auto;
}
.btn-delete {
background-color: #ff4d4f;
color: white;
}
}
</style>

@ -1,4 +1,3 @@
<!-- src/views/equipment/WaterMakerDetail.vue -->
<template>
<div class="water-maker-detail-page">
<!-- 页面标题和面包屑 -->
@ -114,6 +113,49 @@
</div>
</div>
<!-- 关联的供水机列表卡片 -->
<div class="card detail-card">
<h3>
关联的供水机
<span v-if="loadingSuppliers" class="loading-indicator">...</span>
</h3>
<div v-if="supplierDevices.length > 0" class="suppliers-list">
<div
v-for="(supplier, index) in supplierDevices"
:key="index"
class="supplier-item"
>
<div class="supplier-header">
<h4>{{ supplier.deviceName || supplier.deviceId }}</h4>
<span :class="`status-tag ${supplier.status}`">
{{ formatStatus(supplier.status) }}
</span>
</div>
<div class="supplier-details">
<div class="detail-item">
<span class="label">设备ID:</span>
<span class="value">{{ supplier.deviceId }}</span>
</div>
<div class="detail-item">
<span class="label">设备类型:</span>
<span class="value">{{ formatDeviceType(supplier.deviceType) }}</span>
</div>
<div class="detail-item">
<span class="label">安装位置:</span>
<span class="value">{{ supplier.installLocation || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">所属片区:</span>
<span class="value">{{ supplier.areaId || '-' }}</span>
</div>
</div>
</div>
</div>
<div v-else-if="!loadingSuppliers" class="no-data">
该制水机暂未关联任何供水机
</div>
</div>
<!-- 加载中提示 -->
<div v-if="loading" class="loading">
正在加载设备详情...
@ -161,6 +203,18 @@ interface WaterMakerRealtimeData {
createdTime?: string //
}
//
interface SupplierDevice {
deviceId: string
deviceName: string
deviceType: string
areaId: string
installLocation: string
status: string
createTime?: string
lastHeartbeatTime?: string
}
interface DeviceDetail {
deviceInfo: DeviceInfo
realtimeData?: WaterMakerRealtimeData
@ -171,7 +225,9 @@ const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const deviceDetail = ref<DeviceDetail | null>(null)
const supplierDevices = ref<SupplierDevice[]>([]) //
const loading = ref(true)
const loadingSuppliers = ref(false) //
const error = ref('')
// ID
@ -226,6 +282,43 @@ const goBack = () => {
router.go(-1)
}
//
const loadSupplierDevices = async (makerId: string) => {
try {
loadingSuppliers.value = true
// token
const token = authStore.token
if (!token) {
router.push('/login')
return
}
//
const result = await request<ResultVO<SupplierDevice[]>>(
`/api/web/device/maker/${makerId}/suppliers`,
{ method: 'GET' }
)
if (result.code === 200 && result.data && Array.isArray(result.data)) {
supplierDevices.value = result.data
console.log('关联的供水机数据:', result.data)
} else {
console.warn('获取关联供水机失败:', result.message)
supplierDevices.value = []
}
} catch (err) {
console.error('加载关联供水机失败:', err)
supplierDevices.value = []
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
} finally {
loadingSuppliers.value = false
}
}
//
const loadDeviceDetail = async () => {
try {
@ -248,6 +341,11 @@ const loadDeviceDetail = async () => {
if (result.code === 200 && result.data) {
deviceDetail.value = result.data
console.log('设备详情数据:', deviceDetail.value)
//
if (result.data.deviceInfo?.deviceType === 'water_maker') {
await loadSupplierDevices(deviceId)
}
} else {
error.value = result.message || '获取设备详情失败'
}
@ -409,4 +507,52 @@ onMounted(() => {
padding: 16px;
}
}
</style>
/* 新增样式 */
.suppliers-list {
margin-top: 16px;
}
.supplier-item {
border: 1px solid #eee;
border-radius: 6px;
margin-bottom: 16px;
padding: 16px;
background-color: #fafafa;
}
.supplier-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.supplier-header h4 {
margin: 0;
font-size: 16px;
color: #333;
}
.supplier-details {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.no-data {
text-align: center;
color: #999;
font-style: italic;
padding: 20px 0;
}
.loading-indicator {
font-size: 14px;
color: #666;
font-weight: normal;
margin-left: 8px;
}
</style>

Loading…
Cancel
Save