Forráskód Böngészése

fix: 主体管理页面,表格

lxf 2 hete
szülő
commit
d962c67242

BIN
src/assets/page/person.png


BIN
src/assets/page/rank-1.png


BIN
src/assets/page/rank-2.png


BIN
src/assets/page/rank-3.png


+ 8 - 3
src/layout/menu.js

@@ -1,6 +1,6 @@
 export const sideMenus = [
   {
-    path: "/device",
+    path: "/personnel",
     title: "设备管理",
     icon: "device",
   },
@@ -10,8 +10,13 @@ export const sideMenus = [
     icon: "planting",
   },
   {
-    path: "/personnel",
-    title: "人员管理",
+    path: "/device",
+    title: "主体管理",
     icon: "personnel",
   },
+  // {
+  //   path: "/personnel",
+  //   title: "人员管理",
+  //   icon: "personnel",
+  // },
 ];

+ 690 - 223
src/views/device/index.vue

@@ -1,145 +1,253 @@
 <template>
   <div class="device-page">
-    <div class="device-panel">
-
-      <!-- 筛选 -->
-      <div class="filter-bar">
-        <div class="filter-l">
-          <span class="filter-bar__label">搜索姓名</span>
-          <el-select
-            v-model="deviceName"
-            class="filter-bar__select"
-            clearable
-            filterable
-            placeholder="请选择姓名"
-          >
-            <el-option
-              v-for="item in deviceNameOptions"
-              :key="item"
-              :label="item"
-              :value="item"
-            />
-          </el-select>
-        </div>
-        <div class="filter-r">
-          <el-button class="btn-reset" @click="handleReset">重置</el-button>
-          <el-button class="btn-search" type="warning" @click="handleSearch">搜索</el-button>
-        </div>
-      </div>
-
-      <div class="device-content">
-        <div class="toolbar">
-          <div class="tabs">
-            <div v-for="tab in tabs" :key="tab.key" class="tabs__item" :class="{ 'is-active': activeTab === tab.key }"
-              @click="handleTabChange(tab.key)">
-              {{ tab.label }}({{ tab.count }})
+    <div class="device-layout">
+      <!-- 左侧 -->
+      <div class="device-main">
+        <!-- 上:统计卡片 -->
+        <div class="stat-row">
+          <div v-for="item in statCards" :key="item.key" class="stat-card">
+            <div class="stat-card__body">
+              <div class="stat-card__title">{{ item.title }}</div>
+              <div class="stat-card__value">{{ item.value }}</div>
+              <div class="stat-card__extra">
+                {{ item.extra }}
+                <span class="extra-value">{{ item.extraValue }}</span>
+                <span class="extra-unit">人</span>
+              </div>
+            </div>
+            <div class="stat-card__icon">
+              <img class="icon-img" src="@/assets/page/person.png" alt="">
             </div>
           </div>
-          <el-button class="btn-add" type="warning" @click="handleAdd">新增设备</el-button>
         </div>
 
-        <el-table
-          :data="tableData"
-          :span-method="objectSpanMethod"
-          class="device-table"
-          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 class="device-content">
+          <div class="toolbar">
+            <div class="tabs">
+              <div v-for="tab in tabs" :key="tab.key" class="tabs__item" :class="{ 'is-active': activeTab === tab.key }"
+                @click="handleTabChange(tab.key)">
+                {{ tab.label }}({{ tab.count }})
               </div>
