feat: UI rewrite R4-R5 (17 pages)
- R4: equipment+project+QR+notice (8) - R5: pshgl drain user management (9) All pages: consistent design system
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
|||||||
Picker,
|
Picker,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
ActionSheet,
|
ActionSheet,
|
||||||
|
Checkbox,
|
||||||
} from 'vant'
|
} from 'vant'
|
||||||
|
|
||||||
// ── Vant 样式(组件样式按需引入) ──
|
// ── Vant 样式(组件样式按需引入) ──
|
||||||
@@ -124,6 +125,7 @@ const vantComponents = [
|
|||||||
Picker,
|
Picker,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
ActionSheet,
|
ActionSheet,
|
||||||
|
Checkbox,
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const component of vantComponents) {
|
for (const component of vantComponents) {
|
||||||
|
|||||||
@@ -497,6 +497,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: '检查报告',
|
title: '检查报告',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/pshCheckDetail/:detail?',
|
||||||
|
name: 'PshCheckDetail',
|
||||||
|
component: () => import('@/views/pshgl/pshCheckDetail.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '检查详情',
|
||||||
|
},
|
||||||
|
},
|
||||||
// ── 地图模块 ──
|
// ── 地图模块 ──
|
||||||
{
|
{
|
||||||
path: '/map',
|
path: '/map',
|
||||||
|
|||||||
@@ -9,11 +9,22 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const deviceId = computed(() => route.params.id as string | undefined)
|
const deviceId = computed(() => route.params.id as string | undefined)
|
||||||
|
|
||||||
/** 模拟设备详情 */
|
interface DeviceDetail {
|
||||||
const device = ref({
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
model: string
|
||||||
|
location: string
|
||||||
|
status: 'normal' | 'alarm' | 'offline'
|
||||||
|
installDate: string
|
||||||
|
lastMaintain: string
|
||||||
|
protocol: string
|
||||||
|
battery: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = ref<DeviceDetail>({
|
||||||
id: deviceId.value || 'DEV-001',
|
id: deviceId.value || 'DEV-001',
|
||||||
name: '流量监测仪 A-01',
|
name: '流量监测仪 A-01',
|
||||||
type: '流量监测',
|
type: '流量监测',
|
||||||
@@ -26,59 +37,90 @@ const device = ref({
|
|||||||
battery: '85%',
|
battery: '85%',
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusMap: Record<string, string> = {
|
const statusCfg: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
normal: '正常',
|
normal: { label: '正常', color: '#07C160', bg: '#E8F8EF' },
|
||||||
alarm: '告警',
|
alarm: { label: '告警', color: '#EE0A24', bg: '#FDECEC' },
|
||||||
offline: '离线',
|
offline: { label: '离线', color: '#969799', bg: '#F2F3F5' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColorMap: Record<string, string> = {
|
function goMaintenance() {
|
||||||
normal: '#07c160',
|
router.push('/maintenanceRecords')
|
||||||
alarm: '#ee0a24',
|
|
||||||
offline: '#999',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
function goMaintenance() {
|
|
||||||
// 跳转到养护记录
|
|
||||||
router.push('/maintenanceRecords')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="device-detail-page">
|
<div class="page">
|
||||||
<van-nav-bar title="设备详情" left-arrow fixed placeholder @click-left="goBack" />
|
<!-- NavBar -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="设备详情"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="goBack"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 状态卡片 -->
|
<!-- Status Banner -->
|
||||||
<div class="status-card">
|
<div class="status-banner">
|
||||||
<span class="status-dot" :style="{ backgroundColor: statusColorMap[device.status] }" />
|
<span class="status-dot" :style="{ background: statusCfg[device.status].color }"></span>
|
||||||
<span class="status-text" :style="{ color: statusColorMap[device.status] }">
|
<span
|
||||||
{{ statusMap[device.status] }}
|
class="status-text"
|
||||||
|
:style="{ color: statusCfg[device.status].color }"
|
||||||
|
>
|
||||||
|
{{ statusCfg[device.status].label }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="status-name">{{ device.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<!-- Section: 基本信息 -->
|
||||||
<van-cell-group title="基本信息" inset>
|
<div class="section">
|
||||||
<van-cell title="设备编号" :value="device.id" />
|
<div class="section-header">
|
||||||
<van-cell title="设备名称" :value="device.name" />
|
<span class="section-accent"></span>
|
||||||
<van-cell title="设备类型" :value="device.type" />
|
<span class="section-title">基本信息</span>
|
||||||
<van-cell title="设备型号" :value="device.model" />
|
</div>
|
||||||
<van-cell title="安装位置" :label="device.location" />
|
<div class="card">
|
||||||
<van-cell title="安装日期" :value="device.installDate" />
|
<div class="info-grid">
|
||||||
<van-cell title="通信协议" :value="device.protocol" />
|
<div class="info-item">
|
||||||
<van-cell title="电池电量" :value="device.battery" />
|
<span class="info-label">设备编号</span>
|
||||||
</van-cell-group>
|
<span class="info-value">{{ device.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">设备类型</span>
|
||||||
|
<span class="info-value">{{ device.type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">设备型号</span>
|
||||||
|
<span class="info-value">{{ device.model }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">通信协议</span>
|
||||||
|
<span class="info-value">{{ device.protocol }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">安装日期</span>
|
||||||
|
<span class="info-value">{{ device.installDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">电池电量</span>
|
||||||
|
<span class="info-value highlight">{{ device.battery }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">安装位置</span>
|
||||||
|
<span class="info-value">{{ device.location }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">上次养护</span>
|
||||||
|
<span class="info-value">{{ device.lastMaintain }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 最近养护 -->
|
<!-- Action Button -->
|
||||||
<van-cell-group title="最近养护" inset>
|
<div class="action-area">
|
||||||
<van-cell title="上次养护日期" :value="device.lastMaintain" />
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<div class="action-section">
|
|
||||||
<van-button type="primary" block round @click="goMaintenance">
|
<van-button type="primary" block round @click="goMaintenance">
|
||||||
查看养护记录
|
查看养护记录
|
||||||
</van-button>
|
</van-button>
|
||||||
@@ -87,50 +129,134 @@ function goMaintenance() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.device-detail-page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 24px;
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
|
||||||
background: var(--color-primary);
|
|
||||||
--van-nav-bar-title-text-color: #fff;
|
|
||||||
--van-nav-bar-text-color: #fff;
|
|
||||||
--van-nav-bar-icon-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.van-cell-group) {
|
|
||||||
margin: 12px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.van-cell-group__title) {
|
|
||||||
padding: 12px 16px 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
// ── NavBar ──
|
||||||
|
:deep(.van-nav-bar) {
|
||||||
|
background: #1E74FF;
|
||||||
|
--van-nav-bar-title-text-color: #fff;
|
||||||
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Status Banner ──
|
||||||
|
.status-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 12px 16px;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--color-bg-card);
|
margin: 12px;
|
||||||
|
background: #fff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #646566;
|
||||||
|
margin-left: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-section {
|
// ── Section ──
|
||||||
padding: 20px 16px;
|
.section {
|
||||||
|
margin: 0 12px 10px;
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-accent {
|
||||||
|
width: 3px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #1E74FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Card ──
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Info Grid ──
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #F2F3F5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #07C160;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Action Button ──
|
||||||
|
.action-area {
|
||||||
|
padding: 20px 28px;
|
||||||
|
|
||||||
|
:deep(.van-button--primary) {
|
||||||
|
background: #1E74FF;
|
||||||
|
border-color: #1E74FF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,11 +9,21 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const pshId = computed(() => route.params.id as string | undefined)
|
const pshId = computed(() => route.params.id as string | undefined)
|
||||||
|
|
||||||
/** 模拟排水户详情 */
|
interface PshDetail {
|
||||||
const detail = ref({
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
address: string
|
||||||
|
contact: string
|
||||||
|
phone: string
|
||||||
|
licenseNo: string
|
||||||
|
drainType: string
|
||||||
|
qrCodeUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const detail = ref<PshDetail>({
|
||||||
id: pshId.value || 'PSH-001',
|
id: pshId.value || 'PSH-001',
|
||||||
name: '万达广场(城北店)',
|
name: '万达广场(城北店)',
|
||||||
type: '商业',
|
type: '商业',
|
||||||
@@ -22,94 +32,208 @@ const detail = ref({
|
|||||||
phone: '138****5678',
|
phone: '138****5678',
|
||||||
licenseNo: '排许字第2024-0031号',
|
licenseNo: '排许字第2024-0031号',
|
||||||
drainType: '雨污分流',
|
drainType: '雨污分流',
|
||||||
qrCodeUrl: '', // 实际从接口获取
|
qrCodeUrl: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="psh-detail-page">
|
<div class="page">
|
||||||
<van-nav-bar title="排水户详情" left-arrow fixed placeholder @click-left="goBack" />
|
<!-- NavBar -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="排水户详情"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<!-- QR Code Card -->
|
||||||
<van-cell-group title="基本信息" inset>
|
<div class="qr-card">
|
||||||
<van-cell title="排水户编号" :value="detail.id" />
|
<div class="qr-box">
|
||||||
<van-cell title="排水户名称" :value="detail.name" />
|
<van-image
|
||||||
<van-cell title="排水户类型" :value="detail.type" />
|
v-if="detail.qrCodeUrl"
|
||||||
<van-cell title="地址" :value="detail.address" />
|
:src="detail.qrCodeUrl"
|
||||||
<van-cell title="联系人" :value="detail.contact" />
|
width="160"
|
||||||
<van-cell title="联系电话" :value="detail.phone" />
|
height="160"
|
||||||
<van-cell title="排水许可证" :value="detail.licenseNo" />
|
fit="contain"
|
||||||
<van-cell title="排水方式" :value="detail.drainType" />
|
/>
|
||||||
</van-cell-group>
|
<template v-else>
|
||||||
|
<van-icon name="qr" size="72" color="#1E74FF" />
|
||||||
|
<p class="qr-hint">扫描二维码查看排水户信息</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="type-tag"
|
||||||
|
:style="{ color: '#1E74FF', background: '#E3F2FD' }"
|
||||||
|
>
|
||||||
|
{{ detail.drainType }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 二维码 -->
|
<!-- Section: 基本信息 -->
|
||||||
<van-cell-group title="排水户二维码" inset>
|
<div class="section">
|
||||||
<div class="qr-section">
|
<div class="section-header">
|
||||||
<div class="qr-placeholder">
|
<span class="section-accent"></span>
|
||||||
<van-image
|
<span class="section-title">基本信息</span>
|
||||||
v-if="detail.qrCodeUrl"
|
</div>
|
||||||
:src="detail.qrCodeUrl"
|
<div class="card">
|
||||||
width="160"
|
<div class="info-grid">
|
||||||
height="160"
|
<div class="info-item">
|
||||||
fit="contain"
|
<span class="info-label">排水户编号</span>
|
||||||
/>
|
<span class="info-value">{{ detail.id }}</span>
|
||||||
<template v-else>
|
</div>
|
||||||
<van-icon name="qr" size="64" color="#ccc" />
|
<div class="info-item full-width">
|
||||||
<p class="qr-hint">扫码查看排水户信息</p>
|
<span class="info-label">排水户名称</span>
|
||||||
</template>
|
<span class="info-value">{{ detail.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">排水户类型</span>
|
||||||
|
<span class="info-value">{{ detail.type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">地址</span>
|
||||||
|
<span class="info-value">{{ detail.address }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">联系人</span>
|
||||||
|
<span class="info-value">{{ detail.contact }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">联系电话</span>
|
||||||
|
<span class="info-value">{{ detail.phone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">排水许可证</span>
|
||||||
|
<span class="info-value">{{ detail.licenseNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">排水方式</span>
|
||||||
|
<span class="info-value">{{ detail.drainType }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-cell-group>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.psh-detail-page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 24px;
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
|
||||||
background: var(--color-primary);
|
|
||||||
--van-nav-bar-title-text-color: #fff;
|
|
||||||
--van-nav-bar-text-color: #fff;
|
|
||||||
--van-nav-bar-icon-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.van-cell-group) {
|
|
||||||
margin: 12px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.van-cell-group__title) {
|
|
||||||
padding: 12px 16px 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-section {
|
// ── NavBar ──
|
||||||
display: flex;
|
:deep(.van-nav-bar) {
|
||||||
justify-content: center;
|
background: #1E74FF;
|
||||||
padding: 24px;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.qr-placeholder {
|
// ── QR Card ──
|
||||||
width: 160px;
|
.qr-card {
|
||||||
height: 160px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 12px;
|
||||||
|
padding: 24px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.qr-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 2px dashed #ddd;
|
width: 180px;
|
||||||
border-radius: 10px;
|
height: 180px;
|
||||||
background: #fafafa;
|
border: 2px dashed #1E74FF;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #F7FAFF;
|
||||||
|
}
|
||||||
|
|
||||||
.qr-hint {
|
.qr-hint {
|
||||||
margin: 8px 0 0 0;
|
margin: 10px 0 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-placeholder);
|
color: #969799;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.type-tag {
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Section ──
|
||||||
|
.section {
|
||||||
|
margin: 0 12px 10px;
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-accent {
|
||||||
|
width: 3px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #1E74FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Card ──
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Info Grid ──
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #F2F3F5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,11 +10,31 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const detailId = (route.query.id as string) || '1'
|
const detailId = (route.query.id as string) || '1'
|
||||||
|
|
||||||
/** 模拟设备详情 */
|
interface EquipDetail {
|
||||||
const equipment = ref({
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
typeLabel: string
|
||||||
|
model: string
|
||||||
|
sn: string
|
||||||
|
status: 'normal' | 'alarm' | 'offline'
|
||||||
|
location: string
|
||||||
|
installDate: string
|
||||||
|
manufacturer: string
|
||||||
|
range: string
|
||||||
|
precision: string
|
||||||
|
protocol: string
|
||||||
|
ipRating: string
|
||||||
|
lastData: string
|
||||||
|
lastUpdateTime: string
|
||||||
|
batteryLevel: number
|
||||||
|
signalStrength: number
|
||||||
|
maintenanceRecords: { date: string; type: string; result: string; operator: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const equipment = ref<EquipDetail>({
|
||||||
id: detailId,
|
id: detailId,
|
||||||
name: '流量监测仪 A-01',
|
name: '流量监测仪 A-01',
|
||||||
type: 'flow_meter',
|
type: 'flow_meter',
|
||||||
@@ -40,94 +60,343 @@ const equipment = ref({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusMap: Record<string, string> = {
|
const statusCfg: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
normal: '正常',
|
normal: { label: '正常', color: '#07C160', bg: '#E8F8EF' },
|
||||||
alarm: '报警',
|
alarm: { label: '报警', color: '#EE0A24', bg: '#FDECEC' },
|
||||||
offline: '离线',
|
offline: { label: '离线', color: '#969799', bg: '#F2F3F5' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColorMap: Record<string, string> = {
|
function isGoodResult(r: string) {
|
||||||
normal: 'success',
|
return r === '正常' || r === '合格' || r === '已完成'
|
||||||
alarm: 'danger',
|
|
||||||
offline: '#999',
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="equipment-detail-page">
|
<div class="page">
|
||||||
<van-nav-bar title="设备详情" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
|
<!-- NavBar -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="设备详情"
|
||||||
|
left-text="返回"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 状态标签 -->
|
<!-- Status Banner -->
|
||||||
<div class="status-bar">
|
<div class="status-banner">
|
||||||
<van-tag :type="statusColorMap[equipment.status] as never" size="large">
|
<span
|
||||||
{{ statusMap[equipment.status] }}
|
class="status-badge"
|
||||||
</van-tag>
|
:style="{ color: statusCfg[equipment.status].color, background: statusCfg[equipment.status].bg }"
|
||||||
|
>
|
||||||
|
{{ statusCfg[equipment.status].label }}
|
||||||
|
</span>
|
||||||
|
<span class="status-name">{{ equipment.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<!-- Section: 基本信息 -->
|
||||||
<van-cell-group title="基本信息" class="info-group">
|
<div class="section">
|
||||||
<van-cell title="设备名称" :value="equipment.name" />
|
<div class="section-header">
|
||||||
<van-cell title="设备类型" :value="equipment.typeLabel" />
|
<span class="section-accent"></span>
|
||||||
<van-cell title="设备型号" :value="equipment.model" />
|
<span class="section-title">基本信息</span>
|
||||||
<van-cell title="设备编号" :value="equipment.sn" />
|
</div>
|
||||||
<van-cell title="安装位置" :value="equipment.location" />
|
<div class="card">
|
||||||
<van-cell title="安装日期" :value="equipment.installDate" />
|
<div class="info-grid">
|
||||||
<van-cell title="生产厂家" :value="equipment.manufacturer" />
|
<div class="info-item">
|
||||||
</van-cell-group>
|
<span class="info-label">设备名称</span>
|
||||||
|
<span class="info-value">{{ equipment.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">设备类型</span>
|
||||||
|
<span class="info-value">{{ equipment.typeLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">设备型号</span>
|
||||||
|
<span class="info-value">{{ equipment.model }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">设备编号</span>
|
||||||
|
<span class="info-value">{{ equipment.sn }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">安装位置</span>
|
||||||
|
<span class="info-value">{{ equipment.location }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">安装日期</span>
|
||||||
|
<span class="info-value">{{ equipment.installDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">生产厂家</span>
|
||||||
|
<span class="info-value">{{ equipment.manufacturer }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 技术参数 -->
|
<!-- Section: 技术参数 -->
|
||||||
<van-cell-group title="技术参数" class="info-group">
|
<div class="section">
|
||||||
<van-cell title="量程范围" :value="equipment.range" />
|
<div class="section-header">
|
||||||
<van-cell title="精度等级" :value="equipment.precision" />
|
<span class="section-accent"></span>
|
||||||
<van-cell title="通信协议" :value="equipment.protocol" />
|
<span class="section-title">技术参数</span>
|
||||||
<van-cell title="防护等级" :value="equipment.ipRating" />
|
</div>
|
||||||
</van-cell-group>
|
<div class="card">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">量程范围</span>
|
||||||
|
<span class="info-value">{{ equipment.range }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">精度等级</span>
|
||||||
|
<span class="info-value">{{ equipment.precision }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">通信协议</span>
|
||||||
|
<span class="info-value">{{ equipment.protocol }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">防护等级</span>
|
||||||
|
<span class="info-value">{{ equipment.ipRating }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 运行数据 -->
|
<!-- Section: 运行数据 -->
|
||||||
<van-cell-group title="运行数据" class="info-group">
|
<div class="section">
|
||||||
<van-cell title="最新数据" :value="equipment.lastData" />
|
<div class="section-header">
|
||||||
<van-cell title="更新时间" :value="equipment.lastUpdateTime" />
|
<span class="section-accent"></span>
|
||||||
<van-cell title="电池电量" :value="`${equipment.batteryLevel}%`" />
|
<span class="section-title">运行数据</span>
|
||||||
<van-cell title="信号强度" :value="`${equipment.signalStrength}%`" />
|
</div>
|
||||||
</van-cell-group>
|
<div class="card">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item full-width highlight">
|
||||||
|
<span class="info-label">最新数据</span>
|
||||||
|
<span class="info-value data-highlight">{{ equipment.lastData }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">更新时间</span>
|
||||||
|
<span class="info-value">{{ equipment.lastUpdateTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">电池电量</span>
|
||||||
|
<span class="info-value">
|
||||||
|
<van-progress
|
||||||
|
:percentage="equipment.batteryLevel"
|
||||||
|
:color="equipment.batteryLevel > 50 ? '#07C160' : '#FF976A'"
|
||||||
|
:pivot-text="`${equipment.batteryLevel}%`"
|
||||||
|
stroke-width="6"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">信号强度</span>
|
||||||
|
<span class="info-value">
|
||||||
|
<van-progress
|
||||||
|
:percentage="equipment.signalStrength"
|
||||||
|
color="#1E74FF"
|
||||||
|
:pivot-text="`${equipment.signalStrength}%`"
|
||||||
|
stroke-width="6"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 维护记录 -->
|
<!-- Section: 维护记录 -->
|
||||||
<van-cell-group title="维护记录" class="info-group">
|
<div class="section">
|
||||||
<van-cell v-for="(record, idx) in equipment.maintenanceRecords" :key="idx" :title="record.date">
|
<div class="section-header">
|
||||||
<template #label>
|
<span class="section-accent"></span>
|
||||||
<span>{{ record.type }} - {{ record.operator }}</span>
|
<span class="section-title">维护记录</span>
|
||||||
</template>
|
</div>
|
||||||
<template #value>
|
<div class="card">
|
||||||
<van-tag :type="record.result === '正常' || record.result === '合格' || record.result === '已完成' ? 'success' : 'warning'" size="medium">
|
<div
|
||||||
|
v-for="(record, idx) in equipment.maintenanceRecords"
|
||||||
|
:key="idx"
|
||||||
|
class="record-row"
|
||||||
|
:class="{ 'record-last': idx === equipment.maintenanceRecords.length - 1 }"
|
||||||
|
>
|
||||||
|
<div class="record-left">
|
||||||
|
<div class="record-date">{{ record.date }}</div>
|
||||||
|
<div class="record-meta">{{ record.type }} · {{ record.operator }}</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="record-tag"
|
||||||
|
:class="{ 'record-good': isGoodResult(record.result) }"
|
||||||
|
>
|
||||||
{{ record.result }}
|
{{ record.result }}
|
||||||
</van-tag>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
</van-cell>
|
</div>
|
||||||
</van-cell-group>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.equipment-detail-page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
// ── NavBar ──
|
||||||
background: var(--color-primary);
|
:deep(.van-nav-bar) {
|
||||||
--van-nav-bar-title-text-color: #fff;
|
background: #1E74FF;
|
||||||
--van-nav-bar-text-color: #fff;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
--van-nav-bar-icon-color: #fff;
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Status Banner ──
|
||||||
|
.status-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar {
|
// ── Section ──
|
||||||
padding: 12px 16px;
|
.section {
|
||||||
background: var(--color-bg-card);
|
margin: 0 12px 10px;
|
||||||
text-align: center;
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-accent {
|
||||||
|
width: 3px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #1E74FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-group {
|
// ── Card ──
|
||||||
margin-top: 8px;
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Info Grid ──
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #F2F3F5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
background: #F0F7FF;
|
||||||
|
margin: -12px -16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&.data-highlight {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E74FF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Records ──
|
||||||
|
.record-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #F2F3F5;
|
||||||
|
|
||||||
|
&.record-last {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-date {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-tag {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #FFF3ED;
|
||||||
|
color: #FF976A;
|
||||||
|
|
||||||
|
&.record-good {
|
||||||
|
background: #E8F8EF;
|
||||||
|
color: #07C160;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,14 +10,21 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
/** 搜索关键词 */
|
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
|
|
||||||
/** 当前激活的 Tab */
|
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
|
|
||||||
/** 模拟监测设备数据 */
|
interface Equipment {
|
||||||
const mockEquipments = [
|
id: number
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
typeLabel: string
|
||||||
|
status: 'normal' | 'alarm' | 'offline'
|
||||||
|
location: string
|
||||||
|
lastData: string
|
||||||
|
updateTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockEquipments: Equipment[] = [
|
||||||
{ id: 1, name: '流量监测仪 A-01', type: 'flow_meter', typeLabel: '流量计', status: 'normal', location: '城北供水管网1号节点', lastData: '125.6 m³/h', updateTime: '2025-06-15 08:30' },
|
{ id: 1, name: '流量监测仪 A-01', type: 'flow_meter', typeLabel: '流量计', status: 'normal', location: '城北供水管网1号节点', lastData: '125.6 m³/h', updateTime: '2025-06-15 08:30' },
|
||||||
{ id: 2, name: '压力传感器 P-03', type: 'pressure_sensor', typeLabel: '压力传感器', status: 'alarm', location: '高新区主管网3号泵站', lastData: '0.85 MPa', updateTime: '2025-06-15 08:25' },
|
{ id: 2, name: '压力传感器 P-03', type: 'pressure_sensor', typeLabel: '压力传感器', status: 'alarm', location: '高新区主管网3号泵站', lastData: '0.85 MPa', updateTime: '2025-06-15 08:25' },
|
||||||
{ id: 3, name: '水质监测仪 W-07', type: 'water_quality', typeLabel: '水质监测仪', status: 'normal', location: '老城区饮用水源口', lastData: 'pH 7.2 / 浊度 0.5NTU', updateTime: '2025-06-15 08:28' },
|
{ id: 3, name: '水质监测仪 W-07', type: 'water_quality', typeLabel: '水质监测仪', status: 'normal', location: '老城区饮用水源口', lastData: 'pH 7.2 / 浊度 0.5NTU', updateTime: '2025-06-15 08:28' },
|
||||||
@@ -28,24 +35,15 @@ const mockEquipments = [
|
|||||||
{ id: 8, name: '液位计 L-05', type: 'liquid_level', typeLabel: '液位计', status: 'offline', location: '北区污水井5号', lastData: '1.8 m', updateTime: '2025-06-14 18:00' },
|
{ id: 8, name: '液位计 L-05', type: 'liquid_level', typeLabel: '液位计', status: 'offline', location: '北区污水井5号', lastData: '1.8 m', updateTime: '2025-06-14 18:00' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 状态映射 */
|
const statusCfg: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
const statusMap: Record<string, string> = {
|
normal: { label: '正常', color: '#07C160', bg: '#E8F8EF' },
|
||||||
normal: '正常',
|
alarm: { label: '报警', color: '#EE0A24', bg: '#FDECEC' },
|
||||||
alarm: '报警',
|
offline: { label: '离线', color: '#969799', bg: '#F2F3F5' },
|
||||||
offline: '离线',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColorMap: Record<string, string> = {
|
|
||||||
normal: 'success',
|
|
||||||
alarm: 'danger',
|
|
||||||
offline: '#999',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 类型 Tab 映射 */
|
|
||||||
const typeTabs = ['全部', '流量计', '压力传感器', '水质监测仪', '液位计']
|
const typeTabs = ['全部', '流量计', '压力传感器', '水质监测仪', '液位计']
|
||||||
const typeKeys = ['', 'flow_meter', 'pressure_sensor', 'water_quality', 'liquid_level']
|
const typeKeys = ['', 'flow_meter', 'pressure_sensor', 'water_quality', 'liquid_level']
|
||||||
|
|
||||||
/** 筛选后的列表 */
|
|
||||||
const filteredEquipments = computed(() => {
|
const filteredEquipments = computed(() => {
|
||||||
let list = mockEquipments
|
let list = mockEquipments
|
||||||
if (searchText.value) {
|
if (searchText.value) {
|
||||||
@@ -62,22 +60,23 @@ const filteredEquipments = computed(() => {
|
|||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 跳转设备详情 */
|
|
||||||
function goEquipmentInfo(id: number) {
|
function goEquipmentInfo(id: number) {
|
||||||
router.push(`/equipmentInfo?id=${id}`)
|
router.push(`/equipmentInfo?id=${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 跳转地图监控 */
|
|
||||||
function goMapMonitoring() {
|
function goMapMonitoring() {
|
||||||
router.push('/mapMonitoring')
|
router.push('/mapMonitoring')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="equipment-page">
|
<div class="page">
|
||||||
|
<!-- NavBar -->
|
||||||
<van-nav-bar
|
<van-nav-bar
|
||||||
title="监测设备"
|
title="监测设备"
|
||||||
left-arrow fixed placeholder
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
@click-left="router.back()"
|
@click-left="router.back()"
|
||||||
>
|
>
|
||||||
<template #right>
|
<template #right>
|
||||||
@@ -85,85 +84,210 @@ function goMapMonitoring() {
|
|||||||
</template>
|
</template>
|
||||||
</van-nav-bar>
|
</van-nav-bar>
|
||||||
|
|
||||||
<van-search v-model="searchText" placeholder="搜索设备名称、类型、位置" shape="round" />
|
<!-- Search -->
|
||||||
|
<van-search
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索设备名称、类型、位置"
|
||||||
|
shape="round"
|
||||||
|
class="search-bar"
|
||||||
|
/>
|
||||||
|
|
||||||
<van-tabs v-model:active="activeTab" sticky scrollspy>
|
<!-- Type Tabs -->
|
||||||
|
<van-tabs v-model:active="activeTab" sticky swipeable class="tabs-bar">
|
||||||
<van-tab v-for="(tab, idx) in typeTabs" :key="idx" :title="tab" />
|
<van-tab v-for="(tab, idx) in typeTabs" :key="idx" :title="tab" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<div class="equipment-list">
|
<!-- Equipment List -->
|
||||||
|
<div class="content">
|
||||||
<van-empty v-if="filteredEquipments.length === 0" description="暂无监测设备" />
|
<van-empty v-if="filteredEquipments.length === 0" description="暂无监测设备" />
|
||||||
<van-card
|
|
||||||
|
<div
|
||||||
v-for="equip in filteredEquipments"
|
v-for="equip in filteredEquipments"
|
||||||
:key="equip.id"
|
:key="equip.id"
|
||||||
:title="equip.name"
|
class="equip-card"
|
||||||
:desc="`类型: ${equip.typeLabel}`"
|
|
||||||
@click="goEquipmentInfo(equip.id)"
|
@click="goEquipmentInfo(equip.id)"
|
||||||
>
|
>
|
||||||
<template #tags>
|
<!-- Blue accent bar -->
|
||||||
<van-tag :color="statusColorMap[equip.status]" text-color="#fff" size="medium" v-if="equip.status !== 'normal'">
|
<div class="card-accent" />
|
||||||
{{ statusMap[equip.status] }}
|
|
||||||
</van-tag>
|
<div class="card-body">
|
||||||
<van-tag type="success" size="medium" v-else>
|
<div class="card-header">
|
||||||
{{ statusMap[equip.status] }}
|
<span class="card-title">{{ equip.name }}</span>
|
||||||
</van-tag>
|
<span
|
||||||
</template>
|
class="status-tag"
|
||||||
<template #footer>
|
:style="{ color: statusCfg[equip.status].color, background: statusCfg[equip.status].bg }"
|
||||||
<div class="equipment-meta">
|
>
|
||||||
<span class="meta-location">{{ equip.location }}</span>
|
{{ statusCfg[equip.status].label }}
|
||||||
<span class="meta-data">最新数据: {{ equip.lastData }}</span>
|
</span>
|
||||||
<span class="meta-time">更新时间: {{ equip.updateTime }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</van-card>
|
<div class="card-info">
|
||||||
|
<div class="info-row">
|
||||||
|
<van-icon name="label-o" size="14" color="#969799" />
|
||||||
|
<span>{{ equip.typeLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<van-icon name="location-o" size="14" color="#969799" />
|
||||||
|
<span>{{ equip.location }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="data-block">
|
||||||
|
<span class="data-label">最新数据</span>
|
||||||
|
<span class="data-value">{{ equip.lastData }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="update-time">{{ equip.updateTime }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<van-icon name="arrow" color="#C8C9CC" class="card-arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.equipment-page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
// ── NavBar ──
|
||||||
background: var(--color-primary);
|
:deep(.van-nav-bar) {
|
||||||
--van-nav-bar-title-text-color: #fff;
|
background: #1E74FF;
|
||||||
--van-nav-bar-text-color: #fff;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
--van-nav-bar-icon-color: #fff;
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Search ──
|
||||||
|
.search-bar {
|
||||||
|
:deep(.van-search__content) {
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-list {
|
// ── Tabs ──
|
||||||
padding: 0 8px;
|
.tabs-bar {
|
||||||
|
:deep(.van-tabs__nav) {
|
||||||
:deep(.van-card) {
|
background: #fff;
|
||||||
margin: 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.van-tag) {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-meta {
|
// ── Content ──
|
||||||
|
.content {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Card ──
|
||||||
|
.equip-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
position: relative;
|
||||||
gap: 2px;
|
margin-bottom: 10px;
|
||||||
font-size: 12px;
|
background: #fff;
|
||||||
color: var(--color-text-secondary);
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
|
||||||
.meta-location {
|
&:active {
|
||||||
color: var(--color-text-regular);
|
transform: scale(0.985);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-accent {
|
||||||
|
width: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #1E74FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 16px 14px 14px;
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-data {
|
.status-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.meta-time {
|
.card-info {
|
||||||
color: var(--color-text-placeholder);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #646566;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #F2F3F5;
|
||||||
|
|
||||||
|
.data-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #C8C9CC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,19 +2,30 @@
|
|||||||
/**
|
/**
|
||||||
* 监测详情页
|
* 监测详情页
|
||||||
*
|
*
|
||||||
* 展示设备的监测数据详情,包含实时数据、历史趋势图表占位、
|
* 展示设备的监测数据详情,包含实时数据卡片、
|
||||||
* 告警记录和数据分析。
|
* 统计数据和告警记录。
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const detailId = (route.query.id as string) || '1'
|
const detailId = (route.query.id as string) || '1'
|
||||||
|
|
||||||
/** 模拟监测详情 */
|
type StatRow = { avg: string; max: string; min: string; total: string }
|
||||||
const monitoring = ref({
|
|
||||||
|
interface MonitoringDetail {
|
||||||
|
id: string
|
||||||
|
equipmentName: string
|
||||||
|
equipmentType: string
|
||||||
|
currentValue: string
|
||||||
|
status: string
|
||||||
|
updateTime: string
|
||||||
|
statistics: { today: StatRow; yesterday: StatRow }
|
||||||
|
alarmRecords: { time: string; level: 'warning' | 'alarm'; content: string; value: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitoring = ref<MonitoringDetail>({
|
||||||
id: detailId,
|
id: detailId,
|
||||||
equipmentName: '流量监测仪 A-01',
|
equipmentName: '流量监测仪 A-01',
|
||||||
equipmentType: '流量计',
|
equipmentType: '流量计',
|
||||||
@@ -24,7 +35,6 @@ const monitoring = ref({
|
|||||||
statistics: {
|
statistics: {
|
||||||
today: { avg: '122.3 m³/h', max: '135.8 m³/h', min: '110.2 m³/h', total: '2935.2 m³' },
|
today: { avg: '122.3 m³/h', max: '135.8 m³/h', min: '110.2 m³/h', total: '2935.2 m³' },
|
||||||
yesterday: { avg: '118.7 m³/h', max: '130.1 m³/h', min: '105.8 m³/h', total: '2848.8 m³' },
|
yesterday: { avg: '118.7 m³/h', max: '130.1 m³/h', min: '105.8 m³/h', total: '2848.8 m³' },
|
||||||
thisMonth: { avg: '120.5 m³/h', max: '145.2 m³/h', min: '95.6 m³/h', total: '86760 m³' },
|
|
||||||
},
|
},
|
||||||
alarmRecords: [
|
alarmRecords: [
|
||||||
{ time: '2025-06-14 16:30:00', level: 'warning', content: '流量超过预警值 130 m³/h', value: '131.5 m³/h' },
|
{ time: '2025-06-14 16:30:00', level: 'warning', content: '流量超过预警值 130 m³/h', value: '131.5 m³/h' },
|
||||||
@@ -33,104 +43,122 @@ const monitoring = ref({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const levelColorMap: Record<string, string> = {
|
const alarmCfg: Record<string, { color: string; bg: string; icon: string }> = {
|
||||||
warning: '#FF9800',
|
warning: { color: '#FF976A', bg: '#FFF3ED', icon: 'warning-o' },
|
||||||
alarm: '#F44336',
|
alarm: { color: '#EE0A24', bg: '#FDECEC', icon: 'warning-o' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statPeriods = [
|
||||||
|
{ label: '今日统计', key: 'today' as const },
|
||||||
|
{ label: '昨日统计', key: 'yesterday' as const },
|
||||||
|
] as const
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="monitoring-detail-page">
|
<div class="page">
|
||||||
<van-nav-bar title="监测详情" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
|
<!-- NavBar -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="监测详情"
|
||||||
|
left-text="返回"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 实时数据 -->
|
<!-- Realtime Card -->
|
||||||
<div class="realtime-card">
|
<div class="realtime-card">
|
||||||
<div class="realtime-value">{{ monitoring.currentValue }}</div>
|
<div class="realtime-value">{{ monitoring.currentValue }}</div>
|
||||||
<div class="realtime-label">当前流量</div>
|
<div class="realtime-label">当前流量</div>
|
||||||
<div class="realtime-time">更新时间: {{ monitoring.updateTime }}</div>
|
<div class="realtime-time">更新于 {{ monitoring.updateTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图表占位区(趋势图) -->
|
<!-- Stat Cards -->
|
||||||
<van-cell-group title="流量趋势" class="info-group">
|
<div class="stat-row" v-for="period in statPeriods" :key="period.key">
|
||||||
<van-cell>
|
<div class="section-header">
|
||||||
<div class="chart-placeholder">
|
<span class="section-accent"></span>
|
||||||
<van-icon name="chart-trending-o" size="36" color="#ccc" />
|
<span class="section-title">{{ period.label }}</span>
|
||||||
<span>流量趋势图表</span>
|
</div>
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-label">平均流量</span>
|
||||||
|
<span class="stat-value">{{ monitoring.statistics[period.key].avg }}</span>
|
||||||
</div>
|
</div>
|
||||||
</van-cell>
|
<div class="stat-card">
|
||||||
</van-cell-group>
|
<span class="stat-label">最大流量</span>
|
||||||
|
<span class="stat-value stat-peak">{{ monitoring.statistics[period.key].max }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-label">最小流量</span>
|
||||||
|
<span class="stat-value stat-low">{{ monitoring.statistics[period.key].min }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-full">
|
||||||
|
<span class="stat-label">累计流量</span>
|
||||||
|
<span class="stat-value">{{ monitoring.statistics[period.key].total }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 今日统计 -->
|
<!-- Alarm Records -->
|
||||||
<van-cell-group title="今日统计" class="info-group">
|
<div class="section-header section-pad">
|
||||||
<van-cell title="平均流量" :value="monitoring.statistics.today.avg" />
|
<span class="section-accent"></span>
|
||||||
<van-cell title="最大流量" :value="monitoring.statistics.today.max" />
|
<span class="section-title">告警记录</span>
|
||||||
<van-cell title="最小流量" :value="monitoring.statistics.today.min" />
|
</div>
|
||||||
<van-cell title="累计流量" :value="monitoring.statistics.today.total" />
|
<div class="alarm-list">
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<!-- 昨日统计 -->
|
|
||||||
<van-cell-group title="昨日统计" class="info-group">
|
|
||||||
<van-cell title="平均流量" :value="monitoring.statistics.yesterday.avg" />
|
|
||||||
<van-cell title="最大流量" :value="monitoring.statistics.yesterday.max" />
|
|
||||||
<van-cell title="最小流量" :value="monitoring.statistics.yesterday.min" />
|
|
||||||
<van-cell title="累计流量" :value="monitoring.statistics.yesterday.total" />
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<!-- 本月统计 -->
|
|
||||||
<van-cell-group title="本月统计" class="info-group">
|
|
||||||
<van-cell title="平均流量" :value="monitoring.statistics.thisMonth.avg" />
|
|
||||||
<van-cell title="最大流量" :value="monitoring.statistics.thisMonth.max" />
|
|
||||||
<van-cell title="最小流量" :value="monitoring.statistics.thisMonth.min" />
|
|
||||||
<van-cell title="累计流量" :value="monitoring.statistics.thisMonth.total" />
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<!-- 告警记录 -->
|
|
||||||
<van-cell-group title="告警记录" class="info-group">
|
|
||||||
<van-empty v-if="monitoring.alarmRecords.length === 0" description="暂无告警" />
|
<van-empty v-if="monitoring.alarmRecords.length === 0" description="暂无告警" />
|
||||||
<van-cell v-for="(alarm, idx) in monitoring.alarmRecords" :key="idx" :title="alarm.content">
|
<div
|
||||||
<template #label>
|
v-for="(alarm, idx) in monitoring.alarmRecords"
|
||||||
<span>{{ alarm.time }}</span>
|
:key="idx"
|
||||||
</template>
|
class="alarm-card"
|
||||||
<template #value>
|
:style="{ borderLeftColor: alarmCfg[alarm.level].color }"
|
||||||
<div class="alarm-info">
|
>
|
||||||
<span class="alarm-dot" :style="{ backgroundColor: levelColorMap[alarm.level] }" />
|
<div class="alarm-top">
|
||||||
<span class="alarm-value">{{ alarm.value }}</span>
|
<span class="alarm-level" :style="{ color: alarmCfg[alarm.level].color }">
|
||||||
</div>
|
{{ alarm.level === 'alarm' ? '严重' : '预警' }}
|
||||||
</template>
|
</span>
|
||||||
</van-cell>
|
<span class="alarm-time">{{ alarm.time }}</span>
|
||||||
</van-cell-group>
|
</div>
|
||||||
|
<div class="alarm-content">{{ alarm.content }}</div>
|
||||||
|
<div class="alarm-value" v-if="alarm.value !== '-'">
|
||||||
|
触发值:{{ alarm.value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.monitoring-detail-page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
|
||||||
background: var(--color-primary);
|
|
||||||
--van-nav-bar-title-text-color: #fff;
|
|
||||||
--van-nav-bar-text-color: #fff;
|
|
||||||
--van-nav-bar-icon-color: #fff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── NavBar ──
|
||||||
|
:deep(.van-nav-bar) {
|
||||||
|
background: #1E74FF;
|
||||||
|
--van-nav-bar-title-text-color: #fff;
|
||||||
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Realtime Card ──
|
||||||
.realtime-card {
|
.realtime-card {
|
||||||
margin: 8px;
|
margin: 12px;
|
||||||
padding: 20px 16px;
|
padding: 24px 16px;
|
||||||
background: linear-gradient(135deg, var(--color-primary), #36a3f7);
|
background: linear-gradient(135deg, #1E74FF, #42A5F5);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
.realtime-value {
|
.realtime-value {
|
||||||
font-size: 36px;
|
font-size: 40px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.realtime-label {
|
.realtime-label {
|
||||||
margin-top: 8px;
|
margin-top: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
@@ -138,40 +166,120 @@ const levelColorMap: Record<string, string> = {
|
|||||||
.realtime-time {
|
.realtime-time {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
opacity: 0.7;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-placeholder {
|
// ── Section Header ──
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
&.section-pad {
|
||||||
|
padding: 8px 16px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-accent {
|
||||||
|
width: 3px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #1E74FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stat Grid ──
|
||||||
|
.stat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 12px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 180px;
|
|
||||||
background: #f5f7fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--color-text-placeholder);
|
|
||||||
font-size: 13px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-group {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alarm-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
|
&.stat-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
|
||||||
|
&.stat-peak {
|
||||||
|
color: #EE0A24;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stat-low {
|
||||||
|
color: #07C160;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alarm-dot {
|
// ── Alarm List ──
|
||||||
width: 8px;
|
.alarm-list {
|
||||||
height: 8px;
|
padding: 0 12px;
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alarm-value {
|
.alarm-card {
|
||||||
font-size: 13px;
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-left: 4px solid;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.alarm-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-level {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #C8C9CC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-content {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-value {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,47 +11,42 @@ import { showToast } from 'vant'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
/** 当前激活的 Tab */
|
/** Tabs */
|
||||||
const activeName = ref('未读消息')
|
const active = ref(0)
|
||||||
|
|
||||||
/** 列表数据 */
|
interface NoticeItem {
|
||||||
const list = ref<any[]>([])
|
id: number
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
createTime: string
|
||||||
|
}
|
||||||
|
|
||||||
/** 是否加载完成 */
|
const list = ref<NoticeItem[]>([])
|
||||||
const finished = ref(false)
|
const finished = ref(false)
|
||||||
|
|
||||||
/** 是否正在加载 */
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
/** 分页参数 */
|
|
||||||
const pageParams = reactive({
|
const pageParams = reactive({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成模拟消息数据
|
|
||||||
*/
|
|
||||||
function generateMockData(pageNum: number, pageSize: number) {
|
function generateMockData(pageNum: number, pageSize: number) {
|
||||||
const start = (pageNum - 1) * pageSize
|
const start = (pageNum - 1) * pageSize
|
||||||
const total = 25
|
const total = 25
|
||||||
const items: any[] = []
|
|
||||||
const end = Math.min(start + pageSize, total)
|
const end = Math.min(start + pageSize, total)
|
||||||
|
const items: NoticeItem[] = []
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
items.push({
|
items.push({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
title: `通知消息标题 ${i + 1}`,
|
title: `通知消息标题 ${i + 1}`,
|
||||||
message: `这是第 ${i + 1} 条消息的详细内容,用于展示通知列表的效果。`,
|
message: `这是第 ${i + 1} 条消息的详细内容,用于展示通知列表的效果。`,
|
||||||
createTime: `2025-06-${String(10 + Math.floor(i / 3)).padStart(2, '0')} 14:30:00`,
|
createTime: `2025-06-${String(10 + Math.floor(i / 3)).padStart(2, '0')} 14:30`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return { items, isLastPage: end >= total }
|
return { items, isLastPage: end >= total }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function initList() {
|
||||||
* 获取列表数据
|
|
||||||
*/
|
|
||||||
function getList() {
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
pageParams.pageNum = 1
|
pageParams.pageNum = 1
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -59,21 +54,15 @@ function getList() {
|
|||||||
list.value = items
|
list.value = items
|
||||||
finished.value = isLastPage
|
finished.value = isLastPage
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}, 500)
|
}, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tab 切换
|
|
||||||
*/
|
|
||||||
function onChange(name: string | number) {
|
function onChange(name: string | number) {
|
||||||
activeName.value = String(name)
|
active.value = Number(name)
|
||||||
finished.value = false
|
finished.value = false
|
||||||
getList()
|
initList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 无限滚动加载更多
|
|
||||||
*/
|
|
||||||
function onLoad() {
|
function onLoad() {
|
||||||
if (finished.value) return
|
if (finished.value) return
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@@ -83,12 +72,9 @@ function onLoad() {
|
|||||||
list.value = list.value.concat(items)
|
list.value = list.value.concat(items)
|
||||||
finished.value = isLastPage
|
finished.value = isLastPage
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}, 500)
|
}, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 下拉刷新
|
|
||||||
*/
|
|
||||||
function onRefresh() {
|
function onRefresh() {
|
||||||
finished.value = false
|
finished.value = false
|
||||||
pageParams.pageNum = 1
|
pageParams.pageNum = 1
|
||||||
@@ -97,56 +83,56 @@ function onRefresh() {
|
|||||||
list.value = items
|
list.value = items
|
||||||
finished.value = isLastPage
|
finished.value = isLastPage
|
||||||
showToast('刷新成功')
|
showToast('刷新成功')
|
||||||
}, 800)
|
}, 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function toDetail(item: NoticeItem) {
|
||||||
* 点击消息项
|
|
||||||
*/
|
|
||||||
function toDetail(item: any) {
|
|
||||||
showToast(`查看: ${item.title}`)
|
showToast(`查看: ${item.title}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始加载 */
|
initList()
|
||||||
getList()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="notice-page">
|
<div class="page">
|
||||||
<!-- 顶部导航栏 -->
|
<!-- NavBar -->
|
||||||
<van-nav-bar title="消息通知" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
|
<van-nav-bar
|
||||||
|
title="消息通知"
|
||||||
|
left-text="返回"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Tab 切换 -->
|
<!-- Tabs -->
|
||||||
<van-tabs v-model:active="activeName" @change="onChange" sticky>
|
<van-tabs v-model:active="active" @change="onChange" sticky class="tabs-bar">
|
||||||
<van-tab title="未读消息" name="未读消息" />
|
<van-tab title="未读消息" />
|
||||||
<van-tab title="全部消息" name="全部消息" />
|
<van-tab title="全部消息" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<!-- 下拉刷新 + 列表 -->
|
<!-- Pull Refresh + List -->
|
||||||
<van-pull-refresh v-model="isLoading" @refresh="onRefresh">
|
<van-pull-refresh v-model="isLoading" @refresh="onRefresh" class="pull-area">
|
||||||
<van-list
|
<van-list
|
||||||
v-model:loading="isLoading"
|
v-model:loading="isLoading"
|
||||||
:finished="finished"
|
:finished="finished"
|
||||||
finished-text="没有更多了"
|
finished-text="— 没有更多了 —"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="notice-item"
|
|
||||||
v-for="item in list"
|
v-for="item in list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
class="notice-card"
|
||||||
@click="toDetail(item)"
|
@click="toDetail(item)"
|
||||||
>
|
>
|
||||||
<div class="notice-item-header">
|
<div class="card-accent" />
|
||||||
<span class="notice-item-title">{{ item.title }}</span>
|
<div class="card-body">
|
||||||
</div>
|
<div class="notice-title">
|
||||||
<van-divider />
|
<span class="title-dot"></span>
|
||||||
<div class="notice-item-body">
|
{{ item.title }}
|
||||||
<span>{{ item.message }}</span>
|
</div>
|
||||||
</div>
|
<div class="notice-message">{{ item.message }}</div>
|
||||||
<van-divider />
|
<div class="notice-time">{{ item.createTime }}</div>
|
||||||
<div class="notice-item-footer">
|
|
||||||
<span class="notice-item-time-label">通知时间:</span>
|
|
||||||
<span class="notice-item-time">{{ item.createTime }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-list>
|
</van-list>
|
||||||
@@ -155,67 +141,101 @@ getList()
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.notice-page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #f4f7f8;
|
background: #F4F7F8;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
// ── NavBar ──
|
||||||
background: var(--color-primary);
|
:deep(.van-nav-bar) {
|
||||||
--van-nav-bar-title-text-color: #fff;
|
background: #1E74FF;
|
||||||
--van-nav-bar-text-color: #fff;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
--van-nav-bar-icon-color: #fff;
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tabs ──
|
||||||
|
.tabs-bar {
|
||||||
|
:deep(.van-tabs__nav) {
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-item {
|
// ── Pull Area ──
|
||||||
padding: 10px 12px;
|
.pull-area {
|
||||||
margin: 12px 8px;
|
min-height: calc(100vh - 92px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notice Card ──
|
||||||
|
.notice-card {
|
||||||
|
display: flex;
|
||||||
|
margin: 8px 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: #f5f5f5;
|
transform: scale(0.985);
|
||||||
|
background: #FAFAFA;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-header {
|
.card-accent {
|
||||||
|
width: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #1E74FF;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 8px;
|
||||||
margin-bottom: 6px;
|
font-size: 16px;
|
||||||
}
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 17px;
|
|
||||||
color: #333438;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.title-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #EE0A24;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-body {
|
.notice-message {
|
||||||
padding: 0 4px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #535c66;
|
color: #646566;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 10px;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-footer {
|
.notice-time {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-time-label {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #C8C9CC;
|
||||||
}
|
|
||||||
|
|
||||||
&-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Van List ──
|
||||||
|
:deep(.van-list__finished-text) {
|
||||||
|
color: #C8C9CC;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,11 +9,23 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const detailId = computed(() => route.params.detail as string | undefined)
|
const detailId = computed(() => route.params.detail as string | undefined)
|
||||||
|
|
||||||
/** 模拟项目详情 */
|
interface ProjectDetail {
|
||||||
const project = ref({
|
id: string
|
||||||
|
name: string
|
||||||
|
no: string
|
||||||
|
company: string
|
||||||
|
progress: number
|
||||||
|
status: 'building' | 'paused' | 'completed'
|
||||||
|
manager: string
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
budget: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = ref<ProjectDetail>({
|
||||||
id: detailId.value || 'XM-2025-001',
|
id: detailId.value || 'XM-2025-001',
|
||||||
name: '城北雨水管网改造工程',
|
name: '城北雨水管网改造工程',
|
||||||
no: 'XM-2025-001',
|
no: 'XM-2025-001',
|
||||||
@@ -27,25 +39,18 @@ const project = ref({
|
|||||||
description: '对城北片区老旧雨水管网进行全面改造,包括新建DN300-DN600雨水管12.5km,改造检查井280座,新建雨水泵站1座。',
|
description: '对城北片区老旧雨水管网进行全面改造,包括新建DN300-DN600雨水管12.5km,改造检查井280座,新建雨水泵站1座。',
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusMap: Record<string, string> = {
|
const statusCfg: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
building: '在建',
|
building: { label: '在建', color: '#1E74FF', bg: '#E3F2FD' },
|
||||||
paused: '暂停',
|
paused: { label: '暂停', color: '#FF976A', bg: '#FFF3ED' },
|
||||||
completed: '竣工',
|
completed: { label: '竣工', color: '#07C160', bg: '#E8F8EF' },
|
||||||
}
|
|
||||||
|
|
||||||
const statusTagType: Record<string, 'primary' | 'warning' | 'success'> = {
|
|
||||||
building: 'primary',
|
|
||||||
paused: 'warning',
|
|
||||||
completed: 'success',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function progressColor(pct: number): string {
|
function progressColor(pct: number): string {
|
||||||
if (pct >= 80) return '#07c160'
|
if (pct >= 80) return '#07C160'
|
||||||
if (pct >= 40) return '#1989fa'
|
if (pct >= 40) return '#1E74FF'
|
||||||
return '#ff976a'
|
return '#FF976A'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模拟里程碑 */
|
|
||||||
const milestones = ref([
|
const milestones = ref([
|
||||||
{ text: '项目立项', time: '2025-02-15', done: true },
|
{ text: '项目立项', time: '2025-02-15', done: true },
|
||||||
{ text: '施工设计', time: '2025-03-15', done: true },
|
{ text: '施工设计', time: '2025-03-15', done: true },
|
||||||
@@ -56,36 +61,77 @@ const milestones = ref([
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="detail-page">
|
<div class="page">
|
||||||
<van-nav-bar title="项目详情" left-arrow fixed placeholder @click-left="router.back()" />
|
<!-- NavBar -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="项目详情"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<!-- Status Banner -->
|
||||||
<van-cell-group title="基本信息" inset>
|
<div class="status-banner">
|
||||||
<van-cell title="项目编号" :value="project.no" />
|
<span
|
||||||
<van-cell title="项目名称" :value="project.name" />
|
class="status-badge"
|
||||||
<van-cell title="施工单位" :value="project.company" />
|
:style="{ color: statusCfg[project.status].color, background: statusCfg[project.status].bg }"
|
||||||
<van-cell title="负责人" :value="project.manager" />
|
>
|
||||||
<van-cell title="计划工期">
|
{{ statusCfg[project.status].label }}
|
||||||
<span class="date-range">{{ project.startDate }} ~ {{ project.endDate }}</span>
|
</span>
|
||||||
</van-cell>
|
<span class="status-name">{{ project.name }}</span>
|
||||||
<van-cell title="预算金额" :value="project.budget" />
|
</div>
|
||||||
<van-cell title="当前状态">
|
|
||||||
<van-tag :type="statusTagType[project.status]" size="medium">
|
|
||||||
{{ statusMap[project.status] }}
|
|
||||||
</van-tag>
|
|
||||||
</van-cell>
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<!-- 项目描述 -->
|
<!-- Section: 基本信息 -->
|
||||||
<van-cell-group title="项目描述" inset>
|
<div class="section">
|
||||||
<van-cell>
|
<div class="section-header">
|
||||||
<p class="project-desc">{{ project.description }}</p>
|
<span class="section-accent"></span>
|
||||||
</van-cell>
|
<span class="section-title">基本信息</span>
|
||||||
</van-cell-group>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">项目编号</span>
|
||||||
|
<span class="info-value">{{ project.no }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">施工单位</span>
|
||||||
|
<span class="info-value">{{ project.company }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">负责人</span>
|
||||||
|
<span class="info-value">{{ project.manager }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">计划工期</span>
|
||||||
|
<span class="info-value">{{ project.startDate }} ~ {{ project.endDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">预算金额</span>
|
||||||
|
<span class="info-value highlight">{{ project.budget }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 进度 -->
|
<!-- Section: 项目描述 -->
|
||||||
<van-cell-group title="工程进度" inset>
|
<div class="section">
|
||||||
<div class="progress-section">
|
<div class="section-header">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">项目描述</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="desc-text">{{ project.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: 工程进度 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">工程进度</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
<van-progress
|
<van-progress
|
||||||
:percentage="project.progress"
|
:percentage="project.progress"
|
||||||
:color="progressColor(project.progress)"
|
:color="progressColor(project.progress)"
|
||||||
@@ -93,80 +139,179 @@ const milestones = ref([
|
|||||||
stroke-width="10"
|
stroke-width="10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</van-cell-group>
|
</div>
|
||||||
|
|
||||||
<!-- 里程碑 -->
|
<!-- Section: 里程碑 -->
|
||||||
<van-cell-group title="项目里程碑" inset>
|
<div class="section">
|
||||||
<div class="milestones-section">
|
<div class="section-header">
|
||||||
<van-steps :active="milestones.filter(m => m.done).length - 1" direction="vertical" active-color="#1989fa">
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">项目里程碑</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<van-steps
|
||||||
|
:active="milestones.filter(m => m.done).length - 1"
|
||||||
|
direction="vertical"
|
||||||
|
active-color="#1E74FF"
|
||||||
|
>
|
||||||
<van-step v-for="(step, idx) in milestones" :key="idx">
|
<van-step v-for="(step, idx) in milestones" :key="idx">
|
||||||
<template #active-icon>
|
<template #active-icon>
|
||||||
<van-icon name="checked" color="#1989fa" />
|
<van-icon name="checked" color="#1E74FF" />
|
||||||
</template>
|
</template>
|
||||||
<template #inactive-icon>
|
<template #inactive-icon>
|
||||||
<van-icon name="clock-o" color="#ccc" />
|
<van-icon name="clock-o" color="#C8C9CC" />
|
||||||
</template>
|
</template>
|
||||||
<h4>{{ step.text }}</h4>
|
<div class="step-content">
|
||||||
<p>{{ step.time }}</p>
|
<h4 :class="{ 'step-done': step.done }">{{ step.text }}</h4>
|
||||||
|
<p>{{ step.time }}</p>
|
||||||
|
</div>
|
||||||
</van-step>
|
</van-step>
|
||||||
</van-steps>
|
</van-steps>
|
||||||
</div>
|
</div>
|
||||||
</van-cell-group>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.detail-page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
// ── NavBar ──
|
||||||
background: var(--color-primary);
|
:deep(.van-nav-bar) {
|
||||||
--van-nav-bar-title-text-color: #fff;
|
background: #1E74FF;
|
||||||
--van-nav-bar-text-color: #fff;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
--van-nav-bar-icon-color: #fff;
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Status Banner ──
|
||||||
|
.status-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.van-cell-group) {
|
.status-name {
|
||||||
margin: 12px 8px;
|
font-size: 15px;
|
||||||
}
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
:deep(.van-cell-group__title) {
|
|
||||||
padding: 12px 16px 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-range {
|
// ── Section ──
|
||||||
font-size: 13px;
|
.section {
|
||||||
color: var(--color-text-secondary);
|
margin: 0 12px 10px;
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-accent {
|
||||||
|
width: 3px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #1E74FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-desc {
|
// ── Card ──
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Info Grid ──
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #F2F3F5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #EE0A24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Description ──
|
||||||
|
.desc-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--color-text-regular);
|
color: #646566;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-section {
|
// ── Steps ──
|
||||||
padding: 16px;
|
:deep(.van-step__title) {
|
||||||
}
|
.step-content {
|
||||||
|
|
||||||
.milestones-section {
|
|
||||||
padding: 12px 0;
|
|
||||||
|
|
||||||
:deep(.van-step__title) {
|
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--color-text-regular);
|
color: #969799;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&.step-done {
|
||||||
|
color: #323233;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-placeholder);
|
color: #C8C9CC;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,30 +9,20 @@ import { ref, computed } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
|
|
||||||
const statusMap: Record<string, string> = {
|
interface Project {
|
||||||
building: '在建',
|
id: number
|
||||||
paused: '暂停',
|
name: string
|
||||||
completed: '竣工',
|
no: string
|
||||||
|
company: string
|
||||||
|
progress: number
|
||||||
|
status: 'building' | 'paused' | 'completed'
|
||||||
|
manager: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusTagType: Record<string, 'primary' | 'warning' | 'success'> = {
|
const projects: Project[] = [
|
||||||
building: 'primary',
|
|
||||||
paused: 'warning',
|
|
||||||
completed: 'success',
|
|
||||||
}
|
|
||||||
|
|
||||||
function progressColor(pct: number): string {
|
|
||||||
if (pct >= 80) return '#07c160'
|
|
||||||
if (pct >= 40) return '#1989fa'
|
|
||||||
return '#ff976a'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 模拟项目数据 */
|
|
||||||
const projects = [
|
|
||||||
{ id: 1, name: '城北雨水管网改造工程', no: 'XM-2025-001', company: '中建三局', progress: 75, status: 'building', manager: '赵工' },
|
{ id: 1, name: '城北雨水管网改造工程', no: 'XM-2025-001', company: '中建三局', progress: 75, status: 'building', manager: '赵工' },
|
||||||
{ id: 2, name: '东风路排水泵站新建项目', no: 'XM-2025-002', company: '水务工程公司', progress: 30, status: 'building', manager: '钱工' },
|
{ id: 2, name: '东风路排水泵站新建项目', no: 'XM-2025-002', company: '水务工程公司', progress: 30, status: 'building', manager: '钱工' },
|
||||||
{ id: 3, name: '中山河河道整治工程', no: 'XM-2025-003', company: '市政建设集团', progress: 90, status: 'building', manager: '孙工' },
|
{ id: 3, name: '中山河河道整治工程', no: 'XM-2025-003', company: '市政建设集团', progress: 90, status: 'building', manager: '孙工' },
|
||||||
@@ -41,6 +31,18 @@ const projects = [
|
|||||||
{ id: 6, name: '南湖片区海绵城市试点', no: 'XM-2025-006', company: '葛洲坝集团', progress: 55, status: 'building', manager: '吴工' },
|
{ id: 6, name: '南湖片区海绵城市试点', no: 'XM-2025-006', company: '葛洲坝集团', progress: 55, status: 'building', manager: '吴工' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const statusCfg: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
|
building: { label: '在建', color: '#1E74FF', bg: '#E3F2FD' },
|
||||||
|
paused: { label: '暂停', color: '#FF976A', bg: '#FFF3ED' },
|
||||||
|
completed: { label: '竣工', color: '#07C160', bg: '#E8F8EF' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressColor(pct: number): string {
|
||||||
|
if (pct >= 80) return '#07C160'
|
||||||
|
if (pct >= 40) return '#1E74FF'
|
||||||
|
return '#FF976A'
|
||||||
|
}
|
||||||
|
|
||||||
const filteredList = computed(() => {
|
const filteredList = computed(() => {
|
||||||
let list = projects
|
let list = projects
|
||||||
if (searchText.value) {
|
if (searchText.value) {
|
||||||
@@ -63,81 +65,187 @@ function goDetail(id: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page">
|
||||||
<van-nav-bar title="项目管理" left-arrow fixed placeholder @click-left="router.back()" />
|
<!-- NavBar -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="项目管理"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
<van-search v-model="searchText" placeholder="搜索项目名称、编号、单位" shape="round" />
|
<!-- Search -->
|
||||||
|
<van-search
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索项目名称、编号、单位"
|
||||||
|
shape="round"
|
||||||
|
class="search-bar"
|
||||||
|
/>
|
||||||
|
|
||||||
<van-tabs v-model:active="activeTab" sticky>
|
<!-- Status Tabs -->
|
||||||
|
<van-tabs v-model:active="activeTab" sticky swipeable class="tabs-bar">
|
||||||
<van-tab title="全部" />
|
<van-tab title="全部" />
|
||||||
<van-tab title="在建" />
|
<van-tab title="在建" />
|
||||||
<van-tab title="暂停" />
|
<van-tab title="暂停" />
|
||||||
<van-tab title="竣工" />
|
<van-tab title="竣工" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<div class="card-list">
|
<!-- Project List -->
|
||||||
|
<div class="content">
|
||||||
<van-empty v-if="filteredList.length === 0" description="暂无项目" />
|
<van-empty v-if="filteredList.length === 0" description="暂无项目" />
|
||||||
<van-card
|
|
||||||
|
<div
|
||||||
v-for="item in filteredList"
|
v-for="item in filteredList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
class="proj-card"
|
||||||
:desc="`施工单位: ${item.company}`"
|
|
||||||
@click="goDetail(item.id)"
|
@click="goDetail(item.id)"
|
||||||
>
|
>
|
||||||
<template #tags>
|
<div class="card-accent" :style="{ background: progressColor(item.progress) }" />
|
||||||
<van-tag :type="statusTagType[item.status]" size="medium">
|
|
||||||
{{ statusMap[item.status] }}
|
<div class="card-body">
|
||||||
</van-tag>
|
<div class="card-header">
|
||||||
</template>
|
<span class="card-title">{{ item.name }}</span>
|
||||||
<template #footer>
|
<span
|
||||||
<div class="progress-wrap">
|
class="status-tag"
|
||||||
<van-progress
|
:style="{ color: statusCfg[item.status].color, background: statusCfg[item.status].bg }"
|
||||||
:percentage="item.progress"
|
>
|
||||||
:color="progressColor(item.progress)"
|
{{ statusCfg[item.status].label }}
|
||||||
:pivot-text="`${item.progress}%`"
|
</span>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-meta">
|
|
||||||
<span>{{ item.no }}</span>
|
<div class="card-info">
|
||||||
<span>负责人: {{ item.manager }}</span>
|
<span class="info-text">{{ item.no }}</span>
|
||||||
|
<span class="info-divider">|</span>
|
||||||
|
<span class="info-text">{{ item.company }}</span>
|
||||||
|
<span class="info-divider">|</span>
|
||||||
|
<span class="info-text">负责人: {{ item.manager }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</van-card>
|
<van-progress
|
||||||
|
:percentage="item.progress"
|
||||||
|
:color="progressColor(item.progress)"
|
||||||
|
:pivot-text="`${item.progress}%`"
|
||||||
|
stroke-width="6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<van-icon name="arrow" color="#C8C9CC" class="card-arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-container {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
// ── NavBar ──
|
||||||
background: var(--color-primary);
|
:deep(.van-nav-bar) {
|
||||||
--van-nav-bar-title-text-color: #fff;
|
background: #1E74FF;
|
||||||
--van-nav-bar-text-color: #fff;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
--van-nav-bar-icon-color: #fff;
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Search ──
|
||||||
|
.search-bar {
|
||||||
|
:deep(.van-search__content) {
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
// ── Tabs ──
|
||||||
padding: 0 8px;
|
.tabs-bar {
|
||||||
|
:deep(.van-tabs__nav) {
|
||||||
:deep(.van-card) {
|
background: #fff;
|
||||||
margin: 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-wrap {
|
// ── Content ──
|
||||||
margin: 8px 0;
|
.content {
|
||||||
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-meta {
|
// ── Card ──
|
||||||
|
.proj-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
position: relative;
|
||||||
font-size: 12px;
|
margin-bottom: 10px;
|
||||||
color: var(--color-text-secondary);
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.985);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-accent {
|
||||||
|
width: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 16px;
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-divider {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #EBEDF0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -78,14 +78,14 @@ function onConfirmPsh({ selectedOptions }: any) {
|
|||||||
/** 选择评分 */
|
/** 选择评分 */
|
||||||
function onConfirmScore(field: string, { selectedOptions }: any) {
|
function onConfirmScore(field: string, { selectedOptions }: any) {
|
||||||
(formData.value as any)[field] = selectedOptions[0]?.text || ''
|
(formData.value as any)[field] = selectedOptions[0]?.text || ''
|
||||||
const scoreFields: Record<string, string> = {
|
// 关闭对应的选择器弹窗
|
||||||
drainPipeScore: 'showDrainPipeScore',
|
switch (field) {
|
||||||
preTreatmentScore: 'showPreTreatmentScore',
|
case 'drainPipeScore': showDrainPipeScore.value = false; break
|
||||||
oilSeparatorScore: 'showOilSeparatorScore',
|
case 'preTreatmentScore': showPreTreatmentScore.value = false; break
|
||||||
rainSewageScore: 'showRainSewageScore',
|
case 'oilSeparatorScore': showOilSeparatorScore.value = false; break
|
||||||
overallScore: 'showOverallScore',
|
case 'rainSewageScore': showRainSewageScore.value = false; break
|
||||||
|
case 'overallScore': showOverallScore.value = false; break
|
||||||
}
|
}
|
||||||
;(ref as any)[scoreFields[field]].value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确认综合评定 */
|
/** 确认综合评定 */
|
||||||
|
|||||||
223
src/views/pshgl/pshCheckDetail.vue
Normal file
223
src/views/pshgl/pshCheckDetail.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 排水户检查详情页
|
||||||
|
*
|
||||||
|
* 展示已提交的检查报告详情,包含检查项目、评分、
|
||||||
|
* 综合评定、整改建议和现场照片等完整信息。
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const detailId = (route.params.detail || route.query.id) as string
|
||||||
|
|
||||||
|
/** 模拟检查详情数据 */
|
||||||
|
const checkDetail = ref({
|
||||||
|
id: detailId || '1',
|
||||||
|
pshName: '华丰食品厂',
|
||||||
|
checkDate: '2025-06-15',
|
||||||
|
inspector: '张检查员',
|
||||||
|
items: [
|
||||||
|
{ name: '排水管道', description: '管道畅通,无堵塞现象,接口密封良好。', score: 5 },
|
||||||
|
{ name: '预处理设备', description: '设备运行正常,格栅无破损,沉砂池定期清理。', score: 4 },
|
||||||
|
{ name: '油脂分离器', description: '分离器工作正常,油脂收集记录完整。', score: 5 },
|
||||||
|
{ name: '雨污分流', description: '雨污分流管道清晰,无混接现象。', score: 4 },
|
||||||
|
],
|
||||||
|
overall: '合格',
|
||||||
|
overallScore: '4分 - 良好',
|
||||||
|
suggestion: '建议加强日常巡检频次,定期清理隔油池,保持排水管道畅通。',
|
||||||
|
photos: [] as string[],
|
||||||
|
createTime: '2025-06-15 10:30:00',
|
||||||
|
updateTime: '2025-06-15 10:30:00',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 结果颜色映射 */
|
||||||
|
const resultColorMap: Record<string, string> = {
|
||||||
|
'合格': '#07c160',
|
||||||
|
'不合格': '#ee0a24',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分数颜色 */
|
||||||
|
function getScoreColor(score: number): string {
|
||||||
|
if (score >= 5) return '#07c160'
|
||||||
|
if (score >= 3) return '#ff976a'
|
||||||
|
return '#ee0a24'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="detail-page">
|
||||||
|
<van-nav-bar
|
||||||
|
title="检查详情"
|
||||||
|
left-text="返回"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 检查结果状态 -->
|
||||||
|
<div class="result-bar">
|
||||||
|
<span
|
||||||
|
class="result-tag"
|
||||||
|
:style="{ color: resultColorMap[checkDetail.overall], borderColor: resultColorMap[checkDetail.overall] }"
|
||||||
|
>
|
||||||
|
{{ checkDetail.overall }}
|
||||||
|
</span>
|
||||||
|
<span class="result-score">{{ checkDetail.overallScore }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<van-cell-group title="基本信息" class="info-group">
|
||||||
|
<van-cell title="排水户" :value="checkDetail.pshName" />
|
||||||
|
<van-cell title="检查日期" :value="checkDetail.checkDate" />
|
||||||
|
<van-cell title="检查人" :value="checkDetail.inspector" />
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 检查项目 -->
|
||||||
|
<van-cell-group title="检查项目" class="info-group">
|
||||||
|
<van-cell
|
||||||
|
v-for="(item, index) in checkDetail.items"
|
||||||
|
:key="index"
|
||||||
|
:title="`${index + 1}. ${item.name}`"
|
||||||
|
:label="item.description"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
<span class="score-badge" :style="{ background: getScoreColor(item.score) }">
|
||||||
|
{{ item.score }} 分
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 综合评定 -->
|
||||||
|
<van-cell-group title="综合评定" class="info-group">
|
||||||
|
<van-cell title="评定结果" :value="checkDetail.overall" />
|
||||||
|
<van-cell title="综合评分" :value="checkDetail.overallScore" />
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 整改建议 -->
|
||||||
|
<van-cell-group title="整改建议" class="info-group">
|
||||||
|
<van-cell>
|
||||||
|
<p class="desc-text">{{ checkDetail.suggestion || '无' }}</p>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 现场照片 -->
|
||||||
|
<van-cell-group title="现场照片" class="info-group">
|
||||||
|
<van-cell>
|
||||||
|
<div v-if="checkDetail.photos.length === 0" class="no-photo">
|
||||||
|
<span>暂无现场照片</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="photo-grid">
|
||||||
|
<van-image
|
||||||
|
v-for="(photo, idx) in checkDetail.photos"
|
||||||
|
:key="idx"
|
||||||
|
:src="photo"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
fit="cover"
|
||||||
|
radius="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 时间信息 -->
|
||||||
|
<van-cell-group title="操作记录" class="info-group">
|
||||||
|
<van-cell title="创建时间" :value="checkDetail.createTime" />
|
||||||
|
<van-cell title="更新时间" :value="checkDetail.updateTime" />
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<van-button type="primary" block round @click="router.back()">
|
||||||
|
返回列表
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
padding-bottom: 24px;
|
||||||
|
|
||||||
|
:deep(.van-nav-bar) {
|
||||||
|
background: var(--color-primary);
|
||||||
|
--van-nav-bar-title-text-color: #fff;
|
||||||
|
--van-nav-bar-text-color: #fff;
|
||||||
|
--van-nav-bar-icon-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 16px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-score {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--color-text-regular);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-photo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user