Răsfoiți Sursa

巡检计划新建

BAI 6 zile în urmă
părinte
comite
1cdcb694b3
5 a modificat fișierele cu 621 adăugiri și 111 ștergeri
  1. 65 0
      package-lock.json
  2. 2 0
      package.json
  3. 4 0
      src/main.js
  4. 351 0
      src/views/admin/PatrolPlanCreateView.vue
  5. 199 111
      src/views/admin/PatrolPlanView.vue

+ 65 - 0
package-lock.json

@@ -9,6 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@antv/l7": "^2.28.14",
+        "@element-plus/icons-vue": "^2.3.2",
         "autofit.js": "^3.2.8",
         "axios": "^1.13.6",
         "cesium": "^1.139.1",
@@ -19,6 +20,7 @@
         "vue-router": "^5.0.3"
       },
       "devDependencies": {
+        "@playwright/test": "^1.60.0",
         "@vitejs/plugin-vue": "^6.0.2",
         "vite": "^7.3.1",
         "vite-plugin-cesium": "^1.2.23"
@@ -1061,6 +1063,22 @@
         "gl-style-validate": "dist/gl-style-validate.mjs"
       }
     },
+    "node_modules/@playwright/test": {
+      "version": "1.60.0",
+      "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.60.0.tgz",
+      "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "playwright": "1.60.0"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/@popperjs/core": {
       "name": "@sxzz/popperjs-es",
       "version": "2.11.8",
@@ -3437,6 +3455,53 @@
         "pathe": "^2.0.3"
       }
     },