-            </template>
-          </el-table-column>
+            </div>
+            <div class="toolbar__search">
+              <el-input v-model="deviceName" class="search-input" clearable placeholder="请输入农户姓名/农服姓名"
+                @keyup.enter="handleSearch" />
+              <el-button class="btn-search" type="warning" @click="handleSearch">搜索</el-button>
+            </div>
+          </div>
+
+          <div class="table-wrap">
+            <el-table
+              :data="tableData"
+              :span-method="objectSpanMethod"
+              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="cropCategory" label="种植品类" min-width="100" show-overflow-tooltip />
+
+              <el-table-column label="种植品种" min-width="110">
+                <template #default="{ row }">
+                  <span class="variety-tag">{{ row.variety }}</span>
+                </template>
+              </el-table-column>
+
+              <el-table-column prop="phenology" label="物候期" min-width="110" />
+
+              <el-table-column prop="area" label="种植面积" min-width="100" />
+
+              <el-table-column label="起种点" min-width="170">
+                <template #default="{ row }">
+                  <div class="start-point-cell">
+                    <span class="start-point-tag">{{ row.startPoint }}</span>
+                    <span class="start-point-date">{{ row.startDate }}</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.plantLocation">{{ row.plantLocation }}</span>
+                    <el-button link type="warning" class="link-btn" @click="handleViewPlantLocation(row)">
+                      查看点位
+                    </el-button>
+                  </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="(farm, idx) in row.outsourceFarmWorks || []"
+                      :key="idx"
+                      class="farm-tag"
+                    >
+                      {{ farm }}
+                    </span>
+                    <span v-if="!(row.outsourceFarmWorks && row.outsourceFarmWorks.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>
 
-          <el-table-column prop="cropCategory" label="种植品类" min-width="100" show-overflow-tooltip />
+          <div class="pager">
+            <span class="pager__total">共 {{ total }} 项数据</span>
+            <el-pagination v-model:current-page="page" v-model:page-size="pageSize" background
+              layout="sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50]" :total="total" :pager-count="7" />
+          </div>
+        </div>
+      </div>
 
-          <el-table-column label="种植品种" min-width="110">
-            <template #default="{ row }">
-              <span class="variety-tag">{{ row.variety }}</span>
-            </template>
-          </el-table-column>
+      <!-- 右侧:排行榜 -->
+      <aside class="rank-panel">
+        <div class="rank-panel__head">
+          <span class="rank-panel__title">排行榜</span>
+          <el-button link type="warning" class="rank-more" @click="handleRankMore">更多 &gt;</el-button>
+        </div>
 
-          <el-table-column prop="phenology" label="物候期" min-width="110" />
+        <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">
+            {{ tab.label }}
+          </div>
+        </div>
 
-          <el-table-column prop="area" label="种植面积" min-width="100" />
+        <div class="rank-list">
+          <div
+            v-for="item in currentRankList"
+            :key="item.rank"
+            class="rank-item"
+            :class="[`is-top-${Math.min(item.rank, 4)}`, { 'is-top': item.rank <= 3 }]"
+          >
+            <div class="rank-item__head">
+              <span class="rank-item__rank" :class="`rank-item__rank--${item.rank}`">
+                <img
+                  v-if="item.rank <= 3"
+                  class="rank-item__rank-img"
+                  :src="rankIcons[item.rank]"
+                  :alt="`第${item.rank}名`"
+                />
+                <template v-else>{{ item.rank }}</template>
+              </span>
+              <el-avatar :size="40" class="rank-item__avatar">
+                {{ item.name.slice(0, 1) }}
+              </el-avatar>
+              <span class="rank-item__name">{{ item.name }}</span>
+            </div>
 
-          <el-table-column label="起种点" min-width="170">
-            <template #default="{ row }">
-              <div class="start-point-cell">
-                <span class="start-point-tag">{{ row.startPoint }}</span>
-                <span class="start-point-date">{{ row.startDate }}</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.plantLocation">{{ row.plantLocation }}</span>
-                <el-button link type="warning" class="link-btn" @click="handleViewPlantLocation(row)">
-                  查看点位
-                </el-button>
-              </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 class="rank-item__stats">
+              <div class="rank-stat-card">
+                <div class="rank-stat">
+                  <div class="rank-stat__value">{{ item.metricA }}</div>
+                  <div class="rank-stat__label">{{ item.metricALabel }}</div>
+                </div>
+                <div class="rank-stat">
+                  <div class="rank-stat__value">{{ item.metricB }}</div>
+                  <div class="rank-stat__label">{{ item.metricBLabel }}</div>
+                </div>
               </div>
