feat: Batch 4 - remaining pages (23 pages)

Agent Loop: 3 agents, all passed
- 8 pshgl pages (排水户管理)
- 8 fxgl+xjgl pages (防汛+工程)
- 7 remaining (map/project/QR)
This commit is contained in:
2026-06-15 21:27:57 +08:00
parent 35810730e8
commit 2624fb0404
24 changed files with 3764 additions and 0 deletions

View File

@@ -366,6 +366,180 @@ const routes: RouteRecordRaw[] = [
title: '施工问题上报',
},
},
// ── 防汛管理模块 ──
{
path: '/groupsClockList/:id?/:detail?',
name: 'GroupsClockList',
component: () => import('@/views/fxgl/groupsClockList.vue'),
meta: {
title: '打卡记录',
},
},
{
path: '/teamList/:id?',
name: 'TeamList',
component: () => import('@/views/fxgl/teamList.vue'),
meta: {
title: '选择成员',
},
},
{
path: '/instructionList',
name: 'InstructionList',
component: () => import('@/views/fxgl/instructionList.vue'),
meta: {
title: '防汛指令',
},
},
{
path: '/instructionReceive',
name: 'InstructionReceive',
component: () => import('@/views/fxgl/instructionReceive.vue'),
meta: {
title: '指令详情',
},
},
{
path: '/materialList',
name: 'MaterialList',
component: () => import('@/views/fxgl/materialList.vue'),
meta: {
title: '物资管理',
},
},
{
path: '/managementInventory/:id?',
name: 'ManagementInventory',
component: () => import('@/views/fxgl/managementInventory.vue'),
meta: {
title: '出入库',
},
},
// ── 工程管理模块 ──
{
path: '/constructionTracking',
name: 'ConstructionTracking',
component: () => import('@/views/xjgl/constructionList.vue'),
meta: {
title: '工程项目',
},
},
{
path: '/constructionDetail/:id?',
name: 'ConstructionDetail',
component: () => import('@/views/xjgl/constructionDetail.vue'),
meta: {
title: '项目详情',
},
},
// ── 排水户管理模块 ──
{
path: '/pshList',
name: 'PshList',
component: () => import('@/views/pshgl/pshList.vue'),
meta: {
title: '排水户管理',
},
},
{
path: '/pshglDetail/:detail?',
name: 'PshglDetail',
component: () => import('@/views/pshgl/pshglDetail.vue'),
meta: {
title: '排水户详情',
},
},
{
path: '/pshProblemList/:id?',
name: 'PshProblemList',
component: () => import('@/views/pshgl/pshProblemList.vue'),
meta: {
title: '问题列表',
},
},
{
path: '/pshProblemDetail/:detail?/:obj?',
name: 'PshProblemDetail',
component: () => import('@/views/pshgl/pshProblemDetail.vue'),
meta: {
title: '问题详情',
},
},
{
path: '/pshTaskList/:detail?/:obj?',
name: 'PshTaskList',
component: () => import('@/views/pshgl/pshTaskList.vue'),
meta: {
title: '任务管理',
},
},
{
path: '/checkList',
name: 'CheckList',
component: () => import('@/views/pshgl/checkList.vue'),
meta: {
title: '检查记录',
},
},
{
path: '/pshCheckList/:id?/:obj?',
name: 'PshCheckList',
component: () => import('@/views/pshgl/pshCheckList.vue'),
meta: {
title: '选择排水户',
},
},
{
path: '/pshCheck/:id?/:detail?',
name: 'PshCheck',
component: () => import('@/views/pshgl/pshCheck.vue'),
meta: {
title: '检查报告',
},
},
// ── 地图模块 ──
{
path: '/map',
name: 'Map',
component: () => import('@/views/map/index.vue'),
meta: {
title: '地图',
},
},
// ── 项目管理模块 ──
{
path: '/projectManagement',
name: 'ProjectManagement',
component: () => import('@/views/projectManagement/index.vue'),
meta: {
title: '项目管理',
},
},
{
path: '/projectManagementDetail/:detail?',
name: 'ProjectManagementDetail',
component: () => import('@/views/projectManagement/detail.vue'),
meta: {
title: '项目详情',
},
},
// ── 扫码入口模块 ──
{
path: '/pshDetail/:id?',
name: 'PshDetail',
component: () => import('@/views/clientQR/pshDetail.vue'),
meta: {
title: '排水户详情',
},
},
{
path: '/deviceDetails/:id?',
name: 'DeviceDetails',
component: () => import('@/views/clientQR/deviceDetails.vue'),
meta: {
title: '设备详情',
},
},
// 404 兜底
{
path: '/:pathMatch(.*)*',

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
/**
* 设备详情(二维码扫码入口)
*
* 通过扫码获取设备 ID展示设备信息及养护记录入口。
*/
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const deviceId = computed(() => route.params.id as string | undefined)
/** 模拟设备详情 */
const device = ref({
id: deviceId.value || 'DEV-001',
name: '流量监测仪 A-01',
type: '流量监测',
model: 'FLOW-M200',
location: '城北区东风路雨水管网检查井 J-12',
status: 'normal',
installDate: '2024-06-15',
lastMaintain: '2025-05-20',
protocol: 'MQTT',
battery: '85%',
})
const statusMap: Record<string, string> = {
normal: '正常',
alarm: '告警',
offline: '离线',
}
const statusColorMap: Record<string, string> = {
normal: '#07c160',
alarm: '#ee0a24',
offline: '#999',
}
function goBack() {
router.back()
}
function goMaintenance() {
// 跳转到养护记录
router.push('/maintenanceRecords')
}
</script>
<template>
<div class="device-detail-page">
<van-nav-bar title="设备详情" left-arrow fixed placeholder @click-left="goBack" />
<!-- 状态卡片 -->
<div class="status-card">
<span class="status-dot" :style="{ backgroundColor: statusColorMap[device.status] }" />
<span class="status-text" :style="{ color: statusColorMap[device.status] }">
{{ statusMap[device.status] }}
</span>
</div>
<!-- 基本信息 -->
<van-cell-group title="基本信息" inset>
<van-cell title="设备编号" :value="device.id" />
<van-cell title="设备名称" :value="device.name" />
<van-cell title="设备类型" :value="device.type" />
<van-cell title="设备型号" :value="device.model" />
<van-cell title="安装位置" :label="device.location" />
<van-cell title="安装日期" :value="device.installDate" />
<van-cell title="通信协议" :value="device.protocol" />
<van-cell title="电池电量" :value="device.battery" />
</van-cell-group>
<!-- 最近养护 -->
<van-cell-group title="最近养护" inset>
<van-cell title="上次养护日期" :value="device.lastMaintain" />
</van-cell-group>
<!-- 操作按钮 -->
<div class="action-section">
<van-button type="primary" block round @click="goMaintenance">
查看养护记录
</van-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.device-detail-page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: 20px;
: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 {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 12px 16px;
padding: 16px;
background: var(--color-bg-card);
border-radius: 10px;
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.status-text {
font-size: 16px;
font-weight: 600;
}
}
.action-section {
padding: 20px 16px;
}
</style>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
/**
* 排水户详情(二维码扫码入口)
*
* 通过扫码获取排水户 ID展示详细信息及二维码。
*/
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const pshId = computed(() => route.params.id as string | undefined)
/** 模拟排水户详情 */
const detail = ref({
id: pshId.value || 'PSH-001',
name: '万达广场(城北店)',
type: '商业',
address: '城北区东风路 128 号',
contact: '张经理',
phone: '138****5678',
licenseNo: '排许字第2024-0031号',
drainType: '雨污分流',
qrCodeUrl: '', // 实际从接口获取
})
function goBack() {
router.back()
}
</script>
<template>
<div class="psh-detail-page">
<van-nav-bar title="排水户详情" left-arrow fixed placeholder @click-left="goBack" />
<!-- 基本信息 -->
<van-cell-group title="基本信息" inset>
<van-cell title="排水户编号" :value="detail.id" />
<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
v-if="detail.qrCodeUrl"
:src="detail.qrCodeUrl"
width="160"
height="160"
fit="contain"
/>
<template v-else>
<van-icon name="qr" size="64" color="#ccc" />
<p class="qr-hint">扫码查看排水户信息</p>
</template>
</div>
</div>
</van-cell-group>
</div>
</template>
<style lang="scss" scoped>
.psh-detail-page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: 20px;
: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 {
display: flex;
justify-content: center;
padding: 24px;
.qr-placeholder {
width: 160px;
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed #ddd;
border-radius: 10px;
background: #fafafa;
.qr-hint {
margin: 8px 0 0 0;
font-size: 12px;
color: var(--color-text-placeholder);
}
}
}
</style>

View File

@@ -0,0 +1,117 @@
<script setup lang="ts">
/**
* 班组人员打卡列表
*
* 展示防汛值班人员的打卡记录,
* 支持按班组切换和日期筛选。
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const activeTab = ref(0)
const dateFilter = ref('')
/** 打卡状态映射 */
const statusMap: Record<string, string> = {
clocked: '已打卡',
late: '迟到',
missing: '缺卡',
}
const statusColorMap: Record<string, 'success' | 'warning' | 'danger'> = {
clocked: 'success',
late: 'warning',
missing: 'danger',
}
/** 模拟打卡数据 */
const mockRecords = [
{ id: 1, name: '张建国', team: 'A组', time: '08:25', status: 'clocked', location: '泵站1号' },
{ id: 2, name: '李明辉', team: 'A组', time: '08:32', status: 'late', location: '泵站1号' },
{ id: 3, name: '王强', team: 'B组', time: '08:15', status: 'clocked', location: '闸门3号' },
{ id: 4, name: '赵勇', team: 'B组', time: '--:--', status: 'missing', location: '闸门3号' },
{ id: 5, name: '陈志远', team: 'A组', time: '08:20', status: 'clocked', location: '河道巡查点' },
]
const filteredList = computed(() => {
let list = mockRecords
if (activeTab.value === 1) list = list.filter(r => r.team === 'A组')
else if (activeTab.value === 2) list = list.filter(r => r.team === 'B组')
return list
})
function goTeamList() {
router.push('/teamList')
}
</script>
<template>
<div class="page-container">
<van-nav-bar title="打卡记录" left-arrow fixed placeholder @click-left="router.back()">
<template #right>
<van-icon name="user-o" size="20" @click="goTeamList" />
</template>
</van-nav-bar>
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="A组" />
<van-tab title="B组" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无打卡记录" />
<van-card
v-for="item in filteredList"
:key="item.id"
:title="item.name"
:desc="`点位: ${item.location}`"
>
<template #tags>
<van-tag :type="statusColorMap[item.status]" size="medium">
{{ statusMap[item.status] }}
</van-tag>
</template>
<template #footer>
<div class="card-meta">
<span>打卡时间: {{ item.time }}</span>
<span>{{ item.team }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
/**
* 防汛指令列表
*
* 展示下发的防汛指令,支持按状态筛选和搜索,
* 点击可查看指令详情并处理。
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const searchText = ref('')
const activeTab = ref(0)
/** 状态映射 */
const statusMap: Record<string, string> = {
pending: '待接收',
accepted: '已接收',
completed: '已完成',
}
const statusColorMap: Record<string, 'warning' | 'primary' | 'success'> = {
pending: 'warning',
accepted: 'primary',
completed: 'success',
}
/** 级别映射 */
const levelMap: Record<string, string> = {
urgent: '紧急',
normal: '普通',
}
const levelColorMap: Record<string, string> = {
urgent: '#ee0a24',
normal: '#1989fa',
}
/** 模拟指令数据 */
const mockInstructions = [
{ id: 1, title: '启动III级防汛应急响应', level: 'urgent', from: '市防汛指挥部', time: '2025-06-15 08:00', status: 'pending' },
{ id: 2, title: '河道巡查任务通知', level: 'normal', from: '水务局', time: '2025-06-14 14:30', status: 'accepted' },
{ id: 3, title: '泵站设备检修通知', level: 'normal', from: '运维中心', time: '2025-06-13 09:00', status: 'completed' },
{ id: 4, title: '重点积水路段值守', level: 'urgent', from: '市政管理处', time: '2025-06-15 07:30', status: 'pending' },
]
const filteredList = computed(() => {
let list = mockInstructions
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(i =>
i.title.toLowerCase().includes(kw) ||
i.from.toLowerCase().includes(kw)
)
}
if (activeTab.value === 1) list = list.filter(i => i.status === 'pending')
else if (activeTab.value === 2) list = list.filter(i => i.status === 'accepted')
else if (activeTab.value === 3) list = list.filter(i => i.status === 'completed')
return list
})
function goReceive(id: number) {
router.push('/instructionReceive')
}
</script>
<template>
<div class="page-container">
<van-nav-bar title="防汛指令" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索指令标题、来源" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="待接收" />
<van-tab title="已接收" />
<van-tab title="已完成" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无指令" />
<van-card
v-for="item in filteredList"
:key="item.id"
:title="item.title"
:desc="`来自: ${item.from}`"
@click="goReceive(item.id)"
>
<template #tags>
<van-tag :color="levelColorMap[item.level]" size="medium" text-color="#fff">
{{ levelMap[item.level] }}
</van-tag>
<van-tag :type="statusColorMap[item.status]" size="medium">
{{ statusMap[item.status] }}
</van-tag>
</template>
<template #footer>
<div class="card-meta">
<span>{{ item.time }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
:deep(.van-tag) {
margin-right: 4px;
}
}
.card-meta {
font-size: 12px;
color: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
/**
* 指令接收详情
*
* 展示防汛指令的详细内容和操作按钮,
* 支持接收确认和退回操作。
*/
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showSuccessToast, showConfirmDialog } from 'vant'
const router = useRouter()
const route = useRoute()
/** 接收状态 */
const accepted = ref(false)
const remark = ref('')
/** 模拟指令详情 */
const instruction = {
id: 1,
title: '启动III级防汛应急响应',
level: 'urgent',
from: '市防汛指挥部',
time: '2025-06-15 08:00',
content: '根据市气象台发布的暴雨橙色预警预计未来6小时内我市将出现大范围强降雨累计雨量可达80-120毫米。经研究决定自2025年6月15日08时起启动III级防汛应急响应。请各相关单位按要求落实防汛措施。',
contact: '张主任',
phone: '010-88886666',
}
function onAccept() {
accepted.value = true
showSuccessToast('已接收指令')
}
function onDecline() {
showConfirmDialog({
title: '退回指令',
message: '确定要退回该指令吗?请填写退回原因。',
})
.then(() => {
showSuccessToast('已退回')
router.back()
})
.catch(() => {})
}
</script>
<template>
<div class="page-container">
<van-nav-bar
title="指令详情"
left-arrow
fixed
placeholder
@click-left="router.back()"
/>
<van-cell-group inset>
<van-cell title="指令标题" :value="instruction.title" />
<van-cell title="来源单位" :value="instruction.from" />
<van-cell title="发布时间" :value="instruction.time" />
<van-cell title="联 系 人" :value="instruction.contact" />
<van-cell title="联系电话" :value="instruction.phone" />
</van-cell-group>
<van-cell-group inset style="margin-top: 12px">
<van-cell title="指令内容" />
<div class="content-block">{{ instruction.content }}</div>
</van-cell-group>
<van-cell-group v-if="accepted" inset style="margin-top: 12px">
<van-field
v-model="remark"
label="备注"
placeholder="可选填写接收备注"
type="textarea"
rows="2"
autosize
/>
</van-cell-group>
<div class="action-bar">
<van-button
v-if="!accepted"
type="primary"
block
round
@click="onAccept"
>
接收指令
</van-button>
<van-button
v-else
type="success"
block
round
@click="router.back()"
>
确认完成
</van-button>
<van-button
v-if="!accepted"
type="default"
block
round
style="margin-top: 10px"
@click="onDecline"
>
退回
</van-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
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;
}
}
.content-block {
padding: 12px 16px;
font-size: 14px;
line-height: 1.8;
color: var(--color-text-regular);
background: var(--color-bg-card);
white-space: pre-wrap;
}
.action-bar {
padding: 24px 16px;
}
</style>

