1. 特色方功能迭代(固定价格),固定价格目前统一使用药品id(-102),后期会考虑新增特色方的时候自动添加一个特色方药品
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
Close stale issues / stale (push) Has been cancelled

2. 退款的时候可以退回分成
This commit is contained in:
李琦
2026-07-10 08:42:05 +08:00
parent d93e465d3c
commit a05917ac18
12 changed files with 691 additions and 100 deletions

View File

@@ -0,0 +1,10 @@
---
description:
alwaysApply: true
---
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化

View File

@@ -0,0 +1,148 @@
<script lang="ts" setup>
import { Avatar, Button } from 'ant-design-vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
defineProps<{
row: Record<string, any>;
}>();
const emit = defineEmits<{
openDoctor: [row: Record<string, any>];
}>();
/** 默认头像路径 */
const defaultAvatar = '/img/user-default-avatar.png';
/**
* 解析下单微信用户头像,空值回退默认图
*/
function getUserAvatarSrc(row: Record<string, any>) {
const raw = String(row.user?.avatarurl ?? '').trim();
if (!raw) return defaultAvatar;
return resolveAvatarUrl(raw) || defaultAvatar;
}
/**
* 解析医生头像,空值回退默认图
*/
function getDoctorAvatarSrc(row: Record<string, any>) {
const raw = String(row.doctor?.avatar ?? '').trim();
if (!raw) return defaultAvatar;
return resolveAvatarUrl(raw) || defaultAvatar;
}
/**
* 格式化就诊人年龄副文本
*/
function formatPatientAge(row: Record<string, any>) {
const age = row.patient_age;
if (age == null || age === '') return '';
return `${age}`;
}
</script>
<template>
<div class="order-user-info">
<!-- 行1下单微信用户 -->
<div class="order-user-info__row">
<Avatar
:size="24"
:src="getUserAvatarSrc(row)"
class="shrink-0"
/>
<div class="order-user-info__text min-w-0">
<span class="text-[11px] leading-tight text-gray-400 dark:text-slate-500">
下单用户
</span>
<span class="truncate text-xs text-gray-700 dark:text-slate-200">
{{ row.user?.nickname || '—' }}
</span>
<div class="text-[11px] text-gray-500 dark:text-slate-400">
ID{{ row.user?.id ?? '—' }}
</div>
</div>
</div>
<!-- 行2开方医生可点击查看档案 -->
<div class="order-user-info__row">
<Avatar
:size="24"
:src="getDoctorAvatarSrc(row)"
class="shrink-0"
/>
<div class="order-user-info__text min-w-0" >
<span class="text-[11px] leading-tight text-gray-400 dark:text-slate-500">
医生
</span>
<Button
v-if="row.doctor?.name"
class="order-user-info__doctor-btn !h-auto !px-0 !py-0 dark:!text-blue-400"
type="link"
@click="emit('openDoctor', row)"
>
{{ row.doctor.name }}
</Button>
<!-- <template v-if="row.doctor?.name">-->
<!-- <Button-->
<!-- v-if="row.doctor?.name"-->
<!-- class="order-user-info__doctor-btn !h-auto !px-0 !py-0 dark:!text-blue-400"-->
<!-- type="link"-->
<!-- @click="emit('openDoctor', row)"-->
<!-- >-->
<!-- {{ row.doctor.name }}-->
<!-- </Button>-->
<!-- </template>-->
<div v-else class="text-xs text-gray-700 dark:text-slate-200"></div>
</div>
</div>
<!-- 行3就诊人与上两行保持 Avatar + 文本区结构 -->
<div class="order-user-info__row">
<Avatar :size="24" :src="defaultAvatar" class="shrink-0" />
<div class="order-user-info__text min-w-0">
<span class="text-[11px] leading-tight text-gray-400 dark:text-slate-500">
就诊人
</span>
<span class="truncate text-xs text-gray-700 dark:text-slate-200">
{{ row.patient || '—' }}
</span>
<div
v-if="formatPatientAge(row)"
class="text-[11px] text-gray-500 dark:text-slate-400"
>
{{ formatPatientAge(row) }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.order-user-info {
display: flex;
flex-direction: column;
gap: 6px;
line-height: 1.4;
}
.order-user-info__row {
display: flex;
align-items: flex-start;
gap: 6px;
}
.order-user-info__text {
flex: 1;
min-width: 0;
text-align: left;
}
.order-user-info__doctor-btn {
font-size: 12px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
//display: block;
text-align: left;
}
</style>

View File

@@ -26,10 +26,10 @@ export const gridOptions: VxeGridProps<RowType> = {
slots: { default: 'order-store' }, slots: { default: 'order-store' },
}, },
{ {
field: 'user.avatarUrl', field: 'order_user_info',
title: '下单用户信息', title: '用户/医生/就诊人',
slots: { default: 'avatar' }, slots: { default: 'order-user-info' },
width: 100, width: 210,
}, },
{ {
field: 'express_info', field: 'express_info',

View File

@@ -35,8 +35,10 @@ import { getOrderPriceAdjustConfig } from '#/api/order/priceAdjust';
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust'; import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
import { normalizeQuickOptions } from '#/utils/pricePercentAdjust'; import { normalizeQuickOptions } from '#/utils/pricePercentAdjust';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue'; import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import DoctorCardModal from '#/views/doctor/doctor/components/DoctorCardModal.vue';
import DetailModal from './components/detail.vue'; import DetailModal from './components/detail.vue';
import OrderUserInfoCell from './components/cells/OrderUserInfoCell.vue';
import FormModalDemo from './components/modal.vue'; import FormModalDemo from './components/modal.vue';
import ProductOrderExportModal from './components/ProductOrderExportModal.vue'; import ProductOrderExportModal from './components/ProductOrderExportModal.vue';
import Refund from './components/refund.vue'; import Refund from './components/refund.vue';
@@ -198,6 +200,25 @@ const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail, connectedComponent: PrescriptionDetail,
}); });
const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({
connectedComponent: DoctorCardModal,
});
/** 从订单列表以只读模式打开医生档案 */
function showOrderDoctorCard(row: Record<string, any>) {
const doctor = row.doctor;
if (!doctor?.id && !row.su_id && !doctor?.su_id) {
message.warning('该订单无关联医生');
return;
}
DoctorCardModalApi.setData({
id: doctor?.id,
su_id: row.su_id ?? doctor?.su_id,
readonly: true,
});
DoctorCardModalApi.open();
}
const [TraceDrawer, traceDrawerApi] = useVbenDrawer({ const [TraceDrawer, traceDrawerApi] = useVbenDrawer({
connectedComponent: OrderTraceDrawer, connectedComponent: OrderTraceDrawer,
}); });
@@ -501,6 +522,7 @@ const openOrderAmountVerify = () => {
</Table> </Table>
</AntdModal> </AntdModal>
<RefundModal /> <RefundModal />
<DoctorCardModals />
<ExportModal /> <ExportModal />
<PrescriptionDetailModal /> <PrescriptionDetailModal />
<TraceDrawer /> <TraceDrawer />
@@ -547,10 +569,8 @@ const openOrderAmountVerify = () => {
</template> </template>
</TableAction> </TableAction>
</template> </template>
<template #avatar="{ row }"> <template #order-user-info="{ row }">
<Image :src="row.user.avatarurl || '/img/user-default-avatar.png'" /> <OrderUserInfoCell :row="row" @open-doctor="showOrderDoctorCard" />
{{ row.user.nickname }}
<div>ID{{ row.user.id }}</div>
</template> </template>
<template #order-store="{ row }"> <template #order-store="{ row }">
<div class="leading-snug"> <div class="leading-snug">

View File

@@ -66,6 +66,16 @@ export async function updateDoctorSubjectsBindApi(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-subjects-bind`, data); return requestClient.post<any>(`${prefix}update-subjects-bind`, data);
} }
/** doctor_id 为 su_id更新科室绑定 */
export async function updateDoctorDepartBindApi(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-depart-bind`, data);
}
/** doctor_id 为 su_id更新职称绑定 */
export async function updateDoctorTitleBindApi(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-title-bind`, data);
}
/** /**
* 新增医生 * 新增医生
* @param data * @param data

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
import {
getDepartOptionApi,
updateDoctorDepartBindApi,
} from '#/views/doctor/doctor/api';
const gridApi = ref();
const data = ref<Record<string, any>>();
const optionData = ref<{ label: string; value: number }[]>([]);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
const suId = data.value?.su_id;
if (suId == null) {
message.error('缺少医生信息');
return;
}
const departId =
data.value?.depart_id === undefined || data.value?.depart_id === null
? 0
: Number(data.value.depart_id);
await updateDoctorDepartBindApi({
doctor_id: suId,
depart_id: Number.isNaN(departId) ? 0 : departId,
});
modalApi.close();
message.success('保存成功');
gridApi.value?.reload?.();
gridApi.value?.query?.();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
gridApi.value = modalApi.getData()?.gridApi ?? null;
const payload = modalApi.getData<Record<string, any>>();
const { values } = payload ?? {};
if (values) {
data.value = JSON.parse(JSON.stringify(values));
if (
data.value.depart_id === undefined ||
data.value.depart_id === null
) {
data.value.depart_id = undefined;
} else {
data.value.depart_id = Number(data.value.depart_id) || undefined;
}
loadDepartOptions();
}
}
},
});
/** 加载科室下拉选项 */
const loadDepartOptions = () => {
getDepartOptionApi().then((res: any) => {
const list = Array.isArray(res) ? res : [];
optionData.value = list.map((item: any) => ({
label: item.name,
value: item.id,
}));
});
};
</script>
<template>
<Modal class="w-[400px]" title="编辑科室">
<div v-if="data" class="py-4">
<Select
v-model:value="data.depart_id"
:options="optionData"
allow-clear
placeholder="请选择科室(清空表示未绑定)"
show-search
:filter-option="
(input: string, option: any) =>
String(option?.label ?? '')
.toLowerCase()
.includes(input.trim().toLowerCase())
"
style="width: 100%"
/>
</div>
</Modal>
</template>

View File

@@ -0,0 +1,92 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
import { getDoctorTitleOptionApi } from '#/views/doctor/pharmacist/api';
import { updateDoctorTitleBindApi } from '#/views/doctor/doctor/api';
const gridApi = ref();
const data = ref<Record<string, any>>();
const optionData = ref<{ label: string; value: number }[]>([]);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
const suId = data.value?.su_id;
if (suId == null) {
message.error('缺少医生信息');
return;
}
const titleId =
data.value?.title_id === undefined || data.value?.title_id === null
? 0
: Number(data.value.title_id);
await updateDoctorTitleBindApi({
doctor_id: suId,
title_id: Number.isNaN(titleId) ? 0 : titleId,
});
modalApi.close();
message.success('保存成功');
gridApi.value?.reload?.();
gridApi.value?.query?.();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
gridApi.value = modalApi.getData()?.gridApi ?? null;
const payload = modalApi.getData<Record<string, any>>();
const { values } = payload ?? {};
if (values) {
data.value = JSON.parse(JSON.stringify(values));
if (
data.value.title_id === undefined ||
data.value.title_id === null
) {
data.value.title_id = undefined;
} else {
data.value.title_id = Number(data.value.title_id) || undefined;
}
loadTitleOptions();
}
}
},
});
/** 加载职称下拉选项(医生 type=1 */
const loadTitleOptions = () => {
getDoctorTitleOptionApi().then((res: any) => {
const list = Array.isArray(res) ? res : [];
optionData.value = list.map((item: any) => ({
label: item.name,
value: item.id,
}));
});
};
</script>
<template>
<Modal class="w-[400px]" title="编辑职称">
<div v-if="data" class="py-4">
<Select
v-model:value="data.title_id"
:options="optionData"
allow-clear
placeholder="请选择职称(清空表示未绑定)"
show-search
:filter-option="
(input: string, option: any) =>
String(option?.label ?? '')
.toLowerCase()
.includes(input.trim().toLowerCase())
"
style="width: 100%"
/>
</div>
</Modal>
</template>

View File

@@ -45,6 +45,8 @@ const gridApi = ref<any>();
const doctorId = ref(0); const doctorId = ref(0);
const suId = ref(0); const suId = ref(0);
const hideStoresTab = ref(false); const hideStoresTab = ref(false);
/** 只读模式:从订单页等场景打开时禁止编辑与保存 */
const readonly = ref(false);
const loading = ref(false); const loading = ref(false);
const activeTab = ref('overview'); const activeTab = ref('overview');
const cardData = ref<any>(null); const cardData = ref<any>(null);
@@ -67,6 +69,8 @@ const userStore = useUserStore();
const canEditProfile = computed(() => const canEditProfile = computed(() =>
canEditDoctorServiceUser(userStore.userInfo), canEditDoctorServiceUser(userStore.userInfo),
); );
/** 是否允许编辑(权限 + 非只读) */
const canEdit = computed(() => canEditProfile.value && !readonly.value);
const savingServiceUser = ref(false); const savingServiceUser = ref(false);
@@ -177,7 +181,7 @@ async function loadCard() {
} }
async function saveOverview() { async function saveOverview() {
if (!canEditProfile.value) return; if (!canEdit.value) return;
savingOverview.value = true; savingOverview.value = true;
try { try {
const res = await updateDoctor({ const res = await updateDoctor({
@@ -198,7 +202,7 @@ async function saveOverview() {
} }
async function saveServiceUser() { async function saveServiceUser() {
if (!canEditProfile.value || !suId.value) return; if (!canEdit.value || !suId.value) return;
savingServiceUser.value = true; savingServiceUser.value = true;
try { try {
const res = await updateDoctorServiceUserApi({ const res = await updateDoctorServiceUserApi({
@@ -220,7 +224,7 @@ async function saveServiceUser() {
} }
async function saveCredentials() { async function saveCredentials() {
if (!canEditProfile.value) return; if (!canEdit.value) return;
savingCredentials.value = true; savingCredentials.value = true;
try { try {
await updateDoctorCredentialsApi({ await updateDoctorCredentialsApi({
@@ -363,6 +367,7 @@ const [Modal, modalApi] = useVbenModal({
doctorId.value = data?.id ?? data?.values?.id ?? 0; doctorId.value = data?.id ?? data?.values?.id ?? 0;
suId.value = data?.su_id ?? data?.values?.su_id ?? 0; suId.value = data?.su_id ?? data?.values?.su_id ?? 0;
hideStoresTab.value = !!data?.hideStoresTab; hideStoresTab.value = !!data?.hideStoresTab;
readonly.value = !!data?.readonly;
activeTab.value = 'overview'; activeTab.value = 'overview';
prescriptionList.value = []; prescriptionList.value = [];
patientList.value = []; patientList.value = [];
@@ -460,7 +465,7 @@ const [Modal, modalApi] = useVbenModal({
<Col :span="6" class="flex flex-col items-center border-r border-gray-200 dark:border-slate-700"> <Col :span="6" class="flex flex-col items-center border-r border-gray-200 dark:border-slate-700">
<div class="text-gray-500 dark:text-slate-400 mb-3 text-sm">医生头像</div> <div class="text-gray-500 dark:text-slate-400 mb-3 text-sm">医生头像</div>
<AvatarUpload <AvatarUpload
v-if="canEditProfile" v-if="canEdit"
v-model:value="overviewForm.avatar" v-model:value="overviewForm.avatar"
class="shadow-sm rounded-full overflow-hidden" class="shadow-sm rounded-full overflow-hidden"
/> />
@@ -469,18 +474,18 @@ const [Modal, modalApi] = useVbenModal({
<Col :span="18"> <Col :span="18">
<Form layout="vertical" class="grid grid-cols-2 gap-x-6"> <Form layout="vertical" class="grid grid-cols-2 gap-x-6">
<Form.Item label="医生姓名" class="mb-4"> <Form.Item label="医生姓名" class="mb-4">
<Input v-model:value="overviewForm.name" :disabled="!canEditProfile" placeholder="请输入姓名" size="large" /> <Input v-model:value="overviewForm.name" :disabled="!canEdit" placeholder="请输入姓名" size="large" />
</Form.Item> </Form.Item>
<Form.Item label="手机号码" class="mb-4"> <Form.Item label="手机号码" class="mb-4">
<Input v-model:value="overviewForm.mobile" :disabled="!canEditProfile" placeholder="请输入手机号码" size="large" /> <Input v-model:value="overviewForm.mobile" :disabled="!canEdit" placeholder="请输入手机号码" size="large" />
</Form.Item> </Form.Item>
<Form.Item label="专业擅长" class="col-span-2 mb-4"> <Form.Item label="专业擅长" class="col-span-2 mb-4">
<Input.TextArea v-model:value="overviewForm.good_at" :disabled="!canEditProfile" :rows="2" placeholder="填写医生擅长的领域..." /> <Input.TextArea v-model:value="overviewForm.good_at" :disabled="!canEdit" :rows="2" placeholder="填写医生擅长的领域..." />
</Form.Item> </Form.Item>
<Form.Item label="个人简介" class="col-span-2 mb-4"> <Form.Item label="个人简介" class="col-span-2 mb-4">
<Input.TextArea v-model:value="overviewForm.intro" :disabled="!canEditProfile" :rows="3" placeholder="填写医生简介..." /> <Input.TextArea v-model:value="overviewForm.intro" :disabled="!canEdit" :rows="3" placeholder="填写医生简介..." />
</Form.Item> </Form.Item>
<div v-if="canEditProfile" class="col-span-2 text-right mt-2"> <div v-if="canEdit" class="col-span-2 text-right mt-2">
<Button type="primary" size="large" :loading="savingOverview" @click="saveOverview"> <Button type="primary" size="large" :loading="savingOverview" @click="saveOverview">
保存基本信息 保存基本信息
</Button> </Button>
@@ -498,14 +503,14 @@ const [Modal, modalApi] = useVbenModal({
<Col :span="6" class="flex flex-col items-center border-r border-gray-200 dark:border-slate-700"> <Col :span="6" class="flex flex-col items-center border-r border-gray-200 dark:border-slate-700">
<div class="text-gray-500 dark:text-slate-400 mb-3 text-sm">账号头像</div> <div class="text-gray-500 dark:text-slate-400 mb-3 text-sm">账号头像</div>
<AvatarUpload <AvatarUpload
v-if="canEditProfile" v-if="canEdit"
v-model:value="serviceUserForm.avatar" v-model:value="serviceUserForm.avatar"
class="shadow-sm rounded-full overflow-hidden" class="shadow-sm rounded-full overflow-hidden"
/> />
<Avatar v-else :src="resolveAvatarUrl(serviceUserForm.avatar)" :size="80" /> <Avatar v-else :src="resolveAvatarUrl(serviceUserForm.avatar)" :size="80" />
</Col> </Col>
<Col :span="18"> <Col :span="18">
<Form v-if="canEditProfile" layout="vertical" class="grid grid-cols-2 gap-x-6"> <Form v-if="canEdit" layout="vertical" class="grid grid-cols-2 gap-x-6">
<Form.Item label="登录手机号" class="mb-4"> <Form.Item label="登录手机号" class="mb-4">
<Input v-model:value="serviceUserForm.mobile" placeholder="service_user.mobile" size="large" /> <Input v-model:value="serviceUserForm.mobile" placeholder="service_user.mobile" size="large" />
</Form.Item> </Form.Item>
@@ -567,48 +572,60 @@ const [Modal, modalApi] = useVbenModal({
<h3 class="text-base font-semibold text-gray-800 dark:text-slate-200">资质证书管理</h3> <h3 class="text-base font-semibold text-gray-800 dark:text-slate-200">资质证书管理</h3>
<p class="text-sm text-gray-500 dark:text-slate-400 mt-1">请上传清晰无遮挡的证件扫描件或照片</p> <p class="text-sm text-gray-500 dark:text-slate-400 mt-1">请上传清晰无遮挡的证件扫描件或照片</p>
</div> </div>
<Button v-if="canEditProfile" type="primary" :loading="savingCredentials" @click="saveCredentials"> <Button v-if="canEdit" type="primary" :loading="savingCredentials" @click="saveCredentials">
保存全部资质 保存全部资质
</Button> </Button>
</div> </div>
<Form layout="vertical"> <Form layout="vertical">
<!-- 通过比例控制让上传区域更像证件 -->
<div class="grid grid-cols-2 lg:grid-cols-3 gap-6 bg-gray-50 dark:bg-[#1f242e] p-5 rounded-lg border border-gray-100 dark:border-slate-700"> <div class="grid grid-cols-2 lg:grid-cols-3 gap-6 bg-gray-50 dark:bg-[#1f242e] p-5 rounded-lg border border-gray-100 dark:border-slate-700">
<Form.Item label="身份证 (正面)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center"> <Form.Item label="身份证 (正面)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
<div class="aspect-[1.6/1] w-full flex items-center justify-center"> <div class="aspect-[1.6/1] w-full flex items-center justify-center">
<AvatarUpload v-model:value="credentialsForm.card_up" /> <AvatarUpload v-if="canEdit" v-model:value="credentialsForm.card_up" />
<Image v-else-if="credentialsForm.card_up" :src="resolveAvatarUrl(credentialsForm.card_up)" class="max-h-full object-contain" />
<span v-else class="text-gray-400">未上传</span>
</div> </div>
</Form.Item> </Form.Item>
<Form.Item label="身份证 (反面)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center"> <Form.Item label="身份证 (反面)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
<div class="aspect-[1.6/1] w-full flex items-center justify-center"> <div class="aspect-[1.6/1] w-full flex items-center justify-center">
<AvatarUpload v-model:value="credentialsForm.card_down" /> <AvatarUpload v-if="canEdit" v-model:value="credentialsForm.card_down" />
<Image v-else-if="credentialsForm.card_down" :src="resolveAvatarUrl(credentialsForm.card_down)" class="max-h-full object-contain" />
<span v-else class="text-gray-400">未上传</span>
</div> </div>
</Form.Item> </Form.Item>
<Form.Item label="执业资格证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center"> <Form.Item label="执业资格证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
<div class="aspect-[1.6/1] w-full flex items-center justify-center"> <div class="aspect-[1.6/1] w-full flex items-center justify-center">
<AvatarUpload v-model:value="credentialsForm.qualification" /> <AvatarUpload v-if="canEdit" v-model:value="credentialsForm.qualification" />
<Image v-else-if="credentialsForm.qualification" :src="resolveAvatarUrl(credentialsForm.qualification)" class="max-h-full object-contain" />
<span v-else class="text-gray-400">未上传</span>
</div> </div>
</Form.Item> </Form.Item>
<Form.Item label="执业证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center"> <Form.Item label="执业证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
<div class="aspect-[1.6/1] w-full flex items-center justify-center"> <div class="aspect-[1.6/1] w-full flex items-center justify-center">
<AvatarUpload v-model:value="credentialsForm.practicing" /> <AvatarUpload v-if="canEdit" v-model:value="credentialsForm.practicing" />
<Image v-else-if="credentialsForm.practicing" :src="resolveAvatarUrl(credentialsForm.practicing)" class="max-h-full object-contain" />
<span v-else class="text-gray-400">未上传</span>
</div> </div>
</Form.Item> </Form.Item>
<Form.Item label="职称证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center"> <Form.Item label="职称证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
<div class="aspect-[1.6/1] w-full flex items-center justify-center"> <div class="aspect-[1.6/1] w-full flex items-center justify-center">
<AvatarUpload v-model:value="credentialsForm.title" /> <AvatarUpload v-if="canEdit" v-model:value="credentialsForm.title" />
<Image v-else-if="credentialsForm.title" :src="resolveAvatarUrl(credentialsForm.title)" class="max-h-full object-contain" />
<span v-else class="text-gray-400">未上传</span>
</div> </div>
</Form.Item> </Form.Item>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<Form.Item label="医生签名图 (透明底)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 flex-1 text-center"> <Form.Item label="医生签名图 (透明底)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 flex-1 text-center">
<AvatarUpload v-model:value="credentialsForm.sign_image" /> <AvatarUpload v-if="canEdit" v-model:value="credentialsForm.sign_image" />
<Image v-else-if="credentialsForm.sign_image" :src="resolveAvatarUrl(credentialsForm.sign_image)" class="max-h-full object-contain" />
<span v-else class="text-gray-400">未上传</span>
</Form.Item> </Form.Item>
<Form.Item label="签名验证类型" class="mb-0 bg-white dark:bg-[#18181c] p-3 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60"> <Form.Item label="签名验证类型" class="mb-0 bg-white dark:bg-[#18181c] p-3 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60">
<Select v-model:value="credentialsForm.sign_type" size="large" :options="[ <Select v-if="canEdit" v-model:value="credentialsForm.sign_type" size="large" :options="[
{ label: '电子认证签名', value: 1 }, { label: '电子认证签名', value: 1 },
{ label: '手写扫描签名', value: 2 }, { label: '手写扫描签名', value: 2 },
]" /> ]" />
<span v-else>{{ credentialsForm.sign_type === 2 ? '手写扫描签名' : '电子认证签名' }}</span>
</Form.Item> </Form.Item>
</div> </div>
</div> </div>
@@ -670,7 +687,7 @@ const [Modal, modalApi] = useVbenModal({
<div class="text-orange-400 text-5xl mb-4">🖥</div> <div class="text-orange-400 text-5xl mb-4">🖥</div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-slate-200 mb-2">未开通 PC 端管理权限</h3> <h3 class="text-lg font-semibold text-gray-800 dark:text-slate-200 mb-2">未开通 PC 端管理权限</h3>
<p class="text-gray-500 dark:text-slate-400 mb-6 text-sm">开通后医生可使用电脑端登录系统进行高级管理操作如查看完整报表等</p> <p class="text-gray-500 dark:text-slate-400 mb-6 text-sm">开通后医生可使用电脑端登录系统进行高级管理操作如查看完整报表等</p>
<Button type="primary" size="large" @click="openPcWindows">立即开通后台账号</Button> <Button v-if="canEdit" type="primary" size="large" @click="openPcWindows">立即开通后台账号</Button>
</div> </div>
</template> </template>
</div> </div>

View File

@@ -0,0 +1,95 @@
<script lang="ts" setup>
import { Avatar, Button } from 'ant-design-vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
defineProps<{
row: Record<string, any>;
onOpenCard: (row: Record<string, any>) => void;
}>();
</script>
<template>
<div class="doctor-basic-info">
<div class="doctor-basic-info__header">
<Avatar
:size="32"
:src="resolveAvatarUrl(row.avatar) || '/img/user-default-avatar.png'"
class="shrink-0"
/>
<Button
class="doctor-basic-info__name !h-auto !px-0 !py-0"
type="link"
@click="onOpenCard(row)"
>
{{ row.name || '-' }}
</Button>
</div>
<div class="doctor-basic-info__grid">
<div class="info-item">
<span class="info-item__label">手机</span>
<SensitiveText
:record="row"
field="mobile"
empty-text="-"
class="info-item__value"
/>
</div>
<div class="info-item">
<span class="info-item__label">wxID</span>
<span class="info-item__value">{{ row.su_id ?? '-' }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.doctor-basic-info {
line-height: 1.5;
}
.doctor-basic-info__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.doctor-basic-info__name {
font-weight: 500;
font-size: 13px;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.doctor-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__label {
flex-shrink: 0;
min-width: 36px;
color: rgb(156 163 175);
}
.info-item__value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,115 @@
<script lang="ts" setup>
import { Button } from 'ant-design-vue';
defineProps<{
row: Record<string, any>;
onEditDepart: (row: Record<string, any>) => void;
onEditTitle: (row: Record<string, any>) => void;
onEditSubjects: (row: Record<string, any>) => void;
onEditStores: (row: Record<string, any>) => void;
}>();
/**
* 格式化所属诊所列表为单行展示
*/
function formatStores(stores: any[] | undefined) {
if (!stores?.length) return '';
return stores
.map((item) => item?.store?.name)
.filter(Boolean)
.join('、');
}
</script>
<template>
<div class="doctor-professional-info">
<div class="doctor-professional-info__grid">
<div class="info-item">
<span class="info-item__label">科室</span>
<Button
class="info-item__action"
size="small"
type="link"
@click="onEditDepart(row)"
>
{{ row.depart?.name || '绑定' }}
</Button>
</div>
<div class="info-item">
<span class="info-item__label">职称</span>
<Button
class="info-item__action"
size="small"
type="link"
@click="onEditTitle(row)"
>
{{ row.title?.name || '绑定' }}
</Button>
</div>
<div class="info-item">
<span class="info-item__label">诊疗科目</span>
<Button
class="info-item__action"
size="small"
type="link"
@click="onEditSubjects(row)"
>
{{ row.subjects?.name || '绑定' }}
</Button>
</div>
<div class="info-item info-item--stores">
<span class="info-item__label">所属诊所</span>
<Button
class="info-item__action"
size="small"
type="link"
@click="onEditStores(row)"
>
{{ formatStores(row.stores) || '绑定' }}
</Button>
</div>
</div>
</div>
</template>
<style scoped>
.doctor-professional-info {
line-height: 1.5;
}
.doctor-professional-info__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 8px;
}
.info-item {
display: flex;
align-items: flex-start;
gap: 4px;
min-width: 0;
font-size: 12px;
color: rgb(107 114 128);
}
.info-item--stores {
grid-column: 1 / -1;
}
.info-item__label {
flex-shrink: 0;
min-width: 52px;
color: rgb(156 163 175);
}
.info-item__action {
height: auto;
padding: 0;
font-size: 12px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
</style>

View File

@@ -29,27 +29,24 @@ export const gridOptions: VxeGridProps<RowType> = {
}, },
columns: [ columns: [
{ type: 'checkbox', width: 60 }, { type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 }, { field: 'id', align: 'left', title: 'ID', width: 80 },
{ field: 'name', align: 'left', title: '名称', slots: { default: 'name' } },
{ field: 'mobile', align: 'left', title: '手机号', slots: { default: 'mobile' } },
{ {
field: 'avatar', field: 'doctor_basic_info',
align: 'left', align: 'left',
title: '头像', title: '基础信息',
slots: { default: 'avatar' }, width: 220,
width: 130, slots: { default: 'doctor_basic_info' },
}, },
{ field: 'su_id', title: 'wxID' },
{ field: 'stores', title: '所属诊所', slots: { default: 'stores' } },
{ {
field: 'subjects', field: 'doctor_professional_info',
title: '诊疗科目', align: 'left',
slots: { default: 'subjects' }, title: '执业信息',
minWidth: 120, width: 280,
slots: { default: 'doctor_professional_info' },
}, },
{ field: 'service_user', title: '审核状态', slots: { default: 'service-user' } }, { field: 'service_user', title: '审核状态', slots: { default: 'service-user' } },
{ field: 'created_at', title: '注册时间' }, { field: 'created_at', title: '注册时间', width: 160 },
{ type: 'html', title: '操作', width: 340, slots: { default: 'action' } }, { type: 'html', title: '操作', width: 260, slots: { default: 'action' } },
], ],
keepSource: true, keepSource: true,
pagerConfig: {}, pagerConfig: {},

View File

@@ -5,15 +5,18 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue'; import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action'; import { TableAction } from '#/components/table-action';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { auditApi, openPcWindowsApi, refuseApi } from './api'; import { auditApi, openPcWindowsApi, refuseApi } from './api';
import BindDepart from './components/BindDepartModal.vue';
import BindStore from './components/BindStoreModal.vue'; import BindStore from './components/BindStoreModal.vue';
import BindSubjects from './components/BindSubjectsModal.vue'; import BindSubjects from './components/BindSubjectsModal.vue';
import BindTitle from './components/BindTitleModal.vue';
import DoctorBasicInfoCell from './components/cells/DoctorBasicInfoCell.vue';
import DoctorProfessionalInfoCell from './components/cells/DoctorProfessionalInfoCell.vue';
import FormModalDemo from './components/modal.vue'; import FormModalDemo from './components/modal.vue';
import DoctorCardModal from './components/DoctorCardModal.vue'; import DoctorCardModal from './components/DoctorCardModal.vue';
import { formOptions } from './config/search'; import { formOptions } from './config/search';
@@ -52,13 +55,20 @@ const [BindSubjectsModal, BindSubjectsModalApi] = useVbenModal({
connectedComponent: BindSubjects, connectedComponent: BindSubjects,
}); });
const [BindDepartModal, BindDepartModalApi] = useVbenModal({
connectedComponent: BindDepart,
});
const [BindTitleModal, BindTitleModalApi] = useVbenModal({
connectedComponent: BindTitle,
});
const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({ const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({
connectedComponent: DoctorCardModal, connectedComponent: DoctorCardModal,
}); });
const showModal = (data = {}, isUpdate = false) => { const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({ formModalApi.setData({
// 表单值
values: data, values: data,
update: isUpdate, update: isUpdate,
gridApi, gridApi,
@@ -66,15 +76,16 @@ const showModal = (data = {}, isUpdate = false) => {
formModalApi.open(); formModalApi.open();
}; };
const showBindStoreModal = (data: number) => { /** 打开绑定诊所弹窗 */
const showBindStoreModal = (row: Record<string, any>) => {
BindStoreModalApi.setData({ BindStoreModalApi.setData({
// 表单值 values: row,
values: data,
gridApi, gridApi,
}); });
BindStoreModalApi.open(); BindStoreModalApi.open();
}; };
/** 打开绑定诊疗科目弹窗 */
const showBindSubjectsModal = (row: Record<string, any>) => { const showBindSubjectsModal = (row: Record<string, any>) => {
BindSubjectsModalApi.setData({ BindSubjectsModalApi.setData({
values: row, values: row,
@@ -83,6 +94,24 @@ const showBindSubjectsModal = (row: Record<string, any>) => {
BindSubjectsModalApi.open(); BindSubjectsModalApi.open();
}; };
/** 打开编辑科室弹窗 */
const showBindDepartModal = (row: Record<string, any>) => {
BindDepartModalApi.setData({
values: row,
gridApi,
});
BindDepartModalApi.open();
};
/** 打开编辑职称弹窗 */
const showBindTitleModal = (row: Record<string, any>) => {
BindTitleModalApi.setData({
values: row,
gridApi,
});
BindTitleModalApi.open();
};
const showDoctorCard = (row: Record<string, any>) => { const showDoctorCard = (row: Record<string, any>) => {
DoctorCardModalApi.setData({ DoctorCardModalApi.setData({
id: row.id, id: row.id,
@@ -119,6 +148,8 @@ const refuse = (id: number) => {
<FormModal /> <FormModal />
<BindStoreModal /> <BindStoreModal />
<BindSubjectsModal /> <BindSubjectsModal />
<BindDepartModal />
<BindTitleModal />
<DoctorCardModals /> <DoctorCardModals />
<Grid> <Grid>
<template #toolbar-buttons> <template #toolbar-buttons>
@@ -128,22 +159,10 @@ const refuse = (id: number) => {
label: '新增', label: '新增',
type: 'primary', type: 'primary',
icon: 'ant-design:plus-outlined', icon: 'ant-design:plus-outlined',
// auth: ['超级医生', 'sys:user:save'],
onClick: showModal.bind(null), onClick: showModal.bind(null),
}, },
]" ]"
:drop-down-actions="[ :drop-down-actions="[]"
// {
// label: '删除',
// icon: 'ant-design:delete-outlined',
// ifShow: hasTopTableDropDownActions,
// // auth: ['超级医生', 'sys:user:save'],
// popConfirm: {
// title: '确定删除吗',
// confirm: deleteApi.bind(null, false),
// },
// },
]"
> >
<template #more> <template #more>
<Button style="margin-left: 16px"> <Button style="margin-left: 16px">
@@ -153,24 +172,17 @@ const refuse = (id: number) => {
</template> </template>
</TableAction> </TableAction>
</template> </template>
<template #avatar="{ row }"> <template #doctor_basic_info="{ row }">
<Image :src="row.avatar" height="30" width="30" /> <DoctorBasicInfoCell :row="row" :on-open-card="showDoctorCard" />
</template> </template>
<template #name="{ row }"> <template #doctor_professional_info="{ row }">
<Button type="link" size="small" @click="showDoctorCard(row)"> <DoctorProfessionalInfoCell
{{ row.name }} :row="row"
</Button> :on-edit-depart="showBindDepartModal"
</template> :on-edit-title="showBindTitleModal"
<template #mobile="{ row }"> :on-edit-subjects="showBindSubjectsModal"
<SensitiveText :record="row" field="mobile" /> :on-edit-stores="showBindStoreModal"
</template> />
<template #stores="{ row }">
<p v-for="item in row.stores" :key="item.store_id">
{{ item?.store?.name }}
</p>
</template>
<template #subjects="{ row }">
{{ row.subjects?.name ?? '-' }}
</template> </template>
<template #service-user="{ row }"> <template #service-user="{ row }">
<Tag v-if="row.service_user?.status === 2" color="success">已通过</Tag> <Tag v-if="row.service_user?.status === 2" color="success">已通过</Tag>
@@ -203,30 +215,13 @@ const refuse = (id: number) => {
type: 'link', type: 'link',
icon: 'uil:edit', icon: 'uil:edit',
size: 'small', size: 'small',
// auth: ['doctor', 'sys:role:detail'], onClick: showModal.bind(null, row, true),
onClick: showModal.bind(null, row),
},
{
label: '绑定诊所',
type: 'link',
icon: 'uil:edit',
size: 'small',
// auth: ['doctor', 'sys:role:detail'],
onClick: showBindStoreModal.bind(null, row),
},
{
label: '绑定诊疗科目',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showBindSubjectsModal.bind(null, row),
}, },
{ {
label: '开通PC端', label: '开通PC端',
type: 'link', type: 'link',
icon: 'uil:edit', icon: 'uil:edit',
size: 'small', size: 'small',
// auth: ['doctor', 'sys:role:detail'],
onClick: openPcWindows.bind(null, row.id), onClick: openPcWindows.bind(null, row.id),
}, },
]" ]"
@@ -237,7 +232,6 @@ const refuse = (id: number) => {
icon: 'uil:edit', icon: 'uil:edit',
size: 'small', size: 'small',
ifShow: row.service_user?.status === 1, ifShow: row.service_user?.status === 1,
// auth: ['doctor', 'sys:role:detail'],
onClick: audit.bind(null, row.id), onClick: audit.bind(null, row.id),
}, },
{ {
@@ -246,7 +240,6 @@ const refuse = (id: number) => {
icon: 'uil:edit', icon: 'uil:edit',
size: 'small', size: 'small',
ifShow: row.service_user?.status === 1, ifShow: row.service_user?.status === 1,
// auth: ['doctor', 'sys:role:detail'],
onClick: refuse.bind(null, row.id), onClick: refuse.bind(null, row.id),
}, },
]" ]"