fix(login): 密码登录补充密码非空前端校验

- doctor-login submitLogin 增加「请输入密码」前置校验,
  与 getCode 一致,避免空密码误触发登录请求
- 配合后端 service_user 空密码回退 PC 医生/药师密码校验
This commit is contained in:
李琦
2026-07-19 16:07:46 +08:00
parent 4f6ca4b2de
commit cb290f3e95
53 changed files with 4584 additions and 1056 deletions

View File

@@ -0,0 +1,12 @@
---
description:
alwaysApply: true
---
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化
6. 微信小程序 `.vue` 模板(会编译成 WXML禁止使用 `??``?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定

View File

@@ -8,3 +8,5 @@ alwaysApply: true
3. 有封装好的方法、组件需要复用,不要重复造轮子 3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show 4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化 5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化
6. 微信小程序 `.vue` 模板(会编译成 WXML禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定

View File

@@ -15,7 +15,7 @@
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues"> <option name="myValues">
<value> <value>
<list size="24"> <list size="25">
<item index="0" class="java.lang.String" itemvalue="nobr" /> <item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" /> <item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" /> <item index="2" class="java.lang.String" itemvalue="comment" />
@@ -40,6 +40,7 @@
<item index="21" class="java.lang.String" itemvalue="u-subsection" /> <item index="21" class="java.lang.String" itemvalue="u-subsection" />
<item index="22" class="java.lang.String" itemvalue="u-tabbar" /> <item index="22" class="java.lang.String" itemvalue="u-tabbar" />
<item index="23" class="java.lang.String" itemvalue="u-navbar" /> <item index="23" class="java.lang.String" itemvalue="u-navbar" />
<item index="24" class="java.lang.String" itemvalue="u-field" />
</list> </list>
</value> </value>
</option> </option>

View File

@@ -5,8 +5,9 @@ const PREFIX = '/newApi/clinic-admin';
/** 解析 Laravel jok 或 Yii 响应 */ /** 解析 Laravel jok 或 Yii 响应 */
export function unwrapClinicRes(res) { export function unwrapClinicRes(res) {
if (!res) return null; if (!res) return null;
if (res.code === 0 && res.result !== undefined) return res.result; // code 可能是 0 / "0"
if (res.errcode != null && res.errcode !== -1) return res.data; if (Number(res.code) === 0 && res.result !== undefined) return res.result;
if (res.errcode != null && Number(res.errcode) !== -1) return res.data;
if (res.result !== undefined) return res.result; if (res.result !== undefined) return res.result;
return res.data != null ? res.data : res; return res.data != null ? res.data : res;
} }
@@ -20,7 +21,10 @@ function clinicRequest(options) {
}, },
}).then((res) => { }).then((res) => {
const data = unwrapClinicRes(res); const data = unwrapClinicRes(res);
const ok = res && (res.code === 0 || (res.errcode != null && Number(res.errcode) !== -1)); const ok = !!(res && (
Number(res.code) === 0
|| (res.errcode != null && Number(res.errcode) !== -1)
));
return { res, data, ok }; return { res, data, ok };
}); });
} }

View File

@@ -1,110 +1,24 @@
import { req } from '@/common/js/index.js'; /**
import { unwrapClinicRes } from '@/api/clinicAdmin.js'; * 兼容旧引用:平台门店 API 统一走 storeManage按 loginMode 切前缀)
*/
const STORE_PREFIX = '/newApi/platform-admin-store'; export {
const CONFIG_PREFIX = '/newApi/platform-admin-salesperson-store-config'; getStoreList as getPlatformStoreList,
getStoreDetail as getPlatformStoreDetail,
function platformRequest(options) { updateStore as updatePlatformStore,
return req.request({ openStorePcWindows,
...options, openStoreQrCode,
header: { updateStoreShippingFree,
...(options.header || {}), updateStoreSubscribeStatus,
_platformAdmin: '1', updateStoreSeeRate,
}, updateStoreAllowInsurance,
}).then((res) => { updateStoreClinicType,
const data = unwrapClinicRes(res); toggleSalespersonSeePrice,
const ok = res && (res.code === 0 || (res.errcode != null && Number(res.errcode) !== -1)); getStoreDrugsByType,
return { res, data, ok }; updateStoreDrugPrices,
}); getDoctorOption,
} getInternetMedicalStoreOption,
bindOnlineConsultation,
export function getPlatformStoreList(params) { } from './storeManage.js';
return platformRequest({ url: `${STORE_PREFIX}/list`, method: 'GET', data: params });
}
export function getPlatformStoreDetail(id) {
return platformRequest({ url: `${STORE_PREFIX}/detail`, method: 'GET', data: { id } });
}
export function updatePlatformStore(data) {
return platformRequest({ url: `${STORE_PREFIX}/update`, method: 'POST', data });
}
export function deletePlatformStore(id) {
return platformRequest({ url: `${STORE_PREFIX}/delete`, method: 'POST', data: { id } });
}
export function openStorePcWindows(id) {
return platformRequest({ url: `${STORE_PREFIX}/open-pc-windows`, method: 'POST', data: { id } });
}
export function openStoreQrCode(id) {
return platformRequest({ url: `${STORE_PREFIX}/open-qr-code`, method: 'POST', data: { id } });
}
export function updateStoreShippingFree(id) {
return platformRequest({ url: `${STORE_PREFIX}/update-shipping-free`, method: 'POST', data: { id } });
}
export function updateStoreSubscribeStatus(id) {
return platformRequest({ url: `${STORE_PREFIX}/update-subscribe-status`, method: 'POST', data: { id } });
}
export function updateStoreSeeRate(id) {
return platformRequest({ url: `${STORE_PREFIX}/update-see-rate`, method: 'POST', data: { id } });
}
export function updateStoreAllowInsurance(id) {
return platformRequest({ url: `${STORE_PREFIX}/update-allow-insurance-category`, method: 'POST', data: { id } });
}
export function updateStoreClinicType(id, clinicType) {
return platformRequest({
url: `${STORE_PREFIX}/update-clinic-type`,
method: 'POST',
data: { id, clinic_type: clinicType },
});
}
export function toggleSalespersonSeePrice(storeId) {
return platformRequest({
url: `${CONFIG_PREFIX}/toggle-see-price`,
method: 'POST',
data: { store_id: storeId },
});
}
export function getStoreDrugsByType(storeId, type) {
return platformRequest({
url: `${STORE_PREFIX}/get-store-drugs-by-type`,
method: 'GET',
data: { store_id: storeId, type },
});
}
export function updateStoreDrugPrices(data) {
return platformRequest({ url: `${STORE_PREFIX}/update-store-drug-prices`, method: 'POST', data });
}
export function getDoctorOption(storeId) {
return platformRequest({
url: `${STORE_PREFIX}/doctor-option`,
method: 'GET',
data: storeId ? { store_id: storeId } : {},
});
}
export function getInternetMedicalStoreOption() {
return platformRequest({ url: `${STORE_PREFIX}/internet-medical-store-option`, method: 'GET' });
}
export function bindOnlineConsultation(data) {
return platformRequest({ url: `${STORE_PREFIX}/bind-online-consultation`, method: 'POST', data });
}
export function updateStoreExternalField(data) {
return platformRequest({ url: `${STORE_PREFIX}/update-store-external-field`, method: 'POST', data });
}
export const DRUG_TYPE_OPTIONS = [ export const DRUG_TYPE_OPTIONS = [
{ label: '中药', value: 1 }, { label: '中药', value: 1 },
@@ -114,3 +28,13 @@ export const DRUG_TYPE_OPTIONS = [
{ label: '非药品', value: 6 }, { label: '非药品', value: 6 },
{ label: '医疗器械', value: 7 }, { label: '医疗器械', value: 7 },
]; ];
/** @deprecated 请使用 updateStore */
export async function updateStoreExternalField(data) {
const { updateStore } = await import('./storeManage.js');
return updateStore(data);
}
export async function deletePlatformStore() {
return { ok: false, data: null, res: null };
}

View File

@@ -250,6 +250,18 @@ export function addWestPrescription(data) {
}) })
} }
/**
* 按药品查可选配送仓(药店开方选仓)
* 走 PC 同源 admin JWT 接口;医生小程序侧若无权限需后端另开 doctor 路由,此处与 delivery-warehouse-drug 对齐
*/
export function getDeliveryWarehouseOptionsByDrugs(data) {
return req.request({
url: '/newApi/doctor-reception-wx/delivery-warehouse-options-by-drugs',
method: 'GET',
data
})
}
// 检查中药相冲 // 检查中药相冲
export function checkChineseMedicineConflictApi(data) { export function checkChineseMedicineConflictApi(data) {
return req.request({ return req.request({

152
api/storeManage.js Normal file
View File

@@ -0,0 +1,152 @@
/**
* 门店管理 API按 loginMode 切换平台超管 / 业务员前缀
* 业务员与超管复用同一套门店列表/详情页
*/
import { req } from '@/common/js/index.js';
import { unwrapClinicRes } from '@/api/clinicAdmin.js';
function getStoreContext() {
const mode = uni.getStorageSync('loginMode');
if (mode === 'salesperson') {
return {
prefix: '/newApi/salesperson-store',
configPrefix: '',
headerKey: '_salesperson',
isSalesperson: true,
};
}
return {
prefix: '/newApi/platform-admin-store',
configPrefix: '/newApi/platform-admin-salesperson-store-config',
headerKey: '_platformAdmin',
isSalesperson: false,
};
}
function storeRequest(options) {
const ctx = getStoreContext();
return req.request({
...options,
header: {
...(options.header || {}),
[ctx.headerKey]: '1',
},
}).then((res) => {
const data = unwrapClinicRes(res);
const ok = res && (res.code === 0 || (res.errcode != null && Number(res.errcode) !== -1));
return { res, data, ok };
});
}
/** 业务员是否允许编辑门店(系统配置开关) */
export function getSalespersonStoreEditPermission() {
return storeRequest({
url: '/newApi/salesperson-store/edit-permission',
method: 'GET',
});
}
export function getStoreList(params) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/list`, method: 'GET', data: params });
}
export function getStoreDetail(id) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/detail`, method: 'GET', data: { id } });
}
export function updateStore(data) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/update`, method: 'POST', data });
}
export function openStorePcWindows(id) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/open-pc-windows`, method: 'POST', data: { id } });
}
export function openStoreQrCode(id) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/open-qr-code`, method: 'POST', data: { id } });
}
export function updateStoreShippingFree(id) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/update-shipping-free`, method: 'POST', data: { id } });
}
export function updateStoreSubscribeStatus(id) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/update-subscribe-status`, method: 'POST', data: { id } });
}
export function updateStoreSeeRate(id) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/update-see-rate`, method: 'POST', data: { id } });
}
export function updateStoreAllowInsurance(id) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/update-allow-insurance-category`, method: 'POST', data: { id } });
}
export function updateStoreClinicType(id, clinicType) {
const { prefix } = getStoreContext();
return storeRequest({
url: `${prefix}/update-clinic-type`,
method: 'POST',
data: { id, clinic_type: clinicType },
});
}
export function toggleSalespersonSeePrice(storeId) {
const { configPrefix, isSalesperson } = getStoreContext();
if (isSalesperson || !configPrefix) {
return Promise.resolve({ ok: false, data: null, res: null });
}
return storeRequest({
url: `${configPrefix}/toggle-see-price`,
method: 'POST',
data: { store_id: storeId },
});
}
export function getStoreDrugsByType(storeId, type) {
const { prefix } = getStoreContext();
return storeRequest({
url: `${prefix}/get-store-drugs-by-type`,
method: 'GET',
data: { store_id: storeId, type },
});
}
export function updateStoreDrugPrices(data) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/update-store-drug-prices`, method: 'POST', data });
}
export function getDoctorOption(storeId) {
const { prefix } = getStoreContext();
return storeRequest({
url: `${prefix}/doctor-option`,
method: 'GET',
data: storeId ? { store_id: storeId } : {},
});
}
export function getInternetMedicalStoreOption() {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/internet-medical-store-option`, method: 'GET' });
}
export function bindOnlineConsultation(data) {
const { prefix } = getStoreContext();
return storeRequest({ url: `${prefix}/bind-online-consultation`, method: 'POST', data });
}
export function isSalespersonStoreMode() {
return uni.getStorageSync('loginMode') === 'salesperson';
}
export { getStoreContext };

132
api/userPatientProfile.js Normal file
View File

@@ -0,0 +1,132 @@
/**
* 用户管理 / 就诊人档案 API诊所管理员 / 平台超管)
*/
import { req } from '@/common/js/index.js';
import { unwrapClinicRes } from '@/api/clinicAdmin.js';
function getProfileContext() {
const mode = uni.getStorageSync('loginMode');
if (mode === 'clinic_admin') {
return {
prefix: '/newApi/clinic-admin-user-patient-profile',
headerKey: '_clinicAdmin',
};
}
return {
prefix: '/newApi/platform-admin-user-patient-profile',
headerKey: '_platformAdmin',
};
}
function profileRequest(options) {
const ctx = getProfileContext();
return req.request({
...options,
header: {
...(options.header || {}),
[ctx.headerKey]: '1',
},
}).then((res) => {
const data = unwrapClinicRes(res);
// code 可能是数字 0 或字符串 "0";勿用 === 导致 ok 为 false、列表被丢弃
const ok = !!(res && (
Number(res.code) === 0
|| (res.errcode != null && Number(res.errcode) !== -1)
));
return { res, data, ok };
});
}
/**
* 从 profile 接口包装中取出列表 items兼容 result/data/items 多层)
*/
export function pickProfileItems(wrap) {
const raw = (wrap && wrap.data) || null;
const body = wrap && wrap.res;
const fromBody = body && (body.result || body.data);
let payload = raw;
// unwrap 后若仍带着 code/result再剥一层
if (payload && payload.items == null && payload.result != null) {
payload = payload.result;
}
if (payload == null && fromBody) {
payload = fromBody;
}
if (Array.isArray(payload)) {
return payload;
}
if (payload && Array.isArray(payload.items)) {
return payload.items;
}
if (payload && Array.isArray(payload.list)) {
return payload.list;
}
return [];
}
export function getUserPatientList(params) {
const { prefix } = getProfileContext();
return profileRequest({ url: `${prefix}/list`, method: 'GET', data: params });
}
/** 会员管理:微信用户分页列表 */
export function getWxUserList(params) {
const { prefix } = getProfileContext();
return profileRequest({ url: `${prefix}/user-list`, method: 'GET', data: params });
}
/** 某微信用户下的就诊人列表 */
export function getPatientListByUser(userId) {
const { prefix } = getProfileContext();
return profileRequest({
url: `${prefix}/patient-list-by-user`,
method: 'GET',
data: { user_id: userId },
});
}
export function getUserPatientDetail(upId) {
const { prefix } = getProfileContext();
return profileRequest({
url: `${prefix}/detail`,
method: 'GET',
data: { up_id: upId },
});
}
export function getUserPatientRegisterList(upId, params) {
const { prefix } = getProfileContext();
return profileRequest({
url: `${prefix}/register-list`,
method: 'GET',
data: { up_id: upId, ...params },
});
}
/** 就诊人单次挂号详情(基本信息 + 选药 + 回答记录) */
export function getUserPatientRegisterDetail(registerId) {
const { prefix } = getProfileContext();
return profileRequest({
url: `${prefix}/register-detail`,
method: 'GET',
data: { register_id: registerId },
});
}
export function getUserPatientPrescriptionList(upId, params) {
const { prefix } = getProfileContext();
return profileRequest({
url: `${prefix}/prescription-list`,
method: 'GET',
data: { up_id: upId, ...params },
});
}
export function getUserPatientOrderList(upId, params) {
const { prefix } = getProfileContext();
return profileRequest({
url: `${prefix}/order-list`,
method: 'GET',
data: { up_id: upId, ...params },
});
}

View File

@@ -12,6 +12,7 @@ export const config = {
const arr = [ const arr = [
'mobile', 'mobile',
'call_mobile',
'express_mobile', 'express_mobile',
'doctor', 'doctor',
'accept_tel', 'accept_tel',
@@ -23,6 +24,7 @@ const arr = [
] ]
// 敏感数据(解密后做脱敏;需与下方 switch 分支一致) // 敏感数据(解密后做脱敏;需与下方 switch 分支一致)
// 注意call_mobile 只解密不脱敏,供回访拨号使用
const sensitiveData = [ const sensitiveData = [
'id_card', 'id_card',
'idcard', 'idcard',

View File

@@ -34,6 +34,8 @@
<view class="pwd"> <view class="pwd">
<u-field v-model="smsCode" <u-field v-model="smsCode"
type="number"
:maxlength="6"
placeholder="请填写验证码" placeholder="请填写验证码"
:prefixIcon="require('../../static/image/pwd.png')" :prefixIcon="require('../../static/image/pwd.png')"
:placeholder-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }" :placeholder-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }"
@@ -83,7 +85,7 @@
:loading="wxLoading" :loading="wxLoading"
:custom-style="{ background: '#07C160', color: '#fff' }" :custom-style="{ background: '#07C160', color: '#fff' }"
> >
微信一键登录 一键登录
</u-button> </u-button>
</view> </view>
</view> </view>
@@ -221,6 +223,10 @@ export default {
this.$toast('请输入手机号'); this.$toast('请输入手机号');
return; return;
} }
if (!this.form.password) {
this.$toast('请输入密码');
return;
}
if (!this.smsCode) { if (!this.smsCode) {
this.$toast('请输入验证码'); this.$toast('请输入验证码');
return; return;

View File

@@ -13,10 +13,19 @@ const DEV = {
wsUrl: 'ws://127.0.0.1:12080/ws', wsUrl: 'ws://127.0.0.1:12080/ws',
}; };
// const PROD = {
// legacyApiBase: 'https://app.xiaokang88.com/service/v1/',
// newApiBase: 'https://api.xiaokang88.com/api/doctor/',
// wsUrl: 'wss://api.ws.g.xiaokang88.com/ws',
// };
// const TEST = {
const PROD = { const PROD = {
legacyApiBase: 'https://app.xiaokang88.com/service/v1/', legacyApiBase: 'https://xk.test.saas.api-yii.nailaoyun.cn/service/v1/',
newApiBase: 'https://api.xiaokang88.com/api/doctor/', newApiBase: 'https://xk.test.saas.api.nailaoyun.cn/api/doctor/',
wsUrl: 'wss://api.ws.g.xiaokang88.com/ws', // wsUrl: 'wss://api.ws.g.xiaokang88.com/ws',
wsUrl: 'wss://xk.ws.nailaoyun.cn/ws',
// wsUrl: '',
}; };
export function isDoctorWxDev() { export function isDoctorWxDev() {

View File

@@ -441,7 +441,16 @@
}, },
{ {
"path": "order/detail", "path": "order/detail",
"style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom" } "style": {
"navigationBarTitleText": "订单详情",
"navigationStyle": "custom",
"usingComponents": {
"order-price-percent-adjust-popup": "/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup"
},
"componentPlaceholder": {
"order-price-percent-adjust-popup": "view"
}
}
}, },
{ {
"path": "order/trace/index", "path": "order/trace/index",
@@ -471,7 +480,11 @@
{ "path": "withdrawal/settlement/detail", "style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" } }, { "path": "withdrawal/settlement/detail", "style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" } },
{ "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" } }, { "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" } },
{ "path": "warehouse/index", "style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom" } }, { "path": "warehouse/index", "style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom" } },
{ "path": "warehouse/detail", "style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" } } { "path": "warehouse/detail", "style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" } },
{ "path": "user-patient/index", "style": { "navigationBarTitleText": "会员管理", "navigationStyle": "custom" } },
{ "path": "user-patient/patients", "style": { "navigationBarTitleText": "就诊人列表", "navigationStyle": "custom" } },
{ "path": "user-patient/detail", "style": { "navigationBarTitleText": "就诊人详情", "navigationStyle": "custom" } },
{ "path": "user-patient/register-detail", "style": { "navigationBarTitleText": "挂号详情", "navigationStyle": "custom" } }
] ]
}, { }, {
"root": "subPackages/sub_platform_admin", "root": "subPackages/sub_platform_admin",

View File

@@ -76,7 +76,7 @@
</view> </view>
</view> </view>
<my-pharmacist v-if="parseInt(identity)==2" ref="myPharmacist" :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> <my-service v-if="parseInt(identity)>2" ref="myService" :info="userInfo" :avatar="getAvatar"></my-service>
<d-tabbar></d-tabbar> <d-tabbar></d-tabbar>
</view> </view>
</template> </template>
@@ -183,6 +183,9 @@
if (parseInt(this.identity) === 2 && this.$refs.myPharmacist) { if (parseInt(this.identity) === 2 && this.$refs.myPharmacist) {
this.$refs.myPharmacist.refreshWxBind() this.$refs.myPharmacist.refreshWxBind()
} }
if (parseInt(this.identity) > 2 && this.$refs.myService) {
this.$refs.myService.refreshWxBind()
}
}, },
async getInfo() { async getInfo() {
let res = {} let res = {}

View File

@@ -18,31 +18,50 @@
</d-text> </d-text>
</view> </view>
</view> </view>
<view class="p-32 white m-t-12"> <!-- 菜单卡绑定微信 / 个人资料 / 设置 / 切换账号对齐医生我的 -->
<wx-bind-entry ref="wxBindEntry" /> <view class="flex-col m-t-12 white p-row-32">
</view> <view class="wx-bind-wrap">
<view class="p-32 white m-t-12" @click="$go('../../subPackages/sub_pharmacist/pharmacist_info?avatar='+avatar+'&info='+JSON.stringify(info))"> <wx-bind-entry ref="wxBindEntry" />
<view class="flex-row flex-jus-sp flex-ali-center"> </view>
<u-icon margin-left="20" label-size="32" label-color="#31353D" <view class="flex-row bottom-border flex-jus-sp p-col-32 flex-ali-center"
:name="require('../../static/image/grzl.png')" label="个人资料" size="30"></u-icon> @click="$go('../../subPackages/sub_pharmacist/pharmacist_info?avatar='+avatar+'&info='+JSON.stringify(info))">
<view class="flex-row flex-ali-center">
<u-icon margin-left="20" label-size="32" label-color="#31353D"
:name="require('../../static/image/grzl.png')" label="个人资料" size="30"></u-icon>
</view>
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon> <u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view> </view>
</view> <view class="flex-row bottom-border flex-jus-sp p-col-32 flex-ali-center"
<view class="p-32 white m-t-2" @click="$go('../../subPackages/sub_pharmacist/pharmacist_setInfo')"> @click="$go('../../subPackages/sub_pharmacist/pharmacist_setInfo')">
<view class="flex-row flex-jus-sp flex-ali-center"> <view class="flex-row flex-ali-center">
<u-icon margin-left="20" label-size="32" label-color="#31353D" <u-icon margin-left="20" label-size="32" label-color="#31353D"
:name="require('../../static/image/set.png')" label="设置" size="30"></u-icon> :name="require('../../static/image/set.png')" label="设置" size="30"></u-icon>
</view>
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon> <u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view> </view>
<view class="account-switch-wrap">
<account-switch-entry />
</view>
</view>
<!-- 独立绿色退出按钮不再藏在二级设置页 -->
<view class="logout-wrap p-row-32 m-t-12">
<u-button :throttle-time="0" shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '96rpx' }"
@click="logout">退出登录</u-button>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
/**
* 药师「我的」:资料 + 绑定微信 + 切换账号 + 退出登录(对齐医生主页结构)
*/
import { logout } from '@/api/all.js'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue' import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
export default { export default {
components: { WxBindEntry }, components: { WxBindEntry, AccountSwitchEntry },
props:{ props:{
avatar:{ avatar:{
type:String, type:String,
@@ -56,12 +75,7 @@
} }
}, },
data() { data() {
return { return {};
};
},
onLoad() {
}, },
mounted() { mounted() {
this.refreshWxBind() this.refreshWxBind()
@@ -72,6 +86,18 @@
this.$refs.wxBindEntry.refresh() this.$refs.wxBindEntry.refresh()
} }
}, },
/** 退出登录:清本地会话并回登录页 */
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);
}
})
},
} }
}; };
</script> </script>
@@ -85,4 +111,13 @@
background-color: #F9FAFB; background-color: #F9FAFB;
padding: 16rpx; padding: 16rpx;
} }
.wx-bind-wrap {
border-bottom: 1rpx solid #f0f0f0;
}
.account-switch-wrap {
padding-top: 8rpx;
}
.logout-wrap {
margin-bottom: 32rpx;
}
</style> </style>

View File

@@ -11,17 +11,19 @@
</view> </view>
</view> </view>
</view> </view>
<!-- 菜单卡绑定微信 / 业务项 / 设置 / 切换账号 -->
<view class="p-row-32 b-r-8 white m-t-2"> <view class="p-row-32 b-r-8 white m-t-2">
<view class="p-col-32" @click="$go('../../subPackages/sub_my/my_commonReply/index')"> <view class="wx-bind-wrap p-col-24">
<wx-bind-entry ref="wxBindEntry" />
</view>
<view class="p-col-32 bottom-border" @click="$go('../../subPackages/sub_my/my_commonReply/index')">
<view class="flex-row flex-jus-sp flex-ali-center"> <view class="flex-row flex-jus-sp flex-ali-center">
<u-icon margin-left="20" label-size="32" label-color="#31353D" name="chat" label="快捷回复" size="40" <u-icon margin-left="20" label-size="32" label-color="#31353D" name="chat" label="快捷回复" size="40"
color="#6C7380"></u-icon> color="#6C7380"></u-icon>
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon> <u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view> </view>
</view> </view>
</view> <view class="p-col-32 bottom-border flex-jus-sp flex-row flex-ali-center" @click="$go()">
<view class="p-row-32 b-r-8 white m-t-2">
<view class="p-col-32 flex-jus-sp flex-row flex-ali-center bottom-border" @click="$go()">
<u-icon margin-left="20" label-size="32" label-color="#31353D" name="kefu-ermai" label="联系客服" size="40" <u-icon margin-left="20" label-size="32" label-color="#31353D" name="kefu-ermai" label="联系客服" size="40"
color="#6C7380"></u-icon> color="#6C7380"></u-icon>
<view class="flex-row flex-ali-center"> <view class="flex-row flex-ali-center">
@@ -31,19 +33,35 @@
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon> <u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view> </view>
</view> </view>
<view class="p-col-32" @click="$go('../../subPackages/sub_pharmacist/pharmacist_setInfo')"> <view class="p-col-32 bottom-border" @click="$go('../../subPackages/sub_pharmacist/pharmacist_setInfo')">
<view class="flex-row flex-jus-sp flex-ali-center"> <view class="flex-row flex-jus-sp flex-ali-center">
<u-icon margin-left="20" label-size="32" label-color="#31353D" name="setting" label="设置" size="40" <u-icon margin-left="20" label-size="32" label-color="#31353D" name="setting" label="设置" size="40"
color="#6C7380"></u-icon> color="#6C7380"></u-icon>
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon> <u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view> </view>
</view> </view>
<view class="account-switch-wrap p-col-8">
<account-switch-entry />
</view>
</view>
<!-- 独立绿色退出按钮 -->
<view class="logout-wrap p-row-32 m-t-12">
<u-button :throttle-time="0" shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '96rpx' }"
@click="logout">退出登录</u-button>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
/**
* 导医/客服「我的」:绑定微信 + 切换账号 + 退出登录上提到主页(对齐医生)
*/
import { logout } from '@/api/all.js'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
export default { export default {
components: { WxBindEntry, AccountSwitchEntry },
props: { props: {
avatar: { avatar: {
type: String, type: String,
@@ -60,12 +78,26 @@
return { return {
identity: (uni.getStorageSync('userInfo') || {}).role || 3, identity: (uni.getStorageSync('userInfo') || {}).role || 3,
}; };
},
onLoad() {
}, },
methods: { methods: {
/** 父页 onShow 时刷新绑定微信入口展示状态 */
refreshWxBind() {
const ref = this.$refs.wxBindEntry
if (ref && typeof ref.refresh === 'function') {
ref.refresh()
}
},
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);
}
})
},
}, },
options: { options: {
styleIsolation: 'shared' styleIsolation: 'shared'
@@ -78,8 +110,16 @@
} }
</style> </style>
<style lang="scss"> <style lang="scss">
::v-deep .u-badge { .badge {
display: flex !important; background-color: #F7F8FA;
position: relative; border-radius: 32rpx;
width: 64rpx;
height: 40rpx;
}
.wx-bind-wrap {
border-bottom: 1rpx solid #f0f0f0;
}
.logout-wrap {
margin-bottom: 32rpx;
} }
</style> </style>

View File

