1. 业务员查看维度增加
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
CI / CI OK (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

This commit is contained in:
李琦
2026-07-11 17:20:57 +08:00
parent 28ed398581
commit daafdcaed7
13 changed files with 1573 additions and 32 deletions

View File

@@ -1,14 +1,27 @@
<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 { Avatar, Descriptions, Spin } from 'ant-design-vue';
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;
@@ -16,18 +29,74 @@ type PromoterDetail = {
code: string;
phone: string;
role_name: string;
clinic_input_count: number;
pharmacy_input_count: number;
clinic_store_count: number;
pharmacy_store_count: number;
store_total_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-[480px]',
class: 'w-[860px]',
footer: false,
onOpenChange(isOpen) {
if (!isOpen) {
@@ -63,24 +132,59 @@ async function loadDetail(id?: number, code?: string) {
<div class="promoter-detail__title">{{ detail.nick_name || '-' }}</div>
<div class="promoter-detail__role">{{ detail.role_name || '业务员' }}</div>
</div>
<Descriptions bordered :column="1" size="small" class="promoter-detail__desc">
<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_input_count ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="录入药店">
{{ detail.pharmacy_input_count ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="正式诊所">
<Descriptions.Item label="绑定诊所">
{{ detail.clinic_store_count ?? 0 }}
</Descriptions.Item>
<Descriptions.Item label="正式药店">
<Descriptions.Item label="绑定药店">
{{ detail.pharmacy_store_count ?? 0 }}
</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>
@@ -110,11 +214,41 @@ async function loadDetail(id?: number, code?: string) {
margin-top: 8px;
}
.dark .promoter-detail__title {
.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>