1. 修复了一些问题

This commit is contained in:
李琦
2026-06-09 17:02:13 +08:00
parent 9dd9b061c5
commit 3f6da79ece
38 changed files with 3814 additions and 343 deletions

View File

@@ -81,6 +81,10 @@ const BUSINESS_MENUS = [
{ key: 'order', label: '商品订单', desc: '全平台订单概览', icon: 'bag-fill', theme: GLOBAL_THEMES[3], path: '/subPackages/sub_clinic_admin/order/index' },
]
const SALESPERSON_MENUS = [
{ key: 'salesperson-manage', label: '推广员管理', desc: '添加与结算推广员', icon: 'account-fill', theme: GLOBAL_THEMES[2], path: '/subPackages/sub_salesperson_manage/index?mode=clinic' },
]
const WAREHOUSE_FALLBACK = {
key: 'warehouse',
label: '仓库管理',
@@ -105,6 +109,7 @@ export default {
return [
{ key: 'finance', title: '财务管理', items: FINANCE_MENUS },
{ key: 'business', title: '业务管理', items: BUSINESS_MENUS },
{ key: 'salesperson', title: '门店推广员', items: SALESPERSON_MENUS },
{ key: 'warehouse', title: '仓库管理', items: this.warehouseMenus },
]
},

View File

@@ -123,7 +123,9 @@ export default {
profileInfo: null,
stats: {},
menuItems: [
{ key: 'earnings', label: '我的收益', path: `${ROOT}/earnings/index` },
{ 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: 'earnings', label: '我的收益', path: `${ROOT}/earnings/index` },
{ key: 'leads', label: '获客记录', path: `${ROOT}/leads/index` },
{ key: 'commission', label: '分成记录', path: `${ROOT}/commission/index` },
{ key: 'settlement', label: '结算记录', path: `${ROOT}/settlement/index` },
@@ -134,6 +136,8 @@ export default {
// 动态注入多彩主题和增强文案,使用 uView 图标名称
enrichedMenuItems() {
const themes = [
{ bg: 'linear-gradient(135deg, #ECFDF5, #D1FAE5)', color: '#10B981', desc: '线下中药传方', icon: 'edit-pen-fill' },
{ bg: 'linear-gradient(135deg, #F0F9FF, #E0F2FE)', color: '#0EA5E9', desc: '历史传方记录', icon: 'file-text-fill' },
// 收益:使用钻石/钱包相关图标
{ bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316', desc: '查看收益明细', icon: 'red-packet-fill' },
// 获客:使用目标/数据相关图标

View File

@@ -1,215 +1,430 @@
<template>
<clinic-salesperson-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container">
<view class="profile-panel">
<view class="avatar-squircle">
<text>{{ avatarText }}</text>
</view>
<view class="user-info">
<text class="user-name">{{ displayName }}</text>
<view class="role-badge">
<text class="role-text">推广员</text>
</view>
<text v-if="phone" class="phone-line">{{ phone }}</text>
<text v-if="storeName" class="store-line">{{ storeName }}</text>
</view>
</view>
<view class="setting-panel">
<view class="setting-item">
<text class="item-label">所属诊所</text>
<text class="item-value">{{ storeName || '-' }}</text>
</view>
</view>
<view class="setting-panel">
<account-switch-entry />
<view
class="setting-item action-item"
hover-class="item-hover"
:hover-stay-time="100"
@click="logout"
>
<text class="danger-text">退出登录</text>
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
</view>
</view>
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { ClinicSalespersonPageLayout, AccountSwitchEntry },
data() {
return {
displayName: '',
phone: '',
storeName: '',
loading: false,
}
},
computed: {
avatarText() {
return (this.displayName || '推').slice(0, 1)
},
},
onShow() {
this.loadProfile()
},
methods: {
async loadProfile() {
if (this.loading) return
this.loading = true
try {
const wrap = await getClinicSalespersonMyInfo()
if (!wrap || !wrap.ok) return
const info = wrap.data || {}
this.displayName = info.salesperson?.nick_name || '推广员'
this.phone = info.salesperson?.phone || ''
this.storeName = info.store?.name || ''
uni.setStorageSync('clinic_salesperson_user', info)
} finally {
this.loading = false
}
},
logout() {
uni.showModal({
title: '提示',
content: '确定退出登录?',
confirmColor: '#f53f3f',
success: (res) => {
if (!res.confirm) return
clearLoginStorage()
uni.reLaunch({ url: '/pages/login/index' })
},
})
},
},
}
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$theme-dark: #4db6a8;
$color-page-bg: #f5f7f8;
$color-text-title: #222b2a;
$color-text-muted: #86909c;
$color-border: #f2f3f5;
$color-danger: #f53f3f;
.mine-container {
padding: 0 0 60rpx;
background-color: $color-page-bg;
<template>
<clinic-salesperson-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container">
<view class="profile-panel">
<view class="avatar-squircle">
<text>{{ avatarText }}</text>
</view>
<view class="user-info">
<text class="user-name">{{ displayName }}</text>
<view class="role-badge">
<text class="role-text">推广员</text>
</view>
<text v-if="phone" class="phone-line">{{ phone }}</text>
<text v-if="storeName" class="store-line">{{ storeName }}</text>
</view>
</view>
<view class="setting-panel">
<view class="setting-item">
<text class="item-label">所属诊所</text>
<text class="item-value">{{ storeName || '-' }}</text>
</view>
</view>
<view class="setting-panel">
<account-switch-entry />
<view
class="setting-item action-item"
hover-class="item-hover"
:hover-stay-time="100"
@click="logout"
>
<text class="danger-text">退出登录</text>
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
</view>
</view>
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { ClinicSalespersonPageLayout, AccountSwitchEntry },
data() {
return {
displayName: '',
phone: '',
storeName: '',
loading: false,
}
},
computed: {
avatarText() {
return (this.displayName || '推').slice(0, 1)
},
},
onShow() {
this.loadProfile()
},
methods: {
async loadProfile() {
if (this.loading) return
this.loading = true
try {
const wrap = await getClinicSalespersonMyInfo()
if (!wrap || !wrap.ok) return
const info = wrap.data || {}
this.displayName = info.salesperson?.nick_name || '推广员'
this.phone = info.salesperson?.phone || ''
this.storeName = info.store?.name || ''
uni.setStorageSync('clinic_salesperson_user', info)
} finally {
this.loading = false
}
},
logout() {
uni.showModal({
title: '提示',
content: '确定退出登录?',
confirmColor: '#f53f3f',
success: (res) => {
if (!res.confirm) return
clearLoginStorage()
uni.reLaunch({ url: '/pages/login/index' })
},
})
},
},
}
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$theme-dark: #4db6a8;
$color-page-bg: #f5f7f8;
$color-text-title: #222b2a;
$color-text-muted: #86909c;
$color-border: #f2f3f5;
$color-danger: #f53f3f;
.mine-container {
padding: 0 0 60rpx;
background-color: $color-page-bg;
min-height: 100vh;
box-sizing: border-box;
}
.profile-panel {
display: flex;
align-items: center;
background-color: #fff;
padding: 60rpx 40rpx 50rpx;
margin-bottom: 24rpx;
}
.avatar-squircle {
width: 128rpx;
height: 128rpx;
border-radius: 40rpx;
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.25);
color: #fff;
font-size: 52rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.user-info {
margin-left: 32rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.user-name {
font-size: 40rpx;
font-weight: 700;
color: $color-text-title;
margin-bottom: 12rpx;
}
.role-badge {
align-self: flex-start;
background-color: rgba(106, 205, 187, 0.1);
border: 1rpx solid rgba(106, 205, 187, 0.2);
padding: 4rpx 16rpx;
border-radius: 8rpx;
margin-bottom: 12rpx;
}
.role-text {
font-size: 22rpx;
color: $theme-dark;
font-weight: 600;
}
.phone-line,
.store-line {
font-size: 26rpx;
color: $color-text-muted;
margin-top: 8rpx;
}
.setting-panel {
background-color: #fff;
margin-bottom: 24rpx;
padding: 0 40rpx;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx 0;
border-bottom: 1rpx solid $color-border;
}
.setting-panel .setting-item:last-child {
border-bottom: none;
}
.item-label {
font-size: 30rpx;
color: $color-text-title;
font-weight: 500;
}
.item-value {
font-size: 30rpx;
color: $color-text-muted;
}
.action-item {
margin: 0 -40rpx;
padding: 36rpx 40rpx;
}
.item-hover {
background-color: #f7f8fa;
}
.danger-text {
font-size: 30rpx;
color: $color-danger;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<view />
</template>
<script>
const TRANSFER_RX_URL = '/subPackages/sub_workbench/prescription_v2/index?salesperson_transfer=1&initial_category=1'
export default {
onLoad() {
uni.redirectTo({ url: TRANSFER_RX_URL })
},
}
</script>

View File

@@ -0,0 +1,55 @@
<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 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" />
</view>
<text class="diag">诊断{{ item.clinical_diagnose || '-' }}</text>
<text class="time">{{ item.transfer_time_text }}</text>
</view>
<u-button v-if="list.length" plain type="primary" class="mt" @click="goForm">新建传方</u-button>
</view>
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonTransferPrescriptionList } from '@/api/clinicSalesperson.js'
export default {
components: { ClinicSalespersonPageLayout },
data() {
return { list: [] }
},
onShow() {
this.loadList()
},
methods: {
async loadList() {
const wrap = await getClinicSalespersonTransferPrescriptionList({ page: 1, pageSize: 50 })
if (wrap && wrap.ok) {
this.list = wrap.data?.items || []
}
},
goForm() {
uni.navigateTo({ url: '/subPackages/sub_clinic_salesperson/transfer-prescription/form' })
},
goDetail(id) {
uni.showToast({ title: `传方#${id}`, icon: 'none' })
},
},
}
</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; }
.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; }
.mt { margin-top: 24rpx; }
</style>

View File

@@ -0,0 +1,20 @@
<template>
<page-layout :is-platform="true" :title="title" :show-back="showBack" :show-tabbar="showTabbar" :tab-index="tabIndex">
<slot />
</page-layout>
</template>
<script>
import PageLayout from '@/subPackages/sub_salesperson_manage/components/PageLayout.vue';
export default {
name: 'StorePageLayout',
components: { PageLayout },
props: {
title: { type: String, default: '' },
showBack: { type: Boolean, default: true },
showTabbar: { type: Boolean, default: false },
tabIndex: { type: Number, default: 0 },
},
};
</script>

View File

@@ -105,6 +105,16 @@ export default {
key: 'order', label: '商品订单', desc: '全平台订单概览', path: `${shared}/order/index`,
icon: 'bag-fill',
theme: { bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' } // 安全绿
},
{
key: 'store-manage', label: '门店管理', desc: '诊所列表与价格配置', path: `${root}/store/index`,
icon: 'home-fill',
theme: { bg: 'linear-gradient(135deg, #ECFEFF, #CFFAFE)', color: '#06B6D4' }
},
{
key: 'salesperson-manage', label: '推广员管理', desc: '全平台推广员', path: '/subPackages/sub_salesperson_manage/index?mode=platform',
icon: 'account-fill',
theme: { bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' }
}
],
},

View File

@@ -0,0 +1,154 @@
<template>
<store-page-layout title="在线复诊配置" :show-back="true">
<view class="page">
<view class="field">
<text class="label">互联网诊疗资质</text>
<switch :checked="form.is_internet_medical === 1" color="#6ACDBB" @change="onMedicalChange" />
</view>
<view v-if="form.is_internet_medical !== 1 && storeOptions.length" class="field column">
<text class="label">委托诊所</text>
<picker :range="storeLabels" :value="delegateIndex" @change="onDelegateChange">
<view class="picker-val">{{ storeLabels[delegateIndex] || '请选择' }}</view>
</picker>
</view>
<view class="field column">
<text class="label">复诊负责人</text>
<picker :range="doctorLabels" :value="doctorIndex" @change="onDoctorChange">
<view class="picker-val">{{ doctorLabels[doctorIndex] || '请选择' }}</view>
</picker>
</view>
<view class="submit-btn" @click="submit">保存配置</view>
</view>
</store-page-layout>
</template>
<script>
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import {
getPlatformStoreDetail,
getDoctorOption,
getInternetMedicalStoreOption,
bindOnlineConsultation,
} from '@/api/platformStore.js';
export default {
components: { StorePageLayout },
data() {
return {
storeId: 0,
form: {
id: 0,
is_internet_medical: 0,
delegate_store_id: null,
online_consultation_doctor_id: null,
},
storeOptions: [],
doctorOptions: [],
};
},
computed: {
storeLabels() {
return this.storeOptions.map((s) => s.name);
},
doctorLabels() {
return this.doctorOptions.map((d) => d.name || d.nick_name || `医生#${d.id}`);
},
delegateIndex() {
if (!this.form.delegate_store_id) return 0;
const idx = this.storeOptions.findIndex((s) => Number(s.id) === Number(this.form.delegate_store_id));
return idx >= 0 ? idx : 0;
},
doctorIndex() {
if (!this.form.online_consultation_doctor_id) return 0;
const idx = this.doctorOptions.findIndex((d) => Number(d.id) === Number(this.form.online_consultation_doctor_id));
return idx >= 0 ? idx : 0;
},
},
onLoad(options) {
this.storeId = Number(options.id || 0);
this.form.id = this.storeId;
this.init();
},
methods: {
async init() {
const [detailWrap, storeWrap] = await Promise.all([
getPlatformStoreDetail(this.storeId),
getInternetMedicalStoreOption(),
]);
if (detailWrap && detailWrap.ok) {
const d = detailWrap.data || {};
this.form.is_internet_medical = Number(d.is_internet_medical) || 0;
this.form.delegate_store_id = d.delegate_store_id || null;
this.form.online_consultation_doctor_id = d.online_consultation_doctor_id || null;
}
this.storeOptions = (storeWrap && storeWrap.ok && storeWrap.data) ? storeWrap.data : [];
await this.loadDoctors();
},
async loadDoctors() {
const storeId = this.form.is_internet_medical === 1
? this.storeId
: (this.form.delegate_store_id || 0);
if (!storeId) {
this.doctorOptions = [];
return;
}
const wrap = await getDoctorOption(storeId);
this.doctorOptions = (wrap && wrap.ok && wrap.data) ? wrap.data : [];
},
onMedicalChange(e) {
this.form.is_internet_medical = e.detail.value ? 1 : 0;
if (this.form.is_internet_medical === 1) {
this.form.delegate_store_id = null;
}
this.loadDoctors();
},
onDelegateChange(e) {
const idx = Number(e.detail.value);
const item = this.storeOptions[idx];
this.form.delegate_store_id = item ? item.id : null;
this.form.online_consultation_doctor_id = null;
this.loadDoctors();
},
onDoctorChange(e) {
const idx = Number(e.detail.value);
const item = this.doctorOptions[idx];
this.form.online_consultation_doctor_id = item ? item.id : null;
},
async submit() {
uni.showLoading({ title: '保存中' });
try {
const wrap = await bindOnlineConsultation({
id: this.storeId,
is_internet_medical: this.form.is_internet_medical,
delegate_store_id: this.form.delegate_store_id || null,
online_consultation_doctor_id: this.form.online_consultation_doctor_id || null,
});
if (wrap && wrap.ok) {
uni.showToast({ title: '保存成功', icon: 'success' });
setTimeout(() => uni.navigateBack(), 500);
}
} finally {
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.field {
display: flex; align-items: center; justify-content: space-between;
background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; font-size: 28rpx;
}
.field.column { flex-direction: column; align-items: flex-start; gap: 16rpx; }
.label { font-weight: 500; }
.picker-val { color: #6ACDBB; font-size: 28rpx; }
.submit-btn {
margin-top: 40rpx; background: linear-gradient(90deg, #00BFA6, #6ACDBB);
color: #fff; text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<store-page-layout title="门店详情" :show-back="true">
<view class="page" v-if="store.id">
<view class="hero-card">
<text class="name">{{ store.name }}</text>
<text class="sub">ID {{ store.id }} · {{ clinicTypeText(store.clinic_type) }}</text>
<text class="addr">{{ store.position || '暂无地址' }}</text>
</view>
<view class="section">
<text class="section-title">门店配置</text>
<view class="switch-row">
<text>推广员可见价格</text>
<switch :checked="Number(store.salesperson_see_price) === 1" color="#6ACDBB" @change="toggleSeePrice" />
</view>
<view class="switch-row">
<text>包邮</text>
<switch :checked="Number(store.is_shipping_free) === 1" color="#6ACDBB" @change="toggleShipping" />
</view>
<view class="switch-row">
<text>订阅价格波动</text>
<switch :checked="Number(store.subscribe_price_change) === 0" color="#6ACDBB" @change="toggleSubscribe" />
</view>
<view class="switch-row">
<text>查看毛利率</text>
<switch :checked="Number(store.see_rate) === 1" color="#6ACDBB" @change="toggleSeeRate" />
</view>
<view class="switch-row">
<text>开方可选医保</text>
<switch :checked="Number(store.allow_insurance_category) === 1" color="#6ACDBB" @change="toggleInsurance" />
</view>
<view class="picker-row">
<text>诊所类型</text>
<picker :range="clinicTypeLabels" :value="clinicTypeIndex" @change="onClinicType">
<view class="picker-val">{{ clinicTypeLabels[clinicTypeIndex] }}</view>
</picker>
</view>
</view>
<view class="section">
<text class="section-title">快捷操作</text>
<view class="action-grid">
<view class="action-btn" @click="goEdit">编辑资料</view>
<view class="action-btn" @click="goDrugPrice">修改药品价格</view>
<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>
</view>
<view v-if="store.online_consultation_doctor_name || store.delegate_store_name" class="section">
<text class="section-title">在线复诊</text>
<text v-if="store.delegate_store_name" class="info-line">委托诊所{{ store.delegate_store_name }}</text>
<text v-if="store.online_consultation_doctor_name" class="info-line">负责人{{ store.online_consultation_doctor_name }}</text>
</view>
</view>
</store-page-layout>
</template>
<script>
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import {
getPlatformStoreDetail,
toggleSalespersonSeePrice,
updateStoreShippingFree,
updateStoreSubscribeStatus,
updateStoreSeeRate,
updateStoreAllowInsurance,
updateStoreClinicType,
openStorePcWindows,
openStoreQrCode,
} from '@/api/platformStore.js';
import { openQrCodePreview, buildClinicQrPayload } from '@/utils/qrCodePreview.js';
const ROOT = '/subPackages/sub_platform_admin/store';
const MANAGE_ROOT = '/subPackages/sub_salesperson_manage';
export default {
components: { StorePageLayout },
data() {
return {
storeId: 0,
store: {},
clinicTypeLabels: ['未设置', '西医诊所', '中医诊所'],
};
},
computed: {
clinicTypeIndex() {
return Number(this.store.clinic_type) || 0;
},
},
onLoad(options) {
this.storeId = Number(options.id || 0);
this.loadDetail();
},
methods: {
clinicTypeText(type) {
return this.clinicTypeLabels[Number(type) || 0] || '未设置';
},
async loadDetail() {
if (!this.storeId) return;
const wrap = await getPlatformStoreDetail(this.storeId);
if (wrap && wrap.ok) {
this.store = wrap.data || {};
}
},
async toggleSeePrice() {
const wrap = await toggleSalespersonSeePrice(this.storeId);
if (wrap && wrap.ok && wrap.data) {
this.store.salesperson_see_price = wrap.data.see_price;
}
},
async toggleShipping() {
const wrap = await updateStoreShippingFree(this.storeId);
if (wrap && wrap.ok) await this.loadDetail();
},
async toggleSubscribe() {
const wrap = await updateStoreSubscribeStatus(this.storeId);
if (wrap && wrap.ok) await this.loadDetail();
},
async toggleSeeRate() {
const wrap = await updateStoreSeeRate(this.storeId);
if (wrap && wrap.ok) await this.loadDetail();
},
async toggleInsurance() {
const wrap = await updateStoreAllowInsurance(this.storeId);
if (wrap && wrap.ok) await this.loadDetail();
},
async onClinicType(e) {
const idx = Number(e.detail.value);
if (idx <= 0) return;
const wrap = await updateStoreClinicType(this.storeId, idx);
if (wrap && wrap.ok) {
this.store.clinic_type = idx;
uni.showToast({ title: '已更新', icon: 'success' });
}
},
goEdit() {
uni.navigateTo({ url: `${ROOT}/edit?id=${this.storeId}` });
},
goDrugPrice() {
uni.navigateTo({ url: `${ROOT}/drug-price?id=${this.storeId}&name=${encodeURIComponent(this.store.name || '')}` });
},
goConsultation() {
uni.navigateTo({ url: `${ROOT}/consultation?id=${this.storeId}` });
},
goPromoters() {
uni.navigateTo({ url: `${MANAGE_ROOT}/index?mode=platform&store_id=${this.storeId}` });
},
async openQr() {
uni.showLoading({ title: '生成中' });
try {
const wrap = await openStoreQrCode(this.storeId);
if (wrap && wrap.ok) {
await this.loadDetail();
const payload = buildClinicQrPayload(this.store);
openQrCodePreview({ type: 'clinic', title: this.store.name, payload: payload.payload });
}
} finally {
uni.hideLoading();
}
},
async openPc() {
uni.showModal({
title: '开通后台',
content: '确定为该诊所开通 PC 后台账号?',
success: async (res) => {
if (!res.confirm) return;
const wrap = await openStorePcWindows(this.storeId);
if (wrap && wrap.ok) {
uni.showToast({ title: '开通成功', icon: 'success' });
}
},
});
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.hero-card { background: linear-gradient(135deg, #6acdbb, #4db6a8); border-radius: 20rpx; padding: 32rpx; color: #fff; margin-bottom: 24rpx; }
.name { display: block; font-size: 36rpx; font-weight: 700; }
.sub, .addr { display: block; font-size: 24rpx; margin-top: 8rpx; opacity: 0.9; }
.section { background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; }
.section-title { display: block; font-size: 28rpx; font-weight: 600; margin-bottom: 16rpx; }
.switch-row, .picker-row {
display: flex; justify-content: space-between; align-items: center;
padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; font-size: 28rpx;
}
.picker-val { color: #6ACDBB; }
.action-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
.action-btn {
flex: 1; min-width: 40%; text-align: center; padding: 20rpx;
background: #f0fbf8; color: #6ACDBB; border-radius: 12rpx; font-size: 26rpx;
}
.action-btn.warn { background: #FEF3C7; color: #D97706; }
.info-line { display: block; font-size: 26rpx; color: #666; margin-bottom: 8rpx; }
</style>

View File

@@ -0,0 +1,106 @@
<template>
<store-page-layout title="药品价格" :show-back="true">
<view class="page">
<view class="toolbar">
<picker :range="typeLabels" :value="typeIndex" @change="onTypeChange">
<view class="type-picker">{{ typeLabels[typeIndex] }}</view>
</picker>
<view class="save-btn" @click="save">保存</view>
</view>
<view v-if="!list.length && !loading" class="empty">暂无药品</view>
<view v-for="item in list" :key="item.id" class="row">
<view class="drug-info">
<text class="drug-name">{{ item.drug_name || item.name }}</text>
<text class="spec">{{ item.drug_spec || item.spec || '' }}</text>
</view>
<view v-if="dirtyMap[item.id]" class="price-inputs">
<input class="price-input" v-model="dirtyMap[item.id].price" type="digit" placeholder="售价" />
<input class="price-input" v-model="dirtyMap[item.id].buy_price" type="digit" placeholder="供货价" />
</view>
</view>
</view>
</store-page-layout>
</template>
<script>
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import { DRUG_TYPE_OPTIONS, getStoreDrugsByType, updateStoreDrugPrices } from '@/api/platformStore.js';
export default {
components: { StorePageLayout },
data() {
return {
storeId: 0,
typeIndex: 1,
typeLabels: DRUG_TYPE_OPTIONS.map((o) => o.label),
list: [],
dirtyMap: {},
loading: false,
};
},
onLoad(options) {
this.storeId = Number(options.id || 0);
this.reload();
},
methods: {
onTypeChange(e) {
this.typeIndex = Number(e.detail.value);
this.reload();
},
async reload() {
if (!this.storeId) return;
this.loading = true;
try {
const type = DRUG_TYPE_OPTIONS[this.typeIndex].value;
const wrap = await getStoreDrugsByType(this.storeId, type);
const items = (wrap && wrap.ok && wrap.data) ? (wrap.data.items || wrap.data || []) : [];
this.list = Array.isArray(items) ? items : [];
const map = {};
this.list.forEach((row) => {
map[row.id] = {
price: row.price != null ? String(row.price) : '',
buy_price: row.buy_price != null ? String(row.buy_price) : '',
};
});
this.dirtyMap = map;
} finally {
this.loading = false;
}
},
async save() {
const drugs = this.list.map((row) => ({
id: row.id,
price: Number(this.dirtyMap[row.id].price) || 0,
buy_price: Number(this.dirtyMap[row.id].buy_price) || 0,
})).filter((d) => d.id > 0);
if (!drugs.length) {
uni.showToast({ title: '无数据', icon: 'none' });
return;
}
uni.showLoading({ title: '保存中' });
try {
const wrap = await updateStoreDrugPrices({ store_id: this.storeId, drugs });
if (wrap && wrap.ok) {
uni.showToast({ title: '保存成功', icon: 'success' });
}
} finally {
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; padding-bottom: 48rpx; }
.toolbar { display: flex; gap: 16rpx; margin-bottom: 20rpx; align-items: center; }
.type-picker { flex: 1; background: #fff; padding: 20rpx; border-radius: 12rpx; font-size: 26rpx; }
.save-btn { background: #6ACDBB; color: #fff; padding: 20rpx 28rpx; border-radius: 12rpx; font-size: 26rpx; }
.row { background: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 12rpx; }
.drug-name { display: block; font-size: 28rpx; font-weight: 600; }
.spec { font-size: 22rpx; color: #999; }
.price-inputs { display: flex; gap: 12rpx; margin-top: 12rpx; }
.price-input { flex: 1; background: #f5f5f5; border-radius: 8rpx; padding: 12rpx; font-size: 26rpx; text-align: center; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
</style>

View File

@@ -0,0 +1,111 @@
<template>
<store-page-layout title="编辑诊所" :show-back="true">
<view class="page">
<view class="field">
<text class="label">诊所名称</text>
<input class="input" v-model="form.name" placeholder="请输入" />
</view>
<view class="field">
<text class="label">联系人</text>
<input class="input" v-model="form.contact" placeholder="请输入" />
</view>
<view class="field">
<text class="label">联系电话</text>
<input class="input" v-model="form.mobile" type="number" placeholder="请输入" />
</view>
<view class="field">
<text class="label">详细地址</text>
<input class="input" v-model="form.position" placeholder="请输入" />
</view>
<view class="field">
<text class="label">营业开始</text>
<input class="input" v-model="form.start_time" placeholder="如 08:00" />
</view>
<view class="field">
<text class="label">营业结束</text>
<input class="input" v-model="form.end_time" placeholder="如 20:00" />
</view>
<view class="field">
<text class="label">销售倍率</text>
<input class="input" v-model="form.z_sale_percent" type="digit" placeholder="请输入" />
</view>
<view class="submit-btn" @click="submit">保存</view>
</view>
</store-page-layout>
</template>
<script>
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import { getPlatformStoreDetail, updatePlatformStore } from '@/api/platformStore.js';
export default {
components: { StorePageLayout },
data() {
return {
storeId: 0,
form: {
id: 0,
name: '',
contact: '',
mobile: '',
position: '',
start_time: '',
end_time: '',
z_sale_percent: '',
},
};
},
onLoad(options) {
this.storeId = Number(options.id || 0);
this.loadDetail();
},
methods: {
async loadDetail() {
if (!this.storeId) return;
const wrap = await getPlatformStoreDetail(this.storeId);
if (!wrap || !wrap.ok) return;
const d = wrap.data || {};
this.form = {
id: d.id,
name: d.name || '',
contact: d.contact || '',
mobile: d.mobile || '',
position: d.position || '',
start_time: d.start_time || '',
end_time: d.end_time || '',
z_sale_percent: d.z_sale_percent != null ? String(d.z_sale_percent) : '',
};
},
async submit() {
if (!this.form.name) {
uni.showToast({ title: '请填写诊所名称', icon: 'none' });
return;
}
uni.showLoading({ title: '保存中' });
try {
const wrap = await updatePlatformStore({ ...this.form });
if (wrap && wrap.ok) {
uni.showToast({ title: '保存成功', icon: 'success' });
setTimeout(() => uni.navigateBack(), 500);
}
} finally {
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.field {
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
padding: 24rpx; margin-bottom: 16rpx;
}
.label { width: 180rpx; font-size: 28rpx; flex-shrink: 0; }
.input { flex: 1; font-size: 28rpx; text-align: right; }
.submit-btn {
margin-top: 40rpx; background: linear-gradient(90deg, #00BFA6, #6ACDBB);
color: #fff; text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<store-page-layout title="门店管理" :show-back="true">
<view class="page">
<view class="filter-panel">
<input class="filter-input" v-model="filters.name" placeholder="诊所名称" />
<input class="filter-input" v-model="filters.shouzimu" placeholder="拼音首字母" />
<input class="filter-input" v-model="filters.mobile" placeholder="联系人手机号" />
<view class="filter-btn" @click="reload">查询</view>
</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!list.length" class="empty">暂无门店</view>
<view
v-for="item in list"
:key="item.id"
class="card"
hover-class="card-hover"
@click="goDetail(item)"
>
<view class="card-head">
<text class="name">{{ item.name }}</text>
<text class="type-tag">{{ clinicTypeText(item.clinic_type) }}</text>
</view>
<text class="addr">{{ item.position || '暂无地址' }}</text>
<view class="meta-row">
<text>ID {{ item.id }}</text>
<text v-if="item.mobile">电话 {{ item.mobile }}</text>
</view>
<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>
</view>
</view>
</view>
</store-page-layout>
</template>
<script>
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import { getPlatformStoreList } from '@/api/platformStore.js';
const ROOT = '/subPackages/sub_platform_admin/store';
export default {
components: { StorePageLayout },
data() {
return {
filters: { name: '', shouzimu: '', mobile: '' },
list: [],
loading: false,
};
},
onShow() {
this.reload();
},
methods: {
clinicTypeText(type) {
if (Number(type) === 1) return '西医诊所';
if (Number(type) === 2) return '中医诊所';
return '未设置';
},
async reload() {
this.loading = true;
try {
const wrap = await getPlatformStoreList({
page: 1,
pageSize: 50,
...this.filters,
});
const data = wrap && wrap.ok ? wrap.data : null;
if (data && Array.isArray(data.items)) {
this.list = data.items;
} else if (Array.isArray(data)) {
this.list = data;
} else {
this.list = [];
}
} finally {
this.loading = false;
}
},
goDetail(item) {
uni.navigateTo({ url: `${ROOT}/detail?id=${item.id}` });
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.filter-panel { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 20rpx; }
.filter-input {
flex: 1; min-width: 200rpx; background: #fff; border-radius: 12rpx;
padding: 16rpx 20rpx; font-size: 26rpx;
}
.filter-btn {
background: #6ACDBB; color: #fff; padding: 16rpx 32rpx; border-radius: 12rpx; font-size: 26rpx;
}
.empty { text-align: center; color: #999; padding: 80rpx 0; }
.card {
background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
}
.card-hover { opacity: 0.92; }
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
.name { font-size: 30rpx; font-weight: 600; flex: 1; }
.type-tag { font-size: 22rpx; color: #6ACDBB; background: #f0fbf8; padding: 4rpx 12rpx; border-radius: 8rpx; }
.addr { font-size: 24rpx; color: #666; display: block; margin-bottom: 12rpx; }
.meta-row { display: flex; justify-content: space-between; font-size: 22rpx; color: #999; margin-bottom: 12rpx; }
.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; }
</style>

View File

@@ -0,0 +1,235 @@
<template>
<page-layout :is-platform="ctx.isPlatform" :title="pageTitle" :show-back="true">
<view class="page">
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab"
:class="{ active: activeTab === tab.key }"
@click="switchTab(tab.key)"
>{{ tab.label }}</view>
</view>
<view v-if="activeTab !== 'history'" class="toolbar">
<input class="search" v-model="orderNo" placeholder="订单号" @confirm="loadOrders" />
<view class="search-btn" @click="loadOrders">查询</view>
</view>
<template v-if="activeTab === 'pending'">
<view class="settle-actions">
<view class="settle-btn" @click="goPeriodSettle">按时间段结算</view>
<view class="settle-btn primary" @click="goOrderSettle">结算选中订单</view>
</view>
<view v-for="item in orderList" :key="item.order_id" class="order-row" @click="toggleSelect(item)">
<view class="checkbox" :class="{ checked: selectedIds.includes(item.order_id) }" />
<view class="order-info">
<text class="order-no">{{ item.order_no }}</text>
<text class="sub">明细 {{ item.item_count }} · {{ item.order_pay_at_text }}</text>
</view>
<text class="amount">¥{{ item.commission_amount }}</text>
</view>
</template>
<template v-else-if="activeTab === 'settled'">
<view v-for="item in orderList" :key="item.order_id" class="order-row" @click="showOrderDetail(item)">
<view class="order-info">
<text class="order-no">{{ item.order_no }}</text>
<text class="sub">明细 {{ item.item_count }} · {{ item.order_pay_at_text }}</text>
</view>
<text class="amount">¥{{ item.commission_amount }}</text>
</view>
</template>
<template v-else>
<view v-for="item in settlementList" :key="item.id" class="order-row" @click="goSettlementDetail(item.id)">
<view class="order-info">
<text class="order-no">{{ item.settlement_no }}</text>
<text class="sub">{{ item.settlement_type_text }} · {{ item.created_at_text }}</text>
</view>
<text class="amount">¥{{ item.amount }}</text>
</view>
</template>
<view v-if="!loading && isEmpty" class="empty">暂无数据</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
const ROOT = '/subPackages/sub_salesperson_manage';
export default {
components: { PageLayout },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
salespersonId: 0,
storeId: 0,
nickName: '',
activeTab: 'pending',
tabs: [
{ key: 'pending', label: '待结算' },
{ key: 'settled', label: '已结算' },
{ key: 'history', label: '结算历史' },
],
orderNo: '',
orderList: [],
settlementList: [],
selectedIds: [],
loading: false,
};
},
computed: {
pageTitle() {
return this.nickName ? `分成与结算 - ${this.nickName}` : '分成与结算';
},
isEmpty() {
if (this.activeTab === 'history') return !this.settlementList.length;
return !this.orderList.length;
},
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.salespersonId = Number(options.salesperson_id || 0);
this.storeId = Number(options.store_id || 0);
this.nickName = decodeURIComponent(options.nick_name || '');
this.loadData();
},
methods: {
switchTab(key) {
this.activeTab = key;
this.selectedIds = [];
this.loadData();
},
loadData() {
if (this.activeTab === 'history') this.loadSettlements();
else this.loadOrders();
},
buildParams() {
const params = {
salesperson_id: this.salespersonId,
page: 1,
pageSize: 50,
order_no: this.orderNo,
};
if (this.storeId > 0) params.store_id = this.storeId;
if (this.activeTab === 'pending') params.status = 0;
if (this.activeTab === 'settled') params.status = 1;
return params;
},
async loadOrders() {
this.loading = true;
try {
const wrap = await this.ctx.api.getCommissionOrderList(this.buildParams());
this.orderList = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
} finally {
this.loading = false;
}
},
async loadSettlements() {
this.loading = true;
try {
const params = { salesperson_id: this.salespersonId, page: 1, pageSize: 50 };
if (this.storeId > 0) params.store_id = this.storeId;
const wrap = await this.ctx.api.getSettlementList(params);
this.settlementList = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
} finally {
this.loading = false;
}
},
toggleSelect(item) {
const id = item.order_id;
const idx = this.selectedIds.indexOf(id);
if (idx >= 0) this.selectedIds.splice(idx, 1);
else this.selectedIds.push(id);
},
goPeriodSettle() {
uni.navigateTo({
url: `${ROOT}/settlement/preview?mode=${this.ctx.mode}&salesperson_id=${this.salespersonId}&store_id=${this.storeId}`,
});
},
async goOrderSettle() {
if (!this.selectedIds.length) {
uni.showToast({ title: '请勾选订单', icon: 'none' });
return;
}
const params = {
salesperson_id: this.salespersonId,
settlement_type: 2,
order_ids: this.selectedIds.join(','),
};
if (this.storeId > 0) params.store_id = this.storeId;
uni.showLoading({ title: '预览中' });
try {
const wrap = await this.ctx.api.getSettlementPreview(params);
if (!wrap || !wrap.ok) return;
const preview = wrap.data || {};
const q = [
`mode=${this.ctx.mode}`,
`salesperson_id=${this.salespersonId}`,
`store_id=${this.storeId}`,
'settlement_type=2',
`order_ids=${this.selectedIds.join(',')}`,
`amount=${encodeURIComponent(preview.amount || '0.00')}`,
`record_count=${preview.record_count || 0}`,
].join('&');
uni.navigateTo({ url: `${ROOT}/settlement/confirm?${q}` });
} finally {
uni.hideLoading();
}
},
async showOrderDetail(item) {
const params = {
salesperson_id: this.salespersonId,
order_id: item.order_id,
page: 1,
pageSize: 100,
};
if (this.storeId > 0) params.store_id = this.storeId;
const wrap = await this.ctx.api.getCommissionList(params);
const items = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
const lines = items.map((r) => `${r.drug_info || ''} ¥${r.commission_amount}`).join('\n');
uni.showModal({
title: `订单 ${item.order_no}`,
content: lines || '无明细',
showCancel: false,
});
},
goSettlementDetail(id) {
uni.navigateTo({
url: `${ROOT}/settlement/detail?mode=${this.ctx.mode}&id=${id}&store_id=${this.storeId}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.tabs { display: flex; background: #fff; border-radius: 16rpx; margin-bottom: 20rpx; overflow: hidden; }
.tab { flex: 1; text-align: center; padding: 20rpx; font-size: 26rpx; color: #666; }
.tab.active { background: #6ACDBB; color: #fff; font-weight: 600; }
.toolbar { display: flex; gap: 12rpx; margin-bottom: 16rpx; }
.search { flex: 1; background: #fff; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; }
.search-btn { background: #6ACDBB; color: #fff; padding: 16rpx 24rpx; border-radius: 12rpx; font-size: 26rpx; }
.settle-actions { display: flex; gap: 12rpx; margin-bottom: 16rpx; }
.settle-btn { flex: 1; text-align: center; padding: 16rpx; background: #fff; border-radius: 12rpx; font-size: 26rpx; color: #6ACDBB; border: 1rpx solid #6ACDBB; }
.settle-btn.primary { background: #6ACDBB; color: #fff; border: none; }
.order-row {
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
padding: 24rpx; margin-bottom: 12rpx;
}
.checkbox {
width: 36rpx; height: 36rpx; border: 2rpx solid #ddd; border-radius: 8rpx; margin-right: 16rpx;
}
.checkbox.checked { background: #6ACDBB; border-color: #6ACDBB; }
.order-info { flex: 1; }
.order-no { display: block; font-size: 28rpx; font-weight: 600; }
.sub { font-size: 22rpx; color: #999; }
.amount { font-size: 30rpx; color: #F97316; font-weight: 600; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
</style>

View File

@@ -0,0 +1,23 @@
import { createSalespersonManageApi } from '@/api/salespersonManage.js';
const ROOT = '/subPackages/sub_salesperson_manage';
export function resolveManageMode(options) {
const mode = (options && options.mode) || '';
if (mode === 'platform' || mode === 'clinic') return mode;
const loginMode = uni.getStorageSync('loginMode');
return loginMode === 'platform_admin' ? 'platform' : 'clinic';
}
export function getSalespersonManageContext(mode) {
const api = createSalespersonManageApi(mode);
return {
mode,
api,
isPlatform: mode === 'platform',
listPath: `${ROOT}/index?mode=${mode}`,
homePath: mode === 'platform'
? '/subPackages/sub_platform_admin/home/index'
: '/subPackages/sub_clinic_admin/home/index',
};
}

View File

@@ -0,0 +1,17 @@
export function splitTypeLabel(type) {
return Number(type) === 1 ? '百分比' : '固定金额';
}
export function tcmBaseLabel(type) {
return Number(type) === 1 ? '处方总价' : '药店利润';
}
export const SPLIT_TYPE_OPTIONS = [
{ label: '元/件(固定金额)', value: 0 },
{ label: '百分比(%', value: 1 },
];
export const TCM_BASE_OPTIONS = [
{ label: '药店利润', value: 0 },
{ label: '处方总价', value: 1 },
];

View File

@@ -0,0 +1,81 @@
<template>
<view class="manage-page-layout safe-area-inset-bottom">
<u-navbar
:title="title"
:is-back="showBack"
:is-fixed="true"
:border-bottom="false"
title-size="34"
title-color="#fff"
back-icon-color="#fff"
:background="{ background: 'linear-gradient(90deg, #00BFA6 0%, #6ACDBB 93%)' }"
/>
<view class="page-body">
<slot />
</view>
<u-tabbar
v-if="showTabbar"
:value="tabIndex"
:list="tabList"
active-color="#6ACDBB"
@change="onTabChange"
/>
</view>
</template>
<script>
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js';
import { getClinicTabList, switchClinicTab } from '@/subPackages/sub_clinic_admin/common/tabbar.js';
export default {
name: 'SalespersonManagePageLayout',
props: {
isPlatform: { type: Boolean, default: false },
title: { type: String, default: '' },
showBack: { type: Boolean, default: true },
showTabbar: { type: Boolean, default: false },
tabIndex: { type: Number, default: 0 },
},
provide() {
if (this.isPlatform) {
return { businessContext: getPlatformAdminContext() };
}
return {};
},
data() {
return {
tabList: [],
};
},
created() {
if (this.showTabbar) {
this.tabList = this.isPlatform
? (getPlatformAdminContext().tabList || [])
: getClinicTabList();
}
},
methods: {
onTabChange(index) {
if (index === this.tabIndex) return;
const target = this.tabList[index];
if (!target || !target.pagePath) return;
if (this.isPlatform) {
uni.reLaunch({ url: target.pagePath });
} else {
switchClinicTab(index);
}
},
},
};
</script>
<style lang="scss" scoped>
.manage-page-layout {
min-height: 100vh;
background: #f9fafb;
}
.page-body {
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<view class="proof-uploader">
<view class="label">结算凭证至少1张</view>
<view class="image-list">
<view v-for="(url, idx) in images" :key="url" class="image-item">
<image class="thumb" :src="url" mode="aspectFill" @click="preview(idx)" />
<view class="remove" @click="remove(idx)">×</view>
</view>
<view v-if="images.length < maxCount" class="add-btn" @click="chooseImage">
<u-icon name="plus" size="40" color="#999" />
</view>
</view>
</view>
</template>
<script>
import { uploadSalespersonManageFileApi, extractUploadUrl } from '@/api/upload.js';
export default {
name: 'ProofImageUploader',
props: {
value: { type: Array, default: () => [] },
maxCount: { type: Number, default: 5 },
},
computed: {
images: {
get() { return this.value || []; },
set(v) { this.$emit('input', v); },
},
},
methods: {
chooseImage() {
const remain = this.maxCount - this.images.length;
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
success: async (res) => {
const paths = res.tempFilePaths || [];
for (const filePath of paths) {
try {
uni.showLoading({ title: '上传中' });
const uploadRes = await uploadSalespersonManageFileApi({ filePath });
const url = extractUploadUrl(uploadRes);
if (url) {
this.$emit('input', [...this.images, url]);
}
} catch (e) {
uni.showToast({ title: '上传失败', icon: 'none' });
} finally {
uni.hideLoading();
}
}
},
});
},
remove(idx) {
const next = [...this.images];
next.splice(idx, 1);
this.$emit('input', next);
},
preview(idx) {
uni.previewImage({ urls: this.images, current: this.images[idx] });
},
},
};
</script>
<style lang="scss" scoped>
.proof-uploader { margin-top: 24rpx; }
.label { font-size: 26rpx; color: #666; margin-bottom: 16rpx; }
.image-list { display: flex; flex-wrap: wrap; gap: 16rpx; }
.image-item { position: relative; width: 160rpx; height: 160rpx; }
.thumb { width: 100%; height: 100%; border-radius: 12rpx; }
.remove {
position: absolute; top: -8rpx; right: -8rpx;
width: 36rpx; height: 36rpx; background: #ef4444; color: #fff;
border-radius: 50%; text-align: center; line-height: 36rpx; font-size: 28rpx;
}
.add-btn {
width: 160rpx; height: 160rpx; border: 2rpx dashed #ddd; border-radius: 12rpx;
display: flex; align-items: center; justify-content: center; background: #fafafa;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<view class="store-picker">
<view class="picker-row" @click="openPicker">
<text class="label">所属诊所</text>
<text class="value" :class="{ placeholder: !selectedLabel }">{{ selectedLabel || '请选择诊所' }}</text>
<u-icon name="arrow-right" size="28" color="#999" />
</view>
<u-popup v-model="visible" mode="bottom" border-radius="24" height="70%">
<view class="popup-inner">
<view class="popup-title">选择诊所</view>
<input class="search-input" v-model="keyword" placeholder="搜索诊所名称" @input="onSearch" />
<scroll-view scroll-y class="option-list">
<view
v-for="item in options"
:key="item.id"
class="option-item"
@click="select(item)"
>
<text>{{ item.name }}</text>
<text v-if="item.position" class="sub">{{ item.position }}</text>
</view>
<view v-if="!loading && !options.length" class="empty">暂无诊所</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
export default {
name: 'StoreSearchPicker',
props: {
value: { type: [Number, String], default: 0 },
api: { type: Object, required: true },
},
data() {
return {
visible: false,
keyword: '',
options: [],
loading: false,
selectedLabel: '',
searchTimer: null,
};
},
methods: {
openPicker() {
this.visible = true;
this.fetchOptions('');
},
onSearch() {
if (this.searchTimer) clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(() => this.fetchOptions(this.keyword.trim()), 300);
},
async fetchOptions(keyword) {
this.loading = true;
try {
const wrap = await this.api.getPlatformStoreOptions(keyword);
this.options = (wrap && wrap.ok && wrap.data) ? wrap.data : [];
} finally {
this.loading = false;
}
},
select(item) {
this.selectedLabel = item.position ? `${item.name}${item.position}` : item.name;
this.$emit('input', item.id);
this.visible = false;
},
},
};
</script>
<style lang="scss" scoped>
.picker-row {
display: flex; align-items: center; padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.label { width: 180rpx; font-size: 28rpx; color: #333; }
.value { flex: 1; font-size: 28rpx; text-align: right; margin-right: 8rpx; }
.placeholder { color: #bbb; }
.popup-inner { padding: 32rpx; height: 100%; display: flex; flex-direction: column; }
.popup-title { font-size: 32rpx; font-weight: 600; margin-bottom: 24rpx; }
.search-input {
background: #f5f5f5; border-radius: 12rpx; padding: 16rpx 24rpx; margin-bottom: 16rpx;
}
.option-list { flex: 1; }
.option-item {
padding: 24rpx 0; border-bottom: 1rpx solid #f0f0f0;
display: flex; flex-direction: column;
}
.sub { font-size: 24rpx; color: #999; margin-top: 8rpx; }
.empty { text-align: center; color: #999; padding: 60rpx 0; }
</style>

View File

@@ -0,0 +1,175 @@
<template>
<page-layout :is-platform="ctx.isPlatform" title="药品佣金配置" :show-back="true">
<view class="page">
<view class="toolbar">
<input class="search" v-model="keyword" placeholder="搜索药品" @confirm="reload" />
<view class="search-btn" @click="reload">搜索</view>
</view>
<view v-if="salespersonId > 0" class="copy-row">
<picker :range="copyLabels" :value="copyIndex" @change="onCopyFrom">
<view class="copy-picker">{{ copyLabels[copyIndex] || '从其他推广员复制' }}</view>
</picker>
</view>
<view v-if="!list.length && !loading" class="empty">暂无药品</view>
<view v-for="item in list" :key="item.drug_id" class="row">
<view class="drug-info">
<text class="drug-name">{{ item.drug_name }}</text>
<text class="spec">{{ item.drug_spec }}</text>
<text class="price">售价 ¥{{ item.price }} / 供货 ¥{{ item.buy_price }}</text>
</view>
<input
class="commission-input"
v-model="dirtyMap[item.drug_id]"
type="digit"
placeholder="元/件"
@input="onCommissionInput(item.drug_id, $event)"
/>
</view>
<view v-if="salespersonId > 0" class="save-btn" @click="save">保存修改</view>
<view v-else class="hint">请先保存推广员基本信息后再配置药品佣金</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
export default {
components: { PageLayout },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
salespersonId: 0,
storeId: 0,
keyword: '',
list: [],
loading: false,
dirtyMap: {},
copyOptions: [],
copyIndex: 0,
};
},
computed: {
copyLabels() {
return ['从其他推广员复制', ...this.copyOptions.map((o) => o.label)];
},
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.salespersonId = Number(options.salesperson_id || 0);
this.storeId = Number(options.store_id || 0);
this.reload();
if (this.salespersonId > 0) this.loadCopyOptions();
},
methods: {
async reload() {
if (!this.salespersonId) return;
this.loading = true;
try {
const params = {
salesperson_id: this.salespersonId,
keyword: this.keyword,
page: 1,
pageSize: 100,
};
if (this.storeId > 0) params.store_id = this.storeId;
const wrap = await this.ctx.api.getDrugCommissionList(params);
const items = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
this.list = items;
const map = { ...this.dirtyMap };
items.forEach((row) => {
if (map[row.drug_id] === undefined) {
map[row.drug_id] = row.commission || '';
}
});
this.dirtyMap = map;
} finally {
this.loading = false;
}
},
async loadCopyOptions() {
const wrap = await this.ctx.api.getList({ page: 1, pageSize: 100 });
if (!wrap || !wrap.ok) return;
this.copyOptions = (wrap.data.items || [])
.filter((sp) => Number(sp.id) !== this.salespersonId)
.map((sp) => ({
label: sp.nick_name || sp.phone || `推广员#${sp.id}`,
value: Number(sp.id),
}));
},
onCommissionInput(drugId, e) {
this.$set(this.dirtyMap, drugId, e.detail.value);
},
async onCopyFrom(e) {
const idx = Number(e.detail.value);
this.copyIndex = idx;
if (idx <= 0 || !this.copyOptions[idx - 1]) return;
const fromId = this.copyOptions[idx - 1].value;
const wrap = await this.ctx.api.getDrugCommissionCopyFrom(fromId);
if (!wrap || !wrap.ok) return;
const items = wrap.data.items || [];
const map = { ...this.dirtyMap };
items.forEach((row) => {
map[row.drug_id] = row.commission || '';
});
this.dirtyMap = map;
uni.showToast({ title: '已复制', icon: 'success' });
},
async save() {
const items = Object.keys(this.dirtyMap)
.filter((drugId) => this.dirtyMap[drugId] !== '' && this.dirtyMap[drugId] != null)
.map((drugId) => ({
drug_id: Number(drugId),
commission: String(this.dirtyMap[drugId]),
}));
if (!items.length) {
uni.showToast({ title: '无修改', icon: 'none' });
return;
}
uni.showLoading({ title: '保存中' });
try {
const wrap = await this.ctx.api.saveDrugCommission({
salesperson_id: this.salespersonId,
items,
});
if (wrap && wrap.ok) {
uni.showToast({ title: '保存成功', icon: 'success' });
}
} finally {
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; padding-bottom: 120rpx; }
.toolbar { display: flex; gap: 12rpx; margin-bottom: 16rpx; }
.search { flex: 1; background: #fff; border-radius: 12rpx; padding: 16rpx 20rpx; font-size: 26rpx; }
.search-btn { background: #6ACDBB; color: #fff; padding: 16rpx 24rpx; border-radius: 12rpx; font-size: 26rpx; }
.copy-row { margin-bottom: 16rpx; }
.copy-picker { background: #fff; padding: 20rpx; border-radius: 12rpx; font-size: 26rpx; color: #6ACDBB; }
.row {
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
padding: 20rpx; margin-bottom: 12rpx;
}
.drug-info { flex: 1; }
.drug-name { display: block; font-size: 28rpx; font-weight: 600; }
.spec, .price { font-size: 22rpx; color: #999; display: block; margin-top: 4rpx; }
.commission-input {
width: 140rpx; text-align: center; background: #f5f5f5; border-radius: 8rpx;
padding: 12rpx; font-size: 26rpx;
}
.save-btn {
position: fixed; left: 24rpx; right: 24rpx; bottom: 48rpx;
background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
}
.hint { text-align: center; color: #999; padding: 40rpx; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
</style>

View File

@@ -0,0 +1,172 @@
<template>
<page-layout :is-platform="ctx.isPlatform" :title="isUpdate ? '编辑推广员' : '新增推广员'" :show-back="true">
<view class="page">
<store-search-picker v-if="ctx.isPlatform && !isUpdate" v-model="form.store_id" :api="ctx.api" />
<view class="field">
<text class="label">手机号</text>
<input class="input" v-model="form.phone" type="number" maxlength="11" placeholder="请输入手机号" :disabled="isUpdate" />
</view>
<view class="field">
<text class="label">名称</text>
<input class="input" v-model="form.nick_name" placeholder="请输入名称" />
</view>
<view class="field">
<text class="label">西药分成类型</text>
<picker :range="splitTypeLabels" :value="splitTypeIndex" @change="onSplitType">
<view class="picker-val">{{ splitTypeLabels[splitTypeIndex] }}</view>
</picker>
</view>
<view v-if="Number(form.split_type) === 1" class="field">
<text class="label">分成比例%</text>
<input class="input" v-model="form.split" type="digit" placeholder="0-100" />
</view>
<view v-else class="field link" @click="goDrugCommission">
<text class="label">药品佣金配置</text>
<text class="link-text">去配置元/件分成 ></text>
</view>
<view class="section-title">中药推广分成</view>
<view class="field">
<text class="label">中药分成%</text>
<input class="input" v-model="form.tcm_split" type="digit" placeholder="0 表示不分成" />
</view>
<view v-if="Number(form.tcm_split) > 0" class="field">
<text class="label">中药分成基数</text>
<picker :range="tcmBaseLabels" :value="tcmBaseIndex" @change="onTcmBase">
<view class="picker-val">{{ tcmBaseLabels[tcmBaseIndex] }}</view>
</picker>
</view>
<view class="submit-btn" @click="submit">保存</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from './components/PageLayout.vue';
import StoreSearchPicker from './components/StoreSearchPicker.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 },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
isUpdate: false,
form: {
id: 0,
store_id: 0,
phone: '',
nick_name: '',
split_type: 0,
split: 0,
tcm_split: 0,
tcm_base_type: 0,
},
splitTypeLabels: SPLIT_TYPE_OPTIONS.map((o) => o.label),
tcmBaseLabels: TCM_BASE_OPTIONS.map((o) => o.label),
};
},
computed: {
splitTypeIndex() {
return SPLIT_TYPE_OPTIONS.findIndex((o) => o.value === Number(this.form.split_type));
},
tcmBaseIndex() {
return TCM_BASE_OPTIONS.findIndex((o) => o.value === Number(this.form.tcm_base_type));
},
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.isUpdate = !!options.id;
if (options.id) {
this.form.id = Number(options.id);
this.form.store_id = Number(options.store_id || 0);
this.loadDetail(options);
}
},
methods: {
async loadDetail(options) {
const wrap = await this.ctx.api.getList({ page: 1, pageSize: 100 });
if (!wrap || !wrap.ok) return;
const item = (wrap.data.items || []).find((r) => Number(r.id) === Number(options.id));
if (!item) return;
this.form.phone = item.phone || '';
this.form.nick_name = item.nick_name || '';
this.form.split_type = Number(item.split_type) || 0;
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;
if (!this.form.store_id && item.qr_code) {
this.form.store_id = item.qr_code.store_id || 0;
}
},
onSplitType(e) {
const idx = Number(e.detail.value);
this.form.split_type = SPLIT_TYPE_OPTIONS[idx].value;
},
onTcmBase(e) {
const idx = Number(e.detail.value);
this.form.tcm_base_type = TCM_BASE_OPTIONS[idx].value;
},
goDrugCommission() {
const id = this.form.id || '';
const storeId = this.form.store_id || '';
uni.navigateTo({
url: `${ROOT}/drug-commission/index?mode=${this.ctx.mode}&salesperson_id=${id}&store_id=${storeId}`,
});
},
async submit() {
if (!this.form.phone || !this.form.nick_name) {
uni.showToast({ title: '请填写手机号和名称', icon: 'none' });
return;
}
if (this.ctx.isPlatform && !this.isUpdate && !this.form.store_id) {
uni.showToast({ title: '请选择诊所', icon: 'none' });
return;
}
const payload = { ...this.form };
if (Number(payload.split_type) === 0) payload.split = 0;
uni.showLoading({ title: '保存中' });
try {
const wrap = this.isUpdate
? await this.ctx.api.update(payload)
: await this.ctx.api.create(payload);
if (wrap && wrap.ok) {
if (!this.isUpdate && wrap.data && wrap.data.admin_account_created === false) {
uni.showToast({ title: '已创建,请手动开通后台', icon: 'none', duration: 2500 });
} else {
uni.showToast({ title: '保存成功', icon: 'success' });
}
setTimeout(() => uni.navigateBack(), 500);
}
} finally {
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.field {
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
padding: 24rpx; margin-bottom: 16rpx;
}
.field.link { justify-content: space-between; }
.label { width: 200rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
.input { flex: 1; font-size: 28rpx; text-align: right; }
.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; }
.submit-btn {
margin-top: 40rpx; background: linear-gradient(90deg, #00BFA6, #6ACDBB);
color: #fff; text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
}
</style>

View File

@@ -0,0 +1,226 @@
<template>
<page-layout :is-platform="ctx.isPlatform" title="推广员管理" :show-back="true">
<view class="page">
<view v-if="ctx.isPlatform" class="filter-panel">
<input class="filter-input" v-model="filters.store_keyword" placeholder="诊所名称" />
<input class="filter-input" v-model="filters.nick_name" placeholder="推广员名称" />
<input class="filter-input" v-model="filters.phone" placeholder="手机号" />
<view class="filter-btn" @click="reload">查询</view>
</view>
<view v-if="!ctx.isPlatform" class="see-price-row">
<text>推广员可见价格</text>
<switch :checked="seePrice === 1" @change="onToggleSeePrice" color="#6ACDBB" />
</view>
<view class="add-bar">
<view class="add-btn" @click="goCreate">+ 新增推广员</view>
</view>
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="!list.length" class="empty">暂无推广员</view>
<view v-for="item in list" :key="item.id" class="card">
<view class="card-head">
<view>
<text class="name">{{ item.nick_name || '未知' }}</text>
<text v-if="item.store_name" class="store">{{ item.store_name }}</text>
</view>
<view class="tags">
<text class="tag">{{ splitLabel(item.split_type) }}</text>
<text v-if="Number(item.tcm_split) > 0" class="tag tcm">中药 {{ item.tcm_split }}%</text>
</view>
</view>
<view class="card-body">
<text class="phone">{{ item.phone }}</text>
<view class="stat-row">
<text>累计获客</text>
<text class="num">{{ (item.count && item.count.user_number) || 0 }}</text>
</view>
<view class="stat-row">
<text>待结算</text>
<text class="num pending">¥{{ (item.count && item.count.pending_amount) || '0.00' }}</text>
</view>
<view class="stat-row">
<text>后台账号</text>
<text :class="item.has_admin_account ? 'ok' : 'warn'">{{ item.has_admin_account ? '已开通' : '未开通' }}</text>
</view>
</view>
<view class="actions">
<view class="action-btn" @click="goEdit(item)">编辑</view>
<view class="action-btn primary" @click="goCommission(item)">分成与结算</view>
<view v-if="item.qr_code && item.qr_code.qr_code" class="action-btn" @click="openQr(item)">二维码</view>
<view class="action-btn" @click="goLeads(item)">获客</view>
<view
v-if="!item.has_admin_account"
class="action-btn warn"
@click="openAdminAccount(item)"
>开通后台</view>
</view>
</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from './components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from './common/context.js';
import { splitTypeLabel } from './common/labels.js';
import { openQrCodePreview } from '@/utils/qrCodePreview.js';
const ROOT = '/subPackages/sub_salesperson_manage';
export default {
components: { PageLayout },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
filters: { store_keyword: '', nick_name: '', phone: '' },
presetStoreId: 0,
list: [],
loading: false,
seePrice: 0,
openingId: 0,
};
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.presetStoreId = Number(options.store_id || 0);
if (this.presetStoreId > 0) {
this.filters.store_keyword = '';
}
},
onShow() {
this.reload();
if (!this.ctx.isPlatform) this.loadSeePrice();
},
methods: {
splitLabel: splitTypeLabel,
async reload() {
this.loading = true;
try {
const params = {
page: 1,
pageSize: 50,
...this.filters,
};
if (this.presetStoreId > 0) params.store_id = this.presetStoreId;
const wrap = await this.ctx.api.getList(params);
this.list = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
} finally {
this.loading = false;
}
},
async loadSeePrice() {
const wrap = await this.ctx.api.getSeePriceDetail();
if (wrap && wrap.ok && wrap.data) {
this.seePrice = Number(wrap.data.see_price) || 0;
}
},
async onToggleSeePrice(e) {
const wrap = await this.ctx.api.toggleSeePrice();
if (wrap && wrap.ok && wrap.data) {
this.seePrice = Number(wrap.data.see_price) || 0;
uni.showToast({ title: this.seePrice === 1 ? '已开启可见价格' : '已关闭可见价格', icon: 'none' });
}
},
goCreate() {
uni.navigateTo({ url: `${ROOT}/form?mode=${this.ctx.mode}` });
},
goEdit(item) {
uni.navigateTo({
url: `${ROOT}/form?mode=${this.ctx.mode}&id=${item.id}&store_id=${(item.qr_code && item.qr_code.store_id) || ''}`,
});
},
goCommission(item) {
const storeId = (item.qr_code && item.qr_code.store_id) || '';
uni.navigateTo({
url: `${ROOT}/commission/index?mode=${this.ctx.mode}&salesperson_id=${item.id}&nick_name=${encodeURIComponent(item.nick_name || '')}&store_id=${storeId}`,
});
},
goLeads(item) {
uni.navigateTo({
url: `${ROOT}/leads/index?mode=${this.ctx.mode}&salesperson_id=${item.id}&nick_name=${encodeURIComponent(item.nick_name || '')}`,
});
},
openQr(item) {
openQrCodePreview({
type: 'salesperson',
title: item.nick_name || '推广员二维码',
payload: {
nick_name: item.nick_name || '',
qr_code: {
qr_code: (item.qr_code && item.qr_code.qr_code) || '',
store: { name: item.store_name || '' },
},
},
});
},
async openAdminAccount(item) {
if (this.openingId) return;
this.openingId = item.id;
try {
const wrap = await this.ctx.api.openAdminAccount({
salesperson_id: item.id,
store_id: item.qr_code && item.qr_code.store_id,
});
if (wrap && wrap.ok) {
uni.showModal({
title: '开通成功',
content: '默认密码Xk123456@',
showCancel: false,
});
this.reload();
}
} finally {
this.openingId = 0;
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; padding-bottom: 48rpx; }
.filter-panel { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 20rpx; }
.filter-input {
flex: 1; min-width: 200rpx; background: #fff; border-radius: 12rpx;
padding: 16rpx 20rpx; font-size: 26rpx;
}
.filter-btn {
background: #6ACDBB; color: #fff; padding: 16rpx 32rpx; border-radius: 12rpx; font-size: 26rpx;
}
.see-price-row {
display: flex; justify-content: space-between; align-items: center;
background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; font-size: 28rpx;
}
.add-bar { margin-bottom: 20rpx; }
.add-btn {
background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
text-align: center; padding: 20rpx; border-radius: 16rpx; font-size: 28rpx;
}
.loading-tip, .empty { text-align: center; color: #999; padding: 80rpx 0; }
.card {
background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
}
.card-head { display: flex; justify-content: space-between; margin-bottom: 16rpx; }
.name { display: block; font-size: 32rpx; font-weight: 600; }
.store { font-size: 24rpx; color: #999; }
.tags { display: flex; flex-direction: column; gap: 8rpx; align-items: flex-end; }
.tag { font-size: 22rpx; background: #EFF6FF; color: #3B82F6; padding: 4rpx 12rpx; border-radius: 8rpx; }
.tag.tcm { background: #F0FDF4; color: #22C55E; }
.phone { font-size: 26rpx; color: #666; display: block; margin-bottom: 12rpx; }
.stat-row { display: flex; justify-content: space-between; font-size: 26rpx; margin-bottom: 8rpx; }
.num { font-weight: 600; }
.pending { color: #F97316; }
.ok { color: #22C55E; }
.warn { color: #EF4444; }
.actions { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 20rpx; }
.action-btn {
font-size: 24rpx; padding: 10rpx 20rpx; border-radius: 10rpx;
background: #f5f5f5; color: #333;
}
.action-btn.primary { background: #6ACDBB; color: #fff; }
.action-btn.warn { background: #FEF3C7; color: #D97706; }
</style>

View File

@@ -0,0 +1,71 @@
<template>
<page-layout :is-platform="ctx.isPlatform" :title="pageTitle" :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">
<view class="user">
<text class="name">{{ (item.user && item.user.nickname) || '用户' }}</text>
<text class="mobile">{{ (item.user && item.user.mobile) || '' }}</text>
</view>
<text class="time">{{ item.created_at_text || '' }}</text>
</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
export default {
components: { PageLayout },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
salespersonId: 0,
nickName: '',
list: [],
loading: false,
};
},
computed: {
pageTitle() {
return this.nickName ? `获客 - ${this.nickName}` : '获客记录';
},
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.salespersonId = Number(options.salesperson_id || 0);
this.nickName = decodeURIComponent(options.nick_name || '');
this.loadList();
},
methods: {
async loadList() {
if (!this.salespersonId) return;
this.loading = true;
try {
const wrap = await this.ctx.api.getUserBindList({
salesperson_id: this.salespersonId,
page: 1,
pageSize: 50,
});
this.list = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
} finally {
this.loading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
.row {
display: flex; justify-content: space-between; align-items: center;
background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 12rpx;
}
.name { display: block; font-size: 28rpx; font-weight: 600; }
.mobile { font-size: 24rpx; color: #999; }
.time { font-size: 24rpx; color: #bbb; }
</style>

View File

@@ -0,0 +1,122 @@
<template>
<page-layout :is-platform="ctx.isPlatform" title="确认结算" :show-back="true">
<view class="page">
<view class="preview-card">
<text> {{ recordCount }} 条明细</text>
<text class="amount">结算金额 ¥{{ amount }}</text>
</view>
<proof-image-uploader v-model="proofImages" />
<view class="field">
<text class="label">备注</text>
<input class="input" v-model="remark" placeholder="选填" />
</view>
<view class="submit-btn" @click="confirm">确认结算</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from '../components/PageLayout.vue';
import ProofImageUploader from '../components/ProofImageUploader.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
export default {
components: { PageLayout, ProofImageUploader },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
salespersonId: 0,
storeId: 0,
settlementType: 1,
periodStart: 0,
periodEnd: 0,
orderIds: [],
amount: '0.00',
recordCount: 0,
proofImages: [],
remark: '',
};
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.salespersonId = Number(options.salesperson_id || 0);
this.storeId = Number(options.store_id || 0);
this.settlementType = Number(options.settlement_type || 1);
this.periodStart = Number(options.period_start || 0);
this.periodEnd = Number(options.period_end || 0);
this.amount = decodeURIComponent(options.amount || '0.00');
this.recordCount = Number(options.record_count || 0);
if (options.order_ids) {
this.orderIds = String(options.order_ids).split(',').map(Number).filter(Boolean);
}
if (!this.recordCount || !this.amount || this.amount === '0.00') {
this.loadPreview();
}
},
methods: {
buildParams() {
const params = {
salesperson_id: this.salespersonId,
settlement_type: this.settlementType,
};
if (this.storeId > 0) params.store_id = this.storeId;
if (this.settlementType === 1) {
params.period_start = this.periodStart;
params.period_end = this.periodEnd;
} else {
params.order_ids = this.orderIds.join(',');
}
return params;
},
async loadPreview() {
const wrap = await this.ctx.api.getSettlementPreview(this.buildParams());
if (wrap && wrap.ok && wrap.data) {
this.amount = wrap.data.amount || '0.00';
this.recordCount = wrap.data.record_count || 0;
}
},
async confirm() {
if (!this.proofImages.length) {
uni.showToast({ title: '请上传至少1张结算凭证', icon: 'none' });
return;
}
uni.showLoading({ title: '结算中' });
try {
const wrap = await this.ctx.api.confirmSettlement({
...this.buildParams(),
proof_images: this.proofImages,
remark: this.remark.trim(),
});
if (wrap && wrap.ok) {
uni.showToast({ title: '结算成功', icon: 'success' });
setTimeout(() => uni.navigateBack({ delta: 2 }), 500);
}
} finally {
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.preview-card {
background: #FFF7ED; border-radius: 16rpx; padding: 28rpx; margin-bottom: 24rpx;
font-size: 26rpx;
}
.amount { display: block; font-size: 36rpx; font-weight: 700; color: #F97316; margin-top: 12rpx; }
.field {
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
padding: 24rpx; margin-top: 24rpx;
}
.label { width: 100rpx; font-size: 28rpx; }
.input { flex: 1; font-size: 28rpx; text-align: right; }
.submit-btn {
margin-top: 48rpx; background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<page-layout :is-platform="ctx.isPlatform" title="结算详情" :show-back="true">
<view class="page" v-if="detail">
<view class="card">
<text class="no">{{ detail.settlement_no }}</text>
<view class="row"><text>结算方式</text><text>{{ detail.settlement_type_text }}</text></view>
<view class="row"><text>结算金额</text><text class="amount">¥{{ detail.amount }}</text></view>
<view class="row"><text>明细数</text><text>{{ detail.record_count }}</text></view>
<view class="row"><text>时间</text><text>{{ detail.created_at_text }}</text></view>
<view v-if="detail.remark" class="row"><text>备注</text><text>{{ detail.remark }}</text></view>
</view>
<view v-if="proofImages.length" class="proof-section">
<text class="section-title">结算凭证</text>
<view class="proof-list">
<image
v-for="(url, idx) in proofImages"
:key="url"
class="proof-img"
:src="url"
mode="aspectFill"
@click="previewProof(idx)"
/>
</view>
</view>
<view v-if="lines.length" class="lines-section">
<text class="section-title">明细</text>
<view v-for="(line, idx) in lines" :key="idx" class="line-row">
<text class="line-order">{{ line.order_no }}</text>
<text class="line-drug">{{ line.drug_info }}</text>
<text class="line-amount">¥{{ line.commission_amount }}</text>
</view>
</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
export default {
components: { PageLayout },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
detail: null,
lines: [],
proofImages: [],
};
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.loadDetail(Number(options.id || 0), Number(options.store_id || 0));
},
methods: {
async loadDetail(id, storeId) {
if (!id) return;
const params = { id };
if (storeId > 0) params.store_id = storeId;
const wrap = await this.ctx.api.getSettlementDetail(params);
if (!wrap || !wrap.ok) return;
this.detail = wrap.data;
this.lines = (wrap.data && wrap.data.lines) ? wrap.data.lines : (wrap.data.records || []);
const proofs = wrap.data && wrap.data.proof_images;
if (Array.isArray(proofs)) {
this.proofImages = proofs;
} else if (typeof proofs === 'string' && proofs) {
try { this.proofImages = JSON.parse(proofs); } catch (e) { this.proofImages = [proofs]; }
} else {
this.proofImages = [];
}
},
previewProof(idx) {
uni.previewImage({ urls: this.proofImages, current: this.proofImages[idx] });
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.card { background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 24rpx; }
.no { display: block; font-size: 32rpx; font-weight: 600; margin-bottom: 20rpx; }
.row { display: flex; justify-content: space-between; font-size: 26rpx; margin-bottom: 12rpx; color: #666; }
.amount { color: #F97316; font-weight: 600; }
.section-title { font-size: 28rpx; font-weight: 600; margin-bottom: 16rpx; display: block; }
.proof-list { display: flex; flex-wrap: wrap; gap: 16rpx; margin-bottom: 24rpx; }
.proof-img { width: 200rpx; height: 200rpx; border-radius: 12rpx; }
.line-row {
background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 12rpx;
}
.line-order { display: block; font-size: 26rpx; font-weight: 600; }
.line-drug { font-size: 24rpx; color: #666; display: block; margin: 8rpx 0; }
.line-amount { font-size: 26rpx; color: #F97316; }
</style>

View File

@@ -0,0 +1,114 @@
<template>
<page-layout :is-platform="ctx.isPlatform" title="按时间段结算" :show-back="true">
<view class="page">
<view class="field">
<text class="label">开始日期</text>
<picker mode="date" :value="dateStart" @change="onStart">
<view class="picker-val">{{ dateStart || '请选择' }}</view>
</picker>
</view>
<view class="field">
<text class="label">结束日期</text>
<picker mode="date" :value="dateEnd" @change="onEnd">
<view class="picker-val">{{ dateEnd || '请选择' }}</view>
</picker>
</view>
<view v-if="preview" class="preview-card">
<text> {{ preview.record_count }} 条明细</text>
<text v-if="preview.order_count">涉及 {{ preview.order_count }} 个订单</text>
<text class="amount">结算金额 ¥{{ preview.amount }}</text>
</view>
<view class="submit-btn" @click="goConfirm">下一步上传凭证</view>
</view>
</page-layout>
</template>
<script>
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
const ROOT = '/subPackages/sub_salesperson_manage';
export default {
components: { PageLayout },
data() {
return {
ctx: getSalespersonManageContext('clinic'),
salespersonId: 0,
storeId: 0,
dateStart: '',
dateEnd: '',
preview: null,
};
},
onLoad(options) {
this.ctx = getSalespersonManageContext(resolveManageMode(options));
this.salespersonId = Number(options.salesperson_id || 0);
this.storeId = Number(options.store_id || 0);
},
methods: {
onStart(e) { this.dateStart = e.detail.value; },
onEnd(e) { this.dateEnd = e.detail.value; },
toUnixStart(dateStr) {
return Math.floor(new Date(`${dateStr} 00:00:00`).getTime() / 1000);
},
toUnixEnd(dateStr) {
return Math.floor(new Date(`${dateStr} 23:59:59`).getTime() / 1000);
},
async goConfirm() {
if (!this.dateStart || !this.dateEnd) {
uni.showToast({ title: '请选择时间段', icon: 'none' });
return;
}
const params = {
salesperson_id: this.salespersonId,
settlement_type: 1,
period_start: this.toUnixStart(this.dateStart),
period_end: this.toUnixEnd(this.dateEnd),
};
if (this.storeId > 0) params.store_id = this.storeId;
uni.showLoading({ title: '预览中' });
try {
const wrap = await this.ctx.api.getSettlementPreview(params);
if (!wrap || !wrap.ok) return;
this.preview = wrap.data;
const q = [
`mode=${this.ctx.mode}`,
`salesperson_id=${this.salespersonId}`,
`store_id=${this.storeId}`,
'settlement_type=1',
`period_start=${params.period_start}`,
`period_end=${params.period_end}`,
`amount=${encodeURIComponent(this.preview.amount || '')}`,
`record_count=${this.preview.record_count || 0}`,
].join('&');
uni.navigateTo({ url: `${ROOT}/settlement/confirm?${q}` });
} finally {
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.field {
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
padding: 24rpx; margin-bottom: 16rpx;
}
.label { width: 160rpx; font-size: 28rpx; }
.picker-val { flex: 1; text-align: right; font-size: 28rpx; }
.preview-card {
background: #FFF7ED; border-radius: 16rpx; padding: 28rpx; margin: 24rpx 0;
display: flex; flex-direction: column; gap: 8rpx; font-size: 26rpx;
}
.amount { font-size: 36rpx; font-weight: 700; color: #F97316; margin-top: 8rpx; }
.submit-btn {
background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx; margin-top: 40rpx;
}
</style>

View File

@@ -71,7 +71,12 @@
<view class="selected-indicator" v-if="isInSelected(item)"></view>
<d-text :text="getDrugName(item)" className="fs-32 font-bold color-title"></d-text>
</view>
<d-text :text="`¥${parseFloat(item.price || 0).toFixed(2)}/${getUnitName(item)}`" className="fs-32 font-bold color-price"></d-text>
<d-text
v-if="showDrugPrice"
:text="`¥${parseFloat(item.price || 0).toFixed(2)}/${getUnitName(item)}`"
className="fs-32 font-bold color-price"
></d-text>
<d-text v-else text="价格不可见" className="fs-26 color-sub"></d-text>
</view>
<view class="drug-info">
<view class="specification-row flex-row flex-jus-sp flex-ali-center m-b-24">
@@ -174,6 +179,7 @@
<script>
// 保留所有原有逻辑代码与注释
import { getProductListDoctorReception, getChineseMedicineQuickGramsApi } from '@/api/reception.js';
import { getClinicSalespersonChineseDrugList, getClinicSalespersonChineseMedicineQuickGrams } from '@/api/clinicSalesperson.js';
import { withWxKey } from '@/utils/wxListKey.js';
export default {
@@ -207,6 +213,16 @@ export default {
seeRate: {
type: [String, Number],
default: 0
},
/** 推广员传方模式:使用推广员中药列表接口 */
salespersonTransferMode: {
type: Boolean,
default: false
},
/** 是否展示药品单价(传方 see_price=0 时为 false */
showDrugPrice: {
type: Boolean,
default: true
}
},
data() {
@@ -254,16 +270,22 @@ export default {
*/
async fetchQuickGrams() {
try {
const res = await getChineseMedicineQuickGramsApi();
if (res && (res.code === 0 || res.errcode === 0)) {
const data = res.data || res.result || {};
const list = Array.isArray(data.grams) ? data.grams : (Array.isArray(data) ? data : []);
this.commonQuantities = list
.map((v) => parseFloat(v))
.filter((n) => !Number.isNaN(n) && n > 0);
let data = {};
if (this.salespersonTransferMode) {
const wrap = await getClinicSalespersonChineseMedicineQuickGrams();
if (wrap && wrap.ok) {
data = wrap.data || {};
}
} else {
this.commonQuantities = [];
const res = await getChineseMedicineQuickGramsApi();
if (res && (res.code === 0 || res.errcode === 0)) {
data = res.data || res.result || {};
}
}
const list = Array.isArray(data.grams) ? data.grams : (Array.isArray(data) ? data : []);
this.commonQuantities = list
.map((v) => parseFloat(v))
.filter((n) => !Number.isNaN(n) && n > 0);
} catch (e) {
console.error('获取中药快捷克数失败:', e);
this.commonQuantities = [];
@@ -331,53 +353,53 @@ export default {
}
this.loading = true;
try {
const storeId = uni.getStorageSync('store_id') || 11001;
const params = {
store_id: storeId,
type: 1, // 中药
name: name,
page: this.page,
page_size: this.pageSize
};
if (this.registerId) {
params.register_id = this.registerId;
}
const res = await getProductListDoctorReception(params);
if (res && (res.code === 0 || res.errcode === 0)) {
const list = res.data || res.result || [];
this.hasMore = list.length >= this.pageSize;
// 合并已选中的药品数量,使用 $set 确保响应式
// 匹配优先级selectedDrugs.index_id === drug.id再 fallback 到 entity id
const merged = list.map(drug => {
const entityId = (drug.drug && drug.drug.id) || drug.id;
const selected = this.selectedDrugs.find(item => {
const matchByIndexId = (item.index_id !== undefined && item.index_id !== null) && item.index_id === drug.id;
const matchByEntityId = (item.id !== undefined && item.id !== null) && item.id === entityId;
const matchByDrugId = (item.drug_id !== undefined && item.drug_id !== null) && item.drug_id === entityId;
return matchByIndexId || matchByEntityId || matchByDrugId;
});
// 直接使用原对象,使用 $set 添加响应式属性
this.$set(drug, '_quantity', selected ? selected.number : 0);
return drug;
});
if (this.page > 1) {
this.drugList = this.drugList.concat(merged);
} else {
this.drugList = merged;
}
// 确保所有药品都有 _quantity 属性
this.drugList.forEach(drug => {
if (typeof drug._quantity === 'undefined') {
this.$set(drug, '_quantity', 0);
}
});
} else {
if (this.page === 1) {
this.drugList = [];
let list = [];
if (this.salespersonTransferMode) {
const wrap = await getClinicSalespersonChineseDrugList({ name });
if (wrap && wrap.ok) {
list = Array.isArray(wrap.data) ? wrap.data : [];
}
this.hasMore = false;
} else {
const storeId = uni.getStorageSync('store_id') || 11001;
const params = {
store_id: storeId,
type: 1,
name: name,
page: this.page,
page_size: this.pageSize
};
if (this.registerId) {
params.register_id = this.registerId;
}
const res = await getProductListDoctorReception(params);
if (res && (res.code === 0 || res.errcode === 0)) {
list = res.data || res.result || [];
this.hasMore = list.length >= this.pageSize;
}
}
const merged = list.map(drug => {
const entityId = (drug.drug && drug.drug.id) || drug.id;
const selected = this.selectedDrugs.find(item => {
const matchByIndexId = (item.index_id !== undefined && item.index_id !== null) && item.index_id === drug.id;
const matchByEntityId = (item.id !== undefined && item.id !== null) && item.id === entityId;
const matchByDrugId = (item.drug_id !== undefined && item.drug_id !== null) && item.drug_id === entityId;
return matchByIndexId || matchByEntityId || matchByDrugId;
});
this.$set(drug, '_quantity', selected ? selected.number : 0);
return drug;
});
if (this.page > 1) {
this.drugList = this.drugList.concat(merged);
} else {
this.drugList = merged;
}
this.drugList.forEach(drug => {
if (typeof drug._quantity === 'undefined') {
this.$set(drug, '_quantity', 0);
}
});
} catch (error) {
console.error('获取药品列表失败:', error);
this.$toast('获取药品列表失败');

View File

@@ -139,6 +139,7 @@
<script>
// 保留所有原有逻辑代码与注释
import { getDiseaseList, getMyDiseaseList, addMyDisease, deleteMyDisease } from '@/api/reception.js';
import { getClinicSalespersonDiseaseList } from '@/api/clinicSalesperson.js';
export default {
name: 'DiagnosisModal',
@@ -156,6 +157,10 @@ export default {
selectedDiagnosis: {
type: String,
default: ''
},
salespersonTransferMode: {
type: Boolean,
default: false
}
},
data() {
@@ -189,8 +194,9 @@ export default {
this.allDiagnosisList = [];
this.currentPage = 1;
this.hasMore = false;
// 只加载常用诊断列表,不自动加载搜索列表
this.loadDoctorMyDiseaseList();
if (!this.salespersonTransferMode) {
this.loadDoctorMyDiseaseList();
}
}
},
show(newVal) {
@@ -209,7 +215,17 @@ export default {
*/
async loadDiagnosisList(searchKey = '', page = 1, append = false) {
try {
const res = await getDiseaseList(searchKey, page, this.pageSize);
let res;
if (this.salespersonTransferMode) {
const wrap = await getClinicSalespersonDiseaseList({
name: searchKey,
page,
page_size: this.pageSize,
});
res = wrap && wrap.ok ? wrap.res : null;
} else {
res = await getDiseaseList(searchKey, page, this.pageSize);
}
let list = [];
let hasMore = false;

View File

@@ -0,0 +1,228 @@
<template>
<u-popup
v-model="show"
mode="bottom"
:closeable="true"
@close="handleClose"
:safe-area-inset-bottom="true"
: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>
</view>
</view>
</view>
</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
:throttle-time="0"
:loading="submitting"
@click="handleConfirm"
shape="circle"
:custom-style="confirmBtnStyle"
>确认提交</u-button>
</view>
</view>
</u-popup>
</template>
<script>
import { uploadLegacyAdminImage } from '@/api/upload.js';
export default {
name: 'TransferPatientDrawer',
props: {
value: {
type: Boolean,
default: false,
},
submitting: {
type: Boolean,
default: false,
},
},
data() {
return {
show: false,
form: {
patient_name: '',
patient_mobile: '',
patient_address: '',
prescription_images: [],
},
cancelBtnStyle: {
backgroundColor: '#F4F6F8',
color: '#2A2E35',
height: '88rpx',
width: '320rpx',
fontWeight: 'bold',
fontSize: '30rpx',
border: 'none',
},
confirmBtnStyle: {
backgroundColor: '#00A88A',
color: '#fff',
height: '88rpx',
width: '320rpx',
fontWeight: 'bold',
fontSize: '30rpx',
boxShadow: '0 8rpx 20rpx rgba(0, 168, 138, 0.25)',
border: 'none',
},
};
},
watch: {
value(newVal) {
this.show = newVal;
},
show(newVal) {
if (!newVal) {
this.$emit('input', false);
}
},
},
methods: {
handleClose() {
this.show = false;
this.$emit('input', false);
},
chooseImage() {
uni.chooseImage({
count: 1,
success: async (res) => {
const path = res.tempFilePaths[0];
try {
uni.showLoading({ title: '上传中' });
const url = await uploadLegacyAdminImage({ filePath: path });
this.form.prescription_images.push(url);
} catch (e) {
uni.showToast({ title: '上传失败', icon: 'none' });
} finally {
uni.hideLoading();
}
},
});
},
removeImage(idx) {
this.form.prescription_images.splice(idx, 1);
},
handleConfirm() {
const name = (this.form.patient_name || '').trim();
const mobile = (this.form.patient_mobile || '').trim();
const address = (this.form.patient_address || '').trim();
if (!name || !mobile || !address) {
uni.showToast({ title: '请填写收货人、手机号和地址', icon: 'none' });
return;
}
this.$emit('confirm', {
patient_name: name,
patient_mobile: mobile,
patient_address: address,
prescription_images: [...this.form.prescription_images],
});
},
resetForm() {
this.form = {
patient_name: '',
patient_mobile: '',
patient_address: '',
prescription_images: [],
};
},
},
};
</script>
<style lang="scss" scoped>
.page-bg { background-color: #F4F6F8; }
.transfer-patient-drawer {
display: flex;
flex-direction: column;
height: 100%;
}
.modal-header {
padding: 32rpx;
border-bottom: 1px solid #f0f0f0;
}
.scroll-container {
flex: 1;
height: 0;
}
.form-body {
padding: 32rpx;
}
.field {
margin-bottom: 32rpx;
}
.img-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.img-item, .img-add {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
position: relative;
}
.img-item image {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
.img-remove {
position: absolute;
top: -8rpx;
right: -8rpx;
}
.img-add {
border: 1px dashed #ccc;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.modal-footer {
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
}
.shadow-up {
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
}
</style>

View File

@@ -8,11 +8,11 @@
/>
<!-- #endif -->
<!-- 导航栏 -->
<u-navbar class="navbar" :is-back="true" :title="navbarTitle" :custom-back="handleCustomBack" title-color="#000" background="{ background: '#fff' }">
<u-navbar class="navbar" :is-back="true" :title="navbarTitle" :custom-back="boundCustomBack" title-color="#000" background="{ background: '#fff' }">
</u-navbar>
<!-- 处方类型Tab切换 -->
<view class="tabs flex p-32 flex-ali-center flex-jus-sp shadow-sm" style="overflow-x: auto; position: relative; z-index: 10;">
<view v-if="!isSalespersonTransferMode" class="tabs flex p-32 flex-ali-center flex-jus-sp shadow-sm" style="overflow-x: auto; position: relative; z-index: 10;">
<u-button
:throttle-time="0"
:plain="true"
@@ -31,10 +31,19 @@
</u-button>
</view>
<view v-if="registerModeHint" class="register-mode-hint mx-32 m-t-12">
<view v-if="!isSalespersonTransferMode && registerModeHint" class="register-mode-hint mx-32 m-t-12">
<text class="register-mode-hint__text">{{ registerModeHint }}</text>
</view>
<view v-if="!isSalespersonTransferMode && allowInsuranceCategory" class="mx-32 m-t-12 flex flex-ali-center">
<u-button size="mini" :type="feeCategory === 1 ? 'success' : 'default'" @click="feeCategory = 1">自费</u-button>
<u-button size="mini" class="m-l-16" :type="feeCategory === 2 ? 'success' : 'default'" @click="feeCategory = 2">医保</u-button>
</view>
<view v-if="!isSalespersonTransferMode && activeCategory === 1 && hasSalespersonTransfer" class="mx-32 m-t-12">
<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 class="top_title flex-row flex-ali-center">
@@ -192,7 +201,7 @@
</view>
<view class="flex-row" style="height: 50rpx;">
<u-button
v-if="!isCommonPrescription && supportsCommonPrescriptionTemplate"
v-if="!isSalespersonTransferMode && !isCommonPrescription && supportsCommonPrescriptionTemplate"
:throttle-time="0"
shape="circle"
size="mini"
@@ -335,39 +344,48 @@
<!-- 费用计算区域 -->
<view class="fee-summary white radius-12 p-32 mx-32 m-t-24 m-b-40">
<view class="flex-row flex-jus-sp flex-ali-center m-b-24">
<text class="fs-28 color-sub">诊疗费</text>
<view class="flex-row flex-ali-center">
<text class="fs-28 m-r-8"></text>
<u-input
v-model="treatmentPrice"
placeholder="0.00"
input-align="right"
:custom-style="priceInputStyle" />
<view v-if="isTransferFeeHidden" class="transfer-fee-hidden-tip flex-col">
<text class="fs-28 color-title font-bold">本店已关闭推广员查看费用</text>
<text class="fs-24 color-sub m-t-8">提交后由接诊医生确认价格</text>
</view>
<template v-else>
<view v-if="!isSalespersonTransferMode" class="flex-row flex-jus-sp flex-ali-center m-b-24">
<text class="fs-28 color-sub">诊疗费</text>
<view class="flex-row flex-ali-center">
<text class="fs-28 m-r-8"></text>
<u-input
v-model="treatmentPrice"
placeholder="0.00"
input-align="right"
:custom-style="priceInputStyle" />
</view>
</view>
</view>
<view class="flex-row flex-jus-sp flex-ali-center m-b-24" v-if="processingFee > 0">
<text class="fs-28 color-sub">加工费</text>
<text class="fs-30 color-title font-bold">{{processingFee.toFixed(2)}}</text>
</view>
<view class="flex-row flex-jus-sp flex-ali-center m-b-32">
<view class="flex-col">
<text class="fs-28 color-sub">商品计费</text>
<text
v-if="grossMarginText"
class="fs-24 color-primary m-t-8"
>毛利率 {{ grossMarginText }}%</text>
<view
v-if="!isSalespersonTransferMode && processingFee > 0"
class="flex-row flex-jus-sp flex-ali-center m-b-24">
<text class="fs-28 color-sub">加工费</text>
<text class="fs-30 color-title font-bold">{{processingFee.toFixed(2)}}</text>
</view>
<text class="fs-30 color-title font-bold">{{totalProductCost.toFixed(2)}}</text>
</view>
<!-- 总价显示 -->
<view class="total-price-bar flex-row flex-jus-sp flex-ali-center pt-32 border-t-dashed" v-if="currentDrugs.length > 0">
<text class="fs-30 font-bold color-title">合计</text>
<text class="fs-48 font-bold color-price">{{totalPrice}}</text>
</view>
<view class="flex-row flex-jus-sp flex-ali-center m-b-32">
<view class="flex-col">
<text class="fs-28 color-sub">商品计费</text>
<text
v-if="grossMarginText"
class="fs-24 color-primary m-t-8"
>毛利率 {{ grossMarginText }}%</text>
</view>
<text class="fs-30 color-title font-bold">{{totalProductCost.toFixed(2)}}</text>
</view>
<view
class="total-price-bar flex-row flex-jus-sp flex-ali-center pt-32 border-t-dashed"
v-if="currentDrugs.length > 0">
<text class="fs-30 font-bold color-title">合计</text>
<text class="fs-48 font-bold color-price">{{totalPrice}}</text>
</view>
</template>
</view>
<!-- 提示信息 -->
@@ -379,7 +397,7 @@
<!-- 底部操作栏 -->
<view class="bottom flex-jus-sp flex-ali-center safe-area-inset-bottom">
<view v-if="!isCommonPrescription && supportsCommonPrescriptionTemplate" @click="handleSaveAsCommonPrescription" class="btnText">另存常用方</view>
<view v-if="!isSalespersonTransferMode && !isCommonPrescription && supportsCommonPrescriptionTemplate" @click="handleSaveAsCommonPrescription" class="btnText">另存常用方</view>
<u-button
:throttle-time="0"
@click="handleSendPrescription"
@@ -388,10 +406,10 @@
backgroundColor: isSubmitting ? '#A5DCD2' : '#6ACDBB',
color: '#fff',
height: '86rpx',
width: (isCommonPrescription || !supportsCommonPrescriptionTemplate) ? '686rpx' : '448rpx'
width: (isSalespersonTransferMode || isCommonPrescription || !supportsCommonPrescriptionTemplate) ? '686rpx' : '448rpx'
}"
:disabled="isSubmitting">
{{ isCommonPrescription ? '保存常用方' : '发送处方' }}
{{ isCommonPrescription ? '保存常用方' : (isSalespersonTransferMode ? '提交传方' : '发送处方') }}
</u-button>
</view>
@@ -407,6 +425,7 @@
<DiagnosisModal
v-model="showDiagnosisModal"
:selectedDiagnosis="diagnosisText"
:salesperson-transfer-mode="isSalespersonTransferMode"
@confirm="handleDiagnosisConfirm"
/>
@@ -439,9 +458,18 @@
:initial-search-key="chineseModalInitialKeyword"
:register-id="registerId"
:see-rate="seeRate"
:salesperson-transfer-mode="isSalespersonTransferMode"
:show-drug-price="showSalespersonTransferPrice"
@select="handleSelectChineseDrug"
/>
<TransferPatientDrawer
v-model="showTransferPatientDrawer"
:submitting="transferDrawerSubmitting"
ref="transferPatientDrawer"
@confirm="handleTransferPatientConfirm"
/>
<!-- 简单产品选择弹窗 -->
<SimpleProductModal
v-model="showSimpleProductModal"
@@ -491,9 +519,11 @@ import {
saveWestCommonPrescriptionApi,
saveChineseCommonPrescriptionApi,
getCurrentStoreTypeApi,
getSalespersonTransferByRegisterApi,
getPrescriptionInfoApi,
getTraditionalChineseMedicineJson
} from '@/api/reception.js';
import { parseTransferPrescriptionContent } from '@/subPackages/sub_online_reception/utils/transferPrescriptionImport.js';
import { getChatRoomByRegisterApi, sendToUserHttpApi } from '@/api/chat.js';
import { PrescriptionStorage } from './utils/prescriptionStorage.js';
import { PrescriptionCalculator } from './utils/prescriptionCalculator.js';
@@ -507,8 +537,16 @@ import SimpleProductModal from './components/modals/SimpleProductModal.vue';
import WesternMedicineUsageModal from './components/modals/WesternMedicineUsageModal.vue';
import ChineseMedicineConfig from './components/ChineseMedicineConfig.vue';
import TraditionalTermPickerModal from './components/modals/TraditionalTermPickerModal.vue';
import TransferPatientDrawer from './components/modals/TransferPatientDrawer.vue';
import {
createClinicSalespersonTransferPrescription,
getClinicSalespersonSeePriceConfig,
getClinicSalespersonProcessRuleList,
} from '@/api/clinicSalesperson.js';
const PENDING_RX_FROM_CHAT = 'xk_pending_rx_from_chat';
const SALESPERSON_TRANSFER_STORAGE_KEY = 'salesperson_transfer';
const SALESPERSON_HOME_URL = '/subPackages/sub_clinic_salesperson/home/index';
/** 与 closeTopOverlayIfAny 一致:任意为 true 时启用微信小程序 page-container 返回拦截 */
const PRESCRIPTION_OVERLAY_KEYS = [
@@ -520,7 +558,8 @@ const PRESCRIPTION_OVERLAY_KEYS = [
'showCommonPrescriptionModal',
'showDiagnosisModal',
'showDoctorOrderModal',
'showSaveCommonPrescriptionModal'
'showSaveCommonPrescriptionModal',
'showTransferPatientDrawer'
];
export default {
@@ -534,7 +573,8 @@ export default {
SimpleProductModal,
WesternMedicineUsageModal,
ChineseMedicineConfig,
TraditionalTermPickerModal
TraditionalTermPickerModal,
TransferPatientDrawer
},
data() {
return {
@@ -594,6 +634,9 @@ export default {
isSubmitting: false,
identity: uni.getStorageSync('role'),
registerStoreInfo: null,
feeCategory: 1,
salespersonTransferPrescriptionId: 0,
hasSalespersonTransfer: false,
/** 是否可查看毛利率 0/1xk-api see_rate */
seeRate: 0,
sendMode: 0,
@@ -644,15 +687,22 @@ export default {
borderRadius: '8rpx',
padding: '0 16rpx'
},
scrollStyle: {
height: 'calc(100vh - 600rpx)',
marginBottom: '200rpx'
},
/** 微信小程序 page-container有抽屉/弹层时为 true拦截侧滑等返回 */
pageBackGuardShow: false
pageBackGuardShow: false,
/** 程序化隐藏 page-container 时抑制 @leave避免误触发页面退出确认 */
_suppressPageBackGuardLeave: false,
/** 推广员传方模式 */
isSalespersonTransferMode: false,
/** 推广员是否可查看售价 0/1门店 salesperson see_price */
salespersonSeePrice: 0,
showTransferPatientDrawer: false,
transferDrawerSubmitting: false,
};
},
computed: {
allowInsuranceCategory() {
return Number(this.registerStoreInfo?.allow_insurance_category ?? 0) === 1;
},
totalProductCost() {
if (this.currentDrugs.length === 0) return 0;
if (this.activeCategory === 1) {
@@ -690,6 +740,12 @@ export default {
this.chineseConfig.dosage
);
},
showSalespersonTransferPrice() {
return !this.isSalespersonTransferMode || Number(this.salespersonSeePrice) === 1;
},
isTransferFeeHidden() {
return this.isSalespersonTransferMode && Number(this.salespersonSeePrice) === 0;
},
supportsCommonPrescriptionTemplate() {
return [1, 2].includes(this.activeCategory);
},
@@ -702,11 +758,42 @@ export default {
return this.isOnlineRevisit ? '当前:在线复诊开方(提交后将向患者发送处方消息)' : '当前:线下开方';
},
navbarTitle() {
if (this.isSalespersonTransferMode) {
return '传方';
}
if (this.registerOrderType === null || this.registerOrderType === undefined || this.registerOrderType === '') {
return '开方';
}
return this.isOnlineRevisit ? '开方 · 在线复诊' : '开方 · 线下';
},
hasTransferDraft() {
if (!this.isSalespersonTransferMode) return false;
return this.currentDrugs.length > 0
|| this.diagnoses.length > 0
|| !!(this.medicalAdvice && String(this.medicalAdvice).trim());
},
shouldEnablePageBackGuard() {
return this.hasPrescriptionOverlay;
},
scrollStyle() {
try {
const sys = uni.getSystemInfoSync();
const rpxRatio = sys.windowWidth / 750;
const topRpx = this.isSalespersonTransferMode ? 500 : 600;
const bottomRpx = 200;
const heightPx = sys.windowHeight - topRpx * rpxRatio - bottomRpx * rpxRatio;
return {
height: `${Math.max(Math.floor(heightPx), 200)}px`,
marginBottom: '200rpx',
};
} catch (e) {
const topRpx = this.isSalespersonTransferMode ? 500 : 600;
return {
height: `calc(100vh - ${topRpx}rpx)`,
marginBottom: '200rpx',
};
}
},
onlineTcmDiseasesLabel() {
const data = this.traditionalTcmData;
const id = this.onlineTcmDiseasesId;
@@ -749,13 +836,27 @@ export default {
if (!val) this.chineseModalInitialKeyword = '';
},
// #ifdef MP-WEIXIN
hasPrescriptionOverlay(val) {
this.pageBackGuardShow = !!val;
shouldEnablePageBackGuard(val) {
this.setPageBackGuardShow(!!val, { suppressLeave: !val });
this.syncSalespersonTransferUnloadAlert();
},
hasTransferDraft() {
this.syncSalespersonTransferUnloadAlert();
},
// #endif
},
created() {
// u-navbar 在小程序内 bind($parent) 可能拿不到页面实例,用箭头函数固定 this
this.boundCustomBack = () => this.handleCustomBack();
},
onLoad(options) {
this.registerId = options.register_id || '';
this.isSalespersonTransferMode = String(options.salesperson_transfer) === '1';
if (this.isSalespersonTransferMode) {
this.registerId = SALESPERSON_TRANSFER_STORAGE_KEY;
this._presetActiveCategory = 1;
} else {
this.registerId = options.register_id || '';
}
this.patientId = options.patient_id || '';
this.pendingReusePrescriptionId = options.reuse_prescription_id ? String(options.reuse_prescription_id) : '';
this.isCommonPrescription = String(options.save_common) === '1';
@@ -773,33 +874,86 @@ export default {
if (this.activeCategory === 1 && this.chineseConfig.ruleType === 2) {
await this.hydrateChineseProcessRuleListsFromConfig();
}
if (this.activeCategory === 1) await this.checkAndShowTransferTip();
if (!this.isSalespersonTransferMode && this.activeCategory === 1) {
await this.checkAndShowTransferTip();
}
},
onUnload() {
if (this.isCommonPrescription) {
uni.removeStorageSync('add');
}
// #ifdef MP-WEIXIN
if (this.isSalespersonTransferMode) {
uni.disableAlertBeforeUnload();
}
// #endif
},
methods: {
// #ifdef MP-WEIXIN
setPageBackGuardShow(show, { suppressLeave = false } = {}) {
if (suppressLeave) this._suppressPageBackGuardLeave = true;
this.pageBackGuardShow = !!show;
},
syncSalespersonTransferUnloadAlert() {
if (!this.isSalespersonTransferMode) return;
if (this.hasTransferDraft && !this.hasPrescriptionOverlay) {
uni.enableAlertBeforeUnload({ message: '有未提交的传方内容,确定退出?' });
} else {
uni.disableAlertBeforeUnload();
}
},
// #endif
async initializePageData() {
try {
this.restoreFromLocalStorage();
await this.loadPatientInfo();
await this.loadRegisterOrderType();
await this.loadBasicConfigData();
await this.loadRegisterStoreInfo();
if (this.isSalespersonTransferMode) {
await this.loadSalespersonTransferConfig();
await this.loadBasicConfigDataForTransfer();
} else {
await this.loadPatientInfo();
await this.loadRegisterOrderType();
await this.loadBasicConfigData();
await this.loadRegisterStoreInfo();
}
this.loadCurrentCategoryDrugs();
if (this.activeCategory === 1 && this.chineseConfig.ruleType === 2) {
await this.hydrateChineseProcessRuleListsFromConfig();
}
await this.maybeApplyReusePrescription();
if (this.activeCategory === 1) await this.checkAndShowTransferTip();
this.applyPendingExperienceDrugFromChat();
if (!this.isSalespersonTransferMode) {
await this.maybeApplyReusePrescription();
if (this.activeCategory === 1) await this.checkAndShowTransferTip();
this.applyPendingExperienceDrugFromChat();
}
// #ifdef MP-WEIXIN
this.$nextTick(() => {
this.setPageBackGuardShow(this.shouldEnablePageBackGuard);
this.syncSalespersonTransferUnloadAlert();
});
// #endif
} catch (error) {
console.error('初始化页面数据失败:', error);
this.$toast('初始化失败,请重试');
}
},
async loadSalespersonTransferConfig() {
try {
const wrap = await getClinicSalespersonSeePriceConfig();
if (wrap && wrap.ok) {
this.salespersonSeePrice = Number(wrap.data?.see_price ?? 0);
}
} catch (error) {
console.error('获取推广员价格配置失败:', error);
}
},
async loadBasicConfigDataForTransfer() {
try {
if (this.activeCategory === 1 && this.processRuleList.length === 0) {
await this.loadProcessRuleList(0, 0);
}
} catch (error) {
console.error('加载基础配置数据失败:', error);
}
},
restoreFromLocalStorage() {
if (this._presetActiveCategory !== null && this._presetActiveCategory !== undefined && !Number.isNaN(this._presetActiveCategory)) {
this.activeCategory = this._presetActiveCategory;
@@ -972,6 +1126,10 @@ export default {
if (res) {
this.registerStoreInfo = res;
this.seeRate = Number(res.see_rate ?? 0);
if (Number(res.allow_insurance_category ?? 0) !== 1) {
this.feeCategory = 1;
}
this.checkSalespersonTransfer();
if (res.is_from_transfer === 1 && res.delegate_store_id) {
this.selectedStoreId = res.delegate_store_id;
this.sendMode = 1;
@@ -984,11 +1142,65 @@ export default {
console.error('获取挂号诊所信息失败:', error);
}
},
async checkSalespersonTransfer() {
if (!this.registerId) {
this.hasSalespersonTransfer = false;
return;
}
try {
const res = await getSalespersonTransferByRegisterApi(this.registerId);
const list = res?.result || res?.data || res;
this.hasSalespersonTransfer = Array.isArray(list) && list.length > 0;
} catch {
this.hasSalespersonTransfer = false;
}
},
async openSalespersonTransfer() {
try {
const res = await getSalespersonTransferByRegisterApi(this.registerId);
const list = res?.result || res?.data || res;
if (!Array.isArray(list) || !list.length) {
this.$toast('暂无待导入传方');
return;
}
const item = list[0];
const parsed = parseTransferPrescriptionContent(item);
if (!parsed || !parsed.drugList?.length) {
this.$toast('传方数据无效');
return;
}
this.salespersonTransferPrescriptionId = item.id;
this.activeCategory = 1;
parsed.drugList.forEach((drug) => {
this.currentDrugs.push({
index_id: drug.index_id ?? drug.drug_id ?? drug.id,
id: drug.drug_id || drug.id,
drug_id: drug.drug_id || drug.id,
drug_name: drug.drug_name || drug.name,
name: drug.drug_name || drug.name,
number: drug.number || 1,
way_id: drug.way_id || 0,
price: drug.price ?? 0,
buy_price: drug.buy_price,
});
});
if (parsed.clinical_diagnose) {
this.diagnoses = [{ id: Date.now(), name: parsed.clinical_diagnose }];
}
if (parsed.doctor_order) this.medicalAdvice = parsed.doctor_order;
this.saveToLocalStorage();
this.$toast('传方已导入');
} catch (e) {
console.error(e);
this.$toast('加载传方失败');
}
},
async handleSwitchCategory(category) {
this.saveToLocalStorage();
this.activeCategory = category;
this.loadCurrentCategoryDrugs();
if (category === 1) {
await this.checkSalespersonTransfer();
await this.checkAndShowTransferTip();
if (this.processRuleList.length === 0) await this.loadProcessRuleList(0, 0);
}
@@ -1436,8 +1648,16 @@ export default {
async loadProcessRuleList(pid = 0, ruleId = 0) {
try {
const data = ruleId === 0 ? { pid } : { rule_id: ruleId };
const res = await getProcessRuleList(data);
const list = res?.data || res?.result || res || [];
let list = [];
if (this.isSalespersonTransferMode) {
const wrap = await getClinicSalespersonProcessRuleList(data);
if (wrap && wrap.ok) {
list = Array.isArray(wrap.data) ? wrap.data : [];
}
} else {
const res = await getProcessRuleList(data);
list = res?.data || res?.result || res || [];
}
if (ruleId !== 0) {
this.processRuleNoteList = list;
} else if (pid === 0) {
@@ -1643,6 +1863,11 @@ export default {
const validation = this.validatePrescriptionData();
if (!validation.valid) { this.$toast(validation.message); return; }
if (this.isSalespersonTransferMode) {
this.showTransferPatientDrawer = true;
return;
}
this.isSubmitting = true;
try {
if (!this.registerStoreInfo) await this.loadRegisterStoreInfo();
@@ -1741,7 +1966,8 @@ export default {
}
const params = {
patient: patientData, drugs: drugsData, diagnosis: clinical_diagnose, medicalAdvice: this.medicalAdvice, total: parseFloat(this.totalPrice), category: 1, drug_type: 2, register_id: parseInt(this.registerId), treatment_price: parseFloat(this.treatmentPrice || 0), prescription_type: this.activeCategory, doctor_second_sign: doctorSecondSignValue, send_mode: finalSendMode, custom_store_id: finalSendMode === 1 ? finalStoreId : null
patient: patientData, drugs: drugsData, diagnosis: clinical_diagnose, medicalAdvice: this.medicalAdvice, total: parseFloat(this.totalPrice), category: this.feeCategory, drug_type: 2, register_id: parseInt(this.registerId), treatment_price: parseFloat(this.treatmentPrice || 0), prescription_type: this.activeCategory, doctor_second_sign: doctorSecondSignValue, send_mode: finalSendMode, custom_store_id: finalSendMode === 1 ? finalStoreId : null,
salesperson_transfer_prescription_id: this.salespersonTransferPrescriptionId || undefined,
};
if (this.activeCategory === 1) {
params.package_method_id = this.chineseConfig.packageMethodId || null; params.process_rule_id = this.chineseConfig.processRuleId || null; params.process_rule_note_id = this.chineseConfig.processRuleNoteId || null; params.child_process_rule_id = this.chineseConfig.childProcessRuleId || null; params.process_rule_type = this.chineseConfig.ruleType || 1; params.processing_fee = this.processingFee; params.dosage = this.chineseConfig.dosage || 7; params.day_dosage = this.chineseConfig.dayDosage || 2;
@@ -1848,6 +2074,97 @@ export default {
if (this.isCommonPrescription) { uni.removeStorageSync('add'); this.safeNavigateBack(400); }
} 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,
number: drug.number || drug._quantity || 1,
way_id: drug.way_id || 0,
}));
return {
patient_name: patientData.patient_name,
patient_mobile: patientData.patient_mobile,
patient_address: patientData.patient_address,
prescription_images: patientData.prescription_images || [],
clinical_diagnose: clinicalDiagnose,
doctor_order: this.medicalAdvice || '',
dosage: this.chineseConfig.dosage || 7,
day_dosage: this.chineseConfig.dayDosage || 2,
drugs,
};
},
async handleTransferPatientConfirm(patientData) {
this.transferDrawerSubmitting = true;
try {
const wrap = await createClinicSalespersonTransferPrescription(
this.buildSalespersonTransferPayload(patientData)
);
if (wrap && wrap.ok) {
PrescriptionStorage.clearAllPrescriptionData(SALESPERSON_TRANSFER_STORAGE_KEY);
this.showTransferPatientDrawer = false;
if (this.$refs.transferPatientDrawer && this.$refs.transferPatientDrawer.resetForm) {
this.$refs.transferPatientDrawer.resetForm();
}
// #ifdef MP-WEIXIN
this.setPageBackGuardShow(false, { suppressLeave: true });
uni.disableAlertBeforeUnload();
// #endif
this.$toast('提交成功');
setTimeout(() => {
uni.navigateTo({ url: '/subPackages/sub_clinic_salesperson/transfer-prescription/list' });
}, 500);
} else {
this.$toast(wrap?.res?.msg || wrap?.res?.message || '提交失败');
}
} catch (error) {
console.error('提交传方失败:', error);
this.$toast('提交传方失败,请重试');
} finally {
this.transferDrawerSubmitting = false;
}
},
tryLeaveSalespersonTransferPage(onConfirm) {
if (this.hasTransferDraft) {
uni.showModal({
title: '提示',
content: '有未提交的传方内容,确定退出?',
success: ({ confirm }) => {
if (confirm) {
// #ifdef MP-WEIXIN
this.setPageBackGuardShow(false, { suppressLeave: true });
uni.disableAlertBeforeUnload();
// #endif
if (typeof onConfirm === 'function') onConfirm();
} else {
// #ifdef MP-WEIXIN
this.setPageBackGuardShow(this.shouldEnablePageBackGuard, {
suppressLeave: !this.shouldEnablePageBackGuard,
});
// #endif
}
},
});
return;
}
// #ifdef MP-WEIXIN
this.setPageBackGuardShow(false, { suppressLeave: true });
uni.disableAlertBeforeUnload();
// #endif
if (typeof onConfirm === 'function') onConfirm();
},
navigateToSalespersonWorkbench() {
const pages = getCurrentPages();
if (pages.length > 1) {
this.safeNavigateBack(0);
return;
}
// #ifdef MP-WEIXIN
this.setPageBackGuardShow(false, { suppressLeave: true });
uni.disableAlertBeforeUnload();
// #endif
uni.reLaunch({ url: SALESPERSON_HOME_URL });
},
/** 若有弹层打开则关闭第一个并返回 true用于导航返回 / 微信 page-container leave 先关抽屉) */
closeTopOverlayIfAny() {
for (let i = 0; i < PRESCRIPTION_OVERLAY_KEYS.length; i++) {
@@ -1863,7 +2180,7 @@ export default {
safeNavigateBack(delayMs = 0) {
const runNav = () => {
// #ifdef MP-WEIXIN
this.pageBackGuardShow = false;
this.setPageBackGuardShow(false, { suppressLeave: true });
setTimeout(() => {
uni.navigateBack();
}, 16);
@@ -1880,14 +2197,31 @@ export default {
},
// #ifdef MP-WEIXIN
onPageBackGuardLeave() {
this.closeTopOverlayIfAny();
this.$nextTick(() => {
this.pageBackGuardShow = this.hasPrescriptionOverlay;
});
if (this._suppressPageBackGuardLeave) {
this._suppressPageBackGuardLeave = false;
return;
}
if (this.closeTopOverlayIfAny()) {
this.$nextTick(() => {
this.setPageBackGuardShow(this.shouldEnablePageBackGuard, { suppressLeave: true });
this.syncSalespersonTransferUnloadAlert();
});
return;
}
this.setPageBackGuardShow(false);
if (this.isSalespersonTransferMode) {
this.tryLeaveSalespersonTransferPage(() => this.navigateToSalespersonWorkbench());
return;
}
this.safeNavigateBack(0);
},
// #endif
handleCustomBack() {
if (this.closeTopOverlayIfAny()) return;
if (this.isSalespersonTransferMode) {
this.tryLeaveSalespersonTransferPage(() => this.navigateToSalespersonWorkbench());
return;
}
this.safeNavigateBack(0);
}
}