Skip to content

Commit 1639f3f

Browse files
fit2cloudwxxfit2-zhao
authored andcommitted
feat: crm approval popover
1 parent a09913f commit 1639f3f

13 files changed

Lines changed: 478 additions & 36 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<template>
2+
<n-popover class="!p-0" :show-arrow="false" :disabled="props.disabled" @update:show="handlePopoverShow">
3+
<template #trigger>
4+
<slot>
5+
<CrmApprovalStatus :status="props.status" />
6+
</slot>
7+
</template>
8+
<div class="crm-reject-popover">
9+
<div class="crm-reject-popover__header">
10+
<div class="crm-reject-popover__title">{{ title }}</div>
11+
<n-button v-if="showMore" text type="primary" class="!text-[14px]" @click="emit('more')">
12+
{{ t('common.more') }}
13+
</n-button>
14+
</div>
15+
<n-spin :show="loading">
16+
<n-scrollbar class="max-h-[40vh]">
17+
<CrmApprovalApproverList v-model:active-id="activeApproverId" :approvers="approvers" />
18+
<div v-if="currentApproverReason" class="crm-reject-popover__reasons">
19+
<div class="crm-reject-popover__reason">
20+
{{ currentApproverReason }}
21+
</div>
22+
</div>
23+
</n-scrollbar>
24+
</n-spin>
25+
</div>
26+
</n-popover>
27+
</template>
28+
29+
<script setup lang="ts">
30+
import { NButton, NPopover, NScrollbar, NSpin } from 'naive-ui';
31+
32+
import { ProcessStatusEnum } from '@lib/shared/enums/process';
33+
import { useI18n } from '@lib/shared/hooks/useI18n';
34+
import { ProcessStatusType } from '@lib/shared/models/system/process';
35+
36+
import CrmApprovalStatus from '@/components/business/crm-approval-status/index.vue';
37+
import CrmApprovalApproverList, { type ApproverItem } from '@/components/business/crm-approver-avatar-list/index.vue';
38+
39+
import useRejectPopoverDetail, { type ApprovalPopoverFormKeyType } from './useApprovalPopoverDetail';
40+
41+
const props = withDefaults(
42+
defineProps<{
43+
status: ProcessStatusType;
44+
formKey: ApprovalPopoverFormKeyType;
45+
sourceId?: string;
46+
title?: string;
47+
showMore?: boolean;
48+
disabled?: boolean;
49+
}>(),
50+
{
51+
title: '',
52+
showMore: true,
53+
}
54+
);
55+
56+
const emit = defineEmits<{
57+
(e: 'more', sourceId?: string): void;
58+
}>();
59+
60+
const { t } = useI18n();
61+
const { getApprovalPopoverDetail } = useRejectPopoverDetail();
62+
63+
const loading = ref(false);
64+
const approvers = ref<ApproverItem[]>([]);
65+
66+
const activeApproverId = ref('');
67+
const currentApproverMap = computed(() => new Map(approvers.value.map((item) => [item.id, item])));
68+
const currentApproverReason = computed(
69+
() => currentApproverMap.value?.get(activeApproverId.value)?.approveReason ?? '-'
70+
);
71+
72+
const title = computed(() => props.title || t('common.approver'));
73+
74+
async function initDetail() {
75+
if (!props.sourceId) return;
76+
loading.value = true;
77+
try {
78+
const res = await getApprovalPopoverDetail(props.formKey, props.sourceId);
79+
// todo xinxinwu
80+
approvers.value = res.approveUserList || [];
81+
approvers.value = [
82+
{
83+
approveReason:
84+
'原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因原因',
85+
approveResult: ProcessStatusEnum.UNAPPROVED,
86+
id: '31649611428863184211',
87+
name: '吴鑫鑫',
88+
avatar:
89+
'https://s1-imfile.feishucdn.com/static-resource/v1/v3_007d_71e27f5a-1b9a-46fc-bac5-cfd897657dag~?image_size=240x240&cut_type=&quality=&format=png&sticker_format=.webp',
90+
},
91+
];
92+
activeApproverId.value = approvers.value[0]?.id ?? '';
93+
} catch (error) {
94+
// eslint-disable-next-line no-console
95+
console.log(error);
96+
} finally {
97+
loading.value = false;
98+
}
99+
}
100+
101+
function handlePopoverShow(show: boolean) {
102+
if (!show) return;
103+
if (!props.sourceId) return;
104+
initDetail();
105+
}
106+
</script>
107+
108+
<style scoped lang="less">
109+
.crm-reject-popover {
110+
padding: 16px;
111+
width: 344px;
112+
border-radius: 12px;
113+
background: var(--text-n10);
114+
}
115+
.crm-reject-popover__header {
116+
display: flex;
117+
justify-content: space-between;
118+
align-items: center;
119+
}
120+
.crm-reject-popover__title {
121+
font-size: 14px;
122+
font-weight: 600;
123+
color: var(--text-n1);
124+
line-height: 20px;
125+
}
126+
.crm-reject-popover__reasons {
127+
display: flex;
128+
flex-direction: column;
129+
margin-top: 14px;
130+
background: var(--text-n9);
131+
gap: 8px;
132+
}
133+
.crm-reject-popover__reason {
134+
display: box;
135+
overflow: hidden;
136+
padding: 4px 12px;
137+
font-size: 14px;
138+
border-radius: 4px;
139+
text-overflow: ellipsis;
140+
color: var(--text-n2);
141+
line-height: 22px;
142+
-webkit-box-orient: vertical;
143+
-webkit-line-clamp: 2;
144+
}
145+
.crm-reject-popover__empty {
146+
font-size: 14px;
147+
color: var(--text-n4);
148+
line-height: 20px;
149+
}
150+
</style>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { FormDesignKeyEnum } from '@lib/shared/enums/formDesignEnum';
2+
import { ProcessStatusEnum } from '@lib/shared/enums/process';
3+
import { ProcessStatusType } from '@lib/shared/models/system/process';
4+
5+
import { ApproverItem } from '@/components/business/crm-approver-avatar-list/index.vue';
6+
7+
export type ApprovalPopoverFormKeyType =
8+
| FormDesignKeyEnum.CONTRACT
9+
| FormDesignKeyEnum.CONTRACT_INVOICE
10+
| FormDesignKeyEnum.OPPORTUNITY_QUOTATION
11+
| FormDesignKeyEnum.ORDER;
12+
13+
export interface ApprovalPopoverDetail {
14+
resourceId: string;
15+
approveStatus: ProcessStatusType;
16+
approveUserList: ApproverItem[];
17+
}
18+
19+
// todo api
20+
const detailApiMap: Record<ApprovalPopoverFormKeyType, (sourceId: string) => Promise<ApprovalPopoverDetail>> = {
21+
[FormDesignKeyEnum.OPPORTUNITY_QUOTATION]: () =>
22+
Promise.resolve({ approveStatus: ProcessStatusEnum.NONE, approveUserList: [], resourceId: '' }),
23+
[FormDesignKeyEnum.CONTRACT]: (sourceId: string) =>
24+
Promise.resolve({
25+
approveStatus: ProcessStatusEnum.NONE,
26+
approveUserList: [],
27+
resourceId: '',
28+
}),
29+
[FormDesignKeyEnum.CONTRACT_INVOICE]: (sourceId: string) =>
30+
Promise.resolve({
31+
approveStatus: ProcessStatusEnum.NONE,
32+
approveUserList: [],
33+
resourceId: '',
34+
}),
35+
[FormDesignKeyEnum.ORDER]: (sourceId: string) =>
36+
Promise.resolve({
37+
approveStatus: ProcessStatusEnum.NONE,
38+
approveUserList: [],
39+
resourceId: '',
40+
}),
41+
};
42+
43+
function transformDetail(detail: ApprovalPopoverDetail): ApproverItem[] {
44+
return detail.approveUserList.map((e) => {
45+
return {
46+
id: e.id,
47+
name: e.name,
48+
avatar: e.avatar ?? '',
49+
approveResult: e.approveResult,
50+
approveReason: e.approveReason,
51+
};
52+
});
53+
}
54+
55+
export default function useApprovalPopoverDetail() {
56+
async function getApprovalPopoverDetail(formKey: ApprovalPopoverFormKeyType, sourceId: string) {
57+
const api = detailApiMap[formKey];
58+
if (!api) {
59+
return {
60+
approveUserList: [],
61+
};
62+
}
63+
64+
const detail = await api(sourceId);
65+
return {
66+
...detail,
67+
approveUserList: transformDetail(detail),
68+
};
69+
}
70+
71+
return {
72+
getApprovalPopoverDetail,
73+
};
74+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<template>
2+
<div v-if="approvers.length" class="crm-approver-avatar-list">
3+
<div
4+
v-for="approver in approvers"
5+
:key="approver.id"
6+
class="crm-approver-avatar-list__item"
7+
:class="{ 'crm-approver-avatar-list__item--active': approver.id === activeApproverId }"
8+
>
9+
<div
10+
class="crm-approver-avatar-list__avatar-wrap"
11+
:class="{ 'crm-approver-avatar-list__avatar-wrap--active': approver.id === activeApproverId }"
12+
>
13+
<CrmAvatar
14+
:avatar="approver.avatar"
15+
:word="approver.name"
16+
:is-user="false"
17+
:size="props.size"
18+
class="cursor-pointer"
19+
@click="toggleActive(approver)"
20+
/>
21+
<div
22+
v-if="approver.approveResult"
23+
:class="getStatusClass(approver.approveResult)"
24+
class="crm-approver-avatar-list__status"
25+
>
26+
<CrmIcon :type="getStatusIcon(approver.approveResult)" :size="14" />
27+
</div>
28+
</div>
29+
<div
30+
class="one-line-text crm-approver-avatar-list__name cursor-pointer"
31+
:class="{
32+
'crm-approver-avatar-list__name--active-approval': isActiveApproval(approver),
33+
'crm-approver-avatar-list__name--active-rejected': isActiveRejected(approver),
34+
'crm-approver-avatar-list__name--active': approver.id === activeApproverId,
35+
}"
36+
@click="toggleActive(approver)"
37+
>
38+
{{ approver.name }}
39+
</div>
40+
</div>
41+
</div>
42+
</template>
43+
44+
<script setup lang="ts">
45+
import { ProcessStatusEnum } from '@lib/shared/enums/process';
46+
import { ProcessStatusType } from '@lib/shared/models/system/process';
47+
import type { UserInfo } from '@lib/shared/models/user';
48+
49+
import CrmIcon from '@/components/pure/crm-icon-font/index.vue';
50+
import CrmAvatar from '@/components/business/crm-avatar/index.vue';
51+
52+
export interface ApproverItem extends Pick<UserInfo, 'id' | 'name' | 'avatar'> {
53+
approveResult: ProcessStatusType;
54+
approveReason: string;
55+
}
56+
57+
const props = withDefaults(
58+
defineProps<{
59+
approvers?: ApproverItem[];
60+
size?: number;
61+
}>(),
62+
{
63+
approvers: () => [],
64+
size: 24,
65+
}
66+
);
67+
68+
const approvers = computed(() => props.approvers || []);
69+
70+
function getStatusIcon(status: ProcessStatusType) {
71+
return status === ProcessStatusEnum.UNAPPROVED ? 'iconicon_close_circle_filled' : 'iconicon_succeed_filled';
72+
}
73+
74+
function getStatusClass(status: ProcessStatusType) {
75+
switch (status) {
76+
case ProcessStatusEnum.UNAPPROVED:
77+
return 'text-[var(--error-red)]';
78+
case ProcessStatusEnum.APPROVED:
79+
return 'text-[var(--success-green)]';
80+
default:
81+
return 'text-[var(--text-n4)]';
82+
}
83+
}
84+
85+
const activeApproverId = defineModel<string | number>('activeId', {
86+
default: '',
87+
});
88+
89+
function isActiveApproval(approver: ApproverItem) {
90+
return approver.approveResult === ProcessStatusEnum.APPROVED && approver.id === activeApproverId.value;
91+
}
92+
93+
function isActiveRejected(approver: ApproverItem) {
94+
return approver.approveResult === ProcessStatusEnum.UNAPPROVED && approver.id === activeApproverId.value;
95+
}
96+
97+
function toggleActive(approver: ApproverItem) {
98+
activeApproverId.value = approver.id;
99+
}
100+
</script>
101+
102+
<style scoped lang="less">
103+
.crm-approver-avatar-list {
104+
display: flex;
105+
padding: 8px 2px;
106+
flex-wrap: wrap;
107+
gap: 8px;
108+
}
109+
.crm-approver-avatar-list__item {
110+
display: flex;
111+
align-items: center;
112+
min-width: 0;
113+
max-width: 100%;
114+
gap: 8px;
115+
cursor: pointer;
116+
}
117+
.crm-approver-avatar-list__avatar-wrap {
118+
position: relative;
119+
display: flex;
120+
justify-content: center;
121+
align-items: center;
122+
width: v-bind('`${props.size}px`');
123+
height: v-bind('`${props.size}px`');
124+
border-radius: 50%;
125+
transition: box-shadow 0.18s ease;
126+
@apply flex flex-shrink-0 items-center justify-between;
127+
&--active {
128+
box-shadow: 0 0 0 0.5px var(--primary-8);
129+
}
130+
}
131+
.crm-approver-avatar-list__item:hover {
132+
.crm-approver-avatar-list__name {
133+
color: var(--primary-8);
134+
}
135+
}
136+
.crm-approver-avatar-list__status {
137+
position: absolute;
138+
top: -3px;
139+
right: -3px;
140+
display: flex;
141+
justify-content: center;
142+
align-items: center;
143+
width: 12px;
144+
height: 12px;
145+
border-radius: 50%;
146+
background-color: var(--text-n10);
147+
}
148+
.crm-approver-avatar-list__name {
149+
color: var(--text-n1);
150+
&--active-approval {
151+
color: var(--primary-0);
152+
}
153+
&--active-rejected {
154+
color: var(--primary-1);
155+
}
156+
&--active {
157+
color: var(--primary-8);
158+
}
159+
}
160+
.crm-approver-avatar-list__empty {
161+
color: var(--text-n4);
162+
}
163+
</style>

0 commit comments

Comments
 (0)