View File

@@ -0,0 +1,141 @@
<script setup lang="ts">
/**
* 物资出入库
*
* 对防汛物资进行入库或出库操作,
* 记录数量、类型和操作说明。
*/
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showSuccessToast } from 'vant'
const router = useRouter()
const route = useRoute()
/** 操作类型 */
const opType = ref<'in' | 'out'>('in')
const quantity = ref('')
const batchNo = ref('')
const operator = ref('')
const remark = ref('')
/** 模拟物资信息 */
const material = {
id: route.params.id,
name: '柴油水泵',
stock: 12,
unit: '台',
location: '1号仓库',
}
function onSubmit() {
if (!quantity.value) {
showSuccessToast('请填写数量')
return
}
if (!operator.value) {
showSuccessToast('请填写操作人')
return
}
const action = opType.value === 'in' ? '入库' : '出库'
showSuccessToast(`${action}成功`)
router.back()
}
</script>
<template>
<div class="page-container">
<van-nav-bar
title="出入库"
left-arrow
fixed
placeholder
@click-left="router.back()"
/>
<van-cell-group inset>
<van-cell title="物资名称" :value="material.name" />
<van-cell title="当前库存" :value="`${material.stock} ${material.unit}`" />
<van-cell title="存放位置" :value="material.location" />
</van-cell-group>
<van-form @submit="onSubmit" style="margin-top: 12px">
<van-cell-group inset>
<van-field
name="opType"
label="操作类型"
>
<template #input>
<van-radio-group v-model="opType" direction="horizontal">
<van-radio name="in">入库</van-radio>
<van-radio name="out">出库</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field
v-model="quantity"
name="quantity"
label="数量"
:placeholder="`请输入${opType === 'in' ? '入库' : '出库'}数量`"
type="digit"
:rules="[{ required: true, message: '请填写数量' }]"
>
<template #extra>
<span class="unit-text">{{ material.unit }}</span>
</template>
</van-field>
<van-field
v-model="batchNo"
name="batchNo"
label="批次号"
placeholder="可选填写批次号"
/>
<van-field
v-model="operator"
name="operator"
label="操作人"
placeholder="请输入操作人姓名"
:rules="[{ required: true, message: '请填写操作人' }]"
/>
<van-field
v-model="remark"
name="remark"
label="备注"
placeholder="可选填写备注说明"
type="textarea"
rows="2"
autosize
/>
</van-cell-group>
<div class="submit-wrap">
<van-button type="primary" block round native-type="submit">
确认{{ opType === 'in' ? '入库' : '出库' }}
</van-button>
</div>
</van-form>
</div>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.unit-text {
color: var(--color-text-secondary);
font-size: 14px;
}
.submit-wrap {
padding: 24px 16px;
}
</style>

View File

