Quellcode durchsuchen

feat: 主体管理

lxf vor 2 Wochen
Ursprung
Commit
7e1d033e01
3 geänderte Dateien mit 1207 neuen und 323 gelöschten Zeilen
  1. 260 69
      src/views/device/DeviceDetailDialog.vue
  2. 464 178
      src/views/device/DeviceFormDialog.vue
  3. 483 76
      src/views/device/index.vue

+ 260 - 69
src/views/device/DeviceDetailDialog.vue

@@ -11,72 +11,182 @@
   >
     <div v-if="data" class="detail-body">
       <div class="detail-line">
-        <span class="detail-label">姓名/手机号:</span>
+        <span class="detail-label">个人信息:</span>
         <span class="detail-value">{{ displayName }} {{ displayPhone }}</span>
       </div>
 
-      <div class="detail-section">
-        <div class="detail-label detail-label--block">种植品种:</div>
-
-        <div v-if="categoryGroups.length" class="category-list">
-          <div
-            v-for="(group, gIdx) in categoryGroups"
-            :key="gIdx"
-            class="category-block"
-          >
-            <span class="category-tag">品类:{{ group.category }}</span>
-
-            <div class="planting-grid">
-              <div
-                v-for="(item, idx) in group.plantings"
-                :key="idx"
-                class="planting-card"
-              >
-                <span class="variety-tag">{{ item.variety || "-" }}</span>
-                <div class="planting-card__row">
-                  <span class="planting-card__label">种植面积:</span>
-                  <span>{{ item.area || "-" }}</span>
-                </div>
-                <div class="planting-card__row">
-                  <span class="planting-card__label">起种点:</span>
-                  <span class="start-point-tag">{{ item.startPoint || "-" }}</span>
-                  <span class="start-point-date">{{ item.startDate || "" }}</span>
-                </div>
-                <div class="planting-card__row">
-                  <span class="planting-card__label">种植点位:</span>
-                  <span class="location-text" :title="item.plantLocation">
-                    {{ item.plantLocation || "-" }}
-                  </span>
-                  <el-button
-                    link
-                    type="warning"
-                    class="link-btn"
-                    @click="handleViewPlantLocation(item)"
-                  >
-                    查看点位
-                  </el-button>
-                </div>
+      <!-- 农服人员详情 -->
+      <template v-if="isService">
+        <div class="detail-line">
+          <span class="detail-label">团队名称:</span>
+          <span class="detail-value">{{ data.teamName || "-" }}</span>
+        </div>
+
+        <div class="detail-line">
+          <span class="detail-label">团队类型:</span>
+          <div class="detail-value tag-list">
+            <span v-if="data.teamType" class="equip-tag">{{ data.teamType }}</span>
+            <span v-else>-</span>
+          </div>
+        </div>
+
+        <div class="detail-line detail-line--top">
+          <span class="detail-label">服务类别:</span>
+          <div class="detail-value service-cat-detail">
+            <div class="service-cat-detail__row">
+              <span class="service-cat-detail__label">果树类:</span>
+              <div class="tag-list">
+                <span
+                  v-for="(tag, idx) in data.serviceCategories?.fruit || []"
+                  :key="`f-${idx}`"
+                  class="gray-tag"
+                >
+                  {{ tag }}
+                </span>
+                <span v-if="!(data.serviceCategories?.fruit?.length)">-</span>
+              </div>
+            </div>
+            <div class="service-cat-detail__row">
+              <span class="service-cat-detail__label">大田类:</span>
+              <div class="tag-list">
+                <span
+                  v-for="(tag, idx) in data.serviceCategories?.field || []"
+                  :key="`d-${idx}`"
+                  class="gray-tag"
+                >
+                  {{ tag }}
+                </span>
+                <span v-if="!(data.serviceCategories?.field?.length)">-</span>
               </div>
             </div>
           </div>
         </div>
-        <div v-else class="detail-empty">-</div>
-      </div>
 
-      <div class="detail-line detail-line--footer">
-        <span class="detail-label">常驻点位:</span>
-        <span class="detail-value location-text" :title="displayResident">
-          {{ displayResident }}
-        </span>
-        <el-button
-          link
-          type="warning"
-          class="link-btn"
-          @click="handleViewResidentLocation"
-        >
-          查看点位
-        </el-button>
-      </div>
+        <div class="detail-line">
+          <span class="detail-label">农机设备:</span>
+          <div class="detail-value tag-list">
+            <span v-for="(equip, idx) in displayEquipments" :key="idx" class="equip-tag">
+              {{ equip }}
+            </span>
+            <span v-if="!displayEquipments.length">-</span>
+          </div>
+        </div>
+
+        <div class="detail-line">
+          <span class="detail-label">人工服务:</span>
+          <div class="detail-value tag-list">
+            <span v-for="(svc, idx) in displayManualServices" :key="idx" class="farm-tag">
+              {{ svc }}
+            </span>
+            <span v-if="!displayManualServices.length">-</span>
+          </div>
+        </div>
+
+        <div class="detail-line">
+          <span class="detail-label">常驻点位:</span>
+          <span class="detail-value location-text" :title="displayResident">
+            {{ displayResident }}
+          </span>
+          <el-button link type="warning" class="link-btn" @click="handleViewResidentLocation">
+            查看点位
+          </el-button>
+        </div>
+
+        <div class="detail-line">
+          <span class="detail-label">邀请人:</span>
+          <div v-if="displayInviter" class="detail-value inviter-cell">
+            <el-avatar :size="20" class="inviter-cell__avatar">
+              {{ displayInviter.slice(0, 1) }}
+            </el-avatar>
+            <span>{{ displayInviter }}</span>
+          </div>
+          <span v-else class="detail-value">-</span>
+        </div>
+      </template>
+
+      <!-- 权属人员详情 -->
+      <template v-else>
+        <div class="detail-line">
+          <span class="detail-label">农机设备:</span>
+          <div class="detail-value tag-list">
+            <span v-for="(equip, idx) in displayEquipments" :key="idx" class="equip-tag">
+              {{ equip }}
+            </span>
+            <span v-if="!displayEquipments.length">-</span>
+          </div>
+        </div>
+
+        <div class="detail-line">
+          <span class="detail-label">外包农事:</span>
+          <div class="detail-value tag-list">
+            <span v-for="(farm, idx) in displayFarmWorks" :key="idx" class="farm-tag">
+              {{ farm }}
+            </span>
+            <span v-if="!displayFarmWorks.length">-</span>
+          </div>
+        </div>
+
+        <div class="detail-line">
+          <span class="detail-label">常驻点位:</span>
+          <span class="detail-value location-text" :title="displayResident">
+            {{ displayResident }}
+          </span>
+          <el-button link type="warning" class="link-btn" @click="handleViewResidentLocation">
+            查看点位
+          </el-button>
+        </div>
+
+        <div class="detail-line">
+          <span class="detail-label">邀请人:</span>
+          <div v-if="displayInviter" class="detail-value inviter-cell">
+            <el-avatar :size="20" class="inviter-cell__avatar">
+              {{ displayInviter.slice(0, 1) }}
+            </el-avatar>
+            <span>{{ displayInviter }}</span>
+          </div>
+          <span v-else class="detail-value">-</span>
+        </div>
+
+        <div class="detail-section">
+          <div class="detail-label detail-label--block">种植品种:</div>
+
+          <div v-if="categoryGroups.length" class="category-list">
+            <div v-for="(group, gIdx) in categoryGroups" :key="gIdx" class="category-block">
+              <span class="category-tag">品类:{{ group.category }}</span>
+
+              <div class="planting-grid">
+                <div v-for="(item, idx) in group.plantings" :key="idx" class="planting-card">
+                  <span class="variety-tag">{{ item.variety || "-" }}</span>
+                  <div class="planting-card__row">
+                    <span class="planting-card__label">种植面积:</span>
+                    <span>{{ item.area || "-" }}</span>
+                  </div>
+                  <div class="planting-card__row">
+                    <span class="planting-card__label">起种点:</span>
+                    <span class="start-point-tag">{{ item.startPoint || "-" }}</span>
+                    <span class="start-point-date">{{ item.startDate || "" }}</span>
+                  </div>
+                  <div class="planting-card__row">
+                    <span class="planting-card__label">种植点位:</span>
+                    <span class="location-text" :title="item.plantLocation">
+                      {{ item.plantLocation || "-" }}
+                    </span>
+                    <el-button
+                      link
+                      type="warning"
+                      class="link-btn"
+                      @click="handleViewPlantLocation(item)"
+                    >
+                      查看点位
+                    </el-button>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div v-else class="detail-empty">-</div>
+        </div>
+      </template>
     </div>
 
     <template #footer>
