Ver código fonte

Merge branch 'master' of http://www.sysuimars.cn:3000/feiniao/feiniao-be-data

wangsisi 1 semana atrás
pai
commit
41c8342cad

+ 9 - 0
src/styles/common.scss

@@ -1,4 +1,13 @@
 :root {
+  /* Element Plus primary 主题色,参考 https://element-plus.org/zh-CN/guide/theming */
+  --el-color-primary: #{$base-color};
+  --el-color-primary-light-3: #{color.mix(#fff, $base-color, 30%)};
+  --el-color-primary-light-5: #{color.mix(#fff, $base-color, 50%)};
+  --el-color-primary-light-7: #{color.mix(#fff, $base-color, 70%)};
+  --el-color-primary-light-8: #{color.mix(#fff, $base-color, 80%)};
+  --el-color-primary-light-9: #{color.mix(#fff, $base-color, 90%)};
+  --el-color-primary-dark-2: #{color.mix(#000, $base-color, 20%)};
+
   --el-color-warning: #{$warning-color};
   --el-color-warning-light-3: #{color.mix(#fff, $warning-color, 30%)};
   --el-color-warning-light-5: #{color.mix(#fff, $warning-color, 50%)};

+ 1 - 1
src/styles/variables.scss.js

@@ -1,6 +1,6 @@
 const variables = {
   main_bg_color: "#f5f5f5",
-  base_color: "#d4af37",
+  base_color: "#FB8E33",
   warning_color: "#FB8E33",
   nav_height: "64px",
   side_close_width: "64px",

+ 306 - 0
src/views/device/DeviceFormDialog.vue

@@ -0,0 +1,306 @@
+<template>
+  <el-dialog
+    :model-value="modelValue"
+    :title="isEdit ? '编辑设备' : '新增设备'"
+    width="520px"
+    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-width="96px"
+      label-position="right"
+      class="device-form"
+    >
+      <el-form-item label="农机名称" prop="name">
+        <el-input
+          v-model="form.name"
+          :placeholder="isEdit ? '' : '请输入农机'"
+          maxlength="50"
+        />
+      </el-form-item>
+
+      <el-form-item label="农机类型" prop="type">
+        <el-select
+          v-model="form.type"
+          placeholder="请选择农机类型"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in typeOptions"
+            :key="item"
+            :label="item"
+            :value="item"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="农事类别" prop="categories">
+        <el-select
+          v-model="form.categories"
+          multiple
+          collapse-tags
+          collapse-tags-tooltip
+          :max-collapse-tags="3"
+          placeholder="请选择农事类别"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in categoryOptions"
+            :key="item"
+            :label="item"
+            :value="item"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="存放点位" prop="location" required>
+        <div class="field-block">
+          <el-input
+            v-if="isEdit"
+            v-model="form.location"
+            placeholder="请选择存放点位"
+            readonly
+          />
+          <el-button class="btn-outline" @click="handleSelectLocation">
+            {{ isEdit ? "修改点位" : "选择点位" }}
+          </el-button>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="执行区域" prop="areaThumb" required>
+        <div class="field-block">
+          <div v-if="isEdit" class="area-preview">
+            执行区域区域地图地图地图地图
+          </div>
+          <el-button class="btn-outline" @click="handleSelectArea">
+            {{ isEdit ? "修改区域" : "选择区域" }}
+          </el-button>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="负责人" prop="ownerName">
+        <el-input
+          v-model="form.ownerName"
+          :placeholder="isEdit ? '' : '请输入姓名'"
+          maxlength="20"
+        />
+      </el-form-item>
+
+      <el-form-item label="手机号" prop="ownerPhone">
+        <el-input
+          v-model="form.ownerPhone"
+          :placeholder="isEdit ? '' : '请输入手机号'"
+          maxlength="11"
+        />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="close">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">
+          提交
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, reactive, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false,
+  },
+  mode: {
+    type: String,
+    default: "add", // add | edit
+  },
+  data: {
+    type: Object,
+    default: null,
+  },
+});
+
+const emit = defineEmits(["update:modelValue", "submit"]);
+
+const isEdit = computed(() => props.mode === "edit");
+const formRef = ref();
+const submitting = ref(false);
+
+const typeOptions = ["肥水一体", "水肥一体", "植保无人机", "收割机", "播种机"];
+const categoryOptions = ["类别一", "类别二", "类别三", "类别四", "类别五", "类别六", "类别七"];
+
+const createEmptyForm = () => ({
+  id: null,
+  name: "",
+  type: "",
+  categories: [],
+  location: "",
+  areaThumb: "",
+  ownerName: "",
+  ownerPhone: "",
+});
+
+const form = reactive(createEmptyForm());
+
+const rules = {
+  name: [{ required: true, message: "请输入农机名称", trigger: "blur" }],
+  type: [{ required: true, message: "请选择农机类型", trigger: "change" }],
+  categories: [
+    {
+      type: "array",
+      required: true,
+      min: 1,
+      message: "请选择农事类别",
+      trigger: "change",
+    },
+  ],
+  location: [
+    {
+      validator: (_rule, _value, callback) => {
+        if (!form.location) callback(new Error("请选择存放点位"));
+        else callback();
+      },
+      trigger: "change",
+    },
+  ],
+  areaThumb: [
+    {
+      validator: (_rule, _value, callback) => {
+        if (!form.areaThumb) callback(new Error("请选择执行区域"));
+        else callback();
+      },
+      trigger: "change",
+    },
+  ],
+  ownerName: [{ required: true, message: "请输入负责人", trigger: "blur" }],
+  ownerPhone: [
+    { required: true, message: "请输入手机号", trigger: "blur" },
+    {
+      pattern: /^1\d{10}$/,
+      message: "请输入正确的手机号",
+      trigger: "blur",
+    },
+  ],
+};
+
+const fillForm = (data) => {
+  Object.assign(form, createEmptyForm(), {
+    id: data?.id ?? null,
+    name: data?.name || "",
+    type: data?.type || "",
+    categories: [...(data?.categories || [])],
+    location: data?.location || "",
+    areaThumb: data?.areaThumb,
+    ownerName: data?.ownerName || "",
+    ownerPhone: data?.ownerPhone || "",
+  });
+};
+
+watch(
+  () => props.modelValue,
+  (visible) => {
+    if (!visible) return;
+    if (isEdit.value && props.data) {
+      fillForm(props.data);
+    } else {
+      Object.assign(form, createEmptyForm());
+    }
+  }
+);
+
+const close = () => emit("update:modelValue", false);
+
+const handleClosed = () => {
+  submitting.value = false;
+  formRef.value?.resetFields?.();
+  Object.assign(form, createEmptyForm());
+};
+
+const handleSelectLocation = () => {
+  form.location = form.location || "位置位置位置位置位置";
+  ElMessage.success(isEdit.value ? "已修改点位(占位)" : "已选择点位(占位)");
+  formRef.value?.validateField?.("location");
+};
+
+const handleSelectArea = () => {
+  ElMessage.success(isEdit.value ? "已修改区域(占位)" : "已选择区域(占位)");
+  formRef.value?.validateField?.("areaThumb");
+};
+
+const handleSubmit = async () => {
+  await formRef.value?.validate?.(async (valid) => {
+    if (!valid) return;
+    submitting.value = true;
+    try {
+      const payload = {
+        id: form.id,
+        name: form.name,
+        type: form.type,
+        categories: [...form.categories],
+        location: form.location,
+        areaThumb: form.areaThumb,
+        ownerName: form.ownerName,
+        ownerPhone: form.ownerPhone,
+      };
+      emit("submit", { mode: props.mode, data: payload });
+      ElMessage.success(isEdit.value ? "编辑成功" : "新增成功");
+      close();
+    } finally {
+      submitting.value = false;
+    }
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.device-form {
+  padding: 8px 24px 0 8px;
+
+  :deep(.el-form-item) {
+    margin-bottom: 24px;
+  }
+}
+
+.field-block {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 10px;
+  width: 100%;
+}
+
+.btn-outline {
+  height: 32px;
+  padding: 0 16px;
+  color: $base-color;
+  background: #fff;
+  border-color: $base-color;
+  border-radius: 4px;
+
+  &:hover,
+  &:focus {
+    color: #fff;
+    background: $base-color;
+    border-color: $base-color;
+  }
+}
+
+
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  padding-bottom: 8px;
+}
+</style>

+ 591 - 12
src/views/device/index.vue

@@ -1,29 +1,608 @@
 <template>
-  <div class="page-placeholder">
-    <h2>设备管理</h2>
-    <p>页面内容区域,后续在此完善业务。</p>
+  <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>
+          </div>
+          <el-button class="btn-add" type="warning" @click="handleAdd">新增设备</el-button>
+        </div>
+
+        <el-table :data="pagedList" class="device-table" row-key="id" header-cell-class-name="device-table__header"
+          @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="48" align="center" />
+
+          <el-table-column prop="code" label="农机代码" min-width="140" show-overflow-tooltip>
+            <template #default="{ row }">
+              <span class="code-tag">{{ row.code }}</span>
+            </template>
+          </el-table-column>
+
+          <el-table-column prop="name" label="农机名称" min-width="120" show-overflow-tooltip />
+
+          <el-table-column label="农机类型" min-width="110">
+            <template #default="{ row }">
+              <span class="type-tag">{{ row.type }}</span>
+            </template>
+          </el-table-column>
+
+          <el-table-column label="农事类别" min-width="160">
+            <template #default="{ row }">
+              <div class="category-grid">
+                <span v-for="(item, idx) in row.categories" :key="idx" class="category-grid__item">
+                  {{ item }}
+                </span>
+              </div>
+            </template>
+          </el-table-column>
+
+          <el-table-column label="存放点位" min-width="170">
+            <template #default="{ row }">
+              <div class="location-cell">
+                <div class="location-cell__row">
+                  <el-icon class="location-cell__icon">
+                    <Location />
+                  </el-icon>
+                  <span class="location-cell__text" :title="row.location">{{ row.location }}</span>
+                </div>
+                <el-button link type="warning" class="link-btn" @click="handleViewLocation(row)">
+                  查看点位
+                </el-button>
+              </div>
+            </template>
+          </el-table-column>
+
+          <el-table-column label="执行区域" width="120" align="center">
+            <template #default="{ row }">
+              <div class="area-thumb">
+                一区,二区
+                <el-button link type="warning" class="link-btn" @click="handleViewArea(row)">查看区域</el-button>
+              </div>
+            </template>
+          </el-table-column>
+
+          <el-table-column label="负责人" min-width="150">
+            <template #default="{ row }">
+              <div class="owner-cell">
+                <div>{{ row.ownerName }}</div>
+                <div class="owner-cell__phone">{{ row.ownerPhone }}</div>
+              </div>
+            </template>
+          </el-table-column>
+
+          <!-- <el-table-column label="服务记录" min-width="260">
+            <template #default="{ row }">
+              <div class="service-cell">
+                <div v-for="(record, idx) in row.serviceRecords.slice(0, 3)" :key="idx" class="service-cell__line">
+                  {{ record.date }} | {{ record.name }} | 作业区域: {{ record.area }}
+                </div>
+                <el-button link type="warning" class="link-btn" @click="handleViewMore(row)">
+                  查看更多
+                </el-button>
+              </div>
+            </template>
+          </el-table-column> -->
+
+          <el-table-column label="操作" width="150" 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 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>
+
+  <DeviceFormDialog
+    v-model="dialogVisible"
+    :mode="dialogMode"
+    :data="editingRow"
+    @submit="handleDialogSubmit"
+  />
 </template>
 
 <script setup>
+import { computed, ref } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Location } from "@element-plus/icons-vue";
+import DeviceFormDialog from "./DeviceFormDialog.vue";
+
+const tabs = ref([
+  { key: "all", label: "全部设备", count: 158 },
+  { key: "type-a", label: "某某类", count: 152 },
+  { key: "type-b", label: "某某类", count: 158 },
+  { key: "type-c", label: "某某类", count: 158 },
+]);
+
+const activeTab = ref("type-a");
+const page = ref(1);
+const pageSize = ref(20);
+const selectedRows = ref([]);
+const deviceName = ref("");
+const appliedDeviceName = ref("");
+
+const DEVICE_NAMES = ["水肥一体机", "植保无人机", "收割机", "播种机"];
+
+const buildMockRows = (count = 101) => {
+  const list = [];
+  for (let i = 1; i <= count; i++) {
+    list.push({
+      id: i,
+      code: "农场-类型-ID",
+      name: DEVICE_NAMES[i % DEVICE_NAMES.length],
+      type: "水肥一体",
+      categories: ["类别一", "类别二", "类别三", "类别四", "类别五", "类别六"],
+      location: "位置位置位置位置",
+      ownerName: "张扬",
+      ownerPhone: "19871513658",
+      serviceRecords: [
+        { date: "2024-08-20", name: "农事名称", area: "3区" },
+        { date: "2024-08-20", name: "农事名称", area: "3区" },
+        { date: "2024-08-20", name: "农事名称", area: "3区" },
+        { date: "2024-08-19", name: "农事名称", area: "2区" },
+      ],
+      categoryKey: i % 3 === 0 ? "type-b" : i % 2 === 0 ? "type-c" : "type-a",
+    });
+  }
+  return list;
+};
+
+const allList = ref(buildMockRows());
+
+const deviceNameOptions = computed(() => {
+  return [...new Set(allList.value.map((item) => item.name))];
+});
+
+const filteredList = computed(() => {
+  let list = allList.value;
+  if (activeTab.value !== "all") {
+    list = list.filter((item) => item.categoryKey === activeTab.value);
+  }
+  if (appliedDeviceName.value) {
+    list = list.filter((item) => item.name === appliedDeviceName.value);
+  }
+  return list;
+});
+
+const total = computed(() => filteredList.value.length);
+
+const pagedList = computed(() => {
+  const start = (page.value - 1) * pageSize.value;
+  return filteredList.value.slice(start, start + pageSize.value);
+});
+
+const handleTabChange = (key) => {
+  activeTab.value = key;
+  page.value = 1;
+};
+
+const handleSearch = () => {
+  appliedDeviceName.value = deviceName.value || "";
+  page.value = 1;
+};
+
+const handleReset = () => {
+  deviceName.value = "";
+  appliedDeviceName.value = "";
+  page.value = 1;
+};
+
+const handleSelectionChange = (rows) => {
+  selectedRows.value = rows;
+};
+
+const dialogVisible = ref(false);
+const dialogMode = ref("add");
+const editingRow = ref(null);
+
+const handleAdd = () => {
+  dialogMode.value = "add";
+  editingRow.value = null;
+  dialogVisible.value = true;
+};
+
+const handleEdit = (row) => {
+  dialogMode.value = "edit";
+  editingRow.value = { ...row };
+  dialogVisible.value = true;
+};
+
+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],
+        ...data,
+      };
+    }
+    return;
+  }
+
+  const nextId = allList.value.reduce((max, item) => Math.max(max, item.id), 0) + 1;
+  allList.value.unshift({
+    ...data,
+    id: nextId,
+    code: "农场-类型-ID",
+    categoryKey: activeTab.value === "all" ? "type-a" : activeTab.value,
+    serviceRecords: [],
+  });
+};
+
+const handleViewLocation = () => ElMessage.info("查看存放点位");
+const handleViewArea = () => ElMessage.info("查看执行区域");
+const handleDetail = () => ElMessage.info("设备详情");
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`确认删除设备「${row.name}」吗?`, "提示", {
+    type: "warning",
+    confirmButtonText: "删除",
+    cancelButtonText: "取消",
+  })
+    .then(() => {
+      allList.value = allList.value.filter((item) => item.id !== row.id);
+      ElMessage.success("已删除");
+    })
+    .catch(() => { });
+};
 </script>
 
 <style lang="scss" scoped>