@@ -0,0 +1,152 @@
<script setup lang="ts">
/**
* 防汛物资管理
*
* 展示防汛物资库存列表,支持按类型筛选和搜索,
* 显示各物资的库存数量。
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const searchText = ref('')
const activeTab = ref(0)
/** 物资类型映射 */
const typeMap: Record<string, string> = {
pump: '水泵类',
sandbag: '沙袋类',
tool: '工具类',
rescue: '救援类',
}
const typeColorMap: Record<string, string> = {
pump: '#1989fa',
sandbag: '#ff976a',
tool: '#07c160',
rescue: '#ee0a24',
}
/** 库存状态 */
const stockStatusMap: Record<string, { label: string; type: 'success' | 'warning' | 'danger' }> = {
sufficient: { label: '充足', type: 'success' },
normal: { label: '正常', type: '' as 'success' },
low: { label: '偏低', type: 'warning' },
shortage: { label: '紧缺', type: 'danger' },
}
/** 模拟物资数据 */
const mockMaterials = [
{ id: 1, name: '柴油水泵', type: 'pump', stock: 12, unit: '台', minStock: 5, status: 'sufficient', location: '1号仓库' },
{ id: 2, name: '防洪沙袋', type: 'sandbag', stock: 500, unit: '个', minStock: 200, status: 'sufficient', location: '2号仓库' },
{ id: 3, name: '救生衣', type: 'rescue', stock: 30, unit: '件', minStock: 50, status: 'low', location: '1号仓库' },
{ id: 4, name: '铁锹', type: 'tool', stock: 8, unit: '把', minStock: 20, status: 'shortage', location: '2号仓库' },
{ id: 5, name: '发电机', type: 'pump', stock: 6, unit: '台', minStock: 3, status: 'normal', location: '1号仓库' },
{ id: 6, name: '应急照明灯', type: 'tool', stock: 15, unit: '个', minStock: 10, status: 'normal', location: '3号仓库' },
]
const typeTabs = ['all', 'pump', 'sandbag', 'tool', 'rescue']
const typeTabNames = ['全部', '水泵类', '沙袋类', '工具类', '救援类']
const filteredList = computed(() => {
let list = mockMaterials
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(m =>
m.name.toLowerCase().includes(kw) ||
m.location.toLowerCase().includes(kw)
)
}
if (activeTab.value > 0) {
list = list.filter(m => m.type === typeTabs[activeTab.value])
}
return list
})
function goInventory(id: number) {
router.push(`/managementInventory/${id}`)
}
</script>
<template>
<div class="page-container">
<van-nav-bar title="物资管理" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索物资名称、存放位置" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab v-for="(name, i) in typeTabNames" :key="i" :title="name" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无物资" />
<van-card
v-for="item in filteredList"
:key="item.id"
:title="item.name"
:desc="`存放位置: ${item.location}`"
@click="goInventory(item.id)"
>
<template #tags>
<van-tag :color="typeColorMap[item.type]" size="medium" text-color="#fff">
{{ typeMap[item.type] }}
</van-tag>
<van-tag :type="stockStatusMap[item.status].type" size="medium">
{{ stockStatusMap[item.status].label }}
</van-tag>
</template>
<template #footer>
<div class="card-meta">
<span class="stock-count">库存: {{ item.stock }} {{ item.unit }}</span>
<span class="stock-min">最低库存: {{ item.minStock }} {{ item.unit }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
:deep(.van-tag) {
margin-right: 4px;
}
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
.stock-count {
font-weight: 500;
color: var(--color-text-primary);
}
.stock-min {
color: var(--color-text-placeholder);
}
}
</style>

129
src/views/fxgl/teamList.vue Normal file
View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
/**
* 选择班组成员
*
* 展示班组成员列表,支持搜索和多选,
* 用于分配防汛任务时的成员勾选。
*/
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showSuccessToast } from 'vant'
const router = useRouter()
const route = useRoute()
const searchText = ref('')
const checkedIds = ref<number[]>([])
/** 模拟成员数据 */
const mockMembers = [
{ id: 1, name: '张建国', phone: '13800001001', team: 'A组', role: '组长' },
{ id: 2, name: '李明辉', phone: '13800001002', team: 'A组', role: '组员' },
{ id: 3, name: '陈志远', phone: '13800001003', team: 'A组', role: '组员' },
{ id: 4, name: '王强', phone: '13800001004', team: 'B组', role: '组长' },
{ id: 5, name: '赵勇', phone: '13800001005', team: 'B组', role: '组员' },
{ id: 6, name: '周文博', phone: '13800001006', team: 'B组', role: '组员' },
]
const filteredList = computed(() => {
if (!searchText.value) return mockMembers
const kw = searchText.value.toLowerCase()
return mockMembers.filter(m =>
m.name.toLowerCase().includes(kw) ||
m.phone.includes(kw)
)
})
function toggleAll() {
if (checkedIds.value.length === filteredList.value.length) {
checkedIds.value = []
} else {
checkedIds.value = filteredList.value.map(m => m.id)
}
}
function onConfirm() {
showSuccessToast(`已选择 ${checkedIds.value.length}`)
router.back()
}
</script>
<template>
<div class="page-container">
<van-nav-bar title="选择班组成员" left-arrow fixed placeholder @click-left="router.back()">
<template #right>
<span class="nav-action" @click="toggleAll">
{{ checkedIds.length === filteredList.length && filteredList.length > 0 ? '取消全选' : '全选' }}
</span>
</template>
</van-nav-bar>
<van-search v-model="searchText" placeholder="搜索姓名、手机号" shape="round" />
<van-checkbox-group v-model="checkedIds">
<van-cell-group inset>
<van-cell
v-for="item in filteredList"
:key="item.id"
:title="item.name"
:label="`${item.team} · ${item.role}`"
clickable
@click="() => {
const idx = checkedIds.indexOf(item.id)
if (idx >= 0) checkedIds.splice(idx, 1)
else checkedIds.push(item.id)
}"
>
<template #value>
<van-tag :type="item.role === '组长' ? 'primary' : undefined" size="medium">
{{ item.role }}
</van-tag>
</template>
<template #right-icon>
<van-checkbox :name="item.id" />
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<van-empty v-if="filteredList.length === 0" description="暂未找到成员" />
<div class="footer-bar">
<van-button type="primary" block round @click="onConfirm">
确认选择 ({{ checkedIds.length }})
</van-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: 80px;
: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;
}
}
.nav-action {
color: #fff;
font-size: 14px;
cursor: pointer;
}
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
background: var(--color-bg-card);
box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.06);
}
</style>

135
src/views/map/index.vue Normal file
View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
/**
* 全屏地图页面
*
* MapLibre 地图容器,底部工具栏可切换图层、
* 打开弹出窗口等操作。
*/
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import TckzPop from './tckzPop.vue'
import YbPop from './ybPop.vue'
const router = useRouter()
/** 底部工具栏状态 */
const tools = [
{ icon: 'location-o', label: '定位' },
{ icon: 'eye-o', label: '图层' },
{ icon: 'search', label: '搜索' },
{ icon: 'records', label: '台账' },
{ icon: 'data-o', label: '仪表' },
]
/** 弹窗显示控制 */
const showTckz = ref(false)
const showYb = ref(false)
function handleToolClick(label: string) {
if (label === '台账') {
showTckz.value = true
} else if (label === '仪表') {
showYb.value = true
}
}
</script>
<template>
<div class="map-page">
<van-nav-bar title="地图" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
<!-- MapLibre 地图容器占位 -->
<div id="ytmap-container" class="map-container">
<div class="map-placeholder">
<van-icon name="map-marked" size="64" color="#ccc" />
<p class="placeholder-title">智慧水务地图</p>
<p class="placeholder-hint">MapLibre 地图将在此处加载</p>
</div>
</div>
<!-- 底部工具栏 -->
<div class="bottom-tools">
<div
v-for="tool in tools"
:key="tool.label"
class="tool-item"
@click="handleToolClick(tool.label)"
>
<van-icon :name="tool.icon" size="20" />
<span class="tool-label">{{ tool.label }}</span>
</div>
</div>
<!-- 弹窗组件 -->
<TckzPop v-model:show="showTckz" />
<YbPop v-model:show="showYb" />
</div>
</template>
<style lang="scss" scoped>
.map-page {
height: 100vh;
display: flex;
flex-direction: column;
background: var(--color-bg-page);
: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;
}
}
.map-container {
flex: 1;
position: relative;
margin: 0;
}
.map-placeholder {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f0f3f7;
.placeholder-title {
margin: 12px 0 4px 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-regular);
}
.placeholder-hint {
font-size: 13px;
color: var(--color-text-placeholder);
}
}
.bottom-tools {
display: flex;
justify-content: space-around;
align-items: center;
background: var(--color-bg-card);
padding: 8px 16px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
z-index: 10;
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 4px 8px;
.tool-label {
font-size: 12px;
color: var(--color-text-regular);
}
}
}
</style>

119
src/views/map/tckzPop.vue Normal file
View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
/**
* 台账控制弹窗
*
* 从地图底部弹出的台账列表,展示管网/设施信息。
*/
import { ref } from 'vue'
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
'update:show': [val: boolean]
}>()
/** 模拟台账数据 */
const records = ref([
{ id: 1, name: 'DN300 雨水管-城北段', type: '雨水管网', length: '2.3km' },
{ id: 2, name: 'DN400 污水管-高新区', type: '污水管网', length: '1.8km' },
{ id: 3, name: '1# 提升泵站', type: '泵站', length: '-' },
{ id: 4, name: '中山河节制闸', type: '水闸', length: '-' },
])
function onClose() {
emit('update:show', false)
}
</script>
<template>
<van-popup
:show="show"
position="bottom"
round
:style="{ height: '50%' }"
@update:show="emit('update:show', $event)"
>
<div class="tckz-pop">
<div class="pop-header">
<span class="pop-title">台账信息</span>
<van-icon name="cross" size="20" color="#999" @click="onClose" />
</div>
<div class="pop-body">
<div
v-for="item in records"
:key="item.id"
class="record-item"
>
<div class="record-icon">
<van-icon name="notes-o" size="20" color="#1989fa" />
</div>
<div class="record-info">
<span class="record-name">{{ item.name }}</span>
<span class="record-meta">{{ item.type }} · {{ item.length }}</span>
</div>
<van-icon name="arrow" color="#ccc" />
</div>
</div>
</div>
</van-popup>
</template>
<style lang="scss" scoped>
.tckz-pop {
height: 100%;
display: flex;
flex-direction: column;
.pop-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
.pop-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-regular);
}
}
.pop-body {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.record-item {
display: flex;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
.record-icon {
margin-right: 12px;
flex-shrink: 0;
}
.record-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.record-name {
font-size: 14px;
color: var(--color-text-regular);
}
.record-meta {
font-size: 12px;
color: var(--color-text-placeholder);
}
}
}
}
</style>

137
src/views/map/ybPop.vue Normal file
View File

