Selaa lähdekoodia

feat;添加我的方案页面和编辑、详情弹窗

wangsisi 1 viikko sitten
vanhempi
commit
7138d7326a

+ 46 - 106
src/views/planting/components/EditSchemeDialog.vue

@@ -1,12 +1,6 @@
 <template>
-  <el-dialog
-    v-model="visible"
-    title="编辑方案"
-    width="560px"
-    class="edit-scheme-dialog"
-    :close-on-click-modal="false"
-    @closed="onClosed"
-  >
+  <el-dialog v-model="visible" title="编辑方案" width="560px" class="edit-scheme-dialog" :close-on-click-modal="false"
+    @closed="onClosed" align-center append-to-body>
     <el-form ref="formRef" :model="form" :rules="rules" label-width="96px" class="edit-scheme-form">
       <el-form-item label="方案名称:" prop="name">
         <el-input v-model="form.name" placeholder="请输入方案名称" clearable />
@@ -16,22 +10,12 @@
         <div class="crop-selects">
           <el-form-item prop="category" class="crop-selects__item">
             <el-select v-model="form.category" placeholder="请选择品类" clearable>
-              <el-option
-                v-for="item in categoryOptions"
-                :key="item.value"
-                :label="item.label"
-                :value="item.value"
-              />
+              <el-option v-for="item in categoryOptions" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
           </el-form-item>
           <el-form-item prop="variety" class="crop-selects__item">
             <el-select v-model="form.variety" placeholder="请选择品种" clearable>
-              <el-option
-                v-for="item in varietyOptions"
-                :key="item.value"
-                :label="item.label"
-                :value="item.value"
-              />
+              <el-option v-for="item in varietyOptions" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
           </el-form-item>
         </div>
@@ -40,36 +24,28 @@
       <el-form-item label="土壤类型:" prop="soilType">
         <div class="field-block">
           <el-radio-group v-model="form.soilType">
-            <el-radio
-              v-for="item in soilTypeOptions"
-              :key="item.value"
-              :label="item.value"
-            >
+            <el-radio v-for="item in soilTypeOptions" :key="item.value" :label="item.value">
               {{ item.label }}
             </el-radio>
           </el-radio-group>
-          <p class="field-hint">
+          <span class="field-hint">
             遥感监测到某某农场的土壤类型为
-            <em class="field-hint__value">{{ soilTypeLabel }}</em>
-          </p>
+            <span class="field-hint__value">{{ soilTypeLabel }}</span>
+          </span>
         </div>
       </el-form-item>
 
       <el-form-item label="ph值:" prop="phValue">
         <div class="field-block">
           <el-radio-group v-model="form.phValue">
-            <el-radio
-              v-for="item in phOptions"
-              :key="item.value"
-              :label="item.value"
-            >
+            <el-radio v-for="item in phOptions" :key="item.value" :label="item.value">
               {{ item.label }}
             </el-radio>
           </el-radio-group>
-          <p class="field-hint">
+          <span class="field-hint">
             遥感监测到某某农场的土壤ph值为
-            <em class="field-hint__value">{{ phValueLabel }}</em>
-          </p>
+            <span class="field-hint__value">{{ phValueLabel }}</span>
+          </span>
         </div>
       </el-form-item>
     </el-form>
