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:
Ubuntu
2026-06-15 22:59:36 +08:00
parent ce76462a82
commit fd8a7d8099
12 changed files with 1879 additions and 622 deletions

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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) { // ── NavBar ──
background: var(--color-primary); :deep(.van-nav-bar) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-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 { // ── 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>

View File

@@ -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,34 +32,24 @@ 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-cell title="排水户类型" :value="detail.type" />
<van-cell title="地址" :value="detail.address" />
<van-cell title="联系人" :value="detail.contact" />
<van-cell title="联系电话" :value="detail.phone" />
<van-cell title="排水许可证" :value="detail.licenseNo" />
<van-cell title="排水方式" :value="detail.drainType" />
</van-cell-group>
<!-- 二维码 -->
<van-cell-group title="排水户二维码" inset>
<div class="qr-section">
<div class="qr-placeholder">
<van-image <van-image
v-if="detail.qrCodeUrl" v-if="detail.qrCodeUrl"
:src="detail.qrCodeUrl" :src="detail.qrCodeUrl"
@@ -58,58 +58,182 @@ function goBack() {
fit="contain" fit="contain"
/> />
<template v-else> <template v-else>
<van-icon name="qr" size="64" color="#ccc" /> <van-icon name="qr" size="72" color="#1E74FF" />
<p class="qr-hint">扫码查看排水户信息</p> <p class="qr-hint">描二维码查看排水户信息</p>
</template> </template>
</div> </div>
<span
class="type-tag"
:style="{ color: '#1E74FF', background: '#E3F2FD' }"
>
{{ detail.drainType }}
</span>
</div>
<!-- Section: 基本信息 -->
<div class="section">
<div class="section-header">
<span class="section-accent"></span>
<span class="section-title">基本信息</span>
</div>
<div class="card">
<div class="info-grid">
<div class="info-item">
<span class="info-label">排水户编号</span>
<span class="info-value">{{ detail.id }}</span>
</div>
<div class="info-item full-width">
<span class="info-label">排水户名称</span>
<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>
</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) { // ── NavBar ──
background: var(--color-primary); :deep(.van-nav-bar) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-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 { // ── QR Card ──
.qr-card {
display: flex; display: flex;
justify-content: center; flex-direction: column;
padding: 24px; 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-placeholder { .qr-box {
width: 160px;
height: 160px;
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>

View File

@@ -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) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-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>

View File

@@ -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 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>
</template>
</van-card>
</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) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-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) {
background: #fff;
}
}
:deep(.van-card) { // ── Content ──
margin: 8px; .content {
padding: 8px 12px;
}
// ── Card ──
.equip-card {
display: flex;
position: relative;
margin-bottom: 10px;
background: #fff;
border-radius: 10px; border-radius: 10px;
background: var(--color-bg-card); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
} overflow: hidden;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
:deep(.van-tag) { &:active {
margin-right: 4px; transform: scale(0.985);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
} }
.equipment-meta { .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;
}
.status-tag {
flex-shrink: 0;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
}
.card-info {
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; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
font-size: 12px;
color: var(--color-text-secondary);
.meta-location {
color: var(--color-text-regular);
} }
.meta-data { .data-label {
font-size: 11px;
color: #969799;
}
.data-value {
font-size: 13px;
font-weight: 500; font-weight: 500;
color: #323233;
} }
.meta-time { .update-time {
color: var(--color-text-placeholder); font-size: 11px;
color: #C8C9CC;
} }
} }
.card-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
}
</style> </style>

View File

@@ -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 class="stat-card">
<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> </div>
</van-cell>
</van-cell-group>
<!-- 今日统计 --> <!-- 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 }">
{{ alarm.level === 'alarm' ? '严重' : '预警' }}
</span>
<span class="alarm-time">{{ alarm.time }}</span>
</div>
<div class="alarm-content">{{ alarm.content }}</div>
<div class="alarm-value" v-if="alarm.value !== '-'">
触发值{{ alarm.value }}
</div>
</div>
</div> </div>
</template>
</van-cell>
</van-cell-group>
</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) { // ── NavBar ──
background: var(--color-primary); :deep(.van-nav-bar) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-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>

View File

@@ -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 class="notice-title">
<span class="title-dot"></span>
{{ item.title }}
</div> </div>
<van-divider /> <div class="notice-message">{{ item.message }}</div>
<div class="notice-item-body"> <div class="notice-time">{{ item.createTime }}</div>
<span>{{ item.message }}</span>
</div>
<van-divider />
<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) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-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>

View File

@@ -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">
<h4 :class="{ 'step-done': step.done }">{{ step.text }}</h4>
<p>{{ step.time }}</p> <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) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-color: #fff; --van-nav-bar-icon-color: #fff;
}
:deep(.van-cell-group) {
margin: 12px 8px;
}
:deep(.van-cell-group__title) {
padding: 12px 16px 8px;
}
} }
.date-range { // ── 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-size: 13px;
color: var(--color-text-secondary); font-weight: 600;
}
.status-name {
font-size: 15px;
font-weight: 500;
color: #323233;
}
} }
.project-desc { // ── 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-size: 14px;
color: var(--color-text-regular); font-weight: 600;
line-height: 1.6; 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: 700;
color: #EE0A24;
}
}
}
// ── Description ──
.desc-text {
font-size: 14px;
color: #646566;
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;
} }
} }
} }

View File

@@ -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"
:style="{ color: statusCfg[item.status].color, background: statusCfg[item.status].bg }"
>
{{ statusCfg[item.status].label }}
</span>
</div>
<div class="card-info">
<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>
<van-progress <van-progress
:percentage="item.progress" :percentage="item.progress"
:color="progressColor(item.progress)" :color="progressColor(item.progress)"
:pivot-text="`${item.progress}%`" :pivot-text="`${item.progress}%`"
stroke-width="6"
/> />
<van-icon name="arrow" color="#C8C9CC" class="card-arrow" />
</div> </div>
<div class="card-meta">
<span>{{ item.no }}</span>
<span>负责人: {{ item.manager }}</span>
</div> </div>
</template>
</van-card>
</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) {
background: #1E74FF;
--van-nav-bar-title-text-color: #fff; --van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff; --van-nav-bar-text-color: #fff;
--van-nav-bar-icon-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;
margin-bottom: 10px;
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; font-size: 12px;
color: var(--color-text-secondary); color: #969799;
}
.info-divider {
font-size: 12px;
color: #EBEDF0;
}
}
.card-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
} }
</style> </style>

View File

@@ -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
} }
/** 确认综合评定 */ /** 确认综合评定 */

View 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>