@@ -0,0 +1,137 @@
<script setup lang="ts">
/**
* 仪表弹窗
*
* 从地图底部弹出的仪表/监测数据面板。
*/
import { ref } from 'vue'
defineProps<{
show: boolean
}>()
const emit = defineEmits<{
'update:show': [val: boolean]
}>()
/** 模拟仪表数据 */
const gauges = ref([
{ id: 1, name: '流量', value: '12.5', unit: 'm³/h', status: 'normal' },
{ id: 2, name: '压力', value: '0.35', unit: 'MPa', status: 'normal' },
{ id: 3, name: '液位', value: '2.8', unit: 'm', status: 'alarm' },
{ id: 4, name: '水质', value: '6.8', unit: 'pH', status: 'normal' },
])
const statusColorMap: Record<string, string> = {
normal: '#07c160',
alarm: '#ee0a24',
}
</script>
<template>
<van-popup
:show="show"
position="bottom"
round
:style="{ height: '40%' }"
@update:show="emit('update:show', $event)"
>
<div class="yb-pop">
<div class="pop-header">
<span class="pop-title">实时仪表</span>
<van-icon name="cross" size="20" color="#999" @click="emit('update:show', false)" />
</div>
<div class="pop-body">
<div
v-for="gauge in gauges"
:key="gauge.id"
class="gauge-item"
>
<span
class="gauge-dot"
:style="{ backgroundColor: statusColorMap[gauge.status] }"
/>
<div class="gauge-info">
<span class="gauge-label">{{ gauge.name }}</span>
<span class="gauge-value">
{{ gauge.value }}
<small>{{ gauge.unit }}</small>
</span>
</div>
</div>
</div>
</div>
</van-popup>
</template>
<style lang="scss" scoped>
.yb-pop {
height: 100%;
display: flex;
flex-direction: column;
.pop-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
.pop-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-regular);
}
}
.pop-body {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px 20px;
}
.gauge-item {
width: calc(50% - 8px);
display: flex;
align-items: center;
background: #f7f8fa;
border-radius: 10px;
padding: 16px;
.gauge-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 12px;
flex-shrink: 0;
}
.gauge-info {
display: flex;
flex-direction: column;
gap: 4px;
.gauge-label {
font-size: 13px;
color: var(--color-text-secondary);
}
.gauge-value {
font-size: 22px;
font-weight: 700;
color: var(--color-text-regular);
small {
font-size: 12px;
font-weight: 400;
color: var(--color-text-placeholder);
}
}
}
}
}
</style>

View File