@@ -181,87 +157,51 @@ const onConfirm = async () => {
 </script>
 
 <style lang="scss" scoped>
-.edit-scheme-form {
-  padding: 8px 12px 0;
-
-  :deep(.el-form-item) {
-    margin-bottom: 22px;
-  }
+.edit-scheme-dialog {
+  .edit-scheme-form {
+    // 抵消 .el-dialog 默认 padding(16px),让 border 通栏
+    margin: 0 -16px;
+    padding: 10px 16px 0;
+    border-top: 1px solid #e5e6eb;
+    box-sizing: border-box;
+
+    :deep(.el-form-item__label) {
+      color: #1d2129;
+    }
 
-  :deep(.el-form-item__label) {
-    color: rgba(0, 0, 0, 0.65);
-  }
+    .crop-selects {
+      display: flex;
+      gap: 10px;
+      width: 100%;
 
-  .crop-selects {
-    display: flex;
-    gap: 12px;
-    width: 100%;
+      &__item {
+        flex: 1;
+      }
+    }
 
-    &__item {
-      flex: 1;
-      margin-bottom: 0;
+    .field-block {
+      display: flex;
+      flex-direction: column;
+    }
 
-      :deep(.el-form-item__content) {
-        margin-left: 0 !important;
-      }
+    .field-hint {
+      font-size: 13px;
+      color: #86909c;
 
-      .el-select {
-        width: 100%;
+      &__value {
+        color: $warning-color;
       }
     }
   }
 
-  .field-block {
+  .dialog-footer {
     display: flex;
-    flex-direction: column;
+    justify-content: center;
     gap: 8px;
-  }
-
-  .field-hint {
-    margin: 0;
-    font-size: 13px;
-    line-height: 1.5;
-    color: rgba(0, 0, 0, 0.45);
-
-    &__value {
-      color: $warning-color;
-      font-style: normal;
-    }
-  }
-}
-
-.dialog-footer {
-  display: flex;
-  justify-content: center;
-  gap: 12px;
-}
-</style>
-
-<style lang="scss">
-.edit-scheme-dialog {
-  .el-dialog__header {
-    margin-right: 0;
-    padding: 16px 20px;
-    border-bottom: 1px solid #e7e7e7;
-  }
-
-  .el-dialog__title {
-    font-size: 16px;
-    font-weight: 500;
-    color: #303133;
-  }
-
-  .el-dialog__body {
-    padding: 20px 24px 8px;
-  }
-
-  .el-dialog__footer {
-    padding: 16px 20px 20px;
-    border-top: 1px solid #e7e7e7;
-  }
-
-  .el-radio {
-    --el-radio-text-color: rgba(0, 0, 0, 0.65);
+    margin: 0 -16px;
+    padding: 12px 16px 0;
+    border-top: 1px solid #e5e6eb;
+    box-sizing: border-box;
   }
 }
 </style>

+ 234 - 0
src/views/planting/components/FarmWorkDetailDialog.vue

@@ -0,0 +1,234 @@
+<template>
+  <el-dialog v-model="visible" :title="detail.name || '农事详情'" width="720px" class="farm-work-detail-dialog" align-center
+    append-to-body>
+    <div class="detail-body">
+      <div class="detail-row">
+        <span class="detail-row__label">农事类型:</span>
+        <div class="type-tags">
+          <span v-for="(item, idx) in detail.types" :key="idx" class="type-tag">{{ item }}</span>
+        </div>
+      </div>
+
+      <div class="detail-row">
+        <span class="detail-row__label">触发条件:</span>
+        <span class="detail-row__value">{{ detail.trigger }}</span>
+      </div>
+
+      <div class="detail-row detail-row--top">
+        <span class="detail-row__label">药肥处方:</span>
+        <div class="prescription-list">
+          <div v-for="(item, idx) in detail.prescriptions" :key="idx" class="prescription-card">
+            <div class="prescription-card__title">{{ item.category }}</div>
+            <div class="prescription-card__desc">{{ item.desc }}</div>
+            <div class="prescription-card__freq">{{ item.freq }}</div>
+          </div>
+        </div>
+      </div>
+
+      <div class="section-title">执行规程</div>
+
+      <div class="detail-row">
+        <span class="detail-row__label">执行窗口:</span>
+        <span class="detail-row__value">{{ detail.execute.window }}</span>
+      </div>
+
+      <div class="detail-row">
+        <span class="detail-row__label">执行设备:</span>
+        <div class="device-inline">
+          <div class="device-inline__item">
+            <span class="device-inline__label">主要设备</span>
+            <span
+              v-for="(item, idx) in detail.execute.primaryDevices"
+              :key="`p-${idx}`"
+              class="type-tag"
+            >{{ item }}</span>
+          </div>
+          <div class="device-inline__item">
+            <span class="device-inline__label">备用设备</span>
+            <span
+              v-for="(item, idx) in detail.execute.backupDevices"
+              :key="`b-${idx}`"
+              class="type-tag"
+            >{{ item }}</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="detail-row">
+        <span class="detail-row__label">注意事项:</span>
+        <span class="detail-row__value">{{ detail.execute.notes }}</span>
+      </div>
+
+      <div class="section-title">复核规程</div>
+
+      <div class="detail-row">
+        <span class="detail-row__label">复核时间:</span>
+        <span class="detail-row__value">
+          执行后 <span class="days">{{ detail.review.days }}</span> 天
+        </span>
+      </div>
+
+      <div class="detail-row">
+        <span class="detail-row__label">复核标准:</span>
+        <span class="detail-row__value">{{ detail.review.standard }}</span>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  data: { type: Object, default: null },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit("update:modelValue", val),
+});
+
+const detail = computed(() => {
+  const row = props.data || {};
+  return {
+    name: row.name || "播前基肥",
+    types: row.types || ["标准农事", "日常营养", "水肥类"],
+    trigger: row.trigger || "-",
+    prescriptions: row.prescription
+      ? [
+        {
+          category: row.prescription.category || "营养类",
+          desc: row.prescription.desc || "-",
+          freq: row.prescription.freq || "-",
+        },
+        {
+          category: row.prescription.category || "营养类",
+          desc: row.prescription.desc || "-",
+          freq: row.prescription.freq || "-",
+        },
+      ]
+      : [
+        { category: "营养类", desc: "具体处方具体处方具体处方", freq: "频次频次频次频次频次" },
+        { category: "营养类", desc: "具体处方具体处方具体处方", freq: "频次频次频次频次频次" },
+      ],
+    execute: {
+      window: row.execute?.window || "生育期起始-生育期结束",
+      primaryDevices: row.execute?.primaryDevices || ["农机设备一"],
+      backupDevices: row.execute?.backupDevices || ["农机设备一", "农机设备一"],
+      notes: row.execute?.notes || "-",
+    },
+    review: {
+      days: row.review?.days ?? 7,
+      standard: row.review?.standard || "-",
+    },
+  };
+});
+</script>
+
+<style lang="scss" scoped>
+.detail-body {
+  margin: 0 -16px;
+  padding: 20px 26px 20px;
+  border-top: 1px solid #e5e6eb;
+
+  .detail-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 16px;
+
+    &--top {
+      align-items: flex-start;
+    }
+
+    &__label {
+      color: #1D2129;
+    }
+
+    &__value {
+      flex: 1;
+      color: #000;
+    }
+
+    .type-tags {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 4px;
+    }
+    .type-tag {
+      display: inline-block;
+      padding: 3px 6px;
+      border-radius: 2px;
+      font-size: 12px;
+      color: #FB8E33;
+      background: rgba(251, 142, 51, 0.1);
+    }
+
+    .prescription-list {
+      flex: 1;
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 10px;
+
+      .prescription-card {
+        padding: 6px;
+        border: .5px solid #E3E3E3;
+        border-radius: 5px;
+
+        &__title {
+          display: inline-block;
+          margin-bottom: 5px;
+          padding: 2px 6px;
+          border-radius: 2px;
+          font-size: 12px;
+          color: #4C4C4C;
+          background: rgba(125, 125, 125, 0.1);
+        }
+
+        &__freq {
+          margin-top: 2px;
+          font-size: 12px;
+          color: #979797;
+        }
+      }
+    }
+
+    .device-inline {
+      flex: 1;
+      display: flex;
+      flex-wrap: wrap;
+      align-items: center;
+      gap: 20px;
+      min-width: 0;
+
+      &__item {
+        display: inline-flex;
+        flex-wrap: wrap;
+        align-items: center;
+        gap: 8px;
+      }
+
+      &__label {
+        color: #86909c;
+        font-size: 13px;
+        line-height: 1;
+      }
+    }
+
+    .days {
+      color: $warning-color;
+      font-weight: 500;
+    }
+  }
+
+  .section-title {
+    padding-top: 15px;
+    margin-bottom: 16px;
+    font-weight: 500;
+    color: $warning-color;
+  }
+}
+</style>

+ 434 - 0
src/views/planting/components/FarmWorkEditDialog.vue

@@ -0,0 +1,434 @@
+<template>
+  <el-dialog v-model="visible" :title="form.name || '编辑农事'" width="720px" class="farm-work-edit-dialog"
+    :close-on-click-modal="false" align-center append-to-body @closed="onClosed">
+    <el-form ref="formRef" :model="form" label-width="96px" class="farm-work-form">
+      <el-form-item label="农事类型:">
+        <div class="type-select">
+          <span v-for="item in form.types" :key="item" class="type-select__tag">{{ item }}</span>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="触发条件:">
+        <el-input v-model="form.trigger" placeholder="请输入触发条件" />
+      </el-form-item>
+
+      <el-form-item label="药肥处方:">
+        <div class="prescription-block">
+          <el-button class="add-btn" @click="addPrescription">+ 添加处方</el-button>
+          <div class="prescription-list">
+            <div v-for="(item, idx) in form.prescriptions" :key="idx" class="prescription-card">
+              <div class="prescription-card__head">
+                <el-input v-model="item.category" placeholder="请输入类型" />
+                <el-button link class="del-btn" @click="removePrescription(idx)">删除</el-button>
+              </div>
+              <el-input v-model="item.desc" placeholder="请输入具体处方" />
+              <el-input v-model="item.freq" placeholder="请输入使用频次" />
+            </div>
+          </div>
+        </div>
+      </el-form-item>
+
+      <div class="section-title">执行规程</div>
+
+      <el-form-item label="执行窗口:">
+        <div class="inline-fields">
+          <div class="prefix-select">
+            <span class="prefix-select__label">起始时期:</span>
+            <el-select
+              v-model="form.execute.startPeriod"
+              class="prefix-select__inner"
+              multiple
+              :multiple-limit="1"
+              placeholder="请选择"
+            >
+              <el-option
+                v-for="item in periodOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </div>
+          <div class="prefix-select">
+            <span class="prefix-select__label">截至时期:</span>
+            <el-select
+              v-model="form.execute.endPeriod"
+              class="prefix-select__inner"
+              multiple
+              :multiple-limit="1"
+              placeholder="请选择"
+            >
+              <el-option
+                v-for="item in periodOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </div>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="执行设备:">
+        <div class="inline-fields">
+          <div class="prefix-select">
+            <span class="prefix-select__label">主要设备:</span>
+            <el-select
+              v-model="form.execute.primaryDevices"
+              class="prefix-select__inner"
+              multiple
+              placeholder="请选择"
+            >
+              <el-option
+                v-for="item in deviceOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </div>
+          <div class="prefix-select">
+            <span class="prefix-select__label">备用设备:</span>
+            <el-select
+              v-model="form.execute.backupDevices"
+              class="prefix-select__inner"
+              multiple
+              placeholder="请选择"
+            >
+              <el-option
+                v-for="item in deviceOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </div>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="注意事项:">
+        <el-input v-model="form.execute.notes" placeholder="请输入注意事项" />
+      </el-form-item>
+
+      <div class="section-title">复核规程</div>
+
+      <el-form-item label="复核时间:">
+        <div class="review-time-field">
+          <span>执行后</span>
+          <el-select v-model="form.review.days" class="review-time-field__select">
+            <el-option v-for="n in 30" :key="n" :label="String(n)" :value="n" />
+          </el-select>
+          <span class="unit">天</span>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="复核标准:">
+        <el-input v-model="form.review.standard" placeholder="请输入复核标准" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="visible = false">取消</el-button>
+        <el-button type="warning" @click="onSubmit">提交</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, reactive, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  data: { type: Object, default: null },
+});
+
+const emit = defineEmits(["update:modelValue", "submit"]);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit("update:modelValue", val),
+});
+
+const formRef = ref(null);
+const periodOptions = ["生育期一", "生育期二", "生育期三", "生育起始", "生育结束"];
+const deviceOptions = ["农机设备一", "农机设备二", "设备一", "设备二"];
+
+const createDefaultForm = () => ({
+  name: "播前基肥",
+  types: ["标准农事", "日常营养", "水肥类"],
+  trigger: "",
+  prescriptions: [
+    { category: "营养类", desc: "具体处方", freq: "频次" },
+    { category: "", desc: "", freq: "" },
+  ],
+  execute: {
+    startPeriod: ["生育期一"],
+    endPeriod: ["生育期二"],
+    primaryDevices: ["农机设备一"],
+    backupDevices: ["农机设备一", "农机设备二"],
+    notes: "",
+  },
+  review: {
+    days: 7,
+    standard: "",
+  },
+});
+
+const form = reactive(createDefaultForm());
+
+const fillForm = (row) => {
+  Object.assign(form, createDefaultForm());
+  if (!row) return;
+  form.name = row.name || "播前基肥";
+  form.types = [...(row.types || ["标准农事", "日常营养", "水肥类"])];
+  form.trigger = row.trigger || "";
+  form.prescriptions = row.prescription
+    ? [
+      {
+        category: row.prescription.category || "",
+        desc: row.prescription.desc || "",
+        freq: row.prescription.freq || "",
+      },
+      { category: "", desc: "", freq: "" },
+    ]
+    : createDefaultForm().prescriptions;
+  form.execute.startPeriod = ["生育期一"];
+  form.execute.endPeriod = ["生育期二"];
+  form.execute.primaryDevices = [...(row.execute?.primaryDevices || ["农机设备一"])];
+  form.execute.backupDevices = [
+    ...(row.execute?.backupDevices || ["农机设备一", "农机设备二"]),
+  ];
+  form.execute.notes = row.execute?.notes || "";
+  form.review.days = row.review?.days || 7;
+  form.review.standard = row.review?.standard || "";
+};
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (val) fillForm(props.data);
+  }
+);
+
+const addPrescription = () => {
+  form.prescriptions.push({ category: "", desc: "", freq: "" });
+};
+
+const removePrescription = (idx) => {
+  if (form.prescriptions.length <= 1) {
+    ElMessage.warning("至少保留一条处方");
+    return;
+  }
+  form.prescriptions.splice(idx, 1);
+};
+
+const onClosed = () => {
+  Object.assign(form, createDefaultForm());
+};
+
+const onSubmit = () => {
+  emit("submit", JSON.parse(JSON.stringify(form)));
+  visible.value = false;
+  ElMessage.success("提交成功");
+};
+</script>
+
+<style lang="scss" scoped>
+.farm-work-form {
+  margin: 0 -16px;
+  padding: 20px 20px;
+  border-top: 1px solid #e5e6eb;
+
+  :deep(.el-form-item__label) {
+    color: #1d2129;
+  }
+
+  .type-select {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    &__tag {
+      padding: 0 6px;
+      border-radius: 2px;
+      font-size: 12px;
+      color: #FB8E33;
+      background: rgba(251, 142, 51, 0.1);
+      line-height: 24px;
+    }
+  }
+
+  .prescription-block {
+    width: 100%;
+  }
+
+  .add-btn {
+    color: $warning-color;
+    background: rgba($warning-color, 0.1);
+    border-color: transparent;
+    margin-bottom: 10px;
+    font-weight: 400;
+    font-size: 12px;
+
+    &:hover {
+      color: $warning-color;
+      background: rgba($warning-color, 0.16);
+      border-color: transparent;
+    }
+  }
+
+  .prescription-list {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 10px;
+
+    .prescription-card {
+      display: flex;
+      flex-direction: column;
+      gap: 5px;
+      padding: 6px;
+      border: 0.5px solid #E3E3E3;
+      border-radius: 5px;
+
+      &__head {
+        display: flex;
+        gap: 15px;
+        align-items: center;
+
+        .del-btn {
+          color: $warning-color;
+        }
+      }
+    }
+  }
+
+  .section-title {
+    padding-top: 10px;
+    margin-bottom: 16px;
+    padding-left: 12px;
+    font-weight: 500;
+    color: $warning-color;
+  }
+
+  .inline-fields {
+    display: flex;
+    align-items: flex-start;
+    gap: 12px;
+    width: 100%;
+  }
+
+  .prefix-select {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    align-items: flex-start;
+    gap: 4px;
+    min-height: 32px;
+    padding: 3px 8px 3px 12px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    background: #fff;
+    box-sizing: border-box;
+    transition: border-color 0.2s;
+
+    &:hover {
+      border-color: #c0c4cc;
+    }
+
+    &:focus-within {
+      border-color: $warning-color;
+    }
+
+    &__label {
+      flex-shrink: 0;
+      padding-top: 3px;
+      font-size: 13px;
+      line-height: 22px;
+      color: #86909c;
+      white-space: nowrap;
+    }
+
+    &__inner {
+      flex: 1;
+      min-width: 0;
+
+      :deep(.el-select__wrapper) {
+        min-height: 24px;
+        padding: 0;
+        background: transparent;
+        box-shadow: none !important;
+        gap: 4px;
+      }
+
+      :deep(.el-select__selection) {
+        flex-wrap: wrap;
+        gap: 4px;
+      }
+
+      :deep(.el-select__selected-item) {
+        max-width: 100%;
+      }
+
+      :deep(.el-select__input-wrapper) {
+        margin-left: 0;
+      }
+
+      :deep(.el-tag) {
+        max-width: 100%;
+        height: 22px;
+        margin: 0;
+        padding: 0 6px;
+        background: #f2f3f5;
+        border: none;
+        border-radius: 4px;
+        color: #4e5969;
+
+        .el-tag__close {
+          color: #86909c;
+
+          &:hover {
+            background: transparent;
+            color: #4e5969;
+          }
+        }
+      }
+
+      :deep(.el-select__caret) {
+        color: #86909c;
+      }
+
+      :deep(.el-select__suffix) {
+        align-self: flex-start;
+        height: 24px;
+      }
+    }
+  }
+
+  .review-time-field {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    &__select {
+      width: 80px;
+    }
+
+    .unit {
+      color: rgba(0, 0, 0, 0.2);
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+  gap: 8px;
+  margin: 0 -16px;
+  padding: 12px 16px 0;
+  border-top: 1px solid #e5e6eb;
+  box-sizing: border-box;
+}
+</style>

+ 390 - 0
src/views/planting/components/MySchemePanel.vue

@@ -0,0 +1,390 @@
+<template>
+  <div class="my-scheme">
+    <!-- 方案切换 -->
+    <div class="scheme-tabs">
+      <div v-for="item in schemeList" :key="item.id" class="scheme-tab"
+        :class="{ 'is-active': activeSchemeId === item.id }" @click="activeSchemeId = item.id">
+        <span class="scheme-tab__name">{{ item.name }}</span>
+        <el-icon class="scheme-tab__edit" @click.stop="onRenameScheme(item)">
+          <Edit />
+        </el-icon>
+      </div>
+    </div>
+
+    <!-- 表格 -->
+    <div class="table-wrap">
+      <el-table :data="tableData" border class="scheme-table" :header-cell-style="headerCellStyle"
+        :cell-style="{ verticalAlign: 'top' }">
+        <el-table-column prop="name" label="农事名称" min-width="110" fixed />
+
+        <el-table-column label="农事类型" min-width="120">
+          <template #default="{ row }">
+            <div class="type-tags">
+              <span v-for="(item, idx) in row.types" :key="idx" class="type-tag">{{ item }}</span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="trigger" label="触发条件" min-width="160" />
+
+        <el-table-column label="药肥处方" min-width="220">
+          <template #default="{ row }">
+            <div class="prescription">
+              <div class="prescription__title">{{ row.prescription.category }}</div>
+              <div class="prescription__desc">{{ row.prescription.desc }}</div>
+              <div class="prescription__freq">{{ row.prescription.freq }}</div>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="执行窗口" align="center">
+          <el-table-column label="执行窗口" min-width="140" align="center">
+            <template #default="{ row }">
+              <div class="execute-window">{{ row.execute.window }}</div>
+            </template>
+          </el-table-column>
+          <el-table-column label="执行设备" min-width="140">
+            <template #default="{ row }">
+              <div class="device-groups">
+                <div class="device-group">
+                  <div class="device-group__label">主要设备:</div>
+                  <div class="device-group__tags">
+                    <span
+                      v-for="(item, idx) in row.execute.primaryDevices"
+                      :key="`primary-${idx}`"
+                      class="type-tag"
+                    >{{ item }}</span>
+                  </div>
+                </div>
+                <div class="device-group">
+                  <div class="device-group__label">备用设备:</div>
+                  <div class="device-group__tags">
+                    <span
+                      v-for="(item, idx) in row.execute.backupDevices"
+                      :key="`backup-${idx}`"
+                      class="type-tag"
+                    >{{ item }}</span>
+                  </div>
+                </div>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="注意事项" min-width="160" prop="execute.notes" />
+        </el-table-column>
+
+        <el-table-column label="复核规程" align="center">
+          <el-table-column label="复核时间" min-width="120">
+            <template #default="{ row }">
+              <span class="review-time">
+                执行后 <em class="review-time__num">{{ row.review.days }}</em> 天
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="复核标准" prop="review.standard" min-width="160" />
+        </el-table-column>
+
+        <el-table-column label="操作" width="190" fixed="right">
+          <template #default="{ row }">
+            <div class="row-ops">
+              <el-button class="row-ops__btn" @click="onEditRow(row)">编辑</el-button>
+              <el-button class="row-ops__btn" @click="onDetailRow(row)">详情</el-button>
+              <el-button class="row-ops__btn row-ops__btn--danger" type="danger" @click="onDeleteRow(row)">
+                删除
+              </el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 底部操作 -->
+    <div class="page-footer">
+      <el-button type="warning" plain @click="onCopyNew">复制为新的方案</el-button>
+      <el-button type="warning" @click="onSave">保存方案</el-button>
+    </div>
+
+    <FarmWorkEditDialog
+      v-model="editVisible"
+      :data="currentRow"
+      @submit="onEditSubmit"
+    />
+    <FarmWorkDetailDialog v-model="detailVisible" :data="currentRow" />
+  </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { Edit } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FarmWorkEditDialog from "./FarmWorkEditDialog.vue";
+import FarmWorkDetailDialog from "./FarmWorkDetailDialog.vue";
+
+const emit = defineEmits(["edit-scheme"]);
+
+const headerCellStyle = {
+  background: "#F5F5F5",
+  color: "#535353",
+  fontWeight: 400,
+};
+
+const schemeList = ref([
+  { id: 1, name: "方案一" },
+  { id: 2, name: "方案二" },
+  { id: 3, name: "方案二" },
+  { id: 4, name: "方案二" },
+]);
+const activeSchemeId = ref(1);
+const editVisible = ref(false);
+const detailVisible = ref(false);
+const currentRow = ref(null);
+
+const placeholderTrigger =
+  "触发条件条件触发条件触发条件条件触发条件触发条件条件触发条件触发条件条件触发条件触发条件条件触发条件";
+const placeholderNotes =
+  "注意事项注意事项注意事项注意事项注意事项注意事项注意事项注意事项注意事项注意事项";
+const placeholderStandard =
+  "复核标准标准标准标准复核标准标准标准标准复核标准标准标准标准复核标准标准标准标准";
+
+const createRow = () => ({
+  name: "播前基肥",
+  types: ["标准农事", "日常营养", "水肥类"],
+  trigger: placeholderTrigger,
+  prescription: {
+    category: "营养类",
+    desc: "具体处方具体处方具体处方",
+    freq: "频次频次频次频次频次",
+  },
+  execute: {
+    window: "生育起始-生育结束",
+    primaryDevices: ["农机设备一"],
+    backupDevices: ["农机设备一", "农机设备一"],
+    notes: placeholderNotes,
+  },
+  review: {
+    days: 7,
+    standard: placeholderStandard,
+  },
+});
+
+const tableData = ref([createRow(), createRow(), createRow()]);
+
+const onRenameScheme = (item) => {
+  emit("edit-scheme", item);
+};
+
+const onEditRow = (row) => {
+  currentRow.value = row;
+  editVisible.value = true;
+};
+
+const onDetailRow = (row) => {
+  currentRow.value = row;
+  detailVisible.value = true;
+};
+
+const onEditSubmit = () => {
+  // 后续接入保存接口
+};
+
+const onDeleteRow = async (row) => {
+  try {
+    await ElMessageBox.confirm(`确认删除「${row.name}」?`, "提示", {
+      type: "warning",
+      confirmButtonText: "删除",
+      cancelButtonText: "取消",
+    });
+    ElMessage.success("已删除");
+  } catch {
+    // cancelled
+  }
+};
+
+const onCopyNew = () => {
+  ElMessage.success("已复制为新的方案");
+};
+
+const onSave = () => {
+  ElMessage.success("方案已保存");
+};
+</script>
+
+<style lang="scss" scoped>
+$primary: $warning-color;
+$primary-light: color.mix(#fff, $warning-color, 90%);
+$text: rgba(0, 0, 0, 0.6);
+
+.my-scheme {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+
+  .scheme-tabs {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    padding: 20px 0;
+
+    .scheme-tab {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding: 6px 10px;
+      border: 1px solid transparent;
+      border-radius: 3px;
+      background: #F3F3F3;
+      color: $text;
+      cursor: pointer;
+
+      &:hover {
+        border-color: $primary;
+        background: $primary-light;
+        color: $primary;
+      }
+
+      &.is-active {
+        border-color: $primary;
+        background: $primary-light;
+        color: $primary;
+      }
+
+      &__edit {
+        font-size: 14px;
+      }
+    }
+  }
+
+  .table-wrap {
+    flex: 1;
+    min-height: 0;
+    overflow: auto;
+
+    .scheme-table {
+      color: #000;
+
+      .type-tags {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 4px;
+      }
+
+      .type-tag {
+        padding: 1px 6px;
+        border-radius: 4px;
+        font-size: 12px;
+        color: #2199f8;
+        background: rgba(33, 153, 248, 0.1);
+      }
+
+      .prescription {
+        &__title {
+          display: inline-block;
+          width: fit-content;
+          color: #4c4c4c;
+          margin-bottom: 2px;
+          background: rgba(125, 125, 125, 0.1);
+          padding: 0 6px;
+          border-radius: 2px;
+        }
+
+        &__desc {
+          font-size: 13px;
+          line-height: 1.6;
+        }
+
+        &__freq {
+          font-size: 12px;
+          color: #86909c;
+          line-height: 1.5;
+        }
+      }
+
+      .device-groups {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+      }
+
+      .device-group {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 4px;
+
+        &__label {
+          font-size: 13px;
+          line-height: 1.4;
+          color: #86909c;
+        }
+
+        &__tags {
+          display: flex;
+          flex-direction: column;
+          align-items: flex-start;
+          gap: 4px;
+        }
+      }
+
+      .execute-window {
+        color: rgba(0, 0, 0, 0.4);
+      }
+
+      .review-time {
+        color: #000;
+        font-style: normal;
+
+        &__num {
+          color: $primary;
+          font-style: normal;
+          font-weight: 500;
+        }
+      }
+
+      .row-ops {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+
+        &__btn {
+          margin: 0;
+          height: 28px;
+          padding: 0 10px;
+          font-size: 13px;
+          color: #4c4c4c;
+          background: #fff;
+          border: 1px solid #d2d2d2;
+          border-radius: 2px;
+
+          &:hover {
+            color: $primary;
+            border-color: rgba($primary, 0.45);
+            background: rgba($primary, 0.06);
+          }
+
+          &--danger {
+            color: #fff;
+            background: #f56b6b;
+            border-color: #f56b6b;
+
+            &:hover {
+              color: #fff;
+              background: #f78989;
+              border-color: #f78989;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.page-footer {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 12px;
+  padding: 20px 10px 10px;
+}
+</style>

+ 123 - 90
src/views/planting/index.vue

@@ -1,107 +1,145 @@
 <template>
   <div class="planting-page">
     <div class="page-card">
-      <!-- 顶部:Tab + 操作 -->
+      <!-- 顶部:Tab -->
       <div class="page-header">
         <div class="page-tabs">
-          <div v-for="tab in tabs" :key="tab.value" class="page-tabs__item"
-            :class="{ 'is-active': activeTab === tab.value }" @click="activeTab = tab.value">
+          <div
+            v-for="tab in tabs"
+            :key="tab.value"
+            class="page-tabs__item"
+            :class="{ 'is-active': activeTab === tab.value }"
+            @click="activeTab = tab.value"
+          >
             {{ tab.label }}
           </div>
         </div>
-        <el-button type="warning" class="btn-add" @click="onAddDevice">
-          新增设备
-        </el-button>
       </div>
 
-      <!-- 筛选区 -->
-      <div class="filter-panel">
-        <div class="filter-row">
-          <span class="filter-label">区域:</span>
-          <div class="filter-content region-selects">
-            <el-select v-model="filters.province" placeholder="选择省份" clearable class="region-select">
-              <el-option v-for="item in provinceOptions" :key="item.value" :label="item.label" :value="item.value" />
-            </el-select>
-            <el-select v-model="filters.city" placeholder="选择城市" clearable class="region-select">
-              <el-option v-for="item in cityOptions" :key="item.value" :label="item.label" :value="item.value" />
-            </el-select>
+      <!-- 种植方案 -->
+      <template v-if="activeTab === 'scheme'">
+        <div class="filter-panel">
+          <div class="filter-row">
+            <span class="filter-label">区域:</span>
+            <div class="filter-content region-selects">
+              <el-select v-model="filters.province" placeholder="选择省份" clearable class="region-select">
+                <el-option
+                  v-for="item in provinceOptions"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+              <el-select v-model="filters.city" placeholder="选择城市" clearable class="region-select">
+                <el-option
+                  v-for="item in cityOptions"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
           </div>
-        </div>
 
-        <div class="filter-row">
-          <span class="filter-label">品类:</span>
-          <div class="filter-content tag-list">
-            <div v-for="(item, idx) in categoryOptions" :key="`category-${idx}`" type="button" class="filter-tag"
-              :class="{ 'is-active': filters.categoryIndex === idx }" @click="filters.categoryIndex = idx">
-              {{ item }}
+          <div class="filter-row">
+            <span class="filter-label">品类:</span>
+            <div class="filter-content tag-list">
+              <div
+                v-for="(item, idx) in categoryOptions"
+                :key="`category-${idx}`"
+                class="filter-tag"
+                :class="{ 'is-active': filters.categoryIndex === idx }"
+                @click="filters.categoryIndex = idx"
+              >
+                {{ item }}
+              </div>
             </div>
           </div>
-        </div>
 
-        <div class="filter-row">
-          <span class="filter-label">品种:</span>
-          <div class="filter-content tag-list">
-            <div v-for="(item, idx) in varietyOptions" :key="`variety-${idx}`" type="button" class="filter-tag"
-              :class="{ 'is-active': filters.varietyIndex === idx }" @click="filters.varietyIndex = idx">
-              {{ item }}
+          <div class="filter-row">
+            <span class="filter-label">品种:</span>
+            <div class="filter-content tag-list">
+              <div
+                v-for="(item, idx) in varietyOptions"
+                :key="`variety-${idx}`"
+                class="filter-tag"
+                :class="{ 'is-active': filters.varietyIndex === idx }"
+                @click="filters.varietyIndex = idx"
+              >
+                {{ item }}
+              </div>
             </div>
           </div>
         </div>
-      </div>
 
-      <!-- 方案表格 -->
-      <div class="table-wrap">
-        <el-table :data="tableData" border class="scheme-table" :header-cell-style="headerCellStyle"
-          :cell-style="{ verticalAlign: 'top' }">
-          <el-table-column prop="name" label="农事名称" min-width="110" fixed />
-          <el-table-column label="农事类型" min-width="120">
-            <template #default="{ row }">
-              <div class="type-tags">
-                <span v-for="(item, idx) in row.types" :key="idx" class="type-tag">{{ item }}</span>
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column prop="trigger" label="触发条件" min-width="160" />
-          <el-table-column label="药肥处方" min-width="260">
-            <template #default="{ row }">
-              <div class="prescription">
-                <div class="prescription__title">{{ row.prescription.category }}</div>
-                <div v-for="(line, idx) in row.prescription.lines" :key="idx" class="prescription__line">
-                  {{ line }}
-                </div>
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column label="执行规程" align="center">
-            <el-table-column label="执行窗口" min-width="140" prop="execute.window" />
-            <el-table-column label="执行设备" min-width="120">
+        <div class="table-wrap">
+          <el-table
+            :data="tableData"
+            border
+            class="scheme-table"
+            :header-cell-style="headerCellStyle"
+            :cell-style="{ verticalAlign: 'top' }"
+          >
+            <el-table-column prop="name" label="农事名称" min-width="110" fixed />
+            <el-table-column label="农事类型" min-width="120">
               <template #default="{ row }">
                 <div class="type-tags">
-                  <span v-for="(item, idx) in row.execute.devices" :key="idx" class="type-tag">{{ item }}</span>
+                  <span v-for="(item, idx) in row.types" :key="idx" class="type-tag">{{ item }}</span>
                 </div>
               </template>
             </el-table-column>
-            <el-table-column label="注意事项" min-width="160" prop="execute.notes" />
-          </el-table-column>
-          <el-table-column label="复核规程" align="center">
-            <el-table-column label="复核时间" min-width="120">
+            <el-table-column prop="trigger" label="触发条件" min-width="160" />
+            <el-table-column label="药肥处方" min-width="260">
               <template #default="{ row }">
-                <span class="review-time">
-                  执行后 <em class="review-time__num">{{ row.review.days }}</em> 天
-                </span>
+                <div class="prescription">
+                  <div class="prescription__title">{{ row.prescription.category }}</div>
+                  <div
+                    v-for="(line, idx) in row.prescription.lines"
+                    :key="idx"
+                    class="prescription__line"
+                  >
+                    {{ line }}
+                  </div>
+                </div>
               </template>
             </el-table-column>
-            <el-table-column label="复核标准" prop="review.standard" min-width="160" />
-          </el-table-column>
-        </el-table>
-      </div>
+            <el-table-column label="执行规程" align="center">
+              <el-table-column label="执行窗口" min-width="140" prop="execute.window" />
+              <el-table-column label="执行设备" min-width="120">
+                <template #default="{ row }">
+                  <div class="type-tags">
+                    <span
+                      v-for="(item, idx) in row.execute.devices"
+                      :key="idx"
+                      class="type-tag"
+                    >{{ item }}</span>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="注意事项" min-width="160" prop="execute.notes" />
+            </el-table-column>
+            <el-table-column label="复核规程" align="center">
+              <el-table-column label="复核时间" min-width="120">
+                <template #default="{ row }">
+                  <span class="review-time">
+                    执行后 <em class="review-time__num">{{ row.review.days }}</em> 天
+                  </span>
+                </template>
+              </el-table-column>
+              <el-table-column label="复核标准" prop="review.standard" min-width="160" />
+            </el-table-column>
+          </el-table>
+        </div>
 
-      <!-- 底部操作 -->
-      <div class="page-footer">
-        <el-button type="warning" plain class="btn-copy" @click="onCopyScheme">
-          复制为我的方案
-        </el-button>
-      </div>
+        <div class="page-footer">
+          <el-button type="warning" plain class="btn-copy" @click="onCopyScheme">
+            复制为我的方案
+          </el-button>
+        </div>
+      </template>
+
+      <!-- 我的方案 -->
+      <MySchemePanel v-else @edit-scheme="onEditMyScheme" />
     </div>
 
     <EditSchemeDialog v-model="editDialogVisible" @confirm="onEditConfirm" />
@@ -112,6 +150,7 @@
 import { reactive, ref } from "vue";
 import { ElMessage } from "element-plus";
 import EditSchemeDialog from "./components/EditSchemeDialog.vue";
+import MySchemePanel from "./components/MySchemePanel.vue";
 
 const tabs = [
   { label: "种植方案", value: "scheme" },
@@ -220,16 +259,16 @@ const headerCellStyle = {
   fontWeight: 400,
 };
 
-const onAddDevice = () => {
-  ElMessage.info("新增设备功能待接入");
+const onCopyScheme = () => {
+  editDialogVisible.value = true;
 };
 
-const onCopyScheme = () => {
+const onEditMyScheme = () => {
   editDialogVisible.value = true;
 };
 
 const onEditConfirm = () => {
-  ElMessage.success("已复制为我的方案");
+  ElMessage.success(activeTab.value === "mine" ? "方案已更新" : "已复制为我的方案");
 };
 </script>
 
@@ -237,7 +276,7 @@ const onEditConfirm = () => {
 $primary: $warning-color;
 $primary-light: color.mix(#fff, $warning-color, 90%);
 $text: rgba(0, 0, 0, 0.6);
-$border: #E7E7E7;
+$border: #e7e7e7;
 
 .planting-page {
   box-sizing: border-box;
@@ -258,7 +297,6 @@ $border: #E7E7E7;
     .page-header {
       display: flex;
       align-items: center;
-      justify-content: space-between;
       border-bottom: 1px solid $border;
 
       .page-tabs {
@@ -291,11 +329,6 @@ $border: #E7E7E7;
           }
         }
       }
-
-      .btn-add {
-        height: 32px;
-        padding: 0 16px;
-      }
     }
 
     .filter-panel {
@@ -330,7 +363,7 @@ $border: #E7E7E7;
 
             .filter-tag {
               border: 1px solid transparent;
-              background: #F3F3F3;
+              background: #f3f3f3;
               color: $text;
               padding: 5px 10px;
               border-radius: 3px;
@@ -371,7 +404,7 @@ $border: #E7E7E7;
             padding: 1px 6px;
             border-radius: 4px;
             font-size: 12px;
-            color: #2199F8;
+            color: #2199f8;
             background: rgba(33, 153, 248, 0.1);
           }
         }
@@ -380,7 +413,7 @@ $border: #E7E7E7;
           &__title {
             display: inline-block;
             width: fit-content;
-            color: #4C4C4C;
+            color: #4c4c4c;
             margin-bottom: 2px;
             background: rgba(125, 125, 125, 0.1);
             padding: 0 6px;