-.page-placeholder {
+.device-page {
   box-sizing: border-box;
   min-height: 100%;
-  padding: 24px;
+  padding: 25px;
+}
+
+.device-panel {
+  min-height: calc(100% - 0px);
+  display: flex;
+  flex-direction: column;
+}
+
+.device-content {
   background: #fff;
+  border-radius: 5px;
+  padding: 0 10px;
 
-  h2 {
-    margin: 0 0 8px;
-    font-size: 20px;
-    color: #303133;
+}
+
+.filter-bar {
+  display: flex;
+  align-items: center;
+  gap: 30px;
+  padding: 0 12px;
+  border-radius: 5px;
+  height: 72px;
+  background: #fff;
+  margin-bottom: 20px;
+}
+.filter-r {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.filter-bar__label {
+  flex-shrink: 0;
+  font-size: 14px;
+  color: #333;
+  padding-right: 8px;
+}
+
+.filter-bar__select {
+  width: 220px;
+}
+
+.btn-reset {
+  height: 32px;
+  padding: 0 18px;
+  color: $warning-color;
+  background: #fff;
+  border-color: $warning-color;
+  border-radius: 4px;
+
+  &:hover,
+  &:focus {
+    color: #fff;
+    background: $warning-color;
+    border-color: $warning-color;
   }
+}
+
+.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;
+  border-radius: 4px;
+}
+
+.toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 56px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.tabs {
+  display: flex;
+  align-items: stretch;
+  height: 100%;
+  gap: 28px;
+}
 
-  p {
-    margin: 0;
-    color: #909399;
+.tabs__item {
+  position: relative;
+  display: flex;
+  align-items: center;
+  height: 100%;
+  font-size: 14px;
+  color: #666;
+  cursor: pointer;
+  user-select: none;
+  padding: 0 16px;
+
+  &:hover {
+    color: $warning-color;
+  }
+
+  &.is-active {
+    color: $warning-color;
+    font-weight: 500;
+
+    &::after {
+      content: "";
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      height: 2px;
+      background: $warning-color;
+    }
+  }
+}
+
+.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;
+  padding: 0 18px;
+  border-radius: 4px;
+}
+
+.device-table {
+  flex: 1;
+  width: 100%;
+  margin-top: 20px;
+
+  :deep(.device-table__header) {
+    background: #F5F5F5 !important;
+    color: #535353;
+    font-weight: 500;
+  }
+
+  :deep(.el-table__cell) {
+    padding: 12px 0;
+    vertical-align: middle;
   }
+
+  :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;
+  }
+}
+
+.type-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;
+}
+
+.category-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  color: #666;
+  font-size: 14px;
+  line-height: 20px;
+  .category-grid__item {
+    width: fit-content;
+    background: #F2F2F2;
+    border-radius: 2px;
+    padding: 1px 6px;
+  }
+}
+
+.code-tag {
+  color: #818080;
+}
+
+.location-cell {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 2px;
+}
+
+.location-cell__row {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  max-width: 100%;
+}
+
+.location-cell__icon {
+  color: #999;
+  flex-shrink: 0;
+}
+
+.location-cell__text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  color: #535353;
+  font-size: 14px;
+}
+
+.link-btn {
+  padding: 0;
+  height: auto;
+  font-size: 14px;
+}
+
+.area-thumb {
+  width: 88px;
+  height: 56px;
+  margin: 0 auto;
+  border-radius: 2px;
+  overflow: hidden;
+  cursor: pointer;
+}
+
+.owner-cell {
+  color: #535353;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  color: #666;
+  font-size: 14px;
+  line-height: 20px;
+}
+
+.owner-cell__phone {
+  // color: #999;
+  font-weight: 500;
+}
+
+.service-cell {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 2px;
+}
+
+.service-cell__line {
+  color: #666;
+  font-size: 12px;
+  line-height: 18px;
+  white-space: nowrap;
+}
+
+.ops {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+}
+
+.ops__btn {
+  margin: 0 !important;
+  height: 28px;
+  padding: 0 6px;
+  font-size: 14px;
+  color: #4C4C4C;
+  background: #F2F2F2;
+  border: 1px solid #D2D2D2;
+  border-radius: 2px;
+
+  &:hover {
+    color: $warning-color;
+    border-color: rgba($warning-color, 0.45);
+    background: rgba($warning-color, 0.06);
+  }
+}
+
+.ops__btn--danger {
+  color: #fff !important;
+  background: #F56B6B !important;
+  border-color: #F56B6B !important;
+
+  &:hover {
+    color: #fff !important;
+    background: mix(#fff, #F56B6B, 12%) !important;
+    border-color: mix(#fff, #F56B6B, 12%) !important;
+  }
+}
+
+.pager {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 34px;
+}
+
+.pager__total {
+  color: rgba(0, 0, 0, 0.6);
+  font-size: 14px;
+}
+
+:deep(.el-pagination.is-background .el-pager li.is-active) {
+  background-color: $warning-color !important;
+}
+
+:deep(.el-pagination.is-background .el-pager li:hover) {
+  color: $warning-color;
+}
+
+:deep(.el-pagination__jump) {
+  margin-left: 12px;
 }
 </style>