@@ -0,0 +1,173 @@
<script setup lang="ts">
/**
* 项目详情页
*
* 展示单个项目的详细信息、进度和里程碑。
*/
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const detailId = computed(() => route.params.detail as string | undefined)
/** 模拟项目详情 */
const project = ref({
id: detailId.value || 'XM-2025-001',
name: '城北雨水管网改造工程',
no: 'XM-2025-001',
company: '中建三局',
progress: 75,
status: 'building',
manager: '赵工',
startDate: '2025-03-01',
endDate: '2025-12-31',
budget: '3800万元',
description: '对城北片区老旧雨水管网进行全面改造包括新建DN300-DN600雨水管12.5km改造检查井280座新建雨水泵站1座。',
})
const statusMap: Record<string, string> = {
building: '在建',
paused: '暂停',
completed: '竣工',
}
const statusTagType: Record<string, 'primary' | 'warning' | 'success'> = {
building: 'primary',
paused: 'warning',
completed: 'success',
}
function progressColor(pct: number): string {
if (pct >= 80) return '#07c160'
if (pct >= 40) return '#1989fa'
return '#ff976a'
}
/** 模拟里程碑 */
const milestones = ref([
{ text: '项目立项', time: '2025-02-15', done: true },
{ text: '施工设计', time: '2025-03-15', done: true },
{ text: '管网铺设', time: '2025-06-30', done: true },
{ text: '泵站建设', time: '2025-09-30', done: false },
{ text: '竣工验收', time: '2025-12-31', done: false },
])
</script>
<template>
<div class="detail-page">
<van-nav-bar title="项目详情" left-arrow fixed placeholder @click-left="router.back()" />
<!-- 基本信息 -->
<van-cell-group title="基本信息" inset>
<van-cell title="项目编号" :value="project.no" />
<van-cell title="项目名称" :value="project.name" />
<van-cell title="施工单位" :value="project.company" />
<van-cell title="负责人" :value="project.manager" />
<van-cell title="计划工期">
<span class="date-range">{{ project.startDate }} ~ {{ project.endDate }}</span>
</van-cell>
<van-cell title="预算金额" :value="project.budget" />
<van-cell title="当前状态">
<van-tag :type="statusTagType[project.status]" size="medium">
{{ statusMap[project.status] }}
</van-tag>
</van-cell>
</van-cell-group>
<!-- 项目描述 -->
<van-cell-group title="项目描述" inset>
<van-cell>
<p class="project-desc">{{ project.description }}</p>
</van-cell>
</van-cell-group>
<!-- 进度 -->
<van-cell-group title="工程进度" inset>
<div class="progress-section">
<van-progress
:percentage="project.progress"
:color="progressColor(project.progress)"
:pivot-text="`${project.progress}%`"
stroke-width="10"
/>
</div>
</van-cell-group>
<!-- 里程碑 -->
<van-cell-group title="项目里程碑" inset>
<div class="milestones-section">
<van-steps :active="milestones.filter(m => m.done).length - 1" direction="vertical" active-color="#1989fa">
<van-step v-for="(step, idx) in milestones" :key="idx">
<template #active-icon>
<van-icon name="checked" color="#1989fa" />
</template>
<template #inactive-icon>
<van-icon name="clock-o" color="#ccc" />
</template>
<h4>{{ step.text }}</h4>
<p>{{ step.time }}</p>
</van-step>
</van-steps>
</div>
</van-cell-group>
</div>
</template>
<style lang="scss" scoped>
.detail-page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: 20px;
: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;
}
}
.date-range {
font-size: 13px;
color: var(--color-text-secondary);
}
.project-desc {
font-size: 14px;
color: var(--color-text-regular);
line-height: 1.6;
margin: 0;
}
.progress-section {
padding: 16px;
}
.milestones-section {
padding: 12px 0;
:deep(.van-step__title) {
h4 {
margin: 0;
font-size: 14px;
color: var(--color-text-regular);
}
p {
margin: 4px 0 0;
font-size: 12px;
color: var(--color-text-placeholder);
}
}
}
</style>

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
/**
* 项目管理列表页
*
* 展示所有工程项目,支持搜索和按状态筛选,
* 每项显示进度条。
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const searchText = ref('')
const activeTab = ref(0)
const statusMap: Record<string, string> = {
building: '在建',
paused: '暂停',
completed: '竣工',
}
const statusTagType: Record<string, 'primary' | 'warning' | 'success'> = {
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: 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: 4, name: '开发区污水管网铺设', no: 'XM-2025-004', company: '中交一公局', progress: 0, status: 'paused', manager: '李工' },
{ id: 5, name: '老城区雨污分流改造', no: 'XM-2025-005', company: '中铁十二局', progress: 100, status: 'completed', manager: '周工' },
{ id: 6, name: '南湖片区海绵城市试点', no: 'XM-2025-006', company: '葛洲坝集团', progress: 55, status: 'building', manager: '吴工' },
]
const filteredList = computed(() => {
let list = projects
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(p =>
p.name.toLowerCase().includes(kw) ||
p.no.toLowerCase().includes(kw) ||
p.company.toLowerCase().includes(kw)
)
}
if (activeTab.value === 1) list = list.filter(p => p.status === 'building')
else if (activeTab.value === 2) list = list.filter(p => p.status === 'paused')
else if (activeTab.value === 3) list = list.filter(p => p.status === 'completed')
return list
})
function goDetail(id: number) {
router.push(`/projectManagementDetail/${id}`)
}
</script>
<template>
<div class="page-container">
<van-nav-bar title="项目管理" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索项目名称、编号、单位" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="在建" />
<van-tab title="暂停" />
<van-tab title="竣工" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无项目" />
<van-card
v-for="item in filteredList"
:key="item.id"
:title="item.name"
:desc="`施工单位: ${item.company}`"
@click="goDetail(item.id)"
>
<template #tags>
<van-tag :type="statusTagType[item.status]" size="medium">
{{ statusMap[item.status] }}
</van-tag>
</template>
<template #footer>
<div class="progress-wrap">
<van-progress
:percentage="item.progress"
:color="progressColor(item.progress)"
:pivot-text="`${item.progress}%`"
/>
</div>
<div class="card-meta">
<span>{{ item.no }}</span>
<span>负责人: {{ item.manager }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
}
.progress-wrap {
margin: 8px 0;
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,140 @@
<script setup lang="ts">
/**
* 排水户检查列表页
*
* 展示检查记录列表,支持搜索和结果筛选,显示评分。
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
/** 搜索关键词 */
const searchText = ref('')
/** 当前激活的 Tab */
const activeTab = ref(0)
/** 模拟检查记录数据 */
const mockCheckList = [
{ id: 1, pshName: '华丰食品厂', checkDate: '2025-06-15', inspector: '张检查员', result: 'pass', score: 92, items: '排水设施、预处理设备' },
{ id: 2, pshName: '阳光花园小区', checkDate: '2025-06-14', inspector: '李检查员', result: 'pass', score: 85, items: '化粪池、雨污管道' },
{ id: 3, pshName: '万达商业广场', checkDate: '2025-06-13', inspector: '王检查员', result: 'fail', score: 55, items: '油脂分离器、排水口' },
{ id: 4, pshName: '东风化工厂', checkDate: '2025-06-12', inspector: '赵检查员', result: 'fail', score: 40, items: '污水处理设备、排放口' },
{ id: 5, pshName: '翠湖小区', checkDate: '2025-06-11', inspector: '孙检查员', result: 'pass', score: 88, items: '雨污分流、管道畅通' },
]
/** 结果映射 */
const resultMap: Record<string, string> = {
pass: '合格',
fail: '不合格',
}
const resultColorMap: Record<string, 'success' | 'danger'> = {
pass: 'success',
fail: 'danger',
}
/** 分数颜色 */
function getScoreColor(score: number): string {
if (score >= 80) return '#07c160'
if (score >= 60) return '#ff976a'
return '#ee0a24'
}
/** 筛选后的列表 */
const filteredList = computed(() => {
let list = mockCheckList
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(c =>
c.pshName.toLowerCase().includes(kw) ||
c.inspector.toLowerCase().includes(kw) ||
c.items.toLowerCase().includes(kw)
)
}
if (activeTab.value === 1) list = list.filter(c => c.result === 'pass')
else if (activeTab.value === 2) list = list.filter(c => c.result === 'fail')
return list
})
/** 跳转详情 */
function goDetail(id: number) {
router.push(`/pshCheckList/${id}`)
}
</script>
<template>
<div class="check-page">
<van-nav-bar title="检查记录列表" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索排水户、检查员、检查项" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="合格" />
<van-tab title="不合格" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无检查记录" />
<van-card
v-for="item in filteredList"
:key="item.id"
:title="item.pshName"
:desc="`检查项: ${item.items}`"
@click="goDetail(item.id)"
>
<template #tags>
<van-tag :type="resultColorMap[item.result]" size="medium">
{{ resultMap[item.result] }}
</van-tag>
<van-tag :color="getScoreColor(item.score)" size="medium" text-color="#fff">
{{ item.score }}
</van-tag>
</template>
<template #footer>
<div class="card-meta">
<span>检查员: {{ item.inspector }}</span>
<span>{{ item.checkDate }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.check-page {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
:deep(.van-tag) {
margin-right: 4px;
}
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,366 @@
<script setup lang="ts">
/**
* 排水户检查报告表单页
*
* 提供检查报告填写表单,包含排水设施检查、预处理设备等各项目评分,
* 支持图片上传。
*/
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
const router = useRouter()
const route = useRoute()
const checkId = (route.params.id || route.query.id) as string
const detailId = (route.params.detail || route.query.detail) as string
/** 表单数据 */
const formData = ref({
pshName: '',
checkDate: '',
inspector: '',
drainPipe: '',
drainPipeScore: '',
preTreatment: '',
preTreatmentScore: '',
oilSeparator: '',
oilSeparatorScore: '',
rainSewage: '',
rainSewageScore: '',
overall: '',
overallScore: '',
suggestion: '',
})
/** 排水户选项 */
const pshOptions = [
{ text: '华丰食品厂', value: '华丰食品厂' },
{ text: '阳光花园小区', value: '阳光花园小区' },
{ text: '万达商业广场', value: '万达商业广场' },
{ text: '东风化工厂', value: '东风化工厂' },
{ text: '翠湖小区', value: '翠湖小区' },
]
/** 评分选项 */
const scoreOptions = [
{ text: '5分 - 优秀', value: '5' },
{ text: '4分 - 良好', value: '4' },
{ text: '3分 - 一般', value: '3' },
{ text: '2分 - 较差', value: '2' },
{ text: '1分 - 很差', value: '1' },
]
/** 结果选项 */
const overallOptions = [
{ text: '合格', value: 'pass' },
{ text: '不合格', value: 'fail' },
]
/** 选择器状态 */
const showPshPicker = ref(false)
const showDrainPipeScore = ref(false)
const showPreTreatmentScore = ref(false)
const showOilSeparatorScore = ref(false)
const showRainSewageScore = ref(false)
const showOverallScore = ref(false)
const showOverall = ref(false)
/** 上传文件列表 */
const uploaderList = ref<{ url: string }[]>([])
/** 选择排水户 */
function onConfirmPsh({ selectedOptions }: any) {
formData.value.pshName = selectedOptions[0]?.text || ''
showPshPicker.value = false
}
/** 选择评分 */
function onConfirmScore(field: string, { selectedOptions }: any) {
(formData.value as any)[field] = selectedOptions[0]?.text || ''
const scoreFields: Record<string, string> = {
drainPipeScore: 'showDrainPipeScore',
preTreatmentScore: 'showPreTreatmentScore',
oilSeparatorScore: 'showOilSeparatorScore',
rainSewageScore: 'showRainSewageScore',
overallScore: 'showOverallScore',
}
;(ref as any)[scoreFields[field]].value = false
}
/** 确认综合评定 */
function onConfirmOverall({ selectedOptions }: any) {
formData.value.overall = selectedOptions[0]?.text || ''
showOverall.value = false
}
/** 上传图片后回调 */
function afterRead(file: any) {
uploaderList.value.push({ url: file.content || '' })
}
/** 提交表单 */
function onSubmit() {
if (!formData.value.pshName) {
showToast('请选择排水户')
return
}
if (!formData.value.checkDate) {
showToast('请选择检查日期')
return
}
showToast('检查报告提交成功')
router.back()
}
</script>
<template>
<div class="check-form-page">
<van-nav-bar title="检查报告" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
<van-form @submit="onSubmit" class="form-wrapper">
<van-cell-group title="基本信息" class="form-group">
<!-- 排水户选择 -->
<van-field
v-model="formData.pshName"
is-link
readonly
name="pshName"
label="排水户"
placeholder="请选择排水户"
:rules="[{ required: true, message: '请选择排水户' }]"
@click="showPshPicker = true"
/>
<van-popup v-model:show="showPshPicker" round position="bottom">
<van-picker
:columns="pshOptions"
@confirm="onConfirmPsh"
@cancel="showPshPicker = false"
/>
</van-popup>
<!-- 检查日期 -->
<van-field
v-model="formData.checkDate"
name="checkDate"
label="检查日期"
placeholder="请选择日期"
:rules="[{ required: true, message: '请选择检查日期' }]"
/>
<!-- 检查人 -->
<van-field
v-model="formData.inspector"
name="inspector"
label="检查人"
placeholder="请输入检查人姓名"
/>
</van-cell-group>
<van-cell-group title="检查项目" class="form-group">
<!-- 排水管道 -->
<van-field
v-model="formData.drainPipe"
name="drainPipe"
label="排水管道"
type="textarea"
rows="2"
autosize
placeholder="描述排水管道状况"
/>
<van-field
v-model="formData.drainPipeScore"
is-link
readonly
name="drainPipeScore"
label="评分"
placeholder="请选择评分"
@click="showDrainPipeScore = true"
/>
<van-popup v-model:show="showDrainPipeScore" round position="bottom">
<van-picker
:columns="scoreOptions"
@confirm="(v: any) => onConfirmScore('drainPipeScore', v)"
@cancel="showDrainPipeScore = false"
/>
</van-popup>
<!-- 预处理设备 -->
<van-field
v-model="formData.preTreatment"
name="preTreatment"
label="预处理设备"
type="textarea"
rows="2"
autosize
placeholder="描述预处理设备状况"
/>
<van-field
v-model="formData.preTreatmentScore"
is-link
readonly
name="preTreatmentScore"
label="评分"
placeholder="请选择评分"
@click="showPreTreatmentScore = true"
/>
<van-popup v-model:show="showPreTreatmentScore" round position="bottom">
<van-picker
:columns="scoreOptions"
@confirm="(v: any) => onConfirmScore('preTreatmentScore', v)"
@cancel="showPreTreatmentScore = false"
/>
</van-popup>
<!-- 油脂分离器 -->
<van-field
v-model="formData.oilSeparator"
name="oilSeparator"
label="油脂分离器"
type="textarea"
rows="2"
autosize
placeholder="描述油脂分离器状况"
/>
<van-field
v-model="formData.oilSeparatorScore"
is-link
readonly
name="oilSeparatorScore"
label="评分"
placeholder="请选择评分"
@click="showOilSeparatorScore = true"
/>
<van-popup v-model:show="showOilSeparatorScore" round position="bottom">
<van-picker
:columns="scoreOptions"
@confirm="(v: any) => onConfirmScore('oilSeparatorScore', v)"
@cancel="showOilSeparatorScore = false"
/>
</van-popup>
<!-- 雨污分流 -->
<van-field
v-model="formData.rainSewage"
name="rainSewage"
label="雨污分流"
type="textarea"
rows="2"
autosize
placeholder="描述雨污分流状况"
/>
<van-field
v-model="formData.rainSewageScore"
is-link
readonly
name="rainSewageScore"
label="评分"
placeholder="请选择评分"
@click="showRainSewageScore = true"
/>
<van-popup v-model:show="showRainSewageScore" round position="bottom">
<van-picker
:columns="scoreOptions"
@confirm="(v: any) => onConfirmScore('rainSewageScore', v)"
@cancel="showRainSewageScore = false"
/>
</van-popup>
</van-cell-group>
<van-cell-group title="综合评定" class="form-group">
<van-field
v-model="formData.overall"
is-link
readonly
name="overall"
label="综合评定"
placeholder="请选择评定结果"
@click="showOverall = true"
/>
<van-popup v-model:show="showOverall" round position="bottom">
<van-picker
:columns="overallOptions"
@confirm="onConfirmOverall"
@cancel="showOverall = false"
/>
</van-popup>
<van-field
v-model="formData.overallScore"
is-link
readonly
name="overallScore"
label="综合评分"
placeholder="请选择综合评分"
@click="showOverallScore = true"
/>
<van-popup v-model:show="showOverallScore" round position="bottom">
<van-picker
:columns="scoreOptions"
@confirm="(v: any) => onConfirmScore('overallScore', v)"
@cancel="showOverallScore = false"
/>
</van-popup>
<van-field
v-model="formData.suggestion"
name="suggestion"
label="整改建议"
type="textarea"
rows="3"
autosize
placeholder="请输入整改建议"
maxlength="500"
show-word-limit
/>
</van-cell-group>
<van-cell-group title="现场照片" class="form-group">
<van-uploader
v-model="uploaderList"
:after-read="afterRead"
multiple
:max-count="6"
/>
</van-cell-group>
<!-- 提交按钮 -->
<div class="submit-area">
<van-button round block type="primary" native-type="submit">
提交检查报告
</van-button>
</div>
</van-form>
</div>
</template>
<style lang="scss" scoped>
.check-form-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;
}
}
.form-wrapper {
margin-top: 0;
}
.form-group {
margin-top: 8px;
:deep(.van-uploader) {
padding: 8px 0;
}
}
.submit-area {
padding: 24px 16px;
}
</style>