@@ -214,6 +214,8 @@ export function mapOrderListRow(item) {
deliveryText: deliveryMethodText(item.delivery_method), deliveryText: deliveryMethodText(item.delivery_method),
deliveryClass: 'tag-delivery', deliveryClass: 'tag-delivery',
userNickname: user.nickname || '-', userNickname: user.nickname || '-',
patientName: (item.user_patient && item.user_patient.name) || item.patient || '-',
upId: Number(item.up_id || (item.user_patient && item.user_patient.id) || 0),
payTime: formatDateTime(item.pay_time), payTime: formatDateTime(item.pay_time),
createdAt: formatDateTime(item.created_at), createdAt: formatDateTime(item.created_at),
storeName: store.name || '-', storeName: store.name || '-',
@@ -346,7 +348,12 @@ export function mapPrescriptionDetailView(item) {
const name = d.name || d.drug_name || '—' const name = d.name || d.drug_name || '—'
const num = d.number != null ? d.number : '' const num = d.number != null ? d.number : ''
const way = (d.use_way && d.use_way.name) || d.useWay || '煎服' const way = (d.use_way && d.use_way.name) || d.useWay || '煎服'
return { name, qty: num ? num + ' /g' : '', usage: way } return {
name,
qty: num ? num + ' /g' : '',
usage: way,
specification: d.specification || '',
}
}) })
: [] : []
recipes.push({ recipes.push({
@@ -369,6 +376,7 @@ export function mapPrescriptionDetailView(item) {
name: d.name || d.drug_name || '—', name: d.name || d.drug_name || '—',
qty: (d.number != null ? d.number : '') + unitName, qty: (d.number != null ? d.number : '') + unitName,
usage: d.useWay || (d.use_way && d.use_way.name) || '', usage: d.useWay || (d.use_way && d.use_way.name) || '',
specification: d.specification || '',
}], }],
usage: '', usage: '',
process: '', process: '',

View File

@@ -0,0 +1,105 @@
<template>
<!-- 就诊人/微信用户摘要单元格点击打开患者视角详情 -->
<view class="up-cell" @click.stop="handleOpen">
<view class="up-row">
<image class="avatar" :src="userAvatar" mode="aspectFill" />
<view class="text">
<text class="lbl">微信用户</text>
<text class="val">{{ userNickname }}</text>
</view>
</view>
<view class="up-row">
<view class="avatar placeholder" />
<view class="text">
<text class="lbl">就诊人</text>
<text class="val link">{{ patientName }}</text>
</view>
</view>
</view>
</template>
<script>
/**
* 列表内紧凑展示微信用户 + 就诊人,点击抛出 open 事件供父级打开详情抽屉
*/
export default {
name: 'UserPatientCell',
props: {
/** 订单行或用户管理行:需含 user / user_patient / up_id / patient */
row: { type: Object, default: () => ({}) },
},
emits: ['open'],
computed: {
user() {
return this.row.user || {};
},
patient() {
return this.row.user_patient || this.row.userPatient || this.row.patient || {};
},
upId() {
if (typeof this.patient === 'object' && this.patient) {
return Number(this.patient.id || this.row.up_id || 0);
}
return Number(this.row.up_id || 0);
},
userNickname() {
return this.user.nickname || '—';
},
userAvatar() {
return this.user.avatarurl || '/static/image/default-avatar.png';
},
patientName() {
if (typeof this.patient === 'string') return this.patient || '—';
return (this.patient && this.patient.name) || this.row.patient || '—';
},
},
methods: {
handleOpen() {
if (!this.upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' });
return;
}
this.$emit('open', { upId: this.upId, patientName: this.patientName });
},
},
};
</script>
<style lang="scss" scoped>
.up-cell {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.up-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: #f3f4f6;
flex-shrink: 0;
}
.avatar.placeholder {
background: #e5e7eb;
}
.text {
display: flex;
flex-direction: column;
min-width: 0;
}
.lbl {
font-size: 20rpx;
color: #9ca3af;
}
.val {
font-size: 24rpx;
color: #374151;
}
.val.link {
color: #6acdbb;
}
</style>

View File

@@ -0,0 +1,556 @@
<template>
<!-- 患者视角详情page-container 抽屉避免误退出页面 v-if 控制挂载 -->
<page-container
v-if="visible"
:show="visible"
:overlay="true"
position="bottom"
:round="true"
@beforeleave="onBeforeLeave"
@clickoverlay="close"
>
<view class="drawer">
<view class="drawer-head">
<text class="title">患者视角 · {{ patientName || '就诊人' }}</text>
<text class="close" @click="close">关闭</text>
</view>
<view v-if="loading" class="empty">加载中...</view>
<template v-else>
<!-- 资料卡对齐就诊人详情页 -->
<view class="profile-card">
<view class="profile-row">
<image
class="avatar"
:src="profile.user.avatarurl || '/static/image/default-avatar.png'"
mode="aspectFill"
/>
<view class="meta">
<text class="name">{{ profile.user.nickname || '微信用户' }}</text>
<text class="sub">
ID {{ profile.user.id || '—' }}
<template v-if="profile.user.mobile"> · {{ profile.user.mobile }}</template>
</text>
</view>
</view>
<view class="patient-block">
<view class="patient-head">
<view class="patient-head-left">
<text class="p-name">{{ profile.patient.name || '—' }}</text>
<text v-if="sexText !== '—'" class="sex-tag" :class="sexTagClass">{{ sexText }}</text>
</view>
<!-- 回访拨打就诊人/用户手机 -->
<view class="callback-btn" @click.stop="onCallbackCall">
<text class="callback-btn-text">回访</text>
</view>
</view>
<text class="p-sub">
{{ profile.patient.age || '—' }}
<template v-if="profile.patient.mobile"> · {{ profile.patient.mobile }}</template>
</text>
<text v-if="profile.patient.id_card" class="p-sub">身份证 {{ profile.patient.id_card }}</text>
</view>
</view>
<!-- Tab下划线选中态 -->
<view class="tabs">
<view
v-for="(t, idx) in tabs"
:key="t.key"
class="tab"
:class="{ on: tabIndex === idx }"
@click="selectTab(idx)"
>
<text class="tab-text">{{ t.label }}</text>
<view v-if="tabIndex === idx" class="tab-bar" />
</view>
</view>
<!-- 四页左右滑资料 / 挂号 / 处方 / 订单 -->
<swiper
class="tab-swiper"
:style="{ height: swiperHeightPx + 'px' }"
:current="tabIndex"
:duration="200"
@change="onTabSwiperChange"
>
<swiper-item v-for="t in tabs" :key="'sw-' + t.key">
<scroll-view
scroll-y
class="tab-scroll"
:style="{ height: swiperHeightPx + 'px' }"
>
<!-- 资料不绑 tabLoading避免切列表时闪加载中 -->
<view v-if="t.key === 'info'" class="health-card">
<text class="section-title">健康信息</text>
<template v-if="health">
<view class="info-row">
<text class="info-label">既往史</text>
<text class="info-value">{{ historyText(health.person_status, health.person_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">过敏史</text>
<text
class="info-value"
:class="{ danger: Number(health.allergic_status) !== 0 }"
>{{ historyText(health.allergic_status, health.allergic_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">家族遗传史</text>
<text class="info-value">{{ historyText(health.family_status, health.family_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">肝功能异常</text>
<text
class="info-value"
:class="{ danger: Number(health.liver_function) !== 0 }"
>{{ functionText(health.liver_function, health.liver_index) }}</text>
</view>
<view class="info-row">
<text class="info-label">肾功能异常</text>
<text
class="info-value"
:class="{ danger: Number(health.renal_function) !== 0 }"
>{{ functionText(health.renal_function, health.renal_index) }}</text>
</view>
</template>
<view v-else class="empty-inline">暂无健康问诊记录</view>
</view>
<template v-else>
<view v-if="tabLoading && activeTab === t.key" class="empty">加载中...</view>
<template v-else>
<view v-if="!listForTab(t.key).length" class="empty">暂无记录</view>
<view
v-for="item in listForTab(t.key)"
:key="t.key + '-' + item.id"
class="record-card"
@click="onRecordClick(item, t.key)"
>
<view class="record-main">
<template v-if="t.key === 'register'">
<text class="r-title">{{ item.order_no || item.order_number || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
<text class="r-sub">{{ item.status_text || '—' }} · {{ item.created_at || '' }}</text>
</template>
<template v-else-if="t.key === 'prescription'">
<text class="r-title">{{ item.prescription_no || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
<text class="r-sub">{{ item.clinical_diagnose || '—' }} · {{ item.created_at || '' }}</text>
</template>
<template v-else>
<text class="r-title">{{ item.order_no || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · ¥{{ item.total_pay_price || '—' }}</text>
<text class="r-sub">{{ item.created_at || '' }}</text>
</template>
</view>
<text v-if="t.key === 'register'" class="arrow"></text>
</view>
</template>
</template>
</scroll-view>
</swiper-item>
</swiper>
</template>
</view>
</page-container>
</template>
<script>
/**
* 患者视角详情抽屉:微信用户/就诊人信息 + 挂号/处方/订单
* 对齐详情页展示与回访;四 Tab 支持点击与左右滑动(懒加载)
*/
import {
getUserPatientDetail,
getUserPatientRegisterList,
getUserPatientPrescriptionList,
getUserPatientOrderList,
} from '@/api/userPatientProfile.js';
/** 抽屉左右滑提示只展示一次 */
const SWIPE_TIP_STORAGE_KEY = 'user_patient_drawer_swipe_tip';
export default {
name: 'UserPatientDetail',
props: {
visible: { type: Boolean, default: false },
upId: { type: [Number, String], default: 0 },
patientName: { type: String, default: '' },
},
emits: ['close'],
data() {
return {
loading: false,
tabLoading: false,
activeTab: 'info',
tabIndex: 0,
tabSwiperChanging: false,
swiperHeightPx: 320,
tabs: [
{ key: 'info', label: '资料' },
{ key: 'register', label: '挂号' },
{ key: 'prescription', label: '处方' },
{ key: 'order', label: '订单' },
],
profile: {
user: {},
patient: {},
},
health: null,
registerList: [],
prescriptionList: [],
orderList: [],
};
},
computed: {
sexText() {
const sex = this.profile.patient && this.profile.patient.sex;
if (Number(sex) === 1) return '男';
if (Number(sex) === 2) return '女';
return '—';
},
sexTagClass() {
const sex = this.profile.patient && this.profile.patient.sex;
if (Number(sex) === 1) return 'male';
if (Number(sex) === 2) return 'female';
return '';
},
/**
* 回访拨号:只用就诊人 call_mobile拦截器只解密不脱敏
* 不用脱敏后的 mobile也不回退微信用户号
*/
callbackPhone() {
const patient = this.profile.patient || {};
return String(patient.call_mobile || '').trim();
},
},
watch: {
/**
* v-if 挂载时 visible 已是 true必须 immediate否则 loadDetail 永不触发
*/
visible: {
immediate: true,
handler(val) {
if (val && this.upId) {
this.openAndLoad();
}
},
},
/** 抽屉打开时切换不同就诊人,重新拉详情 */
upId(val) {
if (this.visible && val) {
this.openAndLoad();
}
},
},
methods: {
close() {
this.$emit('close');
},
onBeforeLeave() {
this.close();
},
/**
* 打开抽屉:重置 Tab/列表并请求就诊人详情
*/
openAndLoad() {
this.activeTab = 'info';
this.tabIndex = 0;
this.health = null;
this.registerList = [];
this.prescriptionList = [];
this.orderList = [];
this.profile = { user: {}, patient: {} };
this.calcSwiperHeight();
this.loadDetail();
},
/**
* 抽屉内 swiper 高度80vh 资料卡 Tab保证横向滑动可用
*/
calcSwiperHeight() {
try {
const sys = uni.getSystemInfoSync() || {};
const winH = Number(sys.windowHeight) || 600;
const drawerH = Math.floor(winH * 0.8);
const rpx = (Number(sys.windowWidth) || 375) / 750;
const headH = Math.ceil(100 * rpx);
const profileH = Math.ceil(280 * rpx);
const tabsH = Math.ceil(90 * rpx);
const pad = Math.ceil(24 * rpx);
const h = drawerH - headH - profileH - tabsH - pad;
this.swiperHeightPx = h > 160 ? h : 160;
} catch (e) {
this.swiperHeightPx = 320;
}
},
/** 按 Tab key 取对应列表(四页同时渲染) */
listForTab(key) {
if (key === 'register') return this.registerList;
if (key === 'prescription') return this.prescriptionList;
if (key === 'order') return this.orderList;
return [];
},
/** 既往/过敏/家族0 无,否则展示 history */
historyText(status, history) {
if (Number(status) === 0) return '无';
const t = String(history || '').trim();
return t || '有';
},
/** 肝/肾功能0 正常,异常时附带指标文案 */
functionText(flag, indexText) {
if (Number(flag) === 0) return '正常';
const t = String(indexText || '').trim();
return t ? `异常 · ${t}` : '异常';
},
/**
* 回访:调起系统拨号;无号 toast按钮始终显示
*/
onCallbackCall() {
const phone = this.callbackPhone;
if (!phone) {
uni.showToast({ title: '暂无手机号', icon: 'none' });
return;
}
uni.makePhoneCall({
phoneNumber: phone,
});
},
/**
* 首次打开抽屉短暂提示可左右滑动
*/
maybeShowSwipeTip() {
let shown = '';
try {
shown = uni.getStorageSync(SWIPE_TIP_STORAGE_KEY);
} catch (e) {
shown = '';
}
if (shown) return;
uni.showToast({
title: '左右滑动可切换标签',
icon: 'none',
duration: 2000,
});
try {
uni.setStorageSync(SWIPE_TIP_STORAGE_KEY, '1');
} catch (e) {
// ignore
}
},
async loadDetail() {
this.loading = true;
try {
const wrap = await getUserPatientDetail(this.upId);
if (wrap && wrap.ok && wrap.data) {
this.profile = {
user: wrap.data.user || {},
patient: wrap.data.patient || {},
};
this.health = wrap.data.health_inquiry || null;
}
this.calcSwiperHeight();
this.$nextTick(() => {
this.maybeShowSwipeTip();
});
} finally {
this.loading = false;
}
},
/** 点击顶部 Tab */
selectTab(index) {
if (this.tabSwiperChanging) return;
const tab = this.tabs[index];
if (!tab) return;
this.tabIndex = index;
this.switchTab(tab.key);
},
/** swiper 滑动切换 */
onTabSwiperChange(e) {
const index = e && e.detail ? e.detail.current : 0;
const tab = this.tabs[index];
if (!tab || tab.key === this.activeTab) {
this.tabIndex = index;
return;
}
this.tabSwiperChanging = true;
this.tabIndex = index;
this.switchTab(tab.key);
this.$nextTick(() => {
this.tabSwiperChanging = false;
});
},
/** 切换 Tab 并懒加载对应列表 */
async switchTab(key) {
this.activeTab = key;
const idx = this.tabs.findIndex((t) => t.key === key);
if (idx >= 0 && this.tabIndex !== idx) {
this.tabIndex = idx;
}
if (key === 'info') return;
if (key === 'register' && !this.registerList.length) {
await this.loadRegisters();
} else if (key === 'prescription' && !this.prescriptionList.length) {
await this.loadPrescriptions();
} else if (key === 'order' && !this.orderList.length) {
await this.loadOrders();
}
},
async loadRegisters() {
this.tabLoading = true;
try {
const wrap = await getUserPatientRegisterList(this.upId);
this.registerList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
async loadPrescriptions() {
this.tabLoading = true;
try {
const wrap = await getUserPatientPrescriptionList(this.upId);
this.prescriptionList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
async loadOrders() {
this.tabLoading = true;
try {
const wrap = await getUserPatientOrderList(this.upId);
this.orderList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
/**
* 挂号行:关抽屉后进挂号详情(与详情页一致)
*/
onRecordClick(item, tabKey) {
if (tabKey !== 'register') return;
const rid = Number((item && item.id) || 0);
if (!rid) {
uni.showToast({ title: '缺少挂号信息', icon: 'none' });
return;
}
try {
uni.setStorageSync('user_patient_register_detail', item || {});
} catch (e) { /* ignore */ }
this.close();
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${rid}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.drawer {
height: 80vh;
background: #f9fafb;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
}
.drawer-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.title { font-size: 30rpx; font-weight: 600; color: #111827; }
.close { font-size: 26rpx; color: #6acdbb; }
.profile-card {
margin: 20rpx 24rpx 0;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
flex-shrink: 0;
}
.profile-row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: #e5e7eb; }
.meta { display: flex; flex-direction: column; min-width: 0; }
.name { font-size: 28rpx; font-weight: 600; color: #1f2937; }
.sub { font-size: 22rpx; color: #9ca3af; margin-top: 4rpx; }
.patient-block { padding-top: 12rpx; border-top: 1rpx solid #f3f4f6; }
.patient-head {
display: flex; align-items: center; justify-content: space-between; gap: 16rpx;
}
.patient-head-left { display: flex; align-items: center; gap: 12rpx; min-width: 0; flex: 1; }
.p-name { font-size: 28rpx; font-weight: 600; color: #111827; }
.sex-tag {
font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 8rpx;
background: #e8f5f2; color: #2a9d8f;
}
.sex-tag.male { background: #e8f0fe; color: #3b82f6; }
.sex-tag.female { background: #fce7f3; color: #db2777; }
.callback-btn {
flex-shrink: 0;
padding: 8rpx 22rpx;
border-radius: 24rpx;
border: 1rpx solid #6acdbb;
background: #e8f5f2;
}
.callback-btn-text { font-size: 24rpx; color: #1a9b88; font-weight: 500; }
.p-sub { display: block; font-size: 22rpx; color: #6b7280; margin-top: 6rpx; }
.tabs {
display: flex;
background: #fff;
margin: 16rpx 24rpx 0;
border-radius: 16rpx 16rpx 0 0;
padding: 0 8rpx;
border-bottom: 1rpx solid #eef2f1;
flex-shrink: 0;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0 14rpx;
position: relative;
}
.tab-text { font-size: 26rpx; color: #6b7280; }
.tab.on .tab-text { color: #1a9b88; font-weight: 600; }
.tab-bar {
position: absolute; bottom: 0; width: 48rpx; height: 4rpx;
border-radius: 4rpx; background: #6acdbb;
}
.tab-swiper {
width: 100%;
padding: 0 24rpx;
box-sizing: border-box;
}
.tab-scroll { width: 100%; box-sizing: border-box; }
.health-card {
background: #fff; border-radius: 0 0 16rpx 16rpx; padding: 8rpx 20rpx 20rpx;
}
.section-title {
display: block; font-size: 26rpx; font-weight: 600; color: #1f2937;
padding: 16rpx 0 8rpx;
}
.info-row {
display: flex; justify-content: space-between; align-items: flex-start;
padding: 16rpx 0; border-bottom: 1rpx solid #f3f4f6; gap: 24rpx;
}
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 24rpx; color: #9ca3af; flex-shrink: 0; }
.info-value { font-size: 24rpx; color: #374151; text-align: right; flex: 1; word-break: break-all; }
.info-value.danger { color: #dc2626; font-weight: 600; }
.empty-inline { text-align: center; color: #9ca3af; padding: 40rpx 0; font-size: 24rpx; }
.record-card {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 12rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
.record-main { flex: 1; min-width: 0; }
.r-title { display: block; font-size: 26rpx; font-weight: 600; color: #111827; }
.r-sub { display: block; font-size: 22rpx; color: #6b7280; margin-top: 6rpx; }
.arrow { font-size: 36rpx; color: #d1d5db; line-height: 1; }
.empty { text-align: center; color: #9ca3af; padding: 48rpx 0; font-size: 26rpx; }
</style>

View File

@@ -1,14 +1,25 @@
<template> <template>
<view v-if="showBlock" class="express-panel"> <view v-if="showBlock" class="express-panel">
<view class="section-title">物流信息</view> <view class="section-title">物流信息</view>
<template v-if="display.hasData"> <scroll-view v-if="packageList.length > 1" class="pkg-tabs" scroll-x>
<view class="row"><text>物流公司</text><text>{{ display.companyName }}</text></view> <view
<view class="row"><text>运单号</text><text>{{ display.expressNo }}</text></view> v-for="(pkg, idx) in packageList"
<view class="row"><text>最新状态</text><text class="highlight-text">{{ display.stateText }}</text></view> :key="idx"
class="pkg-tab"
<view v-if="display.tracks.length" class="timeline"> :class="{ active: activePkg === idx }"
@click="activePkg = idx"
>
包裹{{ pkg.package_no || idx + 1 }}
<text v-if="pkg.warehouse_name" class="pkg-sub">{{ pkg.warehouse_name }}</text>
</view>
</scroll-view>
<template v-if="currentDisplay.hasData">
<view class="row"><text>物流公司</text><text>{{ currentDisplay.companyName }}</text></view>
<view class="row"><text>运单号</text><text>{{ currentDisplay.expressNo }}</text></view>
<view class="row"><text>最新状态</text><text class="highlight-text">{{ currentDisplay.stateText }}</text></view>
<view v-if="currentDisplay.tracks.length" class="timeline">
<view class="timeline-title">物流追踪</view> <view class="timeline-title">物流追踪</view>
<view v-for="(t, idx) in display.tracks" :key="idx" class="track-item"> <view v-for="(t, idx) in currentDisplay.tracks" :key="idx" class="track-item">
<view class="track-dot" :class="{ 'active': idx === 0 }" /> <view class="track-dot" :class="{ 'active': idx === 0 }" />
<view class="track-body"> <view class="track-body">
<text class="track-status" :class="{ 'active': idx === 0 }">{{ t.status }}</text> <text class="track-status" :class="{ 'active': idx === 0 }">{{ t.status }}</text>
@@ -18,13 +29,14 @@
</view> </view>
</view> </view>
</template> </template>
<view v-else class="empty-express">暂无物流信息</view> <view v-else class="empty-express">
{{ currentPkg && Number(currentPkg.is_send) === 0 ? '本包裹尚未发货' : '暂无物流信息' }}
</view>
</view> </view>
</template> </template>
<script> <script>
import { mapExpressDetail } from '@/subPackages/sub_business_shared/common/display.js' import { mapExpressDetail } from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js' import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
export default { export default {
@@ -34,14 +46,38 @@ export default {
express: { type: Object, default: null }, express: { type: Object, default: null },
show: { type: Boolean, default: true }, show: { type: Boolean, default: true },
}, },
data() {
return { activePkg: 0 }
},
computed: { computed: {
showBlock() { showBlock() {
return this.show return this.show
}, },
display() { packageList() {
const pkgs = this.express && Array.isArray(this.express.packages) ? this.express.packages : []
if (pkgs.length) return pkgs
if (this.express && (this.express.express_no || this.express.detail)) {
return [{ package_no: 1, warehouse_name: '', is_send: 1, express: this.express }]
}
return []
},
currentPkg() {
return this.packageList[this.activePkg] || null
},
currentDisplay() {
const pkg = this.currentPkg
if (pkg && pkg.express) return mapExpressDetail(pkg.express)
if (pkg && Number(pkg.is_send) === 0) {
return { hasData: false, companyName: '', expressNo: '', stateText: '', tracks: [] }
}
return mapExpressDetail(this.express) return mapExpressDetail(this.express)
}, },
}, },
watch: {
express() {
this.activePkg = 0
},
},
} }
</script> </script>
@@ -56,7 +92,6 @@ $color-border: #F2F3F5;
background: #ffffff; background: #ffffff;
padding: 32rpx 40rpx; padding: 32rpx 40rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
/* 移除圆角和阴影,采用通栏设计 */
} }
.section-title { .section-title {
@@ -66,6 +101,34 @@ $color-border: #F2F3F5;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.pkg-tabs {
white-space: nowrap;
margin-bottom: 16rpx;
}
.pkg-tab {
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 12rpx 24rpx;
margin-right: 12rpx;
border-radius: 12rpx;
background: #f5f5f5;
font-size: 26rpx;
color: $color-text-muted;
&.active {
background: rgba(106, 205, 187, 0.15);
color: $theme-primary;
font-weight: 600;
}
.pkg-sub {
font-size: 20rpx;
margin-top: 4rpx;
}
}
.row { .row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -130,7 +193,6 @@ $color-border: #F2F3F5;
margin-left: 8rpx; margin-left: 8rpx;
} }
/* 最后一个元素去掉线 */
.track-item:last-child .track-body { .track-item:last-child .track-body {
border-left-color: transparent; border-left-color: transparent;
padding-bottom: 0; padding-bottom: 0;
@@ -168,4 +230,4 @@ $color-border: #F2F3F5;
font-size: 26rpx; font-size: 26rpx;
padding: 40rpx 0; padding: 40rpx 0;
} }
</style> </style>

View File

@@ -1,189 +1,185 @@
<template> <template>
<drawer-page-container <!-- 小程序抽屉须用 page-container + v-if避免 u-popup 在订单详情页底内联 -->
v-model="show" <page-container
title="处方详情" v-if="show"
height="85%" :show="show"
tall :overlay="true"
:show-close="true" position="bottom"
:closeable="true" :round="true"
@close="onClose" @beforeleave="onClose"
@clickoverlay="onClose"
> >
<view class="popup-wrap"> <view class="rx-drawer">
<scroll-view v-if="view" scroll-y class="popup-scroll"> <view class="rx-drawer-head">
<view class="container safe-area-inset-bottom"> <text class="rx-drawer-title">处方详情</text>
<!-- 处方头部 --> <text class="rx-drawer-close" @click="onClose">关闭</text>
<view class="head"> </view>
<view class="head_record_top"> <view class="popup-wrap">
<view class="record"> <scroll-view v-if="view" scroll-y class="popup-scroll">
处方编号{{ view.prescriptionNo }} <view class="container safe-area-inset-bottom">
<!-- 处方头部 -->
<view class="head">
<view class="head_record_top">
<view class="record">
处方编号{{ view.prescriptionNo }}
</view>
<view class="record_state">
{{ view.typeText }}
</view>
</view> </view>
<view class="record_state"> <!-- 动态医院与公章 -->
{{ view.typeText }} <view class="head_record">
<image :src="view.sealImage || '/static/group/xkyard.png'" mode="aspectFit"></image>
<view class="record_yard">
<view class="yard">
{{ view.storeName }}
</view>
<view class="state">
处方笺
</view>
</view>
</view>
<!-- 时间 -->
<view class="head_record_time">
<text style="text-align: right;">开具日期{{ view.createdAt }}</text>
</view> </view>
</view> </view>
<!-- 患者信息 -->
<!-- 动态医院与公章 --> <view class="my_info">
<view class="head_record"> <view class="info">
<image :src="view.sealImage || '/static/group/xkyard.png'" mode="aspectFit"></image> <view class="info_item">
<view class="record_yard"> <view class="name">姓名<text>{{ view.patientName }}</text></view>
<view class="yard">
{{ view.storeName }}
</view> </view>
<view class="state"> <view class="info_item">
处方笺 <view class="name">性别<text>{{ view.patientSex }}</text></view>
</view>
<view class="info_item">
<view class="name">年龄<text>{{ view.patientAge }}</text></view>
</view>
<view class="info_item">
<view class="name">类别<text>{{ view.category }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="name">科室<text>{{ view.departName }}</text></view>
</view>
<view class="info_item" v-if="view.patientMobile">
<view class="name">电话<text>{{ view.patientMobile }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="names">诊断<text>{{ view.diagnose }}</text></view>
</view>
</view>
<!-- 中医专属字段 -->
<view v-if="view.tcmSyndrome || view.tcmMethod || view.tcmDisease" class="info">
<view v-if="view.tcmSyndrome" class="info_item">
<view class="names">中医证候<text>{{ view.tcmSyndrome }}</text></view>
</view>
<view v-if="view.tcmMethod" class="info_item">
<view class="names">中医治法<text>{{ view.tcmMethod }}</text></view>
</view>
<view v-if="view.tcmDisease" class="info_item">
<view class="names">中医疾病<text>{{ view.tcmDisease }}</text></view>
</view> </view>
</view> </view>
</view> </view>
<view class="bg">
<!-- 时间 --> <!-- 处方明细区 (Rp) -->
<view class="head_record_time"> <view class="my_medical">
<text style="text-align: right;">开具日期{{ view.createdAt }}</text> <view class="title">Rp</view>
</view> <view v-for="(recipe, rIdx) in view.recipes" :key="rIdx" class="item">
</view> <view class="name">
<view class="_name" v-for="(drug, dIdx) in recipe.drugs" :key="dIdx">
<!-- 患者信息 --> <view class="text">
<view class="my_info"> <text>{{ drug.name }}</text>
<view class="info"> <text v-if="drug.specification" class="drug-spec">{{ drug.specification }}</text>
<view class="info_item"> <text class="_abbr" v-if="drug.usage">[{{ drug.usage }}]</text>
<view class="name">姓名<text>{{ view.patientName }}</text></view> </view>
</view> <text class="name_num">{{ drug.qty }}</text>
<view class="info_item">
<view class="name">性别<text>{{ view.patientSex }}</text></view>
</view>
<view class="info_item">
<view class="name">年龄<text>{{ view.patientAge }}</text></view>
</view>
<view class="info_item">
<view class="name">类别<text>{{ view.category }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="name">科室<text>{{ view.departName }}</text></view>
</view>
<view class="info_item" v-if="view.patientMobile">
<view class="name">电话<text>{{ view.patientMobile }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="names">诊断<text>{{ view.diagnose }}</text></view>
</view>
</view>
<!-- 中医专属字段 -->
<view v-if="view.tcmSyndrome || view.tcmMethod || view.tcmDisease" class="info">
<view v-if="view.tcmSyndrome" class="info_item">
<view class="names">中医证候<text>{{ view.tcmSyndrome }}</text></view>
</view>
<view v-if="view.tcmMethod" class="info_item">
<view class="names">中医治法<text>{{ view.tcmMethod }}</text></view>
</view>
<view v-if="view.tcmDisease" class="info_item">
<view class="names">中医疾病<text>{{ view.tcmDisease }}</text></view>
</view>
</view>
</view>
<view class="bg">
<!-- 处方明细区 (Rp) -->
<view class="my_medical">
<view class="title">Rp</view>
<view v-for="(recipe, rIdx) in view.recipes" :key="rIdx" class="item">
<view class="name">
<view class="_name" v-for="(drug, dIdx) in recipe.drugs" :key="dIdx">
<view class="text">
<text>{{ drug.name }}</text>
<text class="_abbr" v-if="drug.usage">[{{ drug.usage }}]</text>
</view> </view>
<text class="name_num">{{ drug.qty }}</text>
</view> </view>
</view> <view class="details" v-if="recipe.usage || recipe.process">
<view class="details" v-if="recipe.usage || recipe.process"> <view class="text">
<view class="text"> <text v-if="recipe.usage">用法{{ recipe.usage }}</text>
<text v-if="recipe.usage">用法{{ recipe.usage }}</text> <text v-if="recipe.process" style="margin-left: 10rpx;">包装方式{{ recipe.process }}</text>
<text v-if="recipe.process" style="margin-left: 10rpx;">包装方式{{ recipe.process }}</text> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 医嘱 -->
<view class="doctor_order">
<view class="yz">
<text class="order_info_left">医嘱:</text>
<view class="order_info">
<view class="info">{{ view.doctorOrder !== '-' ? view.doctorOrder : '无' }}</view>
</view>
</view>
<view class="titles">处方开具已完毕</view>
</view>
</view> </view>
<!-- 动态医生与药师签名区 -->
<!-- 医嘱 --> <view class="my_doctor">
<view class="doctor_order"> <view class="doctor_info">
<view class="yz"> <view class="info">
<text class="order_info_left">:</text> <text></text>
<view class="order_info"> <image v-if="view.doctorSignImage" :src="checkImageUrl(view.doctorSignImage)" mode="aspectFit"></image>
<view class="info">{{ view.doctorOrder !== '-' ? view.doctorOrder : '无' }}</view> <text v-else class="name">{{ view.doctorName }}</text>
</view>
<view class="info">
<text>审核药师</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>发药人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>核对人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>调配人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view> </view>
</view> </view>
<view class="titles">处方开具已完毕</view> <!-- 价格 -->
<view class="price" v-if="view.totalPayPrice">
价格 {{ view.totalPayPrice }}
</view>
</view>
<!-- 动态有效期温馨提示 -->
<view class="my_prompt">
<view class="title">
温馨提示请遵医嘱服药处方{{ view.validHours }}小时有效
</view>
<image src="/static/group/img-yzf.png" mode="aspectFit" v-if="view.status == 2"></image>
</view> </view>
</view> </view>
</scroll-view>
<!-- 动态医生与药师签名区 --> <view v-else-if="loading" class="popup-loading">加载中...</view>
<view class="my_doctor"> <view v-else class="popup-loading">暂无数据</view>
<view class="doctor_info"> </view>
<view class="info">
<text>医师</text>
<image v-if="view.doctorSignImage" :src="checkImageUrl(view.doctorSignImage)" mode="aspectFit"></image>
<text v-else class="name">{{ view.doctorName }}</text>
</view>
<view class="info">
<text>审核药师</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>发药人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>核对人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>调配人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
</view>
<!-- 价格 -->
<view class="price" v-if="view.totalPayPrice">
价格 {{ view.totalPayPrice }}
</view>
</view>
<!-- 动态有效期温馨提示 -->
<view class="my_prompt">
<view class="title">
温馨提示请遵医嘱服药处方{{ view.validHours }}小时有效
</view>
<image src="/static/group/img-yzf.png" mode="aspectFit" v-if="view.status == 2"></image>
</view>
</view>
</scroll-view>
<view v-else-if="loading" class="popup-loading">加载中...</view>
<view v-else class="popup-loading">暂无数据</view>
</view> </view>
</drawer-page-container> </page-container>
</template> </template>
<script> <script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue' /**
* 处方详情抽屉page-container 底部浮层(管理端看方)
*/
import { mapPrescriptionDetailView } from '@/subPackages/sub_business_shared/common/display.js' import { mapPrescriptionDetailView } from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js' import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import { formatStoreNameWithHu } from '@/utils/formatStoreNameWithHu.js'
export default { export default {
mixins: [businessMixin], mixins: [businessMixin],
components: { DrawerPageContainer },
name: 'PrescriptionDetailPopup', name: 'PrescriptionDetailPopup',
data() { data() {
return { return {
@@ -193,7 +189,7 @@ export default {
} }
}, },
methods: { methods: {
// 解析签名图片,自动兼容 base64 和 http 链接 /** 解析签名图片,自动兼容 base64 和 http 链接 */
checkImageUrl(url) { checkImageUrl(url) {
if (!url) return '' if (!url) return ''
return url.includes('http') ? url : 'data:image/jpeg;base64,' + url return url.includes('http') ? url : 'data:image/jpeg;base64,' + url
@@ -205,20 +201,27 @@ export default {
this.loading = true this.loading = true
this.bizApi.getPrescriptionDetail(prescriptionId).then(w => { this.bizApi.getPrescriptionDetail(prescriptionId).then(w => {
if (w.ok && w.data) { if (w.ok && w.data) {
// 复用原有的处方解析逻辑
const mapped = mapPrescriptionDetailView(w.data) const mapped = mapPrescriptionDetailView(w.data)
mapped.storeName = formatStoreNameWithHu(
// --- 动态注入你新版 UI 需要的独有字段 --- w.data.store ? w.data.store.name : '未知诊所',
mapped.storeName = w.data.store ? w.data.store.name : '未知诊所' w.data.is_online,
)
mapped.sealImage = w.data.store ? w.data.store.offical_seal : '' mapped.sealImage = w.data.store ? w.data.store.offical_seal : ''
mapped.typeText = w.data.type == 1 ? '普通' : '常用' mapped.typeText = w.data.type == 1 ? '普通' : '常用'
mapped.totalPayPrice = w.data.total_pay_price || '0.00' mapped.totalPayPrice = w.data.total_pay_price || '0.00'
mapped.doctorSignImage = w.data.doctor_sign_image || '' mapped.doctorSignImage =
mapped.pharmacistSignImage = w.data.Pharmacist_sign_image || '' w.data.doctor_sign_image
|| (w.data.doctor_info && w.data.doctor_info.identity_info && w.data.doctor_info.identity_info.sign_image)
|| (w.data.doctorInfo && w.data.doctorInfo.identityInfo && w.data.doctorInfo.identityInfo.sign_image)
|| ''
mapped.pharmacistSignImage =
w.data.Pharmacist_sign_image
|| (w.data.pharmacist_info && w.data.pharmacist_info.identity && w.data.pharmacist_info.identity.sign_image)
|| (w.data.pharmacistInfo && w.data.pharmacistInfo.identity && w.data.pharmacistInfo.identity.sign_image)
|| ''
mapped.validHours = w.data.valid_hours || 72 mapped.validHours = w.data.valid_hours || 72
mapped.status = w.data.status mapped.status = w.data.status
mapped.patientMobile = w.data.patient ? w.data.patient.mobile : '' mapped.patientMobile = w.data.patient ? w.data.patient.mobile : ''
this.view = mapped this.view = mapped
} }
}).finally(() => { }).finally(() => {
@@ -234,20 +237,37 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.rx-drawer {
height: 85vh;
background: #fff;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
}
.rx-drawer-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.rx-drawer-title { font-size: 32rpx; font-weight: 600; color: #1d2129; }
.rx-drawer-close { font-size: 28rpx; color: #6acdbb; }
.popup-wrap { .popup-wrap {
height: 100%; flex: 1;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #fff; background-color: #fff;
position: relative; position: relative;
padding-top: 60rpx; /* 给默认的关闭按钮留出空间 */
} }
.popup-scroll { .popup-scroll {
flex: 1; flex: 1;
height: 0; height: 0;
} }
.popup-loading { .popup-loading {
text-align: center; text-align: center;
color: #999; color: #999;
@@ -445,6 +465,11 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.drug-spec {
color: #8A92A3;
font-size: 22rpx;
margin-left: 8rpx;
}
._abbr { ._abbr {
color: #A7ABB0; color: #A7ABB0;
font-size: 20rpx; font-size: 20rpx;

View File

@@ -1,4 +1,5 @@
<template> <template>
<view>
<business-page-layout title="订单详情" :show-back="true"> <business-page-layout title="订单详情" :show-back="true">
<view class="detail-container"> <view class="detail-container">
<!-- 头部面板通栏展示金额与状态 --> <!-- 头部面板通栏展示金额与状态 -->
@@ -17,7 +18,14 @@
<view class="sub-block"> <view class="sub-block">
<view class="sub-block-title">基本信息</view> <view class="sub-block-title">基本信息</view>
<view class="row"><text>订单类型</text><text>{{ detailExtra.orderTypeText }}</text></view> <view class="row"><text>订单类型</text><text>{{ detailExtra.orderTypeText }}</text></view>
<view class="row"><text>患者</text><text>{{ info.patient || '-' }}</text></view> <view class="row">
<text>患者</text>
<text
class="patient-link"
:class="{ disabled: !patientUpId }"
@click="openPatient"
>{{ info.patient || '-' }}</text>
</view>
<view class="row"><text>医生</text><text>{{ doctorName }}</text></view> <view class="row"><text>医生</text><text>{{ doctorName }}</text></view>
<view class="row"><text>处方来源</text><text>{{ storeName }}</text></view> <view class="row"><text>处方来源</text><text>{{ storeName }}</text></view>
<view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view> <view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view>
@@ -50,7 +58,14 @@
<!-- 配送信息面板 --> <!-- 配送信息面板 -->
<view v-if="info.id" class="detail-panel"> <view v-if="info.id" class="detail-panel">
<view class="section-title">{{ receiverBlockTitle }}</view> <view class="section-title">{{ receiverBlockTitle }}</view>
<view class="row"><text>{{ receiverLabel }}</text><text>{{ receiverName }}</text></view> <view class="row">
<text>{{ receiverLabel }}</text>
<text
class="patient-link"
:class="{ disabled: !patientUpId }"
@click="openPatient"
>{{ receiverName }}</text>
</view>
<view class="row"><text>联系电话</text><text>{{ receiverMobile }}</text></view> <view class="row"><text>联系电话</text><text>{{ receiverMobile }}</text></view>
<view class="row" v-if="info.delivery_method === 0"> <view class="row" v-if="info.delivery_method === 0">
<text>配送地址</text><text class="addr">{{ deliveryAddress }}</text> <text>配送地址</text><text class="addr">{{ deliveryAddress }}</text>
@@ -148,20 +163,28 @@
</view> </view>
<!-- 处方弹窗 -->
<prescription-detail-popup ref="prescriptionPopup" />
<ship-order-popup ref="shipPopup" :biz-api="bizApi" @success="load" />
<refund-order-popup ref="refundPopup" :biz-api="bizApi" @success="load" />
<erp-sync-popup ref="erpPopup" :biz-api="bizApi" @success="load" />
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="info.price_discount || 100"
:quick-options="priceAdjustQuickOptions"
:submitting="priceAdjustSubmitting"
@close="closePriceAdjust"
@confirm="onSharedPriceAdjustConfirm"
/>
</business-page-layout> </business-page-layout>
<!-- page-container 抽屉挂在页面根级避免被 layout 裁切成页底内联 -->
<prescription-detail-popup ref="prescriptionPopup" />
<ship-order-popup ref="shipPopup" :biz-api="bizApi" @success="load" />
<refund-order-popup ref="refundPopup" :biz-api="bizApi" @success="load" />
<erp-sync-popup ref="erpPopup" :biz-api="bizApi" @success="load" />
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="info.price_discount || 100"
:quick-options="priceAdjustQuickOptions"
:submitting="priceAdjustSubmitting"
@close="closePriceAdjust"
@confirm="onSharedPriceAdjustConfirm"
/>
<user-patient-detail
v-if="patientDetailVisible"
:visible="patientDetailVisible"
:up-id="patientDetailUpId"
:patient-name="patientDetailName"
@close="patientDetailVisible = false"
/>
</view>
</template> </template>
<script> <script>
@@ -185,6 +208,7 @@ import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js' import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import { orderPriceAdjustMixin } from '@/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js' import { orderPriceAdjustMixin } from '@/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js'
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue' import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue'
import UserPatientDetail from '@/subPackages/sub_business_shared/components/UserPatientDetail.vue'
export default { export default {
mixins: [businessMixin, orderPriceAdjustMixin], mixins: [businessMixin, orderPriceAdjustMixin],
@@ -196,6 +220,7 @@ export default {
RefundOrderPopup, RefundOrderPopup,
ErpSyncPopup, ErpSyncPopup,
OrderPricePercentAdjustPopup, OrderPricePercentAdjustPopup,
UserPatientDetail,
}, },
data() { data() {
return { return {
@@ -203,6 +228,9 @@ export default {
info: {}, info: {},
expressInfo: null, expressInfo: null,
detailExtra: {}, detailExtra: {},
patientDetailVisible: false,
patientDetailUpId: 0,
patientDetailName: '',
} }
}, },
computed: { computed: {
@@ -225,6 +253,13 @@ export default {
const s = this.info.store const s = this.info.store
return (s && s.name) ? s.name : '-' return (s && s.name) ? s.name : '-'
}, },
/** 就诊人 ID订单 up_id 或关联 user_patient.id */
patientUpId() {
const up = Number(this.info.up_id || 0)
if (up) return up
const rel = this.info.user_patient
return Number((rel && rel.id) || 0)
},
receiverBlockTitle() { receiverBlockTitle() {
return this.info.delivery_method === 0 ? '收货人信息' : '就诊人信息' return this.info.delivery_method === 0 ? '收货人信息' : '就诊人信息'
}, },
@@ -272,16 +307,54 @@ export default {
this.id = Number(q.id || 0) this.id = Number(q.id || 0)
this.load() this.load()
}, },
/** 离开页关闭处方抽屉,避免 page-container 残留遮罩导致列表白屏 */
onHide() {
this.closePrescriptionPopup()
this.patientDetailVisible = false
},
onUnload() {
this.closePrescriptionPopup()
this.patientDetailVisible = false
},
methods: { methods: {
/** 关闭处方抽屉 */
closePrescriptionPopup() {
const pop = this.$refs.prescriptionPopup
if (pop && typeof pop.onClose === 'function') {
pop.onClose()
}
},
/**
* 打开患者视角抽屉(与订单列表一致)
*/
openPatient() {
const upId = this.patientUpId
if (!upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' })
return
}
this.patientDetailUpId = upId
this.patientDetailName = this.info.patient || ''
this.patientDetailVisible = true
},
load() { load() {
if (!this.id) {
uni.showToast({ title: '缺少订单信息', icon: 'none' })
return
}
this.bizApi.getOrderDetail(this.id).then(w => { this.bizApi.getOrderDetail(this.id).then(w => {
if (!w.ok) return if (!w.ok) {
uni.showToast({ title: '订单加载失败', icon: 'none' })
return
}
this.info = w.data || {} this.info = w.data || {}
this.detailExtra = mapOrderDetailDisplay(this.info) this.detailExtra = mapOrderDetailDisplay(this.info)
this.loadPriceAdjustMeta(() => this.bizApi.getPriceAdjustConfig(this.info.store_id)) this.loadPriceAdjustMeta(() => this.bizApi.getPriceAdjustConfig(this.info.store_id))
if (this.info.delivery_method === 0) { if (this.info.delivery_method === 0) {
this.loadExpress() this.loadExpress()
} }
}).catch(() => {
uni.showToast({ title: '订单加载失败', icon: 'none' })
}) })
}, },
loadExpress() { loadExpress() {
@@ -584,6 +657,11 @@ $color-danger: #F53F3F;
.price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; } .price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; }
.price-link { color: $theme-primary; text-decoration: underline; } .price-link { color: $theme-primary; text-decoration: underline; }
.patient-link {
color: $theme-primary;
text-align: right;
&.disabled { color: inherit; }
}
/* ================= 底部动作栏 ================= */ /* ================= 底部动作栏 ================= */
.actions-bar { .actions-bar {

View File

@@ -1,4 +1,5 @@
<template> <template>
<view>
<business-page-layout title="商品订单" :show-back="true"> <business-page-layout title="商品订单" :show-back="true">
<view class="order-list-container"> <view class="order-list-container">
@@ -96,9 +97,13 @@
<!-- 去掉臃肿的灰色岛采用精致的文本网格 --> <!-- 去掉臃肿的灰色岛采用精致的文本网格 -->
<view class="info-grid"> <view class="info-grid">
<view class="info-item" v-if="rowDisplay(item).userNickname !== '-'"> <view
class="info-item"
v-if="rowDisplay(item).patientName !== '-' || rowDisplay(item).userNickname !== '-'"
@click.stop="openPatient(item)"
>
<text class="info-lbl">就诊人</text> <text class="info-lbl">就诊人</text>
<text class="info-val">{{ rowDisplay(item).userNickname }}</text> <text class="info-val link">{{ rowDisplay(item).patientName !== '-' ? rowDisplay(item).patientName : rowDisplay(item).userNickname }}</text>
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="info-lbl">收件人</text> <text class="info-lbl">收件人</text>
@@ -136,8 +141,16 @@
</view> </view>
</view> </view>
<prescription-detail-popup ref="prescriptionPopup" />
</business-page-layout> </business-page-layout>
<prescription-detail-popup ref="prescriptionPopup" />
<user-patient-detail
v-if="patientDetailVisible"
:visible="patientDetailVisible"
:up-id="patientDetailUpId"
:patient-name="patientDetailName"
@close="patientDetailVisible = false"
/>
</view>
</template> </template>
<script> <script>
@@ -145,6 +158,7 @@
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue' import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import CollapsibleFilterPanel from '@/subPackages/sub_business_shared/components/CollapsibleFilterPanel.vue' import CollapsibleFilterPanel from '@/subPackages/sub_business_shared/components/CollapsibleFilterPanel.vue'
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue' import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
import UserPatientDetail from '@/subPackages/sub_business_shared/components/UserPatientDetail.vue'
import { withWxKey } from '@/utils/wxListKey.js' import { withWxKey } from '@/utils/wxListKey.js'
import { mapOrderListRow } from '@/subPackages/sub_business_shared/common/display.js' import { mapOrderListRow } from '@/subPackages/sub_business_shared/common/display.js'
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js' import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
@@ -176,7 +190,7 @@ const PRESCRIPTION_TYPE_OPTIONS = [
export default { export default {
mixins: [businessMixin], mixins: [businessMixin],
components: { BusinessPageLayout, CollapsibleFilterPanel, PrescriptionDetailPopup }, components: { BusinessPageLayout, CollapsibleFilterPanel, PrescriptionDetailPopup, UserPatientDetail },
data() { data() {
return { return {
list: [], list: [],
@@ -190,7 +204,10 @@ export default {
dateStart: '', dateStart: '',
dateEnd: '', dateEnd: '',
statItems: [], statItems: [],
navTopOffset: 0, // 新增:用于存放导航栏计算后的确切高度 navTopOffset: 0,
patientDetailVisible: false,
patientDetailUpId: 0,
patientDetailName: '',
} }
}, },
computed: { computed: {
@@ -225,10 +242,24 @@ export default {
}) })
this.checkDateEndAndReload() this.checkDateEndAndReload()
}, },
/** 离开/隐藏时关掉处方抽屉,避免 page-container 残留白屏 */
onHide() {
this.closePrescriptionPopup()
},
onUnload() {
this.closePrescriptionPopup()
},
onReachBottom() { onReachBottom() {
this.load(this.page + 1, true) this.load(this.page + 1, true)
}, },
methods: { methods: {
/** 关闭处方抽屉 */
closePrescriptionPopup() {
const pop = this.$refs.prescriptionPopup
if (pop && typeof pop.onClose === 'function') {
pop.onClose()
}
},
applyOrderFilter(f) { applyOrderFilter(f) {
this.orderNo = f.orderNo this.orderNo = f.orderNo
this.statusIndex = f.statusIndex this.statusIndex = f.statusIndex
@@ -337,6 +368,18 @@ export default {
if (!pId) return if (!pId) return
this.$refs.prescriptionPopup.open(pId) this.$refs.prescriptionPopup.open(pId)
}, },
/** 打开患者视角详情抽屉 */
openPatient(item) {
const d = this.rowDisplay(item)
const upId = Number(d.upId || 0)
if (!upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' })
return
}
this.patientDetailUpId = upId
this.patientDetailName = d.patientName !== '-' ? d.patientName : ''
this.patientDetailVisible = true
},
}, },
} }
</script> </script>
@@ -543,6 +586,9 @@ $color-border: #E5E6EB;
.info-val { .info-val {
color: $color-text-body; color: $color-text-body;
flex: 1; flex: 1;
&.link {
color: $theme-primary;
}
} }
.info-sub { .info-sub {
color: $color-text-muted; color: $color-text-muted;

View File

@@ -0,0 +1,515 @@
<template>
<view>
<business-page-layout :title="pageTitle" :show-back="true">
<view class="page">
<view v-if="loading" class="empty">加载中...</view>
<template v-else>
<!-- 顶部用户 + 就诊人分区卡片固定不进 swiper -->
<view class="profile-card">
<view class="profile-row">
<image class="avatar" :src="profile.user.avatarurl || '/static/image/default-avatar.png'" mode="aspectFill" />
<view class="meta">
<text class="name">{{ profile.user.nickname || '微信用户' }}</text>
<text class="sub">ID {{ profile.user.id || '—' }}<template v-if="profile.user.mobile"> · {{ profile.user.mobile }}</template></text>
</view>
</view>
<view class="patient-block">
<view class="patient-head">
<view class="patient-head-left">
<text class="p-name">{{ profile.patient.name || '—' }}</text>
<text v-if="sexText !== '—'" class="sex-tag" :class="sexTagClass">{{ sexText }}</text>
</view>
<!-- 回访拨打就诊人/用户手机 -->
<view class="callback-btn" @click.stop="onCallbackCall">
<text class="callback-btn-text">回访</text>
</view>
</view>
<text class="p-sub">
{{ profile.patient.age || '—' }}
<template v-if="profile.patient.mobile"> · {{ profile.patient.mobile }}</template>
</text>
<!-- 门店后端不返回身份证时不展示 -->
<text v-if="profile.patient.id_card" class="p-sub">身份证 {{ profile.patient.id_card }}</text>
</view>
</view>
<!-- Tab下划线选中态 -->
<view class="tabs">
<view
v-for="(t, idx) in tabs"
:key="t.key"
class="tab"
:class="{ on: tabIndex === idx }"
@click="selectTab(idx)"
>
<text class="tab-text">{{ t.label }}</text>
<view v-if="tabIndex === idx" class="tab-bar" />
</view>
</view>
<!-- 四页左右滑资料 / 挂号 / 处方 / 订单 -->
<swiper
class="tab-swiper"
:style="{ height: swiperHeightPx + 'px' }"
:current="tabIndex"
:duration="200"
@change="onTabSwiperChange"
>
<swiper-item v-for="t in tabs" :key="'sw-' + t.key">
<scroll-view
scroll-y
class="tab-scroll"
:style="{ height: swiperHeightPx + 'px' }"
>
<!-- 资料 -->
<view v-if="t.key === 'info'" class="health-card">
<text class="section-title">健康信息</text>
<template v-if="health">
<view class="info-row">
<text class="info-label">既往史</text>
<text class="info-value">{{ historyText(health.person_status, health.person_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">过敏史</text>
<text
class="info-value"
:class="{ danger: Number(health.allergic_status) !== 0 }"
>{{ historyText(health.allergic_status, health.allergic_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">家族遗传史</text>
<text class="info-value">{{ historyText(health.family_status, health.family_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">肝功能异常</text>
<text
class="info-value"
:class="{ danger: Number(health.liver_function) !== 0 }"
>{{ functionText(health.liver_function, health.liver_index) }}</text>
</view>
<view class="info-row">
<text class="info-label">肾功能异常</text>
<text
class="info-value"
:class="{ danger: Number(health.renal_function) !== 0 }"
>{{ functionText(health.renal_function, health.renal_index) }}</text>
</view>
</template>
<view v-else class="empty-inline">暂无健康问诊记录</view>
</view>
<!-- 挂号 / 处方 / 订单列表 -->
<template v-else>
<view v-if="tabLoading && activeTab === t.key" class="empty">加载中...</view>
<template v-else>
<view v-if="!listForTab(t.key).length" class="empty">暂无记录</view>
<view
v-for="item in listForTab(t.key)"
:key="t.key + '-' + item.id"
class="record-card"
@click="onRecordClick(item, t.key)"
>
<view class="record-main">
<template v-if="t.key === 'register'">
<text class="r-title">{{ item.order_no || item.order_number || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
<text class="r-sub">{{ item.status_text || '—' }} · {{ item.created_at || '' }}</text>
</template>
<template v-else-if="t.key === 'prescription'">
<text class="r-title">{{ item.prescription_no || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
<text class="r-sub">{{ item.clinical_diagnose || '—' }} · {{ item.created_at || '' }}</text>
</template>
<template v-else>
<text class="r-title">{{ item.order_no || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · ¥{{ item.total_pay_price || '—' }}</text>
<text class="r-sub">{{ item.created_at || '' }}</text>
</template>
</view>
<text class="arrow"></text>
</view>
</template>
</template>
</scroll-view>
</swiper-item>
</swiper>
</template>
</view>
</business-page-layout>
<!-- page-container 抽屉挂在页面根级 -->
<prescription-detail-popup ref="prescriptionPopup" />
</view>
</template>
<script>
/**
* 就诊人详情:资料(健康问诊)+ 挂号/处方/订单
* 顶部资料卡固定,下方四 Tab 支持点击与左右滑动切换(懒加载列表)
*/
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue';
import PrescriptionDetailPopup from '@/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue';
import {
getUserPatientDetail,
getUserPatientRegisterList,
getUserPatientPrescriptionList,
getUserPatientOrderList,
} from '@/api/userPatientProfile.js';
/** 就诊人详情左右滑提示只展示一次 */
const SWIPE_TIP_STORAGE_KEY = 'user_patient_detail_swipe_tip';
export default {
components: { BusinessPageLayout, PrescriptionDetailPopup },
data() {
return {
upId: 0,
patientName: '',
loading: false,
tabLoading: false,
activeTab: 'info',
tabIndex: 0,
tabSwiperChanging: false,
swiperHeightPx: 400,
tabs: [
{ key: 'info', label: '资料' },
{ key: 'register', label: '挂号' },
{ key: 'prescription', label: '处方' },
{ key: 'order', label: '订单' },
],
profile: { user: {}, patient: {} },
health: null,
registerList: [],
prescriptionList: [],
orderList: [],
};
},
computed: {
pageTitle() {
return this.patientName ? `就诊人 · ${this.patientName}` : '就诊人详情';
},
sexText() {
const sex = this.profile.patient && this.profile.patient.sex;
if (Number(sex) === 1) return '男';
if (Number(sex) === 2) return '女';
return '—';
},
sexTagClass() {
const sex = this.profile.patient && this.profile.patient.sex;
if (Number(sex) === 1) return 'male';
if (Number(sex) === 2) return 'female';
return '';
},
/**
* 回访拨号:只用就诊人 call_mobile拦截器只解密不脱敏
* 不用脱敏后的 mobile也不回退微信用户号
*/
callbackPhone() {
const patient = this.profile.patient || {};
return String(patient.call_mobile || '').trim();
},
},
onLoad(options) {
this.calcSwiperHeight();
this.upId = Number((options && options.up_id) || 0);
try {
this.patientName = decodeURIComponent((options && options.name) || '');
} catch (e) {
this.patientName = (options && options.name) || '';
}
if (this.upId) {
this.loadDetail();
}
},
onReady() {
this.calcSwiperHeight();
},
/** 离开页时关掉处方抽屉,避免 page-container 残留遮罩 */
onHide() {
this.closePrescriptionPopup();
},
onUnload() {
this.closePrescriptionPopup();
},
methods: {
/**
* 按窗口高度算死 swiper窗口 状态栏 导航 资料卡预估 Tab 边距
*/
calcSwiperHeight() {
try {
const sys = uni.getSystemInfoSync() || {};
const winH = Number(sys.windowHeight) || 600;
const statusBar = Number(sys.statusBarHeight) || 0;
const navbar = Number(this.$navbarHeight) || 44;
const rpx = (Number(sys.windowWidth) || 375) / 750;
const pagePad = Math.ceil(24 * rpx);
const profileH = Math.ceil(280 * rpx);
const tabsH = Math.ceil(90 * rpx);
const h = winH - statusBar - navbar - pagePad - profileH - tabsH;
this.swiperHeightPx = h > 180 ? h : 180;
} catch (e) {
this.swiperHeightPx = 400;
}
},
/** 按 Tab key 取对应列表swiper 四页同时渲染时不能只用 activeTab */
listForTab(key) {
if (key === 'register') return this.registerList;
if (key === 'prescription') return this.prescriptionList;
if (key === 'order') return this.orderList;
return [];
},
/** 关闭处方抽屉,防止 page-container 残留导致返回后白屏 */
closePrescriptionPopup() {
const pop = this.$refs.prescriptionPopup;
if (pop && typeof pop.onClose === 'function') {
pop.onClose();
}
},
/**
* 回访:调起系统拨号盘拨打 callbackPhone
* 无号时 toast不隐藏按钮以免布局跳动
*/
onCallbackCall() {
const phone = this.callbackPhone;
if (!phone) {
uni.showToast({ title: '暂无手机号', icon: 'none' });
return;
}
uni.makePhoneCall({
phoneNumber: phone,
});
},
/** 既往/过敏/家族0 无,否则展示 history */
historyText(status, history) {
if (Number(status) === 0) return '无';
const t = String(history || '').trim();
return t || '有';
},
/** 肝/肾功能0 正常,异常时附带指标文案 */
functionText(flag, indexText) {
if (Number(flag) === 0) return '正常';
const t = String(indexText || '').trim();
return t ? `异常 · ${t}` : '异常';
},
async loadDetail() {
this.loading = true;
try {
const wrap = await getUserPatientDetail(this.upId);
if (wrap && wrap.ok && wrap.data) {
this.profile = {
user: wrap.data.user || {},
patient: wrap.data.patient || {},
};
this.health = wrap.data.health_inquiry || null;
if (!this.patientName && this.profile.patient.name) {
this.patientName = this.profile.patient.name;
}
}
this.calcSwiperHeight();
this.$nextTick(() => {
this.maybeShowSwipeTip();
});
} finally {
this.loading = false;
}
},
/**
* 首次进入短暂提示可左右滑动(独立缓存键,与挂号详情互不影响)
*/
maybeShowSwipeTip() {
let shown = '';
try {
shown = uni.getStorageSync(SWIPE_TIP_STORAGE_KEY);
} catch (e) {
shown = '';
}
if (shown) return;
uni.showToast({
title: '左右滑动可切换标签',
icon: 'none',
duration: 2000,
});
try {
uni.setStorageSync(SWIPE_TIP_STORAGE_KEY, '1');
} catch (e) {
// ignore
}
},
/** 点击顶部 Tab */
selectTab(index) {
if (this.tabSwiperChanging) return;
const tab = this.tabs[index];
if (!tab) return;
this.tabIndex = index;
this.switchTab(tab.key);
},
/** swiper 滑动切换 */
onTabSwiperChange(e) {
const index = e && e.detail ? e.detail.current : 0;
const tab = this.tabs[index];
if (!tab || tab.key === this.activeTab) {
this.tabIndex = index;
return;
}
this.tabSwiperChanging = true;
this.tabIndex = index;
this.switchTab(tab.key);
this.$nextTick(() => {
this.tabSwiperChanging = false;
});
},
/** 切换 Tab 并懒加载对应列表 */
async switchTab(key) {
this.activeTab = key;
const idx = this.tabs.findIndex((t) => t.key === key);
if (idx >= 0 && this.tabIndex !== idx) {
this.tabIndex = idx;
}
if (key === 'info') return;
if (key === 'register' && !this.registerList.length) {
await this.loadRegisters();
} else if (key === 'prescription' && !this.prescriptionList.length) {
await this.loadPrescriptions();
} else if (key === 'order' && !this.orderList.length) {
await this.loadOrders();
}
},
async loadRegisters() {
this.tabLoading = true;
try {
const wrap = await getUserPatientRegisterList(this.upId);
this.registerList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
async loadPrescriptions() {
this.tabLoading = true;
try {
const wrap = await getUserPatientPrescriptionList(this.upId);
this.prescriptionList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
async loadOrders() {
this.tabLoading = true;
try {
const wrap = await getUserPatientOrderList(this.upId);
this.orderList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
/**
* 列表项点击:处方弹窗 / 订单页 / 挂号详情页
* @param item 列表行
* @param tabKey 所在 Tab避免滑动瞬间 activeTab 未同步)
*/
onRecordClick(item, tabKey) {
const key = tabKey || this.activeTab;
if (key === 'prescription') {
const pid = Number((item && item.id) || 0);
if (!pid) {
uni.showToast({ title: '缺少处方信息', icon: 'none' });
return;
}
this.$refs.prescriptionPopup && this.$refs.prescriptionPopup.open(pid);
return;
}
if (key === 'order') {
const oid = Number((item && item.id) || 0);
if (!oid) {
uni.showToast({ title: '缺少订单信息', icon: 'none' });
return;
}
// 诊所端进本包订单详情,平台端进 shared避免跨包 + page-container 残留导致列表白屏
const mode = uni.getStorageSync('loginMode') || '';
const detailUrl = mode === 'clinic_admin'
? `/subPackages/sub_clinic_admin/order/detail?id=${oid}`
: `/subPackages/sub_business_shared/order/detail?id=${oid}`;
uni.navigateTo({ url: detailUrl });
return;
}
if (key === 'register') {
// 轻量详情页用列表字段即可,暂存避免 URL 过长
try {
uni.setStorageSync('user_patient_register_detail', item || {});
} catch (e) { /* ignore */ }
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${Number((item && item.id) || 0)}`,
});
}
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx 24rpx 0; background: #f5f7f8; box-sizing: border-box; }
.profile-card {
background: #fff; border-radius: 20rpx; padding: 28rpx 24rpx; margin-bottom: 20rpx;
}
.profile-row { display: flex; align-items: center; gap: 20rpx; margin-bottom: 20rpx; }
.avatar { width: 72rpx; height: 72rpx; border-radius: 50%; background: #e8f5f2; }
.meta { display: flex; flex-direction: column; min-width: 0; }
.name { font-size: 28rpx; font-weight: 600; color: #1f2937; }
.sub { font-size: 22rpx; color: #9ca3af; margin-top: 6rpx; }
.patient-block { padding-top: 20rpx; border-top: 1rpx solid #eef2f1; }
.patient-head {
display: flex; align-items: center; justify-content: space-between; gap: 16rpx;
}
.patient-head-left { display: flex; align-items: center; gap: 12rpx; min-width: 0; flex: 1; }
.p-name { font-size: 30rpx; font-weight: 600; color: #111827; }
.sex-tag {
font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 8rpx;
background: #e8f5f2; color: #2a9d8f;
}
.callback-btn {
flex-shrink: 0;
padding: 8rpx 22rpx;
border-radius: 24rpx;
border: 1rpx solid #6acdbb;
background: #e8f5f2;
}
.callback-btn-text { font-size: 24rpx; color: #1a9b88; font-weight: 500; }
.sex-tag.male { background: #e8f0fe; color: #3b82f6; }
.sex-tag.female { background: #fce7f3; color: #db2777; }
.p-sub { display: block; font-size: 24rpx; color: #6b7280; margin-top: 8rpx; }
.tabs {
display: flex; background: #fff; border-radius: 16rpx 16rpx 0 0;
margin-bottom: 0; padding: 0 8rpx; border-bottom: 1rpx solid #eef2f1;
}
.tab {
flex: 1; display: flex; flex-direction: column; align-items: center;
padding: 22rpx 0 16rpx; position: relative;
}
.tab-text { font-size: 26rpx; color: #6b7280; }
.tab.on .tab-text { color: #1a9b88; font-weight: 600; }
.tab-bar {
position: absolute; bottom: 0; width: 48rpx; height: 4rpx;
border-radius: 4rpx; background: #6acdbb;
}
.tab-swiper { width: 100%; }
.tab-scroll { width: 100%; box-sizing: border-box; }
.health-card {
background: #fff; border-radius: 0 0 16rpx 16rpx; padding: 24rpx;
}
.section-title {
display: block; font-size: 26rpx; font-weight: 600; color: #1f2937; margin-bottom: 16rpx;
}
.info-row {
display: flex; justify-content: space-between; align-items: flex-start;
padding: 16rpx 0; border-bottom: 1rpx solid #f3f4f6; gap: 24rpx;
}
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 24rpx; color: #9ca3af; flex-shrink: 0; }
.info-value { font-size: 24rpx; color: #374151; text-align: right; flex: 1; word-break: break-all; }
.info-value.danger { color: #dc2626; font-weight: 600; }
.empty-inline { text-align: center; color: #9ca3af; padding: 40rpx 0; font-size: 24rpx; }
.record-card {
background: #fff; border-radius: 14rpx; padding: 22rpx 20rpx; margin-bottom: 12rpx;
display: flex; align-items: center; gap: 12rpx;
}
.record-main { flex: 1; min-width: 0; }
.r-title { display: block; font-size: 26rpx; font-weight: 600; color: #111827; }
.r-sub { display: block; font-size: 22rpx; color: #6b7280; margin-top: 6rpx; }
.arrow { font-size: 36rpx; color: #d1d5db; line-height: 1; padding-left: 4rpx; }
.empty { text-align: center; color: #9ca3af; padding: 80rpx 0; font-size: 28rpx; }
</style>

View File

@@ -0,0 +1,144 @@
<template>
<business-page-layout title="会员管理" :show-back="true">
<view class="page">
<view class="search-bar">
<input
class="search-input"
v-model="keyword"
placeholder="昵称 / 手机 / 就诊人"
confirm-type="search"
@confirm="reload"
/>
<view class="search-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"
@click="goPatients(item)"
>
<view class="user-row">
<image class="avatar" :src="avatarSrc(item.avatarurl)" mode="aspectFill" />
<view class="text">
<text class="name">{{ item.nickname || '—' }}</text>
<text class="sub">
ID{{ item.id || '—' }}
<template v-if="item.mobile"> · {{ item.mobile }}</template>
</text>
</view>
<text class="chevron"></text>
</view>
<!-- 就诊人标签化一行多枚自动换行 -->
<view v-if="item.patients && item.patients.length" class="patients">
<view
v-for="p in item.patients"
:key="p.up_id"
class="patient-tag"
>
<text class="tag-text">{{ patientTagText(p) }}</text>
</view>
</view>
<view v-else class="patients empty-p">暂无就诊人</view>
</view>
</view>
</business-page-layout>
</template>
<script>
/**
* 会员管理用户列表:就诊人以标签展示;点用户进入就诊人列表页
*/
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue';
import { getWxUserList, pickProfileItems } from '@/api/userPatientProfile.js';
export default {
components: { BusinessPageLayout },
data() {
return {
keyword: '',
list: [],
loading: false,
};
},
onShow() {
this.reload();
},
methods: {
avatarSrc(raw) {
const s = String(raw || '').trim();
return s || '/static/image/default-avatar.png';
},
/** 标签文案:姓名 或 姓名 · 脱敏手机 */
patientTagText(p) {
const name = (p && p.name) || '—';
const mobile = (p && p.mobile) ? String(p.mobile).trim() : '';
return mobile ? `${name} · ${mobile}` : name;
},
async reload() {
this.loading = true;
try {
// 无关键字时不要传 keyworduni 请求会把 undefined 序列化成字符串 "undefined"
const params = {
page: 1,
pageSize: 50,
};
const kw = (this.keyword || '').trim();
if (kw) {
params.keyword = kw;
}
const wrap = await getWxUserList(params);
this.list = pickProfileItems(wrap);
} finally {
this.loading = false;
}
},
/** 跳转就诊人列表页 */
goPatients(item) {
const userId = Number((item && item.id) || 0);
if (!userId) {
uni.showToast({ title: '缺少用户信息', icon: 'none' });
return;
}
const nick = encodeURIComponent((item && item.nickname) || '');
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/patients?user_id=${userId}&nickname=${nick}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.search-bar { display: flex; gap: 12rpx; margin-bottom: 20rpx; }
.search-input {
flex: 1; background: #fff; border-radius: 12rpx;
padding: 16rpx 20rpx; font-size: 26rpx;
}
.search-btn {
background: #6acdbb; color: #fff; padding: 16rpx 28rpx;
border-radius: 12rpx; font-size: 26rpx;
}
.card {
background: #fff; border-radius: 16rpx; padding: 20rpx 20rpx 16rpx; margin-bottom: 14rpx;
}
.user-row { display: flex; align-items: center; gap: 14rpx; }
.avatar { width: 56rpx; height: 56rpx; border-radius: 50%; background: #e8f5f2; flex-shrink: 0; }
.text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
.name { font-size: 28rpx; color: #111827; font-weight: 500; }
.sub { font-size: 22rpx; color: #9ca3af; }
.chevron { font-size: 32rpx; color: #d1d5db; line-height: 1; }
.patients {
margin-top: 14rpx; padding-top: 14rpx; border-top: 1rpx solid #eef2f1;
display: flex; flex-wrap: wrap; gap: 12rpx;
}
.patient-tag {
padding: 6rpx 16rpx; border-radius: 8rpx;
background: #f0faf7; border: 1rpx solid #6acdbb;
}
.tag-text { font-size: 22rpx; color: #1a9b88; line-height: 1.4; }
.empty-p { font-size: 22rpx; color: #d1d5db; }
.empty { text-align: center; color: #9ca3af; padding: 80rpx 0; font-size: 28rpx; }
</style>

View File

@@ -0,0 +1,118 @@
<template>
<business-page-layout :title="pageTitle" :show-back="true">
<view class="page">
<view v-if="loading" class="empty">加载中...</view>
<!-- 小程序端勿把 v-else-if v-for 写在同级否则有数据也可能只显示空态 -->
<block v-else>
<view v-if="!patientList.length" class="empty">暂无就诊人</view>
<view
v-for="p in patientList"
:key="p.up_id"
class="card"
@click="goDetail(p)"
>
<view class="card-main">
<view class="name-row">
<text class="pname">{{ p.name || p.patient_name || '—' }}</text>
<!-- 小程序 :class 不支持 sexClass(p) 方法调用用对象字面量 -->
<text
v-if="p.sex == 1 || p.sex == 2"
class="sex-tag"
:class="{ male: p.sex == 1, female: p.sex == 2 }"
>{{ p.sex == 1 ? '男' : '女' }}</text>
</view>
<text class="psub">
<template v-if="p.mobile || p.patient_mobile">{{ p.mobile || p.patient_mobile }}</template>
<template v-else>暂无手机号</template>
</text>
</view>
<text class="arrow"></text>
</view>
</block>
</view>
</business-page-layout>
</template>
<script>
/**
* 某微信用户下的就诊人列表页:左主信息 + 右箭头,点进入详情
*/
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue';
import { getPatientListByUser, pickProfileItems } from '@/api/userPatientProfile.js';
export default {
components: { BusinessPageLayout },
data() {
return {
userId: 0,
nickname: '',
patientList: [],
loading: false,
};
},
computed: {
pageTitle() {
return this.nickname ? `就诊人 · ${this.nickname}` : '就诊人列表';
},
},
onLoad(options) {
this.userId = Number((options && options.user_id) || 0);
try {
this.nickname = decodeURIComponent((options && options.nickname) || '');
} catch (e) {
this.nickname = (options && options.nickname) || '';
}
this.reload();
},
methods: {
async reload() {
if (!this.userId) {
this.patientList = [];
return;
}
this.loading = true;
try {
const wrap = await getPatientListByUser(this.userId);
// 有 items 就展示,避免 ok 误判把有效数据丢掉
this.patientList = pickProfileItems(wrap);
} catch (e) {
console.error('就诊人列表加载失败', e);
this.patientList = [];
} finally {
this.loading = false;
}
},
goDetail(p) {
const upId = Number((p && (p.up_id || p.id)) || 0);
if (!upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' });
return;
}
const name = encodeURIComponent((p && (p.name || p.patient_name)) || '');
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/detail?up_id=${upId}&name=${name}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.card {
background: #fff; border-radius: 16rpx; padding: 24rpx 20rpx; margin-bottom: 14rpx;
display: flex; align-items: center; gap: 12rpx;
}
.card-main { flex: 1; min-width: 0; }
.name-row { display: flex; align-items: center; gap: 12rpx; }
.pname { font-size: 30rpx; color: #111827; font-weight: 500; }
.sex-tag {
font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 8rpx;
background: #e8f5f2; color: #2a9d8f;
}
.sex-tag.male { background: #e8f0fe; color: #3b82f6; }
.sex-tag.female { background: #fce7f3; color: #db2777; }
.psub { display: block; font-size: 24rpx; color: #9ca3af; margin-top: 8rpx; }
.arrow { font-size: 36rpx; color: #d1d5db; line-height: 1; }
.empty { text-align: center; color: #9ca3af; padding: 80rpx 0; font-size: 28rpx; }
</style>

View File

@@ -0,0 +1,440 @@
<template>
<business-page-layout title="挂号详情" :show-back="true">
<view class="page">
<view v-if="loading" class="empty">加载中...</view>
<template v-else-if="!detail.id">
<view class="empty">暂无挂号信息</view>
</template>
<template v-else>
<!-- 有药品/回答时才出 Tab仅基本信息时不展示 Tab -->
<view v-if="visibleTabs.length > 1" class="tabs">
<view
v-for="(t, idx) in visibleTabs"
:key="t.key"
class="tab"
:class="{ on: tabIndex === idx }"
@click="selectTab(idx)"
>
<text class="tab-text">{{ t.label }}</text>
<view v-if="tabIndex === idx" class="tab-bar" />
</view>
</view>
<!-- Tab固定高度 swiper避免与外层页面滚动抢手势 -->
<swiper
v-if="visibleTabs.length > 1"
class="tab-swiper"
:style="{ height: swiperHeightPx + 'px' }"
:current="tabIndex"
:duration="200"
@change="onTabSwiperChange"
>
<swiper-item v-for="t in visibleTabs" :key="'sw-' + t.key">
<scroll-view
scroll-y
class="tab-scroll"
:style="{ height: swiperHeightPx + 'px' }"
>
<view v-if="t.key === 'basic'" class="card">
<view class="row">
<text class="label">订单号</text>
<text class="value">{{ detail.order_no || detail.order_number || '—' }}</text>
</view>
<view class="row">
<text class="label">诊所</text>
<text class="value">{{ detail.store || '—' }}</text>
</view>
<view class="row">
<text class="label">医生</text>
<text class="value">{{ detail.doctor || '—' }}</text>
</view>
<view class="row">
<text class="label">挂号类型</text>
<text class="value">{{ detail.type_text || '—' }}</text>
</view>
<view class="row">
<text class="label">状态</text>
<text class="value accent">{{ detail.status_text || '—' }}</text>
</view>
<view class="row">
<text class="label">金额</text>
<text class="value">¥{{ formatPrice(detail.price) }}</text>
</view>
<view class="row">
<text class="label">支付</text>
<text class="value">{{ Number(detail.is_pay) === 1 ? '已支付' : '未支付' }}</text>
</view>
<view class="row">
<text class="label">创建时间</text>
<text class="value">{{ detail.created_at || '—' }}</text>
</view>
<view v-if="detail.pay_time" class="row">
<text class="label">支付时间</text>
<text class="value">{{ detail.pay_time }}</text>
</view>
</view>
<view v-else-if="t.key === 'drugs'" class="card">
<view v-if="!drugRows.length" class="empty-inline">暂无选药</view>
<view v-for="row in drugRows" :key="row._wxKey" class="drug-row">
<image
v-if="row.image"
class="drug-thumb"
:src="row.image"
mode="aspectFill"
@click="previewDrug(row.image)"
/>
<view v-else class="drug-thumb drug-thumb-placeholder">
<text class="drug-thumb-letter">{{ drugThumbLetter(row.name) }}</text>
</view>
<view class="drug-meta">
<text class="drug-name">{{ row.name || '药品' }}</text>
<text class="drug-spec">{{ row.specification || '暂无规格' }}</text>
</view>
<text class="drug-qty">×{{ row.quantity }}</text>
</view>
</view>
<view v-else-if="t.key === 'answers'" class="card">
<view v-if="!answerRecords.length" class="empty-inline">暂无回答</view>
<view v-for="(a, aidx) in answerRecords" :key="'a-' + aidx" class="row answer-row">
<text class="label">{{ a.question || '问题' }}</text>
<text class="value">{{ a.answer || '—' }}</text>
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
<!-- 仅基本信息无滑动 -->
<view v-else class="card">
<view class="row">
<text class="label">订单号</text>
<text class="value">{{ detail.order_no || detail.order_number || '—' }}</text>
</view>
<view class="row">
<text class="label">诊所</text>
<text class="value">{{ detail.store || '—' }}</text>
</view>
<view class="row">
<text class="label">医生</text>
<text class="value">{{ detail.doctor || '—' }}</text>
</view>
<view class="row">
<text class="label">挂号类型</text>
<text class="value">{{ detail.type_text || '—' }}</text>
</view>
<view class="row">
<text class="label">状态</text>
<text class="value accent">{{ detail.status_text || '—' }}</text>
</view>
<view class="row">
<text class="label">金额</text>
<text class="value">¥{{ formatPrice(detail.price) }}</text>
</view>
<view class="row">
<text class="label">支付</text>
<text class="value">{{ Number(detail.is_pay) === 1 ? '已支付' : '未支付' }}</text>
</view>
<view class="row">
<text class="label">创建时间</text>
<text class="value">{{ detail.created_at || '—' }}</text>
</view>
<view v-if="detail.pay_time" class="row">
<text class="label">支付时间</text>
<text class="value">{{ detail.pay_time }}</text>
</view>
</view>
</template>
</view>
</business-page-layout>
</template>
<script>
/**
* 挂号详情:基本信息 + 条件 Tab本次选药 / 回答记录)
* 多 Tab 时用固定高度 swiper 左右滑切换,并在首次进入时短暂提示
*/
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue';
import { getUserPatientRegisterDetail } from '@/api/userPatientProfile.js';
/** 左右滑切换提示只展示一次,避免每次进入打扰 */
const SWIPE_TIP_STORAGE_KEY = 'user_patient_register_detail_swipe_tip';
export default {
components: { BusinessPageLayout },
data() {
return {
loading: false,
registerId: 0,
activeTab: 'basic',
/** swiper 当前下标,与 activeTab / visibleTabs 同步 */
tabIndex: 0,
/** 点击 Tab 与 swiper change 互斥,避免回环 */
tabSwiperChanging: false,
/** 按窗口算出的 swiper 像素高度,保证横向滑动可用 */
swiperHeightPx: 400,
detail: {},
registerInfo: null,
answerRecords: [],
};
},
computed: {
/** 药品行:多选西药优先,否则单药 */
drugRows() {
const info = this.registerInfo;
if (!info || typeof info !== 'object') return [];
const arr = info.selected_western_drugs;
const fallbackQty = info.number != null && info.number !== '' ? Number(info.number) : 1;
if (Array.isArray(arr) && arr.length) {
return arr.map((d, idx) => {
const q = d && d.quantity != null ? Number(d.quantity) : fallbackQty;
const id = d && (d.drug_id || d.id);
return {
_wxKey: 'd-' + (id != null ? id : idx),
name: (d && d.name) || '',
specification: (d && (d.specification || d.spec)) || '',
image: (d && (d.image || d.drug_image)) || '',
quantity: q >= 1 ? q : 1,
};
});
}
if (info.drug && info.drug.name) {
return [{
_wxKey: 'd-' + (info.drug.drug_id || info.drug.id || 0),
name: info.drug.name,
specification: info.drug.specification || info.drug.spec || '',
image: info.drug.image || info.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
}];
}
return [];
},
hasDrugs() {
return this.drugRows.length > 0;
},
hasAnswers() {
return Array.isArray(this.answerRecords) && this.answerRecords.length > 0;
},
/** 仅展示有数据的 Tab基本信息始终保留 */
visibleTabs() {
const all = [
{ key: 'basic', label: '基本信息' },
{ key: 'drugs', label: '本次挂号选择药品', show: this.hasDrugs },
{ key: 'answers', label: '回答记录', show: this.hasAnswers },
];
return all.filter((t) => t.show !== false);
},
},
onLoad(options) {
this.calcSwiperHeight();
const id = Number((options && options.id) || 0);
this.registerId = id;
let cached = {};
try {
cached = uni.getStorageSync('user_patient_register_detail') || {};
} catch (e) {
cached = {};
}
// 缓存作乐观展示id 不一致则只用 URL 中的 id
if (cached && Number(cached.id) === id) {
this.detail = cached;
} else {
this.detail = { id };
}
if (id) {
this.loadDetail(id);
}
},
onReady() {
// 布局就绪后再算一次,避免状态栏/导航高度未就绪
this.calcSwiperHeight();
},
methods: {
/**
* 按窗口高度算死 swiper 高度:窗口 状态栏 导航 Tab 页边距
* 微信小程序 swiper 无明确高度时横向滑动常失效
*/
calcSwiperHeight() {
try {
const sys = uni.getSystemInfoSync() || {};
const winH = Number(sys.windowHeight) || 600;
const statusBar = Number(sys.statusBarHeight) || 0;
const navbar = Number(this.$navbarHeight) || 44;
const rpx = (Number(sys.windowWidth) || 375) / 750;
const pagePad = Math.ceil(24 * rpx);
const tabsH = Math.ceil(88 * rpx);
const h = winH - statusBar - navbar - pagePad * 2 - tabsH;
this.swiperHeightPx = h > 200 ? h : 200;
} catch (e) {
this.swiperHeightPx = 400;
}
},
formatPrice(v) {
const n = Number(v);
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
},
/** 无图时取药名首字作占位,与接诊面板一致 */
drugThumbLetter(name) {
const s = String(name || '').trim();
return s ? s.charAt(0) : '药';
},
previewDrug(url) {
if (!url) return;
uni.previewImage({ urls: [url], current: url });
},
/**
* 点击顶部 Tab同步 activeTab + tabIndex与工作台 swiper 防抖一致)
*/
selectTab(index) {
if (this.tabSwiperChanging) return;
const tab = this.visibleTabs[index];
if (!tab) return;
this.tabIndex = index;
this.activeTab = tab.key;
},
/**
* 左右滑动 swiper同步 Tab 下标与 activeTab
*/
onTabSwiperChange(e) {
const index = e && e.detail ? e.detail.current : 0;
const tab = this.visibleTabs[index];
if (!tab || tab.key === this.activeTab) return;
this.tabSwiperChanging = true;
this.tabIndex = index;
this.activeTab = tab.key;
this.$nextTick(() => {
this.tabSwiperChanging = false;
});
},
/**
* 按 activeTab 校正 tabIndex数据加载后 Tab 集合可能变化)
*/
syncTabIndexFromActive() {
const keys = this.visibleTabs.map((t) => t.key);
let idx = keys.indexOf(this.activeTab);
if (idx < 0) {
this.activeTab = 'basic';
idx = 0;
}
this.tabIndex = idx;
},
/**
* 多 Tab 时首次进入短暂提示「可左右滑动」;本地标记只提示一次
*/
maybeShowSwipeTip() {
if (this.visibleTabs.length <= 1) return;
let shown = '';
try {
shown = uni.getStorageSync(SWIPE_TIP_STORAGE_KEY);
} catch (e) {
shown = '';
}
if (shown) return;
uni.showToast({
title: '左右滑动可切换标签',
icon: 'none',
duration: 2000,
});
try {
uni.setStorageSync(SWIPE_TIP_STORAGE_KEY, '1');
} catch (e) {
// 写缓存失败不影响主流程
}
},
/** 拉取挂号详情并同步 Tab 可见性 */
async loadDetail(id) {
this.loading = true;
try {
const wrap = await getUserPatientRegisterDetail(id);
if (!wrap || !wrap.ok || !wrap.data) {
uni.showToast({ title: '挂号详情加载失败', icon: 'none' });
return;
}
const data = wrap.data;
this.detail = {
id: data.id,
order_no: data.order_no,
order_number: data.order_number,
store: data.store,
doctor: data.doctor,
type: data.type,
type_text: data.type_text,
status_text: data.status_text,
price: data.price,
is_pay: data.is_pay,
created_at: data.created_at,
pay_time: data.pay_time,
};
this.registerInfo = data.register_info || null;
this.answerRecords = Array.isArray(data.answer_records) ? data.answer_records : [];
this.syncTabIndexFromActive();
this.calcSwiperHeight();
this.$nextTick(() => {
this.maybeShowSwipeTip();
});
} finally {
this.loading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.page {
padding: 24rpx;
box-sizing: border-box;
}
.tabs {
display: flex;
flex-wrap: wrap;
gap: 8rpx 24rpx;
margin-bottom: 16rpx;
padding: 0 4rpx;
}
.tab {
position: relative;
padding: 8rpx 0 12rpx;
}
.tab-text { font-size: 28rpx; color: #6b7280; }
.tab.on .tab-text { color: #1a9b88; font-weight: 600; }
.tab-bar {
position: absolute; left: 20%; right: 20%; bottom: 0;
height: 6rpx; border-radius: 6rpx; background: #6acdbb;
}
.tab-swiper {
width: 100%;
}
.tab-scroll {
width: 100%;
box-sizing: border-box;
}
.card {
background: #fff; border-radius: 20rpx; padding: 8rpx 24rpx;
}
.row {
display: flex; justify-content: space-between; align-items: flex-start;
padding: 24rpx 0; border-bottom: 1rpx solid #f3f4f6; gap: 24rpx;
}
.row:last-child { border-bottom: none; }
.answer-row .label { max-width: 46%; }
.label { font-size: 26rpx; color: #9ca3af; flex-shrink: 0; }
.value { font-size: 26rpx; color: #1f2937; text-align: right; flex: 1; word-break: break-all; }
.value.accent { color: #1a9b88; font-weight: 600; }
.drug-row {
display: flex; align-items: flex-start; gap: 16rpx;
padding: 24rpx 0; border-bottom: 1rpx solid #f3f4f6;
}
.drug-row:last-child { border-bottom: none; }
.drug-thumb {
width: 72rpx; height: 72rpx; border-radius: 10rpx; background: #f3f4f6; flex-shrink: 0;
}
.drug-thumb-placeholder {
display: flex; align-items: center; justify-content: center;
background: #e5e7eb;
}
.drug-thumb-letter { font-size: 28rpx; color: #6b7280; font-weight: 600; }
.drug-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.drug-name { font-size: 28rpx; color: #1f2937; font-weight: 500; }
.drug-spec { font-size: 22rpx; color: #9ca3af; }
.drug-qty { font-size: 28rpx; color: #1f2937; font-weight: 600; flex-shrink: 0; }
.empty { text-align: center; color: #9ca3af; padding: 80rpx 0; font-size: 28rpx; }
.empty-inline { text-align: center; color: #9ca3af; padding: 48rpx 0; font-size: 26rpx; }
</style>

View File

@@ -214,6 +214,8 @@ export function mapOrderListRow(item) {
deliveryText: deliveryMethodText(item.delivery_method), deliveryText: deliveryMethodText(item.delivery_method),
deliveryClass: 'tag-delivery', deliveryClass: 'tag-delivery',
userNickname: user.nickname || '-', userNickname: user.nickname || '-',
patientName: (item.user_patient && item.user_patient.name) || item.patient || '-',
upId: Number(item.up_id || (item.user_patient && item.user_patient.id) || 0),
payTime: formatDateTime(item.pay_time), payTime: formatDateTime(item.pay_time),
createdAt: formatDateTime(item.created_at), createdAt: formatDateTime(item.created_at),
storeName: store.name || '-', storeName: store.name || '-',
@@ -346,7 +348,12 @@ export function mapPrescriptionDetailView(item) {
const name = d.name || d.drug_name || '—' const name = d.name || d.drug_name || '—'
const num = d.number != null ? d.number : '' const num = d.number != null ? d.number : ''
const way = (d.use_way && d.use_way.name) || d.useWay || '煎服' const way = (d.use_way && d.use_way.name) || d.useWay || '煎服'
return { name, qty: num ? num + ' /g' : '', usage: way } return {
name,
qty: num ? num + ' /g' : '',
usage: way,
specification: d.specification || '',
}
}) })
: [] : []
recipes.push({ recipes.push({
@@ -369,6 +376,7 @@ export function mapPrescriptionDetailView(item) {
name: d.name || d.drug_name || '—', name: d.name || d.drug_name || '—',
qty: (d.number != null ? d.number : '') + unitName, qty: (d.number != null ? d.number : '') + unitName,
usage: d.useWay || (d.use_way && d.use_way.name) || '', usage: d.useWay || (d.use_way && d.use_way.name) || '',
specification: d.specification || '',
}], }],
usage: '', usage: '',
process: '', process: '',

View File

@@ -78,7 +78,8 @@ const FINANCE_MENUS = [
] ]
const BUSINESS_MENUS = [ const BUSINESS_MENUS = [
{ key: 'order', label: '商品订单', desc: '全平台订单概览', icon: 'bag-fill', theme: GLOBAL_THEMES[3], path: '/subPackages/sub_clinic_admin/order/index' }, { key: 'order', label: '商品订单', desc: '本诊所订单概览', icon: 'bag-fill', theme: GLOBAL_THEMES[3], path: '/subPackages/sub_clinic_admin/order/index' },
{ key: 'user-patient', label: '会员管理', desc: '本诊所微信用户与就诊人', icon: 'account', theme: GLOBAL_THEMES[0], path: '/subPackages/sub_business_shared/user-patient/index' },
] ]
const SALESPERSON_MENUS = [ const SALESPERSON_MENUS = [

View File

@@ -1,52 +1,41 @@
<template> <template>
<clinic-page-layout title="我的" :show-tabbar="true" :tab-index="1"> <clinic-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container"> <view class="mine-container">
<!-- 资料区圆形头像 + 姓名 + 角色对齐医生我的 -->
<!-- 个人信息面板通栏 -->
<view class="profile-panel"> <view class="profile-panel">
<!-- 超椭圆高质感头像 --> <view class="avatar-circle">
<view class="avatar-squircle">
<text>{{ avatarText }}</text> <text>{{ avatarText }}</text>
</view> </view>
<view class="user-info"> <view class="user-info">
<text class="user-name">{{ displayName }}</text> <text class="user-name">{{ displayName }}</text>
<!-- 角色标签替代普通文本提升专业感 -->
<view class="role-badge"> <view class="role-badge">
<text class="role-text">诊所管理员</text> <text class="role-text">诊所管理员</text>
</view> </view>
<text v-if="storeName && storeName !== '-'" class="store-line">{{ storeName }}</text>
</view> </view>
</view> </view>
<!-- 白色菜单卡绑定微信 + 切换账号 -->
<!-- &lt;!&ndash; 信息与设置面板通栏 &ndash;&gt;-->
<!-- <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"> <view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" /> <wx-bind-entry ref="wxBindEntry" />
<account-switch-entry /> <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 -->
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
</view> </view>
<!-- 独立绿色退出按钮 -->
<view class="logout-wrap">
<u-button
:throttle-time="0"
shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '96rpx' }"
@click="logout"
>退出登录</u-button>
</view>
</view> </view>
</clinic-page-layout> </clinic-page-layout>
</template> </template>
<script> <script>
/**
* 诊所/门店管理员「我的」:布局对齐医生(菜单卡 + 绿色退出)
*/
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue' import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue' import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue' import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
@@ -77,7 +66,7 @@ export default {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定退出登录?', content: '确定退出登录?',
confirmColor: '#f53f3f', // 将确认按钮改为警示红,提升用户体验 confirmColor: '#6ACDBB',
success: (res) => { success: (res) => {
if (!res.confirm) return if (!res.confirm) return
clearLoginStorage() clearLoginStorage()
@@ -90,15 +79,11 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 保持与工作台完全一致的色彩变量 */
$theme-primary: #6acdbb; $theme-primary: #6acdbb;
$theme-dark: #4db6a8; $theme-dark: #4db6a8;
$color-page-bg: #F5F7F8; $color-page-bg: #F5F7F8;
$color-text-title: #222B2A; $color-text-title: #222B2A;
$color-text-body: #4E5969;
$color-text-muted: #86909C; $color-text-muted: #86909C;
$color-border: #F2F3F5;
$color-danger: #F53F3F; /* 现代系统标准红 */
.mine-container { .mine-container {
padding: 0 0 60rpx; padding: 0 0 60rpx;
@@ -106,21 +91,17 @@ $color-danger: #F53F3F; /* 现代系统标准红 */
min-height: 100vh; min-height: 100vh;
box-sizing: border-box; box-sizing: border-box;
} }
/* 个人信息面板:通栏纯白 */
.profile-panel { .profile-panel {
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #ffffff; background-color: #ffffff;
padding: 60rpx 40rpx 50rpx; /* 顶部增加呼吸感 */ padding: 60rpx 40rpx 50rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
} }
.avatar-circle {
/* 超椭圆头像:抛弃死板的纯圆,注入品牌色和微弱发光质感 */
.avatar-squircle {
width: 128rpx; width: 128rpx;
height: 128rpx; height: 128rpx;
border-radius: 40rpx; /* 超椭圆倒角 */ border-radius: 50%;
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%); background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.25); box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.25);
color: #ffffff; color: #ffffff;
@@ -131,23 +112,18 @@ $color-danger: #F53F3F; /* 现代系统标准红 */
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
} }
.user-info { .user-info {
margin-left: 32rpx; margin-left: 32rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.user-name { .user-name {
font-size: 40rpx; font-size: 40rpx;
font-weight: 700; font-weight: 700;
color: $color-text-title; color: $color-text-title;
margin-bottom: 12rpx; margin-bottom: 12rpx;
letter-spacing: 1rpx;
} }
/* 角色标签:用线框+背景色取代纯文本,更具系统身份感 */
.role-badge { .role-badge {
align-self: flex-start; align-self: flex-start;
background-color: rgba(106, 205, 187, 0.1); background-color: rgba(106, 205, 187, 0.1);
@@ -155,62 +131,22 @@ $color-danger: #F53F3F; /* 现代系统标准红 */
padding: 4rpx 16rpx; padding: 4rpx 16rpx;
border-radius: 8rpx; border-radius: 8rpx;
} }
.role-text { .role-text {
font-size: 22rpx; font-size: 22rpx;
color: $theme-dark; color: $theme-dark;
font-weight: 600; font-weight: 600;
} }
.store-line {
/* 设置项面板(通栏组) */ margin-top: 12rpx;
font-size: 26rpx;
color: $color-text-muted;
}
.setting-panel { .setting-panel {
background-color: #ffffff; background-color: #ffffff;
margin-bottom: 24rpx; margin-bottom: 24rpx;
padding: 0 40rpx; padding: 8rpx 40rpx 16rpx;
} }
.logout-wrap {
/* 单个设置项 */ padding: 0 40rpx 32rpx;
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx 0;
border-bottom: 1rpx solid $color-border;
transition: background-color 0.2s;
} }
</style>
/* 移除组内最后一个元素的底边框 */
.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;
font-weight: 400;
}
/* 交互列表项 */
.action-item {
/* 扩大点击热区补偿 padding */
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

@@ -1,14 +1,26 @@
<template> <template>
<view v-if="showBlock" class="express-panel"> <view v-if="showBlock" class="express-panel">
<view class="section-title">物流信息</view> <view class="section-title">物流信息</view>
<template v-if="display.hasData"> <!-- 多包裹 Tab -->
<view class="row"><text>物流公司</text><text>{{ display.companyName }}</text></view> <scroll-view v-if="packageList.length > 1" class="pkg-tabs" scroll-x>
<view class="row"><text>运单号</text><text>{{ display.expressNo }}</text></view> <view
<view class="row"><text>最新状态</text><text class="highlight-text">{{ display.stateText }}</text></view> v-for="(pkg, idx) in packageList"
:key="idx"
<view v-if="display.tracks.length" class="timeline"> class="pkg-tab"
:class="{ active: activePkg === idx }"
@click="activePkg = idx"
>
包裹{{ pkg.package_no || idx + 1 }}
<text v-if="pkg.warehouse_name" class="pkg-sub">{{ pkg.warehouse_name }}</text>
</view>
</scroll-view>
<template v-if="currentDisplay.hasData">
<view class="row"><text>物流公司</text><text>{{ currentDisplay.companyName }}</text></view>
<view class="row"><text>运单号</text><text>{{ currentDisplay.expressNo }}</text></view>
<view class="row"><text>最新状态</text><text class="highlight-text">{{ currentDisplay.stateText }}</text></view>
<view v-if="currentDisplay.tracks.length" class="timeline">
<view class="timeline-title">物流追踪</view> <view class="timeline-title">物流追踪</view>
<view v-for="(t, idx) in display.tracks" :key="idx" class="track-item"> <view v-for="(t, idx) in currentDisplay.tracks" :key="idx" class="track-item">
<view class="track-dot" :class="{ 'active': idx === 0 }" /> <view class="track-dot" :class="{ 'active': idx === 0 }" />
<view class="track-body"> <view class="track-body">
<text class="track-status" :class="{ 'active': idx === 0 }">{{ t.status }}</text> <text class="track-status" :class="{ 'active': idx === 0 }">{{ t.status }}</text>
@@ -18,7 +30,9 @@
</view> </view>
</view> </view>
</template> </template>
<view v-else class="empty-express">暂无物流信息</view> <view v-else class="empty-express">
{{ currentPkg && Number(currentPkg.is_send) === 0 ? '本包裹尚未发货' : '暂无物流信息' }}
</view>
</view> </view>
</template> </template>
@@ -31,14 +45,39 @@ export default {
express: { type: Object, default: null }, express: { type: Object, default: null },
show: { type: Boolean, default: true }, show: { type: Boolean, default: true },
}, },
data() {
return { activePkg: 0 }
},
computed: { computed: {
showBlock() { showBlock() {
return this.show return this.show
}, },
display() { /** 优先 packages否则当作单包 */
packageList() {
const pkgs = this.express && Array.isArray(this.express.packages) ? this.express.packages : []
if (pkgs.length) return pkgs
if (this.express && (this.express.express_no || this.express.detail)) {
return [{ package_no: 1, warehouse_name: '', is_send: 1, express: this.express }]
}
return []
},
currentPkg() {
return this.packageList[this.activePkg] || null
},
currentDisplay() {
const pkg = this.currentPkg
if (pkg && pkg.express) return mapExpressDetail(pkg.express)
if (pkg && Number(pkg.is_send) === 0) {
return { hasData: false, companyName: '', expressNo: '', stateText: '', tracks: [] }
}
return mapExpressDetail(this.express) return mapExpressDetail(this.express)
}, },
}, },
watch: {
express() {
this.activePkg = 0
},
},
} }
</script> </script>
@@ -53,7 +92,6 @@ $color-border: #F2F3F5;
background: #ffffff; background: #ffffff;
padding: 32rpx 40rpx; padding: 32rpx 40rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
/* 移除圆角和阴影,采用通栏设计 */
} }
.section-title { .section-title {
@@ -63,6 +101,34 @@ $color-border: #F2F3F5;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.pkg-tabs {
white-space: nowrap;
margin-bottom: 16rpx;
}
.pkg-tab {
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 12rpx 24rpx;
margin-right: 12rpx;
border-radius: 12rpx;
background: #f5f5f5;
font-size: 26rpx;
color: $color-text-muted;
&.active {
background: rgba(106, 205, 187, 0.15);
color: $theme-primary;
font-weight: 600;
}
.pkg-sub {
font-size: 20rpx;
margin-top: 4rpx;
}
}
.row { .row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -127,7 +193,6 @@ $color-border: #F2F3F5;
margin-left: 8rpx; margin-left: 8rpx;
} }
/* 最后一个元素去掉线 */
.track-item:last-child .track-body { .track-item:last-child .track-body {
border-left-color: transparent; border-left-color: transparent;
padding-bottom: 0; padding-bottom: 0;
@@ -165,4 +230,4 @@ $color-border: #F2F3F5;
font-size: 26rpx; font-size: 26rpx;
padding: 40rpx 0; padding: 40rpx 0;
} }
</style> </style>

View File

@@ -1,187 +1,174 @@
<template> <template>
<drawer-page-container <!-- 小程序抽屉须用 page-container + v-if避免 u-popup 在订单详情页底内联 -->
v-model="show" <page-container
title="处方详情" v-if="show"
height="85%" :show="show"
tall :overlay="true"
:show-close="true" position="bottom"
:closeable="true" :round="true"
@close="onClose" @beforeleave="onClose"
@clickoverlay="onClose"
> >
<view class="popup-wrap"> <view class="rx-drawer">
<scroll-view v-if="view" scroll-y class="popup-scroll"> <view class="rx-drawer-head">
<view class="container safe-area-inset-bottom"> <text class="rx-drawer-title">处方详情</text>
<!-- 处方头部 --> <text class="rx-drawer-close" @click="onClose">关闭</text>
<view class="head"> </view>
<view class="head_record_top"> <view class="popup-wrap">
<view class="record"> <scroll-view v-if="view" scroll-y class="popup-scroll">
处方编号{{ view.prescriptionNo }} <view class="container safe-area-inset-bottom">
<view class="head">
<view class="head_record_top">
<view class="record">
处方编号{{ view.prescriptionNo }}
</view>
<view class="record_state">
{{ view.typeText }}
</view>
</view> </view>
<view class="record_state"> <view class="head_record">
{{ view.typeText }} <image :src="view.sealImage || '/static/group/xkyard.png'" mode="aspectFit"></image>
<view class="record_yard">
<view class="yard">
{{ view.storeName }}
</view>
<view class="state">
处方笺
</view>
</view>
</view>
<view class="head_record_time">
<text style="text-align: right;">开具日期{{ view.createdAt }}</text>
</view> </view>
</view> </view>
<view class="my_info">
<!-- 动态医院与公章 --> <view class="info">
<view class="head_record"> <view class="info_item">
<image :src="view.sealImage || '/static/group/xkyard.png'" mode="aspectFit"></image> <view class="name">姓名<text>{{ view.patientName }}</text></view>
<view class="record_yard">
<view class="yard">
{{ view.storeName }}
</view> </view>
<view class="state"> <view class="info_item">
处方笺 <view class="name">性别<text>{{ view.patientSex }}</text></view>
</view>
<view class="info_item">
<view class="name">年龄<text>{{ view.patientAge }}</text></view>
</view>
<view class="info_item">
<view class="name">类别<text>{{ view.category }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="name">科室<text>{{ view.departName }}</text></view>
</view>
<view class="info_item" v-if="view.patientMobile">
<view class="name">电话<text>{{ view.patientMobile }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="names">诊断<text>{{ view.diagnose }}</text></view>
</view>
</view>
<view v-if="view.tcmSyndrome || view.tcmMethod || view.tcmDisease" class="info">
<view v-if="view.tcmSyndrome" class="info_item">
<view class="names">中医证候<text>{{ view.tcmSyndrome }}</text></view>
</view>
<view v-if="view.tcmMethod" class="info_item">
<view class="names">中医治法<text>{{ view.tcmMethod }}</text></view>
</view>
<view v-if="view.tcmDisease" class="info_item">
<view class="names">中医疾病<text>{{ view.tcmDisease }}</text></view>
</view> </view>
</view> </view>
</view> </view>
<view class="bg">
<!-- 时间 --> <view class="my_medical">
<view class="head_record_time"> <view class="title">Rp</view>
<text style="text-align: right;">开具日期{{ view.createdAt }}</text> <view v-for="(recipe, rIdx) in view.recipes" :key="rIdx" class="item">
</view> <view class="name">
</view> <view class="_name" v-for="(drug, dIdx) in recipe.drugs" :key="dIdx">
<view class="text">
<!-- 患者信息 --> <text>{{ drug.name }}</text>
<view class="my_info"> <text v-if="drug.specification" class="drug-spec">{{ drug.specification }}</text>
<view class="info"> <text class="_abbr" v-if="drug.usage">[{{ drug.usage }}]</text>
<view class="info_item"> </view>
<view class="name">姓名<text>{{ view.patientName }}</text></view> <text class="name_num">{{ drug.qty }}</text>
</view>
<view class="info_item">
<view class="name">性别<text>{{ view.patientSex }}</text></view>
</view>
<view class="info_item">
<view class="name">年龄<text>{{ view.patientAge }}</text></view>
</view>
<view class="info_item">
<view class="name">类别<text>{{ view.category }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="name">科室<text>{{ view.departName }}</text></view>
</view>
<view class="info_item" v-if="view.patientMobile">
<view class="name">电话<text>{{ view.patientMobile }}</text></view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="names">诊断<text>{{ view.diagnose }}</text></view>
</view>
</view>
<!-- 中医专属字段 -->
<view v-if="view.tcmSyndrome || view.tcmMethod || view.tcmDisease" class="info">
<view v-if="view.tcmSyndrome" class="info_item">
<view class="names">中医证候<text>{{ view.tcmSyndrome }}</text></view>
</view>
<view v-if="view.tcmMethod" class="info_item">
<view class="names">中医治法<text>{{ view.tcmMethod }}</text></view>
</view>
<view v-if="view.tcmDisease" class="info_item">
<view class="names">中医疾病<text>{{ view.tcmDisease }}</text></view>
</view>
</view>
</view>
<view class="bg">
<!-- 处方明细区 (Rp) -->
<view class="my_medical">
<view class="title">Rp</view>
<view v-for="(recipe, rIdx) in view.recipes" :key="rIdx" class="item">
<view class="name">
<view class="_name" v-for="(drug, dIdx) in recipe.drugs" :key="dIdx">
<view class="text">
<text>{{ drug.name }}</text>
<text class="_abbr" v-if="drug.usage">[{{ drug.usage }}]</text>
</view> </view>
<text class="name_num">{{ drug.qty }}</text>
</view> </view>
</view> <view class="details" v-if="recipe.usage || recipe.process">
<view class="details" v-if="recipe.usage || recipe.process"> <view class="text">
<view class="text"> <text v-if="recipe.usage">用法{{ recipe.usage }}</text>
<text v-if="recipe.usage">用法{{ recipe.usage }}</text> <text v-if="recipe.process" style="margin-left: 10rpx;">包装方式{{ recipe.process }}</text>
<text v-if="recipe.process" style="margin-left: 10rpx;">包装方式{{ recipe.process }}</text> </view>
</view> </view>
</view> </view>
</view> </view>
<view class="doctor_order">
<view class="yz">
<text class="order_info_left">医嘱:</text>
<view class="order_info">
<view class="info">{{ view.doctorOrder !== '-' ? view.doctorOrder : '无' }}</view>
</view>
</view>
<view class="titles">处方开具已完毕</view>
</view>
</view> </view>
<view class="my_doctor">
<!-- 医嘱 --> <view class="doctor_info">
<view class="doctor_order"> <view class="info">
<view class="yz"> <text>医师</text>
<text class="order_info_left">医嘱:</text> <image v-if="view.doctorSignImage" :src="checkImageUrl(view.doctorSignImage)" mode="aspectFit"></image>
<view class="order_info"> <text v-else class="name">{{ view.doctorName }}</text>
<view class="info">{{ view.doctorOrder !== '-' ? view.doctorOrder : '无' }}</view> </view>
<view class="info">
<text>审核药师</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>发药人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>核对人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>调配人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view> </view>
</view> </view>
<view class="titles">处方开具已完毕</view> <view class="price" v-if="view.totalPayPrice">
价格 {{ view.totalPayPrice }}
</view>
</view>
<view class="my_prompt">
<view class="title">
温馨提示请遵医嘱服药处方{{ view.validHours }}小时有效
</view>
<image src="/static/group/img-yzf.png" mode="aspectFit" v-if="view.status == 2"></image>
</view> </view>
</view> </view>
</scroll-view>
<!-- 动态医生与药师签名区 --> <view v-else-if="loading" class="popup-loading">加载中...</view>
<view class="my_doctor"> <view v-else class="popup-loading">暂无数据</view>
<view class="doctor_info"> </view>
<view class="info">
<text>医师</text>
<image v-if="view.doctorSignImage" :src="checkImageUrl(view.doctorSignImage)" mode="aspectFit"></image>
<text v-else class="name">{{ view.doctorName }}</text>
</view>
<view class="info">
<text>审核药师</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>发药人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>核对人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
<text>调配人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
</view>
<!-- 价格 -->
<view class="price" v-if="view.totalPayPrice">
价格 {{ view.totalPayPrice }}
</view>
</view>
<!-- 动态有效期温馨提示 -->
<view class="my_prompt">
<view class="title">
温馨提示请遵医嘱服药处方{{ view.validHours }}小时有效
</view>
<image src="/static/group/img-yzf.png" mode="aspectFit" v-if="view.status == 2"></image>
</view>
</view>
</scroll-view>
<view v-else-if="loading" class="popup-loading">加载中...</view>
<view v-else class="popup-loading">暂无数据</view>
</view> </view>
</drawer-page-container> </page-container>
</template> </template>
<script> <script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue' /**
* 处方详情抽屉page-container 底部浮层(诊所管理端)
*/
import { getPrescriptionDetail } from '@/api/clinicAdmin.js' import { getPrescriptionDetail } from '@/api/clinicAdmin.js'
import { mapPrescriptionDetailView } from '@/subPackages/sub_clinic_admin/common/display.js' import { mapPrescriptionDetailView } from '@/subPackages/sub_clinic_admin/common/display.js'
import { formatStoreNameWithHu } from '@/utils/formatStoreNameWithHu.js'
export default { export default {
components: { DrawerPageContainer },
name: 'PrescriptionDetailPopup', name: 'PrescriptionDetailPopup',
data() { data() {
return { return {
@@ -191,7 +178,7 @@ export default {
} }
}, },
methods: { methods: {
// 解析签名图片,自动兼容 base64 和 http 链接 /** 解析签名图片,自动兼容 base64 和 http 链接 */
checkImageUrl(url) { checkImageUrl(url) {
if (!url) return '' if (!url) return ''
return url.includes('http') ? url : 'data:image/jpeg;base64,' + url return url.includes('http') ? url : 'data:image/jpeg;base64,' + url
@@ -203,20 +190,27 @@ export default {
this.loading = true this.loading = true
getPrescriptionDetail(prescriptionId).then(w => { getPrescriptionDetail(prescriptionId).then(w => {
if (w.ok && w.data) { if (w.ok && w.data) {
// 复用原有的处方解析逻辑
const mapped = mapPrescriptionDetailView(w.data) const mapped = mapPrescriptionDetailView(w.data)
mapped.storeName = formatStoreNameWithHu(
// --- 动态注入你新版 UI 需要的独有字段 --- w.data.store ? w.data.store.name : '未知诊所',
mapped.storeName = w.data.store ? w.data.store.name : '未知诊所' w.data.is_online,
)
mapped.sealImage = w.data.store ? w.data.store.offical_seal : '' mapped.sealImage = w.data.store ? w.data.store.offical_seal : ''
mapped.typeText = w.data.type == 1 ? '普通' : '常用' mapped.typeText = w.data.type == 1 ? '普通' : '常用'
mapped.totalPayPrice = w.data.total_pay_price || '0.00' mapped.totalPayPrice = w.data.total_pay_price || '0.00'
mapped.doctorSignImage = w.data.doctor_sign_image || '' mapped.doctorSignImage =
mapped.pharmacistSignImage = w.data.Pharmacist_sign_image || '' w.data.doctor_sign_image
|| (w.data.doctor_info && w.data.doctor_info.identity_info && w.data.doctor_info.identity_info.sign_image)
|| (w.data.doctorInfo && w.data.doctorInfo.identityInfo && w.data.doctorInfo.identityInfo.sign_image)
|| ''
mapped.pharmacistSignImage =
w.data.Pharmacist_sign_image
|| (w.data.pharmacist_info && w.data.pharmacist_info.identity && w.data.pharmacist_info.identity.sign_image)
|| (w.data.pharmacistInfo && w.data.pharmacistInfo.identity && w.data.pharmacistInfo.identity.sign_image)
|| ''
mapped.validHours = w.data.valid_hours || 72 mapped.validHours = w.data.valid_hours || 72
mapped.status = w.data.status mapped.status = w.data.status
mapped.patientMobile = w.data.patient ? w.data.patient.mobile : '' mapped.patientMobile = w.data.patient ? w.data.patient.mobile : ''
this.view = mapped this.view = mapped
} }
}).finally(() => { }).finally(() => {
@@ -232,20 +226,37 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.rx-drawer {
height: 85vh;
background: #fff;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
}
.rx-drawer-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.rx-drawer-title { font-size: 32rpx; font-weight: 600; color: #1d2129; }
.rx-drawer-close { font-size: 28rpx; color: #6acdbb; }
.popup-wrap { .popup-wrap {
height: 100%; flex: 1;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #fff; background-color: #fff;
position: relative; position: relative;
padding-top: 60rpx; /* 给默认的关闭按钮留出空间 */
} }
.popup-scroll { .popup-scroll {
flex: 1; flex: 1;
height: 0; height: 0;
} }
.popup-loading { .popup-loading {
text-align: center; text-align: center;
color: #999; color: #999;
@@ -443,6 +454,11 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.drug-spec {
color: #8A92A3;
font-size: 22rpx;
margin-left: 8rpx;
}
._abbr { ._abbr {
color: #A7ABB0; color: #A7ABB0;
font-size: 20rpx; font-size: 20rpx;

View File

@@ -1,4 +1,5 @@
<template> <template>
<view>
<clinic-page-layout title="订单详情" :show-back="true"> <clinic-page-layout title="订单详情" :show-back="true">
<view class="detail-container"> <view class="detail-container">
<!-- 头部面板通栏展示金额与状态 --> <!-- 头部面板通栏展示金额与状态 -->
@@ -17,7 +18,14 @@
<view class="sub-block"> <view class="sub-block">
<view class="sub-block-title">基本信息</view> <view class="sub-block-title">基本信息</view>
<view class="row"><text>订单类型</text><text>{{ detailExtra.orderTypeText }}</text></view> <view class="row"><text>订单类型</text><text>{{ detailExtra.orderTypeText }}</text></view>
<view class="row"><text>患者</text><text>{{ info.patient || '-' }}</text></view> <view class="row">
<text>患者</text>
<text
class="patient-link"
:class="{ disabled: !patientUpId }"
@click="openPatient"
>{{ info.patient || '-' }}</text>
</view>
<view class="row"><text>医生</text><text>{{ doctorName }}</text></view> <view class="row"><text>医生</text><text>{{ doctorName }}</text></view>
<view class="row"><text>处方来源</text><text>{{ storeName }}</text></view> <view class="row"><text>处方来源</text><text>{{ storeName }}</text></view>
<view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view> <view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view>
@@ -49,7 +57,14 @@
<!-- 配送信息面板 --> <!-- 配送信息面板 -->
<view v-if="info.id" class="detail-panel"> <view v-if="info.id" class="detail-panel">
<view class="section-title">{{ receiverBlockTitle }}</view> <view class="section-title">{{ receiverBlockTitle }}</view>
<view class="row"><text>{{ receiverLabel }}</text><text>{{ receiverName }}</text></view> <view class="row">
<text>{{ receiverLabel }}</text>
<text
class="patient-link"
:class="{ disabled: !patientUpId }"
@click="openPatient"
>{{ receiverName }}</text>
</view>
<view class="row"><text>联系电话</text><text>{{ receiverMobile }}</text></view> <view class="row"><text>联系电话</text><text>{{ receiverMobile }}</text></view>
<view class="row" v-if="info.delivery_method === 0"> <view class="row" v-if="info.delivery_method === 0">
<text>配送地址</text><text class="addr">{{ deliveryAddress }}</text> <text>配送地址</text><text class="addr">{{ deliveryAddress }}</text>
@@ -141,17 +156,25 @@
</view> </view>
<!-- 处方弹窗 -->
<prescription-detail-popup ref="prescriptionPopup" />
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="info.price_discount || 100"
:quick-options="priceAdjustQuickOptions"
:submitting="priceAdjustSubmitting"
@close="closePriceAdjust"
@confirm="(p) => onPriceAdjustConfirm(p, { adjustOrderPercent })"
/>
</clinic-page-layout> </clinic-page-layout>
<!-- page-container 抽屉挂在页面根级避免被 layout 裁切成页底内联 -->
<prescription-detail-popup ref="prescriptionPopup" />
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="info.price_discount || 100"
:quick-options="priceAdjustQuickOptions"
:submitting="priceAdjustSubmitting"
@close="closePriceAdjust"
@confirm="(p) => onPriceAdjustConfirm(p, { adjustOrderPercent })"
/>
<user-patient-detail
v-if="patientDetailVisible"
:visible="patientDetailVisible"
:up-id="patientDetailUpId"
:patient-name="patientDetailName"
@close="patientDetailVisible = false"
/>
</view>
</template> </template>
<script> <script>
@@ -167,6 +190,7 @@ import {
} from '@/api/clinicAdmin.js' } from '@/api/clinicAdmin.js'
import { orderPriceAdjustMixin } from '@/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js' import { orderPriceAdjustMixin } from '@/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js'
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue' import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue'
import UserPatientDetail from '@/subPackages/sub_business_shared/components/UserPatientDetail.vue'
import { import {
orderStatusText, orderStatusText,
deliveryMethodText, deliveryMethodText,
@@ -180,13 +204,22 @@ import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
export default { export default {
mixins: [orderPriceAdjustMixin], mixins: [orderPriceAdjustMixin],
components: { ClinicPageLayout, ExpressTimeline, PrescriptionDetailPopup, OrderPricePercentAdjustPopup }, components: {
ClinicPageLayout,
ExpressTimeline,
PrescriptionDetailPopup,
OrderPricePercentAdjustPopup,
UserPatientDetail,
},
data() { data() {
return { return {
id: 0, id: 0,
info: {}, info: {},
expressInfo: null, expressInfo: null,
detailExtra: {}, detailExtra: {},
patientDetailVisible: false,
patientDetailUpId: 0,
patientDetailName: '',
} }
}, },
computed: { computed: {
@@ -209,6 +242,13 @@ export default {
const s = this.info.store const s = this.info.store
return (s && s.name) ? s.name : '-' return (s && s.name) ? s.name : '-'
}, },
/** 就诊人 ID订单 up_id 或关联 user_patient.id */
patientUpId() {
const up = Number(this.info.up_id || 0)
if (up) return up
const rel = this.info.user_patient
return Number((rel && rel.id) || 0)
},
receiverBlockTitle() { receiverBlockTitle() {
return this.info.delivery_method === 0 ? '收货人信息' : '就诊人信息' return this.info.delivery_method === 0 ? '收货人信息' : '就诊人信息'
}, },
@@ -244,16 +284,54 @@ export default {
this.id = Number(q.id || 0) this.id = Number(q.id || 0)
this.load() this.load()
}, },
/** 离开页关闭处方抽屉,避免 page-container 残留遮罩导致列表白屏 */
onHide() {
this.closePrescriptionPopup()
this.patientDetailVisible = false
},
onUnload() {
this.closePrescriptionPopup()
this.patientDetailVisible = false
},
methods: { methods: {
/** 关闭处方抽屉 */
closePrescriptionPopup() {
const pop = this.$refs.prescriptionPopup
if (pop && typeof pop.onClose === 'function') {
pop.onClose()
}
},
/**
* 打开患者视角抽屉(与订单列表一致)
*/
openPatient() {
const upId = this.patientUpId
if (!upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' })
return
}
this.patientDetailUpId = upId
this.patientDetailName = this.info.patient || ''
this.patientDetailVisible = true
},
load() { load() {
if (!this.id) {
uni.showToast({ title: '缺少订单信息', icon: 'none' })
return
}
getOrderDetail(this.id).then(w => { getOrderDetail(this.id).then(w => {
if (!w.ok) return if (!w.ok) {
uni.showToast({ title: '订单加载失败', icon: 'none' })
return
}
this.info = w.data || {} this.info = w.data || {}
this.detailExtra = mapOrderDetailDisplay(this.info) this.detailExtra = mapOrderDetailDisplay(this.info)
this.loadPriceAdjustMeta(() => getPriceAdjustConfig(this.info.store_id)) this.loadPriceAdjustMeta(() => getPriceAdjustConfig(this.info.store_id))
if (this.info.delivery_method === 0) { if (this.info.delivery_method === 0) {
this.loadExpress() this.loadExpress()
} }
}).catch(() => {
uni.showToast({ title: '订单加载失败', icon: 'none' })
}) })
}, },
loadExpress() { loadExpress() {
@@ -539,6 +617,11 @@ $color-danger: #F53F3F;
.price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; } .price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; }
.price-link { color: $theme-primary; text-decoration: underline; } .price-link { color: $theme-primary; text-decoration: underline; }
.patient-link {
color: $theme-primary;
text-align: right;
&.disabled { color: inherit; }
}
/* ================= 底部动作栏 ================= */ /* ================= 底部动作栏 ================= */
.actions-bar { .actions-bar {

View File

@@ -1,4 +1,5 @@
<template> <template>
<view>
<clinic-page-layout title="商品订单" :show-back="true"> <clinic-page-layout title="商品订单" :show-back="true">
<view class="order-list-container"> <view class="order-list-container">
<!-- 顶部筛选 --> <!-- 顶部筛选 -->
@@ -96,9 +97,13 @@
<!-- 信息岛 (Info Island)聚合次要信息的高级灰底区块 --> <!-- 信息岛 (Info Island)聚合次要信息的高级灰底区块 -->
<view class="info-island"> <view class="info-island">
<view class="island-row" v-if="rowDisplay(item).userNickname !== '-'"> <view
class="island-row"
v-if="rowDisplay(item).patientName !== '-' || rowDisplay(item).userNickname !== '-'"
@click.stop="openPatient(item)"
>
<text class="island-label">就诊人</text> <text class="island-label">就诊人</text>
<text class="island-value">{{ rowDisplay(item).userNickname }}</text> <text class="island-value link">{{ rowDisplay(item).patientName !== '-' ? rowDisplay(item).patientName : rowDisplay(item).userNickname }}</text>
</view> </view>
<view class="island-row"> <view class="island-row">
<text class="island-label">收件人</text> <text class="island-label">收件人</text>
@@ -135,8 +140,16 @@
</view> </view>
</view> </view>
<prescription-detail-popup ref="prescriptionPopup" />
</clinic-page-layout> </clinic-page-layout>
<prescription-detail-popup ref="prescriptionPopup" />
<user-patient-detail
v-if="patientDetailVisible"
:visible="patientDetailVisible"
:up-id="patientDetailUpId"
:patient-name="patientDetailName"
@close="patientDetailVisible = false"
/>
</view>
</template> </template>
<script> <script>
@@ -144,6 +157,7 @@ import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-p
import PageStickyHeader from '@/subPackages/sub_clinic_admin/components/PageStickyHeader.vue' import PageStickyHeader from '@/subPackages/sub_clinic_admin/components/PageStickyHeader.vue'
import CollapsibleFilterPanel from '@/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue' import CollapsibleFilterPanel from '@/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue'
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue' import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
import UserPatientDetail from '@/subPackages/sub_business_shared/components/UserPatientDetail.vue'
import { getOrderList, getOrderStatusOption, getOrderSaleAmount } from '@/api/clinicAdmin.js' import { getOrderList, getOrderStatusOption, getOrderSaleAmount } from '@/api/clinicAdmin.js'
import { withWxKey } from '@/utils/wxListKey.js' import { withWxKey } from '@/utils/wxListKey.js'
import { mapOrderListRow } from '@/subPackages/sub_clinic_admin/common/display.js' import { mapOrderListRow } from '@/subPackages/sub_clinic_admin/common/display.js'
@@ -174,7 +188,7 @@ const PRESCRIPTION_TYPE_OPTIONS = [
] ]
export default { export default {
components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel, PrescriptionDetailPopup }, components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel, PrescriptionDetailPopup, UserPatientDetail },
data() { data() {
return { return {
list: [], list: [],
@@ -188,6 +202,9 @@ export default {
dateStart: '', dateStart: '',
dateEnd: '', dateEnd: '',
statItems: [], statItems: [],
patientDetailVisible: false,
patientDetailUpId: 0,
patientDetailName: '',
} }
}, },
computed: { computed: {
@@ -214,10 +231,24 @@ export default {
}) })
this.checkDateEndAndReload() this.checkDateEndAndReload()
}, },
/** 离开/隐藏时关掉处方抽屉,避免 page-container 残留白屏 */
onHide() {
this.closePrescriptionPopup()
},
onUnload() {
this.closePrescriptionPopup()
},
onReachBottom() { onReachBottom() {
this.load(this.page + 1, true) this.load(this.page + 1, true)
}, },
methods: { methods: {
/** 关闭处方抽屉 */
closePrescriptionPopup() {
const pop = this.$refs.prescriptionPopup
if (pop && typeof pop.onClose === 'function') {
pop.onClose()
}
},
applyOrderFilter(f) { applyOrderFilter(f) {
this.orderNo = f.orderNo this.orderNo = f.orderNo
this.statusIndex = f.statusIndex this.statusIndex = f.statusIndex
@@ -326,6 +357,17 @@ export default {
if (!pId) return if (!pId) return
this.$refs.prescriptionPopup.open(pId) this.$refs.prescriptionPopup.open(pId)
}, },
openPatient(item) {
const d = this.rowDisplay(item)
const upId = Number(d.upId || 0)
if (!upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' })
return
}
this.patientDetailUpId = upId
this.patientDetailName = d.patientName !== '-' ? d.patientName : ''
this.patientDetailVisible = true
},
}, },
} }
</script> </script>
@@ -569,6 +611,9 @@ $color-text-muted: #86909C;
color: $color-text-body; color: $color-text-body;
flex: 1; flex: 1;
word-break: break-all; word-break: break-all;
&.link {
color: $theme-primary;
}
} }
.muted { .muted {

View File

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

View File

@@ -15,7 +15,7 @@
<image :src="lists.store.offical_seal||'/static/group/xkyard.png'" mode=""></image> <image :src="lists.store.offical_seal||'/static/group/xkyard.png'" mode=""></image>
<view class="record_yard"> <view class="record_yard">
<view class="yard"> <view class="yard">
{{lists.store.name}} {{ storeDisplayName }}
</view> </view>
<view class="state"> <view class="state">
处方笺 处方笺
@@ -223,7 +223,15 @@
prescripDetail, prescripDetail,
getUseWay getUseWay
} from "@/api/all.js"; } from "@/api/all.js";
import { formatStoreNameWithHu } from '@/utils/formatStoreNameWithHu.js';
export default { export default {
computed: {
/** 在线处方诊所名加(互) */
storeDisplayName() {
const store = this.lists && this.lists.store ? this.lists.store : {}
return formatStoreNameWithHu(store.name, this.lists && this.lists.is_online)
},
},
data() { data() {
return { return {
statusName: { statusName: {

View File

@@ -131,15 +131,35 @@
<text class="block-label">主诉</text> <text class="block-label">主诉</text>
<text class="block-content">{{ parsedObj.chief_complaint }}</text> <text class="block-content">{{ parsedObj.chief_complaint }}</text>
</view> </view>
<view v-if="registerCardDrugNamesLine" class="info-row"> <view v-if="registerCardDrugRows.length" class="drug-list-block">
<text class="label">所选药品</text> <text class="block-label">所选药品</text>
<text class="value">{{ registerCardDrugNamesLine }}</text> <view v-for="row in registerCardDrugRows" :key="row._wxKey" class="info-row drug-spec-row">
</view> <image
<view v-if="parsedObj.number != null && parsedObj.number !== ''" class="info-row"> v-if="row.image"
<text class="label">数量</text> class="drug-thumb"
<text class="value"> {{ parsedObj.number }} </text> :src="row.image"
mode="aspectFill"
@click.stop="$emit('previewImage', row.image)"
/>
<view class="drug-name-wrap">
<text class="label">{{ row.name || '药品' }}</text>
<text v-if="row.specification" class="spec-text">{{ row.specification }}</text>
</view>
<text class="value">×{{ row.quantity }}</text>
</view>
</view> </view>
</view> </view>
<view
v-if="registerCardDrugRows.length && canAddExperienceToRx && !isMine"
class="card-footer card-footer--compact"
>
<button
class="action-btn action-btn--ghost"
@click.stop="$emit('addRegisterDrugsToRx')"
>
添加到处方单
</button>
</view>
</block> </block>
<!-- 11 复诊用药 --> <!-- 11 复诊用药 -->
@@ -154,8 +174,11 @@
<view v-if="parsedObj.question" class="text-block"> <view v-if="parsedObj.question" class="text-block">
<text class="block-content">{{ parsedObj.question }}</text> <text class="block-content">{{ parsedObj.question }}</text>
</view> </view>
<view v-for="row in followUpDrugs" :key="row._wxKey" class="info-row"> <view v-for="row in followUpDrugs" :key="row._wxKey" class="info-row drug-spec-row">
<text class="label">{{ row.name || '药品' }}</text> <view class="drug-name-wrap">
<text class="label">{{ row.name || '药品' }}</text>
<text v-if="row.specification" class="spec-text">{{ row.specification }}</text>
</view>
<text class="value">×{{ row.quantity != null ? row.quantity : 1 }}</text> <text class="value">×{{ row.quantity != null ? row.quantity : 1 }}</text>
</view> </view>
<view v-if="followUpAnsweredLabel" class="info-row"> <view v-if="followUpAnsweredLabel" class="info-row">
@@ -166,6 +189,17 @@
<text class="follow-up-pending-tip">请在挂号后的用药确认页完成选择此处不可更改</text> <text class="follow-up-pending-tip">请在挂号后的用药确认页完成选择此处不可更改</text>
</view> </view>
</view> </view>
<view
v-if="followUpDrugs.length && canAddExperienceToRx && !isMine"
class="card-footer card-footer--compact"
>
<button
class="action-btn action-btn--ghost"
@click.stop="$emit('addRegisterDrugsToRx')"
>
添加到处方单
</button>
</view>
</block> </block>
<!-- 11 就诊经历 --> <!-- 11 就诊经历 -->
@@ -202,7 +236,7 @@
class="action-btn action-btn--ghost" class="action-btn action-btn--ghost"
@click.stop="$emit('addExperienceToRx', experienceRxPayload)" @click.stop="$emit('addExperienceToRx', experienceRxPayload)"
> >
加入处方 添加到处方
</button> </button>
</view> </view>
</block> </block>
@@ -370,16 +404,47 @@ export default {
if (typeof c === 'object') return JSON.stringify(c); if (typeof c === 'object') return JSON.stringify(c);
return String(c); return String(c);
}, },
registerCardDrugNamesLine() { registerCardDrugRows() {
if (this.msg.message_type !== 10) return ''; if (this.msg.message_type !== 10) return [];
const pc = this.parsedObj; const pc = this.parsedObj;
if (!pc || typeof pc !== 'object') return ''; if (!pc || typeof pc !== 'object') return [];
const fallbackQty = pc.number != null && pc.number !== '' ? Number(pc.number) : 1;
const arr = pc.selected_western_drugs; const arr = pc.selected_western_drugs;
if (Array.isArray(arr) && arr.length) { if (Array.isArray(arr) && arr.length) {
return arr.map((d) => d && d.name).filter(Boolean).join('、'); return withWxKey(
arr.map((d) => {
const q = d && d.quantity != null ? Number(d.quantity) : fallbackQty;
return {
name: (d && d.name) || '',
specification: (d && (d.specification || d.spec || d.drug_spec)) || '',
image: (d && (d.image || d.drug_image)) || '',
quantity: q >= 1 ? q : 1,
id: d && (d.drug_id || d.id),
};
}),
'id',
'rd'
);
} }
if (pc.drug && pc.drug.name) return pc.drug.name; if (pc.drug && pc.drug.name) {
return ''; return withWxKey(
[
{
name: pc.drug.name,
specification: pc.drug.specification || pc.drug.spec || '',
image: pc.drug.image || pc.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
id: pc.drug.drug_id || pc.drug.id,
},
],
'id',
'rd'
);
}
return [];
},
registerCardDrugNamesLine() {
return this.registerCardDrugRows.map((r) => r.name).filter(Boolean).join('、');
}, },
followUpAnsweredLabel() { followUpAnsweredLabel() {
const pc = this.parsedObj; const pc = this.parsedObj;
@@ -391,7 +456,14 @@ export default {
}, },
followUpDrugs() { followUpDrugs() {
const drugs = (this.parsedObj && this.parsedObj.drugs) || []; const drugs = (this.parsedObj && this.parsedObj.drugs) || [];
return withWxKey(drugs, 'id', 'fd'); return withWxKey(
drugs.map((d) => ({
...d,
specification: d.specification || d.spec || d.drug_spec || '',
})),
'id',
'fd'
);
}, },
transferQuestionsWithKey() { transferQuestionsWithKey() {
const qs = (this.parsedObj && this.parsedObj.questions) || []; const qs = (this.parsedObj && this.parsedObj.questions) || [];
@@ -683,6 +755,40 @@ $radius-bubble: 12rpx;
color: #999; color: #999;
} }
} }
.drug-spec-row {
align-items: flex-start;
.drug-thumb {
width: 64rpx;
height: 64rpx;
border-radius: 8rpx;
background: #f3f4f6;
flex-shrink: 0;
margin-right: 12rpx;
}
.drug-name-wrap {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4rpx;
flex: 1;
min-width: 0;
}
.spec-text {
display: block;
font-size: 22rpx;
color: #9ca3af;
font-weight: 400;
text-align: left;
}
}
.drug-list-block {
.block-label {
display: block;
font-size: 22rpx;
color: #888;
margin-bottom: 8rpx;
}
}
.desc-text { .desc-text {
font-size: 26rpx; font-size: 26rpx;
color: #333; color: #333;

View File

@@ -1 +1,184 @@
<template> <template>
<view v-if="info" class="panel">
<view class="head-row" v-if="info.user_patient">
<image class="avatar" :src="patientAvatarSrc" mode="aspectFill" />
<view class="head-meta">
<view class="row name-row">
<text class="v name">{{ info.user_patient.name }}</text>
<text class="sub" v-if="info.user_patient.age">{{ info.user_patient.age }}</text>
</view>
<view class="row" v-if="info.order_no">
<text class="k">订单号</text>
<text class="v">{{ info.order_no }}</text>
</view>
</view>
</view>
<view class="row" v-else-if="info.order_no">
<text class="k">订单号</text>
<text class="v">{{ info.order_no }}</text>
</view>
<view class="row" v-if="healthLine"><text class="k">健康问卷</text><text class="v">{{ healthLine }}</text></view>
<view v-if="registerOrder" class="block">
<text class="block-title">本次挂号</text>
<view v-if="registerOrder.order_no" class="row">
<text class="k">挂号单号</text>
<text class="v order-font">{{ registerOrder.order_no }}</text>
</view>
<view v-if="registerOrder.price != null && registerOrder.price !== ''" class="row">
<text class="k">费用</text>
<text class="v price">¥{{ registerOrder.price }}</text>
</view>
<view v-if="registerOrder.status != null && registerOrder.status !== ''" class="row">
<text class="k">挂号状态</text>
<text class="v">{{ registerStatusText(registerOrder.status) }}</text>
</view>
<view v-if="registerOrder.pay_status != null && registerOrder.pay_status !== ''" class="row">
<text class="k">支付状态</text>
<text class="v">{{ registerOrder.pay_status }}</text>
</view>
<view v-for="(sub, six) in childOrders" :key="six" class="sub-order">
<text class="sub-order-label">子订单</text>
<text class="sub-order-no">{{ sub.order_no || sub.no || '—' }}</text>
<text v-if="sub.status != null" class="sub-order-st">{{ sub.status }}</text>
</view>
</view>
</view>
</template>
<script>
/**
* 在线问诊顶部患者信息面板:头像 + 基本信息 + 本次挂号摘要
*/
export default {
name: 'PatientDetailPanel',
props: {
info: { type: Object, default: null }
},
computed: {
/** 有就诊人头像用真实图,否则按性别默认 nan/nv */
patientAvatarSrc() {
const up = this.info && this.info.user_patient;
if (up && up.avatar) return up.avatar;
const sex = up ? Number(up.sex) : 0;
return require(`@/static/image/${sex % 2 === 0 ? 'nv' : 'nan'}.png`);
},
healthLine() {
const h = this.info && this.info.user_patient_health_inquiry;
if (!h) return '';
if (typeof h === 'string') return h;
return h.summary || h.chief_complaint || '';
},
registerOrder() {
const i = this.info;
if (!i) return null;
return i.register_order || null;
},
childOrders() {
const ro = this.registerOrder;
if (!ro) return [];
const raw = ro.child_orders || ro.sub_orders || ro.orders;
return Array.isArray(raw) ? raw : [];
}
},
methods: {
registerStatusText(status) {
const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' };
const n = Number(status);
return map[n] != null ? map[n] : String(status);
}
}
};
</script>
<style lang="scss" scoped>
.panel {
padding: 16rpx 24rpx;
background: #fff;
border-radius: 12rpx;
margin: 16rpx 24rpx;
}
.head-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
}
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #e8f5f2;
flex-shrink: 0;
}
.head-meta {
flex: 1;
min-width: 0;
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 8rpx;
font-size: 26rpx;
}
.name-row {
margin-bottom: 4rpx;
}
.k {
color: #888;
margin-right: 12rpx;
}
.v {
color: #333;
}
.v.name {
font-weight: 600;
}
.sub {
margin-left: 16rpx;
color: #666;
font-size: 24rpx;
}
.block {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
}
.block-title {
display: block;
font-size: 26rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.order-font {
font-family: Courier, monospace;
font-size: 24rpx;
}
.price {
color: #ef4444;
font-weight: 600;
}
.sub-order {
display: flex;
flex-wrap: wrap;
align-items: center;
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
padding: 12rpx;
background: #f9fafb;
border-radius: 8rpx;
}
.sub-order-label {
color: #999;
margin-right: 8rpx;
}
.sub-order-no {
flex: 1;
color: #333;
}
.sub-order-st {
margin-left: 12rpx;
}
</style>

View File

@@ -1,14 +1,14 @@
<template> <template>
<view class="bar" v-if="visible"> <view class="bar" v-if="visible">
<button v-if="showAccept" class="btn primary" size="mini" @click="$emit('accept')">接诊</button> <button v-if="showAccept" class="btn primary" @click="$emit('accept')">接诊</button>
<button v-if="showRefuse" class="btn warn" size="mini" @click="$emit('refuse')">拒诊</button> <button v-if="showRefuse" class="btn warn" @click="$emit('refuse')">拒诊</button>
<button v-if="showPrescription" class="btn primary" size="mini" @click="$emit('prescription')">开方</button> <button v-if="showPrescription" class="btn primary" @click="$emit('prescription')">开方</button>
<button v-if="showEnd" class="btn plain" size="mini" @click="$emit('end')">结束诊断</button> <button v-if="showEnd" class="btn plain" @click="$emit('end')">结束诊断</button>
</view> </view>
</template> </template>
<script> <script>
/** 与 RegisterStatusEnum 对齐1=已支付待接诊 2=接诊中 */ /** 与 RegisterStatusEnum 对齐1=已支付待接诊 2=接诊中;按钮等宽铺满避免挤在左侧 */
export default { export default {
name: 'ReceptionActionBar', name: 'ReceptionActionBar',
props: { props: {
@@ -39,14 +39,25 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.bar { .bar {
display: flex; display: flex;
flex-wrap: wrap; align-items: center;
padding: 16rpx 24rpx; justify-content: space-between;
gap: 16rpx; padding: 20rpx 24rpx;
gap: 24rpx;
background: #f7f8fa; background: #f7f8fa;
border-top: 1rpx solid #eee; border-top: 1rpx solid #eee;
} }
.btn { .btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
margin: 0; margin: 0;
padding: 0;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
&::after {
border: none;
}
} }
.primary { .primary {
background: #6acdbb !important; background: #6acdbb !important;
@@ -60,5 +71,8 @@ export default {
background: #fff !important; background: #fff !important;
color: #333 !important; color: #333 !important;
border: 1rpx solid #ddd !important; border: 1rpx solid #ddd !important;
&::after {
border: none;
}
} }
</style> </style>

View File

@@ -58,6 +58,7 @@
@playAudio="playAudio" @playAudio="playAudio"
@viewPrescription="onBubbleViewPrescription" @viewPrescription="onBubbleViewPrescription"
@addExperienceToRx="onAddExperienceToPrescription" @addExperienceToRx="onAddExperienceToPrescription"
@addRegisterDrugsToRx="onRegisterDrugsBulkToPrescription"
/> />
</view> </view>
@@ -70,8 +71,8 @@
</scroll-view> </scroll-view>
<ReceptionActionBar <ReceptionActionBar
:register-status="Number(patientInfo && patientInfo.status) || 0" :register-status="registerStatusNum"
:room-ended="roomEnded" :room-ended="actionBarRoomEnded"
@accept="onAccept" @accept="onAccept"
@refuse="onRefuseTap" @refuse="onRefuseTap"
@prescription="goPrescriptionV2" @prescription="goPrescriptionV2"
@@ -384,31 +385,58 @@ export default {
navbarTitle() { navbarTitle() {
return this.patientName || '在线问诊'; return this.patientName || '在线问诊';
}, },
/**
* 患者气泡头像:有就诊人头像用真实图,否则按性别默认 nan/nv
*/
patientAvatar() { patientAvatar() {
return this.patientInfo?.user_patient?.avatar || ''; const up = this.patientInfo && this.patientInfo.user_patient;
if (up && up.avatar) return up.avatar;
const sex = up ? Number(up.sex) : 0;
return require(`@/static/image/${sex % 2 === 0 ? 'nv' : 'nan'}.png`);
}, },
doctorAvatar() { doctorAvatar() {
return uni.getStorageSync('doctor_avatar') || ''; return uni.getStorageSync('doctor_avatar') || '';
}, },
/** 当前挂号状态(与 RegisterStatusEnum 对齐1=待接诊 2=接诊中) */
registerStatusNum() {
return Number(this.patientInfo && this.patientInfo.status) || 0;
},
/**
* 问诊会话是否已结束(锁输入、隐藏操作栏)
* 待接诊(1)/接诊中(2) 均不以复用房间的历史结束态为准:
* 否则会藏掉接诊按钮,或接诊后房间未同步导致无法发消息
*/
roomEnded() { roomEnded() {
const st = this.registerStatusNum;
if (st === 1 || st === 2) return false;
return this.roomChatStatus === 1 || this.isConsultationEnded; return this.roomChatStatus === 1 || this.isConsultationEnded;
}, },
inputLocked() { /**
if (this.roomEnded) return true; * 传给 ReceptionActionBar待接诊/接诊中强制不隐藏操作栏
const st = Number(this.patientInfo && this.patientInfo.status); */
if (!st) return false; actionBarRoomEnded() {
const st = this.registerStatusNum;
if (st === 1 || st === 2) return false; if (st === 1 || st === 2) return false;
return this.roomEnded;
},
inputLocked() {
const st = this.registerStatusNum;
// 待接诊、接诊中均可发消息,不因房间残留结束态锁定
if (st === 1 || st === 2) return false;
if (this.roomEnded) return true;
if (!st) return false;
return true; return true;
}, },
lockReason() { lockReason() {
const st = this.registerStatusNum;
if (st === 1 || st === 2) return '';
if (this.roomChatStatus === 1 || this.isConsultationEnded) return '问诊已结束'; if (this.roomChatStatus === 1 || this.isConsultationEnded) return '问诊已结束';
const st = Number(this.patientInfo && this.patientInfo.status);
if (st === 7 || st === 8) return '已拒诊'; if (st === 7 || st === 8) return '已拒诊';
if (st >= 3) return '当前状态不可发送消息'; if (st >= 3) return '当前状态不可发送消息';
return '不可发送'; return '不可发送';
}, },
statusLine() { statusLine() {
const st = Number(this.patientInfo && this.patientInfo.status); const st = this.registerStatusNum;
const m = { const m = {
0: '待支付', 0: '待支付',
1: '待接诊', 1: '待接诊',
@@ -421,7 +449,7 @@ export default {
return m[st] || '问诊中'; return m[st] || '问诊中';
}, },
canAddExperienceDrugFromChat() { canAddExperienceDrugFromChat() {
const st = Number(this.patientInfo && this.patientInfo.status); const st = this.registerStatusNum;
return st === 2 && !this.inputLocked && !!this.registerId; return st === 2 && !this.inputLocked && !!this.registerId;
}, },
registerOrderBlock() { registerOrderBlock() {
@@ -586,7 +614,11 @@ export default {
const res = await getRoomStatusApi({ room_id: this.roomId }); const res = await getRoomStatusApi({ room_id: this.roomId });
const raw = this.unwrap(res) || {}; const raw = this.unwrap(res) || {};
this.roomChatStatus = Number(raw.status) || 0; this.roomChatStatus = Number(raw.status) || 0;
if (this.roomChatStatus === 1) this.isConsultationEnded = true; const st = this.registerStatusNum;
// 待接诊/接诊中:房间可能仍是上一单结束态,不据此标记本单已结束
if (this.roomChatStatus === 1 && st !== 1 && st !== 2) {
this.isConsultationEnded = true;
}
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
@@ -694,31 +726,57 @@ export default {
if (tempIndex !== -1) { if (tempIndex !== -1) {
const patched = ensureWxKey({ ...message, isTemporary: false }, 'id', `m${tempIndex}`); const patched = ensureWxKey({ ...message, isTemporary: false }, 'id', `m${tempIndex}`);
this.$set(this.messages, tempIndex, patched); this.$set(this.messages, tempIndex, patched);
if (message.message_type === chatConfig.messageTypes['end-consultation']) {
this.applyEndConsultationMessage(message);
}
return; return;
} }
} }
const existingIndex = this.messages.findIndex((m) => m.id === message.id); const existingIndex = this.messages.findIndex((m) => m.id === message.id);
if (existingIndex !== -1) { if (existingIndex !== -1) {
this.$set(this.messages, existingIndex, ensureWxKey(message, 'id', `m${existingIndex}`)); this.$set(this.messages, existingIndex, ensureWxKey(message, 'id', `m${existingIndex}`));
if (message.message_type === chatConfig.messageTypes['end-consultation']) {
this.applyEndConsultationMessage(message);
}
return; return;
} }
this.messages.push(ensureWxKey(message, 'id', `m${this.messages.length}`)); this.messages.push(ensureWxKey(message, 'id', `m${this.messages.length}`));
// 结束问诊卡片:待接诊忽略历史结束态;接诊中仅认本单创建之后的结束消息
if (message.message_type === chatConfig.messageTypes['end-consultation']) { if (message.message_type === chatConfig.messageTypes['end-consultation']) {
this.isConsultationEnded = true; this.applyEndConsultationMessage(message);
} }
}, },
/**
* 根据结束问诊消息更新本单结束态,避免医患复用房间时历史卡片误锁待接诊
*/
applyEndConsultationMessage(message) {
const st = this.registerStatusNum;
if (st === 1) {
return;
}
if (st === 2) {
const msgTs = Number(message.created_at || message.timestamp || 0);
const regTs = Number(this.patientInfo && this.patientInfo.created_at) || 0;
const msgSec = msgTs > 1e12 ? Math.floor(msgTs / 1000) : msgTs;
if (regTs > 0 && msgSec < regTs) {
return;
}
}
this.isConsultationEnded = true;
},
isMsgMine(msg) { isMsgMine(msg) {
const senderId = msg.sender_user_id || ''; const senderId = msg.sender_user_id || '';
return senderId === this.currentUserId; return senderId === this.currentUserId;
}, },
getMsgAvatar(msg) { getMsgAvatar(msg) {
const sid = msg.sender_user_id || ''; const sid = msg.sender_user_id || '';
const fallback = 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg'; const assistantFallback = 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg';
if (sid === 'doctor_assistant') return fallback; if (sid === 'doctor_assistant') return assistantFallback;
if (this.isMsgMine(msg)) { if (this.isMsgMine(msg)) {
return this.doctorAvatar || fallback; return this.doctorAvatar || assistantFallback;
} }
return this.patientAvatar || fallback; // 患者侧真实头像或性别默认patientAvatar 已处理)
return this.patientAvatar;
}, },
toMessageMs(t) { toMessageMs(t) {
const n = Number(t) || 0; const n = Number(t) || 0;
@@ -1372,8 +1430,12 @@ export default {
async onAccept() { async onAccept() {
try { try {
await receptionApi(this.registerId); await receptionApi(this.registerId);
// 接诊成功后清掉上一单遗留的结束态,并刷新房间状态
this.isConsultationEnded = false;
this.roomChatStatus = 0;
uni.showToast({ title: '接诊成功', icon: 'none' }); uni.showToast({ title: '接诊成功', icon: 'none' });
await this.loadPatientDetail(); await this.loadPatientDetail();
await this.refreshRoomStatus();
} catch (e) { } catch (e) {
uni.showToast({ title: (e && e.msg) || '接诊失败', icon: 'none' }); uni.showToast({ title: (e && e.msg) || '接诊失败', icon: 'none' });
} }

View File

@@ -6,6 +6,7 @@
<image src="../../static/image/dy.png" class="m-t-8 b-r-8"></image> <image src="../../static/image/dy.png" class="m-t-8 b-r-8"></image>
<d-text text="设置" className="content-c fs-32"></d-text> <d-text text="设置" className="content-c fs-32"></d-text>
</view> </view>
<!-- 协议与客服退出/切账号已上提到我的主页 -->
<view class="p-row-32 white m-t-48"> <view class="p-row-32 white m-t-48">
<view @click="$go('/subPackages/sub_agreement/agreement?type=service_user_agreement')" <view @click="$go('/subPackages/sub_agreement/agreement?type=service_user_agreement')"
class="bottom-border flex-row flex-jus-sp flex-ali-center p-col-32"> class="bottom-border flex-row flex-jus-sp flex-ali-center p-col-32">
@@ -17,44 +18,22 @@
<d-text text="隐私权政策" className="content-c fs-32"></d-text> <d-text text="隐私权政策" className="content-c fs-32"></d-text>
<u-icon name="arrow-right" color="#A7ABB0" size="32"></u-icon> <u-icon name="arrow-right" color="#A7ABB0" size="32"></u-icon>
</view> </view>
<view class="bottom-border flex-row flex-jus-sp flex-ali-center p-col-32"> <view class="flex-row flex-jus-sp flex-ali-center p-col-32">
<d-text text="客服热线" className="content-c fs-32"></d-text> <d-text text="客服热线" className="content-c fs-32"></d-text>
40056256325 40056256325
</view> </view>
</view> </view>
<view class="p-row-32 m-t-8 white">
<account-switch-entry />
</view>
<view class="p-row-32 m-t-8" style="height: 80rpx;">
<u-button :throttle-time="0" shape="circle" :custom-style="{backgroundColor:'#6ACDBB',color:'#fff',heigth:'86rpx'}" @click="logout">
退出登录</u-button>
</view>
</view> </view>
</template> </template>
<script> <script>
import { /**
logout * 药师/导医客服设置页:仅保留协议与客服热线
} from '@/api/all.js' */
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
export default { export default {
components: { AccountSwitchEntry },
data() { data() {
return {} return {}
}, },
methods: {
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);
}
})
}
}
} }
</script> </script>
<style> <style>
@@ -68,11 +47,4 @@
height: 160rpx; height: 160rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.m-t-8 {
::v-deep button {
color: #fff;
background: #6ACDBB;
}
}
</style> </style>

View File

@@ -106,6 +106,11 @@ export default {
icon: 'bag-fill', icon: 'bag-fill',
theme: { bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' } // 安全绿 theme: { bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' } // 安全绿
}, },
{
key: 'user-patient', label: '会员管理', desc: '微信用户与就诊人', path: `${shared}/user-patient/index`,
icon: 'account',
theme: { bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' }
},
{ {
key: 'store-manage', label: '门店管理', desc: '诊所列表与价格配置', path: `${root}/store/index`, key: 'store-manage', label: '门店管理', desc: '诊所列表与价格配置', path: `${root}/store/index`,
icon: 'home-fill', icon: 'home-fill',

View File

@@ -2,7 +2,7 @@
<business-page-layout title="我的" :show-tabbar="true" :tab-index="1"> <business-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container"> <view class="mine-container">
<view class="profile-panel"> <view class="profile-panel">
<view class="avatar-squircle"><text>{{ avatarText }}</text></view> <view class="avatar-circle"><text>{{ avatarText }}</text></view>
<view class="user-info"> <view class="user-info">
<text class="user-name">{{ displayName }}</text> <text class="user-name">{{ displayName }}</text>
<view class="role-badge"><text class="role-text">{{ roleText }}</text></view> <view class="role-badge"><text class="role-text">{{ roleText }}</text></view>
@@ -11,15 +11,23 @@
<view class="setting-panel"> <view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" /> <wx-bind-entry ref="wxBindEntry" />
<account-switch-entry /> <account-switch-entry />
<view class="setting-item action-item" @click="logout"> </view>
<text class="danger-text">退出登录</text> <view class="logout-wrap">
</view> <u-button
:throttle-time="0"
shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '96rpx' }"
@click="logout"
>退出登录</u-button>
</view> </view>
</view> </view>
</business-page-layout> </business-page-layout>
</template> </template>
<script> <script>
/**
* 平台管理员「我的」:对齐医生(菜单卡 + 绿色退出),保留绑定微信
*/
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue' import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue' import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue' import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
@@ -42,7 +50,7 @@ export default {
methods: { methods: {
logout() { logout() {
uni.showModal({ uni.showModal({
title: '提示', content: '确定退出登录?', title: '提示', content: '确定退出登录?', confirmColor: '#6ACDBB',
success: (res) => { success: (res) => {
if (!res.confirm) return if (!res.confirm) return
clearLoginStorage() clearLoginStorage()
@@ -55,14 +63,17 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mine-container { min-height: 100vh; background: #F5F7F8; } .mine-container { min-height: 100vh; background: #F5F7F8; padding-bottom: 60rpx; }
.profile-panel { display: flex; align-items: center; background: #fff; padding: 60rpx 40rpx; margin-bottom: 24rpx; } .profile-panel { display: flex; align-items: center; background: #fff; padding: 60rpx 40rpx; margin-bottom: 24rpx; }
.avatar-squircle { width: 128rpx; height: 128rpx; border-radius: 40rpx; background: linear-gradient(135deg, #6acdbb, #4db6a8); color: #fff; font-size: 52rpx; display: flex; align-items: center; justify-content: center; } .avatar-circle {
width: 128rpx; height: 128rpx; border-radius: 50%;
background: linear-gradient(135deg, #6acdbb, #4db6a8); color: #fff; font-size: 52rpx;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.user-info { margin-left: 32rpx; } .user-info { margin-left: 32rpx; }
.user-name { font-size: 40rpx; font-weight: 700; display: block; margin-bottom: 12rpx; } .user-name { font-size: 40rpx; font-weight: 700; display: block; margin-bottom: 12rpx; }
.role-badge { display: inline-block; background: rgba(106,205,187,.1); padding: 4rpx 16rpx; border-radius: 8rpx; } .role-badge { display: inline-block; background: rgba(106,205,187,.1); padding: 4rpx 16rpx; border-radius: 8rpx; }
.role-text { font-size: 22rpx; color: #4db6a8; } .role-text { font-size: 22rpx; color: #4db6a8; }
.setting-panel { background: #fff; padding: 0 40rpx; margin-bottom: 24rpx; } .setting-panel { background: #fff; padding: 8rpx 40rpx 16rpx; margin-bottom: 24rpx; }
.setting-item { padding: 36rpx 0; border-bottom: 1rpx solid #F2F3F5; } .logout-wrap { padding: 0 40rpx 32rpx; }
.danger-text { color: #F53F3F; font-size: 30rpx; }
</style> </style>

View File

@@ -7,9 +7,20 @@
<text class="addr">{{ store.position || '暂无地址' }}</text> <text class="addr">{{ store.position || '暂无地址' }}</text>
</view> </view>
<view class="section"> <!-- 只读信息业务员无编辑权限时展示配置摘要 -->
<view v-if="!canEdit" class="section">
<text class="section-title">门店信息</text>
<view class="info-row"><text>推广员可见价格</text><text>{{ Number(store.salesperson_see_price) === 1 ? '是' : '否' }}</text></view>
<view class="info-row"><text>包邮</text><text>{{ Number(store.is_shipping_free) === 1 ? '是' : '否' }}</text></view>
<view class="info-row"><text>订阅价格波动</text><text>{{ Number(store.subscribe_price_change) === 0 ? '是' : '否' }}</text></view>
<view class="info-row"><text>查看毛利率</text><text>{{ Number(store.see_rate) === 1 ? '是' : '否' }}</text></view>
<view class="info-row"><text>开方可选医保</text><text>{{ Number(store.allow_insurance_category) === 1 ? '是' : '否' }}</text></view>
<view class="info-row"><text>诊所类型</text><text>{{ clinicTypeText(store.clinic_type) }}</text></view>
</view>
<view v-if="canEdit" class="section">
<text class="section-title">门店配置</text> <text class="section-title">门店配置</text>
<view class="switch-row"> <view v-if="!isSalesperson" class="switch-row">
<text>推广员可见价格</text> <text>推广员可见价格</text>
<switch :checked="Number(store.salesperson_see_price) === 1" color="#6ACDBB" @change="toggleSeePrice" /> <switch :checked="Number(store.salesperson_see_price) === 1" color="#6ACDBB" @change="toggleSeePrice" />
</view> </view>
@@ -40,13 +51,13 @@
<view class="section"> <view class="section">
<text class="section-title">快捷操作</text> <text class="section-title">快捷操作</text>
<view class="action-grid"> <view class="action-grid">
<view class="action-btn" @click="goEdit">编辑资料</view> <view v-if="canEdit" class="action-btn" @click="goEdit">编辑资料</view>
<view class="action-btn" @click="goDrugPrice">修改药品价格</view> <view v-if="canEdit" class="action-btn" @click="goDrugPrice">修改药品价格</view>
<view class="action-btn" @click="goConsultation">在线复诊配置</view> <view v-if="canEdit" class="action-btn" @click="goConsultation">在线复诊配置</view>
<view class="action-btn" @click="goPromoters">管理推广员</view> <view v-if="canEdit && !isSalesperson" class="action-btn" @click="goPromoters">管理推广员</view>
<view class="action-btn" @click="openQr">诊所二维码</view> <view class="action-btn" @click="openQr">诊所二维码</view>
<view class="action-btn" @click="goBankCardReport">{{ bankCardReportButtonText }}</view> <view class="action-btn" @click="goBankCardReport">{{ bankCardReportButtonText }}</view>
<view v-if="!hasAdminAccount" class="action-btn warn" @click="openPc">开通后台</view> <view v-if="canEdit && !hasAdminAccount" class="action-btn warn" @click="openPc">开通后台</view>
</view> </view>
</view> </view>
@@ -60,9 +71,13 @@
</template> </template>
<script> <script>
/**
* 门店详情:超管可编辑;业务员默认只读,系统配置开启后显示编辑入口
*/
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue'; import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import { import {
getPlatformStoreDetail, getStoreDetail,
getSalespersonStoreEditPermission,
toggleSalespersonSeePrice, toggleSalespersonSeePrice,
updateStoreShippingFree, updateStoreShippingFree,
updateStoreSubscribeStatus, updateStoreSubscribeStatus,
@@ -71,7 +86,8 @@ import {
updateStoreClinicType, updateStoreClinicType,
openStorePcWindows, openStorePcWindows,
openStoreQrCode, openStoreQrCode,
} from '@/api/platformStore.js'; isSalespersonStoreMode,
} from '@/api/storeManage.js';
import { openQrCodePreview, buildClinicQrPayload } from '@/utils/qrCodePreview.js'; import { openQrCodePreview, buildClinicQrPayload } from '@/utils/qrCodePreview.js';
const ROOT = '/subPackages/sub_platform_admin/store'; const ROOT = '/subPackages/sub_platform_admin/store';
@@ -84,9 +100,16 @@ export default {
storeId: 0, storeId: 0,
store: {}, store: {},
clinicTypeLabels: ['未设置', '西医诊所', '中医诊所'], clinicTypeLabels: ['未设置', '西医诊所', '中医诊所'],
isSalesperson: false,
/** 业务员编辑开关;超管始终 true */
salespersonEditEnabled: false,
}; };
}, },
computed: { computed: {
canEdit() {
if (!this.isSalesperson) return true;
return !!this.salespersonEditEnabled;
},
clinicTypeIndex() { clinicTypeIndex() {
return Number(this.store.clinic_type) || 0; return Number(this.store.clinic_type) || 0;
}, },
@@ -104,15 +127,28 @@ export default {
}, },
onLoad(options) { onLoad(options) {
this.storeId = Number(options.id || 0); this.storeId = Number(options.id || 0);
this.loadDetail(); this.isSalesperson = isSalespersonStoreMode();
this.bootstrap();
}, },
methods: { methods: {
clinicTypeText(type) { clinicTypeText(type) {
return this.clinicTypeLabels[Number(type) || 0] || '未设置'; return this.clinicTypeLabels[Number(type) || 0] || '未设置';
}, },
async bootstrap() {
if (this.isSalesperson) {
try {
const wrap = await getSalespersonStoreEditPermission();
const data = wrap && wrap.ok ? wrap.data : null;
this.salespersonEditEnabled = !!(data && data.salesperson_store_edit_enabled);
} catch (e) {
this.salespersonEditEnabled = false;
}
}
await this.loadDetail();
},
async loadDetail() { async loadDetail() {
if (!this.storeId) return; if (!this.storeId) return;
const wrap = await getPlatformStoreDetail(this.storeId); const wrap = await getStoreDetail(this.storeId);
if (wrap && wrap.ok) { if (wrap && wrap.ok) {
this.store = wrap.data || {}; this.store = wrap.data || {};
} }
@@ -204,10 +240,11 @@ export default {
.sub, .addr { display: block; font-size: 24rpx; margin-top: 8rpx; opacity: 0.9; } .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 { background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; }
.section-title { display: block; font-size: 28rpx; font-weight: 600; margin-bottom: 16rpx; } .section-title { display: block; font-size: 28rpx; font-weight: 600; margin-bottom: 16rpx; }
.switch-row, .picker-row { .switch-row, .picker-row, .info-row {
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; font-size: 28rpx; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; font-size: 28rpx;
} }
.info-row text:last-child { color: #6b7280; }
.picker-val { color: #6ACDBB; } .picker-val { color: #6ACDBB; }
.action-grid { display: flex; flex-wrap: wrap; gap: 16rpx; } .action-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
.action-btn { .action-btn {

View File

@@ -1,5 +1,5 @@
<template> <template>
<store-page-layout title="门店管理" :show-back="true"> <store-page-layout :title="pageTitle" :show-back="true">
<view class="page"> <view class="page">
<view class="filter-panel"> <view class="filter-panel">
<input class="filter-input" v-model="filters.name" placeholder="诊所名称" /> <input class="filter-input" v-model="filters.name" placeholder="诊所名称" />
@@ -39,8 +39,11 @@
</template> </template>
<script> <script>
/**
* 门店/诊所列表:超管看全量,业务员看自己推广码绑定的诊所
*/
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue'; import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import { getPlatformStoreList } from '@/api/platformStore.js'; import { getStoreList, isSalespersonStoreMode } from '@/api/storeManage.js';
const ROOT = '/subPackages/sub_platform_admin/store'; const ROOT = '/subPackages/sub_platform_admin/store';
@@ -51,9 +54,16 @@ export default {
filters: { name: '', shouzimu: '', mobile: '' }, filters: { name: '', shouzimu: '', mobile: '' },
list: [], list: [],
loading: false, loading: false,
isSalesperson: false,
}; };
}, },
computed: {
pageTitle() {
return this.isSalesperson ? '我的诊所' : '门店管理';
},
},
onShow() { onShow() {
this.isSalesperson = isSalespersonStoreMode();
this.reload(); this.reload();
}, },
methods: { methods: {
@@ -65,7 +75,7 @@ export default {
async reload() { async reload() {
this.loading = true; this.loading = true;
try { try {
const wrap = await getPlatformStoreList({ const wrap = await getStoreList({
page: 1, page: 1,
pageSize: 50, pageSize: 50,
...this.filters, ...this.filters,
@@ -99,20 +109,22 @@ export default {
.filter-btn { .filter-btn {
background: #6ACDBB; color: #fff; padding: 16rpx 32rpx; border-radius: 12rpx; font-size: 26rpx; background: #6ACDBB; color: #fff; padding: 16rpx 32rpx; border-radius: 12rpx; font-size: 26rpx;
} }
.empty { text-align: center; color: #999; padding: 80rpx 0; } .empty { text-align: center; color: #999; padding: 80rpx 0; font-size: 28rpx; }
.card { .card {
background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx; background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
} }
.card-hover { opacity: 0.92; } .card-hover { opacity: 0.92; }
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8rpx; } .card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
.name { font-size: 30rpx; font-weight: 600; flex: 1; } .name { font-size: 30rpx; font-weight: 600; color: #1f2937; }
.type-tag { font-size: 22rpx; color: #6ACDBB; background: #f0fbf8; padding: 4rpx 12rpx; border-radius: 8rpx; } .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; } .addr { display: block; font-size: 24rpx; color: #6b7280; margin-bottom: 12rpx; }
.meta-row { display: flex; justify-content: space-between; font-size: 22rpx; color: #999; margin-bottom: 12rpx; } .meta-row { display: flex; gap: 24rpx; font-size: 22rpx; color: #9ca3af; margin-bottom: 12rpx; }
.tag-row { display: flex; gap: 12rpx; flex-wrap: wrap; } .tag-row { display: flex; flex-wrap: wrap; gap: 8rpx; }
.mini-tag { font-size: 22rpx; color: #999; background: #f5f5f5; padding: 4rpx 12rpx; border-radius: 8rpx; } .mini-tag {
.mini-tag.on { color: #22C55E; background: #F0FDF4; } font-size: 20rpx; padding: 4rpx 10rpx; border-radius: 6rpx;
.mini-tag.warn { color: #D97706; background: #FEF3C7; } background: #f3f4f6; color: #9ca3af;
.mini-tag.bank { color: #3B82F6; background: #EFF6FF; } }
.mini-tag.on { background: #ecfdf5; color: #059669; }
.mini-tag.warn { background: #fef3c7; color: #d97706; }
.mini-tag.bank { background: #eff6ff; color: #3b82f6; }
</style> </style>

View File

@@ -93,6 +93,7 @@ export default {
{ key: 'doctor-form', label: '医生录入', desc: '新增合作医生', path: '/subPackages/sub_salesperson/doctor-input/form', inputType: 'doctor', isForm: true, icon: 'plus-circle-fill', theme: this.themes.doctor }, { key: 'doctor-form', label: '医生录入', desc: '新增合作医生', path: '/subPackages/sub_salesperson/doctor-input/form', inputType: 'doctor', isForm: true, icon: 'plus-circle-fill', theme: this.themes.doctor },
]}, ]},
{ key: 'list', title: '档案管理', items: [ { key: 'list', title: '档案管理', items: [
{ key: 'my-store', label: '我的诊所', desc: '查看所属正式诊所', path: '/subPackages/sub_platform_admin/store/index', icon: 'home', theme: this.themes.store },
{ key: 'store-list', label: '门店列表', desc: '查看已录门店', path: '/subPackages/sub_salesperson/store-input/index', icon: 'home-fill', theme: this.themes.store }, { key: 'store-list', label: '门店列表', desc: '查看已录门店', path: '/subPackages/sub_salesperson/store-input/index', icon: 'home-fill', theme: this.themes.store },
{ key: 'doctor-list', label: '医生列表', desc: '查看已录医生', path: '/subPackages/sub_salesperson/doctor-input/index', icon: 'account-fill', theme: this.themes.doctor }, { key: 'doctor-list', label: '医生列表', desc: '查看已录医生', path: '/subPackages/sub_salesperson/doctor-input/index', icon: 'account-fill', theme: this.themes.doctor },
]} ]}

View File

@@ -2,7 +2,7 @@
<business-page-layout title="我的" :show-tabbar="true" :tab-index="1"> <business-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container"> <view class="mine-container">
<view class="profile-panel"> <view class="profile-panel">
<view class="avatar-squircle"><text>{{ avatarText }}</text></view> <view class="avatar-circle"><text>{{ avatarText }}</text></view>
<view class="user-info"> <view class="user-info">
<text class="user-name">{{ displayName }}</text> <text class="user-name">{{ displayName }}</text>
<view class="role-badge"><text class="role-text">{{ roleText }}</text></view> <view class="role-badge"><text class="role-text">{{ roleText }}</text></view>
@@ -11,13 +11,23 @@
<view class="setting-panel"> <view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" /> <wx-bind-entry ref="wxBindEntry" />
<account-switch-entry /> <account-switch-entry />
<view class="setting-item" @click="logout"><text class="danger-text">退出登录</text></view> </view>
<view class="logout-wrap">
<u-button
:throttle-time="0"
shape="circle"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '96rpx' }"
@click="logout"
>退出登录</u-button>
</view> </view>
</view> </view>
</business-page-layout> </business-page-layout>
</template> </template>
<script> <script>
/**
* 业务员「我的」:对齐医生(菜单卡 + 绿色退出),保留绑定微信
*/
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue' import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue' import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue' import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
@@ -38,7 +48,7 @@ export default {
methods: { methods: {
logout() { logout() {
uni.showModal({ uni.showModal({
title: '提示', content: '确定退出登录?', title: '提示', content: '确定退出登录?', confirmColor: '#6ACDBB',
success: (res) => { success: (res) => {
if (!res.confirm) return if (!res.confirm) return
clearLoginStorage() clearLoginStorage()
@@ -51,12 +61,17 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mine-container { min-height: 100vh; background: #F5F7F8; } .mine-container { min-height: 100vh; background: #F5F7F8; padding-bottom: 60rpx; }
.profile-panel { display: flex; align-items: center; background: #fff; padding: 60rpx 40rpx; margin-bottom: 24rpx; } .profile-panel { display: flex; align-items: center; background: #fff; padding: 60rpx 40rpx; margin-bottom: 24rpx; }
.avatar-squircle { width: 128rpx; height: 128rpx; border-radius: 40rpx; background: linear-gradient(135deg, #6acdbb, #4db6a8); color: #fff; font-size: 52rpx; display: flex; align-items: center; justify-content: center; } .avatar-circle {
width: 128rpx; height: 128rpx; border-radius: 50%;
background: linear-gradient(135deg, #6acdbb, #4db6a8); color: #fff; font-size: 52rpx;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.user-info { margin-left: 32rpx; } .user-info { margin-left: 32rpx; }
.user-name { font-size: 40rpx; font-weight: 700; display: block; margin-bottom: 12rpx; } .user-name { font-size: 40rpx; font-weight: 700; display: block; margin-bottom: 12rpx; }
.role-badge { display: inline-block; background: rgba(106,205,187,.1); padding: 4rpx 16rpx; border-radius: 8rpx; }
.role-text { font-size: 22rpx; color: #4db6a8; } .role-text { font-size: 22rpx; color: #4db6a8; }
.setting-panel { background: #fff; padding: 36rpx 40rpx; } .setting-panel { background: #fff; padding: 8rpx 40rpx 16rpx; margin-bottom: 24rpx; }
.danger-text { color: #F53F3F; } .logout-wrap { padding: 0 40rpx 32rpx; }
</style> </style>

View File

@@ -43,6 +43,7 @@
> >
<text class="selected-chip-name">{{ sel.drug_name || sel.name || '—' }}</text> <text class="selected-chip-name">{{ sel.drug_name || sel.name || '—' }}</text>
<text class="selected-chip-dose">{{ String(sel.number || 0) }}{{ sel.unit && sel.unit.name ? sel.unit.name : 'g' }}</text> <text class="selected-chip-dose">{{ String(sel.number || 0) }}{{ sel.unit && sel.unit.name ? sel.unit.name : 'g' }}</text>
<text v-if="getWayLabel(sel)" class="selected-chip-way">{{ getWayLabel(sel) }}</text>
</view> </view>
<view class="selected-chip-close" :data-wx-key="sel._wxKey" @click.stop="handleSelectedChipRemove"> <view class="selected-chip-close" :data-wx-key="sel._wxKey" @click.stop="handleSelectedChipRemove">
<u-icon name="close" size="22" color="#8A92A3"></u-icon> <u-icon name="close" size="22" color="#8A92A3"></u-icon>
@@ -70,6 +71,7 @@
<view class="flex-row flex-ali-center"> <view class="flex-row flex-ali-center">
<view class="selected-indicator" v-if="isInSelected(item)"></view> <view class="selected-indicator" v-if="isInSelected(item)"></view>
<d-text :text="getDrugName(item)" className="fs-32 font-bold color-title"></d-text> <d-text :text="getDrugName(item)" className="fs-32 font-bold color-title"></d-text>
<text v-if="Number(item.has_delivery_warehouse) === 1" class="multi-wh-tag m-l-12">多仓</text>
</view> </view>
<d-text <d-text
v-if="showDrugPrice" v-if="showDrugPrice"
@@ -98,6 +100,43 @@
</view> </view>
</view> </view>
<!-- 先煎/后下气泡标签卡片点选不用 ActionSheet选项超过 6 个会失败 -->
<view v-if="drugUseWayList.length" class="way-block m-b-16">
<view
class="way-row flex-row flex-ali-center flex-jus-sp"
@click.stop="toggleWayPanel"
:data-index="index"
>
<d-text text="煎法:" className="fs-28 color-title"></d-text>
<view class="way-picker flex-row flex-ali-center">
<text class="fs-26" :class="getWayLabel(item) ? 'color-way' : 'color-sub'">
{{ getWayDisplayName(item) }}
</text>
<u-icon
:name="wayPanelDrugIndex === index ? 'arrow-up' : 'arrow-right'"
size="24"
color="#8A92A3"
class="m-l-8"
></u-icon>
</view>
</view>
<view
v-if="wayPanelDrugIndex === index"
class="way-bubble-card"
@click.stop
>
<view
v-for="w in drugUseWayList"
:key="w.id"
class="way-bubble-tag"
:class="{ active: isWayActive(item, w) }"
@click.stop="selectWayTag"
:data-index="index"
:data-way-id="w.id"
>{{ w.name }}</view>
</view>
</view>
<view class="quantity-input-row flex-row flex-ali-center flex-jus-sp pt-20 border-t-dashed"> <view class="quantity-input-row flex-row flex-ali-center flex-jus-sp pt-20 border-t-dashed">
<view class="quantity-input-left flex-row flex-ali-center"> <view class="quantity-input-left flex-row flex-ali-center">
<d-text text="用量:" className="fs-28 color-title"></d-text> <d-text text="用量:" className="fs-28 color-title"></d-text>
@@ -223,6 +262,11 @@ export default {
showDrugPrice: { showDrugPrice: {
type: Boolean, type: Boolean,
default: true default: true
},
/** 药材级先煎/后下选项(父页 drugUseList.drug_use_way */
drugUseWayList: {
type: Array,
default: () => []
} }
}, },
data() { data() {
@@ -237,13 +281,16 @@ export default {
pageSize: 20, pageSize: 20,
hasMore: false, hasMore: false,
// 常用克数(后端全局配置,每次打开抽屉拉取) // 常用克数(后端全局配置,每次打开抽屉拉取)
commonQuantities: [] commonQuantities: [],
/** 当前展开煎法气泡的药材下标,-1 表示收起 */
wayPanelDrugIndex: -1
}; };
}, },
watch: { watch: {
value(newVal) { value(newVal) {
this.show = newVal; this.show = newVal;
if (newVal) { if (newVal) {
this.wayPanelDrugIndex = -1;
this.selectedDrugs = this.mapSelectedDrugs(JSON.parse(JSON.stringify(this.currentDrugs))); this.selectedDrugs = this.mapSelectedDrugs(JSON.parse(JSON.stringify(this.currentDrugs)));
this.fetchQuickGrams(); this.fetchQuickGrams();
const kw = (this.initialSearchKey || '').trim(); const kw = (this.initialSearchKey || '').trim();
@@ -256,10 +303,13 @@ export default {
} else { } else {
this.resetList(); this.resetList();
} }
} else {
this.wayPanelDrugIndex = -1;
} }
}, },
show(newVal) { show(newVal) {
if (!newVal) { if (!newVal) {
this.wayPanelDrugIndex = -1;
this.$emit('input', false); this.$emit('input', false);
} }
} }
@@ -334,6 +384,7 @@ export default {
this.drugList = []; this.drugList = [];
this.page = 1; this.page = 1;
this.hasMore = false; this.hasMore = false;
this.wayPanelDrugIndex = -1;
}, },
/** /**
* 加载药品列表 * 加载药品列表
@@ -388,17 +439,27 @@ export default {
return matchByIndexId || matchByEntityId || matchByDrugId; return matchByIndexId || matchByEntityId || matchByDrugId;
}); });
this.$set(drug, '_quantity', selected ? selected.number : 0); this.$set(drug, '_quantity', selected ? selected.number : 0);
// 恢复已选煎法(先煎/后下)
const wayId = selected
? Number(selected.way_id ?? selected.use_ways?.id ?? 0)
: Number(drug.way_id || (drug.drug && drug.drug.way_id) || 0);
this.$set(drug, '_way_id', wayId || 0);
return drug; return drug;
}); });
if (this.page > 1) { if (this.page > 1) {
this.drugList = this.drugList.concat(merged); this.drugList = this.drugList.concat(merged);
} else { } else {
// 重新搜索/首屏加载后列表下标变化,收起煎法气泡
this.wayPanelDrugIndex = -1;
this.drugList = merged; this.drugList = merged;
} }
this.drugList.forEach(drug => { this.drugList.forEach(drug => {
if (typeof drug._quantity === 'undefined') { if (typeof drug._quantity === 'undefined') {
this.$set(drug, '_quantity', 0); this.$set(drug, '_quantity', 0);
} }
if (typeof drug._way_id === 'undefined') {
this.$set(drug, '_way_id', Number(drug.way_id || (drug.drug && drug.drug.way_id) || 0));
}
}); });
} catch (error) { } catch (error) {
console.error('获取药品列表失败:', error); console.error('获取药品列表失败:', error);
@@ -420,6 +481,7 @@ export default {
clearTimeout(this.searchTimer); clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(() => { this.searchTimer = setTimeout(() => {
const name = (this.searchKey || '').trim(); const name = (this.searchKey || '').trim();
this.wayPanelDrugIndex = -1;
if (!name) { if (!name) {
this.page = 1; this.page = 1;
this.hasMore = false; this.hasMore = false;
@@ -529,6 +591,10 @@ export default {
const indexId = drug.id; const indexId = drug.id;
const entityId = (drug.drug && drug.drug.id) || drug.id; const entityId = (drug.drug && drug.drug.id) || drug.id;
const wxKeyBase = indexId != null && indexId !== '' ? String(indexId) : String(entityId || ''); const wxKeyBase = indexId != null && indexId !== '' ? String(indexId) : String(entityId || '');
const wayId = Number(
drug._way_id ?? (drug.drug && drug.drug.way_id) ?? drug.way_id ?? 0
);
const wayHit = (this.drugUseWayList || []).find((w) => Number(w.id) === wayId);
return { return {
index_id: indexId, index_id: indexId,
id: entityId, id: entityId,
@@ -536,12 +602,80 @@ export default {
number: drug._quantity, number: drug._quantity,
price: drug.price || 0, price: drug.price || 0,
buy_price: drug.buy_price ?? 0, buy_price: drug.buy_price ?? 0,
way_id: (drug.drug && drug.drug.way_id) || drug.way_id || 0, way_id: wayId || 0,
use_ways: wayHit || null,
select_number: 1, select_number: 1,
_wxKey: wxKeyBase ? `sch-${wxKeyBase}` : `sch${this.selectedDrugs.length}`, _wxKey: wxKeyBase ? `sch-${wxKeyBase}` : `sch${this.selectedDrugs.length}`,
}; };
}, },
/**
* 展示非默认煎法文案;煎服/0 不展示chip或用「煎服默认」在选择行
*/
getWayLabel(drugOrSel) {
if (!drugOrSel) return '';
const wayId = Number(
drugOrSel._way_id ?? drugOrSel.way_id ?? drugOrSel.use_ways?.id ?? 0
);
if (!wayId) return '';
const hit = (this.drugUseWayList || []).find((w) => Number(w.id) === wayId);
const name =
(hit && hit.name) ||
(drugOrSel.use_ways && drugOrSel.use_ways.name) ||
'';
if (!name || name === '煎服') return '';
return name;
},
/**
* 煎法行展示文案:有先煎/后下等显示标签名,否则「煎服(默认)」
*/
getWayDisplayName(drugOrSel) {
return this.getWayLabel(drugOrSel) || '煎服(默认)';
},
/**
* 判断气泡标签是否为当前药材选中煎法
*/
isWayActive(drug, wayItem) {
if (!drug || !wayItem) return false;
const wayId = Number(drug._way_id ?? drug.way_id ?? 0);
return wayId > 0 && wayId === Number(wayItem.id);
},
/**
* 展开/收起当前药材的煎法气泡卡片(同时只开一行)
*/
toggleWayPanel(event) {
const index = Number(event.currentTarget.dataset.index);
if (Number.isNaN(index)) return;
if (!(this.drugUseWayList || []).length) {
this.$toast('暂无煎法选项');
return;
}
this.wayPanelDrugIndex = this.wayPanelDrugIndex === index ? -1 : index;
},
/**
* 气泡标签点选煎法:写入 _way_id 并同步已选处方后收起
*/
selectWayTag(event) {
const ds = event.currentTarget.dataset || {};
const index = Number(ds.index);
const wayId = Number(ds.wayId ?? ds['way-id'] ?? 0);
const drug = this.drugList[index];
if (!drug || Number.isNaN(index)) return;
const picked = (this.drugUseWayList || []).find((w) => Number(w.id) === wayId) || null;
this.$set(drug, '_way_id', wayId || 0);
this.$set(drug, 'way_id', wayId || 0);
const existIndex = this.findSelectedIndex(drug);
if (existIndex !== -1) {
this.$set(this.selectedDrugs[existIndex], 'way_id', wayId || 0);
this.$set(this.selectedDrugs[existIndex], 'use_ways', picked);
this.emitSelect();
} else if (drug._quantity > 0) {
this.tryAutoAddDrug(drug, { showToast: false });
}
this.wayPanelDrugIndex = -1;
},
/** /**
* emit select 事件,传出当前完整的 selectedDrugs深拷贝 * emit select 事件,传出当前完整的 selectedDrugs深拷贝
*/ */
@@ -563,6 +697,11 @@ export default {
const isNew = existIndex === -1; const isNew = existIndex === -1;
if (existIndex !== -1) { if (existIndex !== -1) {
this.$set(this.selectedDrugs[existIndex], 'number', drug._quantity); this.$set(this.selectedDrugs[existIndex], 'number', drug._quantity);
// 保持煎法与列表一致
const wayId = Number(drug._way_id ?? drug.way_id ?? 0);
this.$set(this.selectedDrugs[existIndex], 'way_id', wayId);
const wayHit = (this.drugUseWayList || []).find((w) => Number(w.id) === wayId);
this.$set(this.selectedDrugs[existIndex], 'use_ways', wayHit || null);
} else { } else {
this.selectedDrugs.push(this.buildSelectedItem(drug)); this.selectedDrugs.push(this.buildSelectedItem(drug));
} }
@@ -818,6 +957,7 @@ export default {
.color-title { color: #2A2E35; } .color-title { color: #2A2E35; }
.color-sub { color: #8A92A3; } .color-sub { color: #8A92A3; }
.color-price { color: #F53F3F; } .color-price { color: #F53F3F; }
.color-way { color: #5b8ff9; }
.fs-26 { font-size: 26rpx; } .fs-26 { font-size: 26rpx; }
.fs-28 { font-size: 28rpx; } .fs-28 { font-size: 28rpx; }
.fs-32 { font-size: 32rpx; } .fs-32 { font-size: 32rpx; }
@@ -904,7 +1044,48 @@ export default {
font-size: 24rpx; font-size: 24rpx;
color: #00A88A; color: #00A88A;
margin-left: 8rpx; margin-left: 8rpx;
white-space: nowrap; }
.selected-chip-way {
font-size: 22rpx;
color: #5b8ff9;
margin-left: 8rpx;
font-weight: 500;
}
.way-block {
width: 100%;
}
.way-row {
padding: 8rpx 0;
}
.way-picker {
padding: 8rpx 16rpx;
background: #F7F8FA;
border-radius: 8rpx;
border: 1rpx solid #E2E8F0;
}
/* 煎法气泡卡片:标签直接点选,不限 ActionSheet 6 项 */
.way-bubble-card {
margin-top: 12rpx;
padding: 20rpx;
background: #fff;
border-radius: 16rpx;
border: 1rpx solid #E2E8F0;
box-shadow: 0 8rpx 24rpx rgba(42, 46, 53, 0.1);
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.way-bubble-tag {
padding: 10rpx 24rpx;
background-color: #F4F6F8;
color: #6C7380;
border-radius: 30rpx;
font-size: 24rpx;
line-height: 1.4;
}
.way-bubble-tag.active {
background-color: #00A88A;
color: #fff;
} }
.selected-chip-close { .selected-chip-close {
@@ -1065,4 +1246,17 @@ export default {
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
background-color: #fff; background-color: #fff;
} }
/* 配送仓绑定药品标识 */
.multi-wh-tag {
flex-shrink: 0;
padding: 0 10rpx;
height: 32rpx;
line-height: 30rpx;
font-size: 20rpx;
color: #2b6de5;
background: rgba(43, 109, 229, 0.08);
border: 1rpx solid rgba(43, 109, 229, 0.45);
border-radius: 999rpx;
}
</style> </style>

View File

@@ -44,7 +44,10 @@
></image> ></image>
<view class="flex-1 m-l-24 flex-col"> <view class="flex-1 m-l-24 flex-col">
<view class="flex-row flex-jus-sp flex-ali-start"> <view class="flex-row flex-jus-sp flex-ali-start">
<text class="fs-30 font-bold color-title line-clamp-2 w-70">{{item.drug.drug_name || item.drug.name}}</text> <view class="flex-row flex-ali-center w-70">
<text class="fs-30 font-bold color-title line-clamp-2">{{item.drug.drug_name || item.drug.name}}</text>
<text v-if="Number(item.has_delivery_warehouse) === 1" class="multi-wh-tag m-l-8">多仓</text>
</view>
<text class="fs-32 font-bold color-price">{{parseFloat(item.price || 0).toFixed(2)}}</text> <text class="fs-32 font-bold color-price">{{parseFloat(item.price || 0).toFixed(2)}}</text>
</view> </view>
<view class="flex-row flex-jus-sp flex-ali-center m-t-12"> <view class="flex-row flex-jus-sp flex-ali-center m-t-12">
@@ -357,4 +360,17 @@ export default {
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
background-color: #fff; background-color: #fff;
} }
/* 配送仓绑定药品标识 */
.multi-wh-tag {
flex-shrink: 0;
padding: 0 10rpx;
height: 32rpx;
line-height: 30rpx;
font-size: 20rpx;
color: #2b6de5;
background: rgba(43, 109, 229, 0.08);
border: 1rpx solid rgba(43, 109, 229, 0.45);
border-radius: 999rpx;
}
</style> </style>

View File

@@ -0,0 +1,332 @@
<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<!-- page-container 防止右滑误退出开方页 -->
<page-container
v-if="show"
:show="show"
:overlay="false"
@leave="handleClose"
/>
<!-- #endif -->
<u-popup
v-model="show"
mode="bottom"
:closeable="true"
@close="handleClose"
:safe-area-inset-bottom="true"
:mask-close-able="true"
z-index="10080"
height="75%"
>
<view class="warehouse-select-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 v-if="currentRow" class="step-body">
<view class="step-progress"> {{ stepIndex + 1 }} / {{ localRows.length }} 个药品</view>
<!-- 当前药品 / / 规格 -->
<view class="drug-info flex-row flex-ali-start">
<image
v-if="currentRow.image"
class="drug-img"
:src="currentRow.image"
mode="aspectFill"
/>
<view v-else class="drug-img drug-img-placeholder">暂无图</view>
<view class="drug-meta">
<view class="drug-name">{{ currentRow.drug_name }}</view>
<view class="drug-spec">规格{{ currentRow.specification || '--' }}</view>
</view>
</view>
<view class="card-list">
<view
v-for="opt in currentRow.options"
:key="opt.warehouse_id"
class="wh-card"
:class="{ 'is-active': currentRow.warehouse_id === opt.warehouse_id }"
@click="selectWarehouse(opt.warehouse_id)"
>
<view class="wh-name">{{ opt.warehouse_name }}</view>
<view class="wh-meta">
供货价 {{ opt.quote }} · 库存 {{ opt.available_stock }}
</view>
</view>
</view>
</view>
<view v-else class="empty-tip">当前药品无需选择配送仓库</view>
</scroll-view>
<!-- 底部外包 flex 半宽避免 u-button custom-style flex 在小程序被忽略 -->
<view class="modal-footer white shadow-up">
<view class="footer-btn-wrap">
<u-button
:throttle-time="0"
@click="onLeftClick"
shape="circle"
:custom-style="leftBtnStyle"
>{{ leftBtnText }}</u-button>
</view>
<view class="footer-btn-wrap">
<u-button
:throttle-time="0"
@click="onRightClick"
shape="circle"
:custom-style="rightBtnStyle"
>{{ rightBtnText }}</u-button>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
/**
* 在线复诊开方:按药分步卡片选配送仓
* 默认选中每药 options[0](与 auto 最低 quote 一致)
*/
export default {
name: 'WarehouseSelectDrawer',
props: {
value: {
type: Boolean,
default: false,
},
/** [{ drug_id, drug_name, image, specification, options, warehouse_id? }] */
rows: {
type: Array,
default: () => [],
},
},
data() {
return {
localRows: [],
stepIndex: 0,
};
},
computed: {
show: {
get() {
return this.value;
},
set(v) {
this.$emit('input', v);
},
},
currentRow() {
return this.localRows[this.stepIndex] || null;
},
isFirstStep() {
return this.stepIndex <= 0;
},
isLastStep() {
return this.stepIndex >= Math.max(this.localRows.length - 1, 0);
},
leftBtnText() {
return this.isFirstStep ? '取消' : '上一步';
},
rightBtnText() {
return this.isLastStep ? '确认' : '下一步';
},
leftBtnStyle() {
return {
width: '100%',
height: '88rpx',
margin: '0',
background: '#F5F6F8',
color: '#333',
border: 'none',
fontSize: '30rpx',
fontWeight: 'bold',
};
},
rightBtnStyle() {
return {
width: '100%',
height: '88rpx',
margin: '0',
background: '#2B6DE5',
color: '#fff',
border: 'none',
fontSize: '30rpx',
fontWeight: 'bold',
};
},
},
watch: {
value(val) {
if (val) {
// 打开时默认选中每药最低价仓options[0]);规格/图/名可从 option 回退
this.localRows = (this.rows || []).map((row) => {
const first =
Array.isArray(row.options) && row.options[0] ? row.options[0] : null;
return {
...row,
warehouse_id:
row.warehouse_id ||
(first && first.warehouse_id) ||
0,
options: Array.isArray(row.options) ? row.options : [],
drug_name: row.drug_name || (first && first.drug_name) || '',
image: row.image || (first && first.image) || '',
specification:
row.specification || (first && first.specification) || '',
};
});
this.stepIndex = 0;
}
},
},
methods: {
/** 点击卡片选中当前步骤的仓 */
selectWarehouse(warehouseId) {
if (!this.currentRow) return;
this.$set
? this.$set(this.currentRow, 'warehouse_id', warehouseId)
: (this.currentRow.warehouse_id = warehouseId);
},
onLeftClick() {
if (this.isFirstStep) {
this.handleClose();
return;
}
this.stepIndex -= 1;
},
onRightClick() {
if (!this.currentRow || !this.currentRow.warehouse_id) {
uni.showToast({ title: '请先选择配送仓库', icon: 'none' });
return;
}
if (!this.isLastStep) {
this.stepIndex += 1;
return;
}
this.handleConfirm();
},
handleClose() {
this.show = false;
this.$emit('cancel');
},
handleConfirm() {
const map = {};
for (const row of this.localRows) {
if (!row.warehouse_id) {
uni.showToast({ title: `请为【${row.drug_name}】选择配送仓库`, icon: 'none' });
return;
}
map[row.drug_id] = row.warehouse_id;
}
this.show = false;
this.$emit('confirm', map);
},
},
};
</script>
<style lang="scss" scoped>
.warehouse-select-drawer {
display: flex;
flex-direction: column;
height: 100%;
}
.modal-header {
padding: 32rpx 32rpx 16rpx;
}
.scroll-container {
flex: 1;
height: 0;
padding: 0 24rpx 24rpx;
box-sizing: border-box;
}
.step-progress {
font-size: 24rpx;
color: #8a92a3;
margin-bottom: 20rpx;
}
.drug-info {
padding: 20rpx;
border-radius: 16rpx;
background: #f5f6f8;
margin-bottom: 24rpx;
}
.drug-img {
width: 112rpx;
height: 112rpx;
border-radius: 12rpx;
margin-right: 20rpx;
flex-shrink: 0;
background: #e5e6eb;
}
.drug-img-placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #8a92a3;
}
.drug-meta {
flex: 1;
min-width: 0;
}
.drug-name {
font-size: 30rpx;
font-weight: 600;
color: #1f2329;
}
.drug-spec {
margin-top: 8rpx;
font-size: 24rpx;
color: #8a92a3;
}
.card-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.wh-card {
min-width: 280rpx;
flex: 1;
max-width: 48%;
padding: 20rpx 24rpx;
border-radius: 16rpx;
border: 2rpx solid #e5e6eb;
background: #fff;
box-sizing: border-box;
}
.wh-card.is-active {
border-color: #2b6de5;
background: rgba(43, 109, 229, 0.06);
}
.wh-name {
font-size: 28rpx;
font-weight: 500;
color: #1f2329;
}
.wh-meta {
margin-top: 8rpx;
font-size: 22rpx;
color: #8a92a3;
}
.empty-tip {
padding: 48rpx 0;
text-align: center;
color: #8a92a3;
font-size: 26rpx;
}
.modal-footer {
display: flex;
flex-direction: row;
align-items: center;
gap: 24rpx;
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.footer-btn-wrap {
flex: 1;
min-width: 0;
}
.shadow-up {
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
}
</style>

View File

@@ -46,7 +46,10 @@
></image> ></image>
<view class="flex-1 m-l-24 flex-col"> <view class="flex-1 m-l-24 flex-col">
<view class="flex-row flex-jus-sp flex-ali-start"> <view class="flex-row flex-jus-sp flex-ali-start">
<text class="fs-30 font-bold color-title line-clamp-2 w-70">{{item.drug.drug_name || item.drug.name}}</text> <view class="flex-row flex-ali-center w-70">
<text class="fs-30 font-bold color-title line-clamp-2">{{item.drug.drug_name || item.drug.name}}</text>
<text v-if="Number(item.has_delivery_warehouse) === 1" class="multi-wh-tag m-l-8">多仓</text>
</view>
<text class="fs-32 font-bold color-price">{{parseFloat(item.price || 0).toFixed(2)}}</text> <text class="fs-32 font-bold color-price">{{parseFloat(item.price || 0).toFixed(2)}}</text>
</view> </view>
<view class="flex-row flex-jus-sp flex-ali-center m-t-12"> <view class="flex-row flex-jus-sp flex-ali-center m-t-12">
@@ -390,4 +393,17 @@ export default {
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
background-color: #fff; background-color: #fff;
} }
/* 配送仓绑定药品标识 */
.multi-wh-tag {
flex-shrink: 0;
padding: 0 10rpx;
height: 32rpx;
line-height: 30rpx;
font-size: 20rpx;
color: #2b6de5;
background: rgba(43, 109, 229, 0.08);
border: 1rpx solid rgba(43, 109, 229, 0.45);
border-radius: 999rpx;
}
</style> </style>

View File

@@ -279,7 +279,13 @@
<!-- 中药极简高密度标签云设计 --> <!-- 中药极简高密度标签云设计 -->
<view class="white radius-12 p-32" v-if="currentDrugs.length > 0"> <view class="white radius-12 p-32" v-if="currentDrugs.length > 0">
<view class="flex-row flex-jus-sp flex-ali-center m-b-24"> <view class="flex-row flex-jus-sp flex-ali-center m-b-24">
<text class="fs-28 font-bold color-title">药材清单 ({{currentDrugs.length}})</text> <view class="flex-col">
<text class="fs-28 font-bold color-title">药材清单 ({{currentDrugs.length}})</text>
<text
v-if="!isSpecialPrescriptionCartLocked"
class="fs-22 text-gray m-t-4"
>先煎/后下请在选择中药中设置</text>
</view>
<view class="flex-row"> <view class="flex-row">
<template v-if="!isSpecialPrescriptionCartLocked"> <template v-if="!isSpecialPrescriptionCartLocked">
<text class="fs-26 color-primary m-r-32" @click="handleOpenChineseMedicineModal">修改剂量</text> <text class="fs-26 color-primary m-r-32" @click="handleOpenChineseMedicineModal">修改剂量</text>
@@ -302,6 +308,8 @@
@longpress.stop="!isSpecialPrescriptionCartLocked && onTcmTagLongPress"> @longpress.stop="!isSpecialPrescriptionCartLocked && onTcmTagLongPress">
<text class="tcm-name">{{it.drug_name || it.name}}</text> <text class="tcm-name">{{it.drug_name || it.name}}</text>
<text class="tcm-weight">{{String(it.number)}}{{it.unit ? it.unit.name : 'g'}}</text> <text class="tcm-weight">{{String(it.number)}}{{it.unit ? it.unit.name : 'g'}}</text>
<!-- 展示药材级先煎/后下在选择中药弹窗设置 -->
<text v-if="getTcmWayLabel(it)" class="tcm-way">{{ getTcmWayLabel(it) }}</text>
<text <text
v-if="seeRate == 1 && getChineseItemMargin(it) !== '--'" v-if="seeRate == 1 && getChineseItemMargin(it) !== '--'"
class="tcm-margin" class="tcm-margin"
@@ -546,6 +554,7 @@
:see-rate="seeRate" :see-rate="seeRate"
:salesperson-transfer-mode="isSalespersonTransferMode" :salesperson-transfer-mode="isSalespersonTransferMode"
:show-drug-price="showSalespersonTransferPrice" :show-drug-price="showSalespersonTransferPrice"
:drug-use-way-list="drugUseList.drug_use_way || []"
@select="handleSelectChineseDrug" @select="handleSelectChineseDrug"
/> />
@@ -556,6 +565,14 @@
@confirm="handleTransferPatientConfirm" @confirm="handleTransferPatientConfirm"
/> />
<!-- 在线复诊卡片选配送仓 -->
<WarehouseSelectDrawer
v-model="showWarehouseSelectDrawer"
:rows="warehouseSelectRows"
@confirm="onWarehouseSelectConfirm"
@cancel="onWarehouseSelectCancel"
/>
<!-- 简单产品选择弹窗 --> <!-- 简单产品选择弹窗 -->
<SimpleProductModal <SimpleProductModal
v-model="showSimpleProductModal" v-model="showSimpleProductModal"
@@ -602,6 +619,7 @@
// 注意:这部分保持你原有的逻辑代码,不进行任何修改,确保业务完全正常 // 注意:这部分保持你原有的逻辑代码,不进行任何修改,确保业务完全正常
import { import {
addWestPrescription, addWestPrescription,
getDeliveryWarehouseOptionsByDrugs,
checkChineseMedicineConflictApi, checkChineseMedicineConflictApi,
getDiseaseList, getDiseaseList,
getDrugUseList, getDrugUseList,
@@ -634,6 +652,7 @@ import WesternMedicineUsageModal from './components/modals/WesternMedicineUsageM
import ChineseMedicineConfig from './components/ChineseMedicineConfig.vue'; import ChineseMedicineConfig from './components/ChineseMedicineConfig.vue';
import TraditionalTermPickerModal from './components/modals/TraditionalTermPickerModal.vue'; import TraditionalTermPickerModal from './components/modals/TraditionalTermPickerModal.vue';
import TransferPatientDrawer from './components/modals/TransferPatientDrawer.vue'; import TransferPatientDrawer from './components/modals/TransferPatientDrawer.vue';
import WarehouseSelectDrawer from './components/modals/WarehouseSelectDrawer.vue';
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue'; import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue';
import { applyRatioToDrugs, formatPriceDiscountLabel, normalizeQuickOptions } from '@/subPackages/sub_business_shared/common/pricePercentAdjust.js'; import { applyRatioToDrugs, formatPriceDiscountLabel, normalizeQuickOptions } from '@/subPackages/sub_business_shared/common/pricePercentAdjust.js';
import { import {
@@ -665,7 +684,8 @@ const PRESCRIPTION_OVERLAY_KEYS = [
'showDiagnosisModal', 'showDiagnosisModal',
'showDoctorOrderModal', 'showDoctorOrderModal',
'showSaveCommonPrescriptionModal', 'showSaveCommonPrescriptionModal',
'showTransferPatientDrawer' 'showTransferPatientDrawer',
'showWarehouseSelectDrawer',
]; ];
export default { export default {
@@ -681,6 +701,7 @@ export default {
ChineseMedicineConfig, ChineseMedicineConfig,
TraditionalTermPickerModal, TraditionalTermPickerModal,
TransferPatientDrawer, TransferPatientDrawer,
WarehouseSelectDrawer,
OrderPricePercentAdjustPopup, OrderPricePercentAdjustPopup,
}, },
data() { data() {
@@ -738,6 +759,8 @@ export default {
commonPrescriptionName: '', commonPrescriptionName: '',
isCommonPrescription: false, isCommonPrescription: false,
_presetActiveCategory: null, _presetActiveCategory: null,
/** URL/传方指定了分类时,禁止被诊所类型默认覆盖 */
_categoryLockedByQuery: false,
isSubmitting: false, isSubmitting: false,
identity: uni.getStorageSync('role'), identity: uni.getStorageSync('role'),
registerStoreInfo: null, registerStoreInfo: null,
@@ -817,6 +840,11 @@ export default {
salespersonSeePrice: 0, salespersonSeePrice: 0,
showTransferPatientDrawer: false, showTransferPatientDrawer: false,
transferDrawerSubmitting: false, transferDrawerSubmitting: false,
/** 在线复诊选仓抽屉 */
showWarehouseSelectDrawer: false,
warehouseSelectRows: [],
/** resolvePharmacyWarehouseMap 的 Promise resolvefalse=取消null=无需选map=已选 */
_warehouseSelectResolve: null,
}; };
}, },
computed: { computed: {
@@ -1016,6 +1044,7 @@ export default {
if (this.isSalespersonTransferMode) { if (this.isSalespersonTransferMode) {
this.registerId = SALESPERSON_TRANSFER_STORAGE_KEY; this.registerId = SALESPERSON_TRANSFER_STORAGE_KEY;
this._presetActiveCategory = 1; this._presetActiveCategory = 1;
this._categoryLockedByQuery = true;
// 传方模式默认自制剂 + 剂包 // 传方模式默认自制剂 + 剂包
this.chineseConfig = { this.chineseConfig = {
...this.chineseConfig, ...this.chineseConfig,
@@ -1034,7 +1063,10 @@ export default {
} }
if (options.initial_category !== undefined && options.initial_category !== '') { if (options.initial_category !== undefined && options.initial_category !== '') {
const n = parseInt(options.initial_category, 10); const n = parseInt(options.initial_category, 10);
if (!Number.isNaN(n)) this._presetActiveCategory = n; if (!Number.isNaN(n)) {
this._presetActiveCategory = n;
this._categoryLockedByQuery = true;
}
} }
this.initializePageData(); this.initializePageData();
}, },
@@ -1083,6 +1115,8 @@ export default {
await this.loadRegisterOrderType(); await this.loadRegisterOrderType();
await this.loadBasicConfigData(); await this.loadBasicConfigData();
await this.loadRegisterStoreInfo(); await this.loadRegisterStoreInfo();
// 无 URL/上次切换/草稿时,按诊所类型兜底中药或西药 Tab
this.applyDefaultCategoryByClinicType();
} }
this.loadCurrentCategoryDrugs(); this.loadCurrentCategoryDrugs();
if (this.activeCategory === 1 && this.chineseConfig.ruleType === 2) { if (this.activeCategory === 1 && this.chineseConfig.ruleType === 2) {
@@ -1147,6 +1181,7 @@ export default {
if (this._presetActiveCategory !== null && this._presetActiveCategory !== undefined && !Number.isNaN(this._presetActiveCategory)) { if (this._presetActiveCategory !== null && this._presetActiveCategory !== undefined && !Number.isNaN(this._presetActiveCategory)) {
this.activeCategory = this._presetActiveCategory; this.activeCategory = this._presetActiveCategory;
this._presetActiveCategory = null; this._presetActiveCategory = null;
// 保留 _categoryLockedByQuery避免随后被诊所类型默认覆盖
return; return;
} }
const savedCategory = PrescriptionStorage.getActiveCategory(this.registerId); const savedCategory = PrescriptionStorage.getActiveCategory(this.registerId);
@@ -1163,6 +1198,28 @@ export default {
} }
} }
}, },
/**
* 开方默认 Tab 兜底:仅当无 URL 锁定、无上次切换分类、无任何药品草稿时,
* 按诊所类型设中药(1)/西药(2)。优先级URL > 上次切换 > 草稿 > clinic_type
*/
applyDefaultCategoryByClinicType() {
if (this.isSalespersonTransferMode || this._categoryLockedByQuery) return;
if (!this.registerId) return;
// 最后切换的分类优先,不再用诊所类型覆盖
if (PrescriptionStorage.getActiveCategory(this.registerId)) return;
// 任一类有草稿则沿用 restore 结果
if (PrescriptionStorage.hasDraftWithDrugs(this.registerId)) return;
const clinicType = Number(this.registerStoreInfo?.clinic_type ?? 0);
let next = 0;
if (clinicType === 2) {
next = 1; // 中医诊所 → 中药
} else if (clinicType === 1) {
next = 2; // 西医诊所 → 西(中成)药
}
if (!next || next === this.activeCategory) return;
this.activeCategory = next;
PrescriptionStorage.saveActiveCategory(next, this.registerId);
},
loadCurrentCategoryDrugs() { loadCurrentCategoryDrugs() {
const data = PrescriptionStorage.loadPrescriptionData(this.activeCategory, this.registerId); const data = PrescriptionStorage.loadPrescriptionData(this.activeCategory, this.registerId);
if (data) { if (data) {
@@ -2215,8 +2272,120 @@ export default {
await this.executeSendPrescription(doctorSecondSign, 0, null); await this.executeSendPrescription(doctorSecondSign, 0, null);
} }
}, },
/**
* 主界面药材清单展示先煎/后下文案(在选择中药弹窗设置;默认煎服不展示)
*/
getTcmWayLabel(drug) {
if (!drug) return '';
const wayId = Number(drug.way_id ?? drug.use_ways?.id ?? 0);
if (!wayId) return '';
const list = (this.drugUseList && this.drugUseList.drug_use_way) || [];
const hit = list.find((w) => Number(w.id) === wayId);
const name = (hit && hit.name) || (drug.use_ways && drug.use_ways.name) || '';
if (!name || name === '煎服') return '';
return name;
},
/**
* 在线复诊:有仓药绑定时弹卡片选仓;默认最低价;取消返回 false
*/
async resolvePharmacyWarehouseMap() {
if (!this.isOnlineRevisit) {
return null;
}
const drugs = this.currentDrugs || [];
const drugIds = [];
const needQtyMap = {};
for (const d of drugs) {
const id = Number(d?.id || 0);
const qty = Number(d?.select_number || d?.number || 0);
if (id <= 0 || qty <= 0) continue;
drugIds.push(id);
needQtyMap[id] = (needQtyMap[id] || 0) + qty;
}
if (!drugIds.length) {
return null;
}
try {
const res = await getDeliveryWarehouseOptionsByDrugs({
drug_ids: drugIds.join(','),
need_qty_map: needQtyMap,
});
const map = (res && (res.result || res.data || res)) || {};
const rows = [];
for (const d of drugs) {
const id = Number(d?.id || 0);
if (id <= 0) continue;
const options = map[String(id)] || map[id] || [];
if (!Array.isArray(options) || !options.length) continue;
rows.push({
drug_id: id,
// 优先用仓选项接口带回的药品信息,避免处方草稿未存规格/图
drug_name: String(
(options[0] && options[0].drug_name) ||
d.drug_name ||
d.name ||
`药品#${id}`,
),
image: String(
(options[0] && options[0].image) ||
d.image ||
d._image ||
(d.drug && d.drug.image) ||
'',
),
specification: String(
(options[0] && options[0].specification) ||
d.specification ||
(d.drug && d.drug.specification) ||
'',
),
options: options.map((o) => ({
warehouse_id: Number(o.warehouse_id),
warehouse_name: String(o.warehouse_name || ''),
quote: String(o.quote ?? '0'),
available_stock: Number(o.available_stock ?? 0),
// 保留药品展示字段,供抽屉从 options[0] 回退读取
drug_name: String(o.drug_name || ''),
image: String(o.image || ''),
specification: String(o.specification || ''),
})),
warehouse_id: Number(options[0].warehouse_id),
});
}
if (!rows.length) {
return null;
}
this.warehouseSelectRows = rows;
this.showWarehouseSelectDrawer = true;
return new Promise((resolve) => {
this._warehouseSelectResolve = resolve;
});
} catch (e) {
console.error('加载配送仓库失败', e);
this.$toast((e && e.message) || '加载配送仓库失败');
return false;
}
},
onWarehouseSelectConfirm(map) {
const resolve = this._warehouseSelectResolve;
this._warehouseSelectResolve = null;
this.showWarehouseSelectDrawer = false;
if (typeof resolve === 'function') resolve(map || {});
},
onWarehouseSelectCancel() {
const resolve = this._warehouseSelectResolve;
this._warehouseSelectResolve = null;
this.showWarehouseSelectDrawer = false;
if (typeof resolve === 'function') resolve(false);
},
async executeSendPrescription(doctorSecondSign = 0, sendMode = 0, customStoreId = null) { async executeSendPrescription(doctorSecondSign = 0, sendMode = 0, customStoreId = null) {
try { try {
// 在线复诊有绑仓时先卡片选仓
const warehouseMap = await this.resolvePharmacyWarehouseMap();
if (warehouseMap === false) {
this.isSubmitting = false;
return;
}
if (this.activeCategory === 2) { if (this.activeCategory === 2) {
if (!this.patientInfo || !this.patientId) { if (!this.patientInfo || !this.patientId) {
if (this.patientId && !this.patientInfo) await this.loadPatientInfo(); if (this.patientId && !this.patientInfo) await this.loadPatientInfo();
@@ -2269,6 +2438,13 @@ export default {
price_discount: this.priceDiscount, price_discount: this.priceDiscount,
special_prescription_id: Number(this.appliedSpecialPrescriptionId) || 0, special_prescription_id: Number(this.appliedSpecialPrescriptionId) || 0,
}; };
if (warehouseMap && typeof warehouseMap === 'object') {
// 列表格式避免数字键对象在 form/部分环境序列化丢失
params.warehouse_map = Object.keys(warehouseMap).map((drugId) => ({
drug_id: Number(drugId),
warehouse_id: Number(warehouseMap[drugId]),
}));
}
if (this.activeCategory === 1) { 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; 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;
if (this.isOnlineRevisit) { if (this.isOnlineRevisit) {
@@ -2718,6 +2894,12 @@ export default {
color: #00A88A; color: #00A88A;
margin-left: 12rpx; margin-left: 12rpx;
} }
.tcm-way {
font-size: 22rpx;
color: #5b8ff9;
margin-left: 8rpx;
font-weight: 500;
}
.tcm-margin { .tcm-margin {
font-size: 22rpx; font-size: 22rpx;
color: #ff9800; color: #ff9800;

View File

@@ -0,0 +1,12 @@
/**
* 在线处方诊所名追加「is_online 为 1/2/3线下 0 不加
*/
export function formatStoreNameWithHu(storeName, isOnline) {
const name = String(storeName || '').trim()
if (!name) return ''
const online = Number(isOnline)
if (online === 1 || online === 2 || online === 3) {
return name.endsWith('(互)') ? name : `${name}(互)`
}
return name
}

View File

@@ -6,8 +6,8 @@ export function checkDev(key = '') {
// 判断某些模块是否开启 // 判断某些模块是否开启
switch (key) { switch (key) {
case 'dev': case 'dev':
// return false; return false;
return true; // return true;
case 'open-im': case 'open-im':
// return false; // return false;
return true; return true;