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
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
李琦
2026-05-18 13:33:27 +08:00
parent 0b1b08c7d7
commit c978b978ec
12 changed files with 1306 additions and 130 deletions

View File

@@ -0,0 +1,35 @@
import { requestClient } from '#/api/request';
const prefix = 'salesperson/';
export function getSalespersonList(params: { store_id: number; page?: number; pageSize?: number }) {
return requestClient.get(`${prefix}list`, { params });
}
export function getCommissionList(params: Record<string, any>) {
return requestClient.get(`${prefix}commission-list`, { params });
}
export function getSettlementPreview(params: Record<string, any>) {
return requestClient.get(`${prefix}settlement-preview`, { params });
}
export function confirmSettlement(data: Record<string, any>) {
return requestClient.post(`${prefix}settlement`, data);
}
export function getSettlementList(params: Record<string, any>) {
return requestClient.get(`${prefix}settlement-list`, { params });
}
export function getSettlementDetail(params: { id: number; store_id: number }) {
return requestClient.get(`${prefix}settlement-detail`, { params });
}
export function getDrugCommissionList(params: Record<string, any>) {
return requestClient.get(`${prefix}drug-commission-list`, { params });
}
export function saveDrugCommission(data: Record<string, any>) {
return requestClient.post(`${prefix}save-drug-commission`, data);
}

View File

