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
269 lines
7.7 KiB
Vue
269 lines
7.7 KiB
Vue
<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>
|