-            </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 class="rank-stat-card">
+                <div class="rank-stat">
+                  <div class="rank-stat__value">{{ item.metricC }}</div>
+                  <div class="rank-stat__label">{{ item.metricCLabel }}</div>
+                </div>
+                <div class="rank-stat">
+                  <div class="rank-stat__value">{{ item.metricD }}</div>
+                  <div class="rank-stat__label">{{ item.metricDLabel }}</div>
+                </div>
               </div>
-            </template>
-          </el-table-column>
-        </el-table>
-
-        <div class="pager">
-          <span class="pager__total">共 {{ total }} 项数据</span>
-          <el-pagination v-model:current-page="page" v-model:page-size="pageSize" background
-            layout="sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50]" :total="total" :pager-count="7" />
+            </div>
+          </div>
         </div>
-      </div>
-
+      </aside>
     </div>
   </div>
 
-  <DeviceFormDialog
-    v-model="dialogVisible"
-    :mode="dialogMode"
-    :data="editingRow"
-    @submit="handleDialogSubmit"
-  />
-
-  <DeviceDetailDialog
-    v-model="detailVisible"
-    :data="detailRow"
-    @edit="handleDetailEdit"
-  />
+  <DeviceFormDialog v-model="dialogVisible" :mode="dialogMode" :data="editingRow" @submit="handleDialogSubmit" />
+
+  <DeviceDetailDialog v-model="detailVisible" :data="detailRow" @edit="handleDetailEdit" />
 </template>
 
 <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";
