1. 诊所管理

This commit is contained in:
李琦
2026-06-18 14:09:38 +08:00
parent ef5067d015
commit d26923fd73
7 changed files with 164 additions and 55 deletions

View File

@@ -14,7 +14,7 @@
<view class="quick-wrap">
<text
v-for="opt in discountQuickOptions"
:key="'d-' + opt.value"
:key="opt.value"
class="quick"
@click="discountInput = String(opt.value)"
>{{ opt.name }}</text>
@@ -26,7 +26,7 @@
<view class="quick-wrap">
<text
v-for="opt in markupQuickOptions"
:key="'m-' + opt.value"
:key="opt.value"
class="quick markup"
@click="discountInput = String(opt.value)"
>{{ opt.name }}</text>

View File

@@ -45,7 +45,8 @@
<view class="action-btn" @click="goConsultation">在线复诊配置</view>
<view class="action-btn" @click="goPromoters">管理推广员</view>
<view class="action-btn" @click="openQr">诊所二维码</view>
<view class="action-btn warn" @click="openPc">开通后台</view>
<view class="action-btn" @click="goBankCardReport">{{ bankCardReportButtonText }}</view>
<view v-if="!hasAdminAccount" class="action-btn warn" @click="openPc">开通后台</view>
</view>
</view>
@@ -89,6 +90,17 @@ export default {
clinicTypeIndex() {
return Number(this.store.clinic_type) || 0;
},
hasAdminAccount() {
const admin = this.store.new_admin || this.store.newAdmin;
return !!(admin && (admin.id || admin.code));
},
bankCardReportButtonText() {
const text = this.store.bank_card_report_status_text;
if (Number(this.store.bank_card_report_status) === 0 || !text || text === '未报备') {
return '银行卡报备';
}
return `银行卡报备(${text}`;
},
},
onLoad(options) {
this.storeId = Number(options.id || 0);
@@ -148,6 +160,12 @@ export default {
goPromoters() {
uni.navigateTo({ url: `${MANAGE_ROOT}/index?mode=platform&store_id=${this.storeId}` });
},
goBankCardReport() {
if (!this.storeId) return;
uni.navigateTo({
url: `/subPackages/sub_salesperson/store-bank-card/report?store_id=${this.storeId}`,
});
},
async openQr() {
uni.showLoading({ title: '生成中' });
try {
@@ -170,6 +188,7 @@ export default {
const wrap = await openStorePcWindows(this.storeId);
if (wrap && wrap.ok) {
uni.showToast({ title: '开通成功', icon: 'success' });
await this.loadDetail();
}
},
});

View File

@@ -30,6 +30,8 @@
<view class="tag-row">
<text class="mini-tag" :class="{ on: Number(item.salesperson_see_price) === 1 }">推广员可见价格</text>
<text class="mini-tag" :class="{ on: Number(item.is_shipping_free) === 1 }">包邮</text>
<text v-if="Number(item.bank_card_report_status) === 0" class="mini-tag warn">需报备银行卡</text>
<text v-else-if="item.bank_card_report_status_text" class="mini-tag bank">{{ item.bank_card_report_status_text }}</text>
</view>
</view>
</view>
@@ -111,4 +113,6 @@ export default {
.tag-row { display: flex; gap: 12rpx; flex-wrap: wrap; }
.mini-tag { font-size: 22rpx; color: #999; background: #f5f5f5; padding: 4rpx 12rpx; border-radius: 8rpx; }
.mini-tag.on { color: #22C55E; background: #F0FDF4; }
.mini-tag.warn { color: #D97706; background: #FEF3C7; }
.mini-tag.bank { color: #3B82F6; background: #EFF6FF; }
</style>

View File

@@ -34,6 +34,7 @@ export function applyDoctorDraft(vm, draft) {
vm.selectedStoreInputIds = Array.isArray(ui.selectedStoreInputIds)
? [...ui.selectedStoreInputIds]
: (Array.isArray(ui.selectedStoreIds) ? [...ui.selectedStoreIds] : [])
vm.selectedStoreIds = Array.isArray(ui.selectedStoreIds) ? [...ui.selectedStoreIds] : []
vm.departIndex = ui.departIndex != null ? ui.departIndex : Math.max(0, vm.departOptions.findIndex((d) => d.id === vm.form.depart_id))
vm.titleIndex = ui.titleIndex != null ? ui.titleIndex : Math.max(0, vm.titleOptions.findIndex((t) => t.id === vm.form.title_id))
vm.typeIndex = ui.typeIndex != null ? ui.typeIndex : (Number(vm.form.type) === 2 ? 1 : 0)
@@ -45,7 +46,8 @@ export function serializeDoctorDraft(vm) {
return {
form: JSON.parse(JSON.stringify(vm.form)),
ui: {
selectedStoreInputIds: [...(vm.selectedStoreInputIds || vm.selectedStoreIds || [])],
selectedStoreInputIds: [...(vm.selectedStoreInputIds || [])],
selectedStoreIds: [...(vm.selectedStoreIds || [])],
departIndex: vm.departIndex,
titleIndex: vm.titleIndex,
typeIndex: vm.typeIndex,

View File

@@ -1,35 +1,63 @@
<template>
<view class="picker-wrap">
<view class="store-input-picker" @click="openDrawer">
<text :class="summary ? 'summary-text' : 'placeholder-text'">{{ summary || placeholder }}</text>
<u-icon v-if="!disabled" name="arrow-right" color="#c0c4cc" size="28"></u-icon>
</view>
<drawer-page-container v-model="show" title="选择预填门店">
<view class="drawer-body">
<scroll-view scroll-y class="card-scroll">
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!options.length" class="empty">暂无预填门店</view>
<view
v-for="item in options"
:key="item.id"
class="store-card"
:class="{ active: innerValue.includes(item.id) }"
@click="toggle(item.id)"
>
<view class="card-head">
<text class="card-name">{{ item.name || '-' }}</text>
<text class="card-status" :class="'s' + item.status">{{ item.status_text }}</text>
</view>
<text class="card-mobile">{{ item.mobile || '-' }}</text>
<text v-if="innerValue.includes(item.id)" class="card-check">已选</text>
</view>
</scroll-view>
<view class="drawer-footer">
<u-button type="primary" :custom-style="primaryStyle" @click="confirm">确定{{ innerValue.length }}</u-button>
</view>
<text :class="summary ? 'summary-text' : 'placeholder-text'">{{ summary || placeholder }}</text>
<u-icon v-if="!disabled" name="arrow-right" color="#c0c4cc" size="28"></u-icon>
</view>
</drawer-page-container>
<drawer-page-container v-model="show" title="选择关联门店">
<view class="drawer-body">
<view class="tab-wrap">
<u-subsection
:list="tabLabels"
:current="activeTab"
active-color="#6acdbb"
@change="onTabChange"
/>
</view>
<scroll-view scroll-y class="card-scroll">
<template v-if="activeTab === 0">
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!options.length" class="empty">暂无预填诊所</view>
<view
v-for="item in options"
:key="item.id"
class="store-card"
:class="{ active: innerInputIds.includes(item.id) }"
@click="toggleInput(item.id)"
>
<view class="card-head">
<text class="card-name">{{ item.name || '-' }}</text>
<text class="card-status" :class="'s' + item.status">{{ item.status_text }}</text>
</view>
<text class="card-mobile">{{ item.mobile || '-' }}</text>
<text v-if="innerInputIds.includes(item.id)" class="card-check">已选</text>
</view>
</template>
<template v-else>
<view v-if="formalLoading" class="empty">加载中...</view>
<view v-else-if="!formalOptions.length" class="empty">暂无正式诊所</view>
<view
v-for="item in formalOptions"
:key="item.id"
class="store-card"
:class="{ active: innerStoreIds.includes(item.id) }"
@click="toggleStore(item.id)"
>
<view class="card-head">
<text class="card-name">{{ item.name || '-' }}</text>
</view>
<text v-if="innerStoreIds.includes(item.id)" class="card-check">已选</text>
</view>
</template>
</scroll-view>
<view class="drawer-footer">
<u-button type="primary" :custom-style="primaryStyle" @click="confirm">
确定{{ totalSelected }}
</u-button>
</view>
</view>
</drawer-page-container>
</view>
</template>
@@ -41,16 +69,22 @@ export default {
components: { DrawerPageContainer },
props: {
value: { type: Array, default: () => [] },
storeIds: { type: Array, default: () => [] },
options: { type: Array, default: () => [] },
formalOptions: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
formalLoading: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
placeholder: { type: String, default: '请选择预填门店' },
placeholder: { type: String, default: '请选择关联门店' },
defaultStoreInputId: { type: [Number, String], default: 0 },
},
data() {
return {
show: false,
innerValue: [],
activeTab: 0,
innerInputIds: [],
innerStoreIds: [],
tabLabels: ['预填诊所', '正式诊所'],
primaryStyle: {
backgroundColor: '#6acdbb',
color: '#ffffff',
@@ -59,13 +93,21 @@ export default {
}
},
computed: {
totalSelected() {
return this.innerInputIds.length + this.innerStoreIds.length
},
summary() {
if (!this.value.length) return ''
const names = this.options
const inputNames = this.options
.filter((o) => this.value.includes(o.id))
.map((o) => o.name)
const storeNames = this.formalOptions
.filter((o) => this.storeIds.includes(o.id))
.map((o) => o.name)
const names = [...inputNames, ...storeNames]
if (names.length) return names.join('、')
return `已选 ${this.value.length} 个门店`
const total = this.value.length + this.storeIds.length
if (total > 0) return `已选 ${total} 个门店`
return ''
},
},
watch: {
@@ -82,24 +124,35 @@ export default {
methods: {
openDrawer() {
if (this.disabled) return
this.innerValue = [...this.value]
this.innerInputIds = [...this.value]
this.innerStoreIds = [...this.storeIds]
const preset = Number(this.defaultStoreInputId || 0)
if (preset > 0 && !this.innerValue.includes(preset)) {
this.innerValue.push(preset)
if (preset > 0 && !this.innerInputIds.includes(preset)) {
this.innerInputIds.push(preset)
}
this.activeTab = 0
this.show = true
},
toggle(id) {
const idx = this.innerValue.indexOf(id)
if (idx === -1) this.innerValue.push(id)
else this.innerValue.splice(idx, 1)
onTabChange(index) {
this.activeTab = index
},
toggleInput(id) {
const idx = this.innerInputIds.indexOf(id)
if (idx === -1) this.innerInputIds.push(id)
else this.innerInputIds.splice(idx, 1)
},
toggleStore(id) {
const idx = this.innerStoreIds.indexOf(id)
if (idx === -1) this.innerStoreIds.push(id)
else this.innerStoreIds.splice(idx, 1)
},
confirm() {
if (!this.innerValue.length) {
uni.showToast({ title: '请选择预填门店', icon: 'none' })
if (!this.innerInputIds.length && !this.innerStoreIds.length) {
uni.showToast({ title: '请选择关联门店', icon: 'none' })
return
}
this.$emit('input', [...this.innerValue])
this.$emit('input', [...this.innerInputIds])
this.$emit('update:storeIds', [...this.innerStoreIds])
this.show = false
},
},
@@ -118,7 +171,8 @@ export default {
.summary-text { font-size: 28rpx; color: #303133; flex: 1; }
.placeholder-text { font-size: 28rpx; color: #c0c4cc; flex: 1; }
.drawer-body { display: flex; flex-direction: column; max-height: 70vh; }
.card-scroll { flex: 1; max-height: 58vh; padding: 16rpx 24rpx; box-sizing: border-box; }
.tab-wrap { padding: 16rpx 24rpx 0; flex-shrink: 0; }
.card-scroll { flex: 1; max-height: 52vh; padding: 16rpx 24rpx; box-sizing: border-box; }
.store-card {
background: #f7f8fa;
border-radius: 16rpx;

View File

@@ -8,11 +8,14 @@
<u-form :model="form" ref="uForm" label-width="220" label-align="left">
<view v-show="currentStep === 0" class="step-content">
<view class="section-title">基础信息</view>
<u-form-item label="预填门店" :required="true">
<u-form-item label="关联门店" :required="true">
<store-input-picker
:value="selectedStoreInputIds"
:store-ids.sync="selectedStoreIds"
:options="storeInputOptions"
:formal-options="formalStoreOptions"
:loading="storeInputLoading"
:formal-loading="formalStoreLoading"
:default-store-input-id="presetStoreInputId"
@input="selectedStoreInputIds = $event"
/>
@@ -195,7 +198,10 @@ export default {
showSignPad: false,
storeInputOptions: [],
storeInputLoading: false,
formalStoreOptions: [],
formalStoreLoading: false,
selectedStoreInputIds: [],
selectedStoreIds: [],
presetStoreInputId: 0,
departOptions: [],
titleOptions: [],
@@ -245,10 +251,13 @@ export default {
return this.titleOptions.map((t) => ({ label: t.name, value: t.id }))
},
selectedStoreNames() {
return this.storeInputOptions
const inputNames = this.storeInputOptions
.filter((s) => this.selectedStoreInputIds.includes(s.id))
.map((s) => s.name)
.join('、')
const storeNames = this.formalStoreOptions
.filter((s) => this.selectedStoreIds.includes(s.id))
.map((s) => s.name)
return [...inputNames, ...storeNames].join('、')
},
},
onLoad(q) {
@@ -257,11 +266,16 @@ export default {
if (draftId) this.currentDraftId = draftId
this.presetStoreInputId = !this.id ? Number(q.storeInputId || 0) : 0
this.storeInputLoading = true
this.formalStoreLoading = true
Promise.all([
this.bizApi.getStoreInputOption().then((w) => {
this.storeInputLoading = false
this.storeInputOptions = w.data || []
}).catch(() => { this.storeInputLoading = false }),
this.bizApi.getStoreOption().then((w) => {
this.formalStoreLoading = false
this.formalStoreOptions = w.data || []
}).catch(() => { this.formalStoreLoading = false }),
this.bizApi.getDepartOption().then((w) => { this.departOptions = w.data || [] }),
this.bizApi.getDoctorTitleOption().then((w) => { this.titleOptions = w.data || [] }),
]).then(() => {
@@ -398,7 +412,20 @@ export default {
Object.assign(this.form, createDefaultDoctorInputForm(), w.data)
this.selectedStoreInputIds = w.data.store_input_ids?.length
? [...w.data.store_input_ids]
: (w.data.store_ids || (w.data.store_id ? [w.data.store_id] : []))
: []
if (w.data.stores?.length) {
this.selectedStoreIds = w.data.stores
.filter((s) => !Number(s.store_input_id))
.map((s) => Number(s.store_id))
.filter((id) => id > 0)
} else {
const allStoreIds = (w.data.store_ids || []).map(Number).filter((id) => id > 0)
this.selectedStoreIds = allStoreIds.filter((id) => {
return !this.storeInputOptions.some((opt) => {
return this.selectedStoreInputIds.includes(opt.id) && Number(opt.synced_store_id) === id
})
})
}
this.syncPickerFromForm()
})
},
@@ -411,7 +438,9 @@ export default {
validateCurrentStep() {
const f = this.form
if (this.currentStep === 0) {
if (!this.selectedStoreInputIds.length) return this.showError('请选择预填门店')
if (!this.selectedStoreInputIds.length && !this.selectedStoreIds.length) {
return this.showError('请选择关联门店')
}
if (!String(f.name || '').trim()) return this.showError('请输入医生姓名')
if (!String(f.mobile || '').trim()) return this.showError('请输入手机号')
if (!String(f.idcard || '').trim()) return this.showError('请输入身份证')
@@ -445,8 +474,8 @@ export default {
const payload = {
...this.form,
store_input_ids: this.selectedStoreInputIds,
store_ids: this.selectedStoreInputIds,
store_id: this.selectedStoreInputIds[0] || 0,
store_ids: this.selectedStoreIds,
store_id: this.selectedStoreInputIds[0] || this.selectedStoreIds[0] || 0,
}
const req = this.id
? this.bizApi.updateDoctorInput({ ...payload, id: this.id })

View File

@@ -146,6 +146,7 @@ export default {
return !this.canAudit && Number(this.info.status) === 0 && !!this.editPath
},
canAudit() {
if (Number(this.info.status) !== 0) return false
if (this.inputType === 'doctor') return !!this.bizCap.canDoctorInputAudit
return !!this.bizCap.canStoreInputAudit
},