图表和片区 #139

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

@ -8,11 +8,13 @@
"name": "web",
"version": "0.0.0",
"dependencies": {
"echarts": "^6.0.0",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.3",
"@vitejs/plugin-vue": "^5.0.4",
"typescript": "^5.2.2",
"vite": "^5.1.6",
@ -770,6 +772,13 @@
"win32"
]
},
"node_modules/@tsconfig/node24": {
"version": "24.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.3.tgz",
"integrity": "sha512-vcERKtKQKHgzt/vfS3Gjasd8SUI2a0WZXpgJURdJsMySpS5+ctgbPfuLj2z/W+w4lAfTWxoN4upKfu2WzIRYnw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -991,6 +1000,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@ -1250,6 +1269,12 @@
"node": ">=0.10.0"
}
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@ -1417,6 +1442,15 @@
"peerDependencies": {
"typescript": "*"
}
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
}
}
}

@ -9,11 +9,13 @@
"preview": "vite preview"
},
"dependencies": {
"echarts": "^6.0.0",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.3",
"@vitejs/plugin-vue": "^5.0.4",
"typescript": "^5.2.2",
"vite": "^5.1.6",

@ -1,29 +1,31 @@
// src/api/types/auth.ts
// 登录请求参数 - 匹配后端的 LoginRequest
// src/main/resources/web/src/api/types/auth.ts
export interface LoginRequest {
username: string
password: string
}
export interface LoginVO {
token: string
userInfo: {
id: number
username: string
password: string
userType: string // 添加这个属性
rememberMe?: boolean
realName: string
role: string
avatar: string
}
}
// 通用响应结构
// 在 '@/api/types/auth.ts' 文件中添加
export interface ResultVO<T = any> {
code: number
message: string
data: T
code: number;
message: string;
data?: T;
[key: string]: any;
}
// 登录响应数据 - 匹配后端的 LoginVO
export interface LoginVO {
token: string
userInfo: {
id: number
username: string
realName?: string // 根据后端字段调整
role?: string // 根据后端字段调整
userType?: string // 添加这个字段
avatar?: string
}
}
export interface LoginResponse {
code: number
message: string
data: LoginVO
}

@ -497,6 +497,7 @@ const confirmDelete = async () => {
}
}
//
//
const handleSave = async () => {
saving.value = true
@ -508,12 +509,6 @@ const handleSave = async () => {
return
}
// ID
if (!isEdit.value && (!formData.value.parentAreaId || formData.value.parentAreaId.trim() === '')) {
alert('新增校区时必须选择所属市区')
return
}
//
if (!selectedManager.value) {
alert('请选择负责人')
@ -530,7 +525,19 @@ const handleSave = async () => {
data: Area
}>('/api/web/area/update', {
method: 'PUT',
body: JSON.stringify(formData.value)
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,
address: formData.value.address,
manager: formData.value.manager,
managerPhone: formData.value.managerPhone
})
})
if (response?.code === 200 && response?.data) {
@ -539,25 +546,47 @@ const handleSave = async () => {
} else {
const errorMsg = response?.msg || `更新失败(错误码:${response?.code || '未知'}`
console.error('更新校区失败:', errorMsg)
alert(`更新校区失败:${errorMsg}`)
alert(`更新校区失败:${ errorMsg}`)
}
} else {
// -
if (!formData.value.parentAreaId || formData.value.parentAreaId.trim() === '') {
alert('新增校区时必须选择所属市区')
return
}
//
const selectedCity = cityList.value.find(city =>
city.areaId === formData.value.parentAreaId && city.areaType === 'zone'
)
if (!selectedCity) {
alert('选择的市区不存在或类型错误,请重新选择')
return
}
//
const newCampus = {
areaName: formData.value.areaName,
areaType: 'campus' as const,
parentAreaId: formData.value.parentAreaId, // ID
areaType: 'campus',
parentAreaId: formData.value.parentAreaId,
address: formData.value.address,
manager: formData.value.manager,
managerPhone: formData.value.managerPhone
}
console.log('发送的校区数据:', newCampus) //
response = await request<{
code: number
msg: string
data: Area
}>('/api/web/area/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}`
},
body: JSON.stringify(newCampus)
})
@ -567,7 +596,7 @@ const handleSave = async () => {
} else {
const errorMsg = response?.msg || `新增失败(错误码:${response?.code || '未知'}`
console.error('新增校区失败:', errorMsg)
alert(`新增校区失败:${errorMsg}`)
alert(`新增校区失败:${ errorMsg}`)
}
}
} catch (error: any) {
@ -577,7 +606,7 @@ const handleSave = async () => {
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '保存失败,请稍后重试'
alert(`保存校区失败:${errorMsg}`)
alert(`保存校区失败:${ errorMsg}`)
} finally {
saving.value = false
}

@ -143,10 +143,70 @@
<label>安装日期:</label>
<input v-model="currentTerminal.installDate" type="date">
</div>
<!-- 市区选择 -->
<div class="form-group" v-if="!isEditing">
<label>选择市区:</label>
<select
v-model="selectedCityId"
@change="onCityChange"
class="select-input"
>
<option value="">请选择市区</option>
<option
v-for="city in cityList"
:key="city.areaId"
:value="city.areaId"
>
{{ city.areaName }}
</option>
</select>
</div>
<!-- 校区选择 -->
<div class="form-group" v-if="!isEditing && selectedCityId">
<label>选择校区:</label>
<select
v-model="selectedCampusId"
@change="onCampusChange"
class="select-input"
:disabled="!campusList.length"
>
<option value="">请选择校区</option>
<option
v-for="campus in campusList"
:key="campus.areaId"
:value="campus.areaId"
>
{{ campus.areaName }}
</option>
</select>
<p v-if="selectedCityId && !campusList.length" class="no-data-message">
该市区暂无校区
</p>
</div>
<div class="form-group">
<label>关联设备ID:</label>
<input v-model="currentTerminal.deviceId" type="text">
<select
v-model="currentTerminal.deviceId"
class="select-input"
:disabled="!filteredSupplyDevices.length"
>
<option value="">请选择供水机</option>
<option
v-for="device in filteredSupplyDevices"
:key="device.deviceId"
:value="device.deviceId"
>
{{ device.deviceId }} - {{ device.installLocation }} ({{ device.deviceName }})
</option>
</select>
<p v-if="selectedCampusId && !filteredSupplyDevices.length" class="no-data-message">
该校区暂无可用的供水机
</p>
</div>
<div class="form-actions">
<button type="button" @click="showAddModal = false">取消</button>
<button type="submit">{{ isEditing ? '更新' : '添加' }}</button>
@ -175,7 +235,6 @@ const isTerminalManageVO = (obj: any): obj is TerminalManageVO => {
['active', 'inactive', 'warning', 'fault'].includes(obj.terminalStatus)
}
//
type TerminalStatus = 'active' | 'inactive' | 'fault' | 'warning'
@ -188,6 +247,28 @@ interface TerminalManageVO {
terminalStatus: TerminalStatus
installDate?: string
deviceId?: string
areaId?: string
}
//
interface Device {
deviceId: string
deviceName: string
installLocation: string
deviceType: string
status: string
areaId: string // ID
}
//
interface Area {
areaId: string
areaName: string
areaType: string
parentAreaId: string | null
address: string
manager: string
managerPhone: string
}
//
@ -209,10 +290,19 @@ const currentTerminal = ref<TerminalManageVO>({
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
terminalStatus: 'active',
deviceId: undefined
})
//
//
const availableSupplyDevices = ref<Device[]>([])
//
const cityList = ref<Area[]>([]) //
const campusList = ref<Area[]>([]) //
const selectedCityId = ref('') // ID
const selectedCampusId = ref('') // ID
//
const loadTerminals = async (): Promise<void> => {
try {
@ -227,10 +317,7 @@ const loadTerminals = async (): Promise<void> => {
console.log('开始加载终端机数据...')
//
const result = await request<ResultVO<TerminalManageVO[]>>(
`/api/web/terminal/list`,
{ method: 'GET' }
)
const result = await request<ResultVO<TerminalManageVO[]>>(`/api/web/terminal/list`, { method: 'GET' })
console.log('终端机请求结果:', result)
@ -285,7 +372,191 @@ const loadTerminals = async (): Promise<void> => {
}
}
//
const loadCityList = async (): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
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()
router.push('/login')
}
}
}
// ID
const loadCampusListByCity = async (cityId: string): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
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()
router.push('/login')
}
}
}
//
const loadAvailableSupplyDevices = async (): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
console.log('开始加载设备列表...')
// 使
const params = new URLSearchParams();
params.append('deviceType', 'water_supply'); //
params.append('status', 'online'); // 线
const queryString = params.toString();
const url = `/api/web/device-status/by-type${queryString ? `?${queryString}` : ''}`;
const result = await request<any>(url, { method: 'GET' })
if (result && typeof result === 'object' && 'code' in result) {
if (result.code === 200 && result.data && Array.isArray(result.data)) {
// 使
// loadAvailableSupplyDevices
availableSupplyDevices.value = result.data.map((device: any) => ({
deviceId: device.deviceId,
deviceName: device.deviceName,
installLocation: device.installLocation,
deviceType: device.deviceType,
status: device.status,
areaId: device.areaId ? device.areaId.split('-')[0] : '' //
}))
console.log(`获取到${availableSupplyDevices.value.length}个可用供水机`)
} else {
console.warn('API响应非成功状态或数据格式错误:', result)
availableSupplyDevices.value = []
}
} else if (Array.isArray(result)) {
//
availableSupplyDevices.value = (result as any[]).map((device: any) => ({
deviceId: device.deviceId,
deviceName: device.deviceName,
installLocation: device.installLocation,
deviceType: device.deviceType,
status: device.status,
areaId: device.areaId // ID
}))
} else {
console.warn('API响应数据格式错误:', result)
availableSupplyDevices.value = []
}
} catch (error) {
console.error('加载可用供水机列表失败:', error)
availableSupplyDevices.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const filteredSupplyDevices = computed(() => {
if (!selectedCampusId.value) {
return availableSupplyDevices.value
}
// area_name
const selectedCampus = campusList.value.find(campus => campus.areaId === selectedCampusId.value)
if (!selectedCampus) {
return availableSupplyDevices.value
}
// 使 area_name areaId
return availableSupplyDevices.value.filter(device => {
// areaId area_name
return device.areaId === selectedCampus.areaName
})
})
//
const onCityChange = async () => {
// ID
selectedCampusId.value = ''
currentTerminal.value.deviceId = undefined
campusList.value = []
if (selectedCityId.value) {
await loadCampusListByCity(selectedCityId.value)
}
}
//
const onCampusChange = () => {
// ID
currentTerminal.value.deviceId = undefined
// areaIdareaNameareaId
const selectedCampus = campusList.value.find(campus => campus.areaId === selectedCampusId.value)
if (selectedCampus) {
currentTerminal.value.areaId = selectedCampus.areaName // 使areaNameareaId
} else {
currentTerminal.value.areaId = undefined
}
}
//
@ -345,9 +616,18 @@ const editTerminal = (terminal: TerminalManageVO) => {
currentTerminal.value = { ...terminal }
isEditing.value = true
showAddModal.value = true
//
if (availableSupplyDevices.value.length === 0) {
loadAvailableSupplyDevices()
}
//
if (cityList.value.length === 0) {
loadCityList()
}
}
//
//
const deleteTerminal = async (terminalId: string) => {
if (!confirm(`确定要删除终端 ${terminalId} 吗?`)) {
@ -392,7 +672,6 @@ const deleteTerminal = async (terminalId: string) => {
}
}
//
const saveTerminal = async () => {
try {
@ -404,6 +683,24 @@ const saveTerminal = async () => {
return
}
//
if (!isEditing.value && !selectedCampusId.value) {
alert('请选择校区')
return
}
// areaId
if (!currentTerminal.value.areaId) {
alert('请先选择校区以确定所属片区')
return
}
//
if (!currentTerminal.value.deviceId) {
alert('请选择关联的供水机')
return
}
let result: ResultVO<TerminalManageVO> | TerminalManageVO
if (isEditing.value) {
//
@ -413,6 +710,18 @@ const saveTerminal = async () => {
})
} else {
//
//
if (!selectedCampusId.value) {
alert('请选择校区')
return
}
//
if (!currentTerminal.value.deviceId) {
alert('请选择关联的供水机')
return
}
result = await request<ResultVO<TerminalManageVO> | TerminalManageVO>('/api/web/terminal/add', {
method: 'POST',
body: JSON.stringify(currentTerminal.value)
@ -434,8 +743,12 @@ const saveTerminal = async () => {
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
terminalStatus: 'active',
deviceId: undefined
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
} else if (result.code === 200) {
@ -451,8 +764,12 @@ const saveTerminal = async () => {
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
terminalStatus: 'active',
deviceId: undefined
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
} else {
@ -472,8 +789,12 @@ const saveTerminal = async () => {
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
terminalStatus: 'active',
deviceId: undefined
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
}
@ -490,8 +811,12 @@ const saveTerminal = async () => {
terminalName: '',
longitude: 0,
latitude: 0,
terminalStatus: 'active'
terminalStatus: 'active',
deviceId: undefined
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
alert(isEditing.value ? '终端更新成功' : '终端添加成功')
}
@ -509,6 +834,8 @@ const saveTerminal = async () => {
onMounted(async () => {
console.log('🚀 开始加载终端数据...')
await loadTerminals()
await loadAvailableSupplyDevices()
await loadCityList()
})
</script>
@ -788,6 +1115,29 @@ onMounted(async () => {
border: none;
}
/* 新增:下拉框样式 */
.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;
font-size: 12px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.filters {

@ -84,27 +84,7 @@
@click="updateDeviceStatus(device.deviceId, 'online')"
:disabled="device.status === 'online'"
>
设为在线
</button>
<button
class="btn-offline"
@click="showOfflineModal(device.deviceId)"
:disabled="device.status === 'offline'"
>
设为离线
</button>
<button
class="btn-fault"
@click="showFaultModalFunc(device.deviceId)"
:disabled="device.status === 'fault'"
>
设为故障
</button>
<button
class="btn-delete"
@click="deleteDevice(device.deviceId)"
>
删除
</button>
</td>
@ -150,13 +130,49 @@
<label>设备名称:</label>
<input v-model="newDevice.deviceName" type="text" required>
</div>
<!-- 市区选择 -->
<div class="form-group">
<label>所属片区:</label>
<select v-model="newDevice.areaId" required>
<option value="A区">A</option>
<option value="B区">B</option>
<label>选择市区:</label>
<select
v-model="selectedCityId"
@change="onCityChange"
class="select-input"
>
<option value="">请选择市区</option>
<option
v-for="city in cityList"
:key="city.areaId"
:value="city.areaId"
>
{{ city.areaName }}
</option>
</select>
</div>
<!-- 校区选择 -->
<div class="form-group" v-if="selectedCityId">
<label>选择校区:</label>
<select
v-model="selectedCampusId"
@change="onCampusChange"
class="select-input"
:disabled="!campusList.length"
>
<option value="">请选择校区</option>
<option
v-for="campus in campusList"
:key="campus.areaId"
:value="campus.areaId"
>
{{ campus.areaName }}
</option>
</select>
<p v-if="selectedCityId && !campusList.length" class="no-data-message">
该市区暂无校区
</p>
</div>
<div class="form-group">
<label>安装位置:</label>
<input v-model="newDevice.installLocation" type="text" required>
@ -231,6 +247,17 @@ interface WaterMakerDevice {
createTime?: string
}
//
interface Area {
areaId: string
areaName: string
areaType: string
parentAreaId: string | null
address: string
manager: string
managerPhone: string
}
//
const devices = ref<WaterMakerDevice[]>([])
const searchKeyword = ref('')
@ -253,11 +280,17 @@ const currentDeviceId = ref('')
const newDevice = ref({
deviceId: '',
deviceName: '',
areaId: '市区',
areaId: '',
installLocation: '',
deviceType: 'water_maker'
})
//
const cityList = ref<Area[]>([]) //
const campusList = ref<Area[]>([]) //
const selectedCityId = ref('') // ID
const selectedCampusId = ref('') // ID
const offlineReason = ref('')
const faultInfo = ref({
faultType: '',
@ -310,6 +343,104 @@ const loadDevices = async (): Promise<void> => {
}
}
//
const loadCityList = async (): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
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()
router.push('/login')
}
}
}
// ID
const loadCampusListByCity = async (cityId: string): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
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()
router.push('/login')
}
}
}
//
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)
if (selectedCampus) {
newDevice.value.areaId = selectedCampus.areaName // 使areaNameareaId
} else {
newDevice.value.areaId = ''
}
}
//
const filteredDevices = computed(() => {
return devices.value.filter(device => {
@ -549,6 +680,18 @@ const addDevice = async () => {
return
}
//
if (!selectedCampusId.value) {
alert('请选择校区')
return
}
// areaId
if (!newDevice.value.areaId) {
alert('请先选择校区以确定所属片区')
return
}
//
const deviceToAdd = {
deviceId: newDevice.value.deviceId,
@ -573,10 +716,13 @@ const addDevice = async () => {
newDevice.value = {
deviceId: '',
deviceName: '',
areaId: '市区',
areaId: '',
installLocation: '',
deviceType: 'water_maker'
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
alert('设备添加成功')
} else {
@ -596,6 +742,7 @@ const addDevice = async () => {
onMounted(async () => {
console.log('🚀 开始加载设备数据...')
await loadDevices()
await loadCityList()
})
</script>
@ -880,6 +1027,29 @@ onMounted(async () => {
border: none;
}
/* 新增:下拉框样式 */
.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;
font-size: 12px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.filters {

@ -47,10 +47,7 @@
<span class="label">创建时间:</span>
<span class="value">{{ formatDate(deviceDetail.deviceInfo?.createTime) }}</span>
</div>
<div class="detail-item">
<span class="label">最后心跳时间:</span>
<span class="value">{{ formatDate(deviceDetail.deviceInfo?.lastHeartbeatTime) }}</span>
</div>
</div>
</div>
@ -111,6 +108,26 @@
<span class="value">{{ formatDate(deviceDetail.realtimeData?.recordTime) }}</span>
</div>
</div>
<!-- TDS值柱状图 -->
<div class="chart-container">
<h4>TDS值对比</h4>
<div id="tds-chart" class="chart"></div>
</div>
<!-- 滤芯寿命百分比条码 -->
<div class="chart-container">
<h4>滤芯寿命</h4>
<div class="progress-bar-container">
<div class="progress-bar">
<div
class="progress-bar-fill"
:style="{ width: `${deviceDetail.realtimeData?.filterLife || 0}%` }"
></div>
</div>
<div class="progress-text">{{ deviceDetail.realtimeData?.filterLife || 0 }}%</div>
</div>
</div>
</div>
<!-- 关联的供水机列表卡片 -->
@ -169,11 +186,12 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch, onUnmounted, nextTick } 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'
import * as echarts from 'echarts'
// -
interface DeviceInfo {
@ -230,6 +248,9 @@ const loading = ref(true)
const loadingSuppliers = ref(false) //
const error = ref('')
//
const tdsChart = ref<echarts.ECharts | null>(null)
// ID
const deviceId = route.params.id as string
@ -319,6 +340,72 @@ const loadSupplierDevices = async (makerId: string) => {
}
}
// TDS
const initTDSChart = () => {
const chartDom = document.getElementById('tds-chart')
if (!chartDom) return
tdsChart.value = echarts.init(chartDom)
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['原水TDS', '纯水TDS', '矿化水TDS']
},
yAxis: {
type: 'value',
name: 'ppm'
},
series: [{
data: [
deviceDetail.value?.realtimeData?.tdsValue1 || 0,
deviceDetail.value?.realtimeData?.tdsValue2 || 0,
deviceDetail.value?.realtimeData?.tdsValue3 || 0
],
type: 'bar',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2378f7' },
{ offset: 0.7, color: '#2378f7' },
{ offset: 1, color: '#83bff6' }
])
}
},
//
label: {
show: true,
position: 'top',
formatter: '{c} ppm', //
color: '#333',
fontSize: 12
}
}]
}
tdsChart.value.setOption(option)
}
//
const loadDeviceDetail = async () => {
try {
@ -346,6 +433,11 @@ const loadDeviceDetail = async () => {
if (result.data.deviceInfo?.deviceType === 'water_maker') {
await loadSupplierDevices(deviceId)
}
//
nextTick(() => {
initTDSChart()
})
} else {
error.value = result.message || '获取设备详情失败'
}
@ -361,6 +453,21 @@ const loadDeviceDetail = async () => {
}
}
//
watch(() => deviceDetail.value?.realtimeData, (newData) => {
if (newData && tdsChart.value) {
tdsChart.value.setOption({
series: [{
data: [
newData.tdsValue1 || 0,
newData.tdsValue2 || 0,
newData.tdsValue3 || 0
]
}]
})
}
}, { deep: true })
//
onMounted(() => {
if (deviceId) {
@ -370,8 +477,16 @@ onMounted(() => {
loading.value = false
}
})
//
onUnmounted(() => {
if (tdsChart.value) {
tdsChart.value.dispose()
}
})
</script>
<style scoped>
.water-maker-detail-page {
padding: 20px;
@ -555,4 +670,58 @@ onMounted(() => {
font-weight: normal;
margin-left: 8px;
}
/* 图表相关样式 */
.chart-container {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #eee;
}
.chart-container h4 {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
}
.chart {
height: 300px;
width: 100%;
}
.progress-bar-container {
display: flex;
align-items: center;
gap: 16px;
}
.progress-bar {
flex: 1;
height: 24px;
background-color: #f0f0f0;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #42b983, #359e75);
transition: width 0.3s ease;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
color: white;
font-size: 12px;
font-weight: bold;
}
.progress-text {
min-width: 40px;
text-align: center;
font-weight: bold;
color: #333;
}
</style>

@ -115,6 +115,25 @@
<span class="value">{{ formatDate(deviceDetail.realtimeData?.timestamp) }}</span>
</div>
</div>
<!-- 实时监控数据 - 并列显示三个仪表盘 -->
<div class="chart-container">
<h4>实时监控数据</h4>
<div class="charts-row">
<div class="chart-item">
<h5>水压监控</h5>
<div id="pressure-chart" class="chart"></div>
</div>
<div class="chart-item">
<h5>流量监控</h5>
<div id="flow-chart" class="chart"></div>
</div>
<div class="chart-item">
<h5>温度监控</h5>
<div id="temperature-chart" class="chart"></div>
</div>
</div>
</div>
</div>
<!-- 加载中提示 -->
@ -130,11 +149,12 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, nextTick, onUnmounted } 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'
import * as echarts from 'echarts'
// -
interface DeviceInfo {
@ -174,6 +194,11 @@ const associatedMaker = ref<DeviceInfo | null>(null) // 关联的制水机信息
const loading = ref(true)
const error = ref('')
//
const pressureChart = ref<echarts.ECharts | null>(null)
const flowChart = ref<echarts.ECharts | null>(null)
const temperatureChart = ref<echarts.ECharts | null>(null)
// ID
const deviceId = route.params.id as string
@ -226,6 +251,99 @@ const goBack = () => {
router.go(-1)
}
//
const initPressureChart = () => {
const chartDom = document.getElementById('pressure-chart')
if (!chartDom) return
pressureChart.value = echarts.init(chartDom)
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} MPa'
},
series: [{
name: '水压',
type: 'gauge',
min: 0,
max: 2,
detail: {
formatter: '{value} MPa',
fontSize: 18
},
data: [{
value: deviceDetail.value?.realtimeData?.waterPress || 0,
name: '当前水压'
}]
}]
}
pressureChart.value.setOption(option)
}
//
const initFlowChart = () => {
const chartDom = document.getElementById('flow-chart')
if (!chartDom) return
flowChart.value = echarts.init(chartDom)
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} m³/h'
},
series: [{
name: '流量',
type: 'gauge',
min: 0,
max: 100,
detail: {
formatter: '{value} m³/h',
fontSize: 18
},
data: [{
value: deviceDetail.value?.realtimeData?.waterFlow || 0,
name: '当前流量'
}]
}]
}
flowChart.value.setOption(option)
}
//
const initTemperatureChart = () => {
const chartDom = document.getElementById('temperature-chart')
if (!chartDom) return
temperatureChart.value = echarts.init(chartDom)
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} °C'
},
series: [{
name: '温度',
type: 'gauge',
min: 0,
max: 50,
detail: {
formatter: '{value} °C',
fontSize: 18
},
data: [{
value: deviceDetail.value?.realtimeData?.temperature || 0,
name: '当前温度'
}]
}]
}
temperatureChart.value.setOption(option)
}
//
const loadDeviceDetail = async () => {
try {
@ -253,6 +371,13 @@ const loadDeviceDetail = async () => {
if (result.data.deviceInfo.parentMakerId) {
await loadAssociatedMaker(result.data.deviceInfo.parentMakerId)
}
//
nextTick(() => {
initPressureChart()
initFlowChart()
initTemperatureChart()
})
} else {
error.value = result.message || '获取设备详情失败'
}
@ -268,51 +393,6 @@ const loadDeviceDetail = async () => {
}
}
//
const loadAvailableMakers = async () => {
if (!newDevice.value.areaId) {
availableMakers.value = []
return
}
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
//
const params = new URLSearchParams();
params.append('areaId', newDevice.value.areaId);
params.append('deviceType', 'water_maker');
const queryString = params.toString();
const url = `/api/web/device-status/by-type${queryString ? `?${queryString}` : ''}`;
const response = await request<ResultVO<any[]>>(url, { method: 'GET' });
if (response.code === 200 && response.data && Array.isArray(response.data)) {
// ID
availableMakers.value = response.data.map((maker: any) => ({
id: maker.deviceId,
name: `${maker.deviceId} - ${maker.installLocation}`
}))
} else {
console.error('获取制水机列表失败:', response.message);
availableMakers.value = []
}
} catch (error) {
console.error('加载制水机列表失败:', error);
availableMakers.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const loadAssociatedMaker = async (makerId: string) => {
try {
@ -351,6 +431,19 @@ onMounted(() => {
loading.value = false
}
})
//
onUnmounted(() => {
if (pressureChart.value) {
pressureChart.value.dispose()
}
if (flowChart.value) {
flowChart.value.dispose()
}
if (temperatureChart.value) {
temperatureChart.value.dispose()
}
})
</script>
<style scoped>
@ -488,4 +581,52 @@ onMounted(() => {
padding: 16px;
}
}
/* 图表相关样式 */
.chart-container {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #eee;
}
.chart-container h4 {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
}
.chart-item {
flex: 1;
min-width: 0; /* 防止flex项目溢出 */
}
.chart-item h5 {
margin: 0 0 12px 0;
font-size: 14px;
color: #333;
text-align: center;
}
.chart {
height: 250px;
width: 100%;
}
/* 并列布局 */
.charts-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
/* 响应式:在小屏幕上单列显示 */
@media (max-width: 1024px) {
.charts-row {
flex-direction: column;
}
.chart {
height: 300px;
}
}
</style>

@ -160,8 +160,8 @@
<label class="form-label required">维修片区</label>
<select v-model="form.areaId" required>
<option value="">请选择片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
<option value="A">A</option>
<option value="B">B</option>
</select>
</div>
<div class="form-group">

Loading…
Cancel
Save