@@ -0,0 +1,282 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import {
Button,
DatePicker,
Drawer,
Image,
Input,
message,
Select,
Table,
Tabs,
} from 'ant-design-vue';
import dayjs from 'dayjs';
import SalespersonSettlementConfirmModal from '#/components/salesperson/SalespersonSettlementConfirmModal.vue';
import {
confirmSettlement,
getCommissionList,
getSalespersonList,
getSettlementDetail,
getSettlementList,
getSettlementPreview,
} from '../api/salesperson';
const visible = ref(false);
const storeId = ref(0);
const storeName = ref('');
const salespersonId = ref<number | undefined>();
const salespersonOptions = ref<any[]>([]);
const activeTab = ref('pending');
const loading = ref(false);
const commissionData = ref<any[]>([]);
const commissionTotal = ref(0);
const page = ref(1);
const pageSize = ref(20);
const orderNo = ref('');
const dateRange = ref<any[]>([]);
const selectedOrderIds = ref<number[]>([]);
const selectedRowKeys = ref<number[]>([]);
const settlementList = ref<any[]>([]);
const settlementDetail = ref<any>(null);
const detailVisible = ref(false);
const periodRange = ref<any[]>([]);
const settlementConfirmRef = ref<InstanceType<typeof SalespersonSettlementConfirmModal>>();
const commissionColumns = [
{ title: '订单号', dataIndex: 'order_no', width: 160 },
{ title: '药品信息', dataIndex: 'drug_info', ellipsis: true },
{ title: '分成金额', dataIndex: 'commission_amount', width: 100 },
{ title: '分成时间', dataIndex: 'order_pay_at_text', width: 170 },
];
const settlementColumns = [
{ title: '结算单号', dataIndex: 'settlement_no', width: 160 },
{ title: '方式', dataIndex: 'settlement_type_text', width: 100 },
{ title: '金额', dataIndex: 'amount', width: 100 },
{ title: '明细数', dataIndex: 'record_count', width: 80 },
{ title: '时间', dataIndex: 'created_at_text', width: 170 },
{ title: '操作', key: 'action', width: 80 },
];
const status = computed(() => (activeTab.value === 'pending' ? 0 : activeTab.value === 'settled' ? 1 : undefined));
const rowSelection = computed(() =>
activeTab.value === 'pending'
? {
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: number[], rows: any[]) => {
selectedRowKeys.value = keys as number[];
selectedOrderIds.value = [...new Set(rows.map((r) => r.order_id))];
},
getCheckboxProps: () => ({ disabled: false }),
}
: undefined,
);
function open(row: { id: number; name: string }) {
storeId.value = row.id;
storeName.value = row.name;
salespersonId.value = undefined;
selectedOrderIds.value = [];
selectedRowKeys.value = [];
visible.value = true;
activeTab.value = 'pending';
loadSalespersons();
}
function onPageChange(p: number) {
page.value = p;
loadCommission();
}
async function loadSalespersons() {
const res = await getSalespersonList({ store_id: storeId.value, page: 1, pageSize: 100 });
salespersonOptions.value = res?.items || [];
if (!salespersonId.value && salespersonOptions.value.length) {
salespersonId.value = salespersonOptions.value[0].id;
}
loadCommission();
}
async function loadCommission() {
if (!storeId.value || !salespersonId.value) return;
loading.value = true;
try {
const params: any = {
store_id: storeId.value,
salesperson_id: salespersonId.value,
page: page.value,
pageSize: pageSize.value,
order_no: orderNo.value,
};
if (status.value !== undefined) params.status = status.value;
if (dateRange.value?.length === 2) {
params.start_at = dayjs(dateRange.value[0]).startOf('day').unix();
params.end_at = dayjs(dateRange.value[1]).endOf('day').unix();
}
const res = await getCommissionList(params);
commissionData.value = res?.items || [];
commissionTotal.value = res?.total || 0;
} finally {
loading.value = false;
}
}
async function loadSettlements() {
if (!storeId.value || !salespersonId.value) return;
const res = await getSettlementList({
store_id: storeId.value,
salesperson_id: salespersonId.value,
page: 1,
pageSize: 50,
});
settlementList.value = res?.items || [];
}
async function handleSettleByPeriod() {
if (!periodRange.value?.length || periodRange.value.length !== 2) {
message.warning('请选择结算时间段');
return;
}
const params = {
store_id: storeId.value,
salesperson_id: salespersonId.value,
settlement_type: 1,
period_start: dayjs(periodRange.value[0]).startOf('day').unix(),
period_end: dayjs(periodRange.value[1]).endOf('day').unix(),
};
const preview = await getSettlementPreview(params);
settlementConfirmRef.value?.open('确认按时间段结算', preview, params);
}
function onSettlementSuccess() {
selectedOrderIds.value = [];
selectedRowKeys.value = [];
loadCommission();
loadSettlements();
}
async function handleSettleByOrders() {
if (!selectedOrderIds.value.length) {
message.warning('请勾选订单');
return;
}
const params = {
store_id: storeId.value,
salesperson_id: salespersonId.value,
settlement_type: 2,
order_ids: selectedOrderIds.value,
};
const preview = await getSettlementPreview(params);
settlementConfirmRef.value?.open('确认结算选中订单', preview, params);
}
async function showSettlementDetail(id: number) {
settlementDetail.value = await getSettlementDetail({ id, store_id: storeId.value });
detailVisible.value = true;
}
watch(activeTab, (tab) => {
page.value = 1;
if (tab === 'history') loadSettlements();
else loadCommission();
});
watch(salespersonId, () => {
page.value = 1;
loadCommission();
if (activeTab.value === 'history') loadSettlements();
});
defineExpose({ open });
</script>
<template>
<Drawer v-model:open="visible" :title="`推广员分成 - ${storeName}`" width="920" destroy-on-close>
<div class="mb-4 flex flex-wrap items-center gap-3">
<span>推广员</span>
<Select
v-model:value="salespersonId"
:options="salespersonOptions.map((s) => ({ label: s.nick_name, value: s.id }))"
style="width: 200px"
/>
<Input v-model:value="orderNo" allow-clear placeholder="订单号" style="width: 180px" @press-enter="loadCommission" />
<DatePicker.RangePicker v-model:value="dateRange" />
<Button type="primary" @click="loadCommission">查询</Button>
</div>
<Tabs v-model:activeKey="activeTab">
<Tabs.TabPane key="pending" tab="待结算">
<div class="mb-3 flex flex-wrap gap-2">
<DatePicker.RangePicker v-model:value="periodRange" placeholder="['开始','结束']" />
<Button type="primary" @click="handleSettleByPeriod">按时间段结算</Button>
<Button type="primary" ghost @click="handleSettleByOrders">结算选中订单</Button>
</div>
<Table
:columns="commissionColumns"
:data-source="commissionData"
:loading="loading"
:pagination="{ current: page, pageSize, total: commissionTotal, onChange: onPageChange }"
:row-key="(r) => r.id"
:row-selection="rowSelection"
size="small"
/>
</Tabs.TabPane>
<Tabs.TabPane key="settled" tab="已结算">
<Table
:columns="commissionColumns"
:data-source="commissionData"
:loading="loading"
:pagination="{ current: page, pageSize, total: commissionTotal, onChange: onPageChange }"
:row-key="(r) => r.id"
size="small"
/>
</Tabs.TabPane>
<Tabs.TabPane key="history" tab="结算记录">
<Table :columns="settlementColumns" :data-source="settlementList" :row-key="(r) => r.id" size="small">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<Button type="link" size="small" @click="showSettlementDetail(record.id)">详情</Button>
</template>
</template>
</Table>
</Tabs.TabPane>
</Tabs>
<SalespersonSettlementConfirmModal
ref="settlementConfirmRef"
:confirm-fn="confirmSettlement"
@success="onSettlementSuccess"
/>
<Drawer v-model:open="detailVisible" title="结算单详情" width="720" destroy-on-close>
<div v-if="settlementDetail" class="mb-3">
<p>单号:{{ settlementDetail.settlement_no }}</p>
<p>金额:¥{{ settlementDetail.amount }}({{ settlementDetail.record_count }} 条)</p>
<p v-if="settlementDetail.remark">备注:{{ settlementDetail.remark }}</p>
<div v-if="settlementDetail.proof_images?.length" class="mt-3">
<p class="mb-2 text-sm text-gray-600">结算凭证</p>
<Image.PreviewGroup>
<Image
v-for="(url, idx) in settlementDetail.proof_images"
:key="idx"
:src="url"
:width="96"
:height="96"
class="mr-2 rounded object-cover"
/>
</Image.PreviewGroup>
</div>
</div>
<Table
:columns="commissionColumns"
:data-source="settlementDetail?.records || []"
:row-key="(r) => r.id"
size="small"
/>
</Drawer>
</Drawer>
</template>