View File

@@ -0,0 +1,234 @@
<script setup lang="ts">
/**
* 排水户检查选择列表页
*
* 展示待检查的排水户列表,支持搜索和复选框多选,
* 选中后可进入检查填报页面。
*/
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
const router = useRouter()
const route = useRoute()
const pshId = (route.params.id || route.query.id) as string
/** 搜索关键词 */
const searchText = ref('')
/** 选中的排水户 ID 列表 */
const checkedIds = ref<number[]>([])
/** 模拟待检查排水户数据 */
const mockDrainUsers = [
{ id: 1, name: '华丰食品厂', type: 'industrial', address: '城北工业园区A1栋', lastCheck: '2025-05-15', status: 'normal' },
{ id: 2, name: '阳光花园小区', type: 'domestic', address: '阳光路88号', lastCheck: '2025-04-20', status: 'normal' },
{ id: 3, name: '万达商业广场', type: 'commercial', address: '中山路100号', lastCheck: '2025-05-10', status: 'warning' },
{ id: 4, name: '东风化工厂', type: 'industrial', address: '高新区化工大道56号', lastCheck: '2025-03-01', status: 'danger' },
{ id: 5, name: '翠湖小区', type: 'domestic', address: '翠湖路66号', lastCheck: '2025-06-01', status: 'normal' },
]
/** 类型映射 */
const typeMap: Record<string, string> = {
industrial: '工业',
domestic: '生活',
commercial: '商业',
}
const typeColorMap: Record<string, string> = {
industrial: '#1989fa',
domestic: '#07c160',
commercial: '#ff976a',
}
/** 状态映射 */
const statusMap: Record<string, string> = {
normal: '正常',
warning: '需检查',
danger: '急需检查',
}
const statusColorMap: Record<string, 'success' | 'warning' | 'danger'> = {
normal: 'success',
warning: 'warning',
danger: 'danger',
}
/** 筛选后的列表 */
const filteredList = computed(() => {
let list = mockDrainUsers
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(d =>
d.name.toLowerCase().includes(kw) ||
d.address.toLowerCase().includes(kw)
)
}
return list
})
/** 切换选中 */
function toggleCheck(id: number) {
const idx = checkedIds.value.indexOf(id)
if (idx > -1) {
checkedIds.value.splice(idx, 1)
} else {
checkedIds.value.push(id)
}
}
/** 全选/取消全选 */
const isAllChecked = computed(() => {
return filteredList.value.length > 0 && checkedIds.value.length === filteredList.value.length
})
function toggleAll() {
if (isAllChecked.value) {
checkedIds.value = []
} else {
checkedIds.value = filteredList.value.map(d => d.id)
}
}
/** 开始检查 */
function startCheck() {
if (checkedIds.value.length === 0) {
showToast('请选择排水户')
return
}
const ids = checkedIds.value.join(',')
router.push(`/pshCheck/${ids}`)
}
</script>
<template>
<div class="check-select-page">
<van-nav-bar title="选择检查排水户" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索排水户名称、地址" shape="round" />
<div class="toolbar">
<van-checkbox :model-value="isAllChecked" @change="toggleAll">全选</van-checkbox>
<span class="count-text">已选 {{ checkedIds.length }} </span>
</div>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无排水户" />
<div
v-for="item in filteredList"
:key="item.id"
class="check-card"
@click="toggleCheck(item.id)"
>
<van-checkbox :model-value="checkedIds.includes(item.id)" />
<div class="card-body">
<div class="card-title">
<span>{{ item.name }}</span>
<van-tag :color="typeColorMap[item.type]" size="medium" text-color="#fff">
{{ typeMap[item.type] }}
</van-tag>
<van-tag :type="statusColorMap[item.status]" size="medium">
{{ statusMap[item.status] }}
</van-tag>
</div>
<div class="card-desc">{{ item.address }}</div>
<div class="card-meta">上次检查: {{ item.lastCheck }}</div>
</div>
</div>
</div>
<div class="bottom-bar">
<van-button type="primary" block round @click="startCheck">
开始检查 ({{ checkedIds.length }})
</van-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.check-select-page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: 80px;
: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;
}
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: var(--color-bg-card);
.count-text {
font-size: 13px;
color: var(--color-text-secondary);
}
}
.card-list {
padding: 0 8px;
}
.check-card {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 8px;
padding: 12px;
border-radius: 10px;
background: var(--color-bg-card);
cursor: pointer;
:deep(.van-checkbox) {
margin-top: 2px;
}
}
.card-body {
flex: 1;
min-width: 0;
}
.card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 500;
color: var(--color-text-primary);
:deep(.van-tag) {
flex-shrink: 0;
}
}
.card-desc {
margin-top: 4px;
font-size: 13px;
color: var(--color-text-secondary);
}
.card-meta {
margin-top: 4px;
font-size: 11px;
color: var(--color-text-placeholder);
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
background: var(--color-bg-card);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
</style>

151
src/views/pshgl/pshList.vue Normal file
View File

@@ -0,0 +1,151 @@
<script setup lang="ts">
/**
* 排水户管理列表页
*
* 展示排水户列表,支持搜索和类型筛选,
* 点击卡片跳转排水户详情。
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
/** 搜索关键词 */
const searchText = ref('')
/** 当前激活的 Tab (全部/工业/生活/商业) */
const activeTab = ref(0)
/** 模拟排水户数据 */
const mockPshList = [
{ id: 1, name: '华丰食品厂', type: 'industrial', address: '城北工业园区A1栋', contact: '张经理', phone: '13800001011', status: 'normal', drainNo: 'PSH-2025-001' },
{ id: 2, name: '阳光花园小区', type: 'domestic', address: '阳光路88号', contact: '李主任', phone: '13800001012', status: 'normal', drainNo: 'PSH-2025-002' },
{ id: 3, name: '万达商业广场', type: 'commercial', address: '中山路100号', contact: '王主管', phone: '13800001013', status: 'warning', drainNo: 'PSH-2025-003' },
{ id: 4, name: '东风化工厂', type: 'industrial', address: '高新区化工大道56号', contact: '赵厂长', phone: '13800001014', status: 'danger', drainNo: 'PSH-2025-004' },
{ id: 5, name: '翠湖小区', type: 'domestic', address: '翠湖路66号', contact: '孙主任', phone: '13800001015', status: 'normal', drainNo: 'PSH-2025-005' },
]
/** 类型映射 */
const typeMap: Record<string, string> = {
industrial: '工业',
domestic: '生活',
commercial: '商业',
}
const typeColorMap: Record<string, string> = {
industrial: '#1989fa',
domestic: '#07c160',
commercial: '#ff976a',
}
/** 状态映射 */
const statusMap: Record<string, string> = {
normal: '正常',
warning: '异常',
danger: '严重',
}
const statusColorMap: Record<string, 'success' | 'warning' | 'danger'> = {
normal: 'success',
warning: 'warning',
danger: 'danger',
}
/** 筛选后的列表 */
const filteredList = computed(() => {
let list = mockPshList
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(p =>
p.name.toLowerCase().includes(kw) ||
p.address.toLowerCase().includes(kw) ||
p.drainNo.toLowerCase().includes(kw)
)
}
if (activeTab.value === 1) list = list.filter(p => p.type === 'industrial')
else if (activeTab.value === 2) list = list.filter(p => p.type === 'domestic')
else if (activeTab.value === 3) list = list.filter(p => p.type === 'commercial')
return list
})
/** 跳转详情 */
function goDetail(id: number) {
router.push(`/pshglDetail/${id}`)
}
</script>
<template>
<div class="psh-list-page">
<van-nav-bar title="排水户管理" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索名称、地址、编号" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="工业" />
<van-tab title="生活" />
<van-tab title="商业" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无排水户" />
<van-card
v-for="item in filteredList"
:key="item.id"
:title="item.name"
:desc="item.address"
@click="goDetail(item.id)"
>
<template #tags>
<van-tag :color="typeColorMap[item.type]" size="medium" text-color="#fff">
{{ typeMap[item.type] }}
</van-tag>
<van-tag :type="statusColorMap[item.status]" size="medium">
{{ statusMap[item.status] }}
</van-tag>
</template>
<template #footer>
<div class="card-meta">
<span>联系人: {{ item.contact }}</span>
<span>{{ item.drainNo }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.psh-list-page {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
:deep(.van-tag) {
margin-right: 4px;
}
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,167 @@
<script setup lang="ts">
/**
* 排水户问题详情页
*
* 展示排水户问题详细信息,支持处理/转派等操作。
*/
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
const router = useRouter()
const route = useRoute()
const problemId = (route.params.detail || route.query.id) as string
/** 模拟问题详情 */
const problem = ref({
id: problemId || '1',
pshName: '华丰食品厂',
title: '排水管道堵塞',
severity: 'high',
status: 'pending',
reporter: '张工',
reportTime: '2025-06-15 09:30',
address: '城北工业园区A1栋东侧排水口',
description: '日常巡检中发现华丰食品厂东侧排水管道存在严重堵塞现象,排水不畅导致污水溢出,存在环境污染风险。经初步排查,堵塞原因为油脂凝固和杂物堆积。需尽快安排疏通车进行清淤处理,并建议排水户定期清理隔油池。',
assignee: '待分配',
createTime: '2025-06-15 09:30:00',
updateTime: '2025-06-15 09:30:00',
})
const statusMap: Record<string, string> = {
pending: '待处理',
processing: '处理中',
resolved: '已解决',
}
const statusColorMap: Record<string, 'primary' | 'success' | 'warning' | 'danger'> = {
pending: 'warning',
processing: 'primary',
resolved: 'success',
}
const severityMap: Record<string, string> = {
low: '低',
medium: '中',
high: '高',
critical: '紧急',
}
const severityColorMap: Record<string, string> = {
low: '#07c160',
medium: '#ff976a',
high: '#ee0a24',
critical: '#ee0a24',
}
/** 接单 */
function acceptProblem() {
showToast('已接单处理')
}
/** 转派 */
function transferProblem() {
showToast('转派功能')
}
/** 解决 */
function resolveProblem() {
showToast('已标记为已解决')
}
</script>
<template>
<div class="detail-page">
<van-nav-bar title="问题详情" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
<!-- 状态栏 -->
<div class="status-bar">
<van-tag :type="statusColorMap[problem.status]" size="large">
{{ statusMap[problem.status] }}
</van-tag>
<span class="severity-tag" :style="{ color: severityColorMap[problem.severity] }">
严重程度: {{ severityMap[problem.severity] }}
</span>
</div>
<!-- 基本信息 -->
<van-cell-group title="基本信息" class="info-group">
<van-cell title="问题编号" :value="`PSH-PROB-${problem.id}`" />
<van-cell title="问题标题" :value="problem.title" />
<van-cell title="所属排水户" :value="problem.pshName" />
<van-cell title="上报人员" :value="problem.reporter" />
<van-cell title="上报时间" :value="problem.reportTime" />
<van-cell title="问题地址" :value="problem.address" />
<van-cell title="责任人" :value="problem.assignee" />
</van-cell-group>
<!-- 问题描述 -->
<van-cell-group title="问题描述" class="info-group">
<van-cell>
<p class="desc-text">{{ problem.description }}</p>
</van-cell>
</van-cell-group>
<!-- 时间信息 -->
<van-cell-group title="时间信息" class="info-group">
<van-cell title="创建时间" :value="problem.createTime" />
<van-cell title="更新时间" :value="problem.updateTime" />
</van-cell-group>
<!-- 操作按钮 -->
<div class="action-buttons">
<van-button type="primary" block round @click="acceptProblem">接单处理</van-button>
<van-button plain block round class="mt-sm" @click="transferProblem">转派他人</van-button>
<van-button type="success" block round class="mt-sm" @click="resolveProblem">标记已解决</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;
}
}
.status-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-bg-card);
}
.severity-tag {
font-size: 13px;
font-weight: 500;
}
.info-group {
margin-top: 8px;
}
.desc-text {
font-size: 14px;
line-height: 1.6;
color: var(--color-text-regular);
padding: 4px 0;
}
.action-buttons {
padding: 24px 16px;
.mt-sm {
margin-top: 12px;
}
}
</style>

