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-06-18 13:24:58 +08:00
parent 9a1435996e
commit f36ef45ee2
37 changed files with 2742 additions and 481 deletions

View File

@@ -119,6 +119,19 @@ export const modalFormProps: VbenFormProps = {
defaultValue: 0, // 默认订阅
rules: 'required',
},
{
component: 'RadioGroup',
formItemClass: 'col-span-6',
componentProps: {
options: [
{ label: '关闭', value: 0 },
{ label: '开启', value: 1 },
],
},
fieldName: 'enable_order_price_percent_adjust',
label: '订单打折/涨幅',
defaultValue: 0,
},
{
// 联系人姓名字段
component: 'VbenInput',
@@ -142,14 +155,10 @@ export const modalFormProps: VbenFormProps = {
rules: 'required',
},
{
// 业务码字段
component: 'VbenInput',
component: 'PromoterPicker',
formItemClass: 'col-span-6',
componentProps: {
placeholder: '请输入业务码',
},
fieldName: 'code',
label: '业务码',
label: '业务员',
rules: 'required',
},
{

View File

@@ -1,9 +1,7 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
// 直接使用store的API接口
import { getStoreList } from '#/views/system/store/api';
// 表格行数据类型定义
interface RowType {
id: string;
name: string;
@@ -12,29 +10,27 @@ interface RowType {
created_at: string;
}
// 药店管理表格配置
// 注意:查询时会自动添加type=1参数,只显示药店类型的数据
export const gridOptions: VxeGridProps<RowType> = {
// 复选框配置
checkboxConfig: {
highlight: true,
labelField: '',
},
// 列配置
columnConfig: {
useKey: true,
},
// 行配置
rowConfig: {
useKey: true,
},
// 表格列定义
columns: [
{ type: 'checkbox', width: 60 }, // 复选框列
{ field: 'id', align: 'left', title: 'ID', width: 100 }, // ID列
{ field: 'name', align: 'left', title: '药店名称', width: 140 }, // 药店名称列(已从"诊所名称"改为"药店名称")
{ type: 'checkbox', width: 60 },
{
field: 'store_basic_info',
align: 'left',
title: '基础信息',
width: 280,
slots: { default: 'store_basic_info' },
},
{
// 在线复诊配置列(合并委托诊所和负责人)
field: 'online_consultation_config',
align: 'left',
title: '在线复诊配置',
@@ -42,69 +38,37 @@ export const gridOptions: VxeGridProps<RowType> = {
slots: { default: 'online_consultation_config' },
},
{
field: 'is_shipping_free',
field: 'store_config_1',
align: 'left',
title: '是否包邮',
slots: { default: 'is_shipping_free' },
width: 120,
title: '开关配置',
width: 330,
slots: { default: 'store_config_1' },
},
{
field: 'bank_card_report_status',
field: 'store_config_2',
align: 'left',
title: '银行卡报备',
slots: { default: 'bank_card_report_status' },
width: 120,
title: '业务配置',
width: 330,
slots: { default: 'store_config_2' },
},
{
// 订阅价格波动列(显示为开关)
field: 'subscribe_price_change',
align: 'left',
title: '订阅价格',
slots: { default: 'subscribe_price_change' },
width: 120,
},
{
field: 'see_rate',
align: 'left',
title: '查看毛利率',
slots: { default: 'see_rate' },
width: 120,
},
{
// 药店二维码列(已从"诊所二维码"改为"药店二维码")
field: 'qr_code',
align: 'left',
title: '药店二维码',
slots: { default: 'qr_code' },
width: 130,
},
{ field: 'position', title: '详细地址', width: 140 },
{ field: 'new_admin.nick_name', title: '业务员', slots: { default: 'sales_admin' }, width: 160 },
{ field: 'mobile', title: '联系人、电话', slots: { default: 'mobile' }, width: 140 },
{
field: 'erp_id',
align: 'left',
title: 'ERP ID',
width: 140,
slots: { default: 'erp_id_cell' },
},
{
field: 'mes_id',
align: 'left',
title: 'MES ID',
width: 140,
slots: { default: 'mes_id_cell' },
},
{ field: 'position', title: '详细地址', width: 140 }, // 详细地址列
{ field: 'new_admin.nick_name', title: '业务员', width: 140 }, // 业务员列
{ field: 'mobile', title: '联系人、电话', slots: { default: 'mobile' }, width: 140 }, // 联系人电话列
{
// 营业时间列
field: 'start_time',
title: '营业时间',
width: 140,
slots: { default: 'start-time' },
},
{ field: 'created_at', title: '注册时间', width: 140 }, // 注册时间列
{ field: 'created_at', title: '注册时间', width: 140 },
{
// 操作列
type: 'html',
title: '操作',
width: 200,
@@ -113,18 +77,14 @@ export const gridOptions: VxeGridProps<RowType> = {
},
],
keepSource: true,
// 分页配置
pagerConfig: {},
// 代理配置(数据请求)
proxyConfig: {
ajax: {
// 请求后端接口方法
// 注意:会自动添加type=1参数,只查询药店类型的数据
query: async ({ page }, formValues) => {
return await getStoreList({
page: page.currentPage,
pageSize: page.pageSize,
type: 1, // 固定为药店类型
type: 1,
...formValues,
});
},
@@ -132,20 +92,17 @@ export const gridOptions: VxeGridProps<RowType> = {
},
height: 'auto',
border: false,
// 工具栏配置
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新按钮
print: false, // 打印按钮
export: false, // 导出按钮
zoom: true, // 最大化最小化按钮
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons', // 工具栏按钮插槽
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},

View File

@@ -8,11 +8,12 @@ import { Page, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { useClipboard } from '@vueuse/core';
import { Button, Image, message, Switch, Tag } from 'ant-design-vue';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { Icon } from '#/components/icon';
import QrCodePreview from '#/components/modal/QrCodePreview.vue';
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
import { TableAction } from '#/components/table-action';
// 导入药店管理相关的API(直接使用store的API)
@@ -24,9 +25,13 @@ import {
updateStoreShippingFree,
updateStoreSeeRateStatus,
updateStoreSubscribeStatus,
updateStoreOrderPricePercentAdjust,
updateAllowInsuranceCategoryApi,
toggleSalespersonSeePriceApi,
} from '#/views/system/store/api';
// 导入表单弹窗组件
import FormModalDemo from './components/modal.vue';
import StoreBasicInfoCell from '#/views/system/store/components/cells/StoreBasicInfoCell.vue';
import StoreConfigTogglesCell from '#/views/system/store/components/cells/StoreConfigTogglesCell.vue';
// 导入绑定在线复诊配置弹窗(使用store的组件)
import BindConsultationModal from '#/views/system/store/components/BindConsultationModal.vue';
import DrugPriceModal from '#/views/system/store/components/DrugPriceModal.vue';
@@ -299,6 +304,27 @@ const updateSeeRate = (id: number) => {
});
};
const updateOrderPricePercentAdjust = (id: number) => {
updateStoreOrderPricePercentAdjust({ id }).then(() => {
message.success('修改成功!');
gridApi.query();
});
};
const updateAllowInsurance = (id: number) => {
updateAllowInsuranceCategoryApi({ id }).then(() => {
message.success('修改成功!');
gridApi.query();
});
};
const updateSalespersonSeePrice = (storeId: number) => {
toggleSalespersonSeePriceApi({ store_id: storeId }).then(() => {
message.success('修改成功!');
gridApi.query();
});
};
/**
* 批量同步总仓库药品
*/
@@ -379,6 +405,47 @@ const batchSyncDrugPrice = () => {
</template>
</TableAction>
</template>
<template #store_basic_info="{ row }">
<StoreBasicInfoCell
:row="row"
variant="pharmacy"
:on-copy="copyText"
:on-edit-external-field="showStoreExternalFieldModal"
/>
</template>
<template #store_config_1="{ row }">
<StoreConfigTogglesCell
:row="row"
section="switches"
:get-bank-card-report-tag-color="getBankCardReportTagColor"
:get-bank-card-report-tag-text="getBankCardReportTagText"
:on-shipping-free="updateShippingFree"
:on-subscribe="updateSubscribe"
:on-see-rate="updateSeeRate"
:on-order-price-adjust="updateOrderPricePercentAdjust"
:on-bank-card-report="handleBankCardReportColumnClick"
/>
</template>
<template #store_config_2="{ row }">
<StoreConfigTogglesCell
:row="row"
section="business"
show-insurance
show-salesperson-see-price
:get-bank-card-report-tag-color="getBankCardReportTagColor"
:get-bank-card-report-tag-text="getBankCardReportTagText"
:on-shipping-free="updateShippingFree"
:on-subscribe="updateSubscribe"
:on-see-rate="updateSeeRate"
:on-order-price-adjust="updateOrderPricePercentAdjust"
:on-bank-card-report="handleBankCardReportColumnClick"
:on-allow-insurance="updateAllowInsurance"
:on-salesperson-see-price="updateSalespersonSeePrice"
/>
</template>
<template #sales_admin="{ row }">
<PromoterInfoCard :admin="row.new_admin" size="compact" />
</template>
<template #mobile="{ row }">
<div>
药店联系人:{{ row.contact }}
@@ -406,26 +473,6 @@ const batchSyncDrugPrice = () => {
/>
</div>
</template>
<template #erp_id_cell="{ row }">
<Button
type="link"
size="small"
class="h-auto p-0"
@click="showStoreExternalFieldModal(row, 'erp_id', 'ERP ID')"
>
{{ row.erp_id != null && row.erp_id !== '' ? row.erp_id : '-' }}
</Button>
</template>
<template #mes_id_cell="{ row }">
<Button
type="link"
size="small"
class="h-auto p-0"
@click="showStoreExternalFieldModal(row, 'mes_id', 'MES ID')"
>
{{ row.mes_id != null && row.mes_id !== '' ? row.mes_id : '-' }}
</Button>
</template>
<template #online_consultation_config="{ row }">
<div
v-if="row.is_internet_medical === 1 && row.online_consultation_doctor_name"
@@ -446,43 +493,6 @@ const batchSyncDrugPrice = () => {
绑定
</Button>
</template>
<template #is_shipping_free="{ row }">
<Switch
:checked="row.is_shipping_free"
:checked-value="1"
:un-checked-value="0"
checked-children="包邮"
un-checked-children="不包邮"
@click="updateShippingFree(row.id)"
/>
</template>
<template #bank_card_report_status="{ row }">
<Tag
:color="getBankCardReportTagColor(row.bank_card_report_status)"
style="cursor: pointer"
@click="handleBankCardReportColumnClick(row)"
>
{{ getBankCardReportTagText(row) }}
</Tag>
</template>
<template #subscribe_price_change="{ row }">
<Tag
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateSubscribe(row.id)"
>
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
</Tag>
</template>
<template #see_rate="{ row }">
<Tag
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateSeeRate(row.id)"
>
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
</Tag>
</template>
<template #start-time="{ row }">
早:<Tag color="success">{{ row.start_time }}</Tag>
<br />

View File

@@ -44,11 +44,25 @@ export async function getPromoterOptions() {
nick_name: string;
avatar: string;
code: string;
phone: string;
role_id: number;
role_name: string;
}[]>(`${prefix}promoter-options`);
}
export async function getPromoterDetailApi(params: { id?: number; code?: string }) {
return requestClient.get<{
id: number;
nick_name: string;
avatar: string;
code: string;
phone: string;
role_name: string;
clinic_input_count: number;
pharmacy_input_count: number;
}>(`${prefix}promoter-detail`, { params });
}
export async function auditStoreInput(data: {
id: number;
status: number; // 1=审核通过,2=审核拒绝

View File

@@ -10,6 +10,7 @@ import { Avatar, Button, message, Modal, Tag, Textarea } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
import { auditStoreInput, getStoreInputDetail } from './api';
import AuditModal from './components/AuditModal.vue';
@@ -132,6 +133,9 @@ const getStatusText = (status: number) => {
</div>
<span v-else>-</span>
</template>
<template #promoter_admin="{ row }">
<PromoterInfoCard :admin="row.promoter_admin" size="compact" />
</template>
<template #audit_time="{ row }">
<span v-if="row.audit_time">
{{ new Date(row.audit_time * 1000).toLocaleString() }}

View File

@@ -66,6 +66,12 @@ export const inputGridOptions: VxeGridProps<RowType> = {
minWidth: 140,
slots: { default: 'inputUser' },
},
{
field: 'promoter_admin',
title: '业务员',
minWidth: 160,
slots: { default: 'promoter_admin' },
},
{
type: 'html',
title: '操作',
@@ -145,6 +151,12 @@ export const auditGridOptions: VxeGridProps<RowType> = {
slots: { default: 'inputUser' },
},
{ field: 'auditAdmin.nick_name', title: '审核人' },
{
field: 'promoter_admin',
title: '业务员',
minWidth: 160,
slots: { default: 'promoter_admin' },
},
{ field: 'audit_time', title: '审核时间', slots: { default: 'audit_time' } },
{ field: 'contact', title: '联系人' },
{ field: 'mobile', title: '联系电话' },

View File

@@ -10,6 +10,7 @@ import { Avatar, Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
import {
createStoreInput,
@@ -154,6 +155,9 @@ const getStatusText = (status: number) => {
</div>
<span v-else>-</span>
</template>
<template #promoter_admin="{ row }">
<PromoterInfoCard :admin="row.promoter_admin" size="compact" />
</template>
<template #action="{ row }">
<TableAction
:actions="[

View File

@@ -170,6 +170,10 @@ export async function updateAllowInsuranceCategoryApi(data: { id: number }) {
return requestClient.post<any>(`${prefix}update-allow-insurance-category`, data);
}
export async function updateStoreOrderPricePercentAdjust(data: { id: number }) {
return requestClient.post<any>(`${prefix}update-order-price-percent-adjust`, data);
}
export async function toggleSalespersonSeePriceApi(data: { store_id: number }) {
return requestClient.post<any>('salesperson-store-config/toggle-see-price', data);
}

View File

@@ -0,0 +1,136 @@
<script lang="ts" setup>
import { Button } from 'ant-design-vue';
defineProps<{
row: Record<string, any>;
variant: 'clinic' | 'pharmacy';
onCopy: (text: string | number) => void;
onEditExternalField: (
row: Record<string, any>,
field: 'erp_id' | 'mes_id',
label: string,
) => void;
}>();
function getClinicTypeText(clinicType: number) {
if (clinicType === 1) return '西医诊所';
if (clinicType === 2) return '中医诊所';
return '未设置';
}
</script>
<template>
<div class="store-basic-info">
<div class="store-basic-info__name">{{ row.name || '-' }}</div>
<div class="store-basic-info__grid">
<div class="info-item">
<span class="info-item__label">ID</span>
<Button
type="link"
size="small"
class="info-item__action"
@click="onCopy(row.id)"
>
{{ row.id }}
</Button>
</div>
<div class="info-item">
<span class="info-item__label">ERP</span>
<Button
type="link"
size="small"
class="info-item__action"
@click="onEditExternalField(row, 'erp_id', 'ERP ID')"
>
{{ row.erp_id != null && row.erp_id !== '' ? row.erp_id : '-' }}
</Button>
</div>
<template v-if="variant === 'clinic'">
<div class="info-item">
<span class="info-item__label">诊所类型</span>
<span class="info-item__value">{{ getClinicTypeText(Number(row.clinic_type)) }}</span>
</div>
<div class="info-item">
<span class="info-item__label">管理员</span>
<template v-if="row.store_admin?.phone">
<Button
type="link"
size="small"
class="info-item__action"
@click="onCopy(row.store_admin.phone)"
>
{{ row.store_admin.phone }}
</Button>
</template>
<span v-else class="info-item__value">-</span>
</div>
</template>
<template v-else>
<div class="info-item">
<span class="info-item__label">MES</span>
<Button
type="link"
size="small"
class="info-item__action"
@click="onEditExternalField(row, 'mes_id', 'MES ID')"
>
{{ row.mes_id != null && row.mes_id !== '' ? row.mes_id : '-' }}
</Button>
</div>
<div class="info-item info-item--empty" />
</template>
</div>
</div>
</template>
<style scoped>
.store-basic-info {
line-height: 1.5;
}
.store-basic-info__name {
font-weight: 500;
margin-bottom: 6px;
}
.store-basic-info__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 8px;
}
.info-item {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
font-size: 12px;
color: rgb(107 114 128);
}
.info-item--empty {
visibility: hidden;
}
.info-item__label {
flex-shrink: 0;
min-width: 48px;
color: rgb(156 163 175);
}
.info-item__value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info-item__action {
height: auto;
padding: 0;
font-size: 12px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,244 @@
<script lang="ts" setup>
import { Switch, Tag } from 'ant-design-vue';
defineProps<{
row: Record<string, any>;
section: 'switches' | 'business' | 'all';
showClinicType?: boolean;
showInsurance?: boolean;
showSalespersonSeePrice?: boolean;
getBankCardReportTagColor: (status: number) => string;
getBankCardReportTagText: (row: Record<string, any>) => string;
onShippingFree: (id: number) => void;
onSubscribe: (id: number) => void;
onSeeRate: (id: number) => void;
onOrderPriceAdjust: (id: number) => void;
onBankCardReport: (row: Record<string, any>) => void;
onClinicType?: (row: Record<string, any>) => void;
onAllowInsurance?: (id: number) => void;
onSalespersonSeePrice?: (id: number) => void;
}>();
</script>
<template>
<div class="store-config-toggles">
<template v-if="section === 'switches'">
<div class="config-item">
<span class="config-item__label">是否包邮</span>
<div class="config-item__control">
<Switch
:checked="row.is_shipping_free"
:checked-value="1"
:un-checked-value="0"
checked-children="包邮"
un-checked-children="不包邮"
size="small"
@click="onShippingFree(row.id)"
/>
</div>
</div>
<div class="config-item">
<span class="config-item__label">订阅价格</span>
<div class="config-item__control">
<Tag
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
class="config-item__tag"
@click="onSubscribe(row.id)"
>
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
</Tag>
</div>
</div>
<div class="config-item">
<span class="config-item__label">查看毛利率</span>
<div class="config-item__control">
<Tag
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
class="config-item__tag"
@click="onSeeRate(row.id)"
>
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
</Tag>
</div>
</div>
<div class="config-item">
<span class="config-item__label">订单打折</span>
<div class="config-item__control">
<Tag
:color="Number(row.enable_order_price_percent_adjust) === 1 ? 'success' : 'default'"
class="config-item__tag"
@click="onOrderPriceAdjust(row.id)"
>
{{ Number(row.enable_order_price_percent_adjust) === 1 ? '开启' : '关闭' }}
</Tag>
</div>
</div>
</template>
<template v-else-if="section === 'business'">
<div v-if="showClinicType && onClinicType" class="config-item">
<span class="config-item__label">诊所类型</span>
<div class="config-item__control">
<Tag
:color="row.clinic_type === 1 ? 'blue' : row.clinic_type === 2 ? 'green' : 'default'"
class="config-item__tag clinic-type-tag"
@click="onClinicType(row)"
>
{{ row.clinic_type === 1 ? '西医诊所' : row.clinic_type === 2 ? '中医诊所' : '未设置' }}
</Tag>
</div>
</div>
<div class="config-item">
<span class="config-item__label">银行卡报备</span>
<div class="config-item__control">
<Tag
:color="getBankCardReportTagColor(row.bank_card_report_status)"
class="config-item__tag"
@click="onBankCardReport(row)"
>
{{ getBankCardReportTagText(row) }}
</Tag>
</div>
</div>
<div v-if="showInsurance && onAllowInsurance" class="config-item">
<span class="config-item__label">医保可选</span>
<div class="config-item__control">
<Tag
:color="Number(row.allow_insurance_category) === 1 ? 'success' : 'default'"
class="config-item__tag"
@click="onAllowInsurance(row.id)"
>
{{ Number(row.allow_insurance_category) === 1 ? '可选医保' : '仅自费' }}
</Tag>
</div>
</div>
<div v-if="showSalespersonSeePrice && onSalespersonSeePrice" class="config-item">
<span class="config-item__label">推广员可见价格</span>
<div class="config-item__control">
<Tag
:color="Number(row.salesperson_see_price) === 1 ? 'success' : 'default'"
class="config-item__tag"
@click="onSalespersonSeePrice(row.id)"
>
{{ Number(row.salesperson_see_price) === 1 ? '可见' : '不可见' }}
</Tag>
</div>
</div>
</template>
<template v-else-if="section === 'all'">
<div class="config-item">
<span class="config-item__label">是否包邮</span>
<div class="config-item__control">
<Switch
:checked="row.is_shipping_free"
:checked-value="1"
:un-checked-value="0"
checked-children="包邮"
un-checked-children="不包邮"
size="small"
@click="onShippingFree(row.id)"
/>
</div>
</div>
<div class="config-item">
<span class="config-item__label">银行卡报备</span>
<div class="config-item__control">
<Tag
:color="getBankCardReportTagColor(row.bank_card_report_status)"
class="config-item__tag"
@click="onBankCardReport(row)"
>
{{ getBankCardReportTagText(row) }}
</Tag>
</div>
</div>
<div class="config-item">
<span class="config-item__label">订阅价格</span>
<div class="config-item__control">
<Tag
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
class="config-item__tag"
@click="onSubscribe(row.id)"
>
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
</Tag>
</div>
</div>
<div class="config-item">
<span class="config-item__label">查看毛利率</span>
<div class="config-item__control">
<Tag
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
class="config-item__tag"
@click="onSeeRate(row.id)"
>
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
</Tag>
</div>
</div>
<div class="config-item">
<span class="config-item__label">订单打折</span>
<div class="config-item__control">
<Tag
:color="Number(row.enable_order_price_percent_adjust) === 1 ? 'success' : 'default'"
class="config-item__tag"
@click="onOrderPriceAdjust(row.id)"
>
{{ Number(row.enable_order_price_percent_adjust) === 1 ? '开启' : '关闭' }}
</Tag>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.store-config-toggles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 12px;
}
.config-item {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
font-size: 12px;
}
.config-item__label {
flex-shrink: 0;
min-width: 72px;
color: rgb(156 163 175);
line-height: 1.4;
}
.config-item__control {
min-width: 0;
flex: 1;
}
.config-item__tag {
cursor: pointer;
margin: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.clinic-type-tag {
transition: all 0.2s ease;
user-select: none;
}
.clinic-type-tag:hover {
opacity: 0.8;
transform: scale(1.05);
}
.clinic-type-tag:active {
transform: scale(0.95);
}
</style>

View File

@@ -114,6 +114,19 @@ export const modalFormProps: VbenFormProps = {
label: '是否包邮',
rules: 'required',
},
{
component: 'RadioGroup',
formItemClass: 'col-span-6',
componentProps: {
options: [
{ label: '关闭', value: 0 },
{ label: '开启', value: 1 },
],
},
fieldName: 'enable_order_price_percent_adjust',
label: '订单打折/涨幅',
defaultValue: 0,
},
{
// 诊所类型(单选)
component: 'RadioGroup',
@@ -167,13 +180,10 @@ export const modalFormProps: VbenFormProps = {
rules: 'required',
},
{
component: 'VbenInput',
component: 'PromoterPicker',
formItemClass: 'col-span-6',
componentProps: {
placeholder: '请输入业务码',
},
fieldName: 'code',
label: '业务码',
label: '业务员',
rules: 'required',
},
{

View File

@@ -23,14 +23,12 @@ export const gridOptions: VxeGridProps<RowType> = {
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'name', align: 'left', title: '诊所名称', width: 140 },
{
field: 'clinic_type',
field: 'store_basic_info',
align: 'left',
title: '诊所类型',
width: 120,
slots: { default: 'clinic_type' },
title: '基础信息',
width: 280,
slots: { default: 'store_basic_info' },
},
{
field: 'online_consultation_config',
@@ -40,47 +38,18 @@ export const gridOptions: VxeGridProps<RowType> = {
slots: { default: 'online_consultation_config' },
},
{
field: 'is_shipping_free',
field: 'store_config_1',
align: 'left',
title: '是否包邮',
slots: { default: 'is_shipping_free' },
width: 120,
title: '开关配置',
width: 330,
slots: { default: 'store_config_1' },
},
{
field: 'bank_card_report_status',
field: 'store_config_2',
align: 'left',
title: '银行卡报备',
slots: { default: 'bank_card_report_status' },
width: 120,
},
{
// 订阅价格波动列(显示为开关)
field: 'subscribe_price_change',
align: 'left',
title: '订阅价格',
slots: { default: 'subscribe_price_change' },
width: 120,
},
{
field: 'see_rate',
align: 'left',
title: '查看毛利率',
slots: { default: 'see_rate' },
width: 120,
},
{
field: 'allow_insurance_category',
align: 'left',
title: '医保可选',
slots: { default: 'allow_insurance_category' },
width: 110,
},
{
field: 'salesperson_see_price',
align: 'left',
title: '推广员可见价格',
slots: { default: 'salesperson_see_price' },
width: 130,
title: '业务配置',
width: 330,
slots: { default: 'store_config_2' },
},
{
field: 'qr_code',
@@ -89,24 +58,8 @@ export const gridOptions: VxeGridProps<RowType> = {
slots: { default: 'qr_code' },
width: 130,
},
{
field: 'erp_id',
align: 'left',
title: 'ERP ID',
width: 140,
slots: { default: 'erp_id_cell' },
},
// {
// field: 'mes_id',
// align: 'left',
// title: 'MES ID',
// width: 140,
// slots: { default: 'mes_id_cell' },
// },
// { field: 'shouzimu', title: '诊所首字母' },
{ field: 'position', title: '详细地址', width: 140 },
{ field: 'new_admin.nick_name', title: '业务员', slots: { default: 'sales_admin' }, width: 140 },
{ field: 'store_admin.phone', title: '管理员手机', slots: { default: 'store_admin_phone' }, width: 140 },
{ field: 'new_admin.nick_name', title: '业务员', slots: { default: 'sales_admin' }, width: 160 },
{ field: 'mobile', title: '联系人、电话', slots: { default: 'mobile' }, width: 140 },
{
field: 'start_time',
@@ -127,13 +80,11 @@ export const gridOptions: VxeGridProps<RowType> = {
pagerConfig: {},
proxyConfig: {
ajax: {
// 请求后端接口方法
// 注意:会自动添加type=0参数,只查询诊所类型的数据
query: async ({ page }, formValues) => {
return await getStoreList({
page: page.currentPage,
pageSize: page.pageSize,
type: 0, // 固定为诊所类型
type: 0,
...formValues,
});
},
@@ -142,19 +93,16 @@ export const gridOptions: VxeGridProps<RowType> = {
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
// custom: true, // 自定义列
zoom: true, // 最大化最小化
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},

View File

@@ -8,11 +8,12 @@ import { Page, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { useClipboard } from '@vueuse/core';
import { Button, Image, message, Modal, Switch, Tag } from 'ant-design-vue';
import { Button, Image, message, Modal, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { Icon } from '#/components/icon';
import QrCodePreview from '#/components/modal/QrCodePreview.vue';
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
import { TableAction } from '#/components/table-action';
import {
@@ -26,8 +27,11 @@ import {
updateStoreShippingFree,
updateStoreSeeRateStatus,
updateStoreSubscribeStatus,
updateStoreOrderPricePercentAdjust,
} from './api';
import FormModalDemo from './components/modal.vue';
import StoreBasicInfoCell from './components/cells/StoreBasicInfoCell.vue';
import StoreConfigTogglesCell from './components/cells/StoreConfigTogglesCell.vue';
import DrugPriceModal from './components/DrugPriceModal.vue';
import BindConsultationModal from './components/BindConsultationModal.vue';
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
@@ -296,6 +300,13 @@ const updateSalespersonSeePrice = (storeId: number) => {
});
};
const updateOrderPricePercentAdjust = (id: number) => {
updateStoreOrderPricePercentAdjust({ id }).then(() => {
message.success('修改成功!');
gridApi.query();
});
};
/**
* 批量同步总仓库药品
*/
@@ -408,14 +419,48 @@ 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 #store_basic_info="{ row }">
<StoreBasicInfoCell
:row="row"
variant="clinic"
:on-copy="copyText"
:on-edit-external-field="showStoreExternalFieldModal"
/>
</template>
<template #store_admin_phone="{ row }">
{{ row.store_admin?.phone || '-' }}
<template #store_config_1="{ row }">
<StoreConfigTogglesCell
:row="row"
section="switches"
:get-bank-card-report-tag-color="getBankCardReportTagColor"
:get-bank-card-report-tag-text="getBankCardReportTagText"
:on-shipping-free="updateShippingFree"
:on-subscribe="updateSubscribe"
:on-see-rate="updateSeeRate"
:on-order-price-adjust="updateOrderPricePercentAdjust"
:on-bank-card-report="handleBankCardReportColumnClick"
/>
</template>
<template #store_config_2="{ row }">
<StoreConfigTogglesCell
:row="row"
section="business"
show-clinic-type
show-insurance
show-salesperson-see-price
:get-bank-card-report-tag-color="getBankCardReportTagColor"
:get-bank-card-report-tag-text="getBankCardReportTagText"
:on-shipping-free="updateShippingFree"
:on-subscribe="updateSubscribe"
:on-see-rate="updateSeeRate"
:on-order-price-adjust="updateOrderPricePercentAdjust"
:on-bank-card-report="handleBankCardReportColumnClick"
:on-clinic-type="handleSwitchClinicType"
:on-allow-insurance="updateAllowInsurance"
:on-salesperson-see-price="updateSalespersonSeePrice"
/>
</template>
<template #sales_admin="{ row }">
<PromoterInfoCard :admin="row.new_admin" size="compact" />
</template>
<template #mobile="{ row }">
<div>
@@ -444,37 +489,6 @@ const handleSwitchClinicType = (row: any) => {
/>
</div>
</template>
<template #erp_id_cell="{ row }">
<Button
type="link"
size="small"
class="h-auto p-0"
@click="showStoreExternalFieldModal(row, 'erp_id', 'ERP ID')"
>
{{ row.erp_id != null && row.erp_id !== '' ? row.erp_id : '-' }}
</Button>
</template>
<template #mes_id_cell="{ row }">
<Button
type="link"
size="small"
class="h-auto p-0"
@click="showStoreExternalFieldModal(row, 'mes_id', 'MES ID')"
>
{{ row.mes_id != null && row.mes_id !== '' ? row.mes_id : '-' }}
</Button>
</template>
<template #type="{ row }">
</template>
<template #clinic_type="{ row }">
<Tag
:color="row.clinic_type === 1 ? 'blue' : row.clinic_type === 2 ? 'green' : 'default'"
class="clinic-type-tag cursor-pointer"
@click="handleSwitchClinicType(row)"
>
{{ row.clinic_type === 1 ? '西医诊所' : row.clinic_type === 2 ? '中医诊所' : '未设置' }}
</Tag>
</template>
<template #online_consultation_config="{ row }">
<div
v-if="row.is_internet_medical === 1 && row.online_consultation_doctor_name"
@@ -495,61 +509,6 @@ const handleSwitchClinicType = (row: any) => {
绑定
</Button>
</template>
<template #is_shipping_free="{ row }">
<Switch
:checked="row.is_shipping_free"
:checked-value="1"
:un-checked-value="0"
checked-children="包邮"
un-checked-children="不包邮"
@click="updateShippingFree(row.id)"
/>
</template>
<template #bank_card_report_status="{ row }">
<Tag
:color="getBankCardReportTagColor(row.bank_card_report_status)"
style="cursor: pointer"
@click="handleBankCardReportColumnClick(row)"
>
{{ getBankCardReportTagText(row) }}
</Tag>
</template>
<template #subscribe_price_change="{ row }">
<Tag
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateSubscribe(row.id)"
>
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
</Tag>
</template>
<template #see_rate="{ row }">
<Tag
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateSeeRate(row.id)"
>
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
</Tag>
</template>
<template #allow_insurance_category="{ row }">
<Tag
:color="Number(row.allow_insurance_category) === 1 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateAllowInsurance(row.id)"
>
{{ Number(row.allow_insurance_category) === 1 ? '可选医保' : '仅自费' }}
</Tag>
</template>
<template #salesperson_see_price="{ row }">
<Tag
:color="Number(row.salesperson_see_price) === 1 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateSalespersonSeePrice(row.id)"
>
{{ Number(row.salesperson_see_price) === 1 ? '可见' : '不可见' }}
</Tag>
</template>
<template #start-time="{ row }">
早:<Tag color="success">{{ row.start_time }}</Tag>
<br />
@@ -676,19 +635,4 @@ const handleSwitchClinicType = (row: any) => {
.qr_code:hover {
cursor: pointer;
}
/* 诊所类型标签样式 */
.clinic-type-tag {
transition: all 0.2s ease;
user-select: none;
&:hover {
opacity: 0.8;
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
}
</style>

View File

@@ -0,0 +1,17 @@
import { requestClient } from '#/api/request';
const prefix = 'system-config/';
export async function getSystemConfigList() {
return requestClient.get<any>(`${prefix}list`);
}
export async function saveSystemConfig(items: Array<{ config_key: string; config_value: string }>) {
return requestClient.post<any>(`${prefix}save`, { items });
}
export async function getPriceAdjustConfig(storeId?: number) {
return requestClient.get<any>(`${prefix}price-adjust-config`, {
params: storeId ? { store_id: storeId } : {},
});
}

View File

@@ -0,0 +1,139 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, Input, InputNumber, message, Radio, Space, Tag } from 'ant-design-vue';
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
import { getSystemConfigList, saveSystemConfig } from './api';
defineOptions({ name: 'SystemConfig' });
const loading = ref(false);
const saving = ref(false);
const scope = ref<'both' | 'sale_only'>('sale_only');
const quickOptions = ref<QuickDiscountOption[]>([
{ name: '九五折', value: 95 },
{ name: '九折', value: 90 },
{ name: '八五折', value: 85 },
{ name: '八折', value: 80 },
{ name: '涨10%', value: 110 },
{ name: '涨20%', value: 120 },
]);
const newQuickName = ref('');
const newQuickValue = ref<number | null>(null);
function parseQuickOptions(raw: unknown): QuickDiscountOption[] {
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw);
return parseQuickOptions(parsed);
} catch {
return quickOptions.value;
}
}
if (!Array.isArray(raw)) return quickOptions.value;
const list: QuickDiscountOption[] = [];
for (const item of raw) {
if (item && typeof item === 'object' && 'value' in item) {
const val = Number((item as QuickDiscountOption).value);
if (val > 0) list.push({ name: String((item as QuickDiscountOption).name || `${val}%`), value: val });
} else if (typeof item === 'number' && item > 0) {
list.push({ name: `${item}%`, value: item });
}
}
return list.length ? list : quickOptions.value;
}
async function load() {
loading.value = true;
try {
const list = await getSystemConfigList();
const rows = Array.isArray(list) ? list : list?.data || [];
for (const row of rows) {
if (row.config_key === 'order_price_adjust_scope') {
scope.value = row.config_value === 'both' ? 'both' : 'sale_only';
}
if (row.config_key === 'order_discount_quick_options') {
quickOptions.value = parseQuickOptions(row.config_value);
}
}
} finally {
loading.value = false;
}
}
function addQuick() {
const v = Number(newQuickValue.value);
const name = String(newQuickName.value || '').trim();