1. 特色方模块

This commit is contained in:
李琦
2026-07-03 07:57:26 +08:00
parent 03973e8718
commit f18916f9d5
23 changed files with 1408 additions and 186 deletions

View File

@@ -51,3 +51,35 @@ export function switchDoctorWxAccount(payload) {
data: payload,
});
}
export function getDoctorWxAuthConfig() {
return req.request({
url: `${AUTH_PREFIX}/config`,
method: 'GET',
header: { isToken: false },
});
}
export function doctorWxLoginByWechat(payload) {
return req.request({
url: `${AUTH_PREFIX}/wx-login`,
method: 'POST',
data: payload,
header: { isToken: false },
});
}
export function getDoctorWxBindStatus() {
return doctorAuthRequest({
url: '/wx-bind-status',
method: 'GET',
});
}
export function bindDoctorWxWechat(code) {
return doctorAuthRequest({
url: '/bind-wechat',
method: 'POST',
data: { code },
});
}

View File

@@ -34,6 +34,15 @@ export function getAcceptingPatientList(data) {
})
}
/** 工作台看板统计 */
export function getWorkbenchStats(data) {
return req.request({
url: '/newApi/doctor-reception-wx/workbench-stats',
method: 'GET',
data
})
}
// 获取患者详情(挂号维度的就诊信息,不含处方列表)
export function getPatientItem(id) {
return req.request({
@@ -43,6 +52,15 @@ export function getPatientItem(id) {
})
}
/** 医生应用特色方到开方区 */
export function applySpecialPrescriptionApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/apply-special-prescription',
method: 'POST',
data
})
}
/** 常用回复模板(与 PC online-consultation/get-quick-reply-list 同源) */
export function getQuickReplyListApi() {
return req.request({

10
api/storeConsultation.js Normal file
View File

@@ -0,0 +1,10 @@
import { req } from '@/common/js/index.js';
/** 检查门店在线复诊配置(委托诊所 + 在线复诊医生) */
export function checkOnlineConsultationConfigApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/check-online-consultation-config',
method: 'GET',
data,
});
}

View File

