|
|
@@ -0,0 +1,1528 @@
|
|
|
+<template>
|
|
|
+ <BasicModal
|
|
|
+ :open="open"
|
|
|
+ :width="960"
|
|
|
+ :centered="true"
|
|
|
+ :show-ok-btn="false"
|
|
|
+ :show-cancel-btn="false"
|
|
|
+ :can-fullscreen="false"
|
|
|
+ :use-wrapper="false"
|
|
|
+ :body-style="{ padding: '0' }"
|
|
|
+ wrap-class-name="pg-manage-modal"
|
|
|
+ @cancel="handleClose"
|
|
|
+ >
|
|
|
+ <template #title>
|
|
|
+ <div class="pg-modal-title-block">
|
|
|
+ <div class="pg-modal-title">对象分组</div>
|
|
|
+ <div class="pg-modal-subtitle">维护分组结构、查看成员状态并设置主要对象</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <Spin :spinning="loading" tip="加载分组数据...">
|
|
|
+ <div class="pg-body">
|
|
|
+ <aside class="pg-groups">
|
|
|
+ <div class="pg-groups__head">
|
|
|
+ <span class="pg-groups__title">
|
|
|
+ 分组
|
|
|
+ <em>{{ objectGroups.length }}</em>
|
|
|
+ </span>
|
|
|
+ <Button class="pg-add-group-btn" :disabled="mode !== 'view'" @click="startCreateGroup">
|
|
|
+ <PlusOutlined />
|
|
|
+ 新增分组
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Input v-model:value="groupKeyword" class="pg-group-search" placeholder="搜索分组" allow-clear>
|
|
|
+ <template #prefix>
|
|
|
+ <SearchOutlined />
|
|
|
+ </template>
|
|
|
+ </Input>
|
|
|
+
|
|
|
+ <div class="pg-group-list">
|
|
|
+ <button
|
|
|
+ v-for="group in filteredGroups"
|
|
|
+ :key="group.id"
|
|
|
+ type="button"
|
|
|
+ class="pg-group-item"
|
|
|
+ :class="{ active: group.id === activeGroupId }"
|
|
|
+ :disabled="mode !== 'view'"
|
|
|
+ @click="selectGroup(group.id)"
|
|
|
+ >
|
|
|
+ <span class="pg-group-item__row">
|
|
|
+ <span class="pg-group-item__name">{{ group.name }}</span>
|
|
|
+ <span class="pg-group-item__count">{{ groupMemberCount(group.id) }}</span>
|
|
|
+ </span>
|
|
|
+ <span class="pg-group-item__meta">
|
|
|
+ {{ groupMainName(group.id) ? `主要 ${groupMainName(group.id)}` : '未设置主要对象' }}
|
|
|
+ </span>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div v-if="!filteredGroups.length" class="pg-empty pg-empty--groups">
|
|
|
+ <div class="pg-empty__icon">
|
|
|
+ <TeamOutlined />
|
|
|
+ </div>
|
|
|
+ <div class="pg-empty__title">
|
|
|
+ {{ objectGroups.length ? '没有匹配的分组' : '还没有分组' }}
|
|
|
+ </div>
|
|
|
+ <div v-if="!objectGroups.length" class="pg-empty__desc">创建一个分组后即可开始分析</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pg-groups__foot">共 {{ objectGroups.length }} 个分组</div>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <section class="pg-panel">
|
|
|
+ <template v-if="mode === 'view'">
|
|
|
+ <template v-if="activeGroup">
|
|
|
+ <div class="pg-panel__head">
|
|
|
+ <div class="pg-panel__heading">
|
|
|
+ <div class="pg-panel__title" :title="activeGroup.name">{{ activeGroup.name }}</div>
|
|
|
+ <div class="pg-panel__sub">
|
|
|
+ {{ activeGroupMembers.length }} 位成员
|
|
|
+ <template v-if="mainPersonName"> · 主要对象 {{ mainPersonName }}</template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="pg-panel__actions">
|
|
|
+ <Button @click="startAddMembers">
|
|
|
+ <UserAddOutlined />
|
|
|
+ 添加对象
|
|
|
+ </Button>
|
|
|
+ <Button @click="startEditGroup">
|
|
|
+ <EditOutlined />
|
|
|
+ 编辑分组
|
|
|
+ </Button>
|
|
|
+ <Popconfirm
|
|
|
+ title="删除分组"
|
|
|
+ description="删除后分组及成员关系不可恢复,确认删除?"
|
|
|
+ ok-text="删除"
|
|
|
+ cancel-text="取消"
|
|
|
+ @confirm="handleDeleteGroup"
|
|
|
+ >
|
|
|
+ <Button danger>
|
|
|
+ <DeleteOutlined />
|
|
|
+ 删除分组
|
|
|
+ </Button>
|
|
|
+ </Popconfirm>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pg-member-toolbar">
|
|
|
+ <Input v-model:value="memberKeyword" class="pg-member-search" placeholder="搜索成员" allow-clear>
|
|
|
+ <template #prefix>
|
|
|
+ <SearchOutlined />
|
|
|
+ </template>
|
|
|
+ </Input>
|
|
|
+ <span class="pg-member-total">共 {{ activeGroupMembers.length }} 人</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pg-member-list">
|
|
|
+ <div v-for="member in filteredMembers" :key="member.id" class="pg-member-item">
|
|
|
+ <Avatar :size="36" class="pg-member-avatar">
|
|
|
+ <svg viewBox="0 0 24 24" fill="currentColor">
|
|
|
+ <path
|
|
|
+ d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
|
|
+ />
|
|
|
+ </svg>
|
|
|
+ </Avatar>
|
|
|
+ <div class="pg-member-info">
|
|
|
+ <div class="pg-member-name-row">
|
|
|
+ <span class="pg-member-name" :title="member.personName">{{ member.personName }}</span>
|
|
|
+ <span v-if="member.hasMain" class="pg-member-tag">主要</span>
|
|
|
+ </div>
|
|
|
+ <div class="pg-member-desc">
|
|
|
+ {{ member.hasMain ? '当前主对象' : '分组成员' }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="pg-member-actions">
|
|
|
+ <button v-if="!member.hasMain" type="button" class="pg-link" @click="handleChangeMain(member.id)">
|
|
|
+ 设为主对象
|
|
|
+ </button>
|
|
|
+ <Popconfirm
|
|
|
+ title="移出成员"
|
|
|
+ :description="`确认将「${member.personName}」移出当前分组?`"
|
|
|
+ ok-text="移出"
|
|
|
+ cancel-text="取消"
|
|
|
+ @confirm="handleRemoveMember(member.id)"
|
|
|
+ >
|
|
|
+ <button type="button" class="pg-link pg-link--danger">移出</button>
|
|
|
+ </Popconfirm>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="!filteredMembers.length" class="pg-empty pg-empty--page">
|
|
|
+ <div class="pg-empty__icon">
|
|
|
+ <UserAddOutlined />
|
|
|
+ </div>
|
|
|
+ <div class="pg-empty__title">
|
|
|
+ {{ activeGroupMembers.length ? '没有匹配的成员' : '当前分组暂无成员' }}
|
|
|
+ </div>
|
|
|
+ <div v-if="!activeGroupMembers.length" class="pg-empty__desc"> 添加对象后,分组才会参与分析 </div>
|
|
|
+ <Button v-if="!activeGroupMembers.length" type="primary" @click="startAddMembers"> 添加对象 </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div v-else class="pg-empty pg-empty--page">
|
|
|
+ <div class="pg-empty__icon">
|
|
|
+ <TeamOutlined />
|
|
|
+ </div>
|
|
|
+ <div class="pg-empty__title">
|
|
|
+ {{ objectGroups.length ? '请选择左侧分组' : '还没有分组' }}
|
|
|
+ </div>
|
|
|
+ <div class="pg-empty__desc">
|
|
|
+ {{ objectGroups.length ? '选择分组后可查看和维护成员' : '创建第一个分组,用于分析被调查对象' }}
|
|
|
+ </div>
|
|
|
+ <Button v-if="!objectGroups.length" type="primary" @click="startCreateGroup"> 新增分组 </Button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="mode === 'create' || mode === 'edit'">
|
|
|
+ <div class="pg-form">
|
|
|
+ <div class="pg-form__head">
|
|
|
+ <div class="pg-form__title">{{ mode === 'create' ? '新建分组' : '编辑分组' }}</div>
|
|
|
+ <div class="pg-form__desc">
|
|
|
+ {{
|
|
|
+ mode === 'create'
|
|
|
+ ? '填写分组名称并选择分组主体对象,创建后可继续添加对象'
|
|
|
+ : '修改分组名称与备注,主要对象请在成员列表中调整'
|
|
|
+ }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <label class="pg-field">
|
|
|
+ <span class="pg-field__label">分组名称 <em>*</em></span>
|
|
|
+ <Input
|
|
|
+ v-model:value="form.groupName"
|
|
|
+ :status="groupNameError ? 'error' : undefined"
|
|
|
+ :maxlength="50"
|
|
|
+ placeholder="请输入分组名称"
|
|
|
+ @blur="() => validateGroupName(true)"
|
|
|
+ />
|
|
|
+ <span v-if="groupNameError" class="pg-field__error">{{ groupNameError }}</span>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <label v-if="mode === 'create'" class="pg-field">
|
|
|
+ <span class="pg-field__label">分组主体对象 <em>*</em></span>
|
|
|
+ <Select
|
|
|
+ v-model:value="form.mainPerson"
|
|
|
+ :options="personOptions"
|
|
|
+ placeholder="请选择主体对象"
|
|
|
+ allow-clear
|
|
|
+ show-search
|
|
|
+ option-filter-prop="label"
|
|
|
+ :get-popup-container="getModalPopupContainer"
|
|
|
+ />
|
|
|
+ <span v-if="mainPersonError" class="pg-field__error">{{ mainPersonError }}</span>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <label class="pg-field">
|
|
|
+ <span class="pg-field__label">备注</span>
|
|
|
+ <Input.TextArea
|
|
|
+ v-model:value="form.remark"
|
|
|
+ :rows="3"
|
|
|
+ :maxlength="200"
|
|
|
+ placeholder="选填,便于识别分组用途"
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <div class="pg-form__actions">
|
|
|
+ <Button @click="cancelForm">取消</Button>
|
|
|
+ <Button type="primary" :loading="savingGroup" @click="handleSaveGroup">
|
|
|
+ {{ mode === 'create' ? '创建分组' : '保存修改' }}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="mode === 'addMembers'">
|
|
|
+ <div class="pg-panel__head">
|
|
|
+ <div class="pg-panel__heading">
|
|
|
+ <div class="pg-panel__title">添加对象</div>
|
|
|
+ <div class="pg-panel__sub">
|
|
|
+ 加入「{{ activeGroup?.name }}」,已选 {{ selectedAddNames.length }} 人
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="pg-panel__actions">
|
|
|
+ <Button @click="cancelAddMembers">取消</Button>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ :loading="addingMembers"
|
|
|
+ :disabled="!selectedAddNames.length"
|
|
|
+ @click="confirmAddMembers"
|
|
|
+ >
|
|
|
+ 加入分组
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pg-member-toolbar">
|
|
|
+ <Input
|
|
|
+ v-model:value="addKeyword"
|
|
|
+ class="pg-member-search"
|
|
|
+ placeholder="搜索姓名 / 证件号 / 手机号"
|
|
|
+ allow-clear
|
|
|
+ >
|
|
|
+ <template #prefix>
|
|
|
+ <SearchOutlined />
|
|
|
+ </template>
|
|
|
+ </Input>
|
|
|
+ <span class="pg-member-total">共 {{ filteredAddCandidates.length }} 个可选对象</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pg-candidate-list">
|
|
|
+ <label
|
|
|
+ v-for="item in filteredAddCandidates"
|
|
|
+ :key="item.key"
|
|
|
+ class="pg-candidate"
|
|
|
+ :class="{ 'is-selected': selectedAddNames.includes(item.name) }"
|
|
|
+ >
|
|
|
+ <Checkbox
|
|
|
+ :checked="selectedAddNames.includes(item.name)"
|
|
|
+ @click.stop
|
|
|
+ @change="(e) => setCandidate(item, e.target.checked)"
|
|
|
+ />
|
|
|
+ <PersonAvatar
|
|
|
+ class="pg-candidate__avatar"
|
|
|
+ :src="item.photograph || undefined"
|
|
|
+ :alt="item.name"
|
|
|
+ :size="36"
|
|
|
+ />
|
|
|
+ <span class="pg-candidate__body">
|
|
|
+ <span class="pg-candidate__name">{{ item.name }}</span>
|
|
|
+ <span class="pg-candidate__meta">
|
|
|
+ 证件号 {{ maskCertNo(item.personCertNo) }} · 手机 {{ item.primaryPhone }} · 银行卡
|
|
|
+ {{ item.cardCount }} 张
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <div v-if="!filteredAddCandidates.length" class="pg-empty pg-empty--page">
|
|
|
+ <div class="pg-empty__icon">
|
|
|
+ <TeamOutlined />
|
|
|
+ </div>
|
|
|
+ <div class="pg-empty__title">{{ addEmptyText }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </Spin>
|
|
|
+ </BasicModal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup name="PersonGroupManageModal">
|
|
|
+ import { computed, reactive, ref, watch } from 'vue';
|
|
|
+ import { Avatar, Button, Checkbox, Input, Popconfirm, Select, Spin } from 'ant-design-vue';
|
|
|
+ import {
|
|
|
+ DeleteOutlined,
|
|
|
+ EditOutlined,
|
|
|
+ PlusOutlined,
|
|
|
+ SearchOutlined,
|
|
|
+ TeamOutlined,
|
|
|
+ UserAddOutlined,
|
|
|
+ } from '@ant-design/icons-vue';
|
|
|
+ import { PersonAvatar } from '@/core/components/PersonAvatar';
|
|
|
+ import { BasicModal } from '@/core/components/Modal';
|
|
|
+ import { useMessage } from '@/core/hooks/web/useMessage';
|
|
|
+ import { getModalPopupContainer as resolveModalPopupContainer } from '@/core/utils';
|
|
|
+ import { maskCertNo } from '@/core/utils/sensitive';
|
|
|
+ import {
|
|
|
+ addPersonToGroup,
|
|
|
+ changeMainPerson,
|
|
|
+ createPersonGroup,
|
|
|
+ deletePersonGroup,
|
|
|
+ getPersons,
|
|
|
+ listPersonGroups,
|
|
|
+ remPersonFromGroup,
|
|
|
+ updatePersonGroup,
|
|
|
+ } from '@/person/api/personApi';
|
|
|
+ import type { PersonBasicInfo } from '@/person/api/types/dto';
|
|
|
+
|
|
|
+ type Mode = 'view' | 'create' | 'edit' | 'addMembers';
|
|
|
+
|
|
|
+ interface GroupItem {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ remark?: string;
|
|
|
+ }
|
|
|
+
|
|
|
+ interface MemberItem {
|
|
|
+ id: string;
|
|
|
+ groupId: string;
|
|
|
+ personName: string;
|
|
|
+ hasMain: boolean;
|
|
|
+ }
|
|
|
+
|
|
|
+ interface CandidateItem {
|
|
|
+ key: string;
|
|
|
+ name: string;
|
|
|
+ personCertNo: string;
|
|
|
+ primaryPhone: string;
|
|
|
+ cardCount: number;
|
|
|
+ photograph?: string;
|
|
|
+ }
|
|
|
+
|
|
|
+ const props = withDefaults(defineProps<{ open: boolean }>(), { open: false });
|
|
|
+ const emit = defineEmits<{
|
|
|
+ (e: 'update:open', value: boolean): void;
|
|
|
+ (e: 'change'): void;
|
|
|
+ (e: 'created', groupName: string): void;
|
|
|
+ }>();
|
|
|
+
|
|
|
+ const { createMessage } = useMessage();
|
|
|
+
|
|
|
+ const loading = ref(false);
|
|
|
+ const mode = ref<Mode>('view');
|
|
|
+ const objectGroups = ref<GroupItem[]>([]);
|
|
|
+ const groupMembers = ref<MemberItem[]>([]);
|
|
|
+ const persons = ref<PersonBasicInfo[]>([]);
|
|
|
+ const activeGroupId = ref<string | null>(null);
|
|
|
+
|
|
|
+ const groupKeyword = ref('');
|
|
|
+ const memberKeyword = ref('');
|
|
|
+ const addKeyword = ref('');
|
|
|
+ const selectedAddNames = ref<string[]>([]);
|
|
|
+
|
|
|
+ const savingGroup = ref(false);
|
|
|
+ const addingMembers = ref(false);
|
|
|
+ const groupNameError = ref('');
|
|
|
+ const mainPersonError = ref('');
|
|
|
+ const form = reactive({
|
|
|
+ groupName: '',
|
|
|
+ mainPerson: undefined as string | undefined,
|
|
|
+ remark: '',
|
|
|
+ });
|
|
|
+
|
|
|
+ const normalizeGroupName = (value?: string | null) => value?.trim().toLocaleLowerCase() || '';
|
|
|
+ const hasMainValue = (value?: boolean | 'Y' | 'N') => value === true || value === 'Y';
|
|
|
+
|
|
|
+ const activeGroup = computed(() => objectGroups.value.find((item) => item.id === activeGroupId.value) || null);
|
|
|
+
|
|
|
+ const activeGroupMembers = computed(() => groupMembers.value.filter((item) => item.groupId === activeGroupId.value));
|
|
|
+
|
|
|
+ const mainPersonName = computed(() => activeGroupMembers.value.find((item) => item.hasMain)?.personName || '');
|
|
|
+
|
|
|
+ const memberCountMap = computed(() => {
|
|
|
+ const counts = new Map<string, number>();
|
|
|
+ groupMembers.value.forEach((item) => {
|
|
|
+ counts.set(item.groupId, (counts.get(item.groupId) || 0) + 1);
|
|
|
+ });
|
|
|
+ return counts;
|
|
|
+ });
|
|
|
+
|
|
|
+ const groupMemberCount = (groupId: string) => memberCountMap.value.get(groupId) || 0;
|
|
|
+
|
|
|
+ const groupMainName = (groupId: string) =>
|
|
|
+ groupMembers.value.find((item) => item.groupId === groupId && item.hasMain)?.personName || '';
|
|
|
+
|
|
|
+ const filteredGroups = computed(() => {
|
|
|
+ const keyword = groupKeyword.value.trim().toLocaleLowerCase();
|
|
|
+ if (!keyword) return objectGroups.value;
|
|
|
+ return objectGroups.value.filter((item) =>
|
|
|
+ [item.name, item.remark || ''].some((value) => value.toLocaleLowerCase().includes(keyword)),
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ const filteredMembers = computed(() => {
|
|
|
+ const keyword = memberKeyword.value.trim().toLocaleLowerCase();
|
|
|
+ if (!keyword) return activeGroupMembers.value;
|
|
|
+ return activeGroupMembers.value.filter((item) => item.personName.toLocaleLowerCase().includes(keyword));
|
|
|
+ });
|
|
|
+
|
|
|
+ const groupedPersonNameSet = computed(() => {
|
|
|
+ return new Set(
|
|
|
+ groupMembers.value
|
|
|
+ .map((item) => item.personName?.trim())
|
|
|
+ .filter((item): item is string => !!item && item !== '-'),
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ const personOptions = computed(() =>
|
|
|
+ persons.value
|
|
|
+ .map((item) => {
|
|
|
+ const personName = item.personName?.trim() || '';
|
|
|
+ return { label: personName || '-', value: personName };
|
|
|
+ })
|
|
|
+ .filter((item) => !!item.value && !groupedPersonNameSet.value.has(item.value)),
|
|
|
+ );
|
|
|
+
|
|
|
+ const addCandidates = computed<CandidateItem[]>(() =>
|
|
|
+ persons.value
|
|
|
+ .map((item, index) => {
|
|
|
+ const name = item.personName?.trim() || '';
|
|
|
+ const phoneValues = (item.phoneList || []).map((phone) => phone.personNo || '').filter(Boolean);
|
|
|
+ const cardCount = (item.cardList || []).filter((card) => !!card?.personNo).length;
|
|
|
+ return {
|
|
|
+ key: String(item.id ?? item.objId ?? name ?? index),
|
|
|
+ name: name || `未命名对象${index + 1}`,
|
|
|
+ personCertNo: item.personCertNo || '-',
|
|
|
+ primaryPhone: phoneValues[0] || '-',
|
|
|
+ cardCount,
|
|
|
+ photograph: item.photograph || '',
|
|
|
+ };
|
|
|
+ })
|
|
|
+ .filter((item) => !!item.name && !groupedPersonNameSet.value.has(item.name))
|
|
|
+ .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN')),
|
|
|
+ );
|
|
|
+
|
|
|
+ const filteredAddCandidates = computed(() => {
|
|
|
+ const keyword = addKeyword.value.trim().toLocaleLowerCase();
|
|
|
+ if (!keyword) return addCandidates.value;
|
|
|
+ return addCandidates.value.filter((item) =>
|
|
|
+ [item.name, item.personCertNo, item.primaryPhone].some((value) => value.toLocaleLowerCase().includes(keyword)),
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ const addEmptyText = computed(() => {
|
|
|
+ if (!addCandidates.value.length) return '暂无未分组对象,对象限定在单一分组';
|
|
|
+ return '暂无匹配对象';
|
|
|
+ });
|
|
|
+
|
|
|
+ const getModalPopupContainer = (triggerNode?: HTMLElement) => resolveModalPopupContainer(triggerNode);
|
|
|
+
|
|
|
+ const loadData = async () => {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const [groups, personList] = await Promise.all([listPersonGroups(), getPersons()]);
|
|
|
+ objectGroups.value = (groups || []).map((item) => ({
|
|
|
+ id: item.id || '',
|
|
|
+ name: item.groupName || `分组${item.id}`,
|
|
|
+ remark: item.remark,
|
|
|
+ }));
|
|
|
+ groupMembers.value = (groups || []).flatMap((group) =>
|
|
|
+ (group.personList || []).map((rel) => ({
|
|
|
+ id: rel.id || '',
|
|
|
+ groupId: rel.groupId || group.id || '',
|
|
|
+ personName: rel.personName || '-',
|
|
|
+ hasMain: hasMainValue(rel.hasMain),
|
|
|
+ })),
|
|
|
+ );
|
|
|
+ persons.value = Array.isArray(personList) ? personList : [];
|
|
|
+
|
|
|
+ if (objectGroups.value.length) {
|
|
|
+ const currentExists = objectGroups.value.some((item) => item.id === activeGroupId.value);
|
|
|
+ activeGroupId.value = currentExists ? activeGroupId.value : objectGroups.value[0].id;
|
|
|
+ } else {
|
|
|
+ activeGroupId.value = null;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取分组列表失败', error);
|
|
|
+ createMessage.error('获取分组列表失败');
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const resetForm = () => {
|
|
|
+ form.groupName = '';
|
|
|
+ form.mainPerson = undefined;
|
|
|
+ form.remark = '';
|
|
|
+ groupNameError.value = '';
|
|
|
+ mainPersonError.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const selectGroup = (groupId: string) => {
|
|
|
+ if (mode.value !== 'view') return;
|
|
|
+ activeGroupId.value = groupId;
|
|
|
+ memberKeyword.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const startCreateGroup = () => {
|
|
|
+ mode.value = 'create';
|
|
|
+ resetForm();
|
|
|
+ memberKeyword.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const startEditGroup = () => {
|
|
|
+ if (!activeGroup.value) return;
|
|
|
+ mode.value = 'edit';
|
|
|
+ form.groupName = activeGroup.value.name;
|
|
|
+ form.mainPerson = mainPersonName.value || undefined;
|
|
|
+ form.remark = activeGroup.value.remark || '';
|
|
|
+ groupNameError.value = '';
|
|
|
+ mainPersonError.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const cancelForm = () => {
|
|
|
+ mode.value = 'view';
|
|
|
+ resetForm();
|
|
|
+ };
|
|
|
+
|
|
|
+ const isDuplicateGroupName = (groupName: string) => {
|
|
|
+ const normalizedName = normalizeGroupName(groupName);
|
|
|
+ if (!normalizedName) return false;
|
|
|
+ const editingId = mode.value === 'edit' ? activeGroupId.value : null;
|
|
|
+ return objectGroups.value.some((item) => {
|
|
|
+ if (editingId && item.id === editingId) return false;
|
|
|
+ return normalizeGroupName(item.name) === normalizedName;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const validateGroupName = (showRequired = false) => {
|
|
|
+ const groupName = form.groupName.trim();
|
|
|
+ if (!groupName) {
|
|
|
+ groupNameError.value = showRequired ? '请输入分组名称' : '';
|
|
|
+ return !showRequired;
|
|
|
+ }
|
|
|
+ if (isDuplicateGroupName(groupName)) {
|
|
|
+ groupNameError.value = '分组名称已存在,请重新输入';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ groupNameError.value = '';
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ const validateMainPerson = (showRequired = false) => {
|
|
|
+ if (mode.value !== 'create') {
|
|
|
+ mainPersonError.value = '';
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (!form.mainPerson) {
|
|
|
+ mainPersonError.value = showRequired ? '请选择主体对象' : '';
|
|
|
+ return !showRequired;
|
|
|
+ }
|
|
|
+ mainPersonError.value = '';
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSaveGroup = async () => {
|
|
|
+ if (savingGroup.value) return;
|
|
|
+ const isCreate = mode.value === 'create';
|
|
|
+ const groupName = form.groupName.trim();
|
|
|
+
|
|
|
+ if (!validateGroupName(true)) return;
|
|
|
+ if (isCreate && !validateMainPerson(true)) return;
|
|
|
+
|
|
|
+ savingGroup.value = true;
|
|
|
+ try {
|
|
|
+ if (isCreate) {
|
|
|
+ await createPersonGroup({
|
|
|
+ groupName,
|
|
|
+ remark: form.remark.trim() || undefined,
|
|
|
+ personList: form.mainPerson ? [{ personName: form.mainPerson, hasMain: true }] : undefined,
|
|
|
+ });
|
|
|
+ } else if (activeGroupId.value) {
|
|
|
+ await updatePersonGroup({
|
|
|
+ id: activeGroupId.value,
|
|
|
+ groupName,
|
|
|
+ remark: form.remark.trim() || undefined,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ createMessage.success(isCreate ? '新增分组成功' : '更新分组成功');
|
|
|
+ mode.value = 'view';
|
|
|
+ resetForm();
|
|
|
+ await loadData();
|
|
|
+
|
|
|
+ if (isCreate) {
|
|
|
+ const created = objectGroups.value.find(
|
|
|
+ (item) => normalizeGroupName(item.name) === normalizeGroupName(groupName),
|
|
|
+ );
|
|
|
+ if (created) activeGroupId.value = created.id;
|
|
|
+ emit('created', groupName);
|
|
|
+ }
|
|
|
+ emit('change');
|
|
|
+ } catch (error) {
|
|
|
+ console.error(isCreate ? '新增分组失败' : '更新分组失败', error);
|
|
|
+ createMessage.error(isCreate ? '新增分组失败' : '更新分组失败');
|
|
|
+ } finally {
|
|
|
+ savingGroup.value = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDeleteGroup = async () => {
|
|
|
+ if (!activeGroupId.value) return;
|
|
|
+ try {
|
|
|
+ await deletePersonGroup(activeGroupId.value);
|
|
|
+ createMessage.success('删除分组成功');
|
|
|
+ mode.value = 'view';
|
|
|
+ activeGroupId.value = null;
|
|
|
+ await loadData();
|
|
|
+ emit('change');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除分组失败', error);
|
|
|
+ createMessage.error('删除分组失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const startAddMembers = () => {
|
|
|
+ if (!activeGroup.value) return;
|
|
|
+ mode.value = 'addMembers';
|
|
|
+ addKeyword.value = '';
|
|
|
+ selectedAddNames.value = [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const cancelAddMembers = () => {
|
|
|
+ mode.value = 'view';
|
|
|
+ addKeyword.value = '';
|
|
|
+ selectedAddNames.value = [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const setCandidate = (item: CandidateItem, checked: boolean) => {
|
|
|
+ if (checked) {
|
|
|
+ selectedAddNames.value = Array.from(new Set([...selectedAddNames.value, item.name]));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ selectedAddNames.value = selectedAddNames.value.filter((name) => name !== item.name);
|
|
|
+ };
|
|
|
+
|
|
|
+ const confirmAddMembers = async () => {
|
|
|
+ if (!activeGroupId.value || !selectedAddNames.value.length || addingMembers.value) return;
|
|
|
+ addingMembers.value = true;
|
|
|
+ try {
|
|
|
+ await addPersonToGroup({
|
|
|
+ groupId: activeGroupId.value,
|
|
|
+ personList: selectedAddNames.value,
|
|
|
+ });
|
|
|
+ createMessage.success('添加对象成功');
|
|
|
+ mode.value = 'view';
|
|
|
+ addKeyword.value = '';
|
|
|
+ selectedAddNames.value = [];
|
|
|
+ await loadData();
|
|
|
+ emit('change');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('添加对象失败', error);
|
|
|
+ createMessage.error('添加对象失败');
|
|
|
+ } finally {
|
|
|
+ addingMembers.value = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleRemoveMember = async (memberId: string) => {
|
|
|
+ try {
|
|
|
+ await remPersonFromGroup(memberId);
|
|
|
+ createMessage.success('移出分组成功');
|
|
|
+ await loadData();
|
|
|
+ emit('change');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('移出分组失败', error);
|
|
|
+ createMessage.error('移出分组失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleChangeMain = async (relId: string) => {
|
|
|
+ if (!activeGroupId.value) return;
|
|
|
+ try {
|
|
|
+ await changeMainPerson(relId, activeGroupId.value);
|
|
|
+ createMessage.success('变更主要人员成功');
|
|
|
+ await loadData();
|
|
|
+ emit('change');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('变更主要人员失败', error);
|
|
|
+ createMessage.error('变更主要人员失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleClose = () => {
|
|
|
+ mode.value = 'view';
|
|
|
+ resetForm();
|
|
|
+ groupKeyword.value = '';
|
|
|
+ memberKeyword.value = '';
|
|
|
+ addKeyword.value = '';
|
|
|
+ selectedAddNames.value = [];
|
|
|
+ emit('update:open', false);
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => form.groupName,
|
|
|
+ () => {
|
|
|
+ if (groupNameError.value) validateGroupName(Boolean(form.groupName.trim()));
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => form.mainPerson,
|
|
|
+ () => {
|
|
|
+ if (mainPersonError.value) validateMainPerson(Boolean(form.mainPerson));
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.open,
|
|
|
+ async (open) => {
|
|
|
+ if (!open) return;
|
|
|
+ mode.value = 'view';
|
|
|
+ groupKeyword.value = '';
|
|
|
+ memberKeyword.value = '';
|
|
|
+ addKeyword.value = '';
|
|
|
+ selectedAddNames.value = [];
|
|
|
+ resetForm();
|
|
|
+ await loadData();
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+ );
|
|
|
+
|
|
|
+ defineExpose({
|
|
|
+ startCreateGroup,
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less">
|
|
|
+ /* ---------------- 对象分组管理弹框(960) ---------------- */
|
|
|
+ .pg-manage-modal {
|
|
|
+ .ant-modal {
|
|
|
+ width: 960px !important;
|
|
|
+ max-width: calc(100vw - 40px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 600px;
|
|
|
+ max-height: calc(100vh - 40px);
|
|
|
+ padding: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 16px;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 20px 50px rgb(15 23 42 / 18%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-header {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ margin: 0;
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #eff2f7;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-title {
|
|
|
+ color: inherit;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-close {
|
|
|
+ top: 14px;
|
|
|
+ right: 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #98a2b3;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #f4f6fa;
|
|
|
+ color: #5b6577;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-close-x {
|
|
|
+ width: 34px;
|
|
|
+ height: 34px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-body {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 0 !important;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-body,
|
|
|
+ .ant-modal-body .scroll-container,
|
|
|
+ .ant-modal-body .scrollbar__wrap,
|
|
|
+ .ant-modal-body .scrollbar__view,
|
|
|
+ .ant-modal-body .scrollbar__view > div {
|
|
|
+ height: 100%;
|
|
|
+ min-height: 0;
|
|
|
+ max-height: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-spin-nested-loading,
|
|
|
+ .ant-spin-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-modal-title-block {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-modal-title {
|
|
|
+ color: #111827;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-modal-subtitle {
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 400;
|
|
|
+ line-height: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-body {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 268px) minmax(0, 1fr);
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 14px;
|
|
|
+ height: 100%;
|
|
|
+ padding: 12px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---------------- 左侧分组 ---------------- */
|
|
|
+ .pg-groups {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: 0;
|
|
|
+ max-height: calc(100vh - 280px);
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid #eff2f7;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f8fafc;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-groups__head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-groups__title {
|
|
|
+ color: #111827;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+
|
|
|
+ em {
|
|
|
+ margin-left: 4px;
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 11px;
|
|
|
+ font-style: normal;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-add-group-btn.ant-btn {
|
|
|
+ height: 26px;
|
|
|
+ padding: 0 8px;
|
|
|
+ border: 1px solid #bfcdfb;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fff;
|
|
|
+ color: #2a50ec;
|
|
|
+ font-size: 12px;
|
|
|
+ box-shadow: none;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ border-color: #2a50ec;
|
|
|
+ background: #f4f7ff;
|
|
|
+ color: #2a50ec;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:disabled {
|
|
|
+ border-color: #e7ebf3;
|
|
|
+ color: #b6bfcc;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-search.ant-input-affix-wrapper {
|
|
|
+ height: 30px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ border-color: #e7ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: none;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-list {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ min-height: 0;
|
|
|
+ padding-right: 2px;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ width: 100%;
|
|
|
+ padding: 8px 10px;
|
|
|
+ border: 1px solid #e7ebf3;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #fff;
|
|
|
+ text-align: left;
|
|
|
+ cursor: pointer;
|
|
|
+ appearance: none;
|
|
|
+ transition:
|
|
|
+ border-color 0.16s ease,
|
|
|
+ background 0.16s ease;
|
|
|
+
|
|
|
+ &:hover:not(:disabled) {
|
|
|
+ border-color: #bfcdfb;
|
|
|
+ background: #fbfcff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ border-color: #bfcdfb;
|
|
|
+ background: #eef2ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:disabled {
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item__row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item__name {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ color: #1f2937;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 17px;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item.active .pg-group-item__name {
|
|
|
+ color: #2448c9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item__count {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ min-width: 18px;
|
|
|
+ height: 16px;
|
|
|
+ padding: 0 5px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #f1f5f9;
|
|
|
+ color: #5b6577;
|
|
|
+ font-size: 10px;
|
|
|
+ line-height: 16px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item.active .pg-group-item__count {
|
|
|
+ background: #dfe7ff;
|
|
|
+ color: #2448c9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item__meta {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 10px;
|
|
|
+ line-height: 15px;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-groups__foot {
|
|
|
+ margin-top: 6px;
|
|
|
+ padding-top: 8px;
|
|
|
+ border-top: 1px solid #eff2f7;
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 10px;
|
|
|
+ line-height: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---------------- 右侧面板 ---------------- */
|
|
|
+
|
|
|
+ .pg-panel {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ min-width: 0;
|
|
|
+ min-height: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel__head {
|
|
|
+ display: flex;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 1px solid #eff2f7;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel__heading {
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel__title {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #111827;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 22px;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel__sub {
|
|
|
+ margin-top: 1px;
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel__actions {
|
|
|
+ display: flex;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+
|
|
|
+ .ant-btn {
|
|
|
+ height: 28px;
|
|
|
+ padding: 0 8px;
|
|
|
+ border-color: #e7ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #5b6577;
|
|
|
+ font-size: 12px;
|
|
|
+ box-shadow: none;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ border-color: #bfcdfb;
|
|
|
+ background: #f8faff;
|
|
|
+ color: #2a50ec;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-btn-primary {
|
|
|
+ border-color: #2a50ec;
|
|
|
+ background: #2a50ec;
|
|
|
+ color: #fff;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ border-color: #4b6bf0;
|
|
|
+ background: #4b6bf0;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:disabled,
|
|
|
+ &.ant-btn-disabled {
|
|
|
+ border-color: #e7ebf3;
|
|
|
+ background: #f4f6fa;
|
|
|
+ color: #b6bfcc;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-btn-dangerous {
|
|
|
+ color: #dc2626;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ border-color: #fecaca;
|
|
|
+ background: #fef2f2;
|
|
|
+ color: #dc2626;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-toolbar {
|
|
|
+ display: flex;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 0 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-search.ant-input-affix-wrapper {
|
|
|
+ width: 200px;
|
|
|
+ height: 30px;
|
|
|
+ border-color: #e7ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: none;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-total {
|
|
|
+ margin-left: auto;
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 11px;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-list {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 6px 0 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 6px 10px;
|
|
|
+ border: 1px solid #eff2f7;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #fff;
|
|
|
+ transition:
|
|
|
+ border-color 0.16s ease,
|
|
|
+ background 0.16s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #dfe7f5;
|
|
|
+ background: #fbfcff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-avatar.ant-avatar {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ background: #e8eef8;
|
|
|
+ color: #5b74b1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-avatar svg {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-info {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-name-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-name {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #1f2937;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 20px;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-tag {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #eef2ff;
|
|
|
+ color: #2448c9;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-desc {
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-actions {
|
|
|
+ display: flex;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-link {
|
|
|
+ padding: 0;
|
|
|
+ border: 0;
|
|
|
+ background: transparent;
|
|
|
+ color: #5b6577;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 500;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #2a50ec;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-link--danger:hover {
|
|
|
+ color: #dc2626;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---------------- 表单 / 添加成员 ---------------- */
|
|
|
+ .pg-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ max-width: 440px;
|
|
|
+ padding-top: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-form__head {
|
|
|
+ margin-bottom: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-form__title {
|
|
|
+ color: #111827;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 22px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-form__desc {
|
|
|
+ margin-top: 2px;
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 17px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-field {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-field__label {
|
|
|
+ color: #5b6577;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 18px;
|
|
|
+
|
|
|
+ em {
|
|
|
+ color: #e5484d;
|
|
|
+ font-style: normal;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-field .ant-input,
|
|
|
+ .pg-field .ant-input-affix-wrapper,
|
|
|
+ .pg-field .ant-select-selector {
|
|
|
+ height: 32px !important;
|
|
|
+ border-color: #e7ebf3;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: none;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-field .ant-select-single .ant-select-selector .ant-select-selection-item,
|
|
|
+ .pg-field .ant-select-single .ant-select-selector .ant-select-selection-placeholder {
|
|
|
+ line-height: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-field textarea.ant-input {
|
|
|
+ height: auto;
|
|
|
+ min-height: 68px;
|
|
|
+ padding: 6px 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-field__error {
|
|
|
+ color: #e5484d;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-form__actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+ padding-top: 4px;
|
|
|
+
|
|
|
+ .ant-btn {
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-candidate-list {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 6px 0 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-candidate {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ border: 1px solid #eff2f7;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ transition:
|
|
|
+ border-color 0.16s ease,
|
|
|
+ background 0.16s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #dfe7f5;
|
|
|
+ background: #fbfcff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-selected {
|
|
|
+ border-color: #bfcdfb;
|
|
|
+ background: #f4f7ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-candidate__avatar {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-candidate__body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-candidate__name {
|
|
|
+ color: #1f2937;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-candidate__meta {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #98a2b3;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 16px;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---------------- 空状态 / 底部 ---------------- */
|
|
|
+ .pg-empty {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 6px;
|
|
|
+ padding: 20px 12px;
|
|
|
+ color: #98a2b3;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-empty--page {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-empty--groups {
|
|
|
+ padding: 16px 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-empty__icon {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #f4f6fa;
|
|
|
+ color: #b6c2d6;
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-empty__title {
|
|
|
+ color: #5b6577;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-empty__desc {
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 17px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-empty .ant-btn {
|
|
|
+ height: 30px;
|
|
|
+ margin-top: 2px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 960px) {
|
|
|
+ .pg-body {
|
|
|
+ grid-template-columns: minmax(0, 1fr);
|
|
|
+ height: auto;
|
|
|
+ max-height: calc(100vh - 240px);
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-groups {
|
|
|
+ max-height: 240px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-list {
|
|
|
+ max-height: 140px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel {
|
|
|
+ min-height: 320px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel__actions {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ html[data-theme='dark'] {
|
|
|
+ .pg-manage-modal {
|
|
|
+ .ant-modal-content,
|
|
|
+ .ant-modal-header,
|
|
|
+ .ant-modal-body {
|
|
|
+ background: #111827;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-modal-title,
|
|
|
+ .pg-modal-title,
|
|
|
+ .pg-panel__title,
|
|
|
+ .pg-group-item__name,
|
|
|
+ .pg-member-name,
|
|
|
+ .pg-form__title,
|
|
|
+ .pg-candidate__name {
|
|
|
+ color: #e5e7eb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-modal-subtitle,
|
|
|
+ .pg-panel__sub,
|
|
|
+ .pg-group-item__meta,
|
|
|
+ .pg-member-desc,
|
|
|
+ .pg-form__desc,
|
|
|
+ .pg-candidate__meta {
|
|
|
+ color: #94a3b8;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-groups,
|
|
|
+ .pg-group-item,
|
|
|
+ .pg-member-item,
|
|
|
+ .pg-candidate {
|
|
|
+ border-color: #1f2937;
|
|
|
+ background: #0f172a;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-panel__head,
|
|
|
+ .pg-groups__foot {
|
|
|
+ border-color: #1f2937;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-group-item.active {
|
|
|
+ border-color: #2b3c78;
|
|
|
+ background: #1a2550;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pg-member-avatar.ant-avatar {
|
|
|
+ background: #1e293b;
|
|
|
+ color: #93b4ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|