@@ -104,6 +214,8 @@ const props = defineProps({
 
 const emit = defineEmits(["update:modelValue", "edit"]);
 
+const isService = computed(() => props.data?.tabKey === "service");
+
 const displayName = computed(
   () => props.data?.name || props.data?.ownerName || "-"
 );
@@ -113,8 +225,20 @@ const displayPhone = computed(
 const displayResident = computed(
   () => props.data?.residentLocation || props.data?.location || "-"
 );
+const displayEquipments = computed(() => {
+  const list = props.data?.equipments;
+  return Array.isArray(list) ? list : [];
+});
+const displayFarmWorks = computed(() => {
+  const list = props.data?.outsourceFarmWorks;
+  return Array.isArray(list) ? list : [];
+});
+const displayManualServices = computed(() => {
+  const list = props.data?.manualServices;
+  return Array.isArray(list) ? list : [];
+});
+const displayInviter = computed(() => props.data?.inviterName || "");
 
-/** 按品类分组展示;兼容 categories 数组或单品类 + plantings */
 const categoryGroups = computed(() => {
   const row = props.data;
   if (!row) return [];
@@ -167,16 +291,15 @@ const handleEdit = () => {
   display: flex;
   align-items: center;
   flex-wrap: wrap;
-  gap: 4px 0;
+  gap: 4px 8px;
   min-height: 32px;
   margin-bottom: 16px;
   font-size: 14px;
   line-height: 22px;
   color: #1d2129;
 
-  &--footer {
-    margin-top: 8px;
-    margin-bottom: 0;
+  &--top {
+    align-items: flex-start;
   }
 }
 
@@ -196,7 +319,7 @@ const handleEdit = () => {
 }
 
 .detail-value {
-  color: #1d2129;
+  color: #000;
   font-size: 14px;
   line-height: 22px;
   word-break: break-all;
@@ -207,6 +330,74 @@ const handleEdit = () => {
   font-size: 14px;
 }
 
+.tag-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.equip-tag {
+  display: inline-block;
+  padding: 2px 6px;
+  border-radius: 2px;
+  background: rgba($warning-color, 0.12);
+  color: $warning-color;
+  font-size: 12px;
+  line-height: 22px;
+}
+
+.farm-tag {
+  display: inline-block;
+  padding: 2px 6px;
+  border-radius: 2px;
+  background: rgba(64, 158, 255, 0.12);
+  color: #409eff;
+  font-size: 12px;
+  line-height: 22px;
+}
+
+.gray-tag {
+  display: inline-block;
+  padding: 3px 6px;
+  border-radius: 2px;
+  background: rgba(153, 153, 153, 0.1);
+  color: #494949;
+  font-size: 12px;
+  line-height: 20px;
+}
+
+.service-cat-detail {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.service-cat-detail__row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.service-cat-detail__label {
+  flex-shrink: 0;
+  color: rgba(73, 73, 73, 0.5);
+  font-size: 13px;
+}
+
+.inviter-cell {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.inviter-cell__avatar {
+  flex-shrink: 0;
+  background: #ffe4c4;
+  color: #c07020;
+  font-size: 12px;
+}
+
 .category-list {
   display: flex;
   flex-direction: column;
@@ -225,7 +416,7 @@ const handleEdit = () => {
   padding: 0 10px;
   border-radius: 2px;
   min-width: 130px;
-  background: linear-gradient(270deg, #FFFFFF 0%, #FFEAD8 100%);
+  background: linear-gradient(270deg, #ffffff 0%, #ffead8 100%);
   color: $warning-color;
   font-size: 14px;
   font-weight: 500;
@@ -243,7 +434,7 @@ const handleEdit = () => {
 .planting-card {
   box-sizing: border-box;
   padding: 10px;
-  border: 1px solid #E5E6EB;
+  border: 1px solid #e5e6eb;
   border-radius: 8px;
   background: #fff;
 }
@@ -254,7 +445,7 @@ const handleEdit = () => {
   padding: 1px 6px;
   border-radius: 2px;
   background: rgba(120, 120, 120, 0.1);
-  color: #4C4C4C;
+  color: #4c4c4c;
   font-size: 12px;
   line-height: 20px;
 }
@@ -295,11 +486,11 @@ const handleEdit = () => {
 }
 
 .location-text {
-  max-width: 160px;
+  max-width: 220px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  color: #1d2129;
+  color: #000;
 }
 
 .link-btn {

+ 464 - 178
src/views/device/DeviceFormDialog.vue

@@ -1,120 +1,280 @@
 <template>
-  <el-dialog :model-value="modelValue" :title="isEdit ? '编辑' : '新增'" width="780px" align-center destroy-on-close
-    class="device-form-dialog" @update:model-value="emit('update:modelValue', $event)" @closed="handleClosed">
+  <el-dialog
+    :model-value="modelValue"
+    :title="isEdit ? '编辑' : '新增'"
+    width="780px"
+    align-center
+    destroy-on-close
+    class="device-form-dialog"
+    @update:model-value="emit('update:modelValue', $event)"
+    @closed="handleClosed"
+  >
     <el-form ref="formRef" :model="form" :rules="rules" label-position="left" class="device-form">
-      <el-form-item label="姓名/手机号:" prop="ownerName" class="form-item--inline">
+      <el-form-item label="个人信息:" prop="ownerName" class="form-item--inline">
         <div class="name-phone-row">
-          <el-input v-model="form.ownerName" placeholder="请输入姓名" maxlength="20" class="name-phone-row__name" />
+          <el-input
+            v-model="form.ownerName"
+            placeholder="请输入姓名"
+            maxlength="20"
+            class="name-phone-row__name"
+          />
           <el-form-item prop="ownerPhone" class="name-phone-row__phone-item">
             <el-input v-model="form.ownerPhone" placeholder="请输入手机号" maxlength="11" />
           </el-form-item>
         </div>
       </el-form-item>
 
-      <div class="form-section">
-
-        <div class="form-section__head">
-          <div class="form-section__label">
-            种植品种:
-          </div>
-          <el-button type="warning" @click="addCategory">
-            + 添加品类
-          </el-button>
-        </div>
-
-        <div class="category-list">
-          <div v-for="(group, gIdx) in form.categories" :key="group._key" class="category-block">
-            <div class="category-block__head">
-              <el-select v-model="group.category" placeholder="请选择品类" class="category-select">
-                <el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
+      <!-- 农服人员表单 -->
+      <template v-if="isServiceForm">
+        <el-form-item label="团队名称:" prop="teamName">
+          <el-input v-model="form.teamName" placeholder="请输入团队名称" maxlength="50" />
+        </el-form-item>
+
+        <el-form-item label="团队类型:" prop="teamType">
+          <el-select v-model="form.teamType" placeholder="请选择团队类型" style="width: 100%">
+            <el-option v-for="item in teamTypeOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="服务类别:" class="form-item--service-cat">
+          <div class="service-cat-form">
+            <div class="service-cat-form__row">
+              <span class="service-cat-form__label">果树类:</span>
+              <el-select
+                v-model="form.serviceCategories.fruit"
+                multiple
+                collapse-tags
+                collapse-tags-tooltip
+                :max-collapse-tags="3"
+                placeholder="请选择"
+                class="service-cat-form__select"
+              >
+                <el-option v-for="item in serviceCatOptions" :key="item" :label="item" :value="item" />
               </el-select>
-
-              
-            <div class="link-btn secondary-link" @click="addPlanting(gIdx)">
-              + 添加品种
             </div>
-
-              <el-button v-if="form.categories.length > 1" link type="danger" class="link-btn"
-                @click="removeCategory(gIdx)">
-                删除品类
-              </el-button>
+            <div class="service-cat-form__row">
+              <span class="service-cat-form__label">大田类:</span>
+              <el-select
+                v-model="form.serviceCategories.field"
+                multiple
+                collapse-tags
+                collapse-tags-tooltip
+                :max-collapse-tags="3"
+                placeholder="请选择"
+                class="service-cat-form__select"
+              >
+                <el-option v-for="item in serviceCatOptions" :key="item" :label="item" :value="item" />
+              </el-select>
             </div>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="农机设备:" prop="equipments">
+          <el-select
+            v-model="form.equipments"
+            multiple
+            collapse-tags
+            collapse-tags-tooltip
+            :max-collapse-tags="3"
+            placeholder="请选择农机设备"
+            style="width: 100%"
+          >
+            <el-option v-for="item in equipmentOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="人工服务:" prop="manualServices">
+          <el-select
+            v-model="form.manualServices"
+            multiple
+            collapse-tags
+            collapse-tags-tooltip
+            :max-collapse-tags="3"
+            placeholder="请选择人工服务"
+            style="width: 100%"
+          >
+            <el-option v-for="item in farmWorkOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="常驻点位:" prop="residentLocation" class="form-item--resident">
+          <div class="location-box location-box--full">
+            <span class="location-box__text" :title="form.residentLocation">
+              {{ form.residentLocation || "请选择点位" }}
+            </span>
+            <el-button link type="warning" class="link-btn" @click="openResidentLocation">
+              修改点位
+            </el-button>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="邀请人:">
+          <div v-if="form.inviterName" class="inviter-cell">
+            <el-avatar :size="28" class="inviter-cell__avatar">
+              {{ form.inviterName.slice(0, 1) }}
+            </el-avatar>
+            <span class="inviter-cell__name">{{ form.inviterName }}</span>
+          </div>
+          <span v-else class="inviter-empty">-</span>
+        </el-form-item>
+      </template>
+
+      <!-- 权属人员表单 -->
+      <template v-else>
+        <el-form-item label="农机设备:" prop="equipments">
+          <el-select
+            v-model="form.equipments"
+            multiple
+            collapse-tags
+            collapse-tags-tooltip
+            :max-collapse-tags="3"
+            placeholder="请选择农机设备"
+            style="width: 100%"
+          >
+            <el-option v-for="item in equipmentOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="外包农事:" prop="outsourceFarmWorks">
+          <el-select
+            v-model="form.outsourceFarmWorks"
+            multiple
+            collapse-tags
+            collapse-tags-tooltip
+            :max-collapse-tags="3"
+            placeholder="请选择外包农事"
+            style="width: 100%"
+          >
+            <el-option v-for="item in farmWorkOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="常驻点位:" prop="residentLocation" class="form-item--resident">
+          <div class="location-box location-box--full">
+            <span class="location-box__text" :title="form.residentLocation">
+              {{ form.residentLocation || "请选择点位" }}
+            </span>
+            <el-button link type="warning" class="link-btn" @click="openResidentLocation">
+              修改点位
+            </el-button>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="邀请人:">
+          <div v-if="form.inviterName" class="inviter-cell">
+            <el-avatar :size="28" class="inviter-cell__avatar">
+              {{ form.inviterName.slice(0, 1) }}
+            </el-avatar>
+            <span class="inviter-cell__name">{{ form.inviterName }}</span>
+          </div>
+          <span v-else class="inviter-empty">-</span>
+        </el-form-item>
 
-            <div class="planting-grid">
-              <div v-for="(item, pIdx) in group.plantings" :key="item._key" class="planting-card">
-                <div class="planting-card__top">
-                  <el-select v-model="item.variety" placeholder="请选择品种" class="variety-select">
-                    <el-option v-for="v in varietyOptions" :key="v" :label="v" :value="v" />
-                  </el-select>
-                  <el-button link type="danger" class="link-btn"
-                    :disabled="group.plantings.length <= 1 && form.categories.length <= 1"
-                    @click="removePlanting(gIdx, pIdx)">
-                    删除
-                  </el-button>
-                </div>
+        <div class="form-section">
+          <div class="form-section__head">
+            <div class="form-section__label">种植品种:</div>
+            <el-button type="warning" @click="addCategory">+ 添加品类</el-button>
+          </div>
 
-                <div class="planting-card__row">
-                  <span class="planting-card__label">物候期:</span>
-                  <el-select v-model="item.phenology" placeholder="请选择物候期" class="planting-card__input">
-                    <el-option v-for="ph in phenologyOptions" :key="ph" :label="ph" :value="ph" />
-                  </el-select>
-                </div>
+          <div class="category-list">
+            <div v-for="(group, gIdx) in form.categories" :key="group._key" class="category-block">
+              <div class="category-block__head">
+                <el-select v-model="group.category" placeholder="请选择品类" class="category-select">
+                  <el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
+                </el-select>
+
+                <div class="link-btn secondary-link" @click="addPlanting(gIdx)">+ 添加品种</div>
+
+                <el-button
+                  v-if="form.categories.length > 1"
+                  link
+                  type="danger"
+                  class="link-btn"
+                  @click="removeCategory(gIdx)"
+                >
+                  删除品类
+                </el-button>
+              </div>
 
-                <div class="planting-card__row">
-                  <span class="planting-card__label">种植面积:</span>
-                  <el-input v-model="item.area" placeholder="请输入种植面积" class="planting-card__input" />
-                </div>
+              <div class="planting-grid">
+                <div v-for="(item, pIdx) in group.plantings" :key="item._key" class="planting-card">
+                  <div class="planting-card__top">
+                    <el-select v-model="item.variety" placeholder="请选择品种" class="variety-select">
+                      <el-option v-for="v in varietyOptions" :key="v" :label="v" :value="v" />
+                    </el-select>
+                    <el-button
+                      link
+                      type="danger"
+                      class="link-btn"
+                      :disabled="group.plantings.length <= 1 && form.categories.length <= 1"
+                      @click="removePlanting(gIdx, pIdx)"
+                    >
+                      删除
+                    </el-button>
+                  </div>
 
-                <div class="planting-card__row">
-                  <span class="planting-card__label">起种点:</span>
-                  <el-select v-model="item.startPoint" placeholder="请选择" class="start-point-select">
-                    <el-option v-for="sp in startPointOptions" :key="sp" :label="sp" :value="sp" />
-                  </el-select>
-                  <el-date-picker v-model="item.startDate" type="date" placeholder="选择日期" value-format="YYYY.MM.DD"
-                    format="YYYY.MM.DD" class="start-date-picker" />
-                </div>
+                  <div class="planting-card__row">
+                    <span class="planting-card__label">物候期:</span>
+                    <el-select v-model="item.phenology" placeholder="请选择物候期" class="planting-card__input">
+                      <el-option v-for="ph in phenologyOptions" :key="ph" :label="ph" :value="ph" />
+                    </el-select>
+                  </div>
 
-                <div class="planting-card__row">
-                  <span class="planting-card__label">种植点位:</span>
-                  <div class="location-box">
-                    <span class="location-box__text" :title="item.plantLocation">
-                      {{ item.plantLocation || "请选择点位" }}
-                    </span>
-                    <el-button link type="warning" class="link-btn" @click="openPlantLocation(gIdx, pIdx)">
-                      修改点位
-                    </el-button>
+                  <div class="planting-card__row">
+                    <span class="planting-card__label">种植面积:</span>
+                    <el-input v-model="item.area" placeholder="请输入种植面积" class="planting-card__input" />
+                  </div>
+
+                  <div class="planting-card__row">
+                    <span class="planting-card__label">起种点:</span>
+                    <el-select v-model="item.startPoint" placeholder="请选择" class="start-point-select">
+                      <el-option v-for="sp in startPointOptions" :key="sp" :label="sp" :value="sp" />
+                    </el-select>
+                    <el-date-picker
+                      v-model="item.startDate"
+                      type="date"
+                      placeholder="选择日期"
+                      value-format="YYYY.MM.DD"
+                      format="YYYY.MM.DD"
+                      class="start-date-picker"
+                    />
+                  </div>
+
+                  <div class="planting-card__row">
+                    <span class="planting-card__label">种植点位:</span>
+                    <div class="location-box">
+                      <span class="location-box__text" :title="item.plantLocation">
+                        {{ item.plantLocation || "请选择点位" }}
+                      </span>
+                      <el-button link type="warning" class="link-btn" @click="openPlantLocation(gIdx, pIdx)">
+                        修改点位
+                      </el-button>
+                    </div>
                   </div>
                 </div>
               </div>
             </div>
           </div>
         </div>
-      </div>
-
-      <el-form-item label="常驻点位:" prop="residentLocation" class="form-item--resident">
-        <div class="location-box location-box--full">
-          <span class="location-box__text" :title="form.residentLocation">
-            {{ form.residentLocation || "请选择点位" }}
-          </span>
-          <el-button link type="warning" class="link-btn" @click="openResidentLocation">
-            修改点位
-          </el-button>
-        </div>
-      </el-form-item>
+      </template>
     </el-form>
 
     <template #footer>
       <div class="dialog-footer">
-        <el-button @click="close">取消</el-button>
+        <el-button @click="close">{{ isServiceForm ? "取消编辑" : "取消" }}</el-button>
         <el-button type="primary" :loading="submitting" @click="handleSubmit">
-          提交
+          {{ isServiceForm ? "确认信息" : "提交" }}
         </el-button>
       </div>
     </template>
   </el-dialog>
 
-  <SelectLocationDialog v-model="locationDialogVisible" :farm-id="101532" :location-wkt="activeLocationWkt"
-    @confirm="handleLocationConfirm" />
+  <SelectLocationDialog
+    v-model="locationDialogVisible"
+    :farm-id="101532"
+    :location-wkt="activeLocationWkt"
+    @confirm="handleLocationConfirm"
+  />
 </template>
 
 <script setup>
@@ -129,27 +289,40 @@ const props = defineProps({
   },
   mode: {
     type: String,
-    default: "add", // add | edit
+    default: "add",
   },
   data: {
     type: Object,
     default: null,
   },
+  /** owner | service,新增时用当前 tab;编辑时优先 data.tabKey */
+  personType: {
+    type: String,
+    default: "owner",
+  },
 });
 
 const emit = defineEmits(["update:modelValue", "submit"]);
 
 const isEdit = computed(() => props.mode === "edit");
+const isServiceForm = computed(() => {
+  if (isEdit.value && props.data?.tabKey) return props.data.tabKey === "service";
+  return props.personType === "service";
+});
+
 const formRef = ref();
 const submitting = ref(false);
 const locationDialogVisible = ref(false);
-/** @type {import('vue').Ref<null | { type: 'resident' } | { type: 'plant', categoryIndex: number, plantingIndex: number }>} */
 const locationTarget = ref(null);
 
 const categoryOptions = ["品类一", "品类二", "荔枝", "龙眼"];
 const varietyOptions = ["品种一", "妃子笑", "桂味", "糯米糍", "白糖罂", "仙进奉"];
 const phenologyOptions = ["生育期一", "生育期二", "生育期三", "生育期四"];
 const startPointOptions = ["来白点", "来白点2"];
+const equipmentOptions = ["设备一", "农机设备", "农机设备一", "农机设备二"];
+const farmWorkOptions = ["农事类别一", "农事类别二", "农事类别三"];
+const teamTypeOptions = ["家庭农场", "团队类型一", "合作社", "企业"];
+const serviceCatOptions = ["品类一", "品类二", "品类三"];
 
 let keySeed = 0;
 const nextKey = () => `k-${++keySeed}`;
@@ -176,35 +349,62 @@ const createCategory = (data = {}) => ({
 
 const createEmptyForm = () => ({
   id: null,
+  tabKey: "owner",
   ownerName: "",
   ownerPhone: "",
+  teamName: "",
+  teamType: "",
+  serviceCategories: { fruit: [], field: [] },
+  equipments: [],
+  outsourceFarmWorks: [],
+  manualServices: [],
   residentLocation: "",
   locationWkt: "",
+  inviterName: "张扬",
   categories: [createCategory({ category: "品类一" })],
 });
 
 const form = reactive(createEmptyForm());
 
-const rules = {
-  ownerName: [{ required: true, message: "请输入姓名", trigger: "blur" }],
-  ownerPhone: [
-    { required: true, message: "请输入手机号", trigger: "blur" },
-    {
-      pattern: /^1\d{10}$/,
-      message: "请输入正确的手机号",
-      trigger: "blur",
-    },
-  ],
-  residentLocation: [
-    {
-      validator: (_rule, _value, callback) => {
-        if (!form.residentLocation) callback(new Error("请选择常驻点位"));
-        else callback();
+const rules = computed(() => {
+  const base = {
+    ownerName: [{ required: true, message: "请输入姓名", trigger: "blur" }],
+    ownerPhone: [
+      { required: true, message: "请输入手机号", trigger: "blur" },
+      { pattern: /^1\d{10}$/, message: "请输入正确的手机号", trigger: "blur" },
+    ],
+    equipments: [
+      { type: "array", required: true, min: 1, message: "请选择农机设备", trigger: "change" },
+    ],
+    residentLocation: [
+      {
+        validator: (_rule, _value, callback) => {
+          if (!form.residentLocation) callback(new Error("请选择常驻点位"));
+          else callback();
+        },
+        trigger: "change",
       },
-      trigger: "change",
-    },
-  ],
-};
+    ],
+  };
+
+  if (isServiceForm.value) {
+    return {
+      ...base,
+      teamName: [{ required: true, message: "请输入团队名称", trigger: "blur" }],
+      teamType: [{ required: true, message: "请选择团队类型", trigger: "change" }],
+      manualServices: [
+        { type: "array", required: true, min: 1, message: "请选择人工服务", trigger: "change" },
+      ],
+    };
+  }
+
+  return {
+    ...base,
+    outsourceFarmWorks: [
+      { type: "array", required: true, min: 1, message: "请选择外包农事", trigger: "change" },
+    ],
+  };
+});
 
 const activeLocationWkt = computed(() => {
   const target = locationTarget.value;
@@ -218,10 +418,7 @@ const normalizeCategoriesFromData = (data) => {
   if (Array.isArray(data?.categories) && data.categories.length) {
     return data.categories.map((c) => {
       if (typeof c === "string") {
-        return createCategory({
-          category: c,
-          plantings: data.plantings || [],
-        });
+        return createCategory({ category: c, plantings: data.plantings || [] });
       }
       return createCategory({
         category: c.category || c.name || c.cropCategory || "",
@@ -229,7 +426,6 @@ const normalizeCategoriesFromData = (data) => {
       });
     });
   }
-
   return [
     createCategory({
       category: data?.cropCategory || data?.type || "荔枝",
@@ -241,10 +437,21 @@ const normalizeCategoriesFromData = (data) => {
 const fillForm = (data) => {
   Object.assign(form, createEmptyForm(), {
     id: data?.id ?? null,
+    tabKey: data?.tabKey || props.personType || "owner",
     ownerName: data?.ownerName || data?.name || "",
     ownerPhone: data?.ownerPhone || data?.phone || "",
+    teamName: data?.teamName || "",
+    teamType: data?.teamType || "",
+    serviceCategories: {
+      fruit: [...(data?.serviceCategories?.fruit || [])],
+      field: [...(data?.serviceCategories?.field || [])],
+    },
+    equipments: [...(data?.equipments || [])],
+    outsourceFarmWorks: [...(data?.outsourceFarmWorks || [])],
+    manualServices: [...(data?.manualServices || [])],
     residentLocation: data?.residentLocation || data?.location || "",
     locationWkt: data?.locationWkt || "",
+    inviterName: data?.inviterName || "张扬",
     categories: normalizeCategoriesFromData(data),
   });
 };
@@ -256,7 +463,9 @@ watch(
     if (isEdit.value && props.data) {
       fillForm(props.data);
     } else {
-      Object.assign(form, createEmptyForm());
+      Object.assign(form, createEmptyForm(), {
+        tabKey: props.personType || "owner",
+      });
     }
   }
 );
@@ -348,42 +557,80 @@ const validatePlantings = () => {
 const handleSubmit = async () => {
   await formRef.value?.validate?.(async (valid) => {
     if (!valid) return;
-    if (!validatePlantings()) return;
+
+    if (!isServiceForm.value && !validatePlantings()) return;
+
+    if (isServiceForm.value) {
+      const fruit = form.serviceCategories.fruit || [];
+      const field = form.serviceCategories.field || [];
+      if (!fruit.length && !field.length) {
+        ElMessage.warning("请至少选择一类服务类别");
+        return;
+      }
+    }
 
     submitting.value = true;
     try {
-      const categories = form.categories.map(({ category, plantings }) => ({
-        category,
-        plantings: plantings.map(
-          ({ variety, area, startPoint, startDate, plantLocation, plantLocationWkt, phenology }) => ({
-            variety,
-            area,
-            startPoint,
-            startDate,
-            plantLocation,
-            plantLocationWkt,
-            phenology: phenology || "生育期一",
-          })
-        ),
-      }));
-
-      const plantings = categories.flatMap((g) =>
-        g.plantings.map((p) => ({ ...p, cropCategory: g.category }))
-      );
-
-      const payload = {
-        id: form.id,
-        ownerName: form.ownerName,
-        ownerPhone: form.ownerPhone,
-        name: form.ownerName,
-        phone: form.ownerPhone,
-        residentLocation: form.residentLocation,
-        location: form.residentLocation,
-        locationWkt: form.locationWkt,
-        cropCategory: categories[0]?.category || "",
-        categories,
-        plantings,
-      };
+      const tabKey = isServiceForm.value ? "service" : "owner";
+      let payload;
+
+      if (isServiceForm.value) {
+        payload = {
+          id: form.id,
+          tabKey,
+          ownerName: form.ownerName,
+          ownerPhone: form.ownerPhone,
+          name: form.ownerName,
+          phone: form.ownerPhone,
+          teamName: form.teamName,
+          teamType: form.teamType,
+          serviceCategories: {
+            fruit: [...form.serviceCategories.fruit],
+            field: [...form.serviceCategories.field],
+          },
+          equipments: [...form.equipments],
+          manualServices: [...form.manualServices],
+          residentLocation: form.residentLocation,
+          location: form.residentLocation,
+          locationWkt: form.locationWkt,
+          inviterName: form.inviterName,
+        };
+      } else {
+        const categories = form.categories.map(({ category, plantings }) => ({
+          category,
+          plantings: plantings.map(
+            ({ variety, area, startPoint, startDate, plantLocation, plantLocationWkt, phenology }) => ({
+              variety,
+              area,
+              startPoint,
+              startDate,
+              plantLocation,
+              plantLocationWkt,
+              phenology: phenology || "生育期一",
+            })
+          ),
+        }));
+        const plantings = categories.flatMap((g) =>
+          g.plantings.map((p) => ({ ...p, cropCategory: g.category }))
+        );
+        payload = {
+          id: form.id,
+          tabKey,
+          ownerName: form.ownerName,
+          ownerPhone: form.ownerPhone,
+          name: form.ownerName,
+          phone: form.ownerPhone,
+          equipments: [...form.equipments],
+          outsourceFarmWorks: [...form.outsourceFarmWorks],
+          residentLocation: form.residentLocation,
+          location: form.residentLocation,
+          locationWkt: form.locationWkt,
+          inviterName: form.inviterName,
+          cropCategory: categories[0]?.category || "",
+          categories,
+          plantings,
+        };
+      }
 
       emit("submit", { mode: props.mode, data: payload });
       ElMessage.success(isEdit.value ? "编辑成功" : "新增成功");
@@ -406,6 +653,11 @@ const handleSubmit = async () => {
     margin-bottom: 18px;
   }
 
+  :deep(.el-tag.el-tag--info) {
+    --el-tag-text-color: rgba(0, 0, 0, 0.9);
+    --el-tag-bg-color: #E7E7E7;
+  }
+
   :deep(.el-form-item__label) {
     color: #1d2129;
     font-size: 14px;
@@ -425,12 +677,12 @@ const handleSubmit = async () => {
   width: 100%;
 
   &__name {
-    width: 190px;
+    width: 160px;
     flex-shrink: 0;
   }
 
   &__phone-item {
-    width: 190px;
+    flex: 1;
     margin-bottom: 0 !important;
 
     :deep(.el-form-item__content) {
@@ -439,20 +691,48 @@ const handleSubmit = async () => {
   }
 }
 
-.form-section {
-  margin-bottom: 18px;
+.form-item--service-cat {
+  :deep(.el-form-item__content) {
+    flex: 1;
+  }
+}
+
+.service-cat-form {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  width: 100%;
+}
+
+.service-cat-form__row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+}
+
+.service-cat-form__label {
+  flex-shrink: 0;
+  width: 56px;
+  color: rgba(0, 0, 0, 0.2);
+  font-size: 13px;
+}
+
+.service-cat-form__select {
+  flex: 1;
 }
 
 .form-section__head {
   display: flex;
-  gap: 12px;
   align-items: center;
-  padding-bottom: 10px;
+  justify-content: space-between;
+  margin-bottom: 12px;
 }
 
 .form-section__label {
-  color: #1D2129;
-  font-size: 16px;
+  color: #1d2129;
+  font-size: 14px;
+  line-height: 22px;
 }
 
 .category-list {
@@ -461,11 +741,6 @@ const handleSubmit = async () => {
   gap: 16px;
 }
 
-.category-block + .category-block {
-  border-top: 1px solid #E5E6EB;
-  padding-top: 16px;
-}
-
 .category-block__head {
   display: flex;
   align-items: center;
@@ -477,9 +752,8 @@ const handleSubmit = async () => {
   width: 120px;
 
   :deep(.el-select__wrapper) {
-    // background: rgba($warning-color, 0.12);
-    box-shadow: 0 0 0 1px $warning-color inset;
-    border-radius: 2px;
+    background: rgba($warning-color, 0.12);
+    box-shadow: 0 0 0 1px rgba($warning-color, 0.25) inset;
 
     .el-select__selected-item,
     .el-select__placeholder,
@@ -528,10 +802,8 @@ const handleSubmit = async () => {
 
 .planting-card__label {
   flex-shrink: 0;
-  display: inline-block;
-  width: 70px;
   color: #1d2129;
-  font-size: 14px;
+  font-size: 13px;
   line-height: 22px;
 }
 
@@ -580,24 +852,15 @@ const handleSubmit = async () => {
 
 .link-btn {
   padding: 0;
-  font-size: 14px;
+  height: auto;
+  font-size: 13px;
   flex-shrink: 0;
-  cursor: pointer;
-}
-.secondary-link {
-  padding: 0 6px;
-  color: #FB8E33;
-  background: rgba(251, 142, 51, 0.1);
-  height: 25px;
-  line-height: 25px;
-  border-radius: 2px;
-  &:hover {
-    opacity: 0.8;
-  }
 }
 
-.add-link {
-  margin-top: 8px;
+.secondary-link {
+  color: $warning-color;
+  cursor: pointer;
+  user-select: none;
 }
 
 .form-item--resident {
@@ -606,6 +869,29 @@ const handleSubmit = async () => {
   }
 }
 
+.inviter-cell {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.inviter-cell__avatar {
+  flex-shrink: 0;
+  background: #ffe4c4;
+  color: #c07020;
+  font-size: 12px;
+}
+
+.inviter-cell__name {
+  color: #1d2129;
+  font-size: 14px;
+}
+
+.inviter-empty {
+  color: #86909c;
+  font-size: 14px;
+}
+
 .dialog-footer {
   display: flex;
   justify-content: center;

+ 483 - 76
src/views/device/index.vue

@@ -38,7 +38,9 @@
           </div>
 
           <div class="table-wrap">
+            <!-- 权属人员表格 -->
             <el-table
+              v-if="activeTab === 'owner'"
               :data="tableData"
               :span-method="objectSpanMethod"
               class="device-table"
@@ -91,11 +93,7 @@
               <el-table-column label="农机设备" min-width="120">
                 <template #default="{ row }">
                   <div class="tag-list">
-                    <span
-                      v-for="(equip, idx) in row.equipments || []"
-                      :key="idx"
-                      class="equip-tag"
-                    >
+                    <span v-for="(equip, idx) in row.equipments || []" :key="idx" class="equip-tag">
                       {{ equip }}
                     </span>
                     <span v-if="!(row.equipments && row.equipments.length)">-</span>
@@ -106,11 +104,7 @@
               <el-table-column label="外包农事" min-width="120">
                 <template #default="{ row }">
                   <div class="tag-list">
-                    <span
-                      v-for="(farm, idx) in row.outsourceFarmWorks || []"
-                      :key="idx"
-                      class="farm-tag"
-                    >
+                    <span v-for="(farm, idx) in row.outsourceFarmWorks || []" :key="idx" class="farm-tag">
                       {{ farm }}
                     </span>
                     <span v-if="!(row.outsourceFarmWorks && row.outsourceFarmWorks.length)">-</span>
@@ -153,6 +147,125 @@
                 </template>
               </el-table-column>
             </el-table>
+
+            <!-- 农服人员表格 -->
+            <el-table
+              v-else
+              :data="tableData"
+              class="device-table"
+              height="100%"
+              row-key="rowKey"
+              border
+              header-cell-class-name="device-table__header"
+            >
+              <el-table-column label="个人信息" min-width="120">
+                <template #default="{ row }">
+                  <div class="person-cell">
+                    <div class="person-cell__name">{{ row.name }}</div>
+                    <div class="person-cell__phone">{{ row.phone }}</div>
+                  </div>
+                </template>
+              </el-table-column>
+
+              <el-table-column prop="teamName" label="团队名称" min-width="100" show-overflow-tooltip />
+
+              <el-table-column label="团队类型" min-width="110">
+                <template #default="{ row }">
+                  <span v-if="row.teamType" class="equip-tag">{{ row.teamType }}</span>
+                  <span v-else>-</span>
+                </template>
+              </el-table-column>
+
+              <el-table-column label="服务类别" min-width="260">
+                <template #default="{ row }">
+                  <div class="service-cat-cell">
+                    <div class="service-cat-group">
+                      <div class="service-cat-group__label">果树类</div>
+                      <div class="tag-list">
+                        <span
+                          v-for="(tag, idx) in row.serviceCategories?.fruit || []"
+                          :key="`f-${idx}`"
+                          class="gray-tag"
+                        >
+                          {{ tag }}
+                        </span>
+                        <span v-if="!(row.serviceCategories?.fruit?.length)">-</span>
+                      </div>
+                    </div>
+                    <div class="service-cat-group">
+                      <div class="service-cat-group__label">大田类</div>
+                      <div class="tag-list">
+                        <span
+                          v-for="(tag, idx) in row.serviceCategories?.field || []"
+                          :key="`d-${idx}`"
+                          class="gray-tag"
+                        >
+                          {{ tag }}
+                        </span>
+                        <span v-if="!(row.serviceCategories?.field?.length)">-</span>
+                      </div>
+                    </div>
+                  </div>
+                </template>
+              </el-table-column>
+
+              <el-table-column label="农机设备" min-width="120">
+                <template #default="{ row }">
+                  <div class="tag-list">
+                    <span v-for="(equip, idx) in row.equipments || []" :key="idx" class="equip-tag">
+                      {{ equip }}
+                    </span>
+                    <span v-if="!(row.equipments && row.equipments.length)">-</span>
+                  </div>
+                </template>
+              </el-table-column>
+
+              <el-table-column label="人工服务" min-width="120">
+                <template #default="{ row }">
+                  <div class="tag-list">
+                    <span v-for="(svc, idx) in row.manualServices || []" :key="idx" class="farm-tag">
+                      {{ svc }}
+                    </span>
+                    <span v-if="!(row.manualServices && row.manualServices.length)">-</span>
+                  </div>
+                </template>
+              </el-table-column>
+
+              <el-table-column label="常驻点位" min-width="160">
+                <template #default="{ row }">
+                  <div class="location-cell">
+                    <span class="location-cell__text" :title="row.residentLocation">{{ row.residentLocation }}</span>
+                    <el-button link type="warning" class="link-btn" @click="handleViewResidentLocation(row)">
+                      查看点位
+                    </el-button>
+                  </div>
+                </template>
+              </el-table-column>
+
+              <el-table-column label="邀请人" min-width="130">
+                <template #default="{ row }">
+                  <div v-if="row.inviterName" class="inviter-cell">
+                    <el-avatar :size="28" class="inviter-cell__avatar">
+                      {{ row.inviterName.slice(0, 1) }}
+                    </el-avatar>
+                    <span class="inviter-cell__name">{{ row.inviterName }}</span>
+                  </div>
+                  <span v-else>-</span>
+                </template>
+              </el-table-column>
+
+              <el-table-column label="操作" width="168" fixed="right" align="center">
+                <template #default="{ row }">
+                  <div class="ops">
+                    <el-button class="ops__btn" @click="handleEdit(row)">编辑</el-button>
+                    <el-button class="ops__btn" @click="handleDetail(row)">详情</el-button>
+                    <el-button class="ops__btn ops__btn--danger" type="danger" @click="handleDelete(row)">
+                      删除
+                    </el-button>
+                  </div>
+                </template>
+              </el-table-column>
+            </el-table>
           </div>
 
           <div class="pager">
@@ -171,13 +284,66 @@
         </div>
 
         <div class="rank-tabs">
-          <div v-for="tab in rankTabs" :key="tab.key" class="rank-tabs__item"
-            :class="{ 'is-active': activeRankTab === tab.key }" @click="activeRankTab = tab.key">
+          <div
+            v-for="tab in rankTabs"
+            :key="tab.key"
+            class="rank-tabs__item"
+            :class="{ 'is-active': activeRankTab === tab.key }"
+            @click="activeRankTab = tab.key"
+          >
             {{ tab.label }}
           </div>
         </div>
 
-        <div class="rank-list">
+        <!-- 农户种植面积排行 / 农服服务订单排行 -->
+        <template v-if="activeRankTab === 'area' || activeRankTab === 'order'">
+          <div class="rank-list-head">
+            <span class="rank-list-head__rank">排名</span>
+            <span class="rank-list-head__info">{{ rankRowMeta.infoLabel }}</span>
+            <span class="rank-list-head__value">{{ rankRowMeta.valueLabel }}</span>
+          </div>
+          <div class="rank-list rank-list--row">
+            <div
+              v-for="item in currentRankList"
+              :key="item.rank"
+              class="rank-row"
+              :class="[`is-top-${Math.min(item.rank, 4)}`]"
+            >
+              <span class="rank-row__rank">
+                <img
+                  v-if="item.rank <= 3"
+                  class="rank-row__rank-img"
+                  :src="rankIcons[item.rank]"
+                  :alt="`第${item.rank}名`"
+                />
+                <template v-else>{{ item.rank }}</template>
+              </span>
+              <el-avatar :size="36" class="rank-row__avatar">
+                {{ item.name.slice(0, 1) }}
+              </el-avatar>
+              <div class="rank-row__info">
+                <div class="rank-row__name-line">
+                  <span class="rank-row__name">{{ item.name }}</span>
+                  <span
+                    v-for="(tag, tIdx) in item.tags || []"
+                    :key="tIdx"
+                    class="rank-row__tag"
+                  >
+                    {{ tag }}
+                  </span>
+                </div>
+                <div class="rank-row__location">{{ item.subtitle || "-" }}</div>
+              </div>
+              <div class="rank-row__value">
+                <span class="rank-row__num">{{ item.value }}</span>
+                <span class="rank-row__unit">{{ item.unit }}</span>
+              </div>
+            </div>
+          </div>
+        </template>
+
+        <!-- 管理业绩 -->
+        <div v-else class="rank-list">
           <div
             v-for="item in currentRankList"
             :key="item.rank"
@@ -228,7 +394,13 @@
     </div>
   </div>
 
-  <DeviceFormDialog v-model="dialogVisible" :mode="dialogMode" :data="editingRow" @submit="handleDialogSubmit" />
+  <DeviceFormDialog
+    v-model="dialogVisible"
+    :mode="dialogMode"
+    :data="editingRow"
+    :person-type="activeTab"
+    @submit="handleDialogSubmit"
+  />
 
   <DeviceDetailDialog v-model="detailVisible" :data="detailRow" @edit="handleDetailEdit" />
 </template>
@@ -236,7 +408,6 @@
 <script setup>
 import { computed, ref } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
-import { UserFilled } from "@element-plus/icons-vue";
 import DeviceFormDialog from "./DeviceFormDialog.vue";
 import DeviceDetailDialog from "./DeviceDetailDialog.vue";
 import rank1Icon from "@/assets/page/rank-1.png";
@@ -260,26 +431,45 @@ const VARIETIES = [
 const buildMockRows = (count = 40) => {
   const list = [];
   for (let i = 1; i <= count; i++) {
+    const isService = i % 2 === 0;
     const varieties = i === 1 ? ["妃子笑", "桂味"] : VARIETIES[i % VARIETIES.length];
-    list.push({
+    const base = {
       id: i,
       name: i === 1 ? "张洋" : NAMES[i % NAMES.length],
       phone: i === 1 ? "19871533168" : `1987153${String(3160 + (i % 40)).padStart(4, "0")}`,
-      cropCategory: "荔枝",
       residentLocation: "位置位置位置位置",
-      equipments: ["农机设备"],
-      outsourceFarmWorks: ["农事类别一"],
+      equipments: ["农机设备", "农机设备"],
       inviterName: "张扬",
-      tabKey: i % 2 === 0 ? "service" : "owner",
-      plantings: varieties.map((variety) => ({
-        variety,
-        phenology: "生育期一",
-        area: "158亩",
-        startPoint: "来白点",
-        startDate: "2025.05.08",
-        plantLocation: "位置位置位置位置",
-      })),
-    });
+      tabKey: isService ? "service" : "owner",
+    };
+
+    if (isService) {
+      list.push({
+        ...base,
+        teamName: "荔枝",
+        teamType: "家庭农场",
+        serviceCategories: {
+          fruit: ["品类一", "品类二", "品类二"],
+          field: ["品类一", "品类二", "品类二"],
+        },
+        manualServices: ["农事类别一", "农事类别一"],
+      });
+    } else {
+      list.push({
+        ...base,
+        cropCategory: "荔枝",
+        outsourceFarmWorks: ["农事类别一"],
+        equipments: ["农机设备"],
+        plantings: varieties.map((variety) => ({
+          variety,
+          phenology: "生育期一",
+          area: "158亩",
+          startPoint: "来白点",
+          startDate: "2025.05.08",
+          plantLocation: "位置位置位置位置",
+        })),
+      });
+    }
   }
   return list;
 };
@@ -314,7 +504,7 @@ const statCards = computed(() => [
   },
   {
     key: "service",
-    title: "农服人员总数",
+    title: "农服总数",
     value: serviceCount.value,
     extra: "昨日新增",
     extraValue: "5",
@@ -323,7 +513,7 @@ const statCards = computed(() => [
   },
   {
     key: "owner",
-    title: "权属人员总数",
+    title: "本月邀请农户数",
     value: ownerCount.value,
     extra: "本月新增",
     extraValue: "12",
@@ -332,8 +522,8 @@ const statCards = computed(() => [
   },
   {
     key: "area",
-    title: "种植总面积(亩)",
-    value: 12580,
+    title: "本月邀请农服数",
+    value: 150,
     extra: "本月增加",
     extraValue: "120",
     iconBg: "rgba(251, 142, 51, 0.12)",
@@ -361,8 +551,19 @@ const total = computed(() => filteredPersons.value.length);
 const tableData = computed(() => {
   const start = (page.value - 1) * pageSize.value;
   const persons = filteredPersons.value.slice(start, start + pageSize.value);
-  const rows = [];
 
+  // 农服人员:一人一行,不展开种植记录
+  if (activeTab.value === "service") {
+    return persons.map((person) => ({
+      ...person,
+      rowKey: `s-${person.id}`,
+      location: person.residentLocation,
+      ownerName: person.name,
+      ownerPhone: person.phone,
+    }));
+  }
+
+  const rows = [];
   persons.forEach((person) => {
     const plantings = person.plantings?.length
       ? person.plantings
@@ -380,7 +581,7 @@ const tableData = computed(() => {
         location: person.residentLocation,
         ownerName: person.name,
         ownerPhone: person.phone,
-        categories: [],
+        categories: person.categories || [],
         serviceRecords: [],
       });
     });
@@ -414,47 +615,40 @@ const handleSearch = () => {
 
 /** 排行榜 */
 const rankTabs = [
-  { key: "manage", label: "管理业绩排行" },
+  { key: "manage", label: "团队动态排行" },
   { key: "area", label: "农户种植面积排行" },
   { key: "order", label: "农服服务订单排行" },
 ];
 const activeRankTab = ref("manage");
 
 const buildRankList = (type) => {
-  const names = ["郑以轩", "李明轩", "王晓彤", "赵子涵", "陈思远", "周雨桐"];
+  const names = ["农服", "郑以轩", "郑以轩", "赵子涵", "陈思远", "周雨桐"];
   return names.map((name, idx) => {
     const base = {
       rank: idx + 1,
-      name,
+      name: type === "area" ? ["郑以轩", "李明轩", "王晓彤", "赵子涵", "陈思远", "周雨桐"][idx] : name,
     };
     if (type === "area") {
       return {
         ...base,
-        metricALabel: "种植面积",
-        metricA: `${320 - idx * 28}亩`,
-        metricBLabel: "品种数",
-        metricB: 6 - (idx % 3),
-        metricCLabel: "地块数",
-        metricC: 12 - idx,
-        metricDLabel: "本月新增",
-        metricD: `${8 - idx}亩`,
+        tags: idx % 2 === 0 ? ["荔枝", "龙眼"] : ["荔枝"],
+        subtitle: "位置位置位置位置",
+        value: 2581 - idx * 186,
+        unit: "亩",
       };
     }
     if (type === "order") {
       return {
         ...base,
-        metricALabel: "服务订单",
-        metricA: 86 - idx * 7,
-        metricBLabel: "本月完成",
-        metricB: 18 - idx,
-        metricCLabel: "好评率",
-        metricC: `${98 - idx}%`,
-        metricDLabel: "服务农户",
-        metricD: 40 - idx * 3,
+        tags: ["团队类型"],
+        subtitle: "团队名称",
+        value: 2582 - idx,
+        unit: "单",
       };
     }
     return {
       ...base,
+      name: ["郑以轩", "李明轩", "王晓彤", "赵子涵", "陈思远", "周雨桐"][idx],
       metricALabel: "农户总数",
       metricA: 128 - idx * 9,
       metricBLabel: "本月邀请",
@@ -475,6 +669,13 @@ const rankMap = {
 
 const currentRankList = computed(() => rankMap[activeRankTab.value] || rankMap.manage);
 
+const rankRowMeta = computed(() => {
+  if (activeRankTab.value === "order") {
+    return { infoLabel: "农服信息", valueLabel: "订单量" };
+  }
+  return { infoLabel: "农户信息", valueLabel: "种植面积" };
+});
+
 const handleRankMore = () => ElMessage.info("查看更多排行");
 
 const dialogVisible = ref(false);
@@ -515,6 +716,8 @@ const handleDetailEdit = () => {
 };
 
 const handleDialogSubmit = ({ mode, data }) => {
+  const isService = data.tabKey === "service" || activeTab.value === "service";
+
   const plantings = (data.plantings || []).map((p) => ({
     variety: p.variety,
     phenology: p.phenology || "生育期一",
@@ -541,17 +744,36 @@ const handleDialogSubmit = ({ mode, data }) => {
   if (mode === "edit" && data.id != null) {
     const idx = allList.value.findIndex((item) => item.id === data.id);
     if (idx > -1) {
-      allList.value[idx] = {
-        ...allList.value[idx],
-        name: data.ownerName || data.name || allList.value[idx].name,
-        phone: data.ownerPhone || data.phone || allList.value[idx].phone,
-        residentLocation:
-          data.residentLocation || data.location || allList.value[idx].residentLocation,
-        locationWkt: data.locationWkt || allList.value[idx].locationWkt || "",
-        cropCategory: data.cropCategory || categories[0]?.category || allList.value[idx].cropCategory,
-        categories,
-        plantings: plantings.length ? plantings : allList.value[idx].plantings,
-      };
+      const prev = allList.value[idx];
+      if (isService || prev.tabKey === "service") {
+        allList.value[idx] = {
+          ...prev,
+          name: data.ownerName || data.name || prev.name,
+          phone: data.ownerPhone || data.phone || prev.phone,
+          teamName: data.teamName ?? prev.teamName,
+          teamType: data.teamType ?? prev.teamType,
+          serviceCategories: data.serviceCategories || prev.serviceCategories,
+          equipments: data.equipments || prev.equipments || [],
+          manualServices: data.manualServices || prev.manualServices || [],
+          residentLocation: data.residentLocation || data.location || prev.residentLocation,
+          locationWkt: data.locationWkt || prev.locationWkt || "",
+          inviterName: data.inviterName || prev.inviterName || "张扬",
+        };
+      } else {
+        allList.value[idx] = {
+          ...prev,
+          name: data.ownerName || data.name || prev.name,
+          phone: data.ownerPhone || data.phone || prev.phone,
+          residentLocation: data.residentLocation || data.location || prev.residentLocation,
+          locationWkt: data.locationWkt || prev.locationWkt || "",
+          equipments: data.equipments || prev.equipments || [],
+          outsourceFarmWorks: data.outsourceFarmWorks || prev.outsourceFarmWorks || [],
+          inviterName: data.inviterName || prev.inviterName || "张扬",
+          cropCategory: data.cropCategory || categories[0]?.category || prev.cropCategory,
+          categories,
+          plantings: plantings.length ? plantings : prev.plantings,
+        };
+      }
       if (detailRow.value?.id === data.id) {
         detailRow.value = { ...allList.value[idx] };
       }
@@ -560,6 +782,27 @@ const handleDialogSubmit = ({ mode, data }) => {
   }
 
   const nextId = allList.value.reduce((max, item) => Math.max(max, item.id), 0) + 1;
+  if (isService) {
+    allList.value.unshift({
+      id: nextId,
+      name: data.ownerName || data.name || "新农服",
+      phone: data.ownerPhone || data.phone || "",
+      teamName: data.teamName || "团队名称",
+      teamType: data.teamType || "家庭农场",
+      serviceCategories: data.serviceCategories || {
+        fruit: ["品类一"],
+        field: ["品类一"],
+      },
+      equipments: data.equipments || ["农机设备"],
+      manualServices: data.manualServices || ["农事类别一"],
+      residentLocation: data.residentLocation || data.location || "位置位置位置位置",
+      locationWkt: data.locationWkt || "",
+      inviterName: data.inviterName || "张扬",
+      tabKey: "service",
+    });
+    return;
+  }
+
   allList.value.unshift({
     id: nextId,
     name: data.ownerName || data.name || "新人员",
@@ -575,15 +818,15 @@ const handleDialogSubmit = ({ mode, data }) => {
     plantings: plantings.length
       ? plantings
       : [
-        {
-          variety: "妃子笑",
-          phenology: "生育期一",
-          area: "158亩",
-          startPoint: "来白点",
-          startDate: "2025.05.08",
-          plantLocation: "位置位置位置位置",
-        },
-      ],
+          {
+            variety: "妃子笑",
+            phenology: "生育期一",
+            area: "158亩",
+            startPoint: "来白点",
+            startDate: "2025.05.08",
+            plantLocation: "位置位置位置位置",
+          },
+        ],
   });
 };
 
@@ -845,6 +1088,30 @@ const handleDelete = (row) => {
   line-height: 22px;
 }
 
+.gray-tag {
+  display: inline-block;
+  padding: 2px 8px;
+  border-radius: 4px;
+  background: #f2f3f5;
+  color: #4e5969;
+  font-size: 12px;
+  line-height: 20px;
+}
+
+.service-cat-cell {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 4px 0;
+}
+
+.service-cat-group__label {
+  margin-bottom: 4px;
+  font-size: 12px;
+  color: #86909c;
+  line-height: 18px;
+}
+
 .start-point-cell {
   display: flex;
   align-items: center;
@@ -1013,13 +1280,14 @@ const handleDelete = (row) => {
 .rank-tabs__item {
   padding: 4px 10px;
   border-radius: 4px;
-  background: #F3F3F3;
+  background: #f3f3f3;
   color: rgba(0, 0, 0, 0.6);
   font-size: 12px;
   line-height: 20px;
   border: 1px solid transparent;
   cursor: pointer;
   user-select: none;
+  box-sizing: border-box;
 
   &:hover {
     color: $warning-color;
@@ -1027,12 +1295,38 @@ const handleDelete = (row) => {
 
   &.is-active {
     color: $warning-color;
-    background: rgba($warning-color, 0.12);
+    background: rgba($warning-color, 0.08);
     font-weight: 500;
     border-color: $warning-color;
   }
 }
 
+.rank-list-head {
+  display: flex;
+  align-items: center;
+  padding: 0 12px 10px;
+  font-size: 12px;
+  color: #86909c;
+  flex-shrink: 0;
+}
+
+.rank-list-head__rank {
+  width: 40px;
+  flex-shrink: 0;
+}
+
+.rank-list-head__info {
+  flex: 1;
+  min-width: 0;
+  padding-left: 4px;
+}
+
+.rank-list-head__value {
+  width: 72px;
+  flex-shrink: 0;
+  text-align: right;
+}
+
 .rank-list {
   flex: 1;
   min-height: 0;
@@ -1042,6 +1336,119 @@ const handleDelete = (row) => {
   gap: 16px;
 }
 
+.rank-list--row {
+  gap: 10px;
+}
+
+.rank-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 12px;
+  border-radius: 8px;
+  background: #f5f6f8;
+  border: 1px solid transparent;
+  box-sizing: border-box;
+
+  &.is-top-1 {
+    background: linear-gradient(90deg, #fff0d2 0%, rgba(255, 240, 210, 0) 100%);
+    border-color: #ffe6b8;
+  }
+
+  &.is-top-2 {
+    background: linear-gradient(90deg, #ebeff3 0%, rgba(235, 239, 243, 0) 100%);
+    border-color: #dde3ea;
+  }
+
+  &.is-top-3 {
+    background: linear-gradient(90deg, #ffede5 0%, rgba(255, 237, 229, 0) 100%);
+    border-color: #ffe1d2;
+  }
+}
+
+.rank-row__rank {
+  width: 28px;
+  flex-shrink: 0;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: 600;
+  color: #1d2129;
+}
+
+.rank-row__rank-img {
+  display: block;
+  width: 28px;
+  height: 28px;
+  object-fit: contain;
+}
+
+.rank-row__avatar {
+  flex-shrink: 0;
+  background: #ffe4c4;
+  color: #c07020;
+  font-size: 13px;
+}
+
+.rank-row__info {
+  flex: 1;
+  min-width: 0;
+}
+
+.rank-row__name-line {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.rank-row__name {
+  font-size: 16px;
+  color: #1D2129;
+  line-height: 22px;
+}
+
+.rank-row__tag {
+  display: inline-block;
+  padding: 0 6px;
+  border-radius: 2px;
+  background: #fff;
+  color: #494949;
+  font-size: 12px;
+  height: 20px;
+  line-height: 20px;
+}
+
+.rank-row__location {
+  margin-top: 2px;
+  font-size: 12px;
+  line-height: 18px;
+  color: #4E5969;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.rank-row__value {
+  flex-shrink: 0;
+  display: flex;
+  align-items: baseline;
+  gap: 2px;
+  color: #1d2129;
+}
+
+.rank-row__num {
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.rank-row__unit {
+  font-size: 12px;
+  color: #4e5969;
+}
+
 .rank-item {
   display: flex;
   flex-direction: column;