Files
gao-customer-h5/src/pages/customer/CustomerInviteeList.vue
T
2026-08-20 15:53:03 +08:00

386 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-can="permissionCodes.customer.read" class="customer-invitee-list">
<customer-profile-card
v-if="customer"
class="customer"
:customer="customer"
/>
<div class="invite-list">
<div class="header">
<div class="title">邀请客户</div>
<div v-can="permissionCodes.customer.update" class="actions">
<t-button
theme="primary"
variant="outline"
size="extra-small"
content="选择添加"
@click="selectorVisible = true"
/>
<t-button
theme="primary"
size="extra-small"
content="扫码添加"
@click="openScanPopup"
/>
</div>
</div>
<t-cell-group theme="card">
<t-swipe-cell
v-for="(item, index) in invitees"
:key="item.id || item.code || index"
class="item"
>
<t-cell
class="cell"
:title="item.name || item.code || '-'"
arrow
hover
@click="toCustomerDetail(item)"
>
<template #description>
<telephone-link :value="item.telephone" placeholder="未填写电话" />
</template>
<template #leftIcon>
<t-image
v-if="getCustomerPhotoURL(item)"
class="avatar"
:src="getCustomerPhotoURL(item)"
:alt="item.name"
fit="cover"
/>
<div v-else class="avatar fallback" v-text="(item.name || '客').slice(0, 1)" />
</template>
</t-cell>
<template #right>
<div v-can="permissionCodes.customer.update" class="remove bg-red white" @click="removeInvitee(item)">删除</div>
</template>
</t-swipe-cell>
<div v-if="!invitees.length" class="empty" v-text="isLoading ? '正在加载邀请客户' : '暂无邀请客户'" />
</t-cell-group>
</div>
</div>
<customer-selector
v-model="selectedInviteeId"
v-model:visible="selectorVisible"
@change="handleInviteeSelected"
/>
<t-popup v-model="scanPopupVisible" placement="bottom">
<div class="scan-popup bg-white">
<div class="header">
<t-button
variant="text"
size="small"
@click="closeScanPopup"
content="取消"
/>
<h3>扫码添加邀请客户</h3>
<t-button
theme="primary"
size="small"
content="确认"
@click="confirmScannedInvitee"
/>
</div>
<code-scanner
v-if="scanPopupVisible"
:visible="scanPopupVisible"
:status-text="scanStatus"
active-text="正在识别客户编码,请将二维码移入取景框"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
<div v-if="pendingInvitee" class="confirm">
<customer-profile-card :customer="pendingInvitee" />
</div>
</div>
</t-popup>
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import CustomerAPI from "@/api/CustomerAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer";
import { getCustomerPhotoURL } from "@/utils/customer";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "CustomerInviteeList"
});
const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
const userStore = useUserStore();
const customer = ref<Customer>();
const invitees = ref<Customer[]>([]);
const isLoading = ref(false);
const selectedInviteeId = ref("");
const selectorVisible = ref(false);
const scanPopupVisible = ref(false);
const pendingInvitee = ref<Customer>();
const scanStatus = ref("点击扫码开始识别");
let latestDecodedText = "";
const canUpdateCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.update));
// ---------- 加载 ----------
async function loadCustomer(): Promise<void> {
const customerId = String(route.params.id || "");
if (!customerId) {
customer.value = undefined;
invitees.value = [];
return;
}
isLoading.value = true;
try {
customer.value = await CustomerAPI.detail(customerId);
invitees.value = await CustomerAPI.listInvited(customerId);
} catch (err) {
customer.value = undefined;
invitees.value = [];
console.warn("邀请客户加载失败", err);
Toast.warning("邀请客户加载失败");
} finally {
isLoading.value = false;
}
}
async function loadInvitees(): Promise<void> {
const customerId = customer.value?.id;
if (!customerId) {
invitees.value = [];
return;
}
invitees.value = await CustomerAPI.listInvited(customerId);
}
// ---------- 添加邀请客户 ----------
function openScanPopup(): void {
if (!canUpdateCustomer.value) {
return;
}
pendingInvitee.value = undefined;
latestDecodedText = "";
scanStatus.value = "正在启动扫码预览";
scanPopupVisible.value = true;
}
function closeScanPopup(): void {
scanPopupVisible.value = false;
}
async function handleDecodedText(rawText: string): Promise<void> {
const normalizedText = rawText.trim();
if (!normalizedText || normalizedText === latestDecodedText) {
return;
}
latestDecodedText = normalizedText;
await settingStore.playScanSuccessSound();
const matched = await loadCustomerByCode(normalizedText);
if (!matched) {
scanStatus.value = `已识别 ${normalizedText},未匹配客户档案。`;
return;
}
pendingInvitee.value = matched;
scanStatus.value = `已识别 ${matched.code},请确认是否添加 ${matched.name}。`;
}
function handleScanStatusChange(value: string): void {
scanStatus.value = value;
}
async function confirmScannedInvitee(): Promise<void> {
if (!pendingInvitee.value) {
Toast.warning("请先扫码识别客户");
return;
}
if (await addInvitee(pendingInvitee.value)) {
closeScanPopup();
}
}
async function handleInviteeSelected(id: string): Promise<void> {
if (!id) {
return;
}
selectedInviteeId.value = id;
const invitee = await CustomerAPI.detail(id);
await addInvitee(invitee);
}
async function addInvitee(invitee: Customer): Promise<boolean> {
if (!canUpdateCustomer.value || !customer.value?.id || !invitee.id) {
return false;
}
if (invitee.id === customer.value.id) {
Toast.warning("不能把客户本人添加为邀请客户");
return false;
}
if (isCustomerAncestor(invitee)) {
Toast.warning("不能把上级邀请人添加为邀请客户");
return false;
}
await CustomerAPI.saveInvite(customer.value.id, invitee.id);
await loadInvitees();
Toast.success(`已添加 ${invitee.name || invitee.code}`);
return true;
}
function isCustomerAncestor(invitee: Customer): boolean {
let current = customer.value?.introducerCustomer;
while (current?.id) {
if (current.id === invitee.id) {
return true;
}
current = current.introducerCustomer;
}
return false;
}
// ---------- 删除邀请关系 ----------
async function removeInvitee(item: Customer): Promise<void> {
if (!canUpdateCustomer.value || !item.id) {
return;
}
await CustomerAPI.deleteInvite(item.id);
await loadInvitees();
Toast.success("已删除邀请关系");
}
// ---------- 导航 ----------
async function loadCustomerByCode(code: string): Promise<Customer | undefined> {
return await CustomerAPI.findByCode(code) || undefined;
}
async function toCustomerDetail(target: Customer): Promise<void> {
if (!target.id) {
return;
}
await router.push(`/management/customers/${target.id}`);
}
onMounted(loadCustomer);
watch(() => route.params.id, loadCustomer);
</script>
<style scoped lang="less">
.customer-invitee-list {
gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
display: flex;
flex-direction: column;
.customer {
margin: 0 1rem;
}
.invite-list {
.header {
padding: 0 var(--td-cell-group-title-padding-left, 16px);
display: flex;
font-size: var(--td-cell-group-title-font-size, 14px);
text-align: left;
background: var(--td-cell-group-title-bg-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
align-items: center;
line-height: var(--td-cell-group-title-line-height, 45px);
justify-content: space-between;
.title {
color: var(--td-cell-group-title-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, .4))));
}
.actions {
gap: .875rem;
display: flex;
}
}
.item {
&:last-child {
:deep(.t-cell:after) {
display: none;
}
}
.cell {
padding: 6px 10px 6px 6px;
.avatar {
width: 3.25rem;
height: 3.25rem;
}
}
}
.remove {
width: fit-content;
padding: 0 1rem;
display: flex;
align-items: center;
}
.empty {
padding: 1rem;
color: #52607a;
text-align: center;
background: #f7faff;
border-radius: 1rem;
}
}
}
.scan-popup {
gap: 1rem;
padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom, 0)) 1rem;
display: flex;
overflow: auto;
max-height: 86vh;
border-radius: 1.25rem 1.25rem 0 0;
flex-direction: column;
.header {
display: flex;
align-items: center;
justify-content: space-between;
h3 {
margin: 0;
font-size: 1rem;
}
}
}
:global(html.theme-dark) {
.customer-invitee-list {
.empty {
color: #94a3b8;
background: rgba(30, 41, 59, .9);
}
}
.scan-popup {
color: #e2e8f0;
background: rgba(15, 23, 42, .96);
}
}
</style>