@@ -2,6 +2,7 @@ import Request from './request.js';
import errorCode from './errorCode.js';
import {Base64} from "js-base64";
import { getDoctorWxEnv } from '@/config/app-env.js';
import { resolveDoctorWxAuthToken } from '@/utils/doctorWxAuthToken.js';
const { legacyApiBase } = getDoctorWxEnv();
@@ -66,22 +67,9 @@ const reqInterceptor = async (options) => {
const loginMode = uni.getStorageSync('loginMode')
if (!skipToken) {
if (isDoctorWxAuthReq) {
if (loginMode === 'platform_admin' && platformToken) {
options.header['authorization'] = `Bearer ${platformToken}`
} else if (loginMode === 'salesperson' && salespersonToken) {
options.header['authorization'] = `Bearer ${salespersonToken}`
} else if (loginMode === 'clinic_salesperson' && clinicSalespersonToken) {
options.header['authorization'] = `Bearer ${clinicSalespersonToken}`
} else if (loginMode === 'clinic_admin' && clinicToken) {
options.header['authorization'] = `Bearer ${clinicToken}`
} else if (doctorToken) {
options.header['authorization'] = `Bearer ${doctorToken}`
} else if (platformToken) {
options.header['authorization'] = `Bearer ${platformToken}`
} else if (salespersonToken) {
options.header['authorization'] = `Bearer ${salespersonToken}`
} else if (clinicToken) {
options.header['authorization'] = `Bearer ${clinicToken}`
const authToken = resolveDoctorWxAuthToken()
if (authToken) {
options.header['authorization'] = `Bearer ${authToken}`
}
} else if (isPlatformAdminReq && platformToken) {
options.header['authorization'] = `Bearer ${platformToken}`

View File

@@ -55,6 +55,7 @@ export default {
async onSwitched(result) {
saveLoginSession(result);
await fetchProfileAfterLogin(result);
uni.$emit('login-account-switched');
this.$toast('切换成功');
setTimeout(() => {
navigateAfterLogin(result);

View File

@@ -0,0 +1,122 @@
<template>
<view
v-if="visible"
class="d-top-message"
:class="['d-top-message--' + type, { 'd-top-message--show': animated }]"
:style="{ top: topOffset + 'px' }"
>
<u-icon v-if="type === 'success'" name="checkmark-circle-fill" color="#6ACDBB" size="28" class="d-top-message-icon"></u-icon>
<text class="d-top-message-text">{{ text }}</text>
</view>
</template>
<script>
export default {
name: 'DTopMessage',
data() {
return {
visible: false,
animated: false,
text: '',
type: 'success',
topOffset: 88,
hideTimer: null,
};
},
mounted() {
this.calcTopOffset();
},
beforeDestroy() {
this.clearTimer();
},
methods: {
calcTopOffset() {
try {
const systemInfo = uni.getSystemInfoSync();
const statusBar = systemInfo.statusBarHeight || 0;
if (typeof wx !== 'undefined' && wx.getMenuButtonBoundingClientRect) {
const menuButton = wx.getMenuButtonBoundingClientRect();
this.topOffset = menuButton.bottom + uni.upx2px(16);
} else {
this.topOffset = statusBar + uni.upx2px(88);
}
} catch (e) {
this.topOffset = 88;
}
},
clearTimer() {
if (this.hideTimer) {
clearTimeout(this.hideTimer);
this.hideTimer = null;
}
},
show(options = {}) {
const {
text = '数据已刷新',
type = 'success',
duration = 1500,
} = options;
this.clearTimer();
this.text = text;
this.type = type;
this.visible = true;
this.animated = false;
this.$nextTick(() => {
this.animated = true;
});
this.hideTimer = setTimeout(() => {
this.hide();
}, duration);
},
hide() {
this.clearTimer();
this.animated = false;
setTimeout(() => {
this.visible = false;
}, 200);
},
},
};
</script>
<style lang="scss" scoped>
.d-top-message {
position: fixed;
left: 50%;
z-index: 100000;
display: flex;
flex-direction: row;
align-items: center;
max-width: 80vw;
padding: 10rpx 28rpx;
border-radius: 999rpx;
opacity: 0;
transform: translate(-50%, -12rpx);
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
}
.d-top-message--show {
opacity: 1;
transform: translate(-50%, 0);
}
.d-top-message--success {
background: #E8F8F3;
}
.d-top-message-text {
font-size: 24rpx;
line-height: 1.4;
color: #6ACDBB;
white-space: nowrap;
}
.d-top-message-icon {
margin-right: 8rpx;
flex-shrink: 0;
}
</style>

View File

@@ -73,14 +73,44 @@
登录
</u-button>
</view>
<view class="btn wx-btn" v-if="wxLoginBtn">
<u-button
:throttle-time="20"
shape="circle"
@click="submitWxLogin"
:ripple="true"
:loading="wxLoading"
:custom-style="{ background: '#07C160', color: '#fff' }"
>
微信一键登录
</u-button>
</view>
</view>
</view>
<u-popup v-model="showAgreementConfirm" mode="center" border-radius="24" width="600" z-index="10075">
<view class="confirm-modal">
<view class="confirm-title">服务协议和隐私政策</view>
<view class="confirm-desc">
请您在使用前仔细阅读并同意
<text class="link" @click="$go('/subPackages/sub_agreement/agreement?type=service_user_agreement')">用户服务协议</text>
<text class="link" @click="$go('/subPackages/sub_agreement/agreement?type=service_privacy_agreement')">隐私权政策</text>
</view>
<view class="confirm-footer">
<button class="cancel-btn" @click="showAgreementConfirm = false">不同意</button>
<button class="agree-btn" @click="agreeAndLogin">同意并登录</button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { sendDoctorWxCode, doctorWxLogin } from '@/api/doctorAuth.js';
import { sendDoctorWxCode, doctorWxLogin, getDoctorWxAuthConfig, doctorWxLoginByWechat } from '@/api/doctorAuth.js';
import {
saveLoginSession,
fetchProfileAfterLogin,
@@ -100,10 +130,24 @@ export default {
smsCode: '',
codeText: '',
loading: false,
wxLoading: false,
wxLoginBtn: false,
pendingLoginPayload: null,
loginMode: 'password',
showAgreementConfirm: false,
pendingLoginAction: null,
};
},
mounted() {
this.loadWxConfig();
},
methods: {
loadWxConfig() {
return getDoctorWxAuthConfig().then((res) => {
const result = (res && res.code === 0 && res.result) ? res.result : {};
this.wxLoginBtn = !!result.wx_login_btn;
}).catch(() => {});
},
codeChange(text) {
this.codeText = text;
},
@@ -134,6 +178,44 @@ export default {
uni.hideLoading();
});
},
ensureAgreement(action) {
if (this.form.check) {
return true;
}
this.pendingLoginAction = action;
this.showAgreementConfirm = true;
return false;
},
agreeAndLogin() {
this.form.check = true;
this.showAgreementConfirm = false;
if (this.pendingLoginAction === 'wx') {
this.proceedWxLogin();
} else {
this.doLogin({});
}
this.pendingLoginAction = null;
},
proceedWxLogin() {
// #ifdef MP-WEIXIN
this.wxLoading = true;
uni.login({
provider: 'weixin',
success: (loginRes) => {
if (!loginRes.code) {
this.$toast('获取微信授权失败');
this.wxLoading = false;
return;
}
this.doWxLogin(loginRes.code, {});
},
fail: () => {
this.$toast('微信授权失败');
this.wxLoading = false;
},
});
// #endif
},
submitLogin() {
if (!this.form.mobile) {
this.$toast('请输入手机号');
@@ -143,13 +225,55 @@ export default {
this.$toast('请输入验证码');
return;
}
if (!this.form.check) {
this.$toast('请勾选协议');
if (!this.ensureAgreement('password')) {
return;
}
this.doLogin({});
},
submitWxLogin() {
if (!this.ensureAgreement('wx')) {
return;
}
this.proceedWxLogin();
},
doWxLogin(code, extra = {}) {
this.loginMode = 'wx';
this.wxLoading = true;
doctorWxLoginByWechat({ code, ...extra })
.then((res) => {
if (res.code !== 0 || !res.result) {
return Promise.reject(new Error(res.message || res.msg || '登录失败'));
}
const result = res.result;
if (result.need_select) {
const accounts = Array.isArray(result.accounts) ? result.accounts : [];
if (!accounts.length) {
return Promise.reject(new Error('请选择登录账号'));
}
this.wxLoading = false;
return new Promise((resolve) => {
this.$nextTick(() => {
this.$emit('need-select', {
accounts,
defaultAccountId: result.default_account_id,
defaultAccountType: result.default_account_type,
mode: 'wx',
});
resolve(null);
});
});
}
return this.finishLogin(result);
})
.catch((err) => {
this.$toast(err.message || '登录失败');
})
.finally(() => {
this.wxLoading = false;
});
},
doLogin(extra = {}) {
this.loginMode = 'password';
this.loading = true;
const payload = {
phone: this.form.mobile,
@@ -191,6 +315,25 @@ export default {
});
},
onAccountPicked(account) {
if (this.loginMode === 'wx') {
this.wxLoading = true;
uni.login({
provider: 'weixin',
success: (loginRes) => {
if (!loginRes.code) {
this.$toast('获取微信授权失败');
this.wxLoading = false;
return;
}
this.doWxLogin(loginRes.code, buildDoctorWxAuthExtra(account));
},
fail: () => {
this.$toast('微信授权失败');
this.wxLoading = false;
},
});
return;
}
this.doLogin(buildDoctorWxAuthExtra(account));
},
async finishLogin(result) {
@@ -288,4 +431,66 @@ export default {
color: #FFFFFF;
}
}
.wx-btn {
margin-top: 24rpx;
::v-deep button {
background: #07C160 !important;
}
}
.confirm-modal {
padding: 40rpx 30rpx;
background: #fff;
.confirm-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.confirm-desc {
font-size: 28rpx;
color: #666;
line-height: 1.6;
margin-bottom: 40rpx;
text-align: center;
.link {
color: #2979FF;
}
}
.confirm-footer {
display: flex;
justify-content: space-between;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
border: none;
&::after {
border: none;
}
}
.cancel-btn {
background: #F5F7FA;
color: #909399;
}
.agree-btn {
background: #6ACDBB;
color: #FFFFFF;
}
}
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<view v-if="featureEnabled" class="wx-bind-entry" @click="handleTap">
<view class="wx-bind-left">
<u-icon name="weixin-fill" color="#07C160" size="36"></u-icon>
<text class="wx-bind-label">{{ bound ? '已绑定微信' : '绑定微信' }}</text>
</view>
<view class="wx-bind-right">
<text v-if="bound && openidMasked" class="wx-bind-mask">{{ openidMasked }}</text>
<u-icon name="arrow-right" color="#C9CDD4" size="28"></u-icon>
</view>
</view>
</template>
<script>
import { getDoctorWxAuthConfig, getDoctorWxBindStatus, bindDoctorWxWechat } from '@/api/doctorAuth.js';
import { getCurrentLoginContext } from '@/utils/loginSession.js';
export default {
name: 'WxBindEntry',
data() {
return {
featureEnabled: false,
bound: false,
openidMasked: '',
loading: false,
};
},
mounted() {
uni.$on('login-account-switched', this.refresh);
},
beforeDestroy() {
uni.$off('login-account-switched', this.refresh);
},
methods: {
refresh() {
this.bound = false;
this.openidMasked = '';
const ctx = getCurrentLoginContext();
if (!ctx.phone || !ctx.account_id) {
this.featureEnabled = false;
return Promise.resolve();
}
return getDoctorWxAuthConfig()
.then((res) => {
const result = (res && res.code === 0 && res.result) ? res.result : {};
if (!result.wx_login_btn) {
this.featureEnabled = false;
return;
}
this.featureEnabled = true;
return getDoctorWxBindStatus({
phone: ctx.phone,
account_type: ctx.account_type,
account_id: ctx.account_id,
})
.then(({ data, ok }) => {
if (ok && data) {
this.bound = !!data.bound;
this.openidMasked = data.openid_masked || '';
} else {
this.bound = false;
this.openidMasked = '';
}
});
})
.catch(() => {
this.bound = false;
this.openidMasked = '';
});
},
handleTap() {
if (this.bound || this.loading) {
return;
}
// #ifdef MP-WEIXIN
this.loading = true;
uni.showLoading({ title: '绑定中...', mask: true });
new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (loginRes) => {
if (!loginRes.code) {
reject(new Error('no_code'));
return;
}
resolve(loginRes.code);
},
fail: () => reject(new Error('login_fail')),
});
})
.then((code) => bindDoctorWxWechat(code))
.then(({ data, ok, res }) => {
if (ok) {
this.bound = true;
this.openidMasked = (data && data.openid_masked) || '';
this.$toast && this.$toast('绑定成功');
} else {
this.$toast && this.$toast((res && (res.message || res.msg)) || '绑定失败');
}
})
.catch((err) => {
if (err && err.message === 'no_code') {
this.$toast && this.$toast('获取微信授权失败');
} else if (err && err.message === 'login_fail') {
this.$toast && this.$toast('微信授权失败');
} else {
this.$toast && this.$toast('绑定失败');
}
})
.finally(() => {
uni.hideLoading();
this.loading = false;
});
// #endif
// #ifndef MP-WEIXIN
this.$toast && this.$toast('请在微信小程序中使用');
// #endif
},
},
};
</script>
<style lang="scss" scoped>
.wx-bind-entry {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 1rpx solid #f0f1f3;
}
.wx-bind-left {
display: flex;
flex-direction: row;
align-items: center;
}
.wx-bind-label {
margin-left: 16rpx;
font-size: 28rpx;
color: #31353d;
}
.wx-bind-right {
display: flex;
flex-direction: row;
align-items: center;
}
.wx-bind-mask {
font-size: 24rpx;
color: #6c7380;
margin-right: 8rpx;
}
</style>

View File

@@ -26,6 +26,9 @@
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f7f8fa"
},
"usingComponents": {
"d-top-message": "/components/d-top-message/d-top-message"
}
},
{

View File

@@ -24,6 +24,7 @@ export default {
},
onShow() {
if (this.pendingAccountSelect) return;
this.$refs.doctorLogin?.loadWxConfig?.();
redirectIfLoggedIn();
},
methods: {

View File

@@ -53,6 +53,9 @@
</view>
</view>
<view class="flex-col m-t-2 white p-row-32">
<view class="wx-bind-wrap">
<wx-bind-entry ref="wxBindEntry" />
</view>
<view class="flex-row bottom-border flex-jus-sp p-col-32 flex-ali-center" v-for="(item,i) in items"
:key="i" @click="$go(item.url)">
<view class="flex-row flex-ali-center">
@@ -61,10 +64,18 @@
</view>
<u-icon name="arrow-right" color="#bcbcbc"></u-icon>
</view>
<view class="account-switch-wrap">
<account-switch-entry />
</view>
</view>
<view class="logout-wrap p-row-32 m-t-2">
<u-button :throttle-time="0" shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '96rpx' }"
@click="logout">退出登录</u-button>
</view>
</view>
</view>
<my-pharmacist v-if="parseInt(identity)==2" :info="userInfo" :avatar="getAvatar"></my-pharmacist>
<my-pharmacist v-if="parseInt(identity)==2" ref="myPharmacist" :info="userInfo" :avatar="getAvatar"></my-pharmacist>
<my-service v-if="parseInt(identity)>2" :info="userInfo" :avatar="getAvatar"></my-service>
<d-tabbar></d-tabbar>
</view>
@@ -73,16 +84,21 @@
<script>
import {
info,
logout,
personalData,
leadInfo,
servInfo
} from "@/api/all.js"
import myPharmacist from './my-pharmacist.vue'
import myService from './my-service.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
export default {
components: {
myService,
myPharmacist,
WxBindEntry,
AccountSwitchEntry,
},
data() {
return {
@@ -157,8 +173,17 @@
},
onShow() {
this.getInfo()
this.refreshWxBind()
},
methods: {
refreshWxBind() {
if (parseInt(this.identity) === 1 && this.$refs.wxBindEntry) {
this.$refs.wxBindEntry.refresh()
}
if (parseInt(this.identity) === 2 && this.$refs.myPharmacist) {
this.$refs.myPharmacist.refreshWxBind()
}
},
async getInfo() {
let res = {}
this.identity == 1 && (res = await info({
@@ -167,17 +192,25 @@
if (res.errcode == 0) {
this.doctorInfo = res.data
uni.setStorageSync('userInfo', res.data.DoctorInfo)
} else if (res.msg) {
this.$toast(res.msg);
}
},
logout() {
logout({ store_id: uni.getStorageSync('store_id') || 11001 }).then((res) => {
if (res.errcode != -1) {
uni.clearStorage()
uni.clearStorageSync()
this.$go('/pages/login/index', 2)
} else {
this.$toast(res.msg);
}
})
}
},
mounted() {
this.getInfo()
},
onShow() {
this.getInfo()
}
};
</script>
<style>
@@ -216,6 +249,18 @@
height: 40rpx;
}
.wx-bind-wrap {
padding: 0 0 0 0;
}
.account-switch-wrap {
border-top: 1rpx solid #f0f1f3;
}
.logout-wrap {
padding-bottom: 32rpx;
}
.avatar {
width: 128rpx;
height: 128rpx;

View File

@@ -18,6 +18,9 @@
</d-text>
</view>
</view>
<view class="p-32 white m-t-12">
<wx-bind-entry ref="wxBindEntry" />
</view>
<view class="p-32 white m-t-12" @click="$go('../../subPackages/sub_pharmacist/pharmacist_info?avatar='+avatar+'&info='+JSON.stringify(info))">
<view class="flex-row flex-jus-sp flex-ali-center">
<u-icon margin-left="20" label-size="32" label-color="#31353D"
@@ -37,7 +40,9 @@
</template>
<script>
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
export default {
components: { WxBindEntry },
props:{
avatar:{
type:String,
@@ -57,9 +62,16 @@
},
onLoad() {
},
mounted() {
this.refreshWxBind()
},
methods: {
refreshWxBind() {
if (this.$refs.wxBindEntry) {
this.$refs.wxBindEntry.refresh()
}
},
}
};
</script>

View File

@@ -1,14 +1,13 @@
<template>
<view>
<view class="bg" :style="{height:`calc(${navbarHeight}px + ${navbarHeight}px + 394rpx)`}">
</view>
<view class="navbar" :style="{ height: statusBarHeight+navbarHeight + 'px' }">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="text-align-cen" :style="{ height:navbarHeight + 'px' ,lineHeight: navbarHeight + 'px'}">
工作台
</view>
</view>
<view class="box" :style="{marginTop: statusBarHeight+navbarHeight + 'px'}">
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + navbarHeight + 'px' }">
<view class="box">
<view class="store_" @click="openShow=true">
{{doctorInfo.store.store.name || '默认门店'}}
<u-icon name="map-fill" color="#fff" size="28" margin-left="12"></u-icon>
@@ -41,29 +40,97 @@
<d-text text="我的名片" className="w-c fs-2"></d-text>
</view>
</view>
<view class="box_data flex-row flex-jus-sp flex-ali-center">
<view class="flex-col flex-jus-sp flex-ali-center">
<text class="box_data_nub">{{doctorInfo.wait_accept || '0'}}</text>
<label>待接诊</label>
</view>
<view class="flex-col flex-jus-sp flex-ali-center">
<text class="box_data_nub">{{doctorInfo.accepting || '0'}}</text>
<label>已接诊</label>
</view>
</view>
</view>
<view class="header-spacer" :style="{ height: headerSpacerHeight + 'px' }"></view>
<view class="content">
<view class="function p-32">
<view class="function_grid">
<view class="function_item flex-col flex-ali-center flex-jus-sp"
@click="doctorInfo.DoctorInfo.user.status==3?$go('../../subPackages/sub_workbench/workbench_upInfo/index'):$go(item.url)"
v-for="(item,i) in functions" :key="i">
<image :src="item.icon"></image>
<view>{{item.name}}</view>
<u-badge :count="i===0?doctorInfo.wait_accept:0" v-if="i===0" :offset="[-6,98]"
bgColor="#F44336"></u-badge>
<!-- 看板统计 -->
<view class="dashboard-panel">
<view class="dashboard-tabs">
<view
v-for="(tab, tabIndex) in statsRangeTabs"
:key="tab.value"
class="dashboard-tab"
:class="{ active: statsRange === tab.value }"
@click="selectStatsTab(tabIndex)"
>{{ tab.label }}</view>
</view>
<view class="dashboard-body">
<view v-if="statsLoading && !hasStatsCache(statsRange)" class="dashboard-loading-overlay">
<u-loading mode="circle" size="36"></u-loading>
</view>
<swiper
class="dashboard-swiper"
:current="statsTabIndex"
:duration="200"
@change="onStatsSwiperChange"
>
<swiper-item v-for="tab in statsRangeTabs" :key="tab.value">
<view class="dashboard-grid">
<view class="dashboard-stat">
<text class="dashboard-stat-value">{{ getStatsForRange(tab.value).prescription_count || 0 }}</text>
<text class="dashboard-stat-label">开方数量</text>
</view>
<view class="dashboard-stat">
<text class="dashboard-stat-value">{{ getStatsForRange(tab.value).wait_accept || 0 }}</text>
<text class="dashboard-stat-label">待接诊</text>
</view>
<view class="dashboard-stat">
<text class="dashboard-stat-value">{{ getStatsForRange(tab.value).accepting || 0 }}</text>
<text class="dashboard-stat-label">接诊中</text>
</view>
<view class="dashboard-stat">
<text class="dashboard-stat-value">{{ getStatsForRange(tab.value).completed || 0 }}</text>
<text class="dashboard-stat-label">已完成</text>
</view>
<view class="dashboard-stat dashboard-stat-wide">
<text class="dashboard-stat-value amount">{{ formatStatsAmount(getStatsForRange(tab.value).prescription_amount) }}</text>
<text class="dashboard-stat-label">处方金额</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
<view class="function p-32" v-if="onlineConsultationConfigReady && canUseOnlineConsultation">
<view class="function_grid">
<view
class="function_item flex-col flex-ali-center flex-jus-sp"
v-for="(item, i) in receptionGridEntries"
:key="i"
@click="goReceptionEntry(item.url)"
>
<image :src="item.icon"></image>
<view>{{ item.name }}</view>
<u-badge
:count="i === 0 ? doctorInfo.wait_accept : 0"
v-if="i === 0"
:offset="[-6, 98]"
bgColor="#F44336"
></u-badge>
</view>
</view>
</view>
<view class="function p-32" v-else-if="onlineConsultationConfigReady">
<view
class="offline-entry-row flex-row flex-ali-center"
@click="goReceptionEntry(offlineReceptionEntry.url)"
>
<image class="offline-entry-icon" :src="offlineReceptionEntry.icon"></image>
<view class="offline-entry-body flex-col flex-1">
<text class="offline-entry-title">{{ offlineReceptionEntry.name }}</text>
<text class="offline-entry-desc">到店开方处理线下挂号</text>
</view>
<u-badge
v-if="doctorInfo.wait_accept > 0"
:count="doctorInfo.wait_accept"
:offset="[-8, 8]"
bgColor="#F44336"
></u-badge>
<u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon>
</view>
</view>
<block>
@@ -107,6 +174,13 @@
:type="item.consultation_channel === 'online' ? 'primary' : 'info'"
size="mini"
></u-tag>
<u-tag
v-if="item.special_prescription_patient_record"
class="wb-tag"
:text="item.special_prescription_patient_record.status === 0 ? '特色方·待应用' : '特色方'"
type="warning"
size="mini"
></u-tag>
</view>
<text class="wb-time">{{
item.consultation_channel === 'online' ? formatAcceptingMsgTime(item) : acceptingOfflineRegisterTimeText(item)
@@ -319,6 +393,7 @@
</view>
</u-popup>
<d-top-message ref="topMessage"></d-top-message>
<d-tabbar></d-tabbar>
</view>
</template>
@@ -338,10 +413,57 @@ import {
} from '../../api/consult';
import {
getAcceptingPatientList as fetchAcceptingPatientListApi,
getOnlineConsultationPatientListApi
getOnlineConsultationPatientListApi,
getWorkbenchStats,
} from '../../api/reception';
import { normalizeRoomId } from '@/utils/chat/chatRoomManager.js';
import { formatSessionRelativeTime } from '@/utils/chat/sessionListFormat.js';
import { checkOnlineConsultationConfigApi } from '../../api/storeConsultation.js';
/** 房间 ID 归一化(避免引入 chatRoomManager 整模块) */
function normalizeRoomId(id) {
return String(id == null ? '' : id).trim();
}
/** 会话列表相对时间(与 utils/chat/sessionListFormat.js 一致) */
function formatSessionRelativeTime(item) {
if (!item) return '';
const t =
item.last_message_time ||
item.updated_at ||
item.created_at ||
item.register_time;
if (t == null || t === '') return '';
const ms = typeof t === 'number' ? (t > 1e12 ? t : t * 1000) : new Date(t).getTime();
if (Number.isNaN(ms)) return '';
const d = new Date(ms);
const diff = Math.floor((Date.now() - ms) / 60000);
if (diff < 1) return '刚刚';
if (diff < 60) return `${diff}分钟前`;
const h = Math.floor(diff / 60);
if (h < 24) return `${h}小时前`;
const pad = (n) => (n < 10 ? '0' + n : '' + n);
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
const WORKBENCH_STATS_RANGE_KEY = 'workbench_stats_range';
const OFFLINE_RECEPTION_ENTRY = {
name: '线下接诊',
icon: '../../static/image/zxzx.png',
url: '../../subPackages/sub_workbench/workbench_consult?type=1',
};
const ONLINE_RECEPTION_ENTRY = {
name: '在线接诊',
icon: '../../static/image/xsfz.png',
url: '../../subPackages/sub_online_reception/pages/list',
};
const RECEPTION_GRID_ENTRIES = [OFFLINE_RECEPTION_ENTRY, ONLINE_RECEPTION_ENTRY];
const EMPTY_WORKBENCH_STATS = {
prescription_count: 0,
wait_accept: 0,
accepting: 0,
completed: 0,
prescription_amount: '0.000',
};
export default {
data() {
return {
@@ -353,15 +475,10 @@ export default {
loading: '努力加载中',
nomore: '实在没有了'
},
functions: [{
name: '到店接诊',
icon: '../../static/image/zxzx.png',
url: '../../subPackages/sub_workbench/workbench_consult?type=1'
}, {
name: '在线接诊',
icon: '../../static/image/xsfz.png',
url: '../../subPackages/sub_online_reception/pages/list'
}],
receptionGridEntries: RECEPTION_GRID_ENTRIES,
offlineReceptionEntry: OFFLINE_RECEPTION_ENTRY,
canUseOnlineConsultation: false,
onlineConsultationConfigReady: false,
states: {
1: {
name: '待接诊',
@@ -398,8 +515,26 @@ export default {
see_rate: "",
acceptingPatientList: [], // 正在接诊的患者列表
refreshTimer: null,
refreshIntervalMs: 10000,
refreshIntervalMs: 20000,
isReceptionRefreshing: false,
statsRange: 'today',
statsTabIndex: 0,
statsSwiperChanging: false,
statsRangeTabs: [
{ label: '今日', value: 'today' },
{ label: '本周', value: 'week' },
{ label: '本月', value: 'month' },
{ label: '近一月', value: 'last_30d' },
],
statsCache: {
today: null,
week: null,
month: null,
last_30d: null,
},
workbenchStats: { ...EMPTY_WORKBENCH_STATS },
statsLoading: false,
statsInitialized: false,
};
},
watch: {
@@ -421,7 +556,13 @@ export default {
}
},
computed: {
headerSpacerHeight() {
const contentPx = typeof uni.upx2px === 'function' ? uni.upx2px(312) : 156;
return this.statusBarHeight + this.navbarHeight + contentPx;
},
},
onLoad() {
this.initStatsRangeFromStorage();
},
onUnload() {
this.stopRefreshTimer();
@@ -451,14 +592,32 @@ export default {
}
},
methods: {
initStatsRangeFromStorage() {
const saved = uni.getStorageSync(WORKBENCH_STATS_RANGE_KEY);
const tabIndex = this.statsRangeTabs.findIndex((tab) => tab.value === saved);
if (tabIndex >= 0) {
this.statsRange = saved;
this.statsTabIndex = tabIndex;
}
},
notifyWorkbenchRefreshed() {
if (this.$refs.topMessage) {
this.$refs.topMessage.show({ text: '数据已刷新', type: 'success' });
}
},
refreshWorkbenchData() {
const silentStats = this.statsInitialized || this.hasStatsCache(this.statsRange);
return Promise.all([
this.getNotice(),
this.getInfo(),
this.getStore(),
this.getList(),
this.getAcceptingPatientList()
]);
this.getAcceptingPatientList(),
this.fetchOnlineConsultationConfig(),
this.fetchWorkbenchStats({ silent: silentStats })
]).then(() => {
this.notifyWorkbenchRefreshed();
});
},
refreshReceptionData() {
if (this.isReceptionRefreshing) {
@@ -468,8 +627,12 @@ export default {
return Promise.all([
this.getInfo(),
this.getList(),
this.getAcceptingPatientList()
]).finally(() => {
this.getAcceptingPatientList(),
this.fetchOnlineConsultationConfig(),
this.fetchWorkbenchStats({ silent: true })
]).then(() => {
this.notifyWorkbenchRefreshed();
}).finally(() => {
this.isReceptionRefreshing = false;
});
},
@@ -505,6 +668,33 @@ export default {
this.status = 'loading'
this.page++
},
goReceptionEntry(url) {
const doctorInfo = this.doctorInfo || {};
const user = doctorInfo.DoctorInfo && doctorInfo.DoctorInfo.user;
if (user && user.status === 3) {
this.$go('../../subPackages/sub_workbench/workbench_upInfo/index');
return;
}
this.$go(url);
},
fetchOnlineConsultationConfig() {
const storeId = uni.getStorageSync('store_id') || '11001';
return checkOnlineConsultationConfigApi({ store_id: storeId })
.then((res) => {
const payload = (res && res.result) || (res && res.data) || {};
if (res && (res.code === 0 || res.errcode === 0)) {
this.canUseOnlineConsultation = !!payload.can_use;
} else {
this.canUseOnlineConsultation = false;
}
})
.catch(() => {
this.canUseOnlineConsultation = false;
})
.finally(() => {
this.onlineConsultationConfigReady = true;
});
},
getNotice() {
return getNotice({
page_size: 9999,
@@ -567,7 +757,91 @@ export default {
}
this.openShow = false
this.getInfo()
this.onlineConsultationConfigReady = false
this.refreshWorkbenchData()
})
},
selectStatsTab(index) {
if (this.statsSwiperChanging) return;
const tab = this.statsRangeTabs[index];
if (!tab) return;
this.statsTabIndex = index;
this.changeStatsRange(tab.value);
},
onStatsSwiperChange(e) {
const index = e.detail.current;
const tab = this.statsRangeTabs[index];
if (!tab || tab.value === this.statsRange) return;
this.statsSwiperChanging = true;
this.statsTabIndex = index;
this.changeStatsRange(tab.value);
this.$nextTick(() => {
this.statsSwiperChanging = false;
});
},
changeStatsRange(range) {
if (this.statsRange === range && this.hasStatsCache(range)) {
this.workbenchStats = { ...this.getStatsForRange(range) };
return;
}
this.statsRange = range;
uni.setStorageSync(WORKBENCH_STATS_RANGE_KEY, range);
const tabIndex = this.statsRangeTabs.findIndex((tab) => tab.value === range);
if (tabIndex >= 0) {
this.statsTabIndex = tabIndex;
}
if (this.hasStatsCache(range)) {
this.workbenchStats = { ...this.getStatsForRange(range) };
this.fetchWorkbenchStats({ silent: true });
return;
}
this.fetchWorkbenchStats({ silent: false });
},
hasStatsCache(range) {
return this.statsCache[range] != null;
},
getStatsForRange(range) {
return this.statsCache[range] || { ...EMPTY_WORKBENCH_STATS };
},
normalizeWorkbenchStats(payload = {}) {
return {
prescription_count: payload.prescription_count || 0,
wait_accept: payload.wait_accept || 0,
accepting: payload.accepting || 0,
completed: payload.completed || 0,
prescription_amount: payload.prescription_amount || '0.000',
};
},
formatStatsAmount(amount) {
const num = Number(amount)
if (Number.isNaN(num)) return '0.000'
return num.toFixed(3)
},
fetchWorkbenchStats(options = {}) {
const silent = options.silent === true;
const range = options.range || this.statsRange;
const hasCache = this.hasStatsCache(range);
if (!silent && !hasCache) {
this.statsLoading = true;
}
return getWorkbenchStats({
store_id: uni.getStorageSync('store_id') || '11001',
range,
}).then((res) => {
const payload = (res && res.result) || (res && res.data) || {}
if (res && (res.code === 0 || res.errcode === 0)) {
const stats = this.normalizeWorkbenchStats(payload);
this.$set(this.statsCache, range, stats);
if (range === this.statsRange) {
this.workbenchStats = { ...stats };
}
this.statsInitialized = true;
}
}).catch(() => {
this.$toast && this.$toast('看板数据加载失败')
return Promise.reject(new Error('看板数据加载失败'));
}).finally(() => {
this.statsLoading = false;
})
},
// 消息列表
@@ -762,15 +1036,6 @@ page {
}
</style>
<style lang="scss" scoped>
.bg {
width: 100vw;
background: #6ACDBB;
position: fixed;
left: 0;
top: 0;
z-index: 0;
}
.navbar {
background: transparent;
width: 100vw;
@@ -784,11 +1049,27 @@ page {
z-index: 99999;
}
.header-wrap {
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 100;
background: #6ACDBB;
border-radius: 0 60rpx 60rpx;
padding: 0 32rpx 32rpx;
}
.header-spacer {
width: 100%;
flex-shrink: 0;
}
.content {
padding: 0 24rpx;
position: relative;
z-index: 2;
margin-top: 30rpx;
z-index: 1;
margin-top: 0;
}
/* 顶部门店选择器扁平化 */
@@ -864,8 +1145,7 @@ page {
.box {
position: relative;
padding: 0 32rpx;
height: 318rpx;
min-height: 200rpx;
&_info {
display: flex;
@@ -897,28 +1177,6 @@ page {
}
}
&_data {
width: 100%;
height: 110rpx;
font-size: 24rpx;
color: #FFFFFF;
margin-top: 40rpx;
background: rgba(255,255,255,0.15);
border-radius: 16rpx;
padding: 0 40rpx;
view {
height: 84rpx;
min-width: 120rpx;
}
&_nub {
font-size: 40rpx;
font-weight: bold;
margin-bottom: 4rpx;
}
}
.store_ {
display: inline-flex;
align-items: center;
@@ -933,7 +1191,104 @@ page {
}
}
/* 功能菜单:完全恢复初始原始布局及代码 */
/* 看板统计 */
.dashboard-panel {
background: #fff;
border-radius: 16rpx;
padding: 24rpx 24rpx 8rpx;
margin-top: -24rpx;
margin-bottom: 20rpx;
position: relative;
z-index: 2;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.dashboard-tabs {
display: flex;
flex-direction: row;
margin-bottom: 24rpx;
background: #f5f6f8;
border-radius: 12rpx;
padding: 6rpx;
}
.dashboard-tab {
flex: 1;
text-align: center;
font-size: 24rpx;
color: #6c7380;
padding: 12rpx 0;
border-radius: 8rpx;
}
.dashboard-tab.active {
background: #fff;
color: #6acdbb;
font-weight: 600;
}
.dashboard-body {
position: relative;
min-height: 280rpx;
}
.dashboard-swiper {
height: 280rpx;
}
.dashboard-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.72);
}
.dashboard-grid {
display: flex;
flex-wrap: wrap;
min-height: 280rpx;
}
.dashboard-stat {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 0 24rpx;
}
.dashboard-stat-wide {
width: 100%;
border-top: 1rpx solid #f0f1f3;
margin-top: 8rpx;
padding-top: 24rpx;
}
.dashboard-stat-value {
font-size: 36rpx;
font-weight: 700;
color: #0d111a;
line-height: 1.2;
}
.dashboard-stat-value.amount {
font-size: 40rpx;
color: #6acdbb;
}
.dashboard-stat-label {
font-size: 22rpx;
color: #6c7380;
margin-top: 8rpx;
}
/* 功能菜单:接诊入口 */
.function {
background-color: #fff;
border-radius: 16rpx;
@@ -960,6 +1315,42 @@ page {
}
}
.offline-entry-row {
position: relative;
width: 100%;
min-height: 132rpx;
padding: 24rpx 28rpx;
box-sizing: border-box;
background: linear-gradient(135deg, #f0faf8 0%, #e8f7f4 100%);
border-radius: 16rpx;
border: 1rpx solid rgba(106, 205, 187, 0.25);
}
.offline-entry-icon {
width: 88rpx;
height: 88rpx;
flex-shrink: 0;
}
.offline-entry-body {
margin-left: 24rpx;
min-width: 0;
}
.offline-entry-title {
font-size: 32rpx;
font-weight: 600;
color: #0d111a;
line-height: 44rpx;
}
.offline-entry-desc {
margin-top: 6rpx;
font-size: 24rpx;
color: #6c7380;
line-height: 34rpx;
}
/* 通用板块平铺和消息通知:完全恢复至初始样式 */
.news {
background-color: #fff;

View File

@@ -28,6 +28,7 @@
<!-- 操作面板通栏 -->
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
<view
class="setting-item action-item"
@@ -48,10 +49,11 @@
<script>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { ClinicPageLayout, AccountSwitchEntry },
components: { ClinicPageLayout, AccountSwitchEntry, WxBindEntry },
data() {
return {
displayName: '',
@@ -68,6 +70,7 @@ export default {
const user = uni.getStorageSync('clinic_admin_user') || {}
this.displayName = user.nick_name || user.name || user.account || '管理员'
this.storeName = user.store_name || (user.store && user.store.name) || '-'
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
},
methods: {
logout() {

View File

@@ -48,6 +48,8 @@
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
<view
@@ -84,6 +86,8 @@ import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/co
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
@@ -92,7 +96,7 @@ import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { ClinicSalespersonPageLayout, AccountSwitchEntry },
components: { ClinicSalespersonPageLayout, AccountSwitchEntry, WxBindEntry },
data() {
@@ -124,6 +128,8 @@ export default {
this.loadProfile()
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
},
methods: {

View File

@@ -35,28 +35,17 @@
<d-text :text="userInfo.title.name" color="#31353D"></d-text>
</view> -->
</view>
<view class="p-row-32 m-t-8 white b-r-8">
<account-switch-entry />
</view>
<view class="p-row-32 m-t-8" style="height: 100rpx;">
<u-button :throttle-time="0" shape="circle" :custom-style="{backgroundColor:'#6ACDBB',color:'#fff',heigth:'96rpx'}" @click="logout">
退出登录</u-button>
</view>
</view>
</template>
<script>
import {
info,
logout,
toUpload,
} from '@/api/all.js'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import { chooseAvatarImage } from '@/common/js/select-avatar-image.js';
export default {
components: { AccountSwitchEntry },
components: {},
data() {
return {
doctorInfo: []
@@ -112,29 +101,6 @@
},
});
},
logout() {
logout({store_id: uni.getStorageSync('store_id') || 11001,}).then((res) => {
if (res.errcode != -1) {
uni.clearStorage()
uni.clearStorageSync()
this.$go('/pages/login/index', 2)
} else {
this.$toast(res.msg);
}
})
}
// // 医生详情
// getInfo() {
// info({
// store_id: uni.getStorageSync('store_id') || '11001'
// }).then((res) => {
// // console.log(res.data, 'work-res');
// if (res.errcode == 0) {
// this.doctorInfo = res.data
// }
// })
// }
},
mounted() {
this.getDoctor()

View File

@@ -9,6 +9,7 @@
</view>
</view>
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
<view class="setting-item action-item" @click="logout">
<text class="danger-text">退出登录</text>
@@ -21,11 +22,12 @@
<script>
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { BusinessPageLayout, AccountSwitchEntry },
components: { BusinessPageLayout, AccountSwitchEntry, WxBindEntry },
provide() { return { businessContext: getPlatformAdminContext() } },
data() { return { displayName: '', roleText: '平台管理员' } },
computed: {
@@ -35,6 +37,7 @@ export default {
const user = uni.getStorageSync('platform_admin_user') || {}
this.displayName = user.nick_name || '管理员'
this.roleText = (user.roles && user.roles.name) || '平台管理员'
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
},
methods: {
logout() {

View File

@@ -9,6 +9,7 @@
</view>
</view>
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
<view class="setting-item" @click="logout"><text class="danger-text">退出登录</text></view>
</view>
@@ -19,11 +20,12 @@
<script>
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import { getSalespersonContext } from '@/subPackages/sub_salesperson/common/context.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { BusinessPageLayout, AccountSwitchEntry },
components: { BusinessPageLayout, AccountSwitchEntry, WxBindEntry },
provide() { return { businessContext: getSalespersonContext() } },
data() { return { displayName: '', roleText: '业务员' } },
computed: { avatarText() { return (this.displayName || '业').slice(0, 1) } },
@@ -31,6 +33,7 @@ export default {
const user = uni.getStorageSync('salesperson_user') || {}
this.displayName = user.nick_name || '业务员'
this.roleText = (user.roles && user.roles.name) || '业务员'
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
},
methods: {
logout() {

View File

@@ -35,6 +35,55 @@
<text class="register-mode-hint__text">{{ registerModeHint }}</text>
</view>
<view
v-if="!isSalespersonTransferMode && hasSpecialPrescriptionRecord"
class="special-rx-card mx-32 m-t-12"
>
<view class="special-rx-card__inner flex-row flex-ali-center">
<u-image
v-if="specialPrescriptionRecord.special_prescription && specialPrescriptionRecord.special_prescription.cover_image"
width="96rpx"
height="96rpx"
border-radius="12"
:src="specialPrescriptionRecord.special_prescription.cover_image"
/>
<view class="flex-1 m-l-16">
<view class="fs-28 font-bold text-gray">
{{ (specialPrescriptionRecord.special_prescription && specialPrescriptionRecord.special_prescription.name) || '特色方' }}
</view>
<view class="fs-24 color-sub m-t-8">
剂量{{ specialPrescriptionRecord.dose_count }}
</view>
<view class="m-t-8">
<u-tag
v-if="Number(specialPrescriptionRecord.status) === 0"
text="待导入"
type="warning"
size="mini"
/>
<u-tag
v-else-if="Number(specialPrescriptionRecord.status) === 1"
text="已应用"
type="success"
size="mini"
/>
<u-tag v-else text="已完成" type="info" size="mini" />
</view>
</view>
</view>
<view class="special-rx-card__action m-t-16">
<u-button
size="default"
type="primary"
:loading="applyingSpecialPrescription"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', border: 'none', width: '100%' }"
@click="handleApplySpecialPrescription"
>
导入
</u-button>
</view>
</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>
@@ -539,6 +588,7 @@ import {
getProductListDoctorReception,
getPatientInfoByPatientId,
getPatientItem,
applySpecialPrescriptionApi,
getProcessRuleList,
getCommonPrescriptionListApi,
saveWestCommonPrescriptionApi,
@@ -571,6 +621,10 @@ import {
getClinicSalespersonSeePriceConfig,
getClinicSalespersonProcessRuleList,
} from '@/api/clinicSalesperson.js';
import {
pickSpecialPrescriptionRecord,
applySpecialPrescriptionPayload,
} from './utils/specialPrescription.js';
const PENDING_RX_FROM_CHAT = 'xk_pending_rx_from_chat';
const SALESPERSON_TRANSFER_STORAGE_KEY = 'salesperson_transfer';
@@ -676,6 +730,9 @@ export default {
sendMode: 0,
selectedStoreId: null,
registerOrderType: null,
specialPrescriptionRecord: null,
applyingSpecialPrescription: false,
pendingImportSpecial: false,
/** 在线复诊中药术语表HTTP 拉取,内存缓存) */
traditionalTcmData: null,
@@ -803,6 +860,9 @@ export default {
if (this.registerOrderType === null || this.registerOrderType === undefined || this.registerOrderType === '') return '';
return this.isOnlineRevisit ? '当前:在线复诊开方(提交后将向患者发送处方消息)' : '当前:线下开方';
},
hasSpecialPrescriptionRecord() {
return !!this.specialPrescriptionRecord;
},
navbarTitle() {
if (this.isSalespersonTransferMode) {
return '传方';
@@ -905,6 +965,7 @@ export default {
}
this.patientId = options.patient_id || '';
this.pendingReusePrescriptionId = options.reuse_prescription_id ? String(options.reuse_prescription_id) : '';
this.pendingImportSpecial = String(options.import_special) === '1';
this.isCommonPrescription = String(options.save_common) === '1';
if (!this.isCommonPrescription) {
uni.removeStorageSync('add');
@@ -969,6 +1030,10 @@ export default {
await this.maybeApplyReusePrescription();
if (this.activeCategory === 1) await this.checkAndShowTransferTip();
this.applyPendingExperienceDrugFromChat();
if (this.pendingImportSpecial && this.hasSpecialPrescriptionRecord) {
await this.handleApplySpecialPrescription();
this.pendingImportSpecial = false;
}
}
// #ifdef MP-WEIXIN
this.$nextTick(() => {
@@ -1099,6 +1164,7 @@ export default {
async loadRegisterOrderType() {
if (!this.registerId) {
this.registerOrderType = null;
this.specialPrescriptionRecord = null;
return;
}
try {
@@ -1109,9 +1175,11 @@ export default {
} else {
this.registerOrderType = null;
}
this.specialPrescriptionRecord = pickSpecialPrescriptionRecord(data);
} catch (e) {
console.warn(e);
this.registerOrderType = null;
this.specialPrescriptionRecord = null;
}
},
async loadBasicConfigData() {
@@ -1386,6 +1454,22 @@ export default {
this.saveToLocalStorage();
},
handleOpenCommonPrescriptionModal() { this.showCommonPrescriptionModal = true; },
async handleApplySpecialPrescription() {
if (!this.registerId || this.applyingSpecialPrescription) return;
this.applyingSpecialPrescription = true;
try {
const res = await applySpecialPrescriptionApi({ register_id: this.registerId });
const data = res.result || res.data || res;
applySpecialPrescriptionPayload(this, data);
await this.loadRegisterOrderType();
this.$toast(`已导入特色方:${data.special_prescription_name || ''}`);
} catch (error) {
console.error('导入特色方失败:', error);
this.$toast('导入特色方失败,请稍后重试');
} finally {
this.applyingSpecialPrescription = false;
}
},
async handleSelectCommonPrescription(item) {
try {
const payload = item && item.apply_payload;
@@ -2541,6 +2625,18 @@ export default {
color: #008771;
line-height: 1.5;
}
.special-rx-card {
padding: 20rpx 24rpx;
background: #fffaf5;
border-radius: 12rpx;
border: 1rpx solid #ffe0c2;
}
.special-rx-card__inner {
width: 100%;
}
.special-rx-card__action {
width: 100%;
}
.bottom {
position: fixed;
bottom: 0;

View File

@@ -0,0 +1,54 @@
/**
* 从 patient-item 等接口响应中解析特色方选方记录
*/
export function pickSpecialPrescriptionRecord(data) {
if (!data || typeof data !== 'object') {
return null;
}
return (
data.special_prescription_patient_record
|| data.specialPrescriptionPatientRecord
|| null
);
}
/**
* 将 apply 接口返回的数据写入处方页 state
*/
export function applySpecialPrescriptionPayload(page, data) {
const categoryMap = { west: 2, chinese: 1, granular: 1 };
const prescriptionType = data.prescription_type || 'chinese';
page.activeCategory = categoryMap[prescriptionType] || 1;
page.loadCurrentCategoryDrugs();
const recipes = data.recipes || [];
page.currentDrugs.splice(0, page.currentDrugs.length);
recipes.forEach((recipe) => {
const drugId = recipe.drug_id || recipe.id;
page.currentDrugs.push(page.prepareDrugForCart({
index_id: recipe.id,
id: drugId,
drug_id: drugId,
drug_name: recipe.drug_name || recipe.name,
number: recipe.number || 1,
price: recipe.price || 0,
buy_price: recipe.buy_price,
way_id: recipe.way_id || 0,
}));
});
page.chineseConfig = {
...page.chineseConfig,
ruleType: data.rule_type ?? 1,
dosage: data.dosage ?? data.dose_count ?? 7,
dayDosage: data.day_dosage ?? 2,
packageMethodId: data.package_method_id ?? 2,
};
if (data.clinical_diagnose) {
page.diagnosisText = data.clinical_diagnose;
const names = String(data.clinical_diagnose).split('').filter(Boolean);
page.diagnoses = names.map((name, index) => ({ id: `temp_${index}`, name }));
}
if (data.doctor_order) {
page.medicalAdvice = data.doctor_order;
}
page.saveToLocalStorage();
}

View File

@@ -67,6 +67,54 @@
</view>
</view>
<!-- 特色方信息 -->
<view v-if="infoList.special_prescription_patient_record" class="modern-card">
<view class="card-title">特色方</view>
<view class="special-rx-info flex-row flex-ali-center">
<u-image
v-if="infoList.special_prescription_patient_record.special_prescription && infoList.special_prescription_patient_record.special_prescription.cover_image"
width="96rpx"
height="96rpx"
border-radius="12"
:src="infoList.special_prescription_patient_record.special_prescription.cover_image"
/>
<view class="flex-1 m-l-16">
<view class="info-value font-bold">
{{ (infoList.special_prescription_patient_record.special_prescription && infoList.special_prescription_patient_record.special_prescription.name) || '特色方' }}
</view>
<view class="info-row" style="border-bottom: none; padding: 8rpx 0 0;">
<text class="info-label">剂量</text>
<text class="info-value">{{ infoList.special_prescription_patient_record.dose_count }} </text>
</view>
<view class="m-t-8">
<u-tag
v-if="Number(infoList.special_prescription_patient_record.status) === 0"
text="待导入"
type="warning"
size="mini"
/>
<u-tag
v-else-if="Number(infoList.special_prescription_patient_record.status) === 1"
text="已应用"
type="success"
size="mini"
/>
<u-tag v-else text="已完成" type="info" size="mini" />
</view>
</view>
</view>
<view class="m-t-24">
<u-button
type="primary"
shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', border: 'none' }"
@click="goImportSpecialPrescription"
>
导入
</u-button>
</view>
</view>
<!-- 健康信息 -->
<view class="modern-card">
<view class="card-title">健康信息</view>
@@ -201,6 +249,7 @@ import {
receptionApi,
endOfDiagnosisApi
} from '@/api/reception.js'
import { pickSpecialPrescriptionRecord } from '@/subPackages/sub_workbench/prescription_v2/utils/specialPrescription.js'
function emptyHealthInquiry() {
return {
@@ -235,7 +284,8 @@ function normalizeRegisterDetail(raw) {
store: store && store.name !== undefined ? store : {
name: ''
},
healthInquery: hi || emptyHealthInquiry()
healthInquery: hi || emptyHealthInquiry(),
special_prescription_patient_record: pickSpecialPrescriptionRecord(raw),
})
}
@@ -471,7 +521,7 @@ export default {
this.$toast(this.apiMsg(res))
return
}
const raw = res.result || res.data
const raw = res.result || res.data || res
this.infoList = normalizeRegisterDetail(raw)
if (this.infoList.status !== 7) {
if (this.rxTabIndex === 0) {
@@ -489,6 +539,19 @@ export default {
const patientId = this.infoList.user_patient_id;
this.$go(`/subPackages/sub_workbench/prescription_v2/index?register_id=${registerId}&patient_id=${patientId}`);
},
goImportSpecialPrescription() {
const registerId = this.infoList.id;
const patientId = this.infoList.user_patient_id;
if (!registerId) {
this.$toast('挂号信息无效');
return;
}
let url = `/subPackages/sub_workbench/prescription_v2/index?register_id=${registerId}&import_special=1`;
if (patientId) {
url += `&patient_id=${patientId}`;
}
this.$go(url);
},
goToReusePrescription(prescriptionId) {
const registerId = this.infoList.id;
const patientId = this.infoList.user_patient_id;

View File

@@ -0,0 +1,55 @@
/** doctor-wx-auth 请求与绑定状态查询共用的登录态解析(无 API 依赖,避免与 common/js/index 循环引用) */
export const ADMIN_TOKEN_KEYS = {
platform_admin: 'platform_admin_token',
salesperson: 'salesperson_token',
clinic_salesperson: 'clinic_salesperson_token',
clinic_admin: 'clinic_admin_token',
};
export const ADMIN_USER_KEYS = {
platform_admin: 'platform_admin_user',
salesperson: 'salesperson_user',
clinic_salesperson: 'clinic_salesperson_user',
clinic_admin: 'clinic_admin_user',
};
/** 当前有效的 admin loginMode须同时有 token */
export function resolveActiveAdminLoginMode() {
const stored = uni.getStorageSync('loginMode');
if (stored && ADMIN_TOKEN_KEYS[stored] && uni.getStorageSync(ADMIN_TOKEN_KEYS[stored])) {
return stored;
}
return '';
}
/** doctor-wx-auth 接口应使用的 token优先当前 loginMode 对应 admin否则医生/药师 token不 fallback 其它 admin token */
export function resolveDoctorWxAuthToken() {
const loginMode = resolveActiveAdminLoginMode();
if (loginMode) {
return uni.getStorageSync(ADMIN_TOKEN_KEYS[loginMode]) || '';
}
return uni.getStorageSync('token') || '';
}
/** 当前登录上下文(手机号 + 账号维度,供 wx-bind-status 等) */
export function getCurrentLoginContext() {
const loginMode = resolveActiveAdminLoginMode();
if (loginMode) {
const user = uni.getStorageSync(ADMIN_USER_KEYS[loginMode]) || {};
return {
phone: String(user.phone || '').trim(),
account_type: 'admin',
account_id: Number(user.id || 0),
token: uni.getStorageSync(ADMIN_TOKEN_KEYS[loginMode]) || '',
};
}
const loginInfo = uni.getStorageSync('loginInfo') || uni.getStorageSync('logInfo') || {};
return {
phone: String(loginInfo.mobile || loginInfo.phone || '').trim(),
account_type: 'service_user',
account_id: Number(loginInfo.id || uni.getStorageSync('login_id') || 0),
token: uni.getStorageSync('token') || '',
};
}

View File

@@ -3,6 +3,13 @@ import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js';
import { getPlatformAdminMyInfo } from '@/api/platformAdmin.js';
import { getSalespersonMyInfo } from '@/api/salesperson.js';
import { getUserInfo } from '@/api/all.js';
import {
resolveActiveAdminLoginMode,
getCurrentLoginContext,
resolveDoctorWxAuthToken,
} from '@/utils/doctorWxAuthToken.js';
export { getCurrentLoginContext, resolveDoctorWxAuthToken };
const ADMIN_HOME = {
platform_admin: '/subPackages/sub_platform_admin/home/index',
@@ -11,13 +18,6 @@ const ADMIN_HOME = {
clinic_admin: '/subPackages/sub_clinic_admin/home/index',
};
const ADMIN_TOKEN_KEYS = {
platform_admin: 'platform_admin_token',
salesperson: 'salesperson_token',
clinic_salesperson: 'clinic_salesperson_token',
clinic_admin: 'clinic_admin_token',
};
function applyServiceUserProfile(user) {
if (!user || !user.id) return;
uni.setStorageSync('loginInfo', user);
@@ -60,18 +60,7 @@ export function clearAllLoginStorage() {
}
function inferLoginMode() {
const stored = uni.getStorageSync('loginMode');
if (stored && ADMIN_TOKEN_KEYS[stored] && uni.getStorageSync(ADMIN_TOKEN_KEYS[stored])) {
return stored;
}
const modes = ['platform_admin', 'salesperson', 'clinic_salesperson', 'clinic_admin'];
for (let i = 0; i < modes.length; i++) {
const mode = modes[i];
if (uni.getStorageSync(ADMIN_TOKEN_KEYS[mode])) {
return mode;
}
}
return '';
return resolveActiveAdminLoginMode();
}
/** 已登录时返回应跳转的首页,否则 null */