+    "node_modules/playwright": {
+      "version": "1.60.0",
+      "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz",
+      "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "playwright-core": "1.60.0"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.2"
+      }
+    },
+    "node_modules/playwright-core": {
+      "version": "1.60.0",
+      "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz",
+      "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/playwright/node_modules/fsevents": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
     "node_modules/pmtiles": {
       "version": "2.11.0",
       "resolved": "https://registry.npmmirror.com/pmtiles/-/pmtiles-2.11.0.tgz",

+ 2 - 0
package.json

@@ -10,6 +10,7 @@
   },
   "dependencies": {
     "@antv/l7": "^2.28.14",
+    "@element-plus/icons-vue": "^2.3.2",
     "autofit.js": "^3.2.8",
     "axios": "^1.13.6",
     "cesium": "^1.139.1",
@@ -20,6 +21,7 @@
     "vue-router": "^5.0.3"
   },
   "devDependencies": {
+    "@playwright/test": "^1.60.0",
     "@vitejs/plugin-vue": "^6.0.2",
     "vite": "^7.3.1",
     "vite-plugin-cesium": "^1.2.23"

+ 4 - 0
src/main.js

@@ -5,11 +5,15 @@ import App from './App.vue'
 import router from './router'
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 import autofit from 'autofit.js'
 
 const app = createApp(App)
 app.use(router)
 app.use(ElementPlus)
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
 app.mount('#app')
 
 // 初始化autofit.js

+ 351 - 0
src/views/admin/PatrolPlanCreateView.vue

@@ -0,0 +1,351 @@
+<!-- 新建巡检计划 - 全页面布局 -->
+<template>
+  <div class="plan-create-container">
+    <!-- 顶部导航栏 -->
+    <div class="create-topbar">
+      <el-button text @click="$emit('cancel')" class="back-btn">
+        <el-icon><ArrowLeft /></el-icon> 返回计划列表
+      </el-button>
+      <span class="topbar-title">新建巡检计划</span>
+      <div class="topbar-actions">
+        <el-button @click="$emit('cancel')">取消</el-button>
+        <el-button type="primary" @click="submitForm" :loading="submitting" size="large">
+          创建计划
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 主体:左侧表单 + 右侧时间轴 -->
+    <div class="create-body">
+      <!-- 左侧:基本信息 -->
+      <div class="create-left">
+        <div class="left-card">
+          <div class="card-title">
+            <el-icon><Document /></el-icon> 基本信息
+          </div>
+          <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" size="large">
+            <el-form-item label="计划名称" prop="name">
+              <el-input v-model="form.name" placeholder="例如:大坝日常巡检-2026年7月" maxlength="50" clearable />
+            </el-form-item>
+
+            <el-form-item label="计划类型" prop="type">
+              <el-select v-model="form.type" placeholder="请选择" style="width:100%" clearable>
+                <el-option label="日常巡检" value="日常巡检" />
+                <el-option label="定期巡检" value="定期巡检" />
+                <el-option label="专项巡检" value="专项巡检" />
+                <el-option label="汛期巡检" value="汛期巡检" />
+              </el-select>
+            </el-form-item>
+
+            <el-form-item label="巡检路线" prop="route">
+              <el-input v-model="form.route" placeholder="例如:大坝全线巡检" clearable />
+            </el-form-item>
+
+            <el-row :gutter="12">
+              <el-col :span="12">
+                <el-form-item label="巡检频次" prop="frequency">
+                  <el-input v-model="form.frequency" placeholder="如:每日1次" clearable />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="责任人" prop="responsible">
+                  <el-input v-model="form.responsible" placeholder="姓名" clearable />
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <el-form-item label="计划周期" prop="dateRange">
+              <el-date-picker
+                v-model="form.dateRange"
+                type="daterange"
+                range-separator="至"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                style="width:100%"
+                value-format="YYYY-MM-DD"
+                clearable
+              />
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+
+      <!-- 右侧:巡检时间轴节点 -->
+      <div class="create-right">
+        <div class="right-card">
+          <div class="card-title">
+            <el-icon><Timer /></el-icon> 巡检时间轴节点
+            <span class="card-tip">按巡检顺序设置各节点信息</span>
+          </div>
+
+          <div class="timeline-full-scroll">
+            <el-timeline>
+              <el-timeline-item
+                v-for="(cp, idx) in form.checkpoints"
+                :key="idx"
+                :timestamp="cp.time || '设置时间'"
+                placement="top"
+                size="large"
+                :color="cp.time ? '#409eff' : '#dcdfe6'"
+              >
+                <div class="tn-card">
+                  <div class="tn-card-header">
+                    <span class="tn-badge">#{{ idx + 1 }}</span>
+                  </div>
+                  <div class="tn-card-body">
+                    <div class="tn-row">
+                      <el-input v-model="cp.name" placeholder="节点名称(如:坝顶起点 K0+000)" clearable />
+                    </div>
+                    <div class="tn-row">
+                      <el-input v-model="cp.desc" placeholder="检查内容描述" :rows="2" type="textarea" clearable />
+                    </div>
+                    <div class="tn-row tn-time-row">
+                      <el-icon style="color:#409eff"><Clock /></el-icon>
+                      <el-time-select
+                        v-model="cp.time"
+                        start="06:00"
+                        step="00:15"
+                        end="22:00"
+                        placeholder="选择巡检时间"
+                        style="width:160px"
+                        clearable
+                      />
+                      <span v-if="cp.time" class="time-hint">预计{{ getTimeSlotHint(cp.time) }}进行</span>
+                      <el-button
+                        type="danger"
+                        :icon="Delete"
+                        size="small"
+                        text
+                        class="tn-delete-btn"
+                        @click="removeCheckpoint(idx)"
+                        :disabled="form.checkpoints.length <= 1"
+                      >
+                        删除
+                      </el-button>
+                    </div>
+                  </div>
+                </div>
+              </el-timeline-item>
+            </el-timeline>
+
+            <el-button type="primary" plain @click="addCheckpoint" class="add-node-btn" size="large">
+              <el-icon><Plus /></el-icon> 添加节点
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Delete, Plus, Document, Timer, Clock, ArrowLeft } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+export default {
+  name: 'PatrolPlanCreateView',
+  emits: ['created', 'cancel'],
+  data() {
+    return {
+      submitting: false,
+      form: this.getDefaultForm(),
+      rules: {
+        name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
+        type: [{ required: true, message: '请选择计划类型', trigger: 'change' }],
+        route: [{ required: true, message: '请输入巡检路线', trigger: 'blur' }],
+        frequency: [{ required: true, message: '请输入巡检频次', trigger: 'blur' }],
+        responsible: [{ required: true, message: '请输入责任人', trigger: 'blur' }],
+        dateRange: [{ required: true, message: '请选择计划周期', trigger: 'change' }]
+      }
+    }
+  },
+  methods: {
+    getDefaultForm() {
+      return {
+        name: '',
+        type: '',
+        route: '',
+        frequency: '',
+        responsible: '',
+        dateRange: [],
+        checkpoints: [
+          { name: '', desc: '', time: '' },
+          { name: '', desc: '', time: '' },
+          { name: '', desc: '', time: '' }
+        ]
+      }
+    },
+    getTimeSlotHint(time) {
+      if (!time) return ''
+      const h = parseInt(time.split(':')[0])
+      if (h < 9) return '上午早班'
+      if (h < 12) return '上午'
+      if (h < 14) return '午间'
+      if (h < 18) return '下午'
+      return '傍晚'
+    },
+    addCheckpoint() {
+      this.form.checkpoints.push({ name: '', desc: '', time: '' })
+    },
+    removeCheckpoint(idx) {
+      if (this.form.checkpoints.length <= 1) return
+      this.form.checkpoints.splice(idx, 1)
+    },
+    submitForm() {
+      this.$refs.formRef.validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const f = this.form
+        const validCheckpoints = f.checkpoints.filter(cp => cp.name.trim())
+        const newPlan = {
+          id: Date.now(),
+          name: f.name,
+          type: f.type,
+          route: f.route,
+          frequency: f.frequency,
+          responsible: f.responsible,
+          startDate: f.dateRange[0],
+          endDate: f.dateRange[1],
+          checkpointCount: validCheckpoints.length,
+          completionRate: 0,
+          status: 'pending',
+          statusText: '待执行',
+          checkpoints: validCheckpoints.map(cp => ({
+            name: cp.name,
+            desc: cp.desc || '常规检查',
+            time: cp.time || '08:00',
+            done: false
+          }))
+        }
+        setTimeout(() => {
+          this.submitting = false
+          this.$emit('created', newPlan)
+          ElMessage.success('巡检计划创建成功')
+        }, 300)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.plan-create-container {
+  width: 100%; height: 100%;
+  display: flex; flex-direction: column;
+  overflow: hidden;
+  font-family: 'Alibaba PuHuiTi', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+}
+
+/* ===== 顶部栏 ===== */
+.create-topbar {
+  flex-shrink: 0;
+  display: flex; align-items: center;
+  padding: 12px 24px;
+  background: #fff;
+  border-bottom: 1px solid #e2e8f0;
+  gap: 16px;
+}
+.back-btn { font-size: 14px; }
+.topbar-title {
+  flex: 1; font-size: 18px; font-weight: 700; color: #1e293b;
+  text-align: center;
+}
+.topbar-actions { display: flex; gap: 8px; }
+
+/* ===== 主体 ===== */
+.create-body {
+  flex: 1; min-height: 0;
+  display: flex; gap: 20px;
+  padding: 20px 24px;
+  background: #f1f5f9;
+  overflow: hidden;
+}
+
+/* ===== 左侧基本信息 ===== */
+.create-left {
+  width: 440px; flex-shrink: 0;
+}
+.left-card {
+  background: #fff;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.05);
+}
+.card-title {
+  font-size: 16px; font-weight: 600; color: #1e293b;
+  display: flex; align-items: center; gap: 8px;
+  margin-bottom: 24px; padding-bottom: 12px;
+  border-bottom: 1px solid #f1f5f9;
+}
+.card-tip {
+  font-size: 12px; font-weight: 400; color: #94a3b8; margin-left: auto;
+}
+
+/* ===== 右侧时间轴 ===== */
+.create-right {
+  flex: 1; min-width: 0;
+}
+.right-card {
+  background: #fff;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.05);
+  height: 100%;
+  display: flex; flex-direction: column;
+}
+
+/* 时间轴滚动区域 */
+.timeline-full-scroll {
+  flex: 1; overflow-y: auto;
+  padding-right: 8px;
+}
+.timeline-full-scroll::-webkit-scrollbar {
+  width: 6px;
+}
+.timeline-full-scroll::-webkit-scrollbar-thumb {
+  background: #cbd5e1;
+  border-radius: 3px;
+}
+
+/* 节点卡片 */
+.tn-card {
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+  border-radius: 10px;
+  padding: 14px 16px;
+  transition: border-color 0.2s;
+}
+.tn-card:hover {
+  border-color: #3b82f6;
+  box-shadow: 0 2px 8px rgba(59,130,246,0.08);
+}
+.tn-card-header {
+  display: flex; justify-content: space-between; align-items: center;
+  margin-bottom: 10px;
+}
+.tn-badge {
+  font-size: 12px; font-weight: 700; color: #3b82f6;
+  background: #e0e7ff; padding: 2px 12px; border-radius: 10px;
+}
+.tn-card-body {
+  display: flex; flex-direction: column; gap: 10px;
+}
+.tn-row { width: 100%; }
+.tn-time-row {
+  display: flex; align-items: center; gap: 8px;
+}
+.tn-delete-btn {
+  margin-left: auto;
+}
+.time-hint {
+  font-size: 12px; color: #94a3b8;
+}
+.add-node-btn {
+  margin-top: 16px; width: 100%;
+}
+
+/* Element Plus 时间轴间距修正 */
+:deep(.el-timeline) { padding-left: 8px; }
+:deep(.el-timeline-item__wrapper) { padding-left: 20px; }
+:deep(.el-timeline-item__timestamp) { font-size: 12px; color: #3b82f6; font-weight: 500; }
+</style>

+ 199 - 111
src/views/admin/PatrolPlanView.vue

@@ -1,6 +1,14 @@
 <!-- 巡检计划管理页面 - 卡片仪表盘风格 -->
 <template>
-  <div class="patrol-plan-container">
+  <!-- 创建模式 -->
+  <PatrolPlanCreateView
+    v-if="showCreate"
+    @created="onPlanCreated"
+    @cancel="showCreate = false"
+  />
+
+  <!-- 列表模式 -->
+  <div v-else class="patrol-plan-container">
     <!-- 顶部横幅统计 -->
     <section class="hero-stats">
       <div class="hero-main">
@@ -20,31 +28,39 @@
     <div class="main-area">
       <!-- 左侧:计划卡片网格 -->
       <div class="plan-grid">
-        <div class="plan-card" v-for="item in planList" :key="item.id"
-          :class="{ active: selectedPlan && selectedPlan.id === item.id }"
-          @click="selectPlan(item)">
+        <div
+          v-for="item in planList"
+          :key="item.id"
+          class="plan-card"
+          :class="{ active: selectedPlan?.id === item.id }"
+          @click="selectPlan(item)"
+        >
           <div class="pcard-header">
-            <el-tag :type="getPlanStatusType(item.status)" size="small" effect="light" round>{{ item.statusText }}</el-tag>
-            <el-tag :type="getTypeTag(item.type)" size="small" effect="plain" round>{{ item.type }}</el-tag>
+            <el-tag :type="getPlanStatusType(item.status)" size="small" effect="light" round>
+              {{ item.statusText }}
+            </el-tag>
+            <el-tag :type="getTypeTag(item.type)" size="small" effect="plain" round>
+              {{ item.type }}
+            </el-tag>
           </div>
           <div class="pcard-name">{{ item.name }}</div>
-          <div class="pcard-route">🗺️ {{ item.route }}</div>
+          <div class="pcard-route">
+            <el-icon style="margin-right:4px;vertical-align:-2px"><Location /></el-icon>
+            {{ item.route }}
+          </div>
           <div class="pcard-progress">
-            <div class="progress-bar">
-              <div class="progress-fill" :style="{ width: item.completionRate + '%', background: getProgressColor(item.completionRate) }"></div>
-            </div>
-            <span class="progress-text">{{ item.completionRate }}%</span>
+            <el-progress :percentage="item.completionRate" :color="getProgressColor" :stroke-width="6" />
           </div>
           <div class="pcard-footer">
-            <span>👤 {{ item.responsible }}</span>
-            <span>📍 {{ item.checkpointCount }} 节点</span>
-            <span>{{ item.frequency }}</span>
+            <span><el-icon><User /></el-icon> {{ item.responsible }}</span>
+            <span><el-icon><List /></el-icon> {{ item.checkpointCount }} 节点</span>
+            <span><el-icon><Clock /></el-icon> {{ item.frequency }}</span>
           </div>
         </div>
 
         <!-- 新建卡片 -->
         <div class="plan-card add-card" @click="handleAddPlan">
-          <div class="add-icon">+</div>
+          <el-icon class="add-icon"><Plus /></el-icon>
           <div class="add-text">新建巡检计划</div>
         </div>
       </div>
@@ -54,63 +70,91 @@
         <div class="dp-header">
           <div class="dp-title">{{ selectedPlan.name }}</div>
           <div class="dp-actions">
-            <el-button size="small" @click="handleEditPlan(selectedPlan)">编辑</el-button>
-            <el-button size="small" type="danger" plain @click="handleDeletePlan(selectedPlan)">删除</el-button>
+            <el-button size="small" @click="handleEditPlan(selectedPlan)">
+              <el-icon><Edit /></el-icon> 编辑
+            </el-button>
+            <el-button size="small" type="danger" plain @click="handleDeletePlan(selectedPlan)">
+              <el-icon><Delete /></el-icon> 删除
+            </el-button>
           </div>
         </div>
 
-        <div class="dp-info-grid">
-          <div class="dp-info-item">
-            <span class="dp-label">计划类型</span>
-            <span class="dp-value">{{ selectedPlan.type }}</span>
-          </div>
-          <div class="dp-info-item">
-            <span class="dp-label">巡检路线</span>
-            <span class="dp-value">{{ selectedPlan.route }}</span>
+        <!-- 计划概览卡片 -->
+        <div class="dp-overview">
+          <div class="dp-o-item">
+            <span class="dp-o-label">类型</span>
+            <el-tag :type="getTypeTag(selectedPlan.type)" size="small" effect="plain" round>
+              {{ selectedPlan.type }}
+            </el-tag>
           </div>
-          <div class="dp-info-item">
-            <span class="dp-label">巡检频次</span>
-            <span class="dp-value">{{ selectedPlan.frequency }}</span>
+          <div class="dp-o-item">
+            <span class="dp-o-label">状态</span>
+            <el-tag :type="getPlanStatusType(selectedPlan.status)" size="small" effect="light" round>
+              {{ selectedPlan.statusText }}
+            </el-tag>
           </div>
-          <div class="dp-info-item">
-            <span class="dp-label">检查节点</span>
-            <span class="dp-value">{{ selectedPlan.checkpointCount }} 个</span>
+          <div class="dp-o-item">
+            <span class="dp-o-label">完成率</span>
+            <el-progress :percentage="selectedPlan.completionRate" :color="getProgressColor" :stroke-width="6" width="120px" type="line" />
           </div>
+        </div>
+
+        <!-- 详细信息 -->
+        <div class="dp-info-grid">
           <div class="dp-info-item">
-            <span class="dp-label">责任人</span>
-            <span class="dp-value">{{ selectedPlan.responsible }}</span>
+            <el-icon><Location /></el-icon>
+            <div>
+              <div class="dp-label">巡检路线</div>
+              <div class="dp-value">{{ selectedPlan.route }}</div>
+            </div>
           </div>
           <div class="dp-info-item">
-            <span class="dp-label">计划周期</span>
-            <span class="dp-value">{{ selectedPlan.startDate }} ~ {{ selectedPlan.endDate }}</span>
+            <el-icon><Clock /></el-icon>
+            <div>
+              <div class="dp-label">巡检频次</div>
+              <div class="dp-value">{{ selectedPlan.frequency }}</div>
+            </div>
           </div>
           <div class="dp-info-item">
-            <span class="dp-label">完成率</span>
-            <span class="dp-value">{{ selectedPlan.completionRate }}%</span>
+            <el-icon><User /></el-icon>
+            <div>
+              <div class="dp-label">责任人</div>
+              <div class="dp-value">{{ selectedPlan.responsible }}</div>
+            </div>
           </div>
           <div class="dp-info-item">
-            <span class="dp-label">状态</span>
-            <el-tag :type="getPlanStatusType(selectedPlan.status)" size="small" effect="light" round>{{ selectedPlan.statusText }}</el-tag>
+            <el-icon><Calendar /></el-icon>
+            <div>
+              <div class="dp-label">计划周期</div>
+              <div class="dp-value">{{ selectedPlan.startDate }} ~ {{ selectedPlan.endDate }}</div>
+            </div>
           </div>
         </div>
 
-        <!-- 路线节点列表 -->
-        <div class="dp-section-title">巡检路线节点</div>
-        <div class="checkpoint-list">
-          <div class="checkpoint-item" v-for="(cp, idx) in checkpoints" :key="idx">
-            <div class="cp-index">{{ idx + 1 }}</div>
-            <div class="cp-info">
-              <div class="cp-name">{{ cp.name }}</div>
-              <div class="cp-desc">{{ cp.desc }}</div>
-            </div>
-            <el-tag :type="cp.done ? 'success' : 'info'" size="small" effect="light" round>
-              {{ cp.done ? '已完成' : '待巡检' }}
-            </el-tag>
-          </div>
+        <!-- 巡检时间轴 -->
+        <div class="dp-section-title">
+          <el-icon><Timer /></el-icon> 巡检时间轴
         </div>
+        <el-timeline>
+          <el-timeline-item
+            v-for="(cp, idx) in getPlanCheckpoints(selectedPlan)"
+            :key="idx"
+            :timestamp="cp.time || '--:--'"
+            placement="top"
+            size="large"
+            :color="cp.done ? '#67c23a' : '#409eff'"
+          >
+            <div class="dp-timeline-node">
+              <div class="dp-tn-name">{{ cp.name }}</div>
+              <div class="dp-tn-desc">{{ cp.desc }}</div>
+              <el-tag v-if="cp.done" type="success" size="small" effect="light" round>已完成</el-tag>
+              <el-tag v-else type="info" size="small" effect="light" round>待巡检</el-tag>
+            </div>
+          </el-timeline-item>
+        </el-timeline>
       </div>
       <div class="detail-panel empty" v-else>
-        <div class="empty-icon">👈</div>
+        <el-icon class="empty-icon"><ArrowLeft /></el-icon>
         <div class="empty-text">选择一个计划查看详情</div>
       </div>
     </div>
@@ -118,10 +162,16 @@
 </template>
 
 <script>
+import { Delete, Plus, Location, User, Clock, List, Document, Timer, Edit, Calendar, ArrowLeft } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import PatrolPlanCreateView from './PatrolPlanCreateView.vue'
+
 export default {
   name: 'PatrolPlanView',
+  components: { PatrolPlanCreateView },
   data() {
     return {
+      showCreate: false,
       selectedPlan: null,
       heroItems: [
         { title: '待执行', value: '6', color: '#f59e0b' },
@@ -129,21 +179,20 @@ export default {
         { title: '已完成', value: '12', color: '#22c55e' },
         { title: '已逾期', value: '3', color: '#ef4444' }
       ],
-      checkpoints: [
-        { name: '坝顶起点 K0+000', desc: '坝体外观、路面状况', done: true },
-        { name: '坝顶中部 K0+200', desc: '坝体裂缝、沉降检查', done: true },
-        { name: '溢洪道进口', desc: '水位、漂浮物、闸门状态', done: true },
-        { name: '坝体下游坡 K0+350', desc: '护坡完整性、渗水检查', done: true },
-        { name: '坝脚渗流监测点', desc: '渗流量、水质观测', done: false },
-        { name: '副坝 K0+050', desc: '副坝坝体完整性', done: false }
-      ],
       planList: [
-        { id: 1, name: '大坝日常巡检-2026年6月', type: '日常巡检', route: '大坝全线巡检', checkpointCount: 12, frequency: '每日1次', responsible: '张三', startDate: '2026-06-01', endDate: '2026-06-30', completionRate: 45, status: 'executing', statusText: '执行中' },
-        { id: 2, name: '汛前专项检查', type: '专项巡检', route: '溢洪道+泄洪闸', checkpointCount: 8, frequency: '一次性', responsible: '李四', startDate: '2026-05-15', endDate: '2026-05-20', completionRate: 100, status: 'completed', statusText: '已完成' },
-        { id: 3, name: '副坝渗流巡检', type: '定期巡检', route: '副坝渗流监测点', checkpointCount: 6, frequency: '每周2次', responsible: '王五', startDate: '2026-06-01', endDate: '2026-08-31', completionRate: 30, status: 'executing', statusText: '执行中' },
-        { id: 4, name: '库区岸线巡查', type: '定期巡检', route: '库区左右岸', checkpointCount: 10, frequency: '每周1次', responsible: '赵六', startDate: '2026-06-01', endDate: '2026-09-30', completionRate: 20, status: 'executing', statusText: '执行中' },
-        { id: 5, name: '输水设施检查', type: '定期巡检', route: '输水隧洞+渠道', checkpointCount: 7, frequency: '每月2次', responsible: '张三', startDate: '2026-06-01', endDate: '2026-12-31', completionRate: 0, status: 'pending', statusText: '待执行' },
-        { id: 6, name: '防汛物资盘点', type: '专项巡检', route: '防汛仓库', checkpointCount: 3, frequency: '一次性', responsible: '李四', startDate: '2026-05-01', endDate: '2026-05-05', completionRate: 100, status: 'completed', statusText: '已完成' }
+        { id: 1, name: '大坝日常巡检-2026年6月', type: '日常巡检', route: '大坝全线巡检', checkpointCount: 6, frequency: '每日1次', responsible: '张三', startDate: '2026-06-01', endDate: '2026-06-30', completionRate: 45, status: 'executing', statusText: '执行中', checkpoints: [
+          { name: '坝顶起点 K0+000', desc: '坝体外观、路面状况检查', time: '08:00', done: true },
+          { name: '坝顶中部 K0+200', desc: '坝体裂缝、沉降检查', time: '08:45', done: true },
+          { name: '溢洪道进口', desc: '水位、漂浮物、闸门状态', time: '09:30', done: true },
+          { name: '坝体下游坡 K0+350', desc: '护坡完整性、渗水检查', time: '10:15', done: true },
+          { name: '坝脚渗流监测点', desc: '渗流量、水质观测', time: '11:00', done: false },
+          { name: '副坝 K0+050', desc: '副坝坝体完整性检查', time: '14:00', done: false }
+        ]},
+        { id: 2, name: '汛前专项检查', type: '专项巡检', route: '溢洪道+泄洪闸', checkpointCount: 8, frequency: '一次性', responsible: '李四', startDate: '2026-05-15', endDate: '2026-05-20', completionRate: 100, status: 'completed', statusText: '已完成', checkpoints: [] },
+        { id: 3, name: '副坝渗流巡检', type: '定期巡检', route: '副坝渗流监测点', checkpointCount: 6, frequency: '每周2次', responsible: '王五', startDate: '2026-06-01', endDate: '2026-08-31', completionRate: 30, status: 'executing', statusText: '执行中', checkpoints: [] },
+        { id: 4, name: '库区岸线巡查', type: '定期巡检', route: '库区左右岸', checkpointCount: 10, frequency: '每周1次', responsible: '赵六', startDate: '2026-06-01', endDate: '2026-09-30', completionRate: 20, status: 'executing', statusText: '执行中', checkpoints: [] },
+        { id: 5, name: '输水设施检查', type: '定期巡检', route: '输水隧洞+渠道', checkpointCount: 7, frequency: '每月2次', responsible: '张三', startDate: '2026-06-01', endDate: '2026-12-31', completionRate: 0, status: 'pending', statusText: '待执行', checkpoints: [] },
+        { id: 6, name: '防汛物资盘点', type: '专项巡检', route: '防汛仓库', checkpointCount: 3, frequency: '一次性', responsible: '李四', startDate: '2026-05-01', endDate: '2026-05-05', completionRate: 100, status: 'completed', statusText: '已完成', checkpoints: [] }
       ]
     }
   },
@@ -155,14 +204,40 @@ export default {
       return { '日常巡检': '', '定期巡检': 'success', '专项巡检': 'warning', '汛期巡检': 'danger' }[type] || ''
     },
     getProgressColor(rate) {
-      if (rate >= 80) return 'linear-gradient(90deg, #22c55e, #16a34a)'
-      if (rate >= 50) return 'linear-gradient(90deg, #3b82f6, #2563eb)'
-      return 'linear-gradient(90deg, #f59e0b, #d97706)'
+      if (rate >= 80) return '#22c55e'
+      if (rate >= 50) return '#3b82f6'
+      return '#f59e0b'
+    },
+    getPlanCheckpoints(plan) {
+      return plan.checkpoints && plan.checkpoints.length ? plan.checkpoints : this.defaultCheckpoints
     },
     selectPlan(item) { this.selectedPlan = item },
-    handleAddPlan() { console.log('新建计划') },
+    handleAddPlan() { this.showCreate = true },
     handleEditPlan(row) { console.log('编辑:', row) },
-    handleDeletePlan(row) { console.log('删除:', row) }
+    handleDeletePlan(row) {
+      ElMessageBox.confirm(`确定删除计划「${row.name}」吗?删除后不可恢复。`, '确认删除', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        confirmButtonClass: 'el-button--danger'
+      }).then(() => {
+        this.planList = this.planList.filter(p => p.id !== row.id)
+        if (this.selectedPlan?.id === row.id) this.selectedPlan = null
+        ElMessage.success('计划已删除')
+      }).catch(() => {})
+    },
+    onPlanCreated(newPlan) {
+      this.planList.unshift(newPlan)
+      this.showCreate = false
+    }
+  },
+  computed: {
+    defaultCheckpoints() {
+      return [
+        { name: '起点', desc: '检查准备', time: '08:00', done: false },
+        { name: '终点', desc: '检查总结', time: '17:00', done: false }
+      ]
+    }
   }
 }
 </script>
@@ -176,7 +251,7 @@ export default {
   font-family: 'Alibaba PuHuiTi', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
 }
 
-/* 顶部横幅 */
+/* ========== 顶部横幅 ========== */
 .hero-stats {
   flex-shrink: 0;
   background: linear-gradient(135deg, #1e40af, #3b82f6);
@@ -194,10 +269,10 @@ export default {
 .hero-count { font-size: 24px; font-weight: 700; }
 .hero-name { font-size: 13px; opacity: 0.7; }
 
-/* 主体 */
+/* ========== 主体 ========== */
 .main-area { flex: 1; display: flex; gap: 16px; min-height: 0; }
 
-/* 左侧卡片网格 */
+/* ========== 左侧卡片网格 ========== */
 .plan-grid {
   flex: 1; overflow-y: auto;
   display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -217,33 +292,30 @@ export default {
 .plan-card.active { border-color: #3b82f6; box-shadow: 0 4px 16px rgba(59, 130, 246, 0.15); }
 .pcard-header { display: flex; gap: 8px; margin-bottom: 10px; }
 .pcard-name { font-size: 15px; font-weight: 600; color: #1e293b; margin-bottom: 6px; }
-.pcard-route { font-size: 12px; color: #64748b; margin-bottom: 12px; }
-.pcard-progress { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
-.progress-bar {
-  flex: 1; height: 6px; background: #f1f5f9; border-radius: 3px; overflow: hidden;
-}
-.progress-fill { height: 100%; border-radius: 3px; transition: width 0.3s ease; }
-.progress-text { font-size: 12px; font-weight: 600; color: #3b82f6; min-width: 36px; }
+.pcard-route { font-size: 12px; color: #64748b; margin-bottom: 12px; display: flex; align-items: center; }
+.pcard-progress { margin-bottom: 12px; }
 .pcard-footer {
-  display: flex; gap: 12px; font-size: 11px; color: #94a3b8;
+  display: flex; gap: 16px; font-size: 12px; color: #94a3b8;
 }
+.pcard-footer .el-icon { vertical-align: -2px; margin-right: 2px; }
 
 /* 新建卡片 */
 .add-card {
   display: flex; flex-direction: column; align-items: center; justify-content: center;
-  min-height: 160px;
+  min-height: 180px;
   border: 2px dashed #cbd5e1;
   background: transparent;
   box-shadow: none;
+  gap: 8px;
 }
 .add-card:hover { border-color: #3b82f6; background: rgba(59, 130, 246, 0.03); transform: none; }
-.add-icon { font-size: 36px; color: #94a3b8; line-height: 1; }
-.add-text { font-size: 13px; color: #94a3b8; margin-top: 4px; }
+.add-icon { font-size: 40px; color: #94a3b8; }
+.add-text { font-size: 14px; color: #94a3b8; font-weight: 500; }
 
-/* 右侧详情面板 */
+/* ========== 右侧详情面板 ========== */
 .detail-panel {
-  width: 360px; flex-shrink: 0;
-  background: rgba(255, 255, 255, 0.85);
+  width: 400px; flex-shrink: 0;
+  background: rgba(255, 255, 255, 0.88);
   backdrop-filter: blur(20px);
   border-radius: 12px;
   border: 1px solid rgba(255, 255, 255, 0.8);
@@ -253,31 +325,47 @@ export default {
 }
 .detail-panel.empty {
   display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
+  color: #94a3b8;
+}
+.empty-icon { font-size: 40px; }
+.empty-text { font-size: 14px; }
+.dp-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; gap: 12px; }
+.dp-title { font-size: 17px; font-weight: 700; color: #1e293b; flex: 1; line-height: 1.4; }
+.dp-actions { display: flex; gap: 6px; flex-shrink: 0; }
+
+/* 详情概览卡片 */
+.dp-overview {
+  display: flex; gap: 12px; padding: 12px 16px;
+  background: #f8fafc; border-radius: 10px;
+  margin-bottom: 16px;
+}
+.dp-o-item {
+  flex: 1; display: flex; flex-direction: column; gap: 4px; align-items: center;
 }
-.empty-icon { font-size: 40px; opacity: 0.3; }
-.empty-text { font-size: 13px; color: #94a3b8; }
-.dp-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
-.dp-title { font-size: 17px; font-weight: 700; color: #1e293b; flex: 1; }
-.dp-actions { display: flex; gap: 6px; }
-.dp-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 20px; }
-.dp-info-item { display: flex; flex-direction: column; gap: 2px; }
+.dp-o-label { font-size: 11px; color: #94a3b8; }
+
+/* 详情的键值网格 */
+.dp-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 20px; }
+.dp-info-item {
+  display: flex; align-items: flex-start; gap: 8px;
+  padding: 8px; background: #f8fafc; border-radius: 8px;
+}
+.dp-info-item .el-icon { margin-top: 2px; color: #3b82f6; font-size: 16px; }
 .dp-label { font-size: 11px; color: #94a3b8; }
 .dp-value { font-size: 13px; color: #1e293b; font-weight: 500; }
-.dp-section-title { font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 12px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.06); }
 
-/* 节点列表 */
-.checkpoint-list { display: flex; flex-direction: column; gap: 10px; }
-.checkpoint-item {
-  display: flex; align-items: center; gap: 12px;
-  padding: 10px 12px; background: #f8fafc; border-radius: 8px;
-}
-.cp-index {
-  width: 24px; height: 24px; border-radius: 50%;
-  background: #e0e7ff; color: #4f46e5;
-  display: flex; align-items: center; justify-content: center;
-  font-size: 12px; font-weight: 700; flex-shrink: 0;
+.dp-section-title {
+  font-size: 14px; font-weight: 600; color: #1e293b;
+  margin-bottom: 12px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.06);
+  display: flex; align-items: center; gap: 6px;
 }
-.cp-info { flex: 1; }
-.cp-name { font-size: 13px; font-weight: 500; color: #1e293b; }
-.cp-desc { font-size: 11px; color: #94a3b8; }
+
+/* 详情时间轴节点 */
+.dp-timeline-node { }
+.dp-tn-name { font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 4px; }
+.dp-tn-desc { font-size: 12px; color: #64748b; margin-bottom: 6px; }
+
+/* Element Plus 时间轴间距修正 */
+.detail-panel :deep(.el-timeline) { padding-left: 4px; }
+.detail-panel :deep(.el-timeline-item__wrapper) { padding-left: 18px; }
 </style>