Files
xk-admin/apps/web-antd/src/components/promoter/PromoterDetailModal.vue
李琦 a524982b87
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
1. 业务员查看维度增加
2026-07-11 18:34:29 +08:00

269 lines
7.7 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.
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useVbenModal } from '@vben/common-ui';
import { openRouteInNewWindow } from '@vben/utils';
import dayjs from 'dayjs';
import { Avatar, Descriptions, Empty, message, Spin, Table } from 'ant-design-vue';
import { getPromoterDetailApi } from '#/views/system/store-input/api';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
type PromoterStoreItem = {
store_id: number;
name: string;
type: number;
type_label: string;
input_at: number;
total_amount: string;
month_amount: string;
};
type PromoterDetail = {
id: number;
nick_name: string;
avatar: string;
code: string;
phone: string;
role_name: string;
clinic_store_count: number;
pharmacy_store_count: number;
store_total_count: number;
pending_clinic_count: number;
pending_pharmacy_count: number;
store_list: PromoterStoreItem[];
total_valid_amount: string;
month_valid_amount: string;
};
const router = useRouter();
const loading = ref(false);
const detail = ref<PromoterDetail | null>(null);
const storeColumns = [
{ title: '门店名称', dataIndex: 'name', key: 'name', ellipsis: true },
{ title: '类型', dataIndex: 'type_label', key: 'type_label', width: 72 },
{ title: '录入时间', dataIndex: 'input_at', key: 'input_at', width: 160 },
{ title: '总营业额', dataIndex: 'total_amount', key: 'total_amount', width: 120, align: 'right' as const },
{ title: '本月营业额', dataIndex: 'month_amount', key: 'month_amount', width: 120, align: 'right' as const },
];
/** 解析商品订单列表路由(菜单动态注册,path 含 product-order) */
function findProductOrderListPath(): string | null {
const routes = router.getRoutes();
const hit = routes.find(
(r) =>
typeof r.path === 'string' &&
r.path.length > 0 &&
!r.redirect &&
/product-order/i.test(r.path),
);
return hit?.path ?? null;
}
/** 金额格式化为 ¥ + 2 位小数 */
function formatMoney(value: number | string | undefined) {
const num = Number(value || 0);
return `¥${num.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}
/** Unix 时间戳格式化为可读录入时间 */
function formatInputAt(timestamp?: number) {
if (!timestamp) return '-';
return dayjs.unix(timestamp).format('YYYY-MM-DD HH:mm');
}
/** 新标签页打开商品订单,按门店及时间范围筛选 */
function openStoreOrders(storeId: number, timeScope: 'all' | 'month') {
const path = findProductOrderListPath();
if (!path) {
message.warning('未找到商品订单菜单路由,请从左侧菜单进入商品订单');
return;
}
const resolved = router.resolve({
path,
query: {
store_id: String(storeId),
time_scope: timeScope,
},
});
openRouteInNewWindow(resolved.fullPath);
}
const [Modal, modalApi] = useVbenModal({
title: '业务员详情',
class: 'w-[860px]',
footer: false,
onOpenChange(isOpen) {
if (!isOpen) {
detail.value = null;
return;
}
const data = modalApi.getData<{ id?: number; code?: string }>();
loadDetail(data?.id, data?.code);
},
});
async function loadDetail(id?: number, code?: string) {
loading.value = true;
try {
detail.value = await getPromoterDetailApi({ id, code });
} finally {
loading.value = false;
}
}
</script>
<template>
<Modal>
<Spin :spinning="loading">
<div v-if="detail" class="promoter-detail">
<div class="promoter-detail__header">
<Avatar
v-if="resolveAvatarUrl(detail.avatar)"
:src="resolveAvatarUrl(detail.avatar)"
:size="64"
/>
<Avatar v-else :size="64">{{ (detail.nick_name || '?').charAt(0) }}</Avatar>
<div class="promoter-detail__title">{{ detail.nick_name || '-' }}</div>
<div class="promoter-detail__role">{{ detail.role_name || '业务员' }}</div>
</div>
<Descriptions bordered :column="2" size="small" class="promoter-detail__desc">
<Descriptions.Item label="推广码">{{ detail.code || '-' }}</Descriptions.Item>
<Descriptions.Item label="手机号">
<SensitiveText :record="detail" field="phone" />
</Descriptions.Item>
<Descriptions.Item label="绑定诊所(含待审核)">
{{ detail.clinic_store_count ?? 0 }} 家
<span v-if="detail.pending_clinic_count" class="promoter-detail__pending">
(待审核 {{ detail.pending_clinic_count }})
</span>
</Descriptions.Item>
<Descriptions.Item label="绑定药店(含待审核)">
{{ detail.pharmacy_store_count ?? 0 }} 家
<span v-if="detail.pending_pharmacy_count" class="promoter-detail__pending">
(待审核 {{ detail.pending_pharmacy_count }})
</span>
</Descriptions.Item>
<Descriptions.Item label="绑定门店总营业额">
{{ formatMoney(detail.total_valid_amount) }}
</Descriptions.Item>
<Descriptions.Item label="本月营业额">
{{ formatMoney(detail.month_valid_amount) }}
</Descriptions.Item>
</Descriptions>
<div class="promoter-detail__section-title">正式门店列表</div>
<Table
v-if="detail.store_list?.length"
:columns="storeColumns"
:data-source="detail.store_list"
:pagination="false"
:scroll="{ y: 320 }"
row-key="store_id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'input_at'">
{{ formatInputAt(record.input_at) }}
</template>
<template v-else-if="column.key === 'total_amount'">
<button
type="button"
class="promoter-detail__amount-link"
@click="openStoreOrders(record.store_id, 'all')"
>
{{ formatMoney(record.total_amount) }}
</button>
</template>
<template v-else-if="column.key === 'month_amount'">
<button
type="button"
class="promoter-detail__amount-link"
@click="openStoreOrders(record.store_id, 'month')"
>
{{ formatMoney(record.month_amount) }}
</button>
</template>
</template>
</Table>
<Empty v-else class="promoter-detail__empty" description="暂无绑定正式门店" />
</div>
</Spin>
</Modal>
</template>
<style scoped>
.promoter-detail__header {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.promoter-detail__title {
font-size: 18px;
font-weight: 600;
color: #1d2129;
}
.promoter-detail__role {
font-size: 13px;
color: #86909c;
}
.promoter-detail__desc {
margin-top: 8px;
}
.promoter-detail__pending {
margin-left: 4px;
font-size: 12px;
color: #fa8c16;
}
.promoter-detail__section-title {
margin: 16px 0 8px;
font-size: 14px;
font-weight: 600;
color: #1d2129;
}
.promoter-detail__empty {
padding: 24px 0;
}
.promoter-detail__amount-link {
padding: 0;
font: inherit;
color: #1677ff;
cursor: pointer;
background: none;
border: none;
}
.promoter-detail__amount-link:hover {
color: #4096ff;
text-decoration: underline;
}
.dark .promoter-detail__title,
.dark .promoter-detail__section-title {
color: #f3f4f6;
}
.dark .promoter-detail__role {
color: #9ca3af;
}
.dark .promoter-detail__amount-link {
color: #60a5fa;
}
</style>