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
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:
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Input, Modal, message } from 'ant-design-vue';
|
||||
|
||||
import UploadImage from '#/components/form/components/upload-image.vue';
|
||||
|
||||
export interface SettlementPreviewInfo {
|
||||
record_count: number;
|
||||
order_count?: number;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
confirmFn: (data: Record<string, any>) => Promise<unknown>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const confirmLoading = ref(false);
|
||||
const modalTitle = ref('确认结算');
|
||||
const preview = ref<SettlementPreviewInfo | null>(null);
|
||||
const settleParams = ref<Record<string, any>>({});
|
||||
const proofImages = ref<string[]>([]);
|
||||
const remark = ref('');
|
||||
|
||||
function open(
|
||||
title: string,
|
||||
previewInfo: SettlementPreviewInfo,
|
||||
params: Record<string, any>,
|
||||
) {
|
||||
modalTitle.value = title;
|
||||
preview.value = previewInfo;
|
||||
settleParams.value = { ...params };
|
||||
proofImages.value = [];
|
||||
remark.value = '';
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
if (!proofImages.value.length) {
|
||||
message.warning('请上传至少 1 张结算凭证(如转账截图)');
|
||||
return Promise.reject();
|
||||
}
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
await props.confirmFn({
|
||||
...settleParams.value,
|
||||
proof_images: proofImages.value,
|
||||
remark: remark.value.trim(),
|
||||
});
|
||||
message.success('结算成功');
|
||||
close();
|
||||
emit('success');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="visible"
|
||||
:confirm-loading="confirmLoading"
|
||||
:title="modalTitle"
|
||||
ok-text="确认结算"
|
||||
width="560"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<div v-if="preview" class="mb-4 text-sm text-gray-600">
|
||||
<p>共 {{ preview.record_count }} 条明细</p>
|
||||
<p v-if="preview.order_count != null">{{ preview.order_count }} 个订单</p>
|
||||
<p class="font-medium text-gray-900">合计 ¥{{ preview.amount }}</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 text-sm">
|
||||
<span class="text-red-500">*</span> 结算凭证(转账截图等,最多 9 张)
|
||||
</div>
|
||||
<UploadImage v-model="proofImages" :max-count="9" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-2 text-sm text-gray-600">备注(选填)</div>
|
||||
<Input.TextArea
|
||||
v-model:value="remark"
|
||||
:maxlength="255"
|
||||
:rows="2"
|
||||
placeholder="可填写转账说明等"
|
||||
show-count
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -89,3 +89,62 @@ export async function salespersonCreateApi(data) {
|
||||
export async function salespersonUpdateApi(data) {
|
||||
return requestClient.post<any>(`salesperson/update`, data);
|
||||
}
|
||||
|
||||
export interface DrugCommissionListItem {
|
||||
drug_id: number;
|
||||
drug_name: string;
|
||||
pinyin_simple?: string;
|
||||
image?: string;
|
||||
drug_spec: string;
|
||||
buy_price: string;
|
||||
price: string;
|
||||
commission: string;
|
||||
}
|
||||
|
||||
export interface DrugCommissionListResult {
|
||||
items: DrugCommissionListItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export async function getDrugCommissionListApi(params: Record<string, any>) {
|
||||
return requestClient.get<DrugCommissionListResult>(
|
||||
`salesperson/drug-commission-list`,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveDrugCommissionApi(data: {
|
||||
salesperson_id: number;
|
||||
items: { drug_id: number; commission: string }[];
|
||||
}) {
|
||||
return requestClient.post<any>(`salesperson/save-drug-commission`, data);
|
||||
}
|
||||
|
||||
export async function getDrugCommissionCopyFromApi(fromSalespersonId: number) {
|
||||
return requestClient.get<{ items: { drug_id: number; commission: string }[] }>(
|
||||
`salesperson/drug-commission-copy-from`,
|
||||
{ params: { from_salesperson_id: fromSalespersonId } },
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCommissionListApi(params: Record<string, any>) {
|
||||
return requestClient.get<any>(`salesperson/commission-list`, { params });
|
||||
}
|
||||
|
||||
export async function getSettlementPreviewApi(params: Record<string, any>) {
|
||||
return requestClient.get<any>(`salesperson/settlement-preview`, { params });
|
||||
}
|
||||
|
||||
export async function confirmSettlementApi(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`salesperson/settlement`, data);
|
||||
}
|
||||
|
||||
export async function getSettlementListApi(params: Record<string, any>) {
|
||||
return requestClient.get<any>(`salesperson/settlement-list`, { params });
|
||||
}
|
||||
|
||||
export async function getSettlementDetailApi(params: { id: number }) {
|
||||
return requestClient.get<any>(`salesperson/settlement-detail`, { params });
|
||||
}
|
||||
|
||||
@@ -1,128 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import {useVbenModal} from '@vben/common-ui';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {Button, Card, message} from 'ant-design-vue';
|
||||
import {PlusOutlined} from '@ant-design/icons-vue';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Empty, Space, Tag } from 'ant-design-vue';
|
||||
|
||||
import { salespersonListApi } from '#/views/business/store/settings/api';
|
||||
import QrCodePreview from '#/views/business/store/settings/components/SalespersonQrCodePreview.vue';
|
||||
import salespersonCreateModal
|
||||
from "#/views/business/store/settings/components/SalespersonCreateModal.vue";
|
||||
import {ref} from "vue";
|
||||
import {salespersonListApi} from "#/views/business/store/settings/api";
|
||||
import SalespersonCommissionDrawer from '#/views/business/store/settings/components/SalespersonCommissionDrawer.vue';
|
||||
import SalespersonCreateModal from '#/views/business/store/settings/components/SalespersonCreateModal.vue';
|
||||
|
||||
// defineProps<{
|
||||
// data: any;
|
||||
// }>();
|
||||
|
||||
const data = ref([]);
|
||||
const listData = ref<{ total: number; items: any[] }>({
|
||||
total: 0,
|
||||
items: [],
|
||||
});
|
||||
|
||||
const getList = () => {
|
||||
salespersonListApi().then((res) => {
|
||||
data.value = res;
|
||||
})
|
||||
}
|
||||
listData.value = {
|
||||
total: res?.total ?? 0,
|
||||
items: res?.items ?? [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
getList();
|
||||
const [SalespersonCreateModal, SalespersonCreateModalApi] = useVbenModal({
|
||||
connectedComponent: salespersonCreateModal,
|
||||
|
||||
const [SalespersonCreateModalComponent, SalespersonCreateModalApi] = useVbenModal({
|
||||
connectedComponent: SalespersonCreateModal,
|
||||
});
|
||||
|
||||
const [QrCodePreviewModal, QrCodePreviewApi] = useVbenModal({
|
||||
connectedComponent: QrCodePreview,
|
||||
});
|
||||
|
||||
const openDoctorQrCode = (data) => {
|
||||
QrCodePreviewApi.setData({
|
||||
values: data,
|
||||
const commissionDrawerRef = ref<InstanceType<typeof SalespersonCommissionDrawer>>();
|
||||
|
||||
function openCommissionDrawer(item: any) {
|
||||
commissionDrawerRef.value?.open({
|
||||
id: item.id,
|
||||
nick_name: item.nick_name,
|
||||
});
|
||||
}
|
||||
|
||||
function openQrCode(item: any) {
|
||||
QrCodePreviewApi.setData({ values: item });
|
||||
QrCodePreviewApi.open();
|
||||
};
|
||||
|
||||
const openSalespersonModal = () => {
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
SalespersonCreateModalApi.setData({ getList });
|
||||
SalespersonCreateModalApi.open();
|
||||
};
|
||||
|
||||
const openUpdateSalespersonModal = (values) => {
|
||||
}
|
||||
|
||||
function openEditModal(item: any) {
|
||||
SalespersonCreateModalApi.setData({
|
||||
values,
|
||||
values: item,
|
||||
getList,
|
||||
update: true
|
||||
})
|
||||
update: true,
|
||||
});
|
||||
SalespersonCreateModalApi.open();
|
||||
};
|
||||
}
|
||||
|
||||
function splitTypeLabel(type: number) {
|
||||
return Number(type) === 1 ? '百分比' : '固定金额';
|
||||
}
|
||||
|
||||
function splitTypeColor(type: number) {
|
||||
return Number(type) === 1 ? 'blue' : 'orange';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SalespersonCreateModal/>
|
||||
<QrCodePreviewModal/>
|
||||
<Card :title="`您拥有${data.total}个推广员`" class="w-full">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="openSalespersonModal">新增推广员</Button>
|
||||
</template>
|
||||
<SalespersonCreateModalComponent />
|
||||
<QrCodePreviewModal />
|
||||
<SalespersonCommissionDrawer ref="commissionDrawerRef" @settled="getList" />
|
||||
|
||||
<div v-if="!listData.items.length" class="py-12">
|
||||
<Empty description="暂无推广员">
|
||||
<Button type="primary" @click="openCreateModal">新增推广员</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600"
|
||||
>共 {{ listData.total }} 位推广员</span
|
||||
>
|
||||
<Button type="primary" @click="openCreateModal">新增推广员</Button>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
>
|
||||
<Card
|
||||
v-for="(item, index) in data.items"
|
||||
:key="index"
|
||||
class="rounded-lg shadow-sm"
|
||||
>
|
||||
<!-- 顶部信息栏 -->
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<h3 class="text-lg font-semibold ">
|
||||
{{ item.nick_name || '未知' }}
|
||||
</h3>
|
||||
</div>
|
||||
<Card
|
||||
v-for="item in listData.items"
|
||||
:key="item.id"
|
||||
class="rounded-lg shadow-sm"
|
||||
>
|
||||
<div class="mb-3 flex items-start justify-between gap-2">
|
||||
<h3 class="text-lg font-semibold">{{ item.nick_name || '未知' }}</h3>
|
||||
<Tag :color="splitTypeColor(item.split_type)">
|
||||
{{ splitTypeLabel(item.split_type) }}
|
||||
<template v-if="Number(item.split_type) === 1">
|
||||
· {{ item.split }}%
|
||||
</template>
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<!-- 医生信息 -->
|
||||
<div class="mb-4 space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span>手机号:</span>
|
||||
<span class="">{{ item.phone }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>加入时间:</span>
|
||||
<span>{{
|
||||
item.created_at
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>累计获客:</span>
|
||||
<span class="font-medium text-blue-600">{{ item.count.user_number }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>总佣金:</span>
|
||||
<span class="font-medium text-blue-600">{{ item.count.money }}</span>
|
||||
</div>
|
||||
<div class="mb-4 space-y-1.5 text-sm text-gray-600">
|
||||
<div>{{ item.phone }}</div>
|
||||
<div>加入:{{ item.created_at }}</div>
|
||||
<div class="flex justify-between pt-1">
|
||||
<span>累计获客</span>
|
||||
<span class="font-medium text-blue-600">{{
|
||||
item.count?.user_number ?? 0
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 二维码区域 -->
|
||||
<div class="border-t border-gray-100 pt-4">
|
||||
<p class="mb-2 text-sm">联系二维码:</p>
|
||||
<div
|
||||
v-if="item.qr_code.qr_code"
|
||||
class="cursor-pointer"
|
||||
<div class="flex justify-between">
|
||||
<span>待结算</span>
|
||||
<span class="font-medium text-orange-600"
|
||||
>¥{{ item.count?.pending_amount || '0.00' }}</span
|
||||
>
|
||||
<Button
|
||||
class="w-full"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openDoctorQrCode(item)"
|
||||
>
|
||||
查看二维码
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="py-2 text-center text-sm text-gray-400">
|
||||
暂无二维码
|
||||
</div>
|
||||
<Button @click="openUpdateSalespersonModal(item)" class="w-full mt-3" size="small"
|
||||
type="primary">编辑
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="flex justify-between">
|
||||
<span>已结算</span>
|
||||
<span class="font-medium text-green-600"
|
||||
>¥{{ item.count?.settled_amount || '0.00' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space wrap class="w-full">
|
||||
<Button size="small" type="primary" @click="openCommissionDrawer(item)">
|
||||
分成与结算
|
||||
</Button>
|
||||
<Button size="small" @click="openEditModal(item)">编辑</Button>
|
||||
<Button
|
||||
v-if="Number(item.split_type) === 0"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openEditModal(item)"
|
||||
>
|
||||
药品分成
|
||||
</Button>
|
||||
<Button
|
||||
v-if="item.qr_code?.qr_code"
|
||||
size="small"
|
||||
@click="openQrCode(item)"
|
||||
>
|
||||
二维码
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Image,
|
||||
Input,
|
||||
message,
|
||||
Table,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import SalespersonSettlementConfirmModal from '#/components/salesperson/SalespersonSettlementConfirmModal.vue';
|
||||
|
||||
import {
|
||||
confirmSettlementApi,
|
||||
getCommissionListApi,
|
||||
getSettlementDetailApi,
|
||||
getSettlementListApi,
|
||||
getSettlementPreviewApi,
|
||||
} from '#/views/business/store/settings/api';
|
||||
|
||||
const emit = defineEmits<{
|
||||
settled: [];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const salespersonId = ref(0);
|
||||
const salespersonName = ref('');
|
||||
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(salesperson: { id: number; nick_name?: string }) {
|
||||
salespersonId.value = salesperson.id;
|
||||
salespersonName.value = salesperson.nick_name || '';
|
||||
selectedOrderIds.value = [];
|
||||
selectedRowKeys.value = [];
|
||||
orderNo.value = '';
|
||||
dateRange.value = [];
|
||||
periodRange.value = [];
|
||||
page.value = 1;
|
||||
activeTab.value = 'pending';
|
||||
visible.value = true;
|
||||
loadCommission();
|
||||
}
|
||||
|
||||
function onPageChange(p: number) {
|
||||
page.value = p;
|
||||
loadCommission();
|
||||
}
|
||||
|
||||
async function loadCommission() {
|
||||
if (!salespersonId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: Record<string, any> = {
|
||||
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 getCommissionListApi(params);
|
||||
commissionData.value = res?.items || [];
|
||||
commissionTotal.value = res?.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettlements() {
|
||||
if (!salespersonId.value) return;
|
||||
const res = await getSettlementListApi({
|
||||
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 = {
|
||||
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 getSettlementPreviewApi(params);
|
||||
settlementConfirmRef.value?.open('确认按时间段结算', preview, params);
|
||||
}
|
||||
|
||||
function onSettlementSuccess() {
|
||||
selectedOrderIds.value = [];
|
||||
selectedRowKeys.value = [];
|
||||
loadCommission();
|
||||
loadSettlements();
|
||||
emit('settled');
|
||||
}
|
||||
|
||||
async function handleSettleByOrders() {
|
||||
if (!selectedOrderIds.value.length) {
|
||||
message.warning('请勾选订单');
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
salesperson_id: salespersonId.value,
|
||||
settlement_type: 2,
|
||||
order_ids: selectedOrderIds.value,
|
||||
};
|
||||
const preview = await getSettlementPreviewApi(params);
|
||||
settlementConfirmRef.value?.open('确认结算选中订单', preview, params);
|
||||
}
|
||||
|
||||
async function showSettlementDetail(id: number) {
|
||||
settlementDetail.value = await getSettlementDetailApi({ id });
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
page.value = 1;
|
||||
if (tab === 'history') loadSettlements();
|
||||
else loadCommission();
|
||||
});
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
v-model:open="visible"
|
||||
:title="`分成与结算 - ${salespersonName}`"
|
||||
width="920"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<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="confirmSettlementApi"
|
||||
@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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
@@ -9,15 +9,38 @@ import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
salespersonCreateApi,
|
||||
salespersonUpdateApi,
|
||||
saveNavUrlApi
|
||||
saveDrugCommissionApi,
|
||||
} from '#/views/business/store/settings/api';
|
||||
import SalespersonDrugCommissionEditor from '#/views/business/store/settings/components/SalespersonDrugCommissionEditor.vue';
|
||||
|
||||
import { salespersonModalFormProps } from '../config/form';
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
const splitType = ref(0);
|
||||
const salespersonId = ref(0);
|
||||
const editorRef = ref<InstanceType<typeof SalespersonDrugCommissionEditor>>();
|
||||
|
||||
const [Form, formApi] = useVbenForm(salespersonModalFormProps);
|
||||
const isFixedAmount = computed(() => Number(splitType.value) === 0);
|
||||
const modalClass = computed(() =>
|
||||
isFixedAmount.value ? 'w-[960px]' : 'w-[480px]',
|
||||
);
|
||||
|
||||
watch(splitType, (val) => {
|
||||
if (Number(val) === 0) {
|
||||
nextTick(() => editorRef.value?.loadList());
|
||||
} else {
|
||||
editorRef.value?.resetDirty();
|
||||
}
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
...salespersonModalFormProps,
|
||||
handleValuesChange(values: Record<string, any>) {
|
||||
if (values.split_type !== undefined) {
|
||||
splitType.value = Number(values.split_type);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
@@ -26,43 +49,75 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = isUpdate.value
|
||||
? salespersonUpdateApi
|
||||
: salespersonCreateApi;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
|
||||
const values = await formApi.getValues();
|
||||
const payload = { ...values };
|
||||
if (Number(payload.split_type) === 0) {
|
||||
payload.split = 0;
|
||||
}
|
||||
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
try {
|
||||
const submitApi = isUpdate.value ? salespersonUpdateApi : salespersonCreateApi;
|
||||
const res = await submitApi(payload);
|
||||
const spId = isUpdate.value
|
||||
? Number(payload.id)
|
||||
: Number(res?.id ?? res);
|
||||
|
||||
if (Number(payload.split_type) === 0 && spId > 0) {
|
||||
const dirtyItems = editorRef.value?.getDirtyItems() ?? [];
|
||||
if (dirtyItems.length > 0) {
|
||||
await saveDrugCommissionApi({
|
||||
salesperson_id: spId,
|
||||
items: dirtyItems,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
|
||||
message.success('保存成功');
|
||||
modalApi.getData()?.getList?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.getList : null;
|
||||
if (isOpen) {
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
isUpdate.value = update;
|
||||
formApi.setValues(values);
|
||||
}
|
||||
if (!isOpen) {
|
||||
editorRef.value?.resetDirty();
|
||||
return;
|
||||
}
|
||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||
isUpdate.value = !!update;
|
||||
if (values) {
|
||||
formApi.setValues(values);
|
||||
splitType.value = Number(values.split_type ?? 0);
|
||||
salespersonId.value = Number(values.id ?? 0);
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
splitType.value = 0;
|
||||
salespersonId.value = 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:title="`${isUpdate === true ? '编辑' : '新增'}推广员`"
|
||||
class="w-[30%]"
|
||||
>
|
||||
<Modal :title="`${isUpdate ? '编辑' : '新增'}推广员`" :class="modalClass">
|
||||
<Form />
|
||||
<p
|
||||
v-if="isFixedAmount && !isUpdate"
|
||||
class="mb-2 text-sm text-gray-500"
|
||||
>
|
||||
可先选择已有推广员复制药品分成,再微调后保存。
|
||||
</p>
|
||||
<SalespersonDrugCommissionEditor
|
||||
v-if="isFixedAmount"
|
||||
ref="editorRef"
|
||||
:salesperson-id="salespersonId"
|
||||
:exclude-salesperson-id="salespersonId"
|
||||
class="mt-4"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Select,
|
||||
Table,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getDrugCommissionCopyFromApi,
|
||||
getDrugCommissionListApi,
|
||||
salespersonListApi,
|
||||
type DrugCommissionListItem,
|
||||
} from '#/views/business/store/settings/api';
|
||||
|
||||
export interface DrugCommissionRow extends DrugCommissionListItem {}
|
||||
|
||||
const props = defineProps<{
|
||||
salespersonId?: number;
|
||||
excludeSalespersonId?: number;
|
||||
}>();
|
||||
|
||||
const keyword = ref('');
|
||||
const loading = ref(false);
|
||||
const items = ref<DrugCommissionRow[]>([]);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
const editingDrugId = ref<number | null>(null);
|
||||
const dirtyMap = ref<Record<number, string>>({});
|
||||
|
||||
const salespersonOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const selectedCopyFromId = ref<number | undefined>();
|
||||
|
||||
const columns = [
|
||||
{ title: '药品', key: 'drug_info', width: 280 },
|
||||
{ title: '供货价', dataIndex: 'buy_price', width: 90, align: 'right' as const },
|
||||
{ title: '售价', dataIndex: 'price', width: 90, align: 'right' as const },
|
||||
{ title: '分成(元/件)', key: 'commission', width: 120, align: 'right' as const },
|
||||
];
|
||||
|
||||
function formatMoney(val: string | number) {
|
||||
const n = Number(val);
|
||||
return Number.isFinite(n) ? `¥${n.toFixed(2)}` : '-';
|
||||
}
|
||||
|
||||
function mergeRowCommission(row: DrugCommissionRow): DrugCommissionRow {
|
||||
const dirty = dirtyMap.value[row.drug_id];
|
||||
return dirty !== undefined ? { ...row, commission: dirty } : row;
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getDrugCommissionListApi({
|
||||
salesperson_id: props.salespersonId || 0,
|
||||
keyword: keyword.value,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
});
|
||||
items.value = (res?.items || []).map((row) => mergeRowCommission(row));
|
||||
total.value = res?.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSalespersonOptions() {
|
||||
const res = await salespersonListApi();
|
||||
const list = res?.items || [];
|
||||
salespersonOptions.value = list
|
||||
.filter((sp: any) => Number(sp.id) !== Number(props.excludeSalespersonId || 0))
|
||||
.map((sp: any) => ({
|
||||
label: sp.nick_name || sp.phone || `推广员#${sp.id}`,
|
||||
value: Number(sp.id),
|
||||
}));
|
||||
}
|
||||
|
||||
function onPageChange(p: number) {
|
||||
page.value = p;
|
||||
loadList();
|
||||
}
|
||||
|
||||
function startEdit(drugId: number) {
|
||||
editingDrugId.value = drugId;
|
||||
}
|
||||
|
||||
function finishEdit(record: DrugCommissionRow) {
|
||||
const val = String(record.commission ?? '0');
|
||||
dirtyMap.value[record.drug_id] = val;
|
||||
editingDrugId.value = null;
|
||||
}
|
||||
|
||||
function applyCommissionItems(
|
||||
commissionItems: { drug_id: number; commission: string }[],
|
||||
) {
|
||||
commissionItems.forEach((item) => {
|
||||
dirtyMap.value[item.drug_id] = String(item.commission ?? '0');
|
||||
});
|
||||
items.value = items.value.map((row) => mergeRowCommission(row));
|
||||
message.success(`已复制 ${commissionItems.length} 条药品分成`);
|
||||
}
|
||||
|
||||
function getDirtyItems(): { drug_id: number; commission: string }[] {
|
||||
return Object.entries(dirtyMap.value).map(([drugId, commission]) => ({
|
||||
drug_id: Number(drugId),
|
||||
commission,
|
||||
}));
|
||||
}
|
||||
|
||||
function resetDirty() {
|
||||
dirtyMap.value = {};
|
||||
selectedCopyFromId.value = undefined;
|
||||
}
|
||||
|
||||
async function onCopyFromSelect(id: number | undefined) {
|
||||
if (!id) return;
|
||||
try {
|
||||
const res = await getDrugCommissionCopyFromApi(id);
|
||||
applyCommissionItems(res?.items || []);
|
||||
} catch {
|
||||
message.error('复制失败');
|
||||
} finally {
|
||||
selectedCopyFromId.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const dirtyCount = computed(() => Object.keys(dirtyMap.value).length);
|
||||
|
||||
watch(
|
||||
() => props.salespersonId,
|
||||
() => {
|
||||
page.value = 1;
|
||||
resetDirty();
|
||||
loadList();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.excludeSalespersonId,
|
||||
() => {
|
||||
loadSalespersonOptions();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
loadSalespersonOptions();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
getDirtyItems,
|
||||
resetDirty,
|
||||
loadList,
|
||||
applyCommissionItems,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="salesperson-drug-editor">
|
||||
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="药品名称 / 拼音首拼"
|
||||
style="width: 220px"
|
||||
@press-enter="loadList"
|
||||
/>
|
||||
<Button type="primary" @click="loadList">查询</Button>
|
||||
<Select
|
||||
v-model:value="selectedCopyFromId"
|
||||
:options="salespersonOptions"
|
||||
allow-clear
|
||||
placeholder="复用推广员分成"
|
||||
style="width: 200px"
|
||||
@change="onCopyFromSelect"
|
||||
/>
|
||||
<span class="text-xs text-gray-500">
|
||||
选择已有推广员可一键复制其药品分成(不含中药);点击「分成」可编辑;待保存
|
||||
{{ dirtyCount }} 条
|
||||
</span>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="items"
|
||||
:loading="loading"
|
||||
:pagination="{ current: page, pageSize, total, onChange: onPageChange }"
|
||||
:row-key="(r) => r.drug_id"
|
||||
:scroll="{ y: 360 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'drug_info'">
|
||||
<div class="flex items-center gap-2">
|
||||
<Image
|
||||
v-if="record.image"
|
||||
:src="record.image"
|
||||
:width="40"
|
||||
:height="40"
|
||||
class="shrink-0 rounded object-cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded bg-gray-100 text-xs text-gray-400"
|
||||
>
|
||||
无图
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate font-medium">{{ record.drug_name }}</div>
|
||||
<div v-if="record.pinyin_simple" class="text-xs text-gray-400">
|
||||
{{ record.pinyin_simple }}
|
||||
</div>
|
||||
<div v-if="record.drug_spec" class="truncate text-xs text-gray-500">
|
||||
{{ record.drug_spec }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'commission'">
|
||||
<InputNumber
|
||||
v-if="editingDrugId === record.drug_id"
|
||||
v-model:value="record.commission"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
class="w-full"
|
||||
size="small"
|
||||
autofocus
|
||||
@blur="finishEdit(record)"
|
||||
@press-enter="finishEdit(record)"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="commission-cell"
|
||||
@click="startEdit(record.drug_id)"
|
||||
>
|
||||
{{ formatMoney(record.commission) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'buy_price'">
|
||||
{{ formatMoney(record.buy_price) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'price'">
|
||||
{{ formatMoney(record.price) }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.commission-cell {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
padding: 2px 8px;
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.commission-cell:hover {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
</style>
|
||||
@@ -82,10 +82,24 @@ export const salespersonModalFormProps: VbenFormProps = {
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
component: 'InputNumber',
|
||||
fieldName: 'split',
|
||||
label: '分成',
|
||||
rules: 'required',
|
||||
label: '分成比例(%)',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
precision: 2,
|
||||
class: 'w-full',
|
||||
},
|
||||
dependencies: {
|
||||
show(values) {
|
||||
return Number(values?.split_type) === 1;
|
||||
},
|
||||
rules(values) {
|
||||
return Number(values?.split_type) === 1 ? 'required' : '';
|
||||
},
|
||||
triggerFields: ['split_type'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// component: 'UploadImage',
|
||||
|
||||
@@ -728,8 +728,16 @@ const handleSwitchClinicType = () => {
|
||||
</ImagePreviewGroup>
|
||||
</Card>
|
||||
|
||||
<Card v-else-if="activeTabBar === 6" title="推广员">
|
||||
<SalespersonCard :data="data" />
|
||||
<Card
|
||||
v-else-if="activeTabBar === 6"
|
||||
title="推广员管理"
|
||||
>
|
||||
<template #extra>
|
||||
<span class="text-sm text-gray-500"
|
||||
>固定金额需配置各药品每件佣金</span
|
||||
>
|
||||
</template>
|
||||
<SalespersonCard />
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
35
apps/web-antd/src/views/system/store/api/salesperson.ts
Normal file
35
apps/web-antd/src/views/system/store/api/salesperson.ts
Normal 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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user