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-07-02 16:21:09 +08:00
parent a38da5ce92
commit 949167217f
42 changed files with 3414 additions and 203 deletions

View File

@@ -25,6 +25,7 @@ import { formatAddressDisplay } from '#/util/address-index';
import { createAdminGridOptions } from './table-config';
import { getRoleMeta } from './role-meta';
import DoctorCardModal from '#/views/doctor/doctor/components/DoctorCardModal.vue';
function formatAdminRegion(row: {
province_id?: number;
@@ -107,6 +108,16 @@ const [Modal, modalApi] = useVbenModal({
},
});
const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({
connectedComponent: DoctorCardModal,
});
const openDoctorCard = (row: { doctor_id?: number }) => {
if (!row.doctor_id) return;
DoctorCardModalApi.setData({ su_id: row.doctor_id });
DoctorCardModalApi.open();
};
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
@@ -195,6 +206,7 @@ const copyToClipboard = async (text: string) => {
>
<Form />
</Modal>
<DoctorCardModals />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -229,6 +241,17 @@ const copyToClipboard = async (text: string) => {
<template #avatar="{ row }">
<Image :src="row.avatar" height="30" width="30" />
</template>
<template #nick_name="{ row }">
<Button
v-if="meta.formType === 'doctor' && row.doctor_id"
type="link"
size="small"
@click="openDoctorCard(row)"
>
{{ row.nick_name }}
</Button>
<span v-else>{{ row.nick_name }}</span>
</template>
<template #region="{ row }">
{{ formatAdminRegion(row) }}
</template>

View File

@@ -26,7 +26,7 @@ function buildColumns(formType: AdminFormType) {
const cols: VxeGridProps<RowType>['columns'] = [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'nick_name', align: 'left', title: '名称' },
{ field: 'nick_name', align: 'left', title: '名称', ...(formType === 'doctor' ? { slots: { default: 'nick_name' } } : {}) },
{
field: 'avatar',
align: 'left',

View File

@@ -77,6 +77,7 @@ const [Modal, modalApi] = useVbenModal({
end_time: dayjs(values.end_time || '20:00', 'HH:mm'),
// 确保订阅状态有值,如果后端没有返回则默认为0(订阅)
subscribe_price_change: values.subscribe_price_change ?? 0,
sync_admin_phone: false,
});
} else {
formApi.setValues({

View File

@@ -154,6 +154,23 @@ export const modalFormProps: VbenFormProps = {
label: '联系人手机号',
rules: 'required',
},
{
// 编辑时可选:同步修改药店管理员后台登录手机号
component: 'Switch',
formItemClass: 'col-span-12',
fieldName: 'sync_admin_phone',
label: '同步管理员登录号',
defaultValue: false,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
help: '勾选后,保存时将同步修改药店管理员后台登录手机号(需已开通后台)',
dependencies: {
show: (values) => values.id != null,
triggerFields: ['id'],
},
},
{
component: 'PromoterPicker',
formItemClass: 'col-span-6',

View File

@@ -31,6 +31,16 @@ export async function bindOnlineConsultationApi(data: {
}) {
return requestClient.post<any>(`${prefix}bind-online-consultation`, data);
}
/**
* 绑定特色方默认挂号医生
*/
export async function updateSpecialPrescriptionDoctorApi(data: {
id: number;
special_prescription_doctor_id?: number | null;
}) {
return requestClient.post<any>(`${prefix}update-special-prescription-doctor`, data);
}
/**
* 分页查询用户列表
* @param data

View File

@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, message, Select } from 'ant-design-vue';
import { getDoctorOptionApi, updateSpecialPrescriptionDoctorApi } from '../api';
const formState = ref({
id: undefined as number | undefined,
special_prescription_doctor_id: undefined as number | undefined,
});
const doctorOptions = ref<Array<{ id: number; name: string }>>([]);
const gridApiRef = ref<any>(null);
const loading = ref(false);
async function loadDoctorOptions(storeId?: number) {
if (!storeId) {
doctorOptions.value = [];
return;
}
try {
const doctors = await getDoctorOptionApi(storeId);
doctorOptions.value = doctors || [];
} catch (error) {
console.error('获取医生列表失败:', error);
doctorOptions.value = [];
}
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
loading.value = true;
modalApi.setState({ confirmLoading: true });
try {
await updateSpecialPrescriptionDoctorApi({
id: formState.value.id!,
special_prescription_doctor_id: formState.value.special_prescription_doctor_id || null,
});
message.success('绑定成功');
gridApiRef.value?.reload();
modalApi.close();
} catch {
message.error('绑定失败');
} finally {
loading.value = false;
modalApi.setState({ confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values, gridApi } = modalApi.getData<Record<string, any>>();
gridApiRef.value = gridApi;
formState.value = {
id: values?.id,
special_prescription_doctor_id: values?.special_prescription_doctor_id || undefined,
};
await loadDoctorOptions(formState.value.id);
}
},
});
</script>
<template>
<Modal title="绑定特色方默认医生" class="w-[500px]">
<Form :model="formState" layout="vertical">
<FormItem label="特色方默认挂号医生">
<Select
v-model:value="formState.special_prescription_doctor_id"
:options="doctorOptions"
:field-names="{ label: 'name', value: 'id' }"
placeholder="请选择特色方默认挂号医生"
allow-clear
show-search
:filter-option="(input: string, option: any) => option.name.toLowerCase().includes(input.toLowerCase())"
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -56,6 +56,7 @@ const [Modal, modalApi] = useVbenModal({
end_time: dayjs(values.end_time || '20:00', 'HH:mm'),
// 确保订阅状态有值,如果后端没有返回则默认为0(订阅)
subscribe_price_change: values.subscribe_price_change ?? 0,
sync_admin_phone: false,
});
}
}

View File

@@ -179,6 +179,23 @@ export const modalFormProps: VbenFormProps = {
label: '联系人手机号',
rules: 'required',
},
{
// 编辑时可选:同步修改诊所管理员后台登录手机号
component: 'Switch',
formItemClass: 'col-span-12',
fieldName: 'sync_admin_phone',
label: '同步管理员登录号',
defaultValue: false,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
help: '勾选后,保存时将同步修改诊所管理员后台登录手机号(需已开通后台)',
dependencies: {
show: (values) => values.id != null,
triggerFields: ['id'],
},
},
{
component: 'PromoterPicker',
formItemClass: 'col-span-6',

View File

@@ -37,6 +37,13 @@ export const gridOptions: VxeGridProps<RowType> = {
width: 160,
slots: { default: 'online_consultation_config' },
},
{
field: 'special_prescription_config',
align: 'left',
title: '特色方配置',
width: 140,
slots: { default: 'special_prescription_config' },
},
{
field: 'store_config_1',
align: 'left',

View File

@@ -34,6 +34,7 @@ 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 BindSpecialPrescriptionDoctorModal from './components/BindSpecialPrescriptionDoctorModal.vue';
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
import SalespersonCommissionDrawer from './components/SalespersonCommissionDrawer.vue';
import BankCardReportModal from '#/views/system/store-bank-card/components/BankCardReportModal.vue';
@@ -102,6 +103,11 @@ const [BindConsultationModalComponent, bindConsultationModalApi] = useVbenModal(
connectedComponent: BindConsultationModal,
});
const [BindSpecialPrescriptionDoctorModalComponent, bindSpecialPrescriptionDoctorModalApi] =
useVbenModal({
connectedComponent: BindSpecialPrescriptionDoctorModal,
});
const [StoreExternalFieldModalComponent, storeExternalFieldModalApi] = useVbenModal({
connectedComponent: StoreExternalFieldModal,
});
@@ -216,6 +222,14 @@ const showBindConsultationModal = (row: any) => {
bindConsultationModalApi.open();
};
const showBindSpecialPrescriptionDoctorModal = (row: any) => {
bindSpecialPrescriptionDoctorModalApi.setData({
values: row,
gridApi,
});
bindSpecialPrescriptionDoctorModalApi.open();
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
@@ -369,6 +383,7 @@ const handleSwitchClinicType = (row: any) => {
<QrCodePreviewModal />
<DrugPriceModalComponent />
<BindConsultationModalComponent />
<BindSpecialPrescriptionDoctorModalComponent />
<StoreExternalFieldModalComponent />
<SalespersonCommissionDrawer ref="salespersonDrawerRef" />
<Grid>
@@ -512,6 +527,23 @@ const handleSwitchClinicType = (row: any) => {
绑定
</Button>
</template>
<template #special_prescription_config="{ row }">
<div
v-if="row.special_prescription_doctor_id"
class="cursor-pointer hover:text-blue-500"
@click="showBindSpecialPrescriptionDoctorModal(row)"
>
<div>医生ID:{{ row.special_prescription_doctor_id }}</div>
</div>
<Button
v-else
type="link"
size="small"
@click="showBindSpecialPrescriptionDoctorModal(row)"
>
绑定
</Button>
</template>
<template #start-time="{ row }">
早:<Tag color="success">{{ row.start_time }}</Tag>
<br />
@@ -595,6 +627,14 @@ const handleSwitchClinicType = (row: any) => {
ifShow: isPlatformAdmin,
onClick: showDrugPriceModal.bind(null, row),
},
{
label: '绑定特色方医生',
type: 'link',
icon: 'ant-design:user-add-outlined',
size: 'small',
auth: ['Super Admin', 'Admin'],
onClick: () => showBindSpecialPrescriptionDoctorModal(row),
},
{
label: '查看卡状态',
type: 'link',

View File

@@ -3,16 +3,21 @@ import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, Input, InputNumber, message, Radio, Space, Switch, Tag } from 'ant-design-vue';
import { Button, Card, Input, InputNumber, message, Radio, Space, Switch, Tabs, Tag } from 'ant-design-vue';
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
import FormAvatar from '#/components/form/components/avatar.vue';
import { getSystemConfigList, saveSystemConfig } from './api';
defineOptions({ name: 'SystemConfig' });
const TAB_STORAGE_KEY = 'system_config_active_tab';
const loading = ref(false);
const saving = ref(false);
const activeKey = ref('price_adjust');
const scope = ref<'both' | 'sale_only'>('sale_only');
const quickOptions = ref<QuickDiscountOption[]>([
{ name: '九五折', value: 95 },
@@ -25,6 +30,19 @@ const quickOptions = ref<QuickDiscountOption[]>([
const newQuickName = ref('');
const newQuickValue = ref<number | null>(null);
const inputAutoAuditPass = ref(true);
const miniprogramEmptyImage = ref('');
function readStoredTab() {
const stored = localStorage.getItem(TAB_STORAGE_KEY);
if (stored === 'price_adjust' || stored === 'input_audit' || stored === 'miniprogram') {
activeKey.value = stored;
}
}
function handleTabChange(key: string | number) {
activeKey.value = String(key);
localStorage.setItem(TAB_STORAGE_KEY, String(key));
}
function parseQuickOptions(raw: unknown): QuickDiscountOption[] {
if (typeof raw === 'string') {
@@ -63,6 +81,9 @@ async function load() {
if (row.config_key === 'input_auto_audit_pass') {
inputAutoAuditPass.value = row.config_value === '1' || row.config_value === true || row.config_value === 'true';
}
if (row.config_key === 'miniprogram_empty_image') {
miniprogramEmptyImage.value = String(row.config_value || '');
}
}
} finally {
loading.value = false;
@@ -100,6 +121,10 @@ async function handleSave() {
config_key: 'input_auto_audit_pass',
config_value: inputAutoAuditPass.value ? '1' : '0',
},
{
config_key: 'miniprogram_empty_image',
config_value: miniprogramEmptyImage.value,
},
]);
message.success('保存成功');
} finally {
@@ -107,46 +132,64 @@ async function handleSave() {
}
}
onMounted(load);
onMounted(() => {
readStoredTab();
load();
});
</script>
<template>
<Page auto-content-height title="系统配置">
<Card :loading="loading" title="订单调价配置">
<div class="mb-6">
<div class="mb-2 font-medium">订单打折时价格浮动范围</div>
<Radio.Group v-model:value="scope">
<Radio value="sale_only">仅售价浮动</Radio>
<Radio value="both">供货价与售价一起浮动</Radio>
</Radio.Group>
</div>
<Card :loading="loading">
<Tabs :active-key="activeKey" @change="handleTabChange">
<Tabs.TabPane key="price_adjust" tab="订单调价">
<div class="py-4">
<div class="mb-6">
<div class="mb-2 font-medium">订单打折时价格浮动范围</div>
<Radio.Group v-model:value="scope">
<Radio value="sale_only">仅售价浮动</Radio>
<Radio value="both">供货价与售价一起浮动</Radio>
</Radio.Group>
</div>
<div class="mb-6">
<div class="mb-2 font-medium">调价快捷选项(name 展示名,value 比例:100=原价,90=九折,120=涨20%)</div>
<Space wrap class="mb-3">
<Tag
v-for="item in quickOptions"
:key="item.name + item.value"
closable
:color="item.value < 100 ? 'processing' : 'warning'"
@close="removeQuick(item)"
>
{{ item.name }}({{ item.value }}%)
</Tag>
</Space>
<Space wrap>
<Input v-model:value="newQuickName" placeholder="名称,如九折" style="width: 140px" />
<InputNumber v-model:value="newQuickValue" :min="1" :max="500" placeholder="比例值" />
<Button @click="addQuick">添加快捷项</Button>
</Space>
</div>
<div class="mb-6">
<div class="mb-2 font-medium">调价快捷选项(name 展示名,value 比例:100=原价,90=九折,120=涨20%)</div>
<Space wrap class="mb-3">
<Tag
v-for="item in quickOptions"
:key="item.name + item.value"
closable
:color="item.value < 100 ? 'processing' : 'warning'"
@close="removeQuick(item)"
>
{{ item.name }}({{ item.value }}%)
</Tag>
</Space>
<Space wrap>
<Input v-model:value="newQuickName" placeholder="名称,如九折" style="width: 140px" />
<InputNumber v-model:value="newQuickValue" :min="1" :max="500" placeholder="比例值" />
<Button @click="addQuick">添加快捷项</Button>
</Space>
</div>
</div>
</Tabs.TabPane>
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
</Card>
<Tabs.TabPane key="input_audit" tab="录入审核">
<div class="py-4">
<div class="mb-2 font-medium">诊所/医生信息预填录入后是否自动通过审核</div>
<Switch v-model:checked="inputAutoAuditPass" checked-children="是" un-checked-children="否" />
</div>
</Tabs.TabPane>
<Tabs.TabPane key="miniprogram" tab="小程序">
<div class="py-4">
<div class="mb-2 font-medium">空状态图片</div>
<div class="mb-2 text-sm text-gray-500">用于小程序列表等无数据时的占位图</div>
<FormAvatar v-model:value="miniprogramEmptyImage" />
</div>
</Tabs.TabPane>
</Tabs>
<Card :loading="loading" class="mt-4" title="录入审核配置">
<div class="mb-2 font-medium">诊所/医生信息预填录入后是否自动通过审核</div>
<Switch v-model:checked="inputAutoAuditPass" checked-children="是" un-checked-children="否" />
<div class="mt-4">
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
</div>