|
|
@@ -1,29 +1,510 @@
|
|
|
<template>
|
|
|
- <div class="page-placeholder">
|
|
|
- <h2>人员管理</h2>
|
|
|
- <p>页面内容区域,后续在此完善业务。</p>
|
|
|
+ <div class="personnel-page">
|
|
|
+ <!-- 地图容器结构对齐 SelectAreaDialog:map-wrap > map-el -->
|
|
|
+ <div v-loading="mapLoading" class="map-wrap">
|
|
|
+ <div ref="mapRef" class="map-el"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="!showSubjectPanel" class="page-float">
|
|
|
+ <el-button class="btn-add" type="warning" @click="handleAdd">
|
|
|
+ + 新增主体
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-show="!showSubjectPanel" class="ownership-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <span class="panel-title">权属列表</span>
|
|
|
+ <el-input v-model="keyword" class="panel-search" clearable placeholder="请搜索姓名">
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon class="panel-search__icon">
|
|
|
+ <Search />
|
|
|
+ </el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-filters">
|
|
|
+ <div v-for="tab in tabs" :key="tab.value" class="panel-filters__item"
|
|
|
+ :class="{ 'is-active': activeTab === tab.value }" @click="activeTab = tab.value">
|
|
|
+ {{ tab.label }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-list">
|
|
|
+ <div
|
|
|
+ v-for="item in filteredList"
|
|
|
+ :key="item.id"
|
|
|
+ class="person-card"
|
|
|
+ :class="{ 'is-selected': selectedId === item.id }"
|
|
|
+ @click="selectedId = item.id"
|
|
|
+ >
|
|
|
+ <div class="person-card__head">
|
|
|
+ <el-avatar :size="36" class="person-card__avatar" :src="item.avatar">
|
|
|
+ {{ item.name?.slice(0, 1) }}
|
|
|
+ </el-avatar>
|
|
|
+ <div class="person-card__info">
|
|
|
+ <div class="person-card__name-row">
|
|
|
+ <span class="person-card__name">{{ item.name }}</span>
|
|
|
+ <span class="person-card__role">{{ item.roleLabel }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="person-card__phone">电话: {{ item.phone }}</div>
|
|
|
+ </div>
|
|
|
+ <el-icon class="person-card__edit" @click.stop="handleEdit(item)">
|
|
|
+ <Edit />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="person-card__body">
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-row__label">负责区域:</span>
|
|
|
+ <span class="info-row__value">
|
|
|
+ {{ item.areas }}
|
|
|
+ <span class="link" @click.prevent="handleViewArea(item)">查看区域</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-row__label">区域面积:</span>
|
|
|
+ <span class="info-row__value">{{ item.areaSize }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-row__label">执行类别:</span>
|
|
|
+ <span class="info-row__value">{{ item.categories }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-row__label">负责农机:</span>
|
|
|
+ <span class="info-row__value">{{ item.machines }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-row__label">常驻位置:</span>
|
|
|
+ <span class="info-row__value">{{ item.location }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="person-card__foot">
|
|
|
+ <div class="person-card__actions">
|
|
|
+ <div class="btn-action" @click="handleDownload(item)">下载责任书</div>
|
|
|
+ <div class="btn-action" @click.stop="handleSign(item)">签署责任书</div>
|
|
|
+ </div>
|
|
|
+ <div class="btn-action btn-permission" @click="handlePermission(item)">设置权限</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-empty v-if="!filteredList.length" description="暂无数据" :image-size="80" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <AddSubjectPanel
|
|
|
+ v-model="showSubjectPanel"
|
|
|
+ :mode="subjectMode"
|
|
|
+ :person="editPerson"
|
|
|
+ @save="handleSubjectSave"
|
|
|
+ />
|
|
|
+ <SignResponsibilityDialog
|
|
|
+ v-model="showSignDialog"
|
|
|
+ :person="signPerson"
|
|
|
+ @submit="handleSignSubmit"
|
|
|
+ />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
+import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
|
|
+import { Edit, Search } from "@element-plus/icons-vue";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import PersonnelMap, { DEFAULT_LOCATION } from "./map/personnelMap";
|
|
|
+import AddSubjectPanel from "./components/AddSubjectPanel.vue";
|
|
|
+import SignResponsibilityDialog from "./components/SignResponsibilityDialog.vue";
|
|
|
+
|
|
|
+const DEFAULT_FARM_ID = 101532;
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance();
|
|
|
+const mapRef = ref(null);
|
|
|
+const mapLoading = ref(false);
|
|
|
+let personnelMap = null;
|
|
|
+
|
|
|
+const keyword = ref("");
|
|
|
+const activeTab = ref("all");
|
|
|
+const selectedId = ref(null);
|
|
|
+const showSubjectPanel = ref(false);
|
|
|
+/** add | edit */
|
|
|
+const subjectMode = ref("add");
|
|
|
+const editPerson = ref(null);
|
|
|
+const showSignDialog = ref(false);
|
|
|
+const signPerson = ref(null);
|
|
|
+const tabs = [
|
|
|
+ { label: "全部", value: "all" },
|
|
|
+ { label: "权属人", value: "owner" },
|
|
|
+ { label: "区域服务人员", value: "staff" },
|
|
|
+];
|
|
|
+
|
|
|
+const list = ref([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: "张扬",
|
|
|
+ role: "owner",
|
|
|
+ roleLabel: "权属人",
|
|
|
+ phone: "13939159356",
|
|
|
+ avatar: "",
|
|
|
+ areas: "2区、3区",
|
|
|
+ areaSize: "180亩",
|
|
|
+ categories: "类别一、类别一、类别一、类别一",
|
|
|
+ machines: "农机一、农机一、农机一、农机一",
|
|
|
+ location: "位置位置位置位置位置位置位置位置",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: "张扬",
|
|
|
+ role: "owner",
|
|
|
+ roleLabel: "权属人",
|
|
|
+ phone: "13939159356",
|
|
|
+ avatar: "",
|
|
|
+ areas: "2区、3区",
|
|
|
+ areaSize: "180亩",
|
|
|
+ categories: "类别一、类别一、类别一、类别一",
|
|
|
+ machines: "农机一、农机一、农机一、农机一",
|
|
|
+ location: "位置位置位置位置位置位置位置位置",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: "张扬",
|
|
|
+ role: "staff",
|
|
|
+ roleLabel: "区域服务人员",
|
|
|
+ phone: "13939159356",
|
|
|
+ avatar: "",
|
|
|
+ areas: "2区、3区",
|
|
|
+ areaSize: "180亩",
|
|
|
+ categories: "类别一、类别一、类别一、类别一",
|
|
|
+ machines: "农机一、农机一、农机一、农机一",
|
|
|
+ location: "位置位置位置位置位置位置位置位置",
|
|
|
+ },
|
|
|
+]);
|
|
|
+
|
|
|
+const filteredList = computed(() => {
|
|
|
+ const kw = keyword.value.trim();
|
|
|
+ return list.value.filter((item) => {
|
|
|
+ const matchTab = activeTab.value === "all" || item.role === activeTab.value;
|
|
|
+ const matchKw = !kw || item.name.includes(kw);
|
|
|
+ return matchTab && matchKw;
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+/** 等地图容器有实际宽高后再初始化(对齐弹窗 @opened 时机) */
|
|
|
+const waitForMapEl = () =>
|
|
|
+ new Promise((resolve) => {
|
|
|
+ const tick = (n = 0) => {
|
|
|
+ const el = mapRef.value;
|
|
|
+ if (el && el.clientWidth > 0 && el.clientHeight > 0) {
|
|
|
+ resolve(el);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (n > 60) {
|
|
|
+ resolve(el);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ requestAnimationFrame(() => tick(n + 1));
|
|
|
+ };
|
|
|
+ tick();
|
|
|
+ });
|
|
|
+
|
|
|
+/**
|
|
|
+ * 对齐 SelectAreaDialog.handleOpened:
|
|
|
+ * nextTick → init(PersonnelMap) → loadRegions → updateSize
|
|
|
+ */
|
|
|
+const handleOpened = async () => {
|
|
|
+ await nextTick();
|
|
|
+ const el = await waitForMapEl();
|
|
|
+ if (!el) return;
|
|
|
+
|
|
|
+ if (!personnelMap) {
|
|
|
+ personnelMap = new PersonnelMap();
|
|
|
+ }
|
|
|
+ await personnelMap.init(el, DEFAULT_LOCATION);
|
|
|
+ // 先校正尺寸,再 fit,避免容器尺寸为 0 时适配导致黑边碎块
|
|
|
+ personnelMap.updateSize();
|
|
|
+ await loadRegions();
|
|
|
+ personnelMap.updateSize();
|
|
|
+};
|
|
|
+
|
|
|
+/** 对齐 SelectAreaDialog.loadRegions */
|
|
|
+const loadRegions = async () => {
|
|
|
+ mapLoading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await proxy.VE_API.farm.blueRegionList({
|
|
|
+ farmId: DEFAULT_FARM_ID,
|
|
|
+ regionId: "",
|
|
|
+ });
|
|
|
+ const regionList = Array.isArray(res?.data) ? res.data : [];
|
|
|
+ personnelMap?.setRegions(regionList);
|
|
|
+ if (!regionList.length && res?.msg) {
|
|
|
+ ElMessage.warning(res.msg);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ ElMessage.error("区域数据加载失败");
|
|
|
+ } finally {
|
|
|
+ mapLoading.value = false;
|
|
|
+ await nextTick();
|
|
|
+ personnelMap?.updateSize();
|
|
|
+ personnelMap?.fitToRegions();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const destroyMap = () => {
|
|
|
+ personnelMap?.destroy();
|
|
|
+ personnelMap = null;
|
|
|
+};
|
|
|
+
|
|
|
+const handleAdd = () => {
|
|
|
+ subjectMode.value = "add";
|
|
|
+ editPerson.value = null;
|
|
|
+ showSubjectPanel.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleEdit = (item) => {
|
|
|
+ selectedId.value = item.id;
|
|
|
+ subjectMode.value = "edit";
|
|
|
+ editPerson.value = item;
|
|
|
+ showSubjectPanel.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleSubjectSave = (payload) => {
|
|
|
+ console.log(payload.mode === "edit" ? "编辑主体" : "新增主体", payload);
|
|
|
+ ElMessage.success("保存成功");
|
|
|
+};
|
|
|
+
|
|
|
+const handleViewArea = () => { };
|
|
|
+const handleDownload = () => { };
|
|
|
+
|
|
|
+const handleSign = (item) => {
|
|
|
+ signPerson.value = item;
|
|
|
+ showSignDialog.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleSignSubmit = (payload) => {
|
|
|
+ console.log("签署责任书", payload);
|
|
|
+ ElMessage.success("提交成功");
|
|
|
+};
|
|
|
+
|
|
|
+const handlePermission = () => { };
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ handleOpened();
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ destroyMap();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.page-placeholder {
|
|
|
- box-sizing: border-box;
|
|
|
- min-height: 100%;
|
|
|
- padding: 24px;
|
|
|
- background: #fff;
|
|
|
-
|
|
|
- h2 {
|
|
|
- margin: 0 0 8px;
|
|
|
- font-size: 20px;
|
|
|
- color: #303133;
|
|
|
+$primary: $warning-color;
|
|
|
+
|
|
|
+.personnel-page {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #1a1a1a;
|
|
|
+
|
|
|
+ .map-wrap {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #1a1a1a;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .map-el {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-float {
|
|
|
+ position: absolute;
|
|
|
+ top: 16px;
|
|
|
+ left: 16px;
|
|
|
+ z-index: 10;
|
|
|
+
|
|
|
+ .btn-add {
|
|
|
+ height: 36px;
|
|
|
+ padding: 0 16px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 14px;
|
|
|
+ background: $primary;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- p {
|
|
|
- margin: 0;
|
|
|
- color: #909399;
|
|
|
+ .ownership-panel {
|
|
|
+ position: absolute;
|
|
|
+ top: 25px;
|
|
|
+ right: 20px;
|
|
|
+ bottom: 25px;
|
|
|
+ z-index: 10;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: 380px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 20px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 10px;
|
|
|
+
|
|
|
+ .panel-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+
|
|
|
+ .panel-title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #060606;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-search {
|
|
|
+ width: 150px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-filters {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin: 10px 0;
|
|
|
+
|
|
|
+ &__item {
|
|
|
+ padding: 5px 10px;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(0, 0, 0, 0.6);
|
|
|
+ background: #F3F3F3;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &.is-active {
|
|
|
+ color: $primary;
|
|
|
+ background: rgba($primary, 0.1);
|
|
|
+ border-color: $primary;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-list {
|
|
|
+ overflow: auto;
|
|
|
+
|
|
|
+ .person-card {
|
|
|
+ padding: 12px;
|
|
|
+ border: .5px solid #E5E6EB;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 1px solid transparent;
|
|
|
+
|
|
|
+ &.is-selected {
|
|
|
+ border: 1px solid $primary;
|
|
|
+ box-shadow: 0 4px 6px rgba($primary, 0.2) inset;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__info {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__name-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 7px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__name {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #060606;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__role {
|
|
|
+ padding: 1px 5px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #070707;
|
|
|
+ background: rgba(100, 100, 100, 0.1);
|
|
|
+ border-radius: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__phone {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(105, 105, 105, 0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ &__edit {
|
|
|
+ font-size: 20px;
|
|
|
+ color: $primary;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__foot {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 6px;
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-row {
|
|
|
+ display: flex;
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ &__label {
|
|
|
+ width: 66px;
|
|
|
+ color: #6C6C6C;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__value {
|
|
|
+ flex: 1;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .link {
|
|
|
+ color: $primary;
|
|
|
+ text-decoration: underline;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-action {
|
|
|
+ padding: 6px 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: $primary;
|
|
|
+ background: rgba($primary, 0.12);
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-permission {
|
|
|
+ color: #fff;
|
|
|
+ background: #FB8E33;
|
|
|
+ }
|
|
|
+
|
|
|
+ .person-card+.person-card {
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
</style>
|