+import rank2Icon from "@/assets/page/rank-2.png";
+import rank3Icon from "@/assets/page/rank-3.png";
+
+const rankIcons = {
+  1: rank1Icon,
+  2: rank2Icon,
+  3: rank3Icon,
+};
 
 const NAMES = ["张洋", "李明", "王芳", "赵强", "陈丽"];
 const VARIETIES = [
@@ -152,7 +260,6 @@ const VARIETIES = [
 const buildMockRows = (count = 40) => {
   const list = [];
   for (let i = 1; i <= count; i++) {
-    // 保证首条与设计图一致:一人 + 一品类 + 多品种
     const varieties = i === 1 ? ["妃子笑", "桂味"] : VARIETIES[i % VARIETIES.length];
     list.push({
       id: i,
@@ -160,6 +267,9 @@ const buildMockRows = (count = 40) => {
       phone: i === 1 ? "19871533168" : `1987153${String(3160 + (i % 40)).padStart(4, "0")}`,
       cropCategory: "荔枝",
       residentLocation: "位置位置位置位置",
+      equipments: ["农机设备"],
+      outsourceFarmWorks: ["农事类别一"],
+      inviterName: "张扬",
       tabKey: i % 2 === 0 ? "service" : "owner",
       plantings: varieties.map((variety) => ({
         variety,
@@ -189,28 +299,65 @@ const tabs = computed(() => [
   },
 ]);
 
+const ownerCount = computed(() => allList.value.filter((i) => i.tabKey === "owner").length);
+const serviceCount = computed(() => allList.value.filter((i) => i.tabKey === "service").length);
+
+const statCards = computed(() => [
+  {
+    key: "farmer",
+    title: "农户总数",
+    value: ownerCount.value,
+    extra: "昨日新增",
+    extraValue: "8",
+    iconBg: "rgba(251, 142, 51, 0.12)",
+    iconColor: "#FB8E33",
+  },
+  {
+    key: "service",
+    title: "农服人员总数",
+    value: serviceCount.value,
+    extra: "昨日新增",
+    extraValue: "5",
+    iconBg: "rgba(251, 142, 51, 0.12)",
+    iconColor: "#FB8E33",
+  },
+  {
+    key: "owner",
+    title: "权属人员总数",
+    value: ownerCount.value,
+    extra: "本月新增",
+    extraValue: "12",
+    iconBg: "rgba(251, 142, 51, 0.12)",
+    iconColor: "#FB8E33",
+  },
+  {
+    key: "area",
+    title: "种植总面积(亩)",
+    value: 12580,
+    extra: "本月增加",
+    extraValue: "120",
+    iconBg: "rgba(251, 142, 51, 0.12)",
+    iconColor: "#FB8E33",
+  },
+]);
+
 const activeTab = ref("owner");
 const page = ref(1);
 const pageSize = ref(20);
 const deviceName = ref("");
 const appliedDeviceName = ref("");
 
-const deviceNameOptions = computed(() => {
-  return [...new Set(allList.value.map((item) => item.name))];
-});
-
-/** 人员维度过滤 */
 const filteredPersons = computed(() => {
   let list = allList.value.filter((item) => item.tabKey === activeTab.value);
-  if (appliedDeviceName.value) {
-    list = list.filter((item) => item.name === appliedDeviceName.value);
+  const keyword = appliedDeviceName.value.trim();
+  if (keyword) {
+    list = list.filter((item) => item.name.includes(keyword));
   }
   return list;
 });
 
 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);
@@ -228,7 +375,6 @@ const tableData = computed(() => {
         rowKey: `${person.id}-${idx}`,
         plantIndex: idx,
         plantCount: plantings.length,
-        // 兼容原弹窗字段
         code: person.code || "农场-类型-ID",
         type: person.cropCategory,
         location: person.residentLocation,
@@ -243,18 +389,16 @@ const tableData = computed(() => {
   return rows;
 });
 
-/** 需合并的列:姓名/手机号、种植品类、常驻点位、操作 */
-const MERGE_COLUMN_INDEXES = new Set([0, 1, 7, 8]);
+/** 需合并的列:个人信息、种植品类、农机设备、外包农事、常驻点位、邀请人、操作 */
+const MERGE_COLUMN_INDEXES = new Set([0, 1, 7, 8, 9, 10, 11]);
 
 const objectSpanMethod = ({ row, columnIndex }) => {
   if (!MERGE_COLUMN_INDEXES.has(columnIndex)) {
     return { rowspan: 1, colspan: 1 };
   }
-
   if (row.plantIndex === 0) {
     return { rowspan: row.plantCount, colspan: 1 };
   }
-
   return { rowspan: 0, colspan: 0 };
 };
 
@@ -268,24 +412,77 @@ const handleSearch = () => {
   page.value = 1;
 };
 
-const handleReset = () => {
-  deviceName.value = "";
-  appliedDeviceName.value = "";
-  page.value = 1;
+/** 排行榜 */
+const rankTabs = [
+  { key: "manage", label: "管理业绩排行" },
+  { key: "area", label: "农户种植面积排行" },
+  { key: "order", label: "农服服务订单排行" },
+];
+const activeRankTab = ref("manage");
+
+const buildRankList = (type) => {
+  const names = ["郑以轩", "李明轩", "王晓彤", "赵子涵", "陈思远", "周雨桐"];
+  return names.map((name, idx) => {
+    const base = {
+      rank: idx + 1,
+      name,
+    };
+    if (type === "area") {
+      return {
+        ...base,
+        metricALabel: "种植面积",
+        metricA: `${320 - idx * 28}亩`,
+        metricBLabel: "品种数",
+        metricB: 6 - (idx % 3),
+        metricCLabel: "地块数",
+        metricC: 12 - idx,
+        metricDLabel: "本月新增",
+        metricD: `${8 - idx}亩`,
+      };
+    }
+    if (type === "order") {
+      return {
+        ...base,
+        metricALabel: "服务订单",
+        metricA: 86 - idx * 7,
+        metricBLabel: "本月完成",
+        metricB: 18 - idx,
+        metricCLabel: "好评率",
+        metricC: `${98 - idx}%`,
+        metricDLabel: "服务农户",
+        metricD: 40 - idx * 3,
+      };
+    }
+    return {
+      ...base,
+      metricALabel: "农户总数",
+      metricA: 128 - idx * 9,
+      metricBLabel: "本月邀请",
+      metricB: 16 - idx,
+      metricCLabel: "农服总数",
+      metricC: 56 - idx * 4,
+      metricDLabel: "本月邀请",
+      metricD: 9 - idx,
+    };
+  });
+};
+
+const rankMap = {
+  manage: buildRankList("manage"),
+  area: buildRankList("area"),
+  order: buildRankList("order"),
 };
 
+const currentRankList = computed(() => rankMap[activeRankTab.value] || rankMap.manage);
+
+const handleRankMore = () => ElMessage.info("查看更多排行");
+
 const dialogVisible = ref(false);
 const dialogMode = ref("add");
 const editingRow = ref(null);
 const detailVisible = ref(false);
 const detailRow = ref(null);
 
-const handleAdd = () => {
-  dialogMode.value = "add";
-  editingRow.value = null;
-  dialogVisible.value = true;
-};
-
 const handleEdit = (row) => {
   const person = allList.value.find((item) => item.id === row.id) || row;
   dialogMode.value = "edit";
@@ -302,7 +499,6 @@ const handleDetail = (row) => {
   const person = allList.value.find((item) => item.id === row.id) || row;
   detailRow.value = {
     ...person,
-    // 兼容弹窗字段
     code: person.code || "农场-类型-ID",
     type: person.cropCategory,
     location: person.residentLocation,
@@ -371,20 +567,23 @@ const handleDialogSubmit = ({ mode, data }) => {
     cropCategory: data.cropCategory || categories[0]?.category || "荔枝",
     residentLocation: data.residentLocation || data.location || "位置位置位置位置",
     locationWkt: data.locationWkt || "",
+    equipments: data.equipments || ["农机设备"],
+    outsourceFarmWorks: data.outsourceFarmWorks || ["农事类别一"],
+    inviterName: data.inviterName || "张扬",
     tabKey: activeTab.value,
     categories,
     plantings: plantings.length
       ? plantings
       : [
-          {
-            variety: "妃子笑",
-            phenology: "生育期一",
-            area: "158亩",
-            startPoint: "来白点",
-            startDate: "2025.05.08",
-            plantLocation: "位置位置位置位置",
-          },
-        ],
+        {
+          variety: "妃子笑",
+          phenology: "生育期一",
+          area: "158亩",
+          startPoint: "来白点",
+          startDate: "2025.05.08",
+          plantLocation: "位置位置位置位置",
+        },
+      ],
   });
 };
 
@@ -401,98 +600,126 @@ const handleDelete = (row) => {
       allList.value = allList.value.filter((item) => item.id !== row.id);
       ElMessage.success("已删除");
     })
-    .catch(() => {});
+    .catch(() => { });
 };
 </script>
 
 <style lang="scss" scoped>
 .device-page {
   box-sizing: border-box;
-  min-height: 100%;
-  padding: 25px;
+  height: 100%;
+  overflow: hidden;
+  padding: 20px 24px;
+  display: flex;
+  flex-direction: column;
 }
 
-.device-panel {
-  min-height: calc(100% - 0px);
+.device-layout {
+  flex: 1;
+  min-height: 0;
   display: flex;
-  flex-direction: column;
+  align-items: stretch;
+  gap: 16px;
 }
 
-.device-content {
-  background: #fff;
-  border-radius: 5px;
-  padding: 0 10px;
+.device-main {
+  flex: 1;
+  min-width: 0;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
 
+/* 统计卡片 */
+.stat-row {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 12px;
+  flex-shrink: 0;
 }
 
-.filter-bar {
+.stat-card {
   display: flex;
-  align-items: center;
-  gap: 30px;
-  padding: 0 12px;
-  border-radius: 5px;
-  height: 72px;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 10px;
+  padding: 10px 16px;
   background: #fff;
-  margin-bottom: 20px;
-}
-.filter-r {
-  display: flex;
-  align-items: center;
-  gap: 12px;
+  box-sizing: border-box;
 }
 
-.filter-bar__label {
-  flex-shrink: 0;
+.stat-card__title {
   font-size: 14px;
-  color: #333;
-  padding-right: 8px;
+  color: #1D2129;
+  line-height: 22px;
 }
 
-.filter-bar__select {
-  width: 220px;
+.stat-card__value {
+  margin-top: 12px;
+  font-size: 24px;
+  font-weight: 500;
+  color: #1D2129;
+  line-height: 32px;
 }
 
-.btn-reset {
-  height: 32px;
-  padding: 0 18px;
-  color: $warning-color;
-  background: #fff;
-  border-color: $warning-color;
-  border-radius: 4px;
+.stat-card__extra {
+  margin-top: 4px;
+  font-size: 13px;
+  color: #4E5969;
+  line-height: 22px;
 
-  &:hover,
-  &:focus {
-    color: #fff;
-    background: $warning-color;
-    border-color: $warning-color;
+  .extra-value {
+    font-weight: 500;
+    font-size: 14px;
+    padding: 0 4px;
+    color: #23C343;
   }
 }
 
-.btn-search {
-  // --el-button-bg-color: #{$warning-color};
-  // --el-button-border-color: #{$warning-color};
-  // --el-button-hover-bg-color: #{mix(#fff, $warning-color, 12%)};
-  // --el-button-hover-border-color: #{mix(#fff, $warning-color, 12%)};
-  // --el-button-active-bg-color: #{mix(#000, $warning-color, 8%)};
-  // --el-button-active-border-color: #{mix(#000, $warning-color, 8%)};
-  height: 32px;
-  padding: 0 18px;
+.stat-card__icon {
+  width: 44px;
+  height: 44px;
   border-radius: 4px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  background: rgba(251, 142, 51, 0.1);
+
+  .icon-img {
+    width: 24px;
+    object-fit: contain;
+  }
+}
+
+/* 列表区 */
+.device-content {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 8px;
+  padding: 0 12px 8px;
+  overflow: hidden;
 }
 
 .toolbar {
   display: flex;
   align-items: center;
   justify-content: space-between;
+  gap: 16px;
   height: 56px;
   border-bottom: 1px solid #f0f0f0;
+  flex-shrink: 0;
 }
 
 .tabs {
   display: flex;
   align-items: stretch;
   height: 100%;
-  gap: 28px;
+  gap: 8px;
 }
 
 .tabs__item {
@@ -504,7 +731,7 @@ const handleDelete = (row) => {
   color: #666;
   cursor: pointer;
   user-select: none;
-  padding: 0 16px;
+  padding: 0 14px;
 
   &:hover {
     color: $warning-color;
@@ -526,28 +753,33 @@ const handleDelete = (row) => {
   }
 }
 
-.btn-add {
-  // --el-button-bg-color: #{$warning-color};
-  // --el-button-border-color: #{$warning-color};
-  // --el-button-hover-bg-color: #{mix(#fff, $warning-color, 12%)};
-  // --el-button-hover-border-color: #{mix(#fff, $warning-color, 12%)};
-  // --el-button-active-bg-color: #{mix(#000, $warning-color, 8%)};
-  // --el-button-active-border-color: #{mix(#000, $warning-color, 8%)};
-  height: 36px;
+.toolbar__search {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.search-input {
+  width: 240px;
+}
+
+.btn-search {
+  height: 32px;
   padding: 0 18px;
   border-radius: 4px;
 }
 
-.device-table {
+.table-wrap {
   flex: 1;
+  min-height: 0;
+  margin-top: 12px;
+}
+
+.device-table {
   width: 100%;
-  margin-top: 20px;
 
   :deep(.device-table__header) {
-    &:nth-child(2) {
-      color: #818080;
-    }
-    background: #F5F5F5 !important;
+    background: #f5f5f5 !important;
     color: #535353;
     font-weight: 500;
   }
@@ -560,12 +792,6 @@ const handleDelete = (row) => {
   :deep(.el-table__body .cell) {
     padding: 0 12px;
   }
-
-  :deep(.el-checkbox__input.is-checked .el-checkbox__inner),
-  :deep(.el-checkbox__input.is-indeterminate .el-checkbox__inner) {
-    background-color: $warning-color;
-    border-color: $warning-color;
-  }
 }
 
 .person-cell {
@@ -593,6 +819,32 @@ const handleDelete = (row) => {
   line-height: 22px;
 }
 
+.tag-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.equip-tag {
+  display: inline-block;
+  padding: 2px 10px;
+  border-radius: 4px;
+  background: rgba($warning-color, 0.12);
+  color: $warning-color;
+  font-size: 12px;
+  line-height: 22px;
+}
+
+.farm-tag {
+  display: inline-block;
+  padding: 2px 10px;
+  border-radius: 4px;
+  background: rgba(64, 158, 255, 0.12);
+  color: #409eff;
+  font-size: 12px;
+  line-height: 22px;
+}
+
 .start-point-cell {
   display: flex;
   align-items: center;
@@ -631,6 +883,28 @@ const handleDelete = (row) => {
   font-size: 14px;
 }
 
+.inviter-cell {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+}
+
+.inviter-cell__avatar {
+  flex-shrink: 0;
+  background: #ffe4c4;
+  color: #c07020;
+  font-size: 12px;
+}
+
+.inviter-cell__name {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  color: #535353;
+  font-size: 14px;
+}
+
 .link-btn {
   padding: 0;
   height: auto;
@@ -675,7 +949,8 @@ const handleDelete = (row) => {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 16px 34px;
+  padding: 12px 16px;
+  flex-shrink: 0;
 }
 
 .pager__total {
@@ -694,4 +969,196 @@ const handleDelete = (row) => {
 :deep(.el-pagination__jump) {
   margin-left: 12px;
 }
+
+/* 右侧排行榜 */
+.rank-panel {
+  width: 450px;
+  flex-shrink: 0;
+  min-height: 0;
+  background: #fff;
+  border-radius: 6px;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.rank-panel__head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.rank-panel__title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #1D2129;
+}
+
+.rank-more {
+  padding: 0;
+  height: auto;
+  font-size: 13px;
+}
+
+.rank-tabs {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 14px;
+}
+
+.rank-tabs__item {
+  padding: 4px 10px;
+  border-radius: 4px;
+  background: #F3F3F3;
+  color: rgba(0, 0, 0, 0.6);
+  font-size: 12px;
+  line-height: 20px;
+  border: 1px solid transparent;
+  cursor: pointer;
+  user-select: none;
+
+  &:hover {
+    color: $warning-color;
+  }
+
+  &.is-active {
+    color: $warning-color;
+    background: rgba($warning-color, 0.12);
+    font-weight: 500;
+    border-color: $warning-color;
+  }
+}
+
+.rank-list {
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.rank-item {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  padding: 12px;
+  border-radius: 10px;
+  background: rgba(209, 209, 209, 0.13);
+  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-item__head {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  min-width: 0;
+}
+
+.rank-item__rank {
+  width: 28px;
+  flex-shrink: 0;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  font-size: 18px;
+  font-weight: 600;
+  line-height: 1;
+  color: #4e5969;
+}
+
+.rank-item__rank-img {
+  display: block;
+  width: 30px;
+  height: 30px;
+  object-fit: contain;
+}
+
+.rank-item__avatar {
+  flex-shrink: 0;
+  background: #ffe4c4;
+  color: #c07020;
+  font-size: 14px;
+}
+
+.rank-item__name {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 14px;
+  font-weight: 600;
+  color: #1d2129;
+}
+
+.rank-item__stats {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+
+.rank-stat-card {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 4px;
+  padding: 10px 6px;
+  background: #fff;
+  border-radius: 8px;
+  box-sizing: border-box;
+}
+
+.rank-stat {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  min-width: 0;
+}
+
+.rank-stat__value {
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 24px;
+  color: #1d2129;
+}
+
+.rank-stat__label {
+  margin-top: 2px;
+  font-size: 12px;
+  line-height: 18px;
+  color: #86909c;
+}
+
+@media (max-width: 1400px) {
+  .rank-panel {
+    width: 320px;
+  }
+
+  .stat-row {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
 </style>