View File

@@ -0,0 +1,155 @@
<script setup lang="ts">
/**
* 排水户问题列表页
*
* 展示排水户相关问题列表,支持搜索和状态筛选。
*/
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const pshId = (route.params.id || route.query.id) as string
/** 搜索关键词 */
const searchText = ref('')
/** 当前激活的 Tab */
const activeTab = ref(0)
/** 模拟问题数据 */
const mockProblems = [
{ id: 1, pshName: '华丰食品厂', title: '排水管道堵塞', severity: 'high', status: 'pending', reportTime: '2025-06-15 09:30', reporter: '张工' },
{ id: 2, pshName: '阳光花园小区', title: '化粪池溢出', severity: 'medium', status: 'processing', reportTime: '2025-06-15 11:00', reporter: '李工' },
{ id: 3, pshName: '万达商业广场', title: '油脂分离器故障', severity: 'low', status: 'resolved', reportTime: '2025-06-14 08:00', reporter: '王工' },
{ id: 4, pshName: '东风化工厂', title: '污水超标排放', severity: 'critical', status: 'pending', reportTime: '2025-06-16 10:00', reporter: '赵工' },
{ id: 5, pshName: '翠湖小区', title: '雨污混接', severity: 'medium', status: 'processing', reportTime: '2025-06-15 14:30', reporter: '孙工' },
]
/** 状态映射 */
const statusMap: Record<string, string> = {
pending: '待处理',
processing: '处理中',
resolved: '已解决',
}
const statusColorMap: Record<string, 'primary' | 'success' | 'warning' | 'danger'> = {
pending: 'warning',
processing: 'primary',
resolved: 'success',
}
/** 严重程度映射 */
const severityMap: Record<string, string> = {
low: '低',
medium: '中',
high: '高',
critical: '紧急',
}
const severityColorMap: Record<string, string> = {
low: '#07c160',
medium: '#ff976a',
high: '#ee0a24',
critical: '#ee0a24',
}
/** 筛选后的列表 */
const filteredProblems = computed(() => {
let list = mockProblems
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(p =>
p.title.toLowerCase().includes(kw) ||
p.pshName.toLowerCase().includes(kw) ||
p.reporter.toLowerCase().includes(kw)
)
}
if (activeTab.value === 1) list = list.filter(p => p.status === 'pending')
else if (activeTab.value === 2) list = list.filter(p => p.status === 'processing')
else if (activeTab.value === 3) list = list.filter(p => p.status === 'resolved')
return list
})
/** 跳转详情 */
function goDetail(id: number) {
router.push(`/pshProblemDetail/${id}`)
}
</script>
<template>
<div class="problem-page">
<van-nav-bar title="排水户问题列表" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索问题标题、排水户、上报人" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="待处理" />
<van-tab title="处理中" />
<van-tab title="已解决" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredProblems.length === 0" description="暂无问题" />
<van-card
v-for="item in filteredProblems"
:key="item.id"
:title="item.title"
:desc="`排水户: ${item.pshName}`"
@click="goDetail(item.id)"
>
<template #tags>
<van-tag :type="statusColorMap[item.status]" size="medium">
{{ statusMap[item.status] }}
</van-tag>
<van-tag :color="severityColorMap[item.severity]" size="medium" text-color="#fff" plain>
{{ severityMap[item.severity] }}
</van-tag>
</template>
<template #footer>
<div class="card-meta">
<span>上报人: {{ item.reporter }}</span>
<span>{{ item.reportTime }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.problem-page {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
:deep(.van-tag) {
margin-right: 4px;
}
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,161 @@
<script setup lang="ts">
/**
* 排水户任务管理页
*
* 展示排水户相关任务列表,支持搜索和状态筛选。
*/
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const pshId = (route.params.detail || route.query.id) as string
/** 搜索关键词 */
const searchText = ref('')
/** 当前激活的 Tab */
const activeTab = ref(0)
/** 模拟任务数据 */
const mockTasks = [
{ id: 1, pshName: '华丰食品厂', title: '排水管道清淤', type: 'clean', status: 'pending', assignee: '李师傅', deadline: '2025-06-20', createTime: '2025-06-15' },
{ id: 2, pshName: '阳光花园小区', title: '化粪池清理', type: 'maintenance', status: 'doing', assignee: '王师傅', deadline: '2025-06-18', createTime: '2025-06-14' },
{ id: 3, pshName: '万达商业广场', title: '油脂分离器检修', type: 'repair', status: 'done', assignee: '赵师傅', deadline: '2025-06-16', createTime: '2025-06-13' },
{ id: 4, pshName: '东风化工厂', title: '污水取样检测', type: 'inspection', status: 'pending', assignee: '孙师傅', deadline: '2025-06-22', createTime: '2025-06-16' },
{ id: 5, pshName: '翠湖小区', title: '雨污分流改造', type: 'construction', status: 'doing', assignee: '周师傅', deadline: '2025-07-01', createTime: '2025-06-10' },
]
/** 状态映射 */
const statusMap: Record<string, string> = {
pending: '待执行',
doing: '执行中',
done: '已完成',
}
const statusColorMap: Record<string, 'primary' | 'success' | 'warning'> = {
pending: 'warning',
doing: 'primary',
done: 'success',
}
/** 任务类型映射 */
const typeMap: Record<string, string> = {
clean: '清淤',
maintenance: '养护',
repair: '维修',
inspection: '检查',
construction: '施工',
}
const typeColorMap: Record<string, string> = {
clean: '#1989fa',
maintenance: '#07c160',
repair: '#ff976a',
inspection: '#7232dd',
construction: '#ee0a24',
}
/** 筛选后的任务列表 */
const filteredTasks = computed(() => {
let list = mockTasks
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(t =>
t.title.toLowerCase().includes(kw) ||
t.pshName.toLowerCase().includes(kw) ||
t.assignee.toLowerCase().includes(kw)
)
}
if (activeTab.value === 1) list = list.filter(t => t.status === 'pending')
else if (activeTab.value === 2) list = list.filter(t => t.status === 'doing')
else if (activeTab.value === 3) list = list.filter(t => t.status === 'done')
return list
})
/** 跳转任务详情 */
function goTaskDetail(id: number) {
router.push(`/pshTaskList/${id}`)
}
</script>
<template>
<div class="task-page">
<van-nav-bar title="排水户任务管理" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索任务、排水户、负责人" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="待执行" />
<van-tab title="执行中" />
<van-tab title="已完成" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredTasks.length === 0" description="暂无任务" />
<van-card
v-for="task in filteredTasks"
:key="task.id"
:title="task.title"
:desc="`排水户: ${task.pshName}`"
@click="goTaskDetail(task.id)"
>
<template #tags>
<van-tag :color="typeColorMap[task.type]" size="medium" text-color="#fff">
{{ typeMap[task.type] }}
</van-tag>
<van-tag :type="statusColorMap[task.status]" size="medium">
{{ statusMap[task.status] }}
</van-tag>
</template>
<template #footer>
<div class="card-meta">
<span>负责人: {{ task.assignee }}</span>
</div>
<div class="card-meta">
<span>截止: {{ task.deadline }}</span>
<span>创建: {{ task.createTime }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.task-page {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
:deep(.van-tag) {
margin-right: 4px;
}
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
</style>

View File

@@ -0,0 +1,131 @@
<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 drainUser = ref({
id: detailId || '1',
name: '华丰食品厂',
type: 'industrial',
typeLabel: '工业',
address: '城北工业园区A1栋',
contact: '张经理',
phone: '13800001011',
drainNo: 'PSH-2025-001',
status: 'normal',
statusLabel: '正常',
licenseNo: '排水许可证-2025-001',
licenseDate: '2025-01-15',
licenseExpire: '2030-01-14',
dischargeVolume: '500吨/日',
pipeMaterial: 'PE管',
pipeDiameter: 'DN300',
monitorPoint: '2个监测点',
remark: '该排水户已安装在线监测设备,运行正常。',
})
/** 类型颜色 */
const typeColorMap: Record<string, string> = {
industrial: '#1989fa',
domestic: '#07c160',
commercial: '#ff976a',
}
const statusColorMap: Record<string, 'success' | 'warning' | 'danger'> = {
normal: 'success',
warning: 'warning',
danger: 'danger',
}
</script>
<template>
<div class="detail-page">
<van-nav-bar title="排水户详情" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
<!-- 状态栏 -->
<div class="status-bar">
<van-tag :color="typeColorMap[drainUser.type]" size="large" text-color="#fff">
{{ drainUser.typeLabel }}
</van-tag>
<van-tag :type="statusColorMap[drainUser.status]" size="large">
{{ drainUser.statusLabel }}
</van-tag>
</div>
<!-- 基本信息 -->
<van-cell-group title="基本信息" class="info-group">
<van-cell title="排水户名称" :value="drainUser.name" />
<van-cell title="排水户编号" :value="drainUser.drainNo" />
<van-cell title="地址" :value="drainUser.address" />
<van-cell title="联系人" :value="drainUser.contact" />
<van-cell title="联系电话" :value="drainUser.phone" />
</van-cell-group>
<!-- 排水许可 -->
<van-cell-group title="排水许可" class="info-group">
<van-cell title="许可证编号" :value="drainUser.licenseNo" />
<van-cell title="发证日期" :value="drainUser.licenseDate" />
<van-cell title="有效期至" :value="drainUser.licenseExpire" />
</van-cell-group>
<!-- 排放信息 -->
<van-cell-group title="排放信息" class="info-group">
<van-cell title="排放量" :value="drainUser.dischargeVolume" />
<van-cell title="管道材质" :value="drainUser.pipeMaterial" />
<van-cell title="管径" :value="drainUser.pipeDiameter" />
<van-cell title="监测点" :value="drainUser.monitorPoint" />
</van-cell-group>
<!-- 备注 -->
<van-cell-group title="备注信息" class="info-group">
<van-cell>
<p class="desc-text">{{ drainUser.remark }}</p>
</van-cell>
</van-cell-group>
</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;
}
}
.status-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-bg-card);
}
.info-group {
margin-top: 8px;
}
.desc-text {
font-size: 14px;
line-height: 1.6;
color: var(--color-text-regular);
padding: 4px 0;
}
</style>

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
/**
* 工程项目详情
*
* 展示工程项目的详细信息,包括基本信息、
* 施工进度、现场照片等。
*/
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
/** 模拟项目详情 */
const project = {
id: route.params.id,
name: '城北雨水管网改造工程',
no: 'XM-2025-001',
company: '中建三局',
manager: '赵工',
phone: '13800001010',
progress: 75,
status: 'building',
startDate: '2025-03-01',
endDate: '2025-09-30',
budget: '1250万元',
address: '城北工业园区大道路段',
description: '该项目对城北工业园区范围内雨水管网进行全面改造升级,包括主管网更换、检查井增设、雨水口改造等内容。改造完成后将显著提升区域排水能力。',
}
/** 模拟现场照片 */
const photos = ref([
{ url: 'https://img.yzcdn.cn/vant/cat.jpeg' },
{ url: 'https://img.yzcdn.cn/vant/apple-1.jpg' },
{ url: 'https://img.yzcdn.cn/vant/apple-2.jpg' },
{ url: 'https://img.yzcdn.cn/vant/apple-3.jpg' },
])
function progressColor(pct: number): string {
if (pct >= 80) return '#07c160'
if (pct >= 40) return '#1989fa'
return '#ff976a'
}
const statusMap: Record<string, string> = {
building: '在建',
paused: '暂停',
completed: '竣工',
}
</script>
<template>
<div class="page-container">
<van-nav-bar
title="项目详情"
left-arrow
fixed
placeholder
@click-left="router.back()"
/>
<van-cell-group inset>
<van-cell title="项目名称" :value="project.name" />
<van-cell title="项目编号" :value="project.no" />
<van-cell title="施工单位" :value="project.company" />
<van-cell title="项目负责人" :value="project.manager" />
<van-cell title="联系电话" :value="project.phone" />
<van-cell title="项目状态">
<template #value>
<van-tag type="primary" size="medium">{{ statusMap[project.status] }}</van-tag>
</template>
</van-cell>
<van-cell title="施工地址" :value="project.address" />
<van-cell title="开工日期" :value="project.startDate" />
<van-cell title="计划竣工" :value="project.endDate" />
<van-cell title="项目预算" :value="project.budget" />
</van-cell-group>
<van-cell-group inset style="margin-top: 12px">
<van-cell title="施工进度" />
<div class="progress-section">
<van-progress
:percentage="project.progress"
:color="progressColor(project.progress)"
:pivot-text="`${project.progress}%`"
stroke-width="12"
/>
</div>
</van-cell-group>
<van-cell-group inset style="margin-top: 12px">
<van-cell title="项目概述" />
<div class="content-block">{{ project.description }}</div>
</van-cell-group>
<div class="photo-section" v-if="photos.length > 0">
<div class="section-title">现场照片</div>
<div class="photo-grid">
<van-image
v-for="(photo, idx) in photos"
:key="idx"
:src="photo.url"
width="100%"
height="100"
fit="cover"
radius="8"
lazy-load
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
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;
}
}
.progress-section {
padding: 12px 16px;
background: var(--color-bg-card);
}
.content-block {
padding: 12px 16px;
font-size: 14px;
line-height: 1.8;
color: var(--color-text-regular);
background: var(--color-bg-card);
white-space: pre-wrap;
}
.photo-section {
margin-top: 12px;
padding: 12px 16px;
}
.section-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 10px;
color: var(--color-text-primary);
}
.photo-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
</style>

