1. 特色方功能迭代

This commit is contained in:
李琦
2026-07-07 08:50:05 +08:00
parent 6acddc17d5
commit 6da8ae063a
17 changed files with 949 additions and 95 deletions

View File

@@ -5,6 +5,7 @@
<view v-for="item in list" :key="item.id" class="row">
<view>
<text class="order">{{ item.order_no }} <text class="status" style="margin-left: 20rpx">{{ item.settlement_id > 0 ? '已结算' : '待结算' }}</text></text>
<text v-if="item.patient_name || item.patient_mobile" class="patient">{{ item.patient_name }} {{ item.patient_mobile }}</text>
<text class="drug">{{ item.drug_name }} ×{{ item.qty }}</text>
</view>
<view class="right">
@@ -42,6 +43,7 @@ export default {
.empty { text-align: center; color: #999; padding: 80rpx 0; }
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; display: flex; justify-content: space-between; }
.order { display: block; font-size: 26rpx; font-weight: 600; }
.patient { display: block; font-size: 24rpx; color: #666; margin-top: 6rpx; }
.drug { font-size: 24rpx; color: #666; }
.right { text-align: right; }
.amount { display: block; font-size: 28rpx; color: #4db6a8; font-weight: 600; }

View File

@@ -0,0 +1,142 @@
<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="visible"
:show="visible"
:overlay="false"
@leave="close"
/>
<!-- #endif -->
<u-popup
v-model="visible"
mode="bottom"
border-radius="24"
height="80%"
z-index="10080"
@close="close"
>
<view class="drawer">
<view class="header">
<text class="title">传方详情</text>
<u-icon name="close" size="36" @click="close"></u-icon>
</view>
<scroll-view scroll-y class="body">
<view v-if="loading" class="tip">加载中...</view>
<template v-else-if="detail">
<view class="section">
<view class="row"><text class="label">状态</text><u-tag :text="detail.status_text" size="mini" :type="statusType" /></view>
<view class="row"><text class="label">方式</text><text>{{ detail.transfer_mode_text }}</text></view>
<view class="row"><text class="label">收货人</text><text>{{ detail.patient_name }} {{ detail.patient_mobile }}</text></view>
<view class="row"><text class="label">地址</text><text>{{ detail.patient_address || '—' }}</text></view>
<view v-if="detail.remark" class="row"><text class="label">备注</text><text>{{ detail.remark }}</text></view>
<view v-if="detail.status === 3 && detail.exception_message" class="row exception">
<text class="label">异常</text><text>{{ detail.exception_message }}</text>
</view>
</view>
<view v-if="detail.drug_text_lines && detail.drug_text_lines.length" class="section">
<text class="section-title">药方</text>
<text v-for="(line, i) in detail.drug_text_lines" :key="i" class="drug-line">{{ line }}</text>
</view>
<view v-if="detail.prescription_images && detail.prescription_images.length" class="section">
<text class="section-title">处方图片</text>
<view class="img-row">
<image
v-for="(img, idx) in detail.prescription_images"
:key="idx"
:src="img"
mode="aspectFill"
class="thumb"
@click="preview(img)"
/>
</view>
</view>
<view class="section">
<text class="time">{{ detail.transfer_time_text }}</text>
</view>
</template>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
import { getClinicSalespersonTransferPrescriptionDetail } from '@/api/clinicSalesperson.js'
/**
* 传方详情抽屉
*/
export default {
name: 'TransferDetailDrawer',
data() {
return {
visible: false,
loading: false,
detail: null,
}
},
computed: {
statusType() {
const map = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'error' }
return map[Number(this.detail?.status)] || 'info'
},
},
methods: {
async open(id) {
this.visible = true
this.loading = true
this.detail = null
try {
const wrap = await getClinicSalespersonTransferPrescriptionDetail(id)
if (wrap && wrap.ok) {
this.detail = wrap.data
}
} finally {
this.loading = false
}
},
close() {
this.visible = false
},
preview(url) {
const urls = this.detail?.prescription_images || []
uni.previewImage({ urls, current: url })
},
},
}
</script>
<style lang="scss" scoped>
.drawer { height: 100%; display: flex; flex-direction: column; background: #f5f5f5; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
}
.title { font-size: 32rpx; font-weight: 600; }
.body { flex: 1; height: 0; padding: 24rpx; }
.tip { text-align: center; color: #999; padding: 40rpx; }
.section {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 16rpx;
}
.section-title { font-size: 28rpx; font-weight: 600; display: block; margin-bottom: 12rpx; }
.row {
display: flex;
font-size: 26rpx;
margin-bottom: 12rpx;
line-height: 1.5;
}
.label { color: #86909c; width: 120rpx; flex-shrink: 0; }
.exception .label, .exception text:last-child { color: #ef4444; }
.drug-line { display: block; font-size: 26rpx; color: #008771; margin-bottom: 8rpx; }
.img-row { display: flex; flex-wrap: wrap; gap: 16rpx; }
.thumb { width: 160rpx; height: 160rpx; border-radius: 8rpx; }
.time { font-size: 24rpx; color: #999; }
</style>

View File

@@ -0,0 +1,129 @@
<template>
<u-popup
v-model="visible"
mode="center"
border-radius="24"
width="640rpx"
:mask-close-able="true"
@close="handleClose"
>
<view class="mode-modal">
<view class="mode-header">
<text class="mode-title">选择传方方式</text>
<text class="mode-sub">请选择适合您的传方方式</text>
</view>
<view class="mode-cards">
<view class="mode-card photo" hover-class="mode-card-hover" @click="handleSelect('photo')">
<view class="mode-icon-wrap photo-bg">
<u-icon name="camera-fill" size="48" color="#10B981"></u-icon>
</view>
<text class="mode-name">拍照一键传方</text>
<text class="mode-desc">拍摄处方照片快速提交</text>
</view>
<view class="mode-card manual" hover-class="mode-card-hover" @click="handleSelect('manual')">
<view class="mode-icon-wrap manual-bg">
<u-icon name="edit-pen-fill" size="48" color="#0EA5E9"></u-icon>
</view>
<text class="mode-name">输入药方</text>
<text class="mode-desc">手动选择中药并填写信息</text>
</view>
</view>
<view class="mode-cancel" @click="handleClose">取消</view>
</view>
</u-popup>
</template>
<script>
export default {
name: 'TransferModeModal',
props: {
value: { type: Boolean, default: false },
},
computed: {
visible: {
get() { return this.value },
set(v) { this.$emit('input', v) },
},
},
methods: {
handleClose() {
this.visible = false
this.$emit('close')
},
/** 选择传方方式photo=拍照 manual=手动输入药方 */
handleSelect(mode) {
this.$emit('select', mode)
this.visible = false
},
},
}
</script>
<style lang="scss" scoped>
.mode-modal {
padding: 48rpx 40rpx 32rpx;
background: #fff;
}
.mode-header {
text-align: center;
margin-bottom: 40rpx;
}
.mode-title {
display: block;
font-size: 36rpx;
font-weight: 700;
color: #1d2129;
}
.mode-sub {
display: block;
font-size: 26rpx;
color: #86909c;
margin-top: 12rpx;
}
.mode-cards {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.mode-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 36rpx 32rpx;
border-radius: 20rpx;
border: 2rpx solid #f0f0f0;
background: #fafafa;
}
.mode-card-hover {
opacity: 0.85;
transform: scale(0.98);
}
.mode-icon-wrap {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.photo-bg { background: linear-gradient(135deg, #ecfdf5, #d1fae5); }
.manual-bg { background: linear-gradient(135deg, #f0f9ff, #e0f2fe); }
.mode-name {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
}
.mode-desc {
font-size: 24rpx;
color: #86909c;
margin-top: 8rpx;
}
.mode-cancel {
text-align: center;
padding: 28rpx;
margin-top: 16rpx;
font-size: 28rpx;
color: #86909c;
}
</style>

View File

@@ -88,7 +88,7 @@
:key="item.key"
class="service-card"
hover-class="service-card-hover"
@click="go(item.path)"
@click="onMenuClick(item)"
>
<view class="service-icon-box" :style="{ background: item.theme.bg }">
<!-- 替换为 uView 动态图标 -->
@@ -103,18 +103,28 @@
</view>
</view>
<transfer-mode-modal v-model="showTransferModeModal" @select="onTransferModeSelect" />
<transfer-patient-drawer
ref="photoTransferDrawer"
v-model="showPhotoTransferDrawer"
:photo-mode="true"
:submitting="photoSubmitting"
@confirm="handlePhotoTransferConfirm"
/>
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
import TransferModeModal from '@/subPackages/sub_clinic_salesperson/components/TransferModeModal.vue'
import { getClinicSalespersonMyInfo, createClinicSalespersonTransferPrescription } from '@/api/clinicSalesperson.js'
import { openQrCodePreview, buildSalespersonQrPayload } from '@/utils/qrCodePreview.js'
const ROOT = '/subPackages/sub_clinic_salesperson'
export default {
components: { ClinicSalespersonPageLayout },
// transfer-patient-drawer 跨分包引用,在 pages.json 通过 usingComponents + componentPlaceholder 注册
components: { ClinicSalespersonPageLayout, TransferModeModal },
data() {
return {
displayName: '',
@@ -122,11 +132,15 @@ export default {
qrCode: '',
profileInfo: null,
stats: {},
showTransferModeModal: false,
showPhotoTransferDrawer: false,
photoSubmitting: false,
menuItems: [
// { key: 'transfer', label: '传方', path: '/subPackages/sub_workbench/prescription_v2/index?salesperson_transfer=1&initial_category=1' },
// { key: 'transfer-list', label: '传方记录', path: `${ROOT}/transfer-prescription/list` },
{ key: 'transfer', label: '传方', action: 'transfer' },
{ key: 'transfer-list', label: '传方记录', path: `${ROOT}/transfer-prescription/list` },
// { key: 'earnings', label: '我的收益', path: `${ROOT}/earnings/index` },
{ key: 'leads', label: '获客记录', path: `${ROOT}/leads/index` },
{ key: 'invitees', label: '我邀请的推广员', path: `${ROOT}/invitees/index` },
{ key: 'commission', label: '分成记录', path: `${ROOT}/commission/index` },
{ key: 'settlement', label: '结算记录', path: `${ROOT}/settlement/index` },
],
@@ -185,6 +199,55 @@ export default {
go(path) {
uni.navigateTo({ url: path })
},
/** 菜单点击:传方走方式选择,其余跳转 */
onMenuClick(item) {
if (item.action === 'transfer') {
this.showTransferModeModal = true
return
}
if (item.path) {
this.go(item.path)
}
},
/** 选择传方方式 */
onTransferModeSelect(mode) {
if (mode === 'photo') {
this.showPhotoTransferDrawer = true
return
}
uni.navigateTo({
url: '/subPackages/sub_workbench/prescription_v2/index?salesperson_transfer=1&initial_category=1',
})
},
/** 拍照传方:抽屉确认后直接提交 */
async handlePhotoTransferConfirm(patientData) {
this.photoSubmitting = true
try {
const wrap = await createClinicSalespersonTransferPrescription({
...patientData,
transfer_mode: 2,
drugs: [],
clinical_diagnose: '',
doctor_order: '',
})
if (wrap && wrap.ok) {
this.showPhotoTransferDrawer = false
if (this.$refs.photoTransferDrawer && this.$refs.photoTransferDrawer.resetForm) {
this.$refs.photoTransferDrawer.resetForm()
}
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.navigateTo({ url: '/subPackages/sub_clinic_salesperson/transfer-prescription/list' })
}, 500)
} else {
uni.showToast({ title: wrap?.res?.msg || wrap?.res?.message || '提交失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
} finally {
this.photoSubmitting = false
}
},
},
}
</script>

View File

@@ -0,0 +1,113 @@
<template>
<clinic-salesperson-page-layout :title="pageTitle" :show-back="true">
<view class="page">
<view class="summary">
<view class="sum-row">
<text class="label">邀请分成比例</text>
<text class="val">{{ summary.inviter_split }}%</text>
</view>
<view class="sum-row">
<text class="label">累计带来收益</text>
<text class="val amount">¥{{ summary.invite_total_amount }}</text>
</view>
<view class="sum-row">
<text class="label">待结算 / 已结算</text>
<text class="val">¥{{ summary.invite_pending_amount }} / ¥{{ summary.invite_settled_amount }}</text>
</view>
</view>
<view v-if="!list.length && !loading" class="empty">暂无收益明细</view>
<view v-for="item in list" :key="item.id" class="row">
<view class="top">
<text class="order">{{ item.order_no }}</text>
<text class="status">{{ item.settlement_id > 0 ? '已结算' : '待结算' }}</text>
</view>
<text class="patient">{{ item.patient_name }} {{ item.patient_mobile }}</text>
<text class="drug">{{ item.drug_name }} ×{{ item.qty }}</text>
<view class="bottom">
<text class="amount">¥{{ item.commission_amount }}</text>
<text class="time">{{ item.created_at }}</text>
</view>
</view>
</view>
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonCommissionList } from '@/api/clinicSalesperson.js'
export default {
components: { ClinicSalespersonPageLayout },
data() {
return {
sourceSalespersonId: 0,
nickName: '',
summary: {
inviter_split: '0.00',
invite_total_amount: '0.00',
invite_pending_amount: '0.00',
invite_settled_amount: '0.00',
},
list: [],
loading: false,
}
},
computed: {
pageTitle() {
return this.nickName ? `${this.nickName} 的收益明细` : '收益明细'
},
},
onLoad(options) {
this.sourceSalespersonId = Number(options.source_salesperson_id || 0)
this.nickName = decodeURIComponent(options.nick_name || '')
this.summary = {
inviter_split: options.inviter_split || '0.00',
invite_total_amount: options.invite_total_amount || '0.00',
invite_pending_amount: options.invite_pending_amount || '0.00',
invite_settled_amount: options.invite_settled_amount || '0.00',
}
this.loadList()
},
methods: {
async loadList() {
if (this.sourceSalespersonId <= 0) return
this.loading = true
try {
const wrap = await getClinicSalespersonCommissionList({
page: 1,
pageSize: 100,
commission_source: 1,
source_salesperson_id: this.sourceSalespersonId,
})
if (wrap && wrap.ok) this.list = wrap.data?.items || []
} finally {
this.loading = false
}
},
},
}
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.summary {
background: linear-gradient(135deg, #f0fdf4, #ecfdf5); border-radius: 12rpx;
padding: 24rpx; margin-bottom: 20rpx;
}
.sum-row { display: flex; justify-content: space-between; margin-bottom: 12rpx; font-size: 26rpx; }
.sum-row:last-child { margin-bottom: 0; }
.label { color: #666; }
.val { color: #333; font-weight: 500; }
.val.amount { color: #4db6a8; font-weight: 600; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; }
.top { display: flex; justify-content: space-between; align-items: center; }
.order { font-size: 26rpx; font-weight: 600; }
.status { font-size: 22rpx; color: #999; }
.patient { display: block; font-size: 24rpx; color: #666; margin-top: 8rpx; }
.drug { display: block; font-size: 24rpx; color: #666; margin-top: 4rpx; }
.bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.amount { font-size: 28rpx; color: #4db6a8; font-weight: 600; }
.time { font-size: 22rpx; color: #999; }
</style>

View File

@@ -0,0 +1,85 @@
<template>
<clinic-salesperson-page-layout title="我邀请的推广员" :show-back="true">
<view class="page">
<view v-if="!list.length && !loading" class="empty">暂无下级推广员</view>
<view
v-for="item in list"
:key="item.id"
class="row"
hover-class="row-hover"
@click="goDetail(item)"
>
<image v-if="item.avatar" :src="item.avatar" class="avatar" mode="aspectFill" />
<view v-else class="avatar placeholder">{{ (item.nick_name || '?').charAt(0) }}</view>
<view class="main">
<text class="name">{{ item.nick_name || '推广员' }}</text>
<text class="phone">{{ item.phone }}</text>
<text class="sub">邀请分成 {{ item.inviter_split }}%</text>
</view>
<view class="right">
<text class="amount">¥{{ item.invite_total_amount || '0.00' }}</text>
<text class="hint">待结算 ¥{{ item.invite_pending_amount || '0.00' }}</text>
</view>
</view>
</view>
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonInvitedList } from '@/api/clinicSalesperson.js'
const ROOT = '/subPackages/sub_clinic_salesperson'
export default {
components: { ClinicSalespersonPageLayout },
data() {
return { list: [], loading: false, page: 1 }
},
onLoad() {
this.loadList()
},
onPullDownRefresh() {
this.page = 1
this.loadList(true)
},
methods: {
async loadList(stopRefresh = false) {
this.loading = true
try {
const wrap = await getClinicSalespersonInvitedList({ page: this.page, pageSize: 50 })
if (wrap && wrap.ok) this.list = wrap.data?.items || []
} finally {
this.loading = false
if (stopRefresh) uni.stopPullDownRefresh()
}
},
goDetail(item) {
uni.navigateTo({
url: `${ROOT}/invitees/detail?source_salesperson_id=${item.id}&nick_name=${encodeURIComponent(item.nick_name || '')}&inviter_split=${item.inviter_split || 0}&invite_total_amount=${item.invite_total_amount || 0}&invite_pending_amount=${item.invite_pending_amount || 0}&invite_settled_amount=${item.invite_settled_amount || 0}`,
})
},
},
}
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
.row {
background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx;
display: flex; align-items: center;
}
.row-hover { opacity: 0.92; }
.avatar { width: 88rpx; height: 88rpx; border-radius: 50%; margin-right: 20rpx; flex-shrink: 0; }
.avatar.placeholder {
background: #e8f8f5; color: #6acdbb; display: flex; align-items: center; justify-content: center;
font-size: 36rpx; font-weight: 600;
}
.main { flex: 1; min-width: 0; }
.name { display: block; font-size: 28rpx; font-weight: 600; color: #333; }
.phone, .sub { display: block; font-size: 24rpx; color: #666; margin-top: 4rpx; }
.right { text-align: right; flex-shrink: 0; }
.amount { display: block; font-size: 30rpx; color: #4db6a8; font-weight: 600; }
.hint { font-size: 22rpx; color: #999; }
</style>

View File

@@ -1,44 +1,69 @@
<template>
<clinic-salesperson-page-layout title="传方记录" :show-back="true">
<view class="page">
<view v-if="!list.length" class="empty">暂无传方记录</view>
<view v-for="item in list" :key="item.id" class="row" @click="goDetail(item.id)">
<view v-if="!list.length && !loading" class="empty">暂无传方记录</view>
<view v-for="item in list" :key="item.id" class="row" @click="openDetail(item.id)">
<view class="top">
<text class="name">{{ item.patient_name }} {{ item.patient_mobile }}</text>
<u-tag :text="item.is_imported ? '已导入' : '待导入'" :type="item.is_imported ? 'success' : 'warning'" size="mini" />
<u-tag
:text="item.status_text || (item.is_imported ? '已完成' : '未处理')"
:type="statusTagType(item.status)"
size="mini"
/>
</view>
<text class="diag">诊断{{ item.clinical_diagnose || '-' }}</text>
<text class="time">{{ item.transfer_time_text }}</text>
<text class="meta">{{ item.transfer_mode_text || '' }} · {{ item.transfer_time_text }}</text>
<text v-if="item.drug_text_lines && item.drug_text_lines.length" class="drugs">
药方{{ item.drug_text_lines.join('') }}
</text>
<text v-if="item.remark" class="remark">备注{{ item.remark }}</text>
<text v-if="item.status === 3 && item.exception_message" class="exception">
异常{{ item.exception_message }}
</text>
</view>
<u-button v-if="list.length" plain type="primary" class="mt" @click="goForm">新建传方</u-button>
</view>
<transfer-detail-drawer ref="detailDrawer" />
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import TransferDetailDrawer from '@/subPackages/sub_clinic_salesperson/components/TransferDetailDrawer.vue'
import { getClinicSalespersonTransferPrescriptionList } from '@/api/clinicSalesperson.js'
export default {
components: { ClinicSalespersonPageLayout },
components: { ClinicSalespersonPageLayout, TransferDetailDrawer },
data() {
return { list: [] }
return { list: [], loading: false }
},
onShow() {
this.loadList()
},
methods: {
async loadList() {
const wrap = await getClinicSalespersonTransferPrescriptionList({ page: 1, pageSize: 50 })
if (wrap && wrap.ok) {
this.list = wrap.data?.items || []
this.loading = true
try {
const wrap = await getClinicSalespersonTransferPrescriptionList({ page: 1, pageSize: 50 })
if (wrap && wrap.ok) {
this.list = wrap.data?.items || []
}
} finally {
this.loading = false
}
},
goForm() {
uni.navigateTo({ url: '/subPackages/sub_clinic_salesperson/transfer-prescription/form' })
statusTagType(status) {
const map = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'error' }
return map[Number(status)] || 'info'
},
goDetail(id) {
uni.showToast({ title: `传方#${id}`, icon: 'none' })
goForm() {
uni.navigateBack({ fail: () => {
uni.navigateTo({ url: '/subPackages/sub_clinic_salesperson/home/index' })
}})
},
openDetail(id) {
if (this.$refs.detailDrawer) {
this.$refs.detailDrawer.open(id)
}
},
},
}
@@ -50,6 +75,7 @@ export default {
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; }
.top { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 28rpx; font-weight: 600; }
.diag, .time { display: block; font-size: 24rpx; color: #666; margin-top: 8rpx; }
.meta, .drugs, .remark { display: block; font-size: 24rpx; color: #666; margin-top: 8rpx; }
.exception { display: block; font-size: 24rpx; color: #ef4444; margin-top: 8rpx; }
.mt { margin-top: 24rpx; }
</style>

View File

@@ -0,0 +1,158 @@
<template>
<view class="picker-wrap">
<view class="promoter-picker" @click="onOpen">
<view v-if="selected" class="promoter-selected">
<image v-if="selected.avatar" :src="selected.avatar" class="avatar" mode="aspectFill" />
<view v-else class="avatar placeholder">{{ (selected.nick_name || '?').charAt(0) }}</view>
<view class="meta">
<text class="name">{{ selected.nick_name || '-' }}</text>
<text class="sub">{{ selected.phone || '' }}</text>
</view>
</view>
<text v-else class="placeholder-text">{{ placeholder }}</text>
<u-icon v-if="!disabled" name="arrow-right" color="#c0c4cc" size="28"></u-icon>
</view>
<text v-if="selected && clearable" class="clear-btn" @click.stop="onClear">清空</text>
<drawer-page-container v-model="showPopup" title="选择邀请人推广员">
<view class="search-wrap">
<u-input
v-model="searchKeyword"
placeholder="搜索姓名、手机号"
:clearable="true"
border="surround"
@change="onSearch"
/>
</view>
<scroll-view scroll-y class="sheet-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="option-item"
@click="onPick(item)"
>
<image v-if="item.avatar" :src="item.avatar" class="avatar" mode="aspectFill" />
<view v-else class="avatar placeholder">{{ (item.nick_name || '?').charAt(0) }}</view>
<view class="meta">
<text class="name">{{ item.nick_name || '-' }}</text>
<text v-if="item.phone" class="phone">{{ item.phone }}</text>
</view>
<u-icon v-if="value === item.id" name="checkmark" color="#6acdbb" size="32"></u-icon>
</view>
</scroll-view>
</drawer-page-container>
</view>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
export default {
name: 'SalespersonPicker',
components: { DrawerPageContainer },
props: {
value: { type: Number, default: 0 },
disabled: { type: Boolean, default: false },
clearable: { type: Boolean, default: true },
placeholder: { type: String, default: '请选择邀请人推广员(可选)' },
excludeId: { type: Number, default: 0 },
storeId: { type: Number, default: 0 },
fetchOptions: { type: Function, required: true },
initialSelected: { type: Object, default: null },
},
data() {
return {
showPopup: false,
searchKeyword: '',
loading: false,
options: [],
selected: null,
searchTimer: null,
}
},
watch: {
value: {
immediate: true,
handler(val) {
if (!val) {
this.selected = null
return
}
if (this.initialSelected && Number(this.initialSelected.id) === Number(val)) {
this.selected = this.initialSelected
}
},
},
initialSelected: {
immediate: true,
handler(item) {
if (item && Number(item.id) === Number(this.value)) {
this.selected = item
}
},
},
},
methods: {
onOpen() {
if (this.disabled) return
this.showPopup = true
this.loadOptions('')
},
onSearch() {
if (this.searchTimer) clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => this.loadOptions(this.searchKeyword), 300)
},
async loadOptions(keyword) {
this.loading = true
try {
const wrap = await this.fetchOptions({
keyword: keyword || '',
exclude_id: this.excludeId,
store_id: this.storeId || undefined,
})
this.options = (wrap && wrap.ok && wrap.data) ? wrap.data : []
} finally {
this.loading = false
}
},
onPick(item) {
this.selected = item
this.$emit('input', Number(item.id))
this.$emit('change', item)
this.showPopup = false
},
onClear() {
this.selected = null
this.$emit('input', 0)
this.$emit('change', null)
},
},
}
</script>
<style lang="scss" scoped>
.picker-wrap { position: relative; }
.promoter-picker {
display: flex; align-items: center; justify-content: space-between;
background: #fff; border-radius: 16rpx; padding: 24rpx;
}
.promoter-selected { display: flex; align-items: center; flex: 1; min-width: 0; }
.avatar { width: 72rpx; height: 72rpx; border-radius: 50%; margin-right: 20rpx; flex-shrink: 0; }
.avatar.placeholder {
background: #e8f8f5; color: #6acdbb; display: flex; align-items: center; justify-content: center;
font-size: 32rpx; font-weight: 600;
}
.meta { flex: 1; min-width: 0; }
.name { display: block; font-size: 28rpx; font-weight: 600; color: #333; }
.sub, .phone { font-size: 24rpx; color: #999; }
.placeholder-text { font-size: 28rpx; color: #999; flex: 1; }
.clear-btn { position: absolute; right: 24rpx; top: -36rpx; font-size: 24rpx; color: #999; }
.search-wrap { padding: 0 24rpx 16rpx; }
.sheet-scroll { max-height: 60vh; }
.empty { text-align: center; color: #999; padding: 60rpx 0; font-size: 26rpx; }
.option-item {
display: flex; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -39,6 +39,22 @@
</picker>
</view>
<view class="section-title">邀请关系可选</view>
<view class="field-col">
<text class="label block">邀请人推广员</text>
<salesperson-picker
v-model="form.parent_salesperson_id"
:exclude-id="form.id"
:store-id="form.store_id"
:fetch-options="fetchSalespersonOptions"
:initial-selected="parentInitial"
/>
</view>
<view v-if="Number(form.parent_salesperson_id) > 0" class="field">
<text class="label">邀请分成%</text>
<input class="input" v-model="form.inviter_split" type="digit" placeholder="上级从订单利润中获得的比例" />
</view>
<view class="submit-btn" @click="submit">保存</view>
</view>
</page-layout>
@@ -47,17 +63,19 @@
<script>
import PageLayout from './components/PageLayout.vue';
import StoreSearchPicker from './components/StoreSearchPicker.vue';
import SalespersonPicker from './components/SalespersonPicker.vue';
import { getSalespersonManageContext, resolveManageMode } from './common/context.js';
import { SPLIT_TYPE_OPTIONS, TCM_BASE_OPTIONS } from './common/labels.js';
const ROOT = '/subPackages/sub_salesperson_manage';
export default {
components: { PageLayout, StoreSearchPicker },
components: { PageLayout, StoreSearchPicker, SalespersonPicker },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
isUpdate: false,
parentInitial: null,
form: {
id: 0,
store_id: 0,
@@ -67,6 +85,8 @@ export default {
split: 0,
tcm_split: 0,
tcm_base_type: 0,
parent_salesperson_id: 0,
inviter_split: 0,
},
splitTypeLabels: SPLIT_TYPE_OPTIONS.map((o) => o.label),
tcmBaseLabels: TCM_BASE_OPTIONS.map((o) => o.label),
@@ -90,6 +110,9 @@ export default {
}
},
methods: {
fetchSalespersonOptions(params) {
return this.ctx.api.getSalespersonOptions(params);
},
async loadDetail(options) {
const wrap = await this.ctx.api.getList({ page: 1, pageSize: 100 });
if (!wrap || !wrap.ok) return;
@@ -101,6 +124,15 @@ export default {
this.form.split = item.split || 0;
this.form.tcm_split = item.tcm_split || 0;
this.form.tcm_base_type = Number(item.tcm_base_type) || 0;
this.form.parent_salesperson_id = Number(item.parent_salesperson_id) || 0;
this.form.inviter_split = item.inviter_split || 0;
if (this.form.parent_salesperson_id > 0) {
this.parentInitial = {
id: this.form.parent_salesperson_id,
nick_name: item.parent_nick_name,
phone: item.parent_phone,
};
}
if (!this.form.store_id && item.qr_code) {
this.form.store_id = item.qr_code.store_id || 0;
}
@@ -129,8 +161,16 @@ export default {
uni.showToast({ title: '请选择诊所', icon: 'none' });
return;
}
if (Number(this.form.parent_salesperson_id) > 0 && !(Number(this.form.inviter_split) > 0)) {
uni.showToast({ title: '请填写邀请分成比例', icon: 'none' });
return;
}
const payload = { ...this.form };
if (Number(payload.split_type) === 0) payload.split = 0;
if (!(Number(payload.parent_salesperson_id) > 0)) {
payload.parent_salesperson_id = 0;
payload.inviter_split = 0;
}
uni.showLoading({ title: '保存中' });
try {
@@ -165,6 +205,8 @@ export default {
.picker-val { flex: 1; font-size: 28rpx; text-align: right; color: #333; }
.link-text { font-size: 26rpx; color: #6ACDBB; }
.section-title { font-size: 26rpx; color: #999; margin: 24rpx 0 12rpx 8rpx; }
.field-col { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
.label.block { display: block; width: auto; margin-bottom: 16rpx; }
.submit-btn {
margin-top: 40rpx; background: linear-gradient(90deg, #00BFA6, #6ACDBB);
color: #fff; text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;

View File

@@ -1,5 +1,14 @@
<template>
<u-popup
<view>
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="show"
:show="show"
:overlay="false"
@leave="handleClose"
/>
<!-- #endif -->
<u-popup
v-model="show"
mode="bottom"
:closeable="true"
@@ -8,58 +17,70 @@
:mask-close-able="true"
z-index="10078"
height="75%"
>
<view class="transfer-patient-drawer page-bg">
<view class="modal-header white">
<d-text text="收货信息" className="fs-32 font-bold color-title"></d-text>
</view>
<scroll-view class="scroll-container" scroll-y>
<view class="form-body">
<view class="field">
<d-text text="收货人" className="fs-28 font-bold color-title m-b-12"></d-text>
<u-input v-model="form.patient_name" placeholder="请输入收货人姓名" border="surround" />
</view>
<view class="field">
<d-text text="手机号" className="fs-28 font-bold color-title m-b-12"></d-text>
<u-input v-model="form.patient_mobile" placeholder="请输入手机号" type="number" border="surround" />
</view>
<view class="field">
<d-text text="收货地址" className="fs-28 font-bold color-title m-b-12"></d-text>
<u-input v-model="form.patient_address" placeholder="请输入收货地址" border="surround" />
</view>
<view class="field">
<d-text text="处方图片(可选)" className="fs-28 font-bold color-title m-b-12"></d-text>
<view class="img-list">
<view v-for="(img, idx) in form.prescription_images" :key="idx" class="img-item">
<image :src="img" mode="aspectFill" />
<u-icon name="close-circle-fill" color="#999" size="36" class="img-remove" @click="removeImage(idx)" />
</view>
<view class="img-add" @click="chooseImage" v-if="form.prescription_images.length < 6">
<u-icon name="plus" size="40" color="#B0B6C2"></u-icon>
>
<view class="transfer-patient-drawer page-bg">
<view class="modal-header white">
<d-text text="收货信息" className="fs-32 font-bold color-title"></d-text>
</view>
<scroll-view class="scroll-container" scroll-y>
<view class="form-body">
<view class="field">
<d-text text="收货人" className="fs-28 font-bold color-title m-b-12"></d-text>
<u-input v-model="form.patient_name" placeholder="请输入收货人姓名" border="surround" />
</view>
<view class="field">
<d-text text="手机号" className="fs-28 font-bold color-title m-b-12"></d-text>
<u-input v-model="form.patient_mobile" placeholder="请输入手机号" type="number" border="surround" />
</view>
<view class="field">
<d-text text="收货地址" className="fs-28 font-bold color-title m-b-12"></d-text>
<u-input v-model="form.patient_address" placeholder="请输入收货地址" border="surround" />
</view>
<view class="field">
<d-text
:text="photoMode ? '处方图片(必填)' : '处方图片(可选)'"
className="fs-28 font-bold color-title m-b-12"
></d-text>
<view class="img-list">
<view v-for="(img, idx) in form.prescription_images" :key="idx" class="img-item">
<image :src="img" mode="aspectFill" />
<u-icon name="close-circle-fill" color="#999" size="36" class="img-remove" @click="removeImage(idx)" />
</view>
<view class="img-add" @click="chooseImage" v-if="form.prescription_images.length < 6">
<u-icon name="plus" size="40" color="#B0B6C2"></u-icon>
</view>
</view>
</view>
<view class="field">
<d-text text="备注(选填)" className="fs-28 font-bold color-title m-b-12"></d-text>
<u-input
v-model="form.remark"
type="textarea"
placeholder="可填写补充说明"
border="surround"
:height="120"
/>
</view>
</view>
</view>
</scroll-view>
<view class="modal-footer flex-row flex-jus-between flex-ali-center white shadow-up">
<u-button
</scroll-view>
<view class="modal-footer flex-row flex-jus-between flex-ali-center white shadow-up">
<u-button
:throttle-time="0"
@click="handleClose"
shape="circle"
:custom-style="cancelBtnStyle"
>取消</u-button>
<u-button
>取消</u-button>
<u-button
:throttle-time="0"
:loading="submitting"
@click="handleConfirm"
shape="circle"
:custom-style="confirmBtnStyle"
>确认提交</u-button>
>确认提交</u-button>
</view>
</view>
</view>
</u-popup>
</u-popup>
</view>
</template>
<script>
@@ -76,6 +97,11 @@ export default {
type: Boolean,
default: false,
},
/** 拍照传方模式:处方图片必填 */
photoMode: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -85,6 +111,7 @@ export default {
patient_mobile: '',
patient_address: '',
prescription_images: [],
remark: '',
},
cancelBtnStyle: {
backgroundColor: '#F4F6F8',
@@ -146,15 +173,21 @@ export default {
const name = (this.form.patient_name || '').trim();
const mobile = (this.form.patient_mobile || '').trim();
const address = (this.form.patient_address || '').trim();
const remark = (this.form.remark || '').trim();
if (!name || !mobile || !address) {
uni.showToast({ title: '请填写收货人、手机号和地址', icon: 'none' });
return;
}
if (this.photoMode && this.form.prescription_images.length === 0) {
uni.showToast({ title: '请上传至少一张处方图片', icon: 'none' });
return;
}
this.$emit('confirm', {
patient_name: name,
patient_mobile: mobile,
patient_address: address,
prescription_images: [...this.form.prescription_images],
remark,
});
},
resetForm() {
@@ -163,6 +196,7 @@ export default {
patient_mobile: '',
patient_address: '',
prescription_images: [],
remark: '',
};
},
},

View File

@@ -96,8 +96,8 @@
<u-button size="mini" type="primary" plain @click="openSalespersonTransfer">查看传方</u-button>
</view>
<!-- 诊断输入区域 -->
<view class="top white p-32 flex-col m-t-16 radius-12 mx-32">
<!-- 诊断输入区域传方模式不展示 -->
<view v-if="!isSalespersonTransferMode" class="top white p-32 flex-col m-t-16 radius-12 mx-32">
<view class="top_title flex-row flex-ali-center">
<image :src="require('@/static/image/js.png')" class="size-32"></image>
<d-text text="临床诊断" className="fs-32 m-l-16 font-bold" color="#333"></d-text>
@@ -232,7 +232,7 @@
</view>
</view>
<view class="white p-32 radius-12 m-t-24 flex-col" v-if="currentDrugs.length > 0">
<view class="white p-32 radius-12 m-t-24 flex-col" v-if="currentDrugs.length > 0 && !isSalespersonTransferMode">
<view class="flex-jus-sp flex-row flex-ali-center">
<d-text text="医嘱补充" color="#333" className="font-bold"></d-text>
<d-text @click="handleOpenDoctorOrderModal" text="选择常用医嘱" color="#6ACDBB"></d-text>
@@ -369,7 +369,7 @@
</view>
</view>
<view class="white p-32 radius-12 m-t-24 flex-col" v-if="currentDrugs.length > 0">
<view class="white p-32 radius-12 m-t-24 flex-col" v-if="currentDrugs.length > 0 && !isSalespersonTransferMode">
<view class="flex-jus-sp flex-row flex-ali-center">
<d-text text="医嘱补充" color="#333" className="font-bold"></d-text>
<d-text @click="handleOpenDoctorOrderModal" text="选择常用医嘱" color="#6ACDBB"></d-text>
@@ -381,8 +381,8 @@
</view>
</view>
<!-- 医嘱输入区域中药和简单产品共用 -->
<view class="flex-col white p-32 radius-12 mx-32 m-t-24" v-if="activeCategory === 1 && currentDrugs.length > 0">
<!-- 医嘱输入区域传方模式不展示 -->
<view class="flex-col white p-32 radius-12 mx-32 m-t-24" v-if="activeCategory === 1 && currentDrugs.length > 0 && !isSalespersonTransferMode">
<view class="flex-jus-sp flex-row flex-ali-center">
<d-text text="医嘱补充" color="#333" className="font-bold"></d-text>
<d-text @click="handleOpenDoctorOrderModal" text="选择常用医嘱" color="#6ACDBB" />
@@ -469,8 +469,8 @@
</template>
</view>
<!-- 提示信息 -->
<view class="ac p-b-40 p-x-32 flex-row flex-jus-center">
<!-- 提示信息传方模式不展示 -->
<view v-if="!isSalespersonTransferMode" class="ac p-b-40 p-x-32 flex-row flex-jus-center">
<u-icon name="shield-check" color="#B0B6C2" size="28" class="m-r-8"></u-icon>
<text class="fs-24 color-sub">请确认患者已在实体医院就诊并有明确诊断</text>
</view>
@@ -1016,6 +1016,12 @@ export default {
if (this.isSalespersonTransferMode) {
this.registerId = SALESPERSON_TRANSFER_STORAGE_KEY;
this._presetActiveCategory = 1;
// 传方模式默认自制剂 + 剂包
this.chineseConfig = {
...this.chineseConfig,
ruleType: 1,
packageMethodId: 2,
};
} else {
this.registerId = options.register_id || '';
}
@@ -1082,6 +1088,9 @@ export default {
if (this.activeCategory === 1 && this.chineseConfig.ruleType === 2) {
await this.hydrateChineseProcessRuleListsFromConfig();
}
if (this.isSalespersonTransferMode) {
this.ensureSalespersonTransferPackageMethodDefault();
}
if (!this.isSalespersonTransferMode) {
await this.maybeApplyReusePrescription();
if (this.activeCategory === 1) await this.checkAndShowTransferTip();
@@ -1121,6 +1130,19 @@ export default {
console.error('加载基础配置数据失败:', error);
}
},
/**
* 传方模式草稿未保存包法时默认剂包id=2
*/
ensureSalespersonTransferPackageMethodDefault() {
if (!this.isSalespersonTransferMode || this.activeCategory !== 1) return;
if (!this.chineseConfig.packageMethodId) {
this.chineseConfig = {
...this.chineseConfig,
ruleType: 1,
packageMethodId: 2,
};
}
},
restoreFromLocalStorage() {
if (this._presetActiveCategory !== null && this._presetActiveCategory !== undefined && !Number.isNaN(this._presetActiveCategory)) {
this.activeCategory = this._presetActiveCategory;
@@ -2113,7 +2135,8 @@ export default {
requireOnlineTcm: this.activeCategory === 1 && this.isOnlineRevisit,
onlineTcmDiseasesId: this.onlineTcmDiseasesId,
onlineTcmMethodId: this.onlineTcmMethodId,
onlineTcmSyndromeId: this.onlineTcmSyndromeId
onlineTcmSyndromeId: this.onlineTcmSyndromeId,
skipDiagnosisAndAdvice: this.isSalespersonTransferMode,
});
},
async checkChineseMedicineConflict() {
@@ -2376,7 +2399,6 @@ export default {
} catch (error) { console.error('保存常用方失败:', error); this.$toast('保存常用方失败,请重试'); }
},
buildSalespersonTransferPayload(patientData) {
const clinicalDiagnose = this.diagnosisText || this.diagnoses.map((item) => item.name).join(',');
const drugs = this.currentDrugs.map((drug) => ({
drug_id: drug.id || drug.drug_id || drug.index_id,
drug_name: drug.drug_name || drug.name,
@@ -2388,8 +2410,10 @@ export default {
patient_mobile: patientData.patient_mobile,
patient_address: patientData.patient_address,
prescription_images: patientData.prescription_images || [],
clinical_diagnose: clinicalDiagnose,
doctor_order: this.medicalAdvice || '',
remark: patientData.remark || '',
transfer_mode: 1,
clinical_diagnose: '',
doctor_order: '',
dosage: this.chineseConfig.dosage || 7,
day_dosage: this.chineseConfig.dayDosage || 2,
drugs,

View File

@@ -23,7 +23,8 @@ export class PrescriptionValidator {
requireOnlineTcm,
onlineTcmDiseasesId,
onlineTcmMethodId,
onlineTcmSyndromeId
onlineTcmSyndromeId,
skipDiagnosisAndAdvice,
} = params;
// 验证药品
@@ -34,20 +35,20 @@ export class PrescriptionValidator {
};
}
// 验证诊断
if (!diagnoses || diagnoses.length === 0) {
return {
valid: false,
message: '请添加诊断'
};
}
// 验证医嘱
if (!medicalAdvice || medicalAdvice.trim() === '') {
return {
valid: false,
message: '请输入医嘱'
};
// 传方模式跳过诊断与医嘱校
if (!skipDiagnosisAndAdvice) {
if (!diagnoses || diagnoses.length === 0) {
return {
valid: false,
message: '请添加诊断'
};
}
if (!medicalAdvice || medicalAdvice.trim() === '') {
return {
valid: false,
message: '请输入医嘱'
};
}
}
// 验证中药配置

View File

@@ -4,9 +4,9 @@
<view class="status-banner"
:class="infoList.status==1&&'bg_blue'||infoList.status==2&&'bg_green'||infoList.status==3&&'bg_purple'||infoList.status==7&&'bg_red'">
<view class="status-content flex-row flex-jus-sp flex-ali-center">
<text class="fs-36 font-bold text-white">{{infoList.status==1&&'待接诊'||infoList.status==2&&'已接诊'||infoList.status==3&&'已完成'||infoList.status==7&&'已拒诊'}}</text>
<text class="fs-36 font-bold text-white">{{infoList.status==1&&'待接诊'||infoList.status==2&&'已接诊'||infoList.status==3&&'已完成'||infoList.status==4&&'已取消'||infoList.status==7&&'已拒诊'}}</text>
<image class="status-icon"
:src="infoList.status==1&&'/static/info/djz.png'||infoList.status==2&&'/static/info/yjz.png'||infoList.status==3&&'/static/info/ywc.png'||infoList.status==7&&'/static/info/jz.png'"
:src="infoList.status==1&&'/static/info/djz.png'||infoList.status==2&&'/static/info/yjz.png'||infoList.status==3&&'/static/info/ywc.png'||infoList.status==4&&'/statuic/info/jz.png'||infoList.status==7&&'/static/info/jz.png'"
mode="aspectFit"></image>
</view>
</view>
@@ -61,7 +61,7 @@
<text class="info-label">规格</text>
<text class="info-value">{{ specialPrescriptionPurchaseLabel }}</text>
</view>
<view class="-8">
<view class="">
<u-tag
v-if="Number(infoList.special_prescription_patient_record.status) === 0"
text="待导入"
@@ -84,8 +84,9 @@
shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', border: 'none' }"
@click="goImportSpecialPrescription"
:disabled="infoList.status!=2"
>
导入
{{ infoList.status!=2? '接诊后可导入特色方' : '导入特色方' }}
</u-button>
</view>
</view>
@@ -705,7 +706,7 @@ page {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
margin-bottom: 6rpx;
line-height: 1.5;
&:last-child {