View File

@@ -77,8 +77,8 @@ export const gridOptions: VxeGridProps<RowType> = {
},
// { field: 'shouzimu', title: '诊所首字母' },
{ field: 'position', title: '详细地址' },
{ field: 'new_admin.nick_name', title: '业务员' },
// { field: 'contact', title: '联系人' },
{ field: 'new_admin.nick_name', title: '业务员', slots: { default: 'sales_admin' } },
{ field: 'store_admin.phone', title: '管理员手机', slots: { default: 'store_admin_phone' } },
{ field: 'mobile', title: '联系人、电话', slots: { default: 'mobile' } },
{
field: 'start_time',

View File

@@ -28,6 +28,7 @@ import FormModalDemo from './components/modal.vue';
import DrugPriceModal from './components/DrugPriceModal.vue';
import BindConsultationModal from './components/BindConsultationModal.vue';
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
import SalespersonCommissionDrawer from './components/SalespersonCommissionDrawer.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -177,6 +178,12 @@ function copyText(source) {
}
const { copy } = useClipboard({ legacy: true });
const salespersonDrawerRef = ref<InstanceType<typeof SalespersonCommissionDrawer>>();
const openSalespersonCommission = (row: { id: number; name: string }) => {
salespersonDrawerRef.value?.open(row);
};
/**
* 更新包邮状态
*/
@@ -255,6 +262,7 @@ const handleSwitchClinicType = (row: any) => {
<DrugPriceModalComponent />
<BindConsultationModalComponent />
<StoreExternalFieldModalComponent />
<SalespersonCommissionDrawer ref="salespersonDrawerRef" />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -306,6 +314,15 @@ const handleSwitchClinicType = (row: any) => {
</template>
</TableAction>
</template>
<template #sales_admin="{ row }">
<div>
<div>{{ row.new_admin?.nick_name || '-' }}</div>
<div v-if="row.new_admin?.phone" class="text-xs text-gray-500">{{ row.new_admin.phone }}</div>
</div>
</template>
<template #store_admin_phone="{ row }">
{{ row.store_admin?.phone || '-' }}
</template>
<template #mobile="{ row }">
<div>
诊所联系人:{{ row.contact }}
@@ -486,6 +503,14 @@ const handleSwitchClinicType = (row: any) => {
ifShow: isPlatformAdmin,
onClick: showDrugPriceModal.bind(null, row),
},
{
label: '推广员分成',
type: 'link',
icon: 'ant-design:account-book-outlined',
size: 'small',
auth: ['Super Admin', 'Admin'],
onClick: () => openSalespersonCommission(row),
},
]"
/>
</template>