View File

@@ -0,0 +1,148 @@
<script setup lang="ts">
/**
* 工程项目列表
*
* 展示在建工程项目列表,支持按状态筛选和搜索,
* 显示项目进度信息。
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const searchText = ref('')
const activeTab = ref(0)
/** 状态映射 */
const statusMap: Record<string, string> = {
building: '在建',
paused: '暂停',
completed: '竣工',
}
const statusColorMap: Record<string, 'primary' | 'warning' | 'success'> = {
building: 'primary',
paused: 'warning',
completed: 'success',
}
/** 进度颜色 */
function progressColor(pct: number): string {
if (pct >= 80) return '#07c160'
if (pct >= 40) return '#1989fa'
return '#ff976a'
}
/** 模拟项目数据 */
const mockProjects = [
{ id: 1, name: '城北雨水管网改造工程', no: 'XM-2025-001', company: '中建三局', progress: 75, status: 'building', manager: '赵工', startDate: '2025-03-01' },
{ id: 2, name: '东风路排水泵站新建项目', no: 'XM-2025-002', company: '水务工程公司', progress: 30, status: 'building', manager: '钱工', startDate: '2025-04-15' },
{ id: 3, name: '中山河河道整治工程', no: 'XM-2025-003', company: '市政建设集团', progress: 90, status: 'building', manager: '孙工', startDate: '2025-01-10' },
{ id: 4, name: '开发区污水管网铺设', no: 'XM-2025-004', company: '中交一公局', progress: 0, status: 'paused', manager: '李工', startDate: '2025-05-01' },
{ id: 5, name: '老城区雨污分流改造', no: 'XM-2025-005', company: '中铁十二局', progress: 100, status: 'completed', manager: '周工', startDate: '2024-09-01' },
]
const filteredList = computed(() => {
let list = mockProjects
if (searchText.value) {
const kw = searchText.value.toLowerCase()
list = list.filter(p =>
p.name.toLowerCase().includes(kw) ||
p.no.toLowerCase().includes(kw) ||
p.company.toLowerCase().includes(kw)
)
}
if (activeTab.value === 1) list = list.filter(p => p.status === 'building')
else if (activeTab.value === 2) list = list.filter(p => p.status === 'paused')
else if (activeTab.value === 3) list = list.filter(p => p.status === 'completed')
return list
})
function goDetail(id: number) {
router.push(`/constructionDetail/${id}`)
}
</script>
<template>
<div class="page-container">
<van-nav-bar title="工程项目" left-arrow fixed placeholder @click-left="router.back()" />
<van-search v-model="searchText" placeholder="搜索项目名称、编号、单位" shape="round" />
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="全部" />
<van-tab title="在建" />
<van-tab title="暂停" />
<van-tab title="竣工" />
</van-tabs>
<div class="card-list">
<van-empty v-if="filteredList.length === 0" description="暂无项目" />
<van-card
v-for="item in filteredList"
:key="item.id"
:title="item.name"
:desc="`施工单位: ${item.company}`"
@click="goDetail(item.id)"
>
<template #tags>
<van-tag :type="statusColorMap[item.status]" size="medium">
{{ statusMap[item.status] }}
</van-tag>
</template>
<template #footer>
<div class="progress-wrap">
<van-progress
:percentage="item.progress"
:color="progressColor(item.progress)"
:pivot-text="`${item.progress}%`"
/>
</div>
<div class="card-meta">
<span>{{ item.no }}</span>
<span>负责人: {{ item.manager }}</span>
</div>
</template>
</van-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: var(--color-bg-page);
: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;
}
}
.card-list {
padding: 0 8px;
:deep(.van-card) {
margin: 8px;
border-radius: 10px;
background: var(--color-bg-card);
}
:deep(.van-tag) {
margin-right: 4px;
}
}
.progress-wrap {
margin: 8px 0;
}
.card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
}
</style>