feat: 优化了若干接口,可用性存疑

This commit is contained in:
李琦
2026-08-12 08:57:35 +08:00
parent 929d6f1af6
commit 09db7705e8
92 changed files with 4573 additions and 9472 deletions

View File

@@ -25,6 +25,18 @@ export function createBusinessApi(prefix, headerFlag) {
getMyInfo() {
return bizRequest({ url: `${prefix}-auth/my-info`, method: 'GET' });
},
/** 个人中心 - 我的资料(xk-api GET {prefix}-auth/my-profile,实时库数据) */
getMyProfile() {
return bizRequest({ url: `${prefix}-auth/my-profile`, method: 'GET' });
},
/** 个人中心 - 修改资料(仅昵称/头像,xk-api POST {prefix}-auth/update-my-profile) */
updateMyProfile(data) {
return bizRequest({ url: `${prefix}-auth/update-my-profile`, method: 'POST', data });
},
/** 个人中心 - 修改登录密码(xk-api POST {prefix}-auth/update-password) */
updateMyPassword(data) {
return bizRequest({ url: `${prefix}-auth/update-password`, method: 'POST', data });
},
getInputStats() {
return bizRequest({ url: `${prefix}-auth/input-stats`, method: 'GET' });
},

View File

@@ -51,10 +51,65 @@ export function getClinicAdminMyInfo() {
return clinicRequest({ url: `${PREFIX}-auth/my-info`, method: 'GET' });
}
/** 个人中心 - 我的资料(xk-api GET clinic-admin-auth/my-profile,实时库数据) */
export function getClinicAdminMyProfile() {
return clinicRequest({ url: `${PREFIX}-auth/my-profile`, method: 'GET' });
}
/** 个人中心 - 修改资料(仅昵称/头像,xk-api POST clinic-admin-auth/update-my-profile) */
export function updateClinicAdminMyProfile(data) {
return clinicRequest({ url: `${PREFIX}-auth/update-my-profile`, method: 'POST', data });
}
/** 个人中心 - 修改登录密码(xk-api POST clinic-admin-auth/update-password) */
export function updateClinicAdminMyPassword(data) {
return clinicRequest({ url: `${PREFIX}-auth/update-password`, method: 'POST', data });
}
export function getClinicAdminStoreQr() {
return clinicRequest({ url: `${PREFIX}-auth/store-qr`, method: 'GET' });
}
/**
* 工作台看板统计(今日/本月订单、待发货、待审核提现、下单会员数)
* xk-api GET /clinic-admin-dashboard/stats
*/
export function getClinicAdminDashboardStats() {
return clinicRequest({ url: `${PREFIX}-dashboard/stats`, method: 'GET' });
}
/**
* 工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /clinic-admin-dashboard/entries
*/
export function getClinicAdminWorkbenchEntries() {
return clinicRequest({ url: `${PREFIX}-dashboard/entries`, method: 'GET' });
}
/**
* 功能页用户数据:我的常用 + 最近使用
* xk-api GET /clinic-admin-dashboard/entry-user-data
*/
export function getClinicAdminEntryUserData() {
return clinicRequest({ url: `${PREFIX}-dashboard/entry-user-data`, method: 'GET' });
}
/**
* 收藏/取消收藏功能入口(功能页长按触发),返回 { is_favorite }
* xk-api POST /clinic-admin-dashboard/toggle-favorite-entry
*/
export function toggleClinicAdminFavoriteEntry(code) {
return clinicRequest({ url: `${PREFIX}-dashboard/toggle-favorite-entry`, method: 'POST', data: { code } });
}
/**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /clinic-admin-dashboard/report-entry-usage
*/
export function reportClinicAdminEntryUsage(code) {
return clinicRequest({ url: `${PREFIX}-dashboard/report-entry-usage`, method: 'POST', data: { code } });
}
/**
* 本门店 VIP 开通记录列表
* xk-api GET /clinic-admin-vip/list

View File

@@ -22,6 +22,21 @@ export function getClinicSalespersonMyInfo() {
return clinicSalespersonRequest({ url: `${AUTH_PREFIX}/my-info`, method: 'GET' });
}
/** 个人中心 - 我的资料(xk-api GET clinic-salesperson-auth/my-profile,实时库数据) */
export function getClinicSalespersonMyProfile() {
return clinicSalespersonRequest({ url: `${AUTH_PREFIX}/my-profile`, method: 'GET' });
}
/** 个人中心 - 修改资料(仅昵称/头像,xk-api POST clinic-salesperson-auth/update-my-profile) */
export function updateClinicSalespersonMyProfile(data) {
return clinicSalespersonRequest({ url: `${AUTH_PREFIX}/update-my-profile`, method: 'POST', data });
}
/** 个人中心 - 修改登录密码(xk-api POST clinic-salesperson-auth/update-password) */
export function updateClinicSalespersonMyPassword(data) {
return clinicSalespersonRequest({ url: `${AUTH_PREFIX}/update-password`, method: 'POST', data });
}
export function getClinicSalespersonCommissionList(params) {
return clinicSalespersonRequest({ url: `${PREFIX}/commission-list`, method: 'GET', data: params });
}
@@ -74,3 +89,35 @@ export function getClinicSalespersonTransferPrescriptionDetail(id) {
export function getClinicSalespersonInvitedList(params) {
return clinicSalespersonRequest({ url: `${PREFIX}/invited-salesperson-list`, method: 'GET', data: params });
}
/**
* 推广员工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /clinic-salesperson-dashboard/entries
*/
export function getClinicSalespersonWorkbenchEntries() {
return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/entries', method: 'GET' });
}
/**
* 功能页用户数据:我的常用 + 最近使用
* xk-api GET /clinic-salesperson-dashboard/entry-user-data
*/
export function getClinicSalespersonEntryUserData() {
return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/entry-user-data', method: 'GET' });
}
/**
* 收藏/取消收藏功能入口(功能页长按触发),返回 { is_favorite }
* xk-api POST /clinic-salesperson-dashboard/toggle-favorite-entry
*/
export function toggleClinicSalespersonFavoriteEntry(code) {
return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/toggle-favorite-entry', method: 'POST', data: { code } });
}
/**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /clinic-salesperson-dashboard/report-entry-usage
*/
export function reportClinicSalespersonEntryUsage(code) {
return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/report-entry-usage', method: 'POST', data: { code } });
}

54
api/pharmacistExamine.js Normal file
View File

@@ -0,0 +1,54 @@
/**
* 药师审方相关接口(xk-api,取值用 code / result / message,配合 unwrapXkApi)
*
* 老 Yii service/v1/examine/* 已迁移到 xk-api 的 /pharmacist-examine/*,
* 鉴权复用医生端登录 token(ServiceUser),后端中间件强校验药师角色(role=2)
*/
import {
req
} from '@/common/js/index.js';
// xk-api GET /pharmacist-examine/list 审方列表(status: 0待审核/1已通过/2未通过;分页 page/pageSize)
export function getExamineList(data) {
return req.request({
url: '/newApi/pharmacist-examine/list',
method: 'GET',
data
})
}
// xk-api GET /pharmacist-examine/detail 审方详情(按处方号)
export function getExamineDetail(data) {
return req.request({
url: '/newApi/pharmacist-examine/detail',
method: 'GET',
data
})
}
// xk-api POST /pharmacist-examine/verify 审方通过/驳回(status=1 通过;status=2 驳回需 reject_reason)
export function verifyExamine(data) {
return req.request({
url: '/newApi/pharmacist-examine/verify',
method: 'POST',
data
})
}
// xk-api GET /pharmacist-examine/source 处方溯源(按处方号)
export function getExamineSource(data) {
return req.request({
url: '/newApi/pharmacist-examine/source',
method: 'GET',
data
})
}
// xk-api GET /pharmacist-examine/use-way 中药用法(煎法)字典
export function getExamineUseWay(data) {
return req.request({
url: '/newApi/pharmacist-examine/use-way',
method: 'GET',
data
})
}

View File

@@ -1,5 +1,63 @@
import { req } from '@/common/js/index.js';
import { platformAdminApi } from '@/api/businessApi.js';
import { unwrapClinicRes } from '@/api/clinicAdmin.js';
export { platformAdminApi };
export const getPlatformAdminMyInfo = () => platformAdminApi.getMyInfo();
/**
* 平台工作台专属请求助手(带 _platformAdmin 头 + 统一 { res, data, ok } 返回)
* 单独封装(不进 createBusinessApi):dashboard 系列接口仅平台端有,业务员端无此路由
*/
function platformDashboardRequest(options) {
return req.request({
...options,
header: { ...(options.header || {}), _platformAdmin: '1' },
}).then((res) => {
const data = unwrapClinicRes(res);
const ok = !!(res && Number(res.code) === 0);
return { res, data, ok };
});
}
/**
* 平台工作台看板统计(待审核门店/医生/提现数 + 今日订单概览)
* xk-api GET /platform-admin-dashboard/stats
*/
export function getPlatformAdminDashboardStats() {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/stats', method: 'GET' });
}
/**
* 平台工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /platform-admin-dashboard/entries
*/
export function getPlatformAdminWorkbenchEntries() {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/entries', method: 'GET' });
}
/**
* 功能页用户数据:我的常用 + 最近使用
* xk-api GET /platform-admin-dashboard/entry-user-data
*/
export function getPlatformAdminEntryUserData() {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/entry-user-data', method: 'GET' });
}
/**
* 收藏/取消收藏功能入口(功能页长按触发),返回 { is_favorite }
* xk-api POST /platform-admin-dashboard/toggle-favorite-entry
*/
export function togglePlatformAdminFavoriteEntry(code) {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/toggle-favorite-entry', method: 'POST', data: { code } });
}
/**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /platform-admin-dashboard/report-entry-usage
*/
export function reportPlatformAdminEntryUsage(code) {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/report-entry-usage', method: 'POST', data: { code } });
}
export default platformAdminApi;

View File

@@ -1,5 +1,55 @@
import { req } from '@/common/js/index.js';
import { salespersonApi } from '@/api/businessApi.js';
import { unwrapClinicRes } from '@/api/clinicAdmin.js';
export const getSalespersonMyInfo = salespersonApi.getMyInfo.bind(salespersonApi);
export const getSalespersonInputStats = salespersonApi.getInputStats.bind(salespersonApi);
/**
* 业务员工作台专属请求助手(带 _salesperson 头 + 统一 { res, data, ok } 返回)
* 单独封装(不进 createBusinessApi):dashboard 系列接口仅业务员端有此路由
*/
function salespersonDashboardRequest(options) {
return req.request({
...options,
header: { ...(options.header || {}), _salesperson: '1' },
}).then((res) => {
const data = unwrapClinicRes(res);
const ok = !!(res && Number(res.code) === 0);
return { res, data, ok };
});
}
/**
* 业务员工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /salesperson-dashboard/entries
*/
export function getSalespersonWorkbenchEntries() {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/entries', method: 'GET' });
}
/**
* 功能页用户数据:我的常用 + 最近使用
* xk-api GET /salesperson-dashboard/entry-user-data
*/
export function getSalespersonEntryUserData() {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/entry-user-data', method: 'GET' });
}
/**
* 收藏/取消收藏功能入口(功能页长按触发),返回 { is_favorite }
* xk-api POST /salesperson-dashboard/toggle-favorite-entry
*/
export function toggleSalespersonFavoriteEntry(code) {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/toggle-favorite-entry', method: 'POST', data: { code } });
}
/**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /salesperson-dashboard/report-entry-usage
*/
export function reportSalespersonEntryUsage(code) {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/report-entry-usage', method: 'POST', data: { code } });
}
export default salespersonApi;

View File

@@ -3,6 +3,7 @@ import { req } from '@/common/js/index.js';
const SALESPERSON_PREFIX = '/newApi/salesperson-upload/';
const PLATFORM_PREFIX = '/newApi/platform-admin-upload/';
const CLINIC_ADMIN_PREFIX = '/newApi/clinic-admin-upload/';
const CLINIC_SALESPERSON_PREFIX = '/newApi/clinic-salesperson-upload/';
function uploadChatFileByPrefix(prefix, headerKey, params) {
return req.request({
@@ -102,6 +103,52 @@ export function uploadSalespersonManageFileApi(params) {
return uploadClinicAdminChatFileApi(params);
}
/**
* 个人中心头像上传(xk-api POST {end}-upload/image,UploadController::image)
* 按当前 loginMode 选择四端各自的上传路由与请求头标识;
* uni.uploadFile 返回的 body 是字符串,这里手动 JSON.parse 后按 jok 形态(code/result.url)解包
* @param {string} filePath 本地临时文件路径
* @returns {Promise<{ok: boolean, url: string|null, message?: string}>}
*/
export function uploadProfileAvatarApi(filePath) {
const mode = uni.getStorageSync('loginMode');
let prefix = CLINIC_ADMIN_PREFIX;
let headerKey = '_clinicAdmin';
if (mode === 'platform_admin') {
prefix = PLATFORM_PREFIX;
headerKey = '_platformAdmin';
} else if (mode === 'salesperson') {
prefix = SALESPERSON_PREFIX;
headerKey = '_salesperson';
} else if (mode === 'clinic_salesperson') {
prefix = CLINIC_SALESPERSON_PREFIX;
headerKey = '_clinicSalesperson';
}
return req.request({
url: `${prefix}image`,
type: 'upload',
method: 'POST',
filePath,
name: 'file',
formData: {},
header: { [headerKey]: '1' },
}).then((res) => {
let body = res;
if (typeof body === 'string') {
try {
body = JSON.parse(body);
} catch (e) {
body = null;
}
}
// 该接口是 xk-api:成功 code=0,业务数据在 result
if (body && Number(body.code) === 0 && body.result && body.result.url) {
return { ok: true, url: body.result.url };
}
return { ok: false, url: null, message: (body && body.message) || '上传失败' };
});
}
function unwrapUploadResult(res) {
if (res == null) return null;
if (res.result !== undefined && res.result !== null) return res.result;

View File

@@ -0,0 +1,196 @@
<template>
<view class="admin-my-panel">
<!-- 资料区:有图片头像显示图片,否则文字首字圆(四端统一形态) -->
<view class="profile-panel" hover-class="profile-hover" :hover-stay-time="100" @tap="goProfile">
<image v-if="avatar" class="avatar-img" :src="avatar" mode="aspectFill" />
<view v-else class="avatar-circle"><text>{{ avatarText }}</text></view>
<view class="user-info">
<text class="user-name">{{ displayName }}</text>
<view class="role-badge"><text class="role-text">{{ roleText }}</text></view>
<text v-if="storeName && storeName !== '-'" class="meta-line">{{ storeName }}</text>
<text v-if="phone" class="meta-line">{{ phone }}</text>
</view>
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
<!-- 诊所管理员端在这里插 VIP 会员卡 -->
<slot name="after-profile"></slot>
<!-- 白色菜单卡:个人中心入口(第一项)+ 绑定微信 + 切换账号 + 工作台卡片样式 -->
<view class="setting-panel">
<view class="entry-row" hover-class="entry-row-hover" :hover-stay-time="100" @tap="goProfile">
<text class="entry-label">个人中心</text>
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
<card-pref-entry />
</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>
</template>
<script>
/**
* admin-my-panel 四个管理端「我的」页共享面板
* 为什么做成组件:诊所管理员/平台管理员/业务员/诊所推广员四端「我的」页高度同构
* (资料区 + 菜单卡 + 退出按钮),抽组件统一形态与逻辑,避免四份复制粘贴;
* 各端差异(VIP 卡、门店名、手机号)通过 props / slot 传入
*/
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue';
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue';
import CardPrefEntry from '@/components/workbench-card-pref/card-pref-entry.vue';
import { clearLoginStorage } from '@/utils/loginSession.js';
export default {
name: 'AdminMyPanel',
components: { AccountSwitchEntry, WxBindEntry, CardPrefEntry },
props: {
/** 展示姓名(各端从缓存/接口取好后传入) */
displayName: { type: String, default: '' },
/** 角色徽标文案(诊所管理员/平台管理员/业务员/推广员) */
roleText: { type: String, default: '' },
/** 图片头像地址,为空时降级为文字首字圆 */
avatar: { type: String, default: '' },
/** 副行:所属门店名(可选) */
storeName: { type: String, default: '' },
/** 副行:手机号(可选,推广员端展示) */
phone: { type: String, default: '' },
/** 无姓名时文字头像的兜底字 */
fallbackChar: { type: String, default: '管' },
},
computed: {
/** 文字头像取姓名首字(模板禁止方法调用,统一在 computed 里算好) */
avatarText() {
return (this.displayName || this.fallbackChar).slice(0, 1);
},
},
methods: {
/** 供页面 onShow 调用:刷新微信绑定状态 */
refreshWxBind() {
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh();
},
/** 进入共享个人中心页(sub_business_shared 分包页面可被任意分包 navigateTo) */
goProfile() {
uni.navigateTo({ url: '/subPackages/sub_business_shared/profile/index' });
},
/** 退出登录:确认弹窗 → 清全部登录态 → 回登录页(四端一致) */
logout() {
uni.showModal({
title: '提示',
content: '确定退出登录?',
confirmColor: '#6ACDBB',
success: (res) => {
if (!res.confirm) return;
clearLoginStorage();
uni.reLaunch({ url: '/pages/login/index' });
},
});
},
},
};
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$theme-dark: #4db6a8;
.admin-my-panel {
min-height: 100vh;
padding-bottom: 60rpx;
background: #f5f7f8;
box-sizing: border-box;
}
/* 资料区整块可点进入个人中心 */
.profile-panel {
display: flex;
align-items: center;
background: #fff;
padding: 60rpx 40rpx;
margin-bottom: 24rpx;
}
.profile-hover {
background: #fafbfc;
}
.avatar-img {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
flex-shrink: 0;
background: #f2f3f5;
}
.avatar-circle {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.25);
color: #fff;
font-size: 52rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.user-info {
flex: 1;
min-width: 0;
margin-left: 32rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.user-name {
font-size: 40rpx;
font-weight: 700;
color: #222b2a;
margin-bottom: 12rpx;
}
.role-badge {
align-self: flex-start;
background: rgba(106, 205, 187, 0.1);
border: 1rpx solid rgba(106, 205, 187, 0.2);
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.role-text {
font-size: 22rpx;
color: $theme-dark;
font-weight: 600;
}
.meta-line {
font-size: 26rpx;
color: #86909c;
margin-top: 8rpx;
}
.setting-panel {
background: #fff;
padding: 8rpx 40rpx 16rpx;
margin-bottom: 24rpx;
}
/* 「个人中心」入口行:与 account-switch-entry / card-pref-entry 行规格一致 */
.entry-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx 0;
border-bottom: 1rpx solid #f2f3f5;
}
.entry-row-hover {
background: #f7f8fa;
}
.entry-label {
font-size: 30rpx;
color: #31353d;
}
.logout-wrap {
padding: 0 40rpx 32rpx;
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<!--
工作台卡片样式入口行(四个业务角色「我的」页共用,放 setting-panel 内)
由旧版 u-switch 开关改为导航行:显示当前样式文案 + chevron,点击进入图形化单选页
-->
<view class="switch-row" hover-class="row-hover" @tap="goStylePage">
<view class="pref-text">
<text class="switch-label">工作台卡片样式</text>
<text class="pref-desc">长卡片 / 宫格两种形态可选</text>
</view>
<view class="pref-value">
<text class="value-text">{{ styleLabel }}</text>
<u-icon name="arrow-right" size="24" color="#C0C4CC"></u-icon>
</view>
</view>
</template>
<script>
/**
* card-pref-entry 工作台卡片样式入口
* 为什么做成组件:四个业务端「我的」页都需要同一入口,抽组件避免复制粘贴;
* 偏好存本地 storage(wb_card_style),选择页写入后 uni.$emit 广播,本组件监听同步文案,
* 各工作台首页 onShow 时读取生效
*/
import { getCardStyle } from '@/utils/workbenchCardPref.js';
export default {
name: 'CardPrefEntry',
data() {
return {
/** 当前样式:long 长卡片 / short 宫格 */
style: 'short',
};
},
computed: {
styleLabel() {
return this.style === 'long' ? '长卡片' : '宫格';
},
},
mounted() {
this.style = getCardStyle();
// 选择页切换后广播同步(组件拿不到页面 onShow,用事件总线保证返回时文案已更新)
uni.$on('wbCardStyleChanged', this.onStyleChanged);
},
beforeDestroy() {
uni.$off('wbCardStyleChanged', this.onStyleChanged);
},
methods: {
onStyleChanged(style) {
this.style = style === 'long' ? 'long' : 'short';
},
/** 进入图形化单选页(共享分包页面,四端均可 navigateTo) */
goStylePage() {
uni.navigateTo({ url: '/subPackages/sub_business_shared/card-style/index' });
},
},
};
</script>
<style lang="scss" scoped>
.switch-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx 0;
border-bottom: 1rpx solid #f2f3f5;
}
.row-hover {
opacity: 0.7;
}
.pref-text {
display: flex;
flex-direction: column;
}
.switch-label {
font-size: 30rpx;
color: #31353d;
}
.pref-desc {
margin-top: 8rpx;
font-size: 22rpx;
color: #86909c;
}
.pref-value {
display: flex;
align-items: center;
}
.value-text {
font-size: 26rpx;
color: #86909c;
margin-right: 8rpx;
}
</style>

View File

@@ -0,0 +1,432 @@
<template>
<!--
功能中心页共享组件(诊所管理员/平台管理员/业务员/诊所推广员四端「功能」tab 复用)
结构:顶部搜索框 + u-tabs/swiper 联动(常用 | 全部 | 各分组),金刚区宫格展示入口
交互:点击入口跳转并静默上报使用;长按入口收藏/取消收藏到「我的常用」;搜索本地过滤
放主包 components/ 原因:四端页面分属不同分包,微信不允许跨分包引用组件(同 xk-service-card)
-->
<view class="wf-wrap" :style="{ height: wrapHeight }">
<!-- 搜索框:本地过滤当前角色全部入口,输入即出结果 -->
<view class="wf-search">
<u-search
v-model="keyword"
placeholder="搜索功能"
shape="round"
bg-color="#FFFFFF"
:show-action="false"
:clearabled="true"
></u-search>
</view>
<!-- 加载/失败/空配置三态:占满内容区居中展示 -->
<view v-if="loading" class="wf-center">
<text class="wf-center-text">加载中...</text>
</view>
<view v-else-if="failed" class="wf-center">
<text class="wf-center-text">功能列表加载失败</text>
<view class="wf-retry" hover-class="wf-retry-hover" @tap="reload">
<text>重新加载</text>
</view>
</view>
<view v-else-if="sections.length === 0" class="wf-center">
<text class="wf-center-text">暂无可用功能,请联系管理员配置</text>
</view>
<!-- 搜索态:覆盖 tabs/swiper,展示扁平匹配结果 -->
<scroll-view v-else-if="isSearching" scroll-y class="wf-search-scroll">
<view class="wf-page">
<view class="wf-panel" v-if="searchItems.length">
<view class="wf-grid">
<xk-service-card
v-for="item in searchItems" :key="item.key"
mode="grid"
:icon="item.icon" :label="item.label" :theme="item.theme"
@click="onItemTap(item)" @longpress="onItemLongpress(item)"
></xk-service-card>
</view>
</view>
<view class="wf-hint" v-else>
<text>没有找到「{{ keyword }}」相关功能</text>
</view>
</view>
</scroll-view>
<!-- 常规态:tabs + swiper 双向联动(点 tab / 左右滑都走 switchTab) -->
<block v-else>
<view class="wf-tabs">
<u-tabs
:list="tabs"
:is-scroll="tabs.length > 4"
:current="current"
:height="88"
active-color="#6ACDBB"
@change="switchTab"
></u-tabs>
</view>
<swiper class="wf-swiper" :current="current" @change="onSwiperChange">
<swiper-item v-for="(page, pi) in swiperPages" :key="pi">
<scroll-view scroll-y class="wf-scroll">
<!-- 常用 tab:我的常用 + 最近使用两个分区 -->
<view v-if="page.type === 'fav'" class="wf-page">
<view class="wf-group-title">我的常用</view>
<view class="wf-panel" v-if="favItems.length">
<view class="wf-grid">
<xk-service-card
v-for="item in favItems" :key="item.key"
mode="grid"
:icon="item.icon" :label="item.label" :theme="item.theme"
@click="onItemTap(item)" @longpress="onItemLongpress(item)"
></xk-service-card>
</view>
</view>
<view class="wf-hint" v-else>
<text>长按任意功能,可添加到我的常用</text>
</view>
<view class="wf-group-title">最近使用</view>
<view class="wf-panel" v-if="recentItems.length">
<view class="wf-grid">
<xk-service-card
v-for="item in recentItems" :key="item.key"
mode="grid"
:icon="item.icon" :label="item.label" :theme="item.theme"
@click="onItemTap(item)" @longpress="onItemLongpress(item)"
></xk-service-card>
</view>
</view>
<view class="wf-hint" v-else>
<text>暂无使用记录,点击功能后会出现在这里</text>
</view>
</view>
<!-- 全部 tab:所有分组一页看全(分组标题 + 各组金刚区) -->
<view v-else-if="page.type === 'all'" class="wf-page">
<view v-for="section in sections" :key="section.key">
<view class="wf-group-title">{{ section.title }}</view>
<view class="wf-panel">
<view class="wf-grid">
<xk-service-card
v-for="item in section.items" :key="item.key"
mode="grid"
:icon="item.icon" :label="item.label" :theme="item.theme"
@click="onItemTap(item)" @longpress="onItemLongpress(item)"
></xk-service-card>
</view>
</view>
</view>
</view>
<!-- 分组 tab:该组全部入口金刚区 -->
<view v-else class="wf-page">
<view class="wf-panel">
<view class="wf-grid">
<xk-service-card
v-for="item in page.items" :key="item.key"
mode="grid"
:icon="item.icon" :label="item.label" :theme="item.theme"
@click="onItemTap(item)" @longpress="onItemLongpress(item)"
></xk-service-card>
</view>
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
</block>
</view>
</template>
<script>
/**
* workbench-features 功能中心共享组件
*
* 为什么做成组件而不是共享页面:四端「功能」tab 页需要套各自的 page-layout(navbar/tabbar 配置不同),
* 页面壳由各端提供,数据与交互全部收敛到这里,避免四份复制。
*
* props.api 约定(由各端壳页注入对应角色的接口,全部返回 Promise<{ res, data, ok }>):
* - getEntries() GET 当前角色启用入口(扁平数组)
* - getUserData() GET { favorites: [], recents: [] }
* - toggleFavorite(code) POST 收藏/取消收藏,data = { is_favorite }
* - reportUsage(code) POST 使用上报(静默,失败不影响跳转)
*
* 事件:
* - @special(item):点击了无 path 的特殊入口(如推广员端 cs_transfer 传方弹窗),由壳页按 code 处理
*/
import { groupEntriesToSections, normalizeEntry } from '@/utils/workbenchEntries.js';
export default {
name: 'WorkbenchFeatures',
props: {
/** 角色接口集合:{ getEntries, getUserData, toggleFavorite, reportUsage } */
api: { type: Object, required: true },
},
data() {
return {
loading: true,
failed: false,
/** 分组入口(groupEntriesToSections 输出结构) */
sections: [],
/** 我的常用 / 最近使用(已归一化为渲染项) */
favItems: [],
recentItems: [],
keyword: '',
current: 0,
/** 内容区高度(px):layout 的 navbar 与 tabbar 之间,swiper 需要确定高度才能滚动 */
bodyHeight: 600,
};
},
computed: {
wrapHeight() {
return this.bodyHeight + 'px';
},
isSearching() {
return this.keyword.trim() !== '';
},
/** swiper 页序列:固定「常用」「全部」两页在前,其后每个分组一页 */
swiperPages() {
const pages = [
{ key: '__fav__', type: 'fav', title: '常用', items: [] },
{ key: '__all__', type: 'all', title: '全部', items: [] },
];
this.sections.forEach((s) => {
pages.push({ key: s.key, type: 'group', title: s.title, items: s.items });
});
return pages;
},
/** u-tabs 数据源(name 字段) */
tabs() {
return this.swiperPages.map((p) => ({ name: p.title }));
},
/** 全部入口扁平列表(搜索用) */
allItems() {
const list = [];
this.sections.forEach((s) => {
s.items.forEach((item) => list.push(item));
});
return list;
},
/** 搜索结果:名称/描述包含关键词(大小写不敏感) */
searchItems() {
const kw = this.keyword.trim().toLowerCase();
if (!kw) return [];
return this.allItems.filter((item) => {
const label = (item.label || '').toLowerCase();
const desc = (item.desc || '').toLowerCase();
return label.indexOf(kw) !== -1 || desc.indexOf(kw) !== -1;
});
},
/** 已收藏 code 集合(长按菜单文案判断用) */
favCodes() {
return this.favItems.map((item) => item.key);
},
},
mounted() {
this.calcHeight();
this.reload();
},
methods: {
/**
* 计算内容区高度:窗口高度 - layout 顶部(状态栏+navbar)- layout 底部 tabbar 预留
* 与各端 page-layout 的 bodyStyle 计算保持一致(paddingTop=状态栏+navbar px,paddingBottom=100rpx+安全区)
*/
calcHeight() {
const sys = uni.getSystemInfoSync();
const rpx = sys.windowWidth / 750;
const top = (this.$statusBarHeight || sys.statusBarHeight || 0) + (this.$navbarHeight || 44);
let safeBottom = 0;
if (sys.safeAreaInsets && sys.safeAreaInsets.bottom) {
safeBottom = sys.safeAreaInsets.bottom;
} else if (sys.safeArea && sys.screenHeight) {
safeBottom = sys.screenHeight - sys.safeArea.bottom;
}
this.bodyHeight = Math.max(400, sys.windowHeight - top - 100 * rpx - safeBottom);
},
/** 全量加载:入口列表(必须)+ 用户数据(失败静默,常用/历史留空) */
reload() {
this.loading = true;
this.failed = false;
this.api.getEntries().then(({ data, ok }) => {
if (ok && Array.isArray(data)) {
this.sections = groupEntriesToSections(data);
// 分组数变化时防止 current 越界(固定页为常用+全部两页,如后台调整了分组)
if (this.current >= this.sections.length + 2) this.current = 0;
} else {
this.failed = true;
}
this.loading = false;
}).catch(() => {
this.failed = true;
this.loading = false;
});
this.refreshUserData();
},
/** 刷新常用/最近使用(壳页 onShow 也会调用,保证跳转返回后历史及时更新) */
refreshUserData() {
if (!this.api.getUserData) return;
this.api.getUserData().then(({ data, ok }) => {
if (!ok || !data) return;
this.favItems = this.normalizeList(data.favorites);
this.recentItems = this.normalizeList(data.recents);
}).catch(() => {
// 用户数据是增强信息,失败静默(不打断功能页主流程)
});
},
/** 后端入口对象数组 → 渲染项数组(与首页同一映射,字段完全一致) */
normalizeList(list) {
if (!Array.isArray(list)) return [];
const items = [];
list.forEach((entry) => {
const item = normalizeEntry(entry);
if (item) items.push(item);
});
return items;
},
/** tabs 与 swiper 统一切换入口 */
switchTab(index) {
if (this.current === index) return;
this.current = index;
},
/** swiper 手势滑动回调:回写 current 让顶部 tab 高亮跟随 */
onSwiperChange(e) {
this.switchTab(e.detail.current);
},
/**
* 点击入口:静默上报使用 → 跳转
* 无 path 的特殊入口(如 cs_transfer 传方弹窗)抛给壳页按 code 处理
*/
onItemTap(item) {
if (this.api.reportUsage) {
// 上报是辅助行为:不等待、不处理失败,绝不阻塞跳转
this.api.reportUsage(item.key).catch(() => {});
}
if (!item.path) {
this.$emit('special', item);
return;
}
uni.navigateTo({
url: item.path,
fail: () => {
// 目标是当前页栈不允许 navigateTo 的页面(如各端首页)时降级 reLaunch
uni.reLaunch({ url: item.path });
},
});
},
/** 长按入口:弹收藏/取消收藏操作单 */
onItemLongpress(item) {
const isFav = this.favCodes.indexOf(item.key) !== -1;
uni.showActionSheet({
itemList: [isFav ? '从我的常用移除' : '添加到我的常用'],
success: (r) => {
if (r.tapIndex === 0) this.doToggleFavorite(item);
},
});
},
/** 收藏切换:成功后按后端返回状态提示并刷新常用列表 */
doToggleFavorite(item) {
if (!this.api.toggleFavorite) return;
this.api.toggleFavorite(item.key).then(({ res, data, ok }) => {
if (ok) {
const added = !!(data && data.is_favorite);
uni.showToast({ title: added ? '已添加到常用' : '已从常用移除', icon: 'none' });
this.refreshUserData();
} else {
uni.showToast({ title: (res && res.message) || '操作失败,请稍后重试', icon: 'none' });
}
}).catch(() => {
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
});
},
},
};
</script>
<style lang="scss" scoped>
/* 纵向 flex:搜索/tabs 固定高度,swiper 吃满剩余空间(swiper 必须有确定高度才能滚动) */
.wf-wrap {
display: flex;
flex-direction: column;
background: #f9fafb;
}
.wf-search {
flex-shrink: 0;
padding: 20rpx 24rpx 12rpx;
}
.wf-tabs {
flex-shrink: 0;
background: #FFFFFF;
border-bottom: 1rpx solid rgba(60, 60, 67, 0.06);
}
.wf-swiper {
flex: 1;
min-height: 0;
}
.wf-scroll {
height: 100%;
}
.wf-search-scroll {
flex: 1;
min-height: 0;
}
.wf-page {
padding: 20rpx 24rpx 40rpx;
}
/* 分区标题:对齐首页 panel-title 规格但更轻量 */
.wf-group-title {
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
padding: 8rpx 6rpx 16rpx;
}
/* 金刚区白卡容器:iOS 卡片规格与首页/入口卡一致 */
.wf-panel {
background: #FFFFFF;
border-radius: 32rpx;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 16rpx 12rpx;
margin-bottom: 28rpx;
}
/* 4 列金刚区:minmax(0,1fr) 防止长标签撑爆轨道 */
.wf-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
row-gap: 8rpx;
}
/* 空态提示条:浅灰圆角块,弱引导 */
.wf-hint {
display: flex;
align-items: center;
justify-content: center;
background: rgba(60, 60, 67, 0.04);
border-radius: 24rpx;
padding: 36rpx 24rpx;
margin-bottom: 28rpx;
text {
font-size: 24rpx;
color: #8E8E93;
}
}
/* 加载/失败/空配置居中态 */
.wf-center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.wf-center-text {
font-size: 26rpx;
color: #8E8E93;
}
.wf-retry {
margin-top: 24rpx;
padding: 14rpx 48rpx;
background: #6ACDBB;
border-radius: 40rpx;
transition: opacity 0.2s ease;
text {
font-size: 26rpx;
color: #FFFFFF;
}
}
.wf-retry-hover {
opacity: 0.8;
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<!--
工作台首页「个人功能区」共享组件(诊所管理员/平台管理员/业务员/诊所推广员四端复用)
结构固定三段:最近使用(有记录才显示)→ 我的常用(空态给引导)→ 更多功能整行卡(进功能页)
形态跟随卡片偏好:cardLong=true 长卡一行两个 / false 白卡容器内 4 列金刚区
放主包 components/ 原因:四端首页分属不同分包,微信不允许跨分包引用组件(同 xk-service-card)
-->
<view class="wue-wrap">
<!-- 最近使用:无记录整段隐藏(历史随点击自然累积,无需空态占位) -->
<view v-if="recents.length" class="wue-section">
<view class="wue-header">
<text class="wue-title">最近使用</text>
</view>
<view v-if="cardLong" class="wue-grid is-long">
<view v-for="item in recents" :key="item.key" class="wue-cell">
<xk-service-card
:icon="item.icon" :label="item.label" :desc="item.desc"
:theme="item.theme" :badge="item.badge || 0"
@click="onItemTap(item)"
></xk-service-card>
</view>
</view>
<view v-else class="wue-panel">
<view class="wue-grid">
<view v-for="item in recents" :key="item.key" class="wue-cell">
<xk-service-card
mode="grid"
:icon="item.icon" :label="item.label"
:theme="item.theme" :badge="item.badge || 0"
@click="onItemTap(item)"
></xk-service-card>
</view>
</view>
</view>
</view>
<!-- 我的常用:空态给引导条(点击也进功能页),避免新用户首页空洞无从下手 -->
<view class="wue-section">
<view class="wue-header">
<text class="wue-title">我的常用</text>
</view>
<view v-if="favorites.length && cardLong" class="wue-grid is-long">
<view v-for="item in favorites" :key="item.key" class="wue-cell">
<xk-service-card
:icon="item.icon" :label="item.label" :desc="item.desc"
:theme="item.theme" :badge="item.badge || 0"
@click="onItemTap(item)"
></xk-service-card>
</view>
</view>
<view v-else-if="favorites.length" class="wue-panel">
<view class="wue-grid">
<view v-for="item in favorites" :key="item.key" class="wue-cell">
<xk-service-card
mode="grid"
:icon="item.icon" :label="item.label"
:theme="item.theme" :badge="item.badge || 0"
@click="onItemTap(item)"
></xk-service-card>
</view>
</view>
</view>
<view v-else class="wue-hint" hover-class="wue-hint-hover" @tap="$emit('more')">
<u-icon name="star" size="30" color="#8E8E93"></u-icon>
<text class="wue-hint-text">暂无常用功能,去功能页长按任意入口即可添加</text>
<u-icon name="arrow-right" size="22" color="#C7C7CC"></u-icon>
</view>
</view>
<!-- 更多功能:整行 iOS 行卡固定入口,通往功能页(全部入口在功能 tab) -->
<view class="wue-section">
<xk-service-card
icon="grid-fill" label="更多功能" desc="查看全部功能入口"
:theme="moreTheme"
@click="$emit('more')"
></xk-service-card>
</view>
</view>
</template>
<script>
/**
* workbench-user-entries 首页个人功能区组件
*
* 为什么做成组件:四端首页从「铺全部功能入口」改为「只展示最近使用 + 我的常用 + 更多功能」,
* 三段结构与双形态布局完全一致,收敛到主包组件避免四份复制。
* 数据与行为职责划分:
* - 父页面负责拉取 entry-user-data、按 code 注入本端行为(角标/表单预填/传方弹窗)后传入
* - 本组件只管渲染与转发事件:@item-click(item) 点了某入口、@more 要去功能页
* 点击上报使用也由父页面在 @item-click 里做(各端上报接口不同)
*/
export default {
name: 'WorkbenchUserEntries',
props: {
/** 最近使用入口(已归一化 + 行为装饰) */
recents: { type: Array, default: () => [] },
/** 我的常用入口(已归一化 + 行为装饰) */
favorites: { type: Array, default: () => [] },
/** 卡片形态:true 长卡一行两个 / false 金刚区宫格 */
cardLong: { type: Boolean, default: false },
},
data() {
return {
/** 「更多功能」卡磁贴主题:品牌青绿,与工作台主色一致 */
moreTheme: { bg: 'linear-gradient(135deg, #ECFDF5, #D1FAE5)', color: '#6ACDBB' },
};
},
methods: {
/** 入口点击统一转发:跳转/上报/特殊行为都由父页面处理 */
onItemTap(item) {
this.$emit('item-click', item);
},
},
};
</script>
<style lang="scss" scoped>
.wue-section {
margin: 0 30rpx 30rpx;
}
.wue-header {
margin-bottom: 24rpx;
padding: 0 10rpx;
}
/* 分区标题:对齐四首页既有 panel-title 规格 */
.wue-title {
font-size: 34rpx;
font-weight: 700;
color: #1E293B;
}
/* 金刚区白卡容器:iOS 卡片规格(大圆角 + 发丝线 + 轻阴影) */
.wue-panel {
background: #FFFFFF;
border-radius: 32rpx;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 16rpx 12rpx;
}
/* 金刚区宫格:4 列图标磁贴(cardLong=false 缺省形态);minmax(0,1fr) 防长文案撑破轨道 */
.wue-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
row-gap: 8rpx;
}
/* 长卡片模式:一行两张 iOS 行卡(卡内标题/描述单行省略,窄卡不溢出) */
.wue-grid.is-long {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
}
/* 卡片外层 cell:min-width:0 让内部文本省略生效 */
.wue-cell {
display: flex;
flex-direction: column;
min-width: 0;
}
/* 常用空态引导条:白卡内浅灰引导,点击直达功能页 */
.wue-hint {
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 32rpx;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 32rpx 28rpx;
}
.wue-hint-hover {
opacity: 0.75;
}
.wue-hint-text {
flex: 1;
min-width: 0;
margin: 0 16rpx;
font-size: 24rpx;
color: #8E8E93;
}
</style>

View File

@@ -0,0 +1,301 @@
<template>
<!--
通用业务列表卡片(对齐医生工作台 flat-card 设计规范)
结构:头像区(可选) + 标题行(标题/meta/标签/右侧时间) + 内容行(插槽或 rows) + 底部行(状态 + 主/次操作按钮)
用法示例:
<xk-list-card
:title="item.name" :meta="'男 · 32岁'" :time="item.created_at"
:tags="[{ text: '线上', type: 'primary' }]"
:rows="['手机号:138xxxx']"
:status="{ text: '待审核', color: '#FF9900' }"
:actions="[{ key: 'edit', text: '编辑', type: 'ghost' }, { key: 'detail', text: '查看', type: 'primary' }]"
@action="onAction" @click="onCardTap"
/>
-->
<view :class="cardClass" hover-class="xk-card-hover" @tap="onCardTap">
<!-- 头像区:传 avatar 图片,或 avatar-text 首字圆底,二选一;支持未读角标 -->
<view class="xk-avatar-wrap" v-if="showAvatarArea">
<slot name="avatar">
<u-image v-if="avatar" width="80rpx" height="80rpx" shape="circle" :src="avatar">
<u-loading slot="loading"></u-loading>
</u-image>
<view v-else class="xk-avatar-text">{{ avatarChar }}</view>
</slot>
<u-badge v-if="badgeCount > 0" :count="badgeCount" :offset="[-4, -4]" bgColor="#F44336" :overflow-count="99"></u-badge>
</view>
<view class="xk-card-body flex-col flex-1">
<!-- 第一行:标题 + meta + 标签,右侧时间 -->
<view class="flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-ali-center flex-1 text-hide">
<text class="xk-name text-hide">{{ title }}</text>
<text class="xk-meta" v-if="meta">{{ meta }}</text>
<!-- :key 直接用下标:非 h5 平台不支持 'tag-'+ti 这类拼接表达式 -->
<u-tag v-for="(tag, ti) in tags" :key="ti" class="xk-tag"
:text="tag.text" :type="tag.type || 'info'" size="mini"></u-tag>
<slot name="title-extra"></slot>
</view>
<text class="xk-time" v-if="time">{{ time }}</text>
</view>
<!-- 内容区:优先用 body 插槽,否则渲染 rows 文本行 -->
<slot name="body">
<text v-for="(row, ri) in rows" :key="ri" class="xk-sub text-hide m-t-8">{{ row }}</text>
</slot>
<!-- 底部行:左状态 + 右操作按钮(有其一才渲染,避免空白高度) -->
<view class="flex-row flex-jus-sp flex-ali-center m-t-12" v-if="showFooter">
<view class="xk-status flex-row flex-ali-center text-hide">
<slot name="footer-left">
<text v-if="status && status.text" class="xk-status-text" :style="{ color: status.color || '#999' }">{{ status.text }}</text>
</slot>
</view>
<view class="xk-actions flex-row flex-ali-center">
<slot name="footer-right">
<view v-for="(act, ai) in actions" :key="ai"
:class="['xk-action-btn', act.type === 'primary' ? 'btn-primary' : (act.type === 'danger' ? 'btn-danger' : 'btn-ghost')]"
@tap.stop="onActionTap(act)">{{ act.text }}</view>
</slot>
</view>
</view>
</view>
<!-- 右侧箭头:无操作按钮的纯跳转卡可开启,暗示整卡可点 -->
<view class="xk-arrow" v-if="arrow">
<u-icon name="arrow-right" color="#C0C4CC" size="28"></u-icon>
</view>
</view>
</template>
<script>
/**
* xk-list-card 通用业务列表卡片
* 为什么要做这个组件:医生端各角色的列表卡(患者/门店/推广员/获客/传方等)此前是十几处
* 各写各的白底矩形,信息层级和按钮风格不统一。统一抽成本组件后,所有列表卡自动对齐
* 医生工作台 flat-card 的设计规范(信息分层 + 状态色 + 主/次操作按钮 + 青绿主色)。
*/
export default {
name: 'xk-list-card',
props: {
/** 卡片形态:card=独立白卡(灰底页面用,默认);flat=平铺分割线(白底容器内用) */
mode: { type: String, default: 'card' },
/** 头像图片地址;为空且 avatarText 也为空时不渲染头像区 */
avatar: { type: String, default: '' },
/** 头像兜底文字(一般取名字首字),渲染为青绿圆底白字 */
avatarText: { type: String, default: '' },
/** 头像上的未读/待办角标数 */
badgeCount: { type: Number, default: 0 },
/** 标题(姓名/门店名等),必填 */
title: { type: String, default: '' },
/** 标题右侧灰字 meta(性别年龄/编号等) */
meta: { type: String, default: '' },
/** 第一行右侧时间文案 */
time: { type: String, default: '' },
/** 标签数组 [{ text, type }],type 对应 u-tag 的 type(primary/success/warning/error/info) */
tags: { type: Array, default: () => [] },
/** 内容文本行数组,每项一行灰字;复杂内容用 body 插槽 */
rows: { type: Array, default: () => [] },
/** 底部左侧状态 { text, color } */
status: { type: Object, default: null },
/** 底部右侧操作按钮 [{ key, text, type: 'primary'|'ghost'|'danger' }],点击 emit('action', act) */
actions: { type: Array, default: () => [] },
/** 是否显示右侧箭头(纯跳转卡用,与 actions 二选一) */
arrow: { type: Boolean, default: false },
/** 整卡是否可点击(控制 hover 反馈;点击 emit('click')) */
clickable: { type: Boolean, default: true }
},
computed: {
/** 是否渲染头像区:有图或有兜底文字才占位,避免无头像场景左侧空白 */
showAvatarArea() {
return !!(this.avatar || this.avatarText || this.$slots.avatar)
},
/** 头像兜底字符:取 avatarText 首字 */
avatarChar() {
return this.avatarText ? this.avatarText.slice(0, 1) : ''
},
/** 底部行是否渲染:有状态、操作或对应插槽才渲染 */
showFooter() {
const hasStatus = !!(this.status && this.status.text)
return hasStatus || this.actions.length > 0 || !!this.$slots['footer-left'] || !!this.$slots['footer-right']
},
/** 卡片根 class:小程序模板禁止 :class 里调函数,这里用 computed 计算 */
cardClass() {
return ['xk-list-card', 'flex-row', 'flex-ali-start', this.mode === 'flat' ? 'is-flat' : 'is-card', this.clickable ? 'is-clickable' : '']
}
},
methods: {
/** 整卡点击:透传给父级(一般跳详情) */
onCardTap() {
if (this.clickable) this.$emit('click')
},
/** 操作按钮点击:stop 阻止冒泡避免同时触发整卡点击 */
onActionTap(act) {
this.$emit('action', act)
}
}
}
</script>
<style lang="scss" scoped>
/* ===== 局部工具类 =====
微信自定义组件默认隔离 app.wxss(styleIsolation),全局原子类进不来,
所以组件内自带一份用到的布局工具类,保证任何页面引用都不会跑版 */
.flex-row {
display: flex;
flex-direction: row;
}
.flex-col {
display: flex;
flex-direction: column;
}
.flex-1 {
flex: 1;
}
.flex-ali-start {
align-items: flex-start;
}
.flex-ali-center {
align-items: center;
}
.flex-jus-sp {
justify-content: space-between;
}
.text-hide {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.m-t-8 {
margin-top: 8rpx;
}
.m-t-12 {
margin-top: 12rpx;
}
/* ===== 卡片容器:两种形态 ===== */
.xk-list-card {
position: relative;
box-sizing: border-box;
}
/* 独立白卡:灰底页面上的悬浮卡,与工作台看板同规格圆角阴影
横向留白由页面容器控制(各页 padding 24rpx),组件只负责纵向间距 */
.is-card {
background: #fff;
border-radius: 24rpx;
padding: 28rpx 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
}
/* 平铺形态:白底容器内的分割线列表(对齐工作台 flat-card) */
.is-flat {
padding: 24rpx 20rpx;
border-bottom: 1rpx solid #f5f6f8;
&:last-child {
border-bottom: none;
}
}
/* 点击按压反馈:轻微下沉 + 底色变化 */
.xk-card-hover {
&.is-card {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
&.is-flat {
background: #fafbfc;
}
}
/* ===== 头像区 ===== */
.xk-avatar-wrap {
width: 80rpx;
position: relative;
flex-shrink: 0;
margin-right: 20rpx;
}
/* 无头像图时的首字兜底:青绿浅底 + 主色文字 */
.xk-avatar-text {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e8f7f3;
color: #4db6a8;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
/* ===== 信息分层(与工作台 .wb-* 同规格) ===== */
.xk-card-body {
width: auto !important;
min-width: 0;
}
.xk-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-right: 16rpx;
}
.xk-meta {
flex-shrink: 0;
font-size: 24rpx;
color: #888;
}
.xk-tag {
margin-left: 12rpx;
flex-shrink: 0;
transform: scale(0.9);
transform-origin: left center;
}
.xk-time {
flex-shrink: 0;
font-size: 22rpx;
color: #999;
margin-left: 16rpx;
}
.xk-sub {
display: block;
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
.xk-status-text {
font-size: 24rpx;
}
/* ===== 操作按钮:主实心青绿 / 次浅底描边 / 危险红 ===== */
.xk-actions {
flex-shrink: 0;
gap: 16rpx;
}
.xk-action-btn {
padding: 10rpx 28rpx;
font-size: 24rpx;
border-radius: 30rpx;
font-weight: 500;
line-height: 1.4;
text-align: center;
}
.btn-primary {
color: #fff;
background: #6ACDBB;
}
.btn-ghost {
color: #6ACDBB;
background: #f0f9f7;
border: 1rpx solid #c2ece2;
}
.btn-danger {
color: #fa3534;
background: #fef0f0;
border: 1rpx solid #fbc4c4;
}
/* 右侧箭头 */
.xk-arrow {
flex-shrink: 0;
margin-left: 12rpx;
align-self: center;
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<!--
工作台功能入口卡(iOS 风格,诊所管理员/平台管理员/业务员/诊所推广员四端复用)
两种形态由 mode 控制:
- list(缺省):iOS 行卡(磁贴 + 标题/描述 + chevron),长卡片模式与首页整行列表用
- grid:金刚区竖排项(磁贴 + 标签,透明底),首页宫格模式与功能页用,外层白卡容器由页面提供
用法:
<xk-service-card :icon="item.icon" :label="item.label" :desc="item.desc"
:theme="item.theme" :badge="item.badge" mode="grid" @click="go(item)" @longpress="onLongpress(item)" />
-->
<view
v-if="mode !== 'grid'"
class="xk-service-card" hover-class="xk-service-card-hover" :hover-stay-time="60"
@tap="$emit('click')" @longpress="$emit('longpress')"
>
<!-- 磁贴底色用内联对象字面量绑定:computed 返回对象在 mp-weixin 端曾出现 style 未生效,字面量是旧版验证过的稳妥写法 -->
<view class="icon-tile" :style="{ background: (theme && theme.color) || '#8E8E93' }">
<u-icon :name="icon" size="40" color="#FFFFFF"></u-icon>
<!-- iOS 风格顶部高光层:叠白色渐变模拟系统图标的微立体质感 -->
<view class="icon-tile-gloss"></view>
<!-- 待办角标:iOS 系统红(#FF3B30),挂在磁贴右上角 -->
<u-badge v-if="badge > 0" :count="badge" :offset="[-10, -10]" bgColor="#FF3B30" :overflow-count="99"></u-badge>
</view>
<view class="info">
<text class="name">{{ label }}</text>
<text class="desc">{{ desc || '快捷进入' }}</text>
</view>
<!-- iOS disclosure chevron:弱化的可点击暗示 -->
<view class="arrow">
<u-icon name="arrow-right" size="22" color="#C7C7CC"></u-icon>
</view>
</view>
<!-- grid 金刚区形态:竖排图标 + 标签,无描述无 chevron,信息密度高 -->
<view
v-else
class="xk-service-cell" hover-class="xk-service-cell-hover" :hover-stay-time="60"
@tap="$emit('click')" @longpress="$emit('longpress')"
>
<view class="icon-tile is-grid" :style="{ background: (theme && theme.color) || '#8E8E93' }">
<u-icon :name="icon" size="44" color="#FFFFFF"></u-icon>
<view class="icon-tile-gloss"></view>
<u-badge v-if="badge > 0" :count="badge" :offset="[-8, -8]" bgColor="#FF3B30" :overflow-count="99"></u-badge>
</view>
<text class="cell-label">{{ label }}</text>
</view>
</template>
<script>
/**
* xk-service-card 工作台功能入口卡(iOS 风格版)
*
* 为什么这样封装:
* 1. 四个管理员端首页分属四个不同分包,微信不允许跨分包引用组件,
* 共享组件源码必须放主包 components/(easycom 自动注册);
* manifest 已开启 optimization.subPackages + lazyCodeLoading,
* 组件代码按需注入,不会造成主包冗余加载
* 2. props 与旧版兼容(icon/label/desc/theme/badge),新增 mode 切换行卡/金刚区两种形态,
* 后台 xk_wx_workbench_entry 下发的 theme JSON({bg,color})无需变更
*
* iOS 风格要点:
* - 图标磁贴取 theme.color 作纯色底 + 白色图标(对齐 iOS 设置页图标规范),
* 旧版的浅色渐变底 theme.bg 保留兼容但不再使用
* - 行卡:大圆角 + 发丝线描边 + 极轻阴影;按压回弹缩放(iOS spring 手感)
* - 金刚区:透明底竖排,按压整体缩放;外层白色容器卡由页面统一提供
*
* 事件:@click 点击进入;@longpress 长按(功能页用于收藏,不绑定则无行为)
*/
export default {
name: 'xk-service-card',
props: {
/** uView 图标名 */
icon: { type: String, default: 'grid-fill' },
/** 入口标题 */
label: { type: String, default: '' },
/** 入口描述(空则显示「快捷进入」,grid 形态不展示) */
desc: { type: String, default: '' },
/** 图标区主题 { bg: 渐变底(兼容保留), color: 磁贴底色 } */
theme: {
type: Object,
default: () => ({ bg: 'linear-gradient(135deg, #F3F4F6, #E5E7EB)', color: '#8E8E93' })
},
/** 待办角标数,>0 时显示 */
badge: { type: Number, default: 0 },
/** 形态:list 行卡(缺省)/ grid 金刚区竖排项 */
mode: { type: String, default: 'list' }
}
}
</script>
<style lang="scss" scoped>
/* iOS 卡片容器:大圆角 + 发丝线 + 极轻弥散阴影,按压回弹缩放 */
.xk-service-card {
position: relative;
background: #FFFFFF;
border-radius: 32rpx;
padding: 28rpx 24rpx;
display: flex;
flex-direction: row;
align-items: center;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
transition: transform 0.25s cubic-bezier(0.34, 1.3, 0.64, 1), box-shadow 0.25s ease, background 0.25s ease;
}
.xk-service-card-hover {
transform: scale(0.965);
background: #F7F7FA;
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.05);
}
/* iOS 图标磁贴:主题纯色底 + 白色图标,圆角率对齐系统图标(约 25%),兼作角标定位容器 */
/* CSS 里给兜底灰底:即使内联主题色因异常数据缺失,白色图标也不会「隐身」在白卡上 */
.icon-tile {
position: relative;
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: #8E8E93;
display: flex;
align-items: center;
justify-content: center;
margin-right: 22rpx;
flex-shrink: 0;
}
/* 磁贴高光层:上白下微暗的渐变叠加,模拟 iOS 图标的立体光泽(不挡角标,角标在其后渲染层级更高) */
.icon-tile-gloss {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 20rpx;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.28) 0%, rgba(255, 255, 255, 0.05) 45%, rgba(0, 0, 0, 0.05) 100%);
pointer-events: none;
}
.info {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
/* 标题/描述:对齐 iOS label / secondaryLabel 配色,均单行省略防止长文案撑破卡片 */
.name {
font-size: 29rpx;
font-weight: 600;
color: #1C1C1E;
line-height: 1.25;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.desc {
font-size: 22rpx;
color: #8E8E93;
margin-top: 6rpx;
line-height: 1.2;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 右侧 chevron:iOS disclosure 灰 */
.arrow {
flex-shrink: 0;
margin-left: 8rpx;
}
/* ---- grid 金刚区形态 ---- */
/* 竖排项:透明底(外层白卡由页面提供),按压整体缩放 + 微降透明度 */
.xk-service-cell {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 4rpx;
border-radius: 20rpx;
transition: transform 0.2s cubic-bezier(0.34, 1.3, 0.64, 1), opacity 0.2s ease;
}
.xk-service-cell-hover {
transform: scale(0.92);
opacity: 0.75;
}
/* 金刚区磁贴:比行卡略大(88rpx),圆角率同步放大,去掉行卡的右边距 */
.icon-tile.is-grid {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
margin-right: 0;
}
.icon-tile.is-grid .icon-tile-gloss {
border-radius: 24rpx;
}
/* 金刚区标签:单行省略,宽度受 cell 约束 */
.cell-label {
margin-top: 12rpx;
max-width: 100%;
font-size: 24rpx;
font-weight: 500;
color: #1C1C1E;
line-height: 1.2;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@@ -12,13 +12,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/login/register",
"style": {
"navigationBarTitleText": "注册",
"navigationStyle": "custom"
}
},
{
"path": "pages/workbench/index",
"style": {
@@ -86,31 +79,12 @@
"enablePullDownRefresh": true
}
},
{
"path": "workbench_identity",
"style": {
"navigationBarTitleText": "选择身份",
"navigationStyle": "custom"
}
},
{
"path": "workbench_reception",
"style": {
"navigationBarTitleText": "加载中..."
}
},
{
"path": "workbench_succeed",
"style": {
"navigationBarTitleText": "加载中..."
}
},
{
"path": "workbench_announcement",
"style": {
"navigationBarTitleText": "停诊公告"
}
},
{
"path": "workbench_upInfo/select",
"style": {
@@ -119,44 +93,12 @@
"backgroundColor": "#000"
}
},
{
"path": "workbench_evaluation",
"style": {
"navigationBarTitleText": "我的评价",
"enablePullDownRefresh": true
}
},
{
"path": "workbench_reason",
"style": {
"navigationBarTitleText": "选择退诊原因"
}
},
{
"path": "workbench_recipe/index",
"style": {
"navigationBarTitleText": "处方详情",
"navigationStyle": "custom"
}
},
{
"path": "workbench_recipe/add",
"style": {
"navigationBarTitleText": "添加商品"
}
},
{
"path": "workbench_recipe/addType2",
"style": {
"navigationBarTitleText": "添加商品"
}
},
{
"path": "workbench_recipe/addProductServicePackage",
"style": {
"navigationBarTitleText": "添加产品服务包"
}
},
{
"path": "workbench_recipe/common",
"style": {
@@ -182,24 +124,6 @@
}
}
},
{
"path": "workbench_basicInfo",
"style": {
"navigationBarTitleText": "基础健康信息"
}
},
{
"path": "workbench_inquiries",
"style": {
"navigationBarTitleText": "问诊记录"
}
},
{
"path": "workbench_used_language",
"style": {
"navigationBarTitleText": "常用语管理"
}
},
{
"path": "workbench_code",
"style": {
@@ -249,18 +173,6 @@
"navigationBarTitleText": "群发信息"
}
},
{
"path": "article_select",
"style": {
"navigationBarTitleText": "选择文章"
}
},
{
"path": "patient_details",
"style": {
"navigationBarTitleText": "患者详情"
}
},
{
"path": "to-store",
"style": {
@@ -342,13 +254,6 @@
"navigationStyle": "custom"
}
},
{
"path": "im",
"style": {
"navigationBarTitleText": "咨询"
}
},
{
"path": "pharmacist_setInfo",
"style": {
@@ -356,20 +261,6 @@
"navigationStyle": "custom"
}
},
{
"path": "change_password",
"style": {
"navigationBarTitleText": "修改登录密码"
}
},
{
"path": "pharmacist_prescription",
"style": {
"navigationBarTitleText": "查看处方",
"navigationStyle": "custom"
}
},
{
"path": "pharmacist_traceability",
"style": {
@@ -377,20 +268,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pharmacist_access",
"style": {
"navigationBarTitleText": "待接入用户"
}
},
{
"path": "pharmacist_triage",
"style": {
"navigationBarTitleText": "选择医生",
"enablePullDownRefresh": true
}
},
{
"path": "myrecord-detail",
"style": {
@@ -406,6 +283,10 @@
"path": "home/index",
"style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" }
},
{
"path": "features/index",
"style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" }
},
{
"path": "my/index",
"style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" }
@@ -459,6 +340,7 @@
"style": {
"navigationBarTitleText": "订单详情",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"usingComponents": {
"order-price-percent-adjust-popup": "/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup"
},
@@ -483,8 +365,9 @@
}, {
"root": "subPackages/sub_business_shared",
"pages": [
{ "path": "card-style/index", "style": { "navigationBarTitleText": "卡片样式", "navigationStyle": "custom" } },
{ "path": "order/index", "style": { "navigationBarTitleText": "商品订单", "navigationStyle": "custom", "disableScroll": true } },
{ "path": "order/detail", "style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom" } },
{ "path": "order/detail", "style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "order/trace/index", "style": { "navigationBarTitleText": "订单追溯", "navigationStyle": "custom" } },
{ "path": "reconciliation/index", "style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom" } },
{ "path": "withdrawal/index", "style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom" } },
@@ -499,12 +382,14 @@
{ "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" } }
{ "path": "user-patient/register-detail", "style": { "navigationBarTitleText": "挂号详情", "navigationStyle": "custom" } },
{ "path": "profile/index", "style": { "navigationBarTitleText": "个人中心", "navigationStyle": "custom" } }
]
}, {
"root": "subPackages/sub_platform_admin",
"pages": [
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } },
{ "path": "features/index", "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
{ "path": "withdrawal/audit/index", "style": { "navigationBarTitleText": "提现审核", "navigationStyle": "custom" } },
{ "path": "store/index", "style": { "navigationBarTitleText": "门店管理", "navigationStyle": "custom" } },
@@ -533,6 +418,7 @@
"root": "subPackages/sub_salesperson",
"pages": [
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } },
{ "path": "features/index", "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
{ "path": "store-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom" } },
{ "path": "store-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } },
@@ -558,14 +444,16 @@
}
}
},
{
"path": "features/index",
"style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" }
},
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
{ "path": "earnings/index", "style": { "navigationBarTitleText": "我的收益", "navigationStyle": "custom" } },
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom" } },
{ "path": "commission/index", "style": { "navigationBarTitleText": "服务费记录", "navigationStyle": "custom" } },
{ "path": "invitees/index", "style": { "navigationBarTitleText": "我邀请的推广员", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "invitees/detail", "style": { "navigationBarTitleText": "收益明细", "navigationStyle": "custom" } },
{ "path": "settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } },
{ "path": "transfer-prescription/form", "style": { "navigationBarTitleText": "传方", "navigationStyle": "custom" } },
{ "path": "transfer-prescription/list", "style": { "navigationBarTitleText": "传方记录", "navigationStyle": "custom" } }
]
}, {
@@ -575,11 +463,6 @@
"style": {
"navigationBarTitleText": "协议"
}
}, {
"path": "web-view",
"style": {
"navigationBarTitleText": "视频"
}
}]
}, {
"root": "subPackages/sub_online_reception",

View File

@@ -1,313 +0,0 @@
<template>
<view class="container safe-area-inset-bottom">
<view class="title">
欢迎注册
</view>
<view class="box">
<view class="info">
<view class="pwd">
<d-input :maxlength="11" height="96" v-model="form['mobile']"
:prefixIcon="require('../../static/image/act.png')" borderRadius="96rpx" prefixIconSize="40rpx"
placeholder="请输入手机号" @input="is_d=false" :placeholder-style="{fontSize:'32rpx'}"
:custom-style="{fontSize:'32rpx',paddingLeft:'60rpx'}" />
</view>
<view class="pwd">
<u-field v-model="smsCode" placeholder="请填写验证码" :placeholder-style="{fontSize:'32rpx'}">
<u-button size="mini" slot="right" @click="getCode">{{codeText}}</u-button>
</u-field>
<u-verification-code ref="uCode" @change="codeChange" class="code">
</u-verification-code>
</view>
<view class="flex-row" style="margin-top: 88rpx;">
<u-icon size="35" :color="form['check']?'#6ACDBB':'#e0fdff'" name="checkmark-circle-fill"
@click="form['check']=!form['check']"></u-icon>
<view class="agreement">
我已阅读并同意
<text
@click="$go('/subPackages/sub_agreement/agreement?type=service_user_agreement')">《用户服务协议》</text>
和
<text
@click="$go('/subPackages/sub_agreement/agreement?type=service_privacy_agreement')">《隐私权政策》</text>
</view>
</view>
<!-- :disabled="!form['mobile']||!form['pwd']" -->
<view class="btn">
<u-button :throttle-time="20" shape="circle" @click="register" :ripple="true" :loading="loading">
注册</u-button>
</view>
<view class="register" @click="$go('/pages/login/index')">
<text>手机号登录</text>
</view>
</view>
</view>
</view>
</template>
<script>
import {
login,
register,
agreement,
getUserInfo,
loginOrRe
} from '../../api/all';
import { isPhone, assertPhone } from '@/utils/phone.js';
export default {
data() {
return {
loading: false,
form: {
check: false
},
switchVal: false,
is_d: false,
statusBarHeight: this.$statusBarHeight,
navbarHeight: this.$navbarHeight,
mobile: '',
codeText: '',
smsCode: '',
code: '',
role: 1,
};
},
onLoad(e) {
// console.log(e, 'role');
this.role = e.role
},
mounted() {
uni.login({
provider: 'weixin',
success: (loginRes) => {
this.code = loginRes.code
// console.log(loginRes);
uni.setStorageSync('code', loginRes.code)
}
})
},
methods: {
//登录或者注册接口
register() {
// 与后端 checkPhone 统一号段,不用 uView 默认 mobile 规则
if (!isPhone(this.form['mobile'])) {
this.$toast('请输入正确的手机号')
return
}
if (!this.smsCode) {
this.$toast('请输入验证码')
return
}
if (!this.form['check']) {
this.$toast('请勾选协议')
return
}
const data = {
store_id: '11001',
role: this.role,
plate_type: 2,
mobile: this.form['mobile'],
status: 4,
smsCode: this.smsCode,
code: uni.getStorageSync('code')
}
loginOrRe(data).then(res => {
if (res.errcode != -1) {
uni.setStorageSync('token', res.data['token'])
uni.setStorageSync('logInfo', res.data['user'])
// 存储doctor_id或user_id,用于WebSocket绑定
if (res.data['user'] && res.data['user']['id']) {
uni.setStorageSync('doctor_id', res.data['user']['id']);
uni.setStorageSync('user_id', res.data['user']['id']);
}
// 触发WebSocket初始化(注册成功后)
uni.$emit('user-login');
setTimeout(() => {
uni.showToast({
title: '开始注册',
icon: 'success'
})
}, 1000)
//跳转到激活页面
setTimeout(() => {
this.$go('../../subPackages/sub_workbench/workbench_succeed')
}, 800)
} else {
this.$toast(res.msg)
//跳转到激活页面
// setTimeout(() => {
// this.$go('../../subPackages/sub_workbench/workbench_succeed')
// }, 900)
}
// console.log(res);
})
},
checkboxChange(val) {
console.log(val);
},
codeChange(text) {
this.codeText = text;
},
getCode() {
if (this.$refs.uCode.canGetCode) {
if (!assertPhone(this.form['mobile'], '请输入手机号', '请输入正确的手机号')) {
return
}
// 模拟向后端请求验证码
uni.showLoading({
title: '正在获取验证码'
})
setTimeout(() => {
uni.hideLoading();
// 通知验证码组件内部开始倒计时
//发送验证码接口
const data = {
store_id: '11001',
mobile: this.form['mobile']
}
register(data).then(res => {
console.log(res);
if (res.errcode != -1) {
// this.code = res.data.code
uni.setStorageSync('smsCode', res.data['smsCode'])
// if(this.code==res.data.smsCode){
// this.$go('../../subPackages/sub_workbench/workbench_upInfo/index')
// }else{
// this.$u.toast('验证码错误')
// return
// }
}
})
this.$refs.uCode.start();
}, 1000);
} else {
this.$u.toast('倒计时结束后再发送');
}
}
}
};
</script>
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
padding-bottom: env(safe-area-inset-bottom);
}
.title {
width: 750rpx;
padding-top: 200rpx;
text-align: center;
font-size: 48rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #0D111A;
background: linear-gradient(180deg, #EAF2FF 0%, #FFFFFF 100%);
margin-bottom: 60rpx;
}
.flex-row {
display: flex;
flex-direction: row;
margin-bottom: 34rpx;
.agreement {
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
margin-left: 16rpx;
text {
color: #2979FF;
size: 24rpx;
}
}
}
.box {
box-sizing: border-box;
padding: 0 52rpx;
}
.code {
font-size: 32rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #1777FF;
background: #F9FAFB;
}
.agreement {
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
margin-left: 16rpx;
}
.pwd {
width: 646rpx;
height: 96rpx;
background: #F9FAFB;
border-radius: 48rpx 48rpx 48rpx 48rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #CCCCCC;
margin: 0rpx 0 30rpx;
::v-deep button {
color: #1777FF;
border: 0;
}
}
.hint {
margin-top: 80rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
}
.btn {
::v-deep button {
width: 648rpx;
height: 100rpx;
line-height: 100rpx;
background: #6ACDBB;
border: 0;
font-size: 32rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #FFFFFF;
}
}
.register {
display: flex;
justify-content: center;
align-items: center;
margin-top: 15px;
text {
color: #999;
font-size: 14px;
cursor: pointer;
word-spacing: 3px;
}
}
</style>

View File

@@ -104,10 +104,6 @@
return {
userInfo: uni.getStorageSync('userInfo') || {},
functions: [{
// name: '我的评价 我的常用方',
// icon: '../../static/image/wdpj.png',
// url: '../../subPackages/sub_workbench/workbench_evaluation'
// }, {
name: '我的处方',
icon: '../../static/image/wd-cf.png',
url: '/subPackages/sub_my/prescriptionList'
@@ -129,20 +125,6 @@
}
],
items: [
// {
// name: '常用语管理',
// icon: '../../static/image/wj.png',
// url: '../../subPackages/sub_my/my_commonReply/index'
// }, {
// name: '账号设置我的评价',
// icon: '../../static/image/set.png',
// url: '../../subPackages/sub_pharmacist/pharmacist_setInfo'
// },
// {
// name: '我的评价',
// icon: '../../static/image/wj.png',
// url: '../../subPackages/sub_workbench/workbench_evaluation'
// },
{
name: '常用医嘱管理',
icon: '../../static/image/i1.png',

View File

@@ -16,13 +16,6 @@
<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">
<u-icon margin-left="20" label-size="32" label-color="#31353D" name="chat" label="快捷回复" size="40"
color="#6C7380"></u-icon>
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view>
</view>
<view class="p-col-32 bottom-border flex-jus-sp flex-row flex-ali-center" @click="$go()">
<u-icon margin-left="20" label-size="32" label-color="#31353D" name="kefu-ermai" label="联系客服" size="40"
color="#6C7380"></u-icon>

View File

@@ -8,64 +8,53 @@
<u-search placeholder="请输入患者姓名" @input="handleSearch" v-model="keyword" :show-action="false"></u-search>
</view>
<view class="box p-row-2 m-t-2">
<!-- 接诊入口卡:双列布局,图标 + 标题 + 描述,与工作台宫格同风格 -->
<view class="flex-row flex-jus-sp">
<view class="function white b-r-8 flex-row p-32 flex-1"
<view class="entry-card flex-1" hover-class="entry-card-hover"
@click="$go('../../subPackages/sub_patient/to-store?id='+1)">
<image src="../../static/image/hz_xsjz.png"></image>
<view class="m-l-16 l-19">
<d-text text="到店接诊" size="32" color="#31353D"></d-text>
<image class="entry-icon" src="../../static/image/hz_xsjz.png"></image>
<view class="entry-info">
<text class="entry-name">到店接诊</text>
<text class="entry-desc">处理到店患者</text>
</view>
</view>
<view class="function white b-r-8 flex-row p-32 flex-1 m-l-2"
<view class="entry-card flex-1 m-l-2" hover-class="entry-card-hover"
@click="$go('../../subPackages/sub_patient/to-store?id='+2)">
<image src="../../static/image/hz_ddjz.png"></image>
<view class="m-l-16 l-19">
<d-text text="线上接诊" size="32" color="#31353D"></d-text>
<image class="entry-icon" src="../../static/image/hz_ddjz.png"></image>
<view class="entry-info">
<text class="entry-name">线上接诊</text>
<text class="entry-desc">线上问诊接待</text>
</view>
</view>
</view>
<view class="list-new" @click="$go('../../subPackages/sub_patient/patient_massInfo')">
<!-- 群发消息入口行 -->
<view class="list-new" hover-class="entry-card-hover" @click="$go('../../subPackages/sub_patient/patient_massInfo')">
<view class="l-l-row">
<image src="/static/image/hz-qfxx.png" mode=""></image>
<d-text text="群发消息" className="main-c m-r-16 fs-32"></d-text>
</view>
<u-icon name="arrow-right" color="#333333"></u-icon>
</view>
<view class="flex-col b-r-8 white p-32 m-t-2">
<view class="title flex-row flex-jus-sp">
<d-text text="患者管理" className="main-c m-r-16 fs-32"></d-text>
<!-- <u-icon label="筛选" size="40" label-size="28" margin-right="10" label-color="#6C7380" :name="require('@/static/image/sx.png')">
</u-icon> -->
<!-- <u-icon v-if="!isSearch" @click="isSearch = true" name="search" size="40rpx" color="#333"></u-icon> -->
<!-- <view v-else class="flex-row flex-con flex-jus-sp flex-ali-center">
<d-input @input="handleSearch" class="flex-con m-r08" v-model="keyword"
:custom-style="{fontSize:'28rpx'}" placeholder="请输入搜索内容" />
<u-icon @click="isSearch = false" name="close-circle-fill" size="40rpx" color="#999"></u-icon>
</view> -->
</view>
<view v-for="(item, index) in list" :key="index" class="flex-row flex-ali-center m-t-50"
@click="toDetail(item.patient_id)">
<u-image width="80rpx" height="80rpx" shape="circle"
:src="item.user.avatarurl || require(`../../static/image/${item.sex%2==0?'nv':'nan'}.png`)">
<u-loading slot="loading"></u-loading>
</u-image>
<view class="flex-col m-l-2 flex-jus-sp">
<view class="flex-row flex-ali-end">
<d-text :text="item.patient" className="content-c fs-32"></d-text>
<d-text :text="item.age + '岁'" className="fs-28 tips-c m-l-2"></d-text>
</view>
<!-- <view class="flex-row flex-ali-center">
<view class="m-r08">
<u-tag text="高血糖" size="mini" border-color="rgba(41,121,255,0.08)"
bg-color="rgba(41,121,255,0.08)" color="#6ACDBB" />
</view>
<view class="m-r08">
<u-tag text="高血压" size="mini" border-color="rgba(41,121,255,0.08)"
bg-color="rgba(41,121,255,0.08)" color="#6ACDBB" />
</view>
</view> -->
<view class="entry-info">
<text class="entry-name">群发消息</text>
<text class="entry-desc">向患者批量发送通知</text>
</view>
</view>
<u-icon name="arrow-right" color="#C0C4CC"></u-icon>
</view>
<!-- 患者列表:白底容器 + flat 形态列表卡(信息分层 + 箭头暗示可点) -->
<view class="flex-col b-r-8 white patient-panel m-t-2">
<view class="title flex-row flex-jus-sp patient-panel-title">
<d-text text="患者管理" className="main-c m-r-16 fs-32"></d-text>
</view>
<xk-list-card
v-for="(item, index) in list" :key="index"
mode="flat"
:avatar="item.user.avatarurl || require(`../../static/image/${item.sex%2==0?'nv':'nan'}.png`)"
:title="item.patient"
:meta="(item.sex % 2 != 0 ? '男' : '女') + ' · ' + item.age + '岁'"
:arrow="true"
@click="toDetail(item.patient_id)"
></xk-list-card>
<view class="patient-empty" v-if="!loading && list.length == 0">
<text>暂无患者,接诊后自动建立档案</text>
</view>
</view>
</view>
@@ -198,29 +187,60 @@
z-index: 982;
}
.function {
image {
width: 80rpx;
height: 80rpx;
}
/* 接诊入口双列卡:白卡大圆角 + 轻阴影,与工作台宫格同规格 */
.entry-card {
display: flex;
flex-direction: row;
align-items: center;
background: #fff;
border-radius: 24rpx;
padding: 28rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
transition: all 0.2s ease;
}
.l-19 {
.entry-card-hover {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.entry-icon {
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
}
/* 入口卡文案分层:标题 + 灰字描述 */
.entry-info {
display: flex;
align-items: center;
flex-direction: column;
margin-left: 20rpx;
min-width: 0;
}
.entry-name {
font-size: 30rpx;
font-weight: 600;
color: #31353D;
}
.entry-desc {
font-size: 22rpx;
color: #94A3B8;
margin-top: 4rpx;
}
.list-new {
width: 710rpx;
height: 108rpx;
background: #FFFFFF;
border-radius: 12rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin: 20rpx 0;
padding: 0 32rpx;
padding: 24rpx 32rpx;
color: #333333;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
.l-l-row {
display: flex;
@@ -229,9 +249,30 @@
image {
width: 56rpx;
height: 56rpx;
margin-right: 12rpx;
vertical-align: middle;
flex-shrink: 0;
}
}
}
/* 患者管理白底容器:标题带内边距,列表项走 flat 卡自带分割线 */
.patient-panel {
border-radius: 24rpx;
padding: 32rpx 12rpx 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
}
.patient-panel-title {
padding: 0 20rpx 8rpx;
}
/* 空态提示 */
.patient-empty {
padding: 60rpx 0;
text-align: center;
text {
font-size: 24rpx;
color: #999;
}
}
</style>

View File

@@ -1,95 +1,56 @@
<template>
<view class="safe-area-inset-bottom">
<view class="page-wrap safe-area-inset-bottom">
<u-navbar title="审方" :is-back="false" title-size="34" title-color="#fff" :background="{
background:'linear-gradient(90deg, #00BFA6 0%, #6ACDBB 93%)'}">
</u-navbar>
<!-- 头部 tabs -->
<!-- 头部 tabs:与下方 swiper 双向联动(点 tab / 左右滑都走 switchTab) -->
<view class="head">
<u-tabs :list="lists" :is-scroll="false" :current="currents" @change="change" active-color="#6ACDBB"
<u-tabs :list="lists" :is-scroll="false" :current="currents" @change="switchTab" active-color="#6ACDBB"
:height="99">
</u-tabs>
</view>
<!-- 空页 -->
<view class="empty" v-if="listInfo.length==0 ||status!=2">
<!-- 未通过入驻审核:整页空态守卫,不渲染 swiper -->
<view class="empty" v-if="status != 2">
<image src="/static/image/none.png" mode="aspectFit"></image>
<text v-if="status!=2">您还没有通过审核,请稍后再试~</text>
<text>您还没有通过审核,请稍后再试~</text>
</view>
<!-- 列表 -->
<block v-if="status==2">
<!-- 西药 -->
<!-- <view class="lists" v-for="(item,index) in listWest" :key="item.id"
@click="toDetail(item.id,item.prescription_no)">
<view class="info">
<view class="bh">
<image src="/static/image/tubiao1.png" mode=""></image>
{{item.prescription_no}}
</view>
<view class="name">
{{item.patients.name}} {{(item.patients.sex)%2==0?'女':'男'}} {{item.patients.age}}
</view>
<view class="time">
{{item.created_at | dateFormat}}
</view>
</view>
<view class="states" :class="item.status==1?'tg':'wtg'">
<view class="tag_states">
<u-tag text="西药" type="primary" />
</view>
{{item.status==0&&'待审核' || item.status==1&&'已通过' || item.status==2&&'未通过'}}
</view>
</view> -->
<!-- 审方列表:swiper 承载三个状态 tab,支持左右滑动切换;每个 tab 独立分页 + 上拉加载 -->
<swiper v-else class="tab-swiper" :current="currents" @change="onSwiperChange">
<swiper-item v-for="(tab, tabIndex) in tabPages" :key="tab.status">
<scroll-view scroll-y class="tab-scroll" @scrolltolower="loadMore(tabIndex)">
<view class="list-wrap">
<xk-list-card
v-for="item in tab.list" :key="item.id"
:avatar-text="item.name"
:title="item.name"
:meta="item.meta"
:time="item.time"
:tags="[item.tag]"
:rows="['处方号:' + item.prescription_no]"
:status="{ text: lists[tabIndex].name, color: tabStatusColors[tabIndex] }"
:actions="[{ key: 'detail', text: tabIndex === 0 ? '去审方' : '查看详情', type: tabIndex === 0 ? 'primary' : 'ghost' }]"
@action="toDetail(item.id, item.prescription_no)"
@click="toDetail(item.id, item.prescription_no)"
></xk-list-card>
<!-- 中药 -->
<!-- <view class="lists" v-for="(item,index) in listChinese" :key="item.id"
@click="toDetail(item.id,item.prescription_no)">
<view class="info">
<view class="bh">
<image src="/static/image/tubiao1.png" mode=""></image>
{{item.prescription_no}}
</view>
<view class="name">
{{item.patients.name}} &nbsp; {{(item.patients.sex)%2==0?'女':'男'}} &nbsp; {{item.patients.age}}
</view>
<view class="time">
{{item.created_at | dateFormat}}
</view>
</view>
<view class="states" :class="item.status==1?'tg':'wtg'">
<view class="tag_states">
<u-tag text="中药" type="success" />
</view>
{{item.status==0&&'待审核' || item.status==1&&'已通过' || item.status==2&&'未通过'}}
</view>
</view> -->
<!-- 各 tab 独立空态:首屏加载完成且无数据时展示 -->
<view class="tab-empty" v-if="tab.loaded && tab.list.length === 0">
<image src="/static/image/none.png" mode="aspectFit"></image>
<text>暂无{{lists[tabIndex].name}}的处方</text>
</view>
<!-- 颗粒 -->
<view class="lists" v-for="(item,index) in listInfo" :key="item.id"
@click="toDetail(item.id,item.prescription_no)">
<view class="info">
<view class="bh">
<image src="/static/image/tubiao1.png" mode=""></image>
{{item.prescription_no}}
<!-- 加载状态脚标:有数据时提示分页进度 -->
<view class="load-more" v-if="tab.list.length > 0">
<text v-if="tab.loading">加载中...</text>
<text v-else-if="tab.finished">没有更多了</text>
</view>
</view>
<view class="name">
{{item.patients.name}} &nbsp; {{(item.patients.sex)%2==0?'女':'男'}} &nbsp; {{item.patients.age}}
</view>
<view class="time">
{{item.created_at | dateFormat}}
</view>
</view>
<view class="states" :class="item.status==1?'tg':'wtg'">
<view class="tag_states">
<u-tag text="颗粒" type="warning" v-if="item.prescription_type==3" />
<u-tag text="中药" type="success" v-if="item.prescription_type==1" />
<u-tag text="西药" type="primary" v-if="item.prescription_type==2" />
</view>
{{item.status==0&&'待审核' || item.status==1&&'已通过' || item.status==2&&'未通过'}}
</view>
</view>
</block>
</scroll-view>
</swiper-item>
</swiper>
<d-loading-page :loading="loading"></d-loading-page>
<u-tabbar v-model="current" :list="listed" active-color="#6ACDBB" @change="changed"></u-tabbar>
@@ -97,25 +58,24 @@
</template>
<script>
// 审方列表页:老 Yii examine/list 已迁移到 xk-api(/pharmacist-examine/list,标准分页)
// 交互升级:u-tabs + swiper 双向联动(左右滑切 tab),每个 tab 独立数据缓存,切换不丢已加载内容
import {
getUserInfo,
prescripRecord
getUserInfo
} from "@/api/all.js";
import {
getExamineList
} from "@/api/pharmacistExamine.js";
import {
unwrapXkApi
} from "@/utils/api-response.js";
export default {
data() {
return {
status: uni.getStorageSync('login_status'),
keyword: "",
show: false,
identity: uni.getStorageSync("identity") || 2,
index: 0,
loading: true,
userInfo: uni.getStorageSync("userInfo") || {},
listInfo: [], // 中药
// listWest: [], // 西药
// listGranular: [], // 颗粒
waitList: [],
// is_req: false,
listed: '',
current: 0,
lists: [{
@@ -128,81 +88,144 @@
name: '未通过',
}
],
currents: 0, // 审核状态
// 审核状态色:橙=待审核 / 绿=已通过 / 红=未通过(数组下标对应 tab)
tabStatusColors: ['#FF7D00', '#00B42A', '#F53F3F'],
currents: 0, // 当前选中 tab(tabs 与 swiper 共用)
// 每个 tab 独立分页状态:切 tab 懒加载首屏,已加载的数据不重复请求
tabPages: [
{ status: 0, list: [], page: 1, finished: false, loading: false, loaded: false },
{ status: 1, list: [], page: 1, finished: false, loading: false, loaded: false },
{ status: 2, list: [], page: 1, finished: false, loading: false, loaded: false },
],
};
},
computed: {
},
onLoad() {
this.listed = [{
iconPath: require("../../static/image/w-sf.png"),
selectedIconPath: require("../../static/image/sf.png"),
text: '审方',
// pagePath: '/pages/pharmacist/index',
current: 0
},
{
iconPath: require("../../static/image/w-wd.png"),
selectedIconPath: require("../../static/image/wd.png"),
text: '我的',
// pagePath: '/pages/pharmacist/pharmacist-my',
}
]
},
onHide() {
// this.is_req = true;
},
onShow() {
this.current = 0
this.getList()
// 每次回到页面刷新当前 tab(审方通过/驳回返回后能看到最新状态),其它 tab 保留缓存
this.refreshTab(this.currents)
if (this.status != 2) {
this.getKnow()
}
},
methods: {
change(index) {
this.currents = index;
this.getList()
/** tabs 与 swiper 统一切换入口:点 tab、手势左右滑都收敛到这里 */
switchTab(index) {
if (this.currents === index) return
this.currents = index
// 懒加载:该 tab 从未加载过才拉首屏,避免每次切换全量重刷
if (!this.tabPages[index].loaded) {
this.refreshTab(index)
}
},
/** swiper 手势滑动回调:回写 currents 让顶部 tab 高亮跟随 */
onSwiperChange(e) {
this.switchTab(e.detail.current)
},
/** 刷新指定 tab(重置到第一页重新拉取) */
refreshTab(index) {
const tab = this.tabPages[index]
tab.page = 1
tab.finished = false
this.fetchTab(index, true)
},
/** 上拉加载下一页 */
loadMore(index) {
const tab = this.tabPages[index]
if (tab.loading || tab.finished) return
tab.page += 1
this.fetchTab(index, false)
},
/**
* 拉取某个 tab 的列表数据
* @param index tab 下标(对应审核状态 0/1/2)
* @param isRefresh true=覆盖列表(刷新),false=追加(加载更多)
*/
async fetchTab(index, isRefresh) {
const tab = this.tabPages[index]
if (tab.loading) return
tab.loading = true
let success = false
try {
const res = await getExamineList({
status: tab.status,
page: tab.page,
pageSize: 20,
})
const { ok, payload, message } = unwrapXkApi(res)
if (ok) {
const items = ((payload && payload.items) || []).map(this.normalizeItem)
tab.list = isRefresh ? items : tab.list.concat(items)
// 已到最后一页则停止加载更多
tab.finished = tab.page >= ((payload && payload.page_count) || 1)
success = true
} else {
this.$toast(message || '获取列表失败')
if (isRefresh) tab.list = []
}
} catch (e) {
this.$toast('网络异常,请稍后重试')
}
// 加载更多失败:回滚页码,避免重试时跳过一页数据
if (!success && !isRefresh) {
tab.page = Math.max(1, tab.page - 1)
}
tab.loading = false
// 仅成功才标记已加载:失败的 tab 切走再切回可自动重试首屏
if (success) tab.loaded = true
this.$nextTick(() => {
this.loading = false
})
},
/**
* 列表项归一化:模板禁止方法调用/可选链,展示字段全部在这里算好
* created_at 后端查询器已格式化为字符串,截取到分钟即可
*/
normalizeItem(it) {
const patients = it.user_patient || {}
return {
id: it.id,
prescription_no: it.prescription_no,
name: patients.name || '',
meta: ((patients.sex || 0) % 2 == 0 ? '女' : '男') + ' · ' + (patients.age || '-') + '岁',
time: (it.created_at || '').slice(0, 16),
tag: this.prescriptionTypeTag(it.prescription_type),
}
},
/** 处方类型标签:1中药 2西药 3颗粒,颜色与老版 u-tag 对应 */
prescriptionTypeTag(type) {
const t = Number(type)
if (t === 1) return { text: '中药', type: 'success' }
if (t === 2) return { text: '西药', type: 'primary' }
return { text: '颗粒', type: 'warning' }
},
/** 底部 tabbar 切换:我的 */
changed(index) {
this.current = index;
if (this.current == 1) {
this.$go('/pages/pharmacist/pharmacist-my')
}
},
async getList() {
let res = {};
res = await prescripRecord({
store_id: uni.getStorageSync('store_id') || '11001',
status: this.currents
})
// this.listChinese = [], this.listWest = [], this.listGranular = [];
if (res.errcode == 0) {
// console.log(res, 'lb');
this.listInfo = res.data.prescription
// this.listWest = (res.data.west).reverse();
// this.listGranular = (res.data.granular).reverse()
this.$nextTick(() => {
this.loading = false;
});
} else {
this.$toast(res.msg);
this.loading = false;
this.listInfo = []
}
},
// 详情
toDetail(id, no) {
uni.navigateTo({
url: '../../subPackages/sub_pharmacist/myrecord-detail?id=' + id + '&prescription_no=' + no
})
},
/** 入驻状态未通过时刷新登录态(走老 Yii user/info,与登录体系一致) */
getKnow() {
const data = {
store_id: uni.getStorageSync('store_id') || '11001',
@@ -222,22 +245,6 @@
this.$go('/pages/workbench/examine')
}
},
filters: {
//过滤器 用于格式化时间
dateFormat: function(value) {
var date = new Date(value * 1000); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
var year = date.getFullYear();
var month = ("0" + (date.getMonth() + 1)).slice(-2);
var sdate = ("0" + date.getDate()).slice(-2);
var hour = ("0" + date.getHours()).slice(-2);
var minute = ("0" + date.getMinutes()).slice(-2);
var second = ("0" + date.getSeconds()).slice(-2);
// 拼接
var result = year + '-' + month + '-' + sdate + ' ' + hour + ":" + minute //+ ":" + second;
// 返回
return result;
},
},
}
</script>
<style>
@@ -250,13 +257,27 @@
z-index: 999;
}
/* 页面纵向 flex 布局:navbar / tabs 占固定高度,swiper 吃满剩余空间(swiper 必须有确定高度才能滚动) */
.page-wrap {
display: flex;
flex-direction: column;
height: 100vh;
}
.head {
background-color: #fff;
width: 750rpx;
height: 99rpx;
position: sticky;
top: 180rpx;
z-index: 9999;
flex-shrink: 0;
}
.tab-swiper {
flex: 1;
min-height: 0;
}
.tab-scroll {
height: 100%;
}
.empty {
@@ -271,62 +292,38 @@
}
}
.lists {
width: 750rpx;
padding: 24rpx 36rpx;
background: #FFFFFF;
margin: 30rpx auto 20rpx;
/* 各 tab 内空态:尺寸比整页空态略小,居中展示 */
.tab-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
font-family: PingFang SC-Regular, PingFang SC;
padding: 120rpx 0;
.info {
.bh {
font-weight: 400;
color: #666666;
image {
width: 38rpx;
height: 38rpx;
vertical-align: middle;
margin-right: 18rpx;
}
}
.name {
font-weight: bold;
color: #333333;
margin: 18rpx 0;
}
.time {
width: 100%;
font-weight: 400;
color: #999999;
display: flex;
justify-content: space-between;
align-items: center;
}
image {
width: 360rpx;
height: 320rpx;
}
.states {
font-weight: 400;
color: #6A6DCD;
display: flex;
flex-direction: column;
.tag_states {
margin-bottom: 44rpx;
}
}
.wtg {
color: #FF477E;
}
.tg {
color: #6ACDBB;
text {
margin-top: 16rpx;
font-size: 28rpx;
color: #A7ABB0;
}
}
</style>
/* 分页脚标:加载中 / 没有更多 */
.load-more {
text-align: center;
padding: 24rpx 0;
text {
font-size: 24rpx;
color: #A7ABB0;
}
}
/* 列表容器:灰底页统一 24rpx 横向留白,底部避开固定 tabbar */
.list-wrap {
padding: 24rpx 24rpx 140rpx;
}
</style>

View File

@@ -63,8 +63,6 @@
};
},
onLoad() {
// this.$go('../../subPackages/sub_workbench/workbench_recipe/index')
// console.log(1);
this.screenHeight = uni.getSystemInfoSync().windowHeight
},
onShow() {

View File

@@ -238,126 +238,120 @@
@refresherrefresh="page=1;status = 'loading';loading = true;infoList=[];$store.dispatch('getDocSession')"
:refresher-triggered="loading" :refresher-enabled="false" v-if="!loading">
<!-- 系统消息 -->
<view class="news_item flex-row flex-jus-sp flex-ali-center"
<!-- 系统消息(有未读):flat-card 分层结构 + 右箭头 -->
<view class="news_item flat-card flex-row flex-ali-start"
@click="$go('../../subPackages/sub_patient/patient_label?type=1')" v-if="systemList.num>0">
<view style="width: 80rpx;position: relative;">
<view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" src="/static/image/home1.png">
<u-loading slot="loading"></u-loading>
</u-image>
<u-badge :count="systemList.num" v-if="systemList.num>0" :offset="[0,0]"
bgColor="#F44336" :overflow-count="9999"></u-badge>
<u-badge :count="systemList.num" v-if="systemList.num>0" :offset="[-4,-4]"
bgColor="#F44336" :overflow-count="99"></u-badge>
</view>
<view class="news_item_info flex-col flex-jus-sp">
<view class="wb-card-body flex-col flex-1 text-hide">
<view class="flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-ali-center">
<d-text text="系统消息" className="content-c fs-32 m-r-16"></d-text>
</view>
<d-text :text="systemList.time" size="24"></d-text>
<text class="wb-name">系统消息</text>
<text class="wb-time">{{ systemList.time }}</text>
</view>
<view class="flex-row flex-jus-sp">
<view class="news_item_info_msg text-hide">
<text style="font-size: 24rpx;margin-right: 20rpx;"
v-if="systemList.num>0">你有{{systemList.num}}条未读消息</text>
</view>
<view class="wb-preview text-hide m-t-8">
<text>你有{{systemList.num}}条未读消息</text>
</view>
</view>
<view class="wb-entry-arrow">
<u-icon name="arrow-right" color="#C0C4CC" size="28"></u-icon>
</view>
</view>
<!-- 患者消息 -->
<view class="news_item flex-row flex-jus-sp flex-ali-center"
<!-- 患者消息(有待接诊):flat-card 分层结构 + 右箭头 -->
<view class="news_item flat-card flex-row flex-ali-start"
@click="$go('../../subPackages/sub_patient/patient_label?type=2')" v-if="infoList.num>0">
<view style="width: 80rpx;position: relative;">
<view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" :src="'/static/image/home2.png'">
<u-loading slot="loading"></u-loading>
</u-image>
<u-badge :count="infoList.num" v-if="infoList.num>0" :offset="[0,0]" bgColor="#F44336"
:overflow-count="9999"></u-badge>
<u-badge :count="infoList.num" v-if="infoList.num>0" :offset="[-4,-4]" bgColor="#F44336"
:overflow-count="99"></u-badge>
</view>
<view class="news_item_info flex-col flex-jus-sp">
<view class="wb-card-body flex-col flex-1 text-hide">
<view class="flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-ali-center">
<d-text text="患者消息" className="content-c fs-32 m-r-16"></d-text>
</view>
<d-text :text="infoList.time" size="24"></d-text>
<text class="wb-name">患者消息</text>
<text class="wb-time">{{ infoList.time }}</text>
</view>
<view class="flex-row flex-jus-sp">
<view class="news_item_info_msg text-hide">
<text style="font-size: 24rpx;margin-right: 20rpx;"
v-if="infoList.num>0">你有{{infoList.num}}名患者待接诊</text>
</view>
<view class="wb-preview text-hide m-t-8">
<text>你有{{infoList.num}}名患者待接诊</text>
</view>
</view>
<view class="wb-entry-arrow">
<u-icon name="arrow-right" color="#C0C4CC" size="28"></u-icon>
</view>
</view>
<view class="news_item flex-row flex-jus-sp flex-ali-center"
<!-- 系统消息(无未读兜底入口) -->
<view class="news_item flat-card flex-row flex-ali-start"
v-if="!loading&&systemList.length==0"
@click="$go('../../subPackages/sub_patient/patient_label?type=1')">
<view style="width: 80rpx;position: relative;">
<view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" src="/static/image/home1.png">
<u-loading slot="loading"></u-loading>
</u-image>
</view>
<view class="news_item_info flex-col flex-jus-sp">
<view class="wb-card-body flex-col flex-1 text-hide">
<view class="flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-ali-center">
<d-text text="系统消息" size="34" color="#000"></d-text>
</view>
<d-text :text="systemList.time" size="24"></d-text>
<text class="wb-name">系统消息</text>
<text class="wb-time">{{ systemList.time }}</text>
</view>
<view class="flex-row flex-jus-sp">
<view class="news_item_info_msg text-hide">
<text style="font-size: 28rpx;margin-right: 20rpx;">点击查看历史消息</text>
</view>
<view class="wb-preview text-hide m-t-8">
<text>点击查看历史消息</text>
</view>
</view>
<view class="wb-entry-arrow">
<u-icon name="arrow-right" color="#C0C4CC" size="28"></u-icon>
</view>
</view>
<!-- 历史患者 -->
<view class="news_item flex-row flex-jus-sp flex-ali-center" v-if="!loading&&infoList.length==0"
<!-- 患者消息(无待接诊兜底入口) -->
<view class="news_item flat-card flex-row flex-ali-start" v-if="!loading&&infoList.length==0"
@click="$go('../../subPackages/sub_patient/patient_label?type=2')">
<view style="width: 80rpx;position: relative;">
<view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" :src="'/static/image/home2.png'">
<u-loading slot="loading"></u-loading>
</u-image>
</view>
<view class="news_item_info flex-col flex-jus-sp">
<view class="wb-card-body flex-col flex-1 text-hide">
<view class="flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-ali-center">
<d-text text="患者消息" size="34" color="#000"></d-text>
</view>
<d-text :text="infoList.time" size="24"></d-text>
<text class="wb-name">患者消息</text>
<text class="wb-time">{{ infoList.time }}</text>
</view>
<view class="flex-row flex-jus-sp">
<view class="news_item_info_msg text-hide">
<text style="font-size: 28rpx;margin-right: 20rpx;">点击查看历史消息</text>
</view>
<view class="wb-preview text-hide m-t-8">
<text>点击查看历史消息</text>
</view>
</view>
<view class="wb-entry-arrow">
<u-icon name="arrow-right" color="#C0C4CC" size="28"></u-icon>
</view>
</view>
<!-- 处方 -->
<view v-if="noticeList.length" class="news_item flex-row flex-jus-sp flex-ali-center"
<!-- 待流转处方 -->
<view v-if="noticeList.length" class="news_item flat-card flex-row flex-ali-start"
@click="$go('/subPackages/sub_workbench/prescriptionList')">
<view style="width: 80rpx;position: relative;">
<view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" src="/static/image/home1.png">
<u-loading slot="loading"></u-loading>
</u-image>
<u-badge :count="noticeList.length" :offset="[0,0]"
bgColor="#F44336" :overflow-count="9999"></u-badge>
<u-badge :count="noticeList.length" :offset="[-4,-4]"
bgColor="#F44336" :overflow-count="99"></u-badge>
</view>
<view class="news_item_info flex-col flex-jus-sp">
<view class="wb-card-body flex-col flex-1 text-hide">
<view class="flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-ali-center">
<d-text text="待流转处方消息" className="content-c fs-32 m-r-16"></d-text>
</view>
<text class="wb-name">待流转处方消息</text>
</view>
<view class="flex-row flex-jus-sp">
<view class="news_item_info_msg text-hide">
<text style="font-size: 24rpx;margin-right: 20rpx;">你有{{noticeList.length}}条未读消息</text>
</view>
<view class="wb-preview text-hide m-t-8">
<text>你有{{noticeList.length}}条未读消息</text>
</view>
</view>
<view class="wb-entry-arrow">
<u-icon name="arrow-right" color="#C0C4CC" size="28"></u-icon>
</view>
</view>
</scroll-view>
</view>
@@ -1463,6 +1457,13 @@ page {
align-items: center;
}
/* 消息入口行右侧箭头:垂直居中,暗示整行可点 */
.wb-entry-arrow {
flex-shrink: 0;
margin-left: 12rpx;
align-self: center;
}
.wb-action-btn {
padding: 10rpx 28rpx;
font-size: 24rpx;

View File

@@ -1,136 +0,0 @@
{
"easycom": {
"autoscan": true,
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
"^d-(.*)": "@/components/d-$1/d-$1.vue"
},
"pages": [{
"path": "pages/workbench/index",
"style": {
"navigationBarTitleText": "工作台",
"navigationStyle": "custom"
}
},
{
"path": "pages/patient/index",
"style": {
"navigationBarTitleText": "患者",
"navigationStyle": "custom"
}
},
{
"path": "pages/article/index",
"style": {
"navigationBarTitleText": "我的文章",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/my/index",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/pharmacist/index",
"style": {
"navigationBarTitleText": "药师",
"navigationStyle": "custom"
}
}
],
"subPackages": [{
"root": "subPackages/sub_workbench",
"pages": [{
"path": "workbench_code",
"style": {
"navigationBarTitleText": "医生码"
}
}]
}, {
"root": "subPackages/sub_patient",
"pages": [{
"path": "patient_label",
"style": {
"navigationBarTitleText": "标签管理"
}
},
{
"path": "patient_massInfo",
"style": {
"navigationBarTitleText": "群发信息"
}
},
{
"path": "patient_select",
"style": {
"navigationBarTitleText": "群发信息"
}
},
{
"path": "patient_details",
"style": {
"navigationBarTitleText": "患者详情"
}
}
]
},
{
"root": "subPackages/sub_article",
"pages": [{
"path": "article_release",
"style": {
"navigationBarTitleText": "无标题文档"
}
}, {
"path": "article_draft",
"style": {
"navigationBarTitleText": "我的草稿"
}
}, {
"path": "article_video",
"style": {
"navigationBarTitleText": "无标题文档"
}
}
]
}
],
"tabBar": {
"color": "#666666",
"selectedColor": "#1575FC",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [{
"pagePath": "pages/workbench/index",
"text": "工作台"
},
{
"pagePath": "pages/patient/index",
"text": "患者"
},
{
"pagePath": "pages/article/index",
"text": "我的文章"
},
{
"pagePath": "pages/my/index",
"text": "我的"
},
{
"pagePath": "pages/pharmacist/index",
"text": "审方"
}
]
},
"globalStyle": {
"navigationBarTitleText": "掌上互联网医院",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f7f8fa"
}
}

View File

@@ -1,29 +0,0 @@
<template>
<web-view v-if="src" @message="getMessage" ref="webview" :src="src">
</web-view>
</template>
<script>
export default {
data() {
return {
src: "",
};
},
onLoad(op) {
const userInfo = uni.getStorageSync("userInfo");
this.src = `https://wap.nbxlmy.com?name=${userInfo.name}&name2=${op.name2}&avatar=${userInfo.avatar}&type=1&userID=${op.userId}&SDKAppID=${op.appid}&myUserID=${op.myUserId}&userSig=${op.key}&avatar2=${op.avatarurl}`;
console.log(this.src, " this.src");
},
mounted() {
console.log(this.$refs);
},
methods: {
getMessage(e) {
if (e) {
}
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,278 @@
<template>
<!--
工作台卡片样式选择页(四个业务角色端共用,跨分包 navigateTo 进入)
两张 CSS 手绘预览卡单选:长卡片(一行两张 iOS 行卡)/ 宫格(金刚区),点击即持久化,
返回工作台 onShow 重读生效;「我的」页入口行通过 uni.$emit 同步文案
-->
<view class="cs-page">
<u-navbar
title="卡片样式"
:is-back="true"
title-size="34"
title-color="#fff"
back-icon-color="#fff"
:background="{ background: 'linear-gradient(90deg, #00BFA6 0%, #6ACDBB 93%)' }"
></u-navbar>
<view class="cs-body">
<view class="cs-tip">选择工作台功能入口的展示样式,选中立即生效</view>
<!-- 选项一:长卡片(一行两张 iOS 行卡示意,2×2 排布) -->
<view class="style-option" :class="{ active: style === 'long' }" hover-class="option-hover" @tap="choose('long')">
<view class="preview">
<view class="mock-row-grid">
<view class="mock-row">
<view class="mock-tile tile-a"></view>
<view class="mock-lines">
<view class="mock-line-main"></view>
<view class="mock-line-sub"></view>
</view>
</view>
<view class="mock-row">
<view class="mock-tile tile-b"></view>
<view class="mock-lines">
<view class="mock-line-main"></view>
<view class="mock-line-sub"></view>
</view>
</view>
<view class="mock-row">
<view class="mock-tile tile-c"></view>
<view class="mock-lines">
<view class="mock-line-main"></view>
<view class="mock-line-sub"></view>
</view>
</view>
<view class="mock-row">
<view class="mock-tile tile-d"></view>
<view class="mock-lines">
<view class="mock-line-main"></view>
<view class="mock-line-sub"></view>
</view>
</view>
</view>
</view>
<view class="option-meta">
<view class="option-text">
<text class="option-name">长卡片</text>
<text class="option-desc">一行两张大卡,带功能描述,信息更完整</text>
</view>
<view class="option-check" :class="{ checked: style === 'long' }">
<u-icon v-if="style === 'long'" name="checkmark" size="26" color="#FFFFFF"></u-icon>
</view>
</view>
</view>
<!-- 选项二:宫格(金刚区示意) -->
<view class="style-option" :class="{ active: style === 'short' }" hover-class="option-hover" @tap="choose('short')">
<view class="preview">
<view class="mock-grid">
<view class="mock-cell">
<view class="mock-tile tile-grid tile-a"></view>
<view class="mock-label"></view>
</view>
<view class="mock-cell">
<view class="mock-tile tile-grid tile-b"></view>
<view class="mock-label"></view>
</view>
<view class="mock-cell">
<view class="mock-tile tile-grid tile-c"></view>
<view class="mock-label"></view>
</view>
<view class="mock-cell">
<view class="mock-tile tile-grid tile-d"></view>
<view class="mock-label"></view>
</view>
</view>
</view>
<view class="option-meta">
<view class="option-text">
<text class="option-name">宫格</text>
<text class="option-desc">金刚区图标排列,一屏看到更多功能</text>
</view>
<view class="option-check" :class="{ checked: style === 'short' }">
<u-icon v-if="style === 'short'" name="checkmark" size="26" color="#FFFFFF"></u-icon>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
/**
* 卡片样式选择页
* 存储沿用 utils/workbenchCardPref.js 的 wb_card_style 键(long / short),
* short 的渲染含义为金刚区宫格(四首页与本页文案保持一致)。
* 为什么放 sub_business_shared:四端「我的」页都要进入,共享分包页面可被任意分包 navigateTo。
*/
import { getCardStyle, setCardStyle } from '@/utils/workbenchCardPref.js';
export default {
data() {
return {
/** 当前选中样式:long 长卡片 / short 宫格 */
style: 'short',
};
},
onLoad() {
this.style = getCardStyle();
},
methods: {
/** 点击预览卡即选中并持久化;广播给「我的」页入口行同步文案 */
choose(style) {
if (this.style === style) return;
this.style = style;
setCardStyle(style);
uni.$emit('wbCardStyleChanged', style);
uni.showToast({ title: style === 'long' ? '已切换为长卡片' : '已切换为宫格', icon: 'none' });
},
},
};
</script>
<style>
page {
background-color: #f9fafb;
}
</style>
<style lang="scss" scoped>
.cs-body {
padding: 24rpx 32rpx 60rpx;
}
.cs-tip {
font-size: 24rpx;
color: #8E8E93;
padding: 8rpx 6rpx 24rpx;
}
/* 选项卡片:iOS 卡片规格,选中态主题色描边 + 轻主题底 */
.style-option {
background: #FFFFFF;
border-radius: 32rpx;
border: 4rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 28rpx;
margin-bottom: 28rpx;
transition: border-color 0.2s ease, background 0.2s ease;
}
.style-option.active {
border-color: #6ACDBB;
background: rgba(106, 205, 187, 0.04);
}
.option-hover {
opacity: 0.9;
}
/* 预览区:浅灰底衬托手绘示意 */
.preview {
background: #F4F6F8;
border-radius: 20rpx;
padding: 24rpx;
}
/* --- 长卡片示意:2×2 排布的行卡(磁贴 + 两行文字线),对应真实的一行两张大卡 --- */
.mock-row-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
}
.mock-row {
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 16rpx;
padding: 16rpx;
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.04);
min-width: 0;
}
.mock-tile {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
.tile-a { background: #6ACDBB; }
.tile-b { background: #3B82F6; }
.tile-c { background: #F97316; }
.tile-d { background: #A855F7; }
.mock-lines {
flex: 1;
min-width: 0;
margin-left: 14rpx;
}
.mock-line-main {
width: 55%;
height: 14rpx;
border-radius: 8rpx;
background: #D8DCE1;
margin-bottom: 8rpx;
}
.mock-line-sub {
width: 80%;
height: 10rpx;
border-radius: 6rpx;
background: #EBEEF2;
}
/* --- 宫格示意:白卡内 4 列磁贴 + 标签线 --- */
.mock-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
background: #FFFFFF;
border-radius: 16rpx;
padding: 20rpx 8rpx;
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.04);
}
.mock-cell {
display: flex;
flex-direction: column;
align-items: center;
}
.mock-tile.tile-grid {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
}
.mock-label {
width: 56rpx;
height: 12rpx;
border-radius: 6rpx;
background: #EBEEF2;
margin-top: 12rpx;
}
/* --- 选项文字与选中对勾 --- */
.option-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24rpx;
}
.option-text {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.option-name {
font-size: 30rpx;
font-weight: 600;
color: #1C1C1E;
}
.option-desc {
font-size: 24rpx;
color: #8E8E93;
margin-top: 6rpx;
}
/* 圆形对勾:未选中空描边圆,选中主题色实心 + 白勾 */
.option-check {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 3rpx solid #D8DCE1;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-left: 20rpx;
transition: background 0.2s ease, border-color 0.2s ease;
}
.option-check.checked {
background: #6ACDBB;
border-color: #6ACDBB;
}
</style>

View File

@@ -30,6 +30,7 @@ export function getPlatformAdminContext() {
},
tabList: [
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: PLATFORM_HOME },
{ text: '功能', iconPath: 'grid', selectedIconPath: 'grid-fill', pagePath: '/subPackages/sub_platform_admin/features/index' },
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: PLATFORM_MY },
],
};
@@ -54,6 +55,7 @@ export function getSalespersonContext() {
},
tabList: [
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: SALESPERSON_HOME },
{ text: '功能', iconPath: 'grid', selectedIconPath: 'grid-fill', pagePath: '/subPackages/sub_salesperson/features/index' },
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: SALESPERSON_MY },
],
};
@@ -80,6 +82,7 @@ export function getClinicAdminContext() {
},
tabList: [
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: '/subPackages/sub_clinic_admin/home/index' },
{ text: '功能', iconPath: 'grid', selectedIconPath: 'grid-fill', pagePath: '/subPackages/sub_clinic_admin/features/index' },
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: '/subPackages/sub_clinic_admin/my/index' },
],
};

View File

@@ -0,0 +1,80 @@
/**
* 个人中心多端上下文(sub_business_shared/profile 专用)
* 为什么单独建而不是扩展 resolveBusinessContext:现有 resolveBusinessContext 只覆盖
* 平台管理员/业务员/诊所管理员 3 端且服务于订单等业务页,这里需要覆盖含诊所推广员的
* 4 端并聚焦「个人资料」能力,独立文件避免影响既有业务页
* 四端接口均为 xk-api(jok 形态),API 封装统一返回 { res, data, ok }
*/
import {
getClinicAdminMyProfile,
updateClinicAdminMyProfile,
updateClinicAdminMyPassword,
} from '@/api/clinicAdmin.js';
import { platformAdminApi, salespersonApi } from '@/api/businessApi.js';
import {
getClinicSalespersonMyProfile,
updateClinicSalespersonMyProfile,
updateClinicSalespersonMyPassword,
} from '@/api/clinicSalesperson.js';
const CONTEXT_MAP = {
clinic_admin: {
mode: 'clinic_admin',
roleFallback: '诊所管理员',
userStorageKey: 'clinic_admin_user',
getMyProfile: getClinicAdminMyProfile,
updateMyProfile: updateClinicAdminMyProfile,
updateMyPassword: updateClinicAdminMyPassword,
},
platform_admin: {
mode: 'platform_admin',
roleFallback: '平台管理员',
userStorageKey: 'platform_admin_user',
getMyProfile: () => platformAdminApi.getMyProfile(),
updateMyProfile: (data) => platformAdminApi.updateMyProfile(data),
updateMyPassword: (data) => platformAdminApi.updateMyPassword(data),
},
salesperson: {
mode: 'salesperson',
roleFallback: '业务员',
userStorageKey: 'salesperson_user',
getMyProfile: () => salespersonApi.getMyProfile(),
updateMyProfile: (data) => salespersonApi.updateMyProfile(data),
updateMyPassword: (data) => salespersonApi.updateMyPassword(data),
},
clinic_salesperson: {
mode: 'clinic_salesperson',
roleFallback: '推广员',
userStorageKey: 'clinic_salesperson_user',
getMyProfile: getClinicSalespersonMyProfile,
updateMyProfile: updateClinicSalespersonMyProfile,
updateMyPassword: updateClinicSalespersonMyPassword,
},
};
/**
* 按当前 loginMode 解析个人中心上下文(默认诊所管理员,避免异常 mode 导致白屏)
*/
export function resolveProfileContext() {
const mode = uni.getStorageSync('loginMode');
return CONTEXT_MAP[mode] || CONTEXT_MAP.clinic_admin;
}
/**
* 资料保存成功后把昵称/头像写回各端 user 缓存
* 为什么要写回:四端「我的」页 onShow 先读本地缓存兜底首屏,不写回的话返回时会闪旧资料
* 注意推广员端缓存是 { salesperson, store } 嵌套结构,与其余三端平铺结构不同
* @param {object} ctx resolveProfileContext 返回的上下文
* @param {object} profile 最新资料(至少含 nick_name / avatar)
*/
export function applyProfileToCache(ctx, profile) {
if (!ctx || !profile) return;
const cached = uni.getStorageSync(ctx.userStorageKey) || {};
const patch = { nick_name: profile.nick_name || '', avatar: profile.avatar || '' };
if (ctx.mode === 'clinic_salesperson') {
cached.salesperson = Object.assign({}, cached.salesperson || {}, patch);
uni.setStorageSync(ctx.userStorageKey, cached);
return;
}
uni.setStorageSync(ctx.userStorageKey, Object.assign({}, cached, patch));
}

View File

@@ -1,22 +1,38 @@
/**
* 诊所管理员端 tabbar 配置(sub_business_shared/clinic-page-layout 用的副本)
* 与 sub_clinic_admin/common/tabbar.js 保持同步:三 tab(工作台/功能/我的)+ uView 图标名
*/
export const TAB_WORKBENCH = 0
export const TAB_MY = 1
export const TAB_FEATURES = 1
export const TAB_MY = 2
const ROOT = '/subPackages/sub_clinic_admin'
export const CLINIC_TAB_ROUTES = [
'/subPackages/sub_clinic_admin/home/index',
'/subPackages/sub_clinic_admin/my/index',
`${ROOT}/home/index`,
`${ROOT}/features/index`,
`${ROOT}/my/index`,
]
export function getClinicTabList() {
return [
{
iconPath: require('@/static/image/w-sf.png'),
selectedIconPath: require('@/static/image/sf.png'),
iconPath: 'home',
selectedIconPath: 'home-fill',
text: '工作台',
pagePath: `${ROOT}/home/index`,
},
{
iconPath: require('@/static/image/w-wd.png'),
selectedIconPath: require('@/static/image/wd.png'),
iconPath: 'grid',
selectedIconPath: 'grid-fill',
text: '功能',
pagePath: `${ROOT}/features/index`,
},
{
iconPath: 'account',
selectedIconPath: 'account-fill',
text: '我的',
pagePath: `${ROOT}/my/index`,
},
]
}

View File

@@ -4,16 +4,24 @@
<view v-if="!auditMode" class="toolbar">
<button class="btn" @click="goForm()">新建录入</button>
</view>
<view v-for="item in list" :key="item.id" class="card" @click="goDetail(item)">
<view class="row">
<text class="name">{{ item.name }}</text>
<text class="status" :class="'s' + item.status">{{ statusText(item.status) }}</text>
</view>
<view class="sub">{{ item.mobile }} · {{ formatTime(item.created_at) }}</view>
<view v-if="inputType === 'store' && item.promoter_admin" class="promoter-row" @click.stop>
<promoter-info-card :admin="item.promoter_admin" />
</view>
</view>
<!-- 录入列表卡:统一 xk-list-card 规范;门店卡附带推广员信息(body 插槽) -->
<xk-list-card
v-for="item in list" :key="item.id"
:avatar-text="item.name"
:title="item.name"
:time="formatTime(item.created_at)"
:status="{ text: statusText(item.status), color: statusColor(item.status) }"
:actions="[{ key: 'detail', text: '查看详情', type: 'ghost' }]"
@action="goDetail(item)"
@click="goDetail(item)"
>
<template #body>
<text class="card-sub">{{ item.mobile || '-' }}</text>
<view v-if="inputType === 'store' && item.promoter_admin" class="promoter-row" @tap.stop>
<promoter-info-card :admin="item.promoter_admin" />
</view>
</template>
</xk-list-card>
<view v-if="!list.length" class="empty">暂无记录</view>
</view>
</business-page-layout>
@@ -61,6 +69,10 @@ export default {
statusText(s) {
return { 0: '待审核', 1: '已通过', 2: '已拒绝' }[Number(s)] || '-'
},
/** 状态色映射:与 statusText 对应(橙=待审核 / 绿=通过 / 红=拒绝) */
statusColor(s) {
return { 0: '#FF7D00', 1: '#00B42A', 2: '#F53F3F' }[Number(s)] || '#999'
},
formatTime(t) {
if (!t) return '-'
const d = new Date(Number(t) * 1000)
@@ -78,14 +90,8 @@ export default {
.page-wrap { padding: 24rpx; }
.toolbar { margin-bottom: 24rpx; }
.btn { background: #6acdbb; color: #fff; font-size: 28rpx; }
.card { background: #fff; padding: 28rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 30rpx; font-weight: 600; }
.status { font-size: 24rpx; }
.s0 { color: #FF7D00; }
.s1 { color: #00B42A; }
.s2 { color: #F53F3F; }
.sub { margin-top: 12rpx; color: #86909C; font-size: 24rpx; }
/* 卡内副文案行(body 插槽内自带样式,组件 rows 样式不作用于插槽) */
.card-sub { display: block; margin-top: 8rpx; color: #86909C; font-size: 24rpx; }
.promoter-row { margin-top: 16rpx; padding-top: 12rpx; border-top: 1rpx solid #f5f5f5; }
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
</style>

View File

@@ -88,9 +88,13 @@ $color-text-body: #4E5969;
$color-text-muted: #86909C;
$color-border: #F2F3F5;
/* iOS 卡片规格:与订单详情各面板一致(大圆角 + 发丝线 + 轻阴影) */
.express-panel {
background: #ffffff;
padding: 32rpx 40rpx;
border-radius: 32rpx;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 28rpx;
margin-bottom: 24rpx;
}

View File

@@ -2,15 +2,24 @@
<view>
<business-page-layout title="订单详情" :show-back="true">
<view class="detail-container">
<!-- 头部面板:通栏,展示金额与状态 -->
<!-- 状态头卡:状态大字(状态色区分)+ 实付金额 + 可复制单号 -->
<view v-if="info.id" class="detail-panel header-panel">
<view class="order-head">
<view class="status-head">
<view class="status-main">
<view class="status-dot" :style="{ background: statusColor }"></view>
<text class="status-text" :style="{ color: statusColor }">{{ detailExtra.statusText }}</text>
</view>
<view class="pay-group">
<text class="pay-label">实付</text>
<text class="pay-total">{{ money(info.total_pay_price) }}</text>
</view>
</view>
<view class="order-no-row">
<text class="order-no-main">订单号:{{ info.order_no }}</text>
<text class="pay-total">{{ money(info.total_pay_price) }}</text>
<view class="copy-btn" hover-class="copy-btn-hover" @tap="copyOrderNo"><text>复制</text></view>
</view>
<view class="tags">
<text class="modern-tag tag-status">{{ detailExtra.statusText }}</text>
<text class="modern-tag tag-type">{{ detailExtra.prescriptionTypeText }}</text>
<text class="modern-tag tag-delivery">{{ detailExtra.deliveryText }}</text>
<text v-if="detailExtra.isProxyPay" class="modern-tag tag-proxy">代支付</text>
@@ -309,11 +318,29 @@ export default {
canViewErpMenu() {
return this.canViewErp && Number(this.info.prescription_type) === 1
},
/** 订单状态色:待办橙 / 在途蓝 / 完成绿 / 退款红 / 取消灰(与列表状态语义一致) */
statusColor() {
const m = {
0: '#FF7D00', 1: '#FF7D00', 2: '#3B82F6', 3: '#00B42A', 4: '#F53F3F',
5: '#FF7D00', 6: '#00B42A', 7: '#00B42A', 8: '#F53F3F', 9: '#86909C',
}
return m[Number(this.info.status)] || '#86909C'
},
},
onLoad(q) {
this.id = Number(q.id || 0)
this.load()
},
/** 下拉刷新:重拉详情(含物流),完成后收起刷新动画 */
onPullDownRefresh() {
const done = () => uni.stopPullDownRefresh()
const p = this.load()
if (p && p.then) {
p.then(done, done)
} else {
done()
}
},
/** 离开页关闭处方抽屉,避免 page-container 残留遮罩导致列表白屏 */
onHide() {
this.closePrescriptionPopup()
@@ -344,12 +371,13 @@ export default {
this.patientDetailName = this.info.patient || ''
this.patientDetailVisible = true
},
/** 拉取订单详情;返回 Promise 供下拉刷新在完成后收起动画 */
load() {
if (!this.id) {
uni.showToast({ title: '缺少订单信息', icon: 'none' })
return
}
this.bizApi.getOrderDetail(this.id).then(w => {
return this.bizApi.getOrderDetail(this.id).then(w => {
if (!w.ok) {
uni.showToast({ title: '订单加载失败', icon: 'none' })
return
@@ -364,6 +392,13 @@ export default {
uni.showToast({ title: '订单加载失败', icon: 'none' })
})
},
/** 复制订单号(状态头卡复制按钮) */
copyOrderNo() {
uni.setClipboardData({
data: String(this.info.order_no || ''),
success: () => uni.showToast({ title: '订单号已复制', icon: 'none' }),
})
},
loadExpress() {
this.bizApi.getExpressDetailByOrderId(this.id).then(w => {
if (w.ok) this.expressInfo = w.data
@@ -435,48 +470,110 @@ export default {
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$color-page-bg: #F5F7F8;
$color-text-title: #222B2A;
$color-page-bg: #F4F6F8;
$color-text-title: #1C1C1E;
$color-text-body: #4E5969;
$color-text-muted: #86909C;
$color-text-muted: #8E8E93;
$color-border: #F2F3F5;
$color-danger: #F53F3F;
.detail-container {
background-color: $color-page-bg;
min-height: 100vh;
/* 适配底部操作栏,留出极大的安全边距 */
padding-bottom: 180rpx;
/* iOS 卡片流:两侧留白 + 适配底部操作栏的安全边距 */
padding: 20rpx 24rpx 180rpx;
}
/* 通栏面板 */
/* iOS 卡片面板:大圆角 + 发丝线 + 轻阴影(与工作台卡片同规格) */
.detail-panel {
background: #ffffff;
padding: 32rpx 40rpx;
border-radius: 32rpx;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 28rpx;
margin-bottom: 24rpx;
}
.header-panel {
padding-top: 40rpx;
padding-top: 32rpx;
}
.order-head {
/* 状态头:左状态大字(色点 + 文案),右实付金额 */
.status-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.order-no-main {
font-size: 28rpx;
.status-main {
display: flex;
align-items: center;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.status-text {
font-size: 36rpx;
font-weight: 700;
}
.pay-group {
display: flex;
align-items: baseline;
}
.pay-label {
font-size: 22rpx;
color: $color-text-muted;
margin-right: 8rpx;
}
/* 单号行:灰底圆角条 + 复制按钮 */
.order-no-row {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(60, 60, 67, 0.04);
border-radius: 16rpx;
padding: 14rpx 20rpx;
margin-bottom: 24rpx;
}
.order-no-main {
font-size: 24rpx;
color: $color-text-muted;
flex: 1;
min-width: 0;
word-break: break-all;
}
.copy-btn {
flex-shrink: 0;
margin-left: 16rpx;
padding: 4rpx 20rpx;
border-radius: 20rpx;
border: 1rpx solid $theme-primary;
text {
font-size: 22rpx;
color: $theme-primary;
}
}
.copy-btn-hover {
opacity: 0.6;
}
.pay-total {
font-size: 40rpx;
color: $color-text-title;
font-weight: 700;
font-family: monospace, sans-serif;
font-family: "DIN Alternate", monospace, sans-serif;
}
.tags {
@@ -493,7 +590,6 @@ $color-danger: #F53F3F;
font-weight: 500;
}
.tag-status { background: rgba(106, 205, 187, 0.1); color: $theme-primary; }
.tag-type { background: rgba(230, 162, 60, 0.1); color: #e6a23c; }
.tag-delivery { background: rgba(91, 127, 214, 0.1); color: #5b7fd6; }
.tag-proxy { background: rgba(114, 46, 209, 0.1); color: #722ED1; }
@@ -678,6 +774,7 @@ $color-danger: #F53F3F;
left: 0;
right: 0;
background: #ffffff;
border-top: 1rpx solid rgba(60, 60, 67, 0.08);
padding: 24rpx 40rpx;
/* 自动撑起 iOS 底部安全距离 */
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));

View File

@@ -0,0 +1,416 @@
<template>
<!--
个人中心(四个管理端共用,跨分包 navigateTo 进入)
头像/昵称可编辑,账号/手机号/角色/所属门店等只读,支持修改登录密码;
按 loginMode 走各端 API(profileContext),保存成功写回各端 user 缓存
-->
<view class="profile-page">
<u-navbar
title="个人中心"
:is-back="true"
title-size="34"
title-color="#fff"
back-icon-color="#fff"
:background="{ background: 'linear-gradient(90deg, #00BFA6 0%, #6ACDBB 93%)' }"
></u-navbar>
<view class="profile-body">
<!-- 可编辑区:头像 + 昵称 -->
<view class="panel">
<view class="row" hover-class="row-hover" @tap="changeAvatar">
<text class="row-label">头像</text>
<view class="row-value">
<image v-if="profile.avatar" class="avatar-img" :src="profile.avatar" mode="aspectFill" />
<view v-else class="avatar-circle"><text>{{ avatarText }}</text></view>
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
</view>
<view class="row row-last" hover-class="row-hover" @tap="openNickDrawer">
<text class="row-label">昵称</text>
<view class="row-value">
<text class="value-text">{{ profile.nick_name || '-' }}</text>
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
</view>
</view>
<!-- 只读区:账号身份信息不允许自助修改 -->
<view class="panel">
<view v-for="(item, index) in readonlyRows" :key="item.label" class="row" :class="{ 'row-last': index === readonlyRows.length - 1 }">
<text class="row-label">{{ item.label }}</text>
<view class="row-value">
<text class="value-text value-readonly">{{ item.value }}</text>
</view>
</view>
</view>
<!-- 安全区:修改密码 -->
<view class="panel">
<view class="row row-last" hover-class="row-hover" @tap="openPwdDrawer">
<text class="row-label">修改密码</text>
<view class="row-value">
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
</view>
</view>
</view>
</view>
<!-- 昵称编辑抽屉(page-container 防误退,v-if 由组件内部处理) -->
<drawer-page-container v-model="nickDrawerVisible" title="修改昵称">
<view class="drawer-form">
<input
v-model="nickForm.nick_name"
class="form-input"
type="text"
:maxlength="20"
placeholder="请输入昵称(最长 20 个字)"
placeholder-class="input-placeholder"
/>
<u-button
:throttle-time="0"
shape="circle"
:loading="saving"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '88rpx', marginTop: '40rpx' }"
@click="saveNickName"
>保存</u-button>
</view>
</drawer-page-container>
<!-- 修改密码抽屉 -->
<drawer-page-container v-model="pwdDrawerVisible" title="修改密码">
<view class="drawer-form">
<view class="pwd-field">
<text class="pwd-label">旧密码</text>
<input
v-model="pwdForm.password"
class="form-input"
:password="true"
placeholder="请输入旧密码"
placeholder-class="input-placeholder"
/>
</view>
<view class="pwd-field">
<text class="pwd-label">新密码</text>
<input
v-model="pwdForm.new_password"
class="form-input"
:password="true"
placeholder="请输入新密码(5-18 位)"
placeholder-class="input-placeholder"
/>
</view>
<view class="pwd-field">
<text class="pwd-label">确认新密码</text>
<input
v-model="pwdForm.new_password_confirmation"
class="form-input"
:password="true"
placeholder="请再次输入新密码"
placeholder-class="input-placeholder"
/>
</view>
<u-button
:throttle-time="0"
shape="circle"
:loading="saving"
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff', height: '88rpx', marginTop: '40rpx' }"
@click="savePassword"
>确认修改</u-button>
</view>
</drawer-page-container>
</view>
</template>
<script>
/**
* 共享个人中心页:按 loginMode 解析四端上下文(API/缓存 key/角色兜底文案)
* 头像流程:chooseImage → 压缩 → 内容安全检查 → {end}-upload/image → update-my-profile
* 所有接口均为 xk-api(code/result 形态),API 封装已统一解包为 { data, ok }
*/
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue';
import { resolveProfileContext, applyProfileToCache } from '@/subPackages/sub_business_shared/common/profileContext.js';
import { uploadProfileAvatarApi } from '@/api/upload.js';
import { prepareImagePath } from '@/common/js/image-compress.js';
import { checkImage, handleSecurityError, SCENE_PROFILE } from '@/common/js/content-security.js';
export default {
components: { DrawerPageContainer },
data() {
return {
/** 当前端上下文(onLoad 解析一次,页面生命周期内 loginMode 不会变) */
ctx: null,
/** 个人资料(my-profile 实时接口返回) */
profile: {},
nickDrawerVisible: false,
nickForm: { nick_name: '' },
pwdDrawerVisible: false,
pwdForm: { password: '', new_password: '', new_password_confirmation: '' },
saving: false,
};
},
computed: {
/** 文字头像兜底:昵称首字 */
avatarText() {
const name = this.profile.nick_name || '用';
return name.slice(0, 1);
},
/** 只读信息行:可选字段(门店/工号/最后登录)有值才展示 */
readonlyRows() {
const p = this.profile || {};
const rows = [
{ label: '登录账号', value: p.login_account || '-' },
{ label: '手机号', value: p.phone || '-' },
{ label: '角色', value: p.role_name || (this.ctx && this.ctx.roleFallback) || '-' },
];
if (p.store_name) rows.push({ label: '所属门店', value: p.store_name });
if (p.job_number) rows.push({ label: '工号', value: p.job_number });
if (p.last_login_time) rows.push({ label: '最后登录', value: p.last_login_time });
return rows;
},
},
onLoad() {
this.ctx = resolveProfileContext();
this.loadProfile();
},
methods: {
/** 拉取实时资料(xk-api my-profile,封装已解包 { data, ok };失败静默保留空态) */
async loadProfile() {
try {
const { data, ok } = await this.ctx.getMyProfile();
if (ok && data) {
this.profile = data;
}
} catch (e) {
console.warn('拉取个人资料失败', e);
}
},
/**
* 更换头像:选图 → 压缩(大图省流量)→ 内容安全检查 → 上传各端 image 路由 → 保存资料
* 上传成功即调 update-my-profile 落库并刷新 Redis 会话,无需用户再点保存
*/
changeAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
uni.showLoading({ title: '上传中...', mask: true });
try {
const tempFile = res.tempFiles[0];
const prepared = await prepareImagePath({ path: tempFile.path, size: tempFile.size });
if (!prepared) {
uni.hideLoading();
return;
}
try {
await checkImage(prepared.path, SCENE_PROFILE);
} catch (error) {
uni.hideLoading();
handleSecurityError(error);
return;
}
const upload = await uploadProfileAvatarApi(prepared.path);
if (!upload.ok) {
uni.hideLoading();
uni.showToast({ icon: 'none', title: upload.message || '上传失败' });
return;
}
// saveProfile 失败时内部已 toast 具体原因,这里必须中断,避免弹出误导的成功提示
const saved = await this.saveProfile({ nick_name: this.profile.nick_name, avatar: upload.url });
uni.hideLoading();
if (saved) {
uni.showToast({ icon: 'success', title: '头像已更新' });
}
} catch (e) {
uni.hideLoading();
uni.showToast({ icon: 'none', title: (e && e.msg) || '上传失败' });
}
},
});
},
/** 打开昵称编辑抽屉(回填当前昵称) */
openNickDrawer() {
this.nickForm.nick_name = this.profile.nick_name || '';
this.nickDrawerVisible = true;
},
/** 保存昵称(非空 + 长度校验,后端还有全表唯一校验) */
async saveNickName() {
const nickName = (this.nickForm.nick_name || '').trim();
if (!nickName) {
uni.showToast({ icon: 'none', title: '请输入昵称' });
return;
}
if (nickName.length > 20) {
uni.showToast({ icon: 'none', title: '昵称最长 20 个字' });
return;
}
this.saving = true;
try {
const ok = await this.saveProfile({ nick_name: nickName, avatar: this.profile.avatar || '' });
if (ok) {
this.nickDrawerVisible = false;
uni.showToast({ icon: 'success', title: '保存成功' });
}
} finally {
this.saving = false;
}
},
/**
* 提交资料保存(昵称/头像共用):成功后用返回的最新资料刷新页面并写回各端缓存
* @returns {Promise<boolean>} 是否保存成功
*/
async saveProfile(payload) {
try {
const { data, ok, res } = await this.ctx.updateMyProfile(payload);
if (!ok) {
uni.showToast({ icon: 'none', title: (res && res.message) || '保存失败' });
return false;
}
if (data) {
this.profile = data;
}
applyProfileToCache(this.ctx, this.profile);
return true;
} catch (e) {
// 网络异常等 reject:拦截器已统一 toast,这里只保证流程返回失败
return false;
}
},
/** 打开修改密码抽屉(每次清空表单,避免残留上次输入) */
openPwdDrawer() {
this.pwdForm = { password: '', new_password: '', new_password_confirmation: '' };
this.pwdDrawerVisible = true;
},
/** 提交改密:前端做基础校验,旧密码是否正确由后端校验 */
async savePassword() {
const { password, new_password, new_password_confirmation } = this.pwdForm;
if (!password) {
uni.showToast({ icon: 'none', title: '请输入旧密码' });
return;
}
// 与 PC 端 passwordModalForm 同口径:5-18 位(后端 admin/update-password 共用)
if (!new_password || new_password.length < 5 || new_password.length > 18) {
uni.showToast({ icon: 'none', title: '新密码需 5-18 位' });
return;
}
if (new_password !== new_password_confirmation) {
uni.showToast({ icon: 'none', title: '两次密码不一致' });
return;
}
this.saving = true;
try {
const { ok, res } = await this.ctx.updateMyPassword({
password,
new_password,
new_password_confirmation,
});
if (!ok) {
uni.showToast({ icon: 'none', title: (res && res.message) || '修改失败' });
return;
}
this.pwdDrawerVisible = false;
uni.showToast({ icon: 'success', title: '修改成功' });
} catch (e) {
// 网络异常等 reject:拦截器已统一 toast,这里只复位提交态
} finally {
this.saving = false;
}
},
},
};
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$theme-dark: #4db6a8;
.profile-page {
min-height: 100vh;
background: #f5f7f8;
}
.profile-body {
padding: 24rpx 24rpx 60rpx;
}
.panel {
background: #fff;
border-radius: 16rpx;
padding: 0 32rpx;
margin-bottom: 24rpx;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 1rpx solid #f2f3f5;
}
.row-last {
border-bottom: none;
}
.row-hover {
opacity: 0.7;
}
.row-label {
font-size: 30rpx;
color: #31353d;
flex-shrink: 0;
}
.row-value {
display: flex;
align-items: center;
min-width: 0;
margin-left: 24rpx;
}
.value-text {
font-size: 28rpx;
color: #4e5969;
margin-right: 8rpx;
max-width: 420rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.value-readonly {
color: #86909c;
margin-right: 0;
}
.avatar-img {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
margin-right: 8rpx;
background: #f2f3f5;
}
.avatar-circle {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
color: #fff;
font-size: 40rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
}
/* 抽屉表单 */
.drawer-form {
padding: 32rpx 32rpx 48rpx;
}
.pwd-field {
margin-bottom: 24rpx;
}
.pwd-label {
display: block;
font-size: 28rpx;
color: #31353d;
margin-bottom: 12rpx;
}
.form-input {
height: 88rpx;
background: #f7f8fa;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1d2129;
}
.input-placeholder {
color: #c9cdd4;
}
</style>

View File

@@ -13,35 +13,30 @@
</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!list.length" class="empty">暂无用户</view>
<view
<!-- 会员列表卡:统一 xk-list-card 规范(头像 + 昵称/ID/手机分层 + 就诊人标签 + 箭头) -->
<xk-list-card
v-for="item in list"
:key="item.id"
class="card"
:avatar="avatarSrc(item.avatarurl)"
:title="item.nickname || '—'"
:arrow="true"
@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>
<template #body>
<text class="card-sub">ID:{{ item.id || '—' }}<template v-if="item.mobile"> · {{ item.mobile }}</template></text>
<!-- 就诊人标签化:一行多枚、自动换行 -->
<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>
<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 v-else class="patients empty-p">暂无就诊人</view>
</template>
</xk-list-card>
</view>
</business-page-layout>
</template>
@@ -121,22 +116,15 @@ export default {
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; }
/* 卡内副文案行(body 插槽自带样式) */
.card-sub { display: block; margin-top: 4rpx; font-size: 22rpx; color: #9ca3af; }
.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;
padding: 6rpx 16rpx; border-radius: 20rpx;
background: #f0f9f7; border: 1rpx solid #c2ece2;
}
.tag-text { font-size: 22rpx; color: #1a9b88; line-height: 1.4; }
.empty-p { font-size: 22rpx; color: #d1d5db; }

View File

@@ -1,9 +1,17 @@
/**
* 诊所管理员端 tabbar 配置(clinic-page-layout 与 sub_salesperson_manage/PageLayout 共用)
* 三 tab:工作台 / 功能 / 我的;图标统一用 uView 图标名(与平台/业务员/推广员三端一致,去掉旧 PNG 依赖)
*/
export const TAB_WORKBENCH = 0
export const TAB_MY = 1
const ROOT = '/subPackages/sub_clinic_salesperson';
export const TAB_FEATURES = 1
export const TAB_MY = 2
const ROOT = '/subPackages/sub_clinic_admin'
export const CLINIC_TAB_ROUTES = [
'/subPackages/sub_clinic_admin/home/index',
'/subPackages/sub_clinic_admin/my/index',
`${ROOT}/home/index`,
`${ROOT}/features/index`,
`${ROOT}/my/index`,
]
export function getClinicTabList() {
@@ -14,6 +22,12 @@ export function getClinicTabList() {
text: '工作台',
pagePath: `${ROOT}/home/index`,
},
{
iconPath: 'grid',
selectedIconPath: 'grid-fill',
text: '功能',
pagePath: `${ROOT}/features/index`,
},
{
iconPath: 'account',
selectedIconPath: 'account-fill',

View File

@@ -10,10 +10,12 @@
<view class="clinic-body" :style="bodyStyle">
<slot />
</view>
<!-- 高度显式传 rpx(100 与 body 预留一致):默认 50px 固定像素在宽屏机型会与内部 rpx 定位的 icon/文字冲突挤压 -->
<u-tabbar
v-if="showTabbar"
:value="tabIndex"
:list="tabList"
:height="100"
active-color="#6ACDBB"
@change="onTabChange"
/>

View File

@@ -1,31 +1,41 @@
<template>
<!-- 会员卡片:点击进入会员详情(开通信息/开通记录) -->
<!-- 会员卡片:黑金质感卡面,点击进入会员详情(开通信息/开通记录) -->
<view class="vip-card" @click="onTap">
<view class="vip-card-inner">
<!-- 卡面装饰层:右上金色光晕 + 斜切高光,营造实体卡光泽 -->
<view class="vip-glow"></view>
<view class="vip-sheen"></view>
<view class="vip-badge-combo">
<image
v-if="levelBadge"
class="vip-level"
:src="levelBadge"
mode="aspectFit"
/>
<view class="vip-badge-ring">
<image
v-if="levelBadge"
class="vip-level"
:src="levelBadge"
mode="aspectFit"
/>
<view v-if="!levelBadge" class="vip-badge-placeholder">
<text class="vip-badge-placeholder-text">V</text>
</view>
</view>
<image
v-if="durationBadge"
class="vip-ribbon"
:src="durationBadge"
mode="aspectFit"
/>
<view v-if="!durationBadge && !levelBadge" class="vip-badge-placeholder">
<text class="vip-badge-placeholder-text">V</text>
</view>
</view>
<view class="vip-info">
<text class="vip-title">{{ levelName }}</text>
<text class="vip-sub">{{ durationLabel }}</text>
<view class="vip-title-row">
<text class="vip-title">{{ levelName }}</text>
<text class="vip-duration-chip">{{ durationLabel }}</text>
</view>
<text class="vip-expire">{{ expireText }}</text>
<text v-if="hasOpenInfo && openTypeText" class="vip-meta">开通方式:{{ openTypeText }}</text>
<text v-if="hasOpenInfo && operatorText" class="vip-meta">开通人:{{ operatorText }}</text>
<text class="vip-hint">点击查看会员详情</text>
<view class="vip-hint-row">
<text class="vip-hint">查看会员详情</text>
<text class="vip-hint-arrow">›</text>
</view>
</view>
</view>
</view>
@@ -110,87 +120,152 @@ export default {
</script>
<style lang="scss" scoped>
/* 黑金会员卡:深色曜石底 + 香槟金文字 + 光泽装饰,参考实体贵宾卡质感 */
.vip-card {
margin: 0 24rpx 24rpx;
}
.vip-card-inner {
position: relative;
display: flex;
align-items: center;
padding: 36rpx 32rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #2c3e50 0%, #4a6741 55%, #6acdbb 100%);
box-shadow: 0 8rpx 24rpx rgba(44, 62, 80, 0.18);
border-radius: 24rpx;
overflow: hidden;
/* 深色底叠加暗金光晕:比单一渐变更有层次 */
background:
radial-gradient(120% 160% at 100% 0%, rgba(212, 175, 55, 0.22) 0%, rgba(212, 175, 55, 0) 45%),
linear-gradient(135deg, #23201a 0%, #12100c 55%, #1f1a12 100%);
border: 1rpx solid rgba(220, 186, 110, 0.35);
box-shadow:
0 12rpx 32rpx rgba(0, 0, 0, 0.28),
0 2rpx 8rpx rgba(212, 175, 55, 0.12);
}
/* 组合徽标容器:等级在下,丝带叠在中下部 */
/* 右上角金色光晕:模拟灯光打在卡面上的高光 */
.vip-glow {
position: absolute;
top: -90rpx;
right: -60rpx;
width: 260rpx;
height: 260rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(240, 217, 168, 0.28) 0%, rgba(240, 217, 168, 0) 70%);
pointer-events: none;
}
/* 斜切高光条:静态流光,增加金属光泽感 */
.vip-sheen {
position: absolute;
top: 0;
bottom: 0;
left: 30%;
width: 120rpx;
transform: skewX(-24deg);
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.05) 50%,
rgba(255, 255, 255, 0) 100%
);
pointer-events: none;
}
/* 组合徽标容器:金环衬底,等级徽标居中,期限丝带叠在下缘 */
.vip-badge-combo {
position: relative;
width: 160rpx;
height: 160rpx;
width: 168rpx;
height: 168rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.vip-badge-ring {
width: 148rpx;
height: 148rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle, rgba(212, 175, 55, 0.16) 0%, rgba(212, 175, 55, 0.04) 100%);
border: 1rpx solid rgba(220, 186, 110, 0.4);
box-shadow: inset 0 0 20rpx rgba(212, 175, 55, 0.15);
}
.vip-level {
width: 140rpx;
height: 140rpx;
position: relative;
z-index: 1;
width: 128rpx;
height: 128rpx;
}
.vip-ribbon {
position: absolute;
left: 50%;
bottom: 10rpx;
bottom: 4rpx;
transform: translateX(-50%);
width: 200rpx;
height: 72rpx;
z-index: 2;
}
.vip-badge-placeholder {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.vip-badge-placeholder-text {
color: #fff;
font-size: 52rpx;
color: #f0d9a8;
font-size: 56rpx;
font-weight: 700;
text-shadow: 0 2rpx 8rpx rgba(212, 175, 55, 0.4);
}
.vip-info {
margin-left: 24rpx;
position: relative;
margin-left: 28rpx;
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.vip-title {
color: #ffffff;
font-size: 34rpx;
font-weight: 700;
margin-bottom: 8rpx;
.vip-title-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.vip-sub {
color: rgba(255, 255, 255, 0.88);
font-size: 24rpx;
margin-bottom: 8rpx;
.vip-title {
color: #f5e3bd;
font-size: 36rpx;
font-weight: 700;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.35);
}
/* 期限徽章:描边金字小胶囊,替代原来的普通文本行 */
.vip-duration-chip {
margin-left: 14rpx;
padding: 2rpx 14rpx;
font-size: 20rpx;
color: #e8cf9a;
border: 1rpx solid rgba(232, 207, 154, 0.5);
border-radius: 999rpx;
background: rgba(212, 175, 55, 0.1);
flex-shrink: 0;
}
.vip-expire {
color: rgba(255, 255, 255, 0.75);
color: rgba(240, 224, 190, 0.78);
font-size: 22rpx;
margin-bottom: 6rpx;
}
.vip-meta {
color: rgba(255, 255, 255, 0.82);
color: rgba(240, 224, 190, 0.6);
font-size: 22rpx;
line-height: 1.5;
}
.vip-hint-row {
display: flex;
align-items: center;
margin-top: 12rpx;
}
.vip-hint {
margin-top: 10rpx;
color: rgba(255, 255, 255, 0.7);
color: rgba(232, 207, 154, 0.72);
font-size: 20rpx;
}
.vip-hint-arrow {
margin-left: 4rpx;
color: rgba(232, 207, 154, 0.72);
font-size: 24rpx;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<clinic-page-layout title="功能" :show-tabbar="true" :tab-index="1">
<workbench-features ref="features" :api="featuresApi"></workbench-features>
</clinic-page-layout>
</template>
<script>
/**
* 诊所管理员端「功能」tab 薄壳页
* 壳页只负责:套本端 layout(navbar/tabbar 配置)+ 注入本端角色接口,
* 搜索/分组滑动/长按收藏/使用上报等全部逻辑在主包共享组件 workbench-features(四端复用)
*/
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import {
getClinicAdminWorkbenchEntries,
getClinicAdminEntryUserData,
toggleClinicAdminFavoriteEntry,
reportClinicAdminEntryUsage,
} from '@/api/clinicAdmin.js'
export default {
components: { ClinicPageLayout },
data() {
return {
/** 注入共享组件的本端接口集合(统一返回 { res, data, ok }) */
featuresApi: {
getEntries: getClinicAdminWorkbenchEntries,
getUserData: getClinicAdminEntryUserData,
toggleFavorite: toggleClinicAdminFavoriteEntry,
reportUsage: reportClinicAdminEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在)
if (this.$refs.features) this.$refs.features.refreshUserData()
},
}
</script>

View File

@@ -27,73 +27,75 @@
</view>
</view>
<!-- 主体内容区:快捷服务(多彩微拟物双列卡片) -->
<view class="content-wrapper">
<view v-for="section in sections" :key="section.key" class="section-panel">
<view class="panel-header">
<text class="panel-title">{{ section.title }}</text>
<!-- 经营看板:hero 下沉浮卡,今日/本月经营数据一览(对齐医生端看板视觉) -->
<view class="dashboard-wrapper">
<view class="dashboard-card">
<view class="stat-item">
<text class="stat-label">今日订单</text>
<view class="stat-value-group">
<text class="stat-value">{{ dashboard.today_order_count }}</text>
<text class="stat-unit">单</text>
</view>
</view>
<view class="service-grid">
<view
v-for="item in section.items"
:key="item.key"
class="service-card"
hover-class="service-card-hover"
@click="go(item.path)"
>
<view class="service-icon-box" :style="{ background: item.theme.bg }">
<u-icon :name="item.icon" size="38" :color="item.theme.color"></u-icon>
</view>
<view class="service-info">
<text class="service-name">{{ item.label }}</text>
<text class="service-desc">{{ item.desc || '快捷进入' }}</text>
</view>
<view class="stat-item">
<text class="stat-label">今日营业额</text>
<view class="stat-value-group">
<text class="stat-value highlight">{{ dashboard.today_order_amount }}</text>
<text class="stat-unit">元</text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">本月订单</text>
<view class="stat-value-group">
<text class="stat-value">{{ dashboard.month_order_count }}</text>
<text class="stat-unit">单</text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">本月营业额</text>
<view class="stat-value-group">
<text class="stat-value highlight">{{ dashboard.month_order_amount }}</text>
<text class="stat-unit">元</text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">待发货</text>
<view class="stat-value-group">
<text class="stat-value" :class="{ warn: dashboard.wait_send_count > 0 }">{{ dashboard.wait_send_count }}</text>
<text class="stat-unit">单</text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">下单会员</text>
<view class="stat-value-group">
<text class="stat-value">{{ dashboard.member_count }}</text>
<text class="stat-unit">人</text>
</view>
</view>
</view>
</view>
<!-- 主体内容区:个人功能区(最近使用 / 我的常用 / 更多功能,全部入口收敛到功能 tab) -->
<view class="content-wrapper">
<workbench-user-entries
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
@item-click="onEntryTap"
@more="goFeatures"
></workbench-user-entries>
</view>
</view>
</clinic-page-layout>
</template>
<script>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import { getWarehouseProductTypeOptions, getClinicAdminStoreQr } from '@/api/clinicAdmin.js'
import { getClinicAdminStoreQr, getClinicAdminDashboardStats, getClinicAdminEntryUserData, reportClinicAdminEntryUsage } from '@/api/clinicAdmin.js'
import { openQrCodePreview, buildClinicQrPayload } from '@/utils/qrCodePreview.js'
// 全局主题色池
const GLOBAL_THEMES = [
{ bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' }, // 橙金
{ bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' }, // 科技蓝
{ bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' }, // 优雅紫
{ bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' }, // 安全绿
]
// 补充了 desc 字段以适配新的 UI
const FINANCE_MENUS = [
{ key: 'withdrawal', label: '提现管理', desc: '提现流水查询', icon: 'red-packet-fill', theme: GLOBAL_THEMES[0], path: '/subPackages/sub_clinic_admin/withdrawal/index' },
{ key: 'reconciliation', label: '对账单', desc: '财务对账明细', icon: 'file-text-fill', theme: GLOBAL_THEMES[1], path: '/subPackages/sub_clinic_admin/reconciliation/index' },
]
const BUSINESS_MENUS = [
{ 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 = [
{ key: 'salesperson-manage', label: '推广员管理', desc: '添加与结算推广员', icon: 'account-fill', theme: GLOBAL_THEMES[2], path: '/subPackages/sub_salesperson_manage/index?mode=clinic' },
]
const WAREHOUSE_FALLBACK = {
key: 'warehouse',
label: '仓库管理',
desc: '库存管理中心',
icon: 'car-fill',
theme: { bg: 'linear-gradient(135deg, #F3F4F6, #E5E7EB)', color: '#6B7280' }, // 占位灰
path: '/subPackages/sub_clinic_admin/warehouse/index',
}
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
export default {
components: { ClinicPageLayout },
@@ -104,32 +106,101 @@ export default {
storeQrData: null,
/** 门店类型 0诊所 1药店 */
storeType: 0,
warehouseMenus: [WAREHOUSE_FALLBACK],
/** 我的常用/最近使用原始入口(归一化后存储,行为装饰在 computed 注入保证角标响应式) */
favRaw: [],
recentRaw: [],
/** 卡片形态偏好:true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
/** 经营看板数据(接口返回前展示 0,避免布局跳动) */
dashboard: {
today_order_count: 0,
today_order_amount: 0,
month_order_count: 0,
month_order_amount: 0,
wait_send_count: 0,
member_count: 0,
pending_withdrawal_count: 0,
pending_withdrawal_amount: 0,
},
}
},
computed: {
storeKindLabel() {
return Number(this.storeType) === 1 ? '药店' : '诊所'
},
sections() {
return [
{ key: 'finance', title: '财务管理', items: FINANCE_MENUS },
{ key: 'business', title: '业务管理', items: BUSINESS_MENUS },
{ key: 'salesperson', title: '门店推广员', items: SALESPERSON_MENUS },
{ key: 'warehouse', title: '仓库管理', items: this.warehouseMenus },
]
/** 我的常用(角标等行为装饰在这里注入,保证看板数据返回后角标响应式更新) */
favItems() {
return this.favRaw.map((item) => this.decorateItem(item))
},
/** 最近使用 */
recentItems() {
return this.recentRaw.map((item) => this.decorateItem(item))
},
},
onShow() {
// 卡片形态偏好每次 onShow 重读:我的页切换后返回立即生效
this.cardLong = getCardStyle() === 'long'
this.loadDashboard()
this.loadUserEntries()
const user = uni.getStorageSync('clinic_admin_user') || {}
this.storeName = user.store_name || (user.store && user.store.name) || user.nick_name || '门店管理'
if (user.store && user.store.type != null) {
this.storeType = Number(user.store.type) === 1 ? 1 : 0
}
this.loadStoreQr()
this.loadWarehouseMenus()
},
methods: {
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底,功能不可达时去功能 tab) */
async loadUserEntries() {
try {
const wrap = await getClinicAdminEntryUserData()
if (wrap && wrap.ok && wrap.data) {
this.favRaw = this.normalizeList(wrap.data.favorites)
this.recentRaw = this.normalizeList(wrap.data.recents)
}
} catch (e) {
console.error('常用/最近使用加载失败', e)
}
},
/** 后端入口对象数组 → 渲染项数组(与功能页同一映射) */
normalizeList(list) {
if (!Array.isArray(list)) return []
const items = []
list.forEach((entry) => {
const item = normalizeEntry(entry)
if (item) items.push(item)
})
return items
},
/** 按 code 注入本端行为:提现管理带待审核角标 */
decorateItem(item) {
if (item.key === 'clinic_admin_withdrawal') {
return { ...item, badge: this.dashboard.pending_withdrawal_count }
}
return item
},
/** 入口点击:静默上报使用(保持最近使用新鲜)→ 跳转 */
onEntryTap(item) {
reportClinicAdminEntryUsage(item.key).catch(() => {})
if (item.path) {
uni.navigateTo({ url: item.path })
}
},
/** 更多功能/空态引导:切到功能 tab(tabbar 页统一走 reLaunch) */
goFeatures() {
uni.reLaunch({ url: '/subPackages/sub_clinic_admin/features/index' })
},
/** 拉取经营看板统计:失败静默保持 0,不阻塞首页其他模块 */
async loadDashboard() {
try {
const wrap = await getClinicAdminDashboardStats()
if (wrap && wrap.ok && wrap.data) {
this.dashboard = { ...this.dashboard, ...wrap.data }
}
} catch (e) {
console.error('看板统计加载失败', e)
}
},
async loadStoreQr() {
this.storeQrCode = ''
this.storeQrData = null
@@ -152,35 +223,6 @@ export default {
const { type, payload } = buildClinicQrPayload(this.storeQrData)
openQrCodePreview({ type, payload })
},
loadWarehouseMenus() {
getWarehouseProductTypeOptions().then(w => {
const raw = w.data
const options = Array.isArray(raw) ? raw : []
const warehouseMenus = options.map((opt, index) => {
const value = opt.value != null ? opt.value : opt.id
const label = opt.label || opt.name || '仓库'
const title = label.indexOf('仓库') >= 0 ? label : label
// 为动态仓库分配不同的主题色
const theme = GLOBAL_THEMES[index % GLOBAL_THEMES.length]
return {
key: 'warehouse-' + value,
label: title,
desc: `${title}库存管理`, // 增强描述
icon: 'car-fill', // 统一使用车辆图标代表仓库物流
theme: theme,
path: '/subPackages/sub_clinic_admin/warehouse/index?type=' + encodeURIComponent(value) + '&label=' + encodeURIComponent(title),
}
})
this.warehouseMenus = warehouseMenus.length ? warehouseMenus : [WAREHOUSE_FALLBACK]
}).catch(() => {
this.warehouseMenus = [WAREHOUSE_FALLBACK]
})
},
go(path) {
uni.navigateTo({ url: path })
},
},
}
</script>
@@ -271,78 +313,66 @@ export default {
border-radius: 20rpx;
}
/* ================= 主体内容区:快捷服务 ================= */
/* ================= 经营看板:hero 下沉浮白卡(统一工作台看板规格) ================= */
.dashboard-wrapper {
margin: -30rpx 30rpx 0;
position: relative;
z-index: 11;
}
.dashboard-card {
background: #fff;
border-radius: 24rpx;
padding: 32rpx 20rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
row-gap: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-label {
font-size: 22rpx;
color: #94A3B8;
margin-bottom: 10rpx;
}
.stat-value-group {
display: flex;
align-items: baseline;
}
/* 数值:DIN 数字字体统一看板视觉语言 */
.stat-value {
font-size: 38rpx;
font-weight: 700;
color: #1E293B;
font-family: "DIN Alternate", sans-serif;
}
.stat-value.highlight {
color: #059669;
}
.stat-value.warn {
color: #F97316;
}
.stat-unit {
font-size: 20rpx;
color: #94A3B8;
margin-left: 4rpx;
}
/* ================= 主体内容区:个人功能区(分区/宫格样式在 workbench-user-entries 组件内) ================= */
.content-wrapper {
margin-top: 30rpx; /* 轻微向上偏移,与顶部区域产生连接感 */
position: relative;
z-index: 10;
}
.section-panel {
margin: 0 30rpx 30rpx;
background: transparent;
}
.panel-header {
margin-bottom: 24rpx;
padding: 0 10rpx;
}
.panel-title {
font-size: 34rpx;
font-weight: 700;
color: #1E293B; /* 深石板灰 */
}
/* 宫格卡片布局:与平台端/推广员端一致的两列 */
.service-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.service-card {
background: #FFFFFF;
border-radius: 28rpx;
padding: 30rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
transition: all 0.2s ease;
border: 1px solid rgba(0, 0, 0, 0.01);
}
.service-card-hover {
transform: translateY(-2rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.service-icon-box {
width: 76rpx;
height: 76rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.5); /* 顶部微高光,增加拟物质感 */
}
.service-info {
display: flex;
flex-direction: column;
}
.service-name {
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
margin-bottom: 6rpx;
}
.service-desc {
font-size: 20rpx;
color: #94A3B8; /* 银灰色 */
}
</style>

View File

@@ -1,76 +1,53 @@
<template>
<clinic-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container">
<!-- 资料区:圆形头像 + 姓名 + 角色(对齐医生「我的」) -->
<view class="profile-panel">
<view class="avatar-circle">
<text>{{ avatarText }}</text>
</view>
<view class="user-info">
<text class="user-name">{{ displayName }}</text>
<view class="role-badge">
<text class="role-text">诊所管理员</text>
</view>
<text v-if="storeName && storeName !== '-'" class="store-line">{{ storeName }}</text>
</view>
</view>
<!-- 会员卡片:功能列表入口上方 -->
<vip-member-card :vip="vipInfo" />
<!-- 白色菜单卡:绑定微信 + 切换账号 -->
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
</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>
<clinic-page-layout title="我的" :show-tabbar="true" :tab-index="2">
<!-- 页面主体统一收进 admin-my-panel(资料区 + 个人中心入口 + 菜单卡 + 退出) -->
<admin-my-panel
ref="myPanel"
:display-name="displayName"
:avatar="avatar"
role-text="诊所管理员"
:store-name="storeName"
fallback-char="管"
>
<!-- 会员卡片:资料区与功能列表之间(仅诊所管理员端有) -->
<template #after-profile>
<vip-member-card :vip="vipInfo" />
</template>
</admin-my-panel>
</clinic-page-layout>
</template>
<script>
/**
* 诊所/门店管理员「我的」:布局对齐医生(菜单卡 + 绿色退出)
* 资料区与功能入口之间展示门店 VIP 会员卡片
* 诊所/门店管理员「我的」:主体复用 admin-my-panel 共享组件
* 本页只保留数据加载(本地缓存 + my-info 刷新 VIP),展示与退出逻辑都在组件内
*/
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import VipMemberCard from '@/subPackages/sub_clinic_admin/components/vip-member-card.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getClinicAdminMyInfo } from '@/api/clinicAdmin.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { ClinicPageLayout, VipMemberCard, AccountSwitchEntry, WxBindEntry },
components: { ClinicPageLayout, VipMemberCard, AdminMyPanel },
data() {
return {
displayName: '',
avatar: '',
storeName: '',
vipInfo: null,
}
},
computed: {
avatarText() {
const n = this.displayName || '管'
return n.slice(0, 1)
},
},
onShow() {
this.loadLocalUser()
this.refreshVip()
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
methods: {
/** 先读本地缓存,保证首屏有基础信息 */
/** 先读本地缓存,保证首屏有基础信息(个人中心改完昵称/头像返回时也走这里立即生效) */
loadLocalUser() {
const user = uni.getStorageSync('clinic_admin_user') || {}
this.displayName = user.nick_name || user.name || user.account || '管理员'
this.avatar = user.avatar || ''
this.storeName = user.store_name || (user.store && user.store.name) || '-'
if (user.vip) {
this.vipInfo = user.vip
@@ -88,6 +65,9 @@ export default {
if (info.nick_name || info.name) {
this.displayName = info.nick_name || info.name || this.displayName
}
if (info.avatar) {
this.avatar = info.avatar
}
if (info.store_name) {
this.storeName = info.store_name
}
@@ -101,91 +81,6 @@ export default {
console.warn('刷新诊所管理员VIP失败', e)
}
},
logout() {
uni.showModal({
title: '提示',
content: '确定退出登录?',
confirmColor: '#6ACDBB',
success: (res) => {
if (!res.confirm) return
clearLoginStorage()
uni.reLaunch({ url: '/pages/login/index' })
},
})
},
},
}
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$theme-dark: #4db6a8;
$color-page-bg: #F5F7F8;
$color-text-title: #222B2A;
$color-text-muted: #86909C;
.mine-container {
padding: 0 0 60rpx;
background-color: $color-page-bg;
min-height: 100vh;
box-sizing: border-box;
}
.profile-panel {
display: flex;
align-items: center;
background-color: #ffffff;
padding: 60rpx 40rpx 50rpx;
margin-bottom: 24rpx;
}
.avatar-circle {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.25);
color: #ffffff;
font-size: 52rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.user-info {
margin-left: 32rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.user-name {
font-size: 40rpx;
font-weight: 700;
color: $color-text-title;
margin-bottom: 12rpx;
}
.role-badge {
align-self: flex-start;
background-color: rgba(106, 205, 187, 0.1);
border: 1rpx solid rgba(106, 205, 187, 0.2);
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.role-text {
font-size: 22rpx;
color: $theme-dark;
font-weight: 600;
}
.store-line {
margin-top: 12rpx;
font-size: 26rpx;
color: $color-text-muted;
}
.setting-panel {
background-color: #ffffff;
margin-bottom: 24rpx;
padding: 8rpx 40rpx 16rpx;
}
.logout-wrap {
padding: 0 40rpx 32rpx;
}
</style>

View File

@@ -1,28 +1,34 @@
<template>
<clinic-page-layout title="会员详情" :show-back="true">
<view class="vip-detail-page">
<!-- 当前会员信息卡 -->
<!-- 当前会员信息卡:黑金质感,与「我的」页会员卡呼应 -->
<view class="vip-summary">
<view class="vip-glow"></view>
<view class="vip-sheen"></view>
<view class="vip-badge-combo">
<image
v-if="levelBadge"
class="vip-level"
:src="levelBadge"
mode="aspectFit"
/>
<view class="vip-badge-ring">
<image
v-if="levelBadge"
class="vip-level"
:src="levelBadge"
mode="aspectFit"
/>
<view v-if="!levelBadge" class="vip-badge-placeholder">
<text class="vip-badge-placeholder-text">V</text>
</view>
</view>
<image
v-if="durationBadge"
class="vip-ribbon"
:src="durationBadge"
mode="aspectFit"
/>
<view v-if="!durationBadge && !levelBadge" class="vip-badge-placeholder">
<text class="vip-badge-placeholder-text">V</text>
</view>
</view>
<view class="vip-summary-info">
<text class="vip-summary-title">{{ levelText }}</text>
<text class="vip-summary-sub">{{ durationLabel }}</text>
<view class="vip-summary-title-row">
<text class="vip-summary-title">{{ levelText }}</text>
<text class="vip-summary-chip">{{ durationLabel }}</text>
</view>
<text class="vip-summary-expire">{{ expireText }}</text>
</view>
</view>
@@ -297,71 +303,125 @@ $color-text-muted: #86909c;
background: $color-page-bg;
box-sizing: border-box;
}
/* 黑金头卡:与「我的」页 vip-member-card 同一套视觉语言 */
.vip-summary {
position: relative;
display: flex;
align-items: center;
padding: 36rpx 32rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #2c3e50 0%, #4a6741 55%, #6acdbb 100%);
box-shadow: 0 8rpx 24rpx rgba(44, 62, 80, 0.18);
border-radius: 24rpx;
overflow: hidden;
background:
radial-gradient(120% 160% at 100% 0%, rgba(212, 175, 55, 0.22) 0%, rgba(212, 175, 55, 0) 45%),
linear-gradient(135deg, #23201a 0%, #12100c 55%, #1f1a12 100%);
border: 1rpx solid rgba(220, 186, 110, 0.35);
box-shadow:
0 12rpx 32rpx rgba(0, 0, 0, 0.28),
0 2rpx 8rpx rgba(212, 175, 55, 0.12);
margin-bottom: 24rpx;
}
.vip-glow {
position: absolute;
top: -90rpx;
right: -60rpx;
width: 260rpx;
height: 260rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(240, 217, 168, 0.28) 0%, rgba(240, 217, 168, 0) 70%);
pointer-events: none;
}
.vip-sheen {
position: absolute;
top: 0;
bottom: 0;
left: 30%;
width: 120rpx;
transform: skewX(-24deg);
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.05) 50%,
rgba(255, 255, 255, 0) 100%
);
pointer-events: none;
}
.vip-badge-combo {
position: relative;
width: 140rpx;
height: 140rpx;
width: 148rpx;
height: 148rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.vip-badge-ring {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle, rgba(212, 175, 55, 0.16) 0%, rgba(212, 175, 55, 0.04) 100%);
border: 1rpx solid rgba(220, 186, 110, 0.4);
box-shadow: inset 0 0 20rpx rgba(212, 175, 55, 0.15);
}
.vip-level {
width: 120rpx;
height: 120rpx;
position: relative;
z-index: 1;
width: 110rpx;
height: 110rpx;
}
.vip-ribbon {
position: absolute;
left: 50%;
bottom: 8rpx;
bottom: 2rpx;
transform: translateX(-50%);
width: 170rpx;
height: 60rpx;
z-index: 2;
}
.vip-badge-placeholder {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.vip-badge-placeholder-text {
color: #fff;
color: #f0d9a8;
font-size: 48rpx;
font-weight: 700;
text-shadow: 0 2rpx 8rpx rgba(212, 175, 55, 0.4);
}
.vip-summary-info {
margin-left: 24rpx;
position: relative;
margin-left: 28rpx;
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.vip-summary-title {
color: #fff;
font-size: 32rpx;
font-weight: 700;
margin-bottom: 8rpx;
.vip-summary-title-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.vip-summary-title {
color: #f5e3bd;
font-size: 34rpx;
font-weight: 700;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.35);
}
.vip-summary-chip {
margin-left: 14rpx;
padding: 2rpx 14rpx;
font-size: 20rpx;
color: #e8cf9a;
border: 1rpx solid rgba(232, 207, 154, 0.5);
border-radius: 999rpx;
background: rgba(212, 175, 55, 0.1);
flex-shrink: 0;
}
.vip-summary-sub,
.vip-summary-expire {
color: rgba(255, 255, 255, 0.85);
color: rgba(240, 224, 190, 0.78);
font-size: 24rpx;
margin-bottom: 6rpx;
}
.info-panel {
background: #fff;

View File

@@ -88,9 +88,13 @@ $color-text-body: #4E5969;
$color-text-muted: #86909C;
$color-border: #F2F3F5;
/* iOS 卡片规格:与订单详情各面板一致(大圆角 + 发丝线 + 轻阴影) */
.express-panel {
background: #ffffff;
padding: 32rpx 40rpx;
border-radius: 32rpx;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 28rpx;
margin-bottom: 24rpx;
}

View File

@@ -2,15 +2,24 @@
<view>
<clinic-page-layout title="订单详情" :show-back="true">
<view class="detail-container">
<!-- 头部面板:通栏,展示金额与状态 -->
<!-- 状态头卡:状态大字(状态色区分)+ 实付金额 + 可复制单号 -->
<view v-if="info.id" class="detail-panel header-panel">
<view class="order-head">
<view class="status-head">
<view class="status-main">
<view class="status-dot" :style="{ background: statusColor }"></view>
<text class="status-text" :style="{ color: statusColor }">{{ detailExtra.statusText }}</text>
</view>
<view class="pay-group">
<text class="pay-label">实付</text>
<text class="pay-total">{{ money(info.total_pay_price) }}</text>
</view>
</view>
<view class="order-no-row">
<text class="order-no-main">订单号:{{ info.order_no }}</text>
<text class="pay-total">{{ money(info.total_pay_price) }}</text>
<view class="copy-btn" hover-class="copy-btn-hover" @tap="copyOrderNo"><text>复制</text></view>
</view>
<view class="tags">
<text class="modern-tag tag-status">{{ detailExtra.statusText }}</text>
<text class="modern-tag tag-type">{{ detailExtra.prescriptionTypeText }}</text>
<text class="modern-tag tag-delivery">{{ detailExtra.deliveryText }}</text>
<text v-if="detailExtra.isProxyPay" class="modern-tag tag-proxy">代支付</text>
@@ -285,12 +294,30 @@ export default {
},
canRefund() {
return this.info.status === 2 || this.info.status === 3
}
},
/** 订单状态色:待办橙 / 在途蓝 / 完成绿 / 退款红 / 取消灰(与列表状态语义一致) */
statusColor() {
const m = {
0: '#FF7D00', 1: '#FF7D00', 2: '#3B82F6', 3: '#00B42A', 4: '#F53F3F',
5: '#FF7D00', 6: '#00B42A', 7: '#00B42A', 8: '#F53F3F', 9: '#86909C',
}
return m[Number(this.info.status)] || '#86909C'
},
},
onLoad(q) {
this.id = Number(q.id || 0)
this.load()
},
/** 下拉刷新:重拉详情(含物流),完成后收起刷新动画 */
onPullDownRefresh() {
const done = () => uni.stopPullDownRefresh()
const p = this.load()
if (p && p.then) {
p.then(done, done)
} else {
done()
}
},
/** 离开页关闭处方抽屉,避免 page-container 残留遮罩导致列表白屏 */
onHide() {
this.closePrescriptionPopup()
@@ -321,12 +348,13 @@ export default {
this.patientDetailName = this.info.patient || ''
this.patientDetailVisible = true
},
/** 拉取订单详情;返回 Promise 供下拉刷新在完成后收起动画 */
load() {
if (!this.id) {
uni.showToast({ title: '缺少订单信息', icon: 'none' })
return
}
getOrderDetail(this.id).then(w => {
return getOrderDetail(this.id).then(w => {
if (!w.ok) {
uni.showToast({ title: '订单加载失败', icon: 'none' })
return
@@ -341,6 +369,13 @@ export default {
uni.showToast({ title: '订单加载失败', icon: 'none' })
})
},
/** 复制订单号(状态头卡复制按钮) */
copyOrderNo() {
uni.setClipboardData({
data: String(this.info.order_no || ''),
success: () => uni.showToast({ title: '订单号已复制', icon: 'none' }),
})
},
loadExpress() {
getExpressDetailByOrderId(this.id).then(w => {
if (w.ok) this.expressInfo = w.data
@@ -395,48 +430,110 @@ export default {
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$color-page-bg: #F5F7F8;
$color-text-title: #222B2A;
$color-page-bg: #F4F6F8;
$color-text-title: #1C1C1E;
$color-text-body: #4E5969;
$color-text-muted: #86909C;
$color-text-muted: #8E8E93;
$color-border: #F2F3F5;
$color-danger: #F53F3F;
.detail-container {
background-color: $color-page-bg;
min-height: 100vh;
/* 适配底部操作栏,留出极大的安全边距 */
padding-bottom: 180rpx;
/* iOS 卡片流:两侧留白 + 适配底部操作栏的安全边距 */
padding: 20rpx 24rpx 180rpx;
}
/* 通栏面板 */
/* iOS 卡片面板:大圆角 + 发丝线 + 轻阴影(与工作台卡片同规格) */
.detail-panel {
background: #ffffff;
padding: 32rpx 40rpx;
border-radius: 32rpx;
border: 1rpx solid rgba(60, 60, 67, 0.08);
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 28rpx;
margin-bottom: 24rpx;
}
.header-panel {
padding-top: 40rpx;
padding-top: 32rpx;
}
.order-head {
/* 状态头:左状态大字(色点 + 文案),右实付金额 */
.status-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.order-no-main {
font-size: 28rpx;
.status-main {
display: flex;
align-items: center;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.status-text {
font-size: 36rpx;
font-weight: 700;
}
.pay-group {
display: flex;
align-items: baseline;
}
.pay-label {
font-size: 22rpx;
color: $color-text-muted;
margin-right: 8rpx;
}
/* 单号行:灰底圆角条 + 复制按钮 */
.order-no-row {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(60, 60, 67, 0.04);
border-radius: 16rpx;
padding: 14rpx 20rpx;
margin-bottom: 24rpx;
}
.order-no-main {
font-size: 24rpx;
color: $color-text-muted;
flex: 1;
min-width: 0;
word-break: break-all;
}
.copy-btn {
flex-shrink: 0;
margin-left: 16rpx;
padding: 4rpx 20rpx;
border-radius: 20rpx;
border: 1rpx solid $theme-primary;
text {
font-size: 22rpx;
color: $theme-primary;
}
}
.copy-btn-hover {
opacity: 0.6;
}
.pay-total {
font-size: 40rpx;
color: $color-text-title;
font-weight: 700;
font-family: monospace, sans-serif;
font-family: "DIN Alternate", monospace, sans-serif;
}
.tags {
@@ -453,7 +550,6 @@ $color-danger: #F53F3F;
font-weight: 500;
}
.tag-status { background: rgba(106, 205, 187, 0.1); color: $theme-primary; }
.tag-type { background: rgba(230, 162, 60, 0.1); color: #e6a23c; }
.tag-delivery { background: rgba(91, 127, 214, 0.1); color: #5b7fd6; }
.tag-proxy { background: rgba(114, 46, 209, 0.1); color: #722ED1; }
@@ -638,6 +734,7 @@ $color-danger: #F53F3F;
left: 0;
right: 0;
background: #ffffff;
border-top: 1rpx solid rgba(60, 60, 67, 0.08);
padding: 24rpx 40rpx;
/* 自动撑起 iOS 底部安全距离 */
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));

View File

@@ -9,6 +9,12 @@ export function getClinicSalespersonContext() {
text: '工作台',
pagePath: `${ROOT}/home/index`,
},
{
iconPath: 'grid',
selectedIconPath: 'grid-fill',
text: '功能',
pagePath: `${ROOT}/features/index`,
},
{
iconPath: 'account',
selectedIconPath: 'account-fill',

View File

@@ -10,10 +10,12 @@
<view class="clinic-body" :style="bodyStyle">
<slot />
</view>
<!-- 高度显式传 rpx(100 与 body 预留一致):默认 50px 固定像素在宽屏机型会与内部 rpx 定位的 icon/文字冲突挤压 -->
<u-tabbar
v-if="showTabbar"
:value="tabIndex"
:list="tabList"
:height="100"
active-color="#6ACDBB"
@change="onTabChange"
/>

View File

@@ -1,45 +0,0 @@
<template>
<clinic-salesperson-page-layout title="我的收益" :show-back="true">
<view class="page">
<view class="card" v-for="item in cards" :key="item.label">
<text class="label">{{ item.label }}</text>
<text class="value">{{ item.value }}</text>
</view>
</view>
</clinic-salesperson-page-layout>
</template>
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
export default {
components: { ClinicSalespersonPageLayout },
data() {
return { cards: [] }
},
onLoad() {
this.loadData()
},
methods: {
async loadData() {
const wrap = await getClinicSalespersonMyInfo()
if (!wrap || !wrap.ok) return
const stats = wrap.data?.stats || {}
this.cards = [
{ label: '累计服务费', value: `¥${stats.money || '0.00'}` },
{ label: '待结算', value: `¥${stats.pending_amount || '0.00'}` },
{ label: '已结算', value: `¥${stats.settled_amount || '0.00'}` },
{ label: '累计获客', value: `${stats.user_number || 0} 人` },
]
},
},
}
</script>
<style lang="scss" scoped>
.page { padding: 24rpx; }
.card { background: #fff; border-radius: 16rpx; padding: 32rpx; margin-bottom: 20rpx; display: flex; justify-content: space-between; }
.label { color: #666; font-size: 28rpx; }
.value { font-size: 32rpx; font-weight: 600; color: #333; }
</style>

View File

@@ -0,0 +1,50 @@
<template>
<clinic-salesperson-page-layout title="功能" :show-tabbar="true" :tab-index="1">
<workbench-features ref="features" :api="featuresApi" @special="onSpecialEntry"></workbench-features>
</clinic-salesperson-page-layout>
</template>
<script>
/**
* 诊所推广员端「功能」tab 薄壳页
* 壳页只负责:套本端 layout(navbar/tabbar 配置)+ 注入本端角色接口 + 处理本端特殊入口,
* 搜索/分组滑动/长按收藏/使用上报等全部逻辑在主包共享组件 workbench-features(四端复用)
*/
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import {
getClinicSalespersonWorkbenchEntries,
getClinicSalespersonEntryUserData,
toggleClinicSalespersonFavoriteEntry,
reportClinicSalespersonEntryUsage,
} from '@/api/clinicSalesperson.js'
export default {
components: { ClinicSalespersonPageLayout },
data() {
return {
/** 注入共享组件的本端接口集合(统一返回 { res, data, ok }) */
featuresApi: {
getEntries: getClinicSalespersonWorkbenchEntries,
getUserData: getClinicSalespersonEntryUserData,
toggleFavorite: toggleClinicSalespersonFavoriteEntry,
reportUsage: reportClinicSalespersonEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在)
if (this.$refs.features) this.$refs.features.refreshUserData()
},
methods: {
/**
* 无 path 的特殊入口处理:cs_transfer 的传方方式弹窗与拍方抽屉都挂在首页,
* 带 openTransfer 参数回首页自动弹出,避免在功能页重复引入整套传方组件
*/
onSpecialEntry(item) {
if (item.key === 'cs_transfer') {
uni.reLaunch({ url: '/subPackages/sub_clinic_salesperson/home/index?openTransfer=1' })
}
},
},
}
</script>

View File

@@ -76,31 +76,14 @@
</view>
</view>
<!-- 快捷服务区:多彩微拟物质感 -->
<view class="section-panel">
<view class="panel-header">
<text class="panel-title">常用服务</text>
</view>
<view class="service-grid">
<view
v-for="(item, index) in enrichedMenuItems"
:key="item.key"
class="service-card"
hover-class="service-card-hover"
@click="onMenuClick(item)"
>
<view class="service-icon-box" :style="{ background: item.theme.bg }">
<!-- 替换为 uView 动态图标 -->
<u-icon :name="item.icon" size="38" :color="item.theme.color"></u-icon>
</view>
<view class="service-info">
<text class="service-name">{{ item.label }}</text>
<text class="service-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view>
<!-- 主体内容区:个人功能区(最近使用 / 我的常用 / 更多功能,全部入口收敛到功能 tab) -->
<workbench-user-entries
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
@item-click="onEntryTap"
@more="goFeatures"
></workbench-user-entries>
</view>
<transfer-mode-modal v-model="showTransferModeModal" @select="onTransferModeSelect" />
@@ -118,10 +101,10 @@
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import TransferModeModal from '@/subPackages/sub_clinic_salesperson/components/TransferModeModal.vue'
import { getClinicSalespersonMyInfo, createClinicSalespersonTransferPrescription } from '@/api/clinicSalesperson.js'
import { getClinicSalespersonMyInfo, createClinicSalespersonTransferPrescription, getClinicSalespersonEntryUserData, reportClinicSalespersonEntryUsage } from '@/api/clinicSalesperson.js'
import { openQrCodePreview, buildSalespersonQrPayload } from '@/utils/qrCodePreview.js'
const ROOT = '/subPackages/sub_clinic_salesperson'
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
export default {
// transfer-patient-drawer 跨分包引用,在 pages.json 通过 usingComponents + componentPlaceholder 注册
@@ -136,44 +119,74 @@ export default {
showTransferModeModal: false,
showPhotoTransferDrawer: false,
photoSubmitting: false,
menuItems: [
{ key: 'transfer', label: '传方', action: 'transfer' },
{ key: 'transfer-list', label: '传方记录', path: `${ROOT}/transfer-prescription/list` },
// { key: 'earnings', label: '我的收益', path: `${ROOT}/earnings/index` },
{ key: 'leads', label: '获客记录', path: `${ROOT}/leads/index` },
{ key: 'invitees', label: '我邀请的推广员', path: `${ROOT}/invitees/index` },
{ key: 'commission', label: '服务费记录', path: `${ROOT}/commission/index` },
{ key: 'settlement', label: '结算记录', path: `${ROOT}/settlement/index` },
],
/** 我的常用/最近使用原始入口(归一化后存储,传方弹窗行为在 computed 注入) */
favRaw: [],
recentRaw: [],
/** 卡片形态偏好:true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
}
},
computed: {
// 动态注入多彩主题和增强文案,使用 uView 图标名称
enrichedMenuItems() {
const themes = [
{ bg: 'linear-gradient(135deg, #ECFDF5, #D1FAE5)', color: '#10B981', desc: '线下中药传方', icon: 'edit-pen-fill' },
{ bg: 'linear-gradient(135deg, #F0F9FF, #E0F2FE)', color: '#0EA5E9', desc: '历史传方记录', icon: 'file-text-fill' },
// 收益:使用钻石/钱包相关图标
{ bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316', desc: '查看收益明细', icon: 'red-packet-fill' },
// 获客:使用目标/数据相关图标
{ bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6', desc: '客户转化漏斗', icon: 'bag-fill' },
// 分成:使用图表/比例相关图标
{ bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7', desc: '服务费与记录', icon: 'red-packet-fill' },
// 结算:使用卡片/提现相关图标
{ bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E', desc: '提现与账单', icon: 'rmb-circle-fill' },
];
return this.menuItems.map((item, index) => ({
...item,
icon: themes[index % themes.length].icon,
desc: themes[index % themes.length].desc,
theme: themes[index % themes.length]
}));
/** 我的常用(cs_transfer 无 path,按 code 注入传方弹窗行为) */
favItems() {
return this.favRaw.map((item) => this.decorateItem(item))
},
/** 最近使用 */
recentItems() {
return this.recentRaw.map((item) => this.decorateItem(item))
},
},
onLoad(options) {
// 功能页点击 cs_transfer(传方弹窗挂在本页)时带 openTransfer=1 reLaunch 回来自动弹出
if (options && options.openTransfer) {
this.showTransferModeModal = true
}
},
onShow() {
// 卡片形态偏好每次 onShow 重读:我的页切换后返回立即生效
this.cardLong = getCardStyle() === 'long'
this.loadInfo()
this.loadUserEntries()
},
methods: {
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底) */
async loadUserEntries() {
try {
const wrap = await getClinicSalespersonEntryUserData()
if (wrap && wrap.ok && wrap.data) {
this.favRaw = this.normalizeList(wrap.data.favorites)
this.recentRaw = this.normalizeList(wrap.data.recents)
}
} catch (e) {
console.error('常用/最近使用加载失败', e)
}
},
/** 后端入口对象数组 → 渲染项数组(与功能页同一映射) */
normalizeList(list) {
if (!Array.isArray(list)) return []
const items = []
list.forEach((entry) => {
const item = normalizeEntry(entry)
if (item) items.push(item)
})
return items
},
/** 按 code 注入本端行为:传方入口弹方式选择弹窗(无 path) */
decorateItem(item) {
if (item.key === 'cs_transfer') {
return { ...item, action: 'transfer' }
}
return item
},
/** 入口点击:静默上报使用(保持最近使用新鲜)→ 传方弹窗/普通跳转 */
onEntryTap(item) {
reportClinicSalespersonEntryUsage(item.key).catch(() => {})
this.onMenuClick(item)
},
/** 更多功能/空态引导:切到功能 tab(tabbar 页统一走 reLaunch) */
goFeatures() {
uni.reLaunch({ url: '/subPackages/sub_clinic_salesperson/features/index' })
},
async loadInfo() {
const wrap = await getClinicSalespersonMyInfo()
if (!wrap || !wrap.ok) return
@@ -520,72 +533,5 @@ export default {
}
/* ================= 快捷服务区:多彩拟物 ================= */
.section-panel {
margin: 0 30rpx;
background: transparent;
}
.panel-header {
margin-bottom: 24rpx;
padding: 0 10rpx;
}
.panel-title {
font-size: 34rpx;
font-weight: 700;
color: #1E293B;
}
.service-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.service-card {
background: #FFFFFF;
border-radius: 28rpx;
padding: 30rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
transition: all 0.2s ease;
border: 1px solid rgba(0, 0, 0, 0.01);
}
.service-card-hover {
transform: translateY(-2rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.service-icon-box {
width: 76rpx;
height: 76rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.5); /* 顶部微高光,增加拟物质感 */
}
/* 移除了无用的 .service-icon 类 */
.service-info {
display: flex;
flex-direction: column;
}
.service-name {
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
margin-bottom: 6rpx;
}
.service-desc {
font-size: 20rpx;
color: #94A3B8;
}
/* 个人功能区(分区/宫格样式在 workbench-user-entries 组件内) */
</style>

View File

@@ -2,13 +2,15 @@
<clinic-salesperson-page-layout title="获客记录" :show-back="true">
<view class="page">
<view v-if="!list.length" class="empty">暂无获客记录</view>
<view v-for="item in list" :key="item.id" class="row">
<view>
<text class="name">{{ item.user_nick_name || '用户' }}</text>
<text class="phone">{{ item.user_phone }}</text>
</view>
<text class="time">{{ item.bind_at_text }}</text>
</view>
<!-- 获客记录卡:统一 xk-list-card 规范(首字头像 + 昵称/手机分层 + 绑定时间) -->
<xk-list-card
v-for="item in list" :key="item.id"
:avatar-text="item.user_nick_name || '用'"
:title="item.user_nick_name || '用户'"
:time="item.bind_at_text"
:rows="[item.user_phone || '-']"
:clickable="false"
></xk-list-card>
</view>
</clinic-salesperson-page-layout>
</template>
@@ -37,8 +39,4 @@ export default {
<style lang="scss" scoped>
.page { padding: 24rpx; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; display: flex; justify-content: space-between; align-items: center; }
.name { display: block; font-size: 28rpx; font-weight: 600; }
.phone { font-size: 24rpx; color: #666; }
.time { font-size: 22rpx; color: #999; }
</style>

View File

@@ -1,70 +1,55 @@
<template>
<clinic-salesperson-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container">
<view class="profile-panel">
<view class="avatar-circle">
<text>{{ avatarText }}</text>
</view>
<view class="user-info">
<text class="user-name">{{ displayName }}</text>
<view class="role-badge">
<text class="role-text">推广员</text>
</view>
<text v-if="phone" class="meta-line">{{ phone }}</text>
</view>
</view>
<view v-if="storeName" class="setting-panel info-panel">
<view class="info-row">
<text class="item-label">所属诊所</text>
<text class="item-value">{{ storeName }}</text>
</view>
</view>
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
</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>
<!-- 页面主体统一收进 admin-my-panel(资料区 + 个人中心入口 + 菜单卡 + 退出) -->
<admin-my-panel
ref="myPanel"
:display-name="displayName"
:avatar="avatar"
role-text="推广员"
:store-name="storeName"
:phone="phone"
fallback-char="推"
/>
</clinic-salesperson-page-layout>
</template>
<script>
/**
* 推广员「我的」:对齐医生(菜单卡 + 绿色退出),保留所属诊所与绑定微信
* 推广员「我的」:主体复用 admin-my-panel 共享组件
* 本页只保留 my-info 拉取(姓名/手机号/所属诊所)并写回缓存
*/
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { ClinicSalespersonPageLayout, AccountSwitchEntry, WxBindEntry },
components: { ClinicSalespersonPageLayout, AdminMyPanel },
data() {
return {
displayName: '',
avatar: '',
phone: '',
storeName: '',
loading: false,
}
},
computed: {
avatarText() {
return (this.displayName || '推').slice(0, 1)
},
},
onShow() {
// 先读缓存(个人中心改完昵称/头像返回立即生效),再拉接口刷新
this.loadLocalUser()
this.loadProfile()
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
methods: {
/** 读本地缓存兜底首屏(my-info 缓存结构:{ salesperson, store }) */
loadLocalUser() {
const info = uni.getStorageSync('clinic_salesperson_user') || {}
const sp = info.salesperson || {}
this.displayName = sp.nick_name || this.displayName || '推广员'
this.avatar = sp.avatar || ''
this.phone = sp.phone || ''
this.storeName = (info.store && info.store.name) || ''
},
/** 拉取 my-info 刷新资料并写回缓存 */
async loadProfile() {
if (this.loading) return
this.loading = true
@@ -72,117 +57,16 @@ export default {
const wrap = await getClinicSalespersonMyInfo()
if (!wrap || !wrap.ok) return
const info = wrap.data || {}
this.displayName = info.salesperson?.nick_name || '推广员'
this.phone = info.salesperson?.phone || ''
this.storeName = info.store?.name || ''
const sp = info.salesperson || {}
this.displayName = sp.nick_name || '推广员'
this.avatar = sp.avatar || ''
this.phone = sp.phone || ''
this.storeName = (info.store && info.store.name) || ''
uni.setStorageSync('clinic_salesperson_user', info)
} finally {
this.loading = false
}
},
logout() {
uni.showModal({
title: '提示',
content: '确定退出登录?',
confirmColor: '#6ACDBB',
success: (res) => {
if (!res.confirm) return
clearLoginStorage()
uni.reLaunch({ url: '/pages/login/index' })
},
})
},
},
}
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$theme-dark: #4db6a8;
$color-page-bg: #f5f7f8;
$color-text-title: #222b2a;
$color-text-muted: #86909c;
.mine-container {
padding: 0 0 60rpx;
background-color: $color-page-bg;
min-height: 100vh;
box-sizing: border-box;
}
.profile-panel {
display: flex;
align-items: center;
background-color: #fff;
padding: 60rpx 40rpx 50rpx;
margin-bottom: 24rpx;
}
.avatar-circle {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.25);
color: #fff;
font-size: 52rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.user-info {
margin-left: 32rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.user-name {
font-size: 40rpx;
font-weight: 700;
color: $color-text-title;
margin-bottom: 12rpx;
}
.role-badge {
align-self: flex-start;
background-color: rgba(106, 205, 187, 0.1);
border: 1rpx solid rgba(106, 205, 187, 0.2);
padding: 4rpx 16rpx;
border-radius: 8rpx;
margin-bottom: 8rpx;
}
.role-text {
font-size: 22rpx;
color: $theme-dark;
font-weight: 600;
}
.meta-line {
font-size: 26rpx;
color: $color-text-muted;
margin-top: 4rpx;
}
.setting-panel {
background-color: #fff;
margin-bottom: 24rpx;
padding: 8rpx 40rpx 16rpx;
}
.info-panel {
padding: 28rpx 40rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-label {
font-size: 30rpx;
color: $color-text-title;
font-weight: 500;
}
.item-value {
font-size: 30rpx;
color: $color-text-muted;
}
.logout-wrap {
padding: 0 40rpx 32rpx;
}
</style>

View File

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

View File

@@ -2,24 +2,29 @@
<clinic-salesperson-page-layout title="传方记录" :show-back="true">
<view class="page">
<view v-if="!list.length && !loading" class="empty">暂无传方记录</view>
<view v-for="item in list" :key="item.id" class="row" @click="openDetail(item.id)">
<view class="top">
<text class="name">{{ item.patient_name }} {{ item.patient_mobile }}</text>
<u-tag
:text="item.status_text || (item.is_imported ? '已完成' : '未处理')"
:type="statusTagType(item.status)"
size="mini"
/>
</view>
<text class="meta">{{ item.transfer_mode_text || '' }} · {{ item.transfer_time_text }}</text>
<text v-if="item.drug_text_lines && item.drug_text_lines.length" class="drugs">
药方:{{ item.drug_text_lines.join(';') }}
</text>
<text v-if="item.remark" class="remark">备注:{{ item.remark }}</text>
<text v-if="item.status === 3 && item.exception_message" class="exception">
异常:{{ item.exception_message }}
</text>
</view>
<!-- 传方记录卡:统一 xk-list-card 规范(患者信息分层 + 状态标签 + 查看详情操作) -->
<xk-list-card
v-for="item in list" :key="item.id"
:avatar-text="item.patient_name"
:title="item.patient_name"
:meta="item.patient_mobile"
:time="item.transfer_time_text"
:tags="[{ text: item.status_text || (item.is_imported ? '已完成' : '未处理'), type: statusTagType(item.status) }]"
:actions="[{ key: 'detail', text: '查看详情', type: 'ghost' }]"
@action="openDetail(item.id)"
@click="openDetail(item.id)"
>
<template #body>
<text class="card-sub" v-if="item.transfer_mode_text">{{ item.transfer_mode_text }}</text>
<text v-if="item.drug_text_lines && item.drug_text_lines.length" class="card-sub drugs">
药方:{{ item.drug_text_lines.join(';') }}
</text>
<text v-if="item.remark" class="card-sub">备注:{{ item.remark }}</text>
<text v-if="item.status === 3 && item.exception_message" class="card-sub exception">
异常:{{ item.exception_message }}
</text>
</template>
</xk-list-card>
<u-button v-if="list.length" plain type="primary" class="mt" @click="goForm">新建传方</u-button>
</view>
<transfer-detail-drawer ref="detailDrawer" />
@@ -72,10 +77,14 @@ export default {
<style lang="scss" scoped>
.page { padding: 24rpx; }
.empty { text-align: center; color: #999; padding: 80rpx 0; }
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; }
.top { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 28rpx; font-weight: 600; }
.meta, .drugs, .remark { display: block; font-size: 24rpx; color: #666; margin-top: 8rpx; }
.exception { display: block; font-size: 24rpx; color: #ef4444; margin-top: 8rpx; }
/* 卡内副文案行(body 插槽自带样式):药方允许两行截断,异常标红 */
.card-sub { display: block; font-size: 24rpx; color: #666; margin-top: 8rpx; }
.drugs {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.exception { color: #ef4444; }
.mt { margin-top: 24rpx; }
</style>

View File

@@ -1,199 +0,0 @@
<template>
<view class="page">
<!-- <view class="search white">
<u-search placeholder="请输入文章标题" @input="handleSearch" v-model="keyword" :show-action="false"></u-search>
</view> -->
<view class="box p-row-2 m-t-2">
<view class="flex-col b-r-8 white p-32 m-t-2 p-t-0" >
<view class="flex-row m-t-32 flex-ali-center" v-for="(item,index) in list" :key="index"
@click="changeSelect(index)">
<view style="width: 40rpx;height: 40rpx;" v-if="item.select">
<u-icon name="checkmark-circle-fill" color="#6ACDBB" size="40"></u-icon>
</view>
<view class="select" v-else></view>
<!-- <view class="m-l-32">
<u-image width="80rpx" height="80rpx" :src="item.cover">
<u-loading slot="loading"></u-loading>
</u-image>
</view> -->
<view class="flex-col m-l-2 flex-ali-center">
<view class="flex-row">
<d-text :text="item.title" className="content-c fs-32"></d-text>
</view>
</view>
<!-- <view class="flex-row flex-ali-center" v-if="item.tags&&item.tags.length>0">
<view class="m-r08" v-for="(it,idx) in item.tags" :key="idx">
<u-tag :text="it" size="mini" border-color="rgba(41,121,255,0.08)"
bg-color="rgba(41,121,255,0.08)" color="#6ACDBB" />
</view>
</view> -->
</view>
</view>
</view>
<view class="button">
<u-button :throttle-time="0" shape="circle" @click="submit" :ripple="true"
:disabled="getSelect==0">{{`确定(${getSelect})`}}</u-button>
</view>
</view>
</template>
<script>
import { doctorArticleList } from "@/api/all.js";
export default {
data() {
return {
male: this.$male,
girl: this.$girl,
keyword: '',
navbarHeight: this.$navbarHeight,
statusBarHeight: this.$statusBarHeight,
list: [],
page: 1,
loading: true,
loading2: false,
status: "loading",
loadText: {
loadmore: "轻轻上拉",
loading: "努力加载中",
nomore: "已加载全部",
},
noMore: false,
};
},
computed: {
getSelect() {
return this.list.reduce((co, item) => {
if (item['select']) co++
return co
}, 0)
}
},
onShow() {
this.init();
},
onReachBottom() {
if (this.noMore) {
this.$toast("数据已全部加载");
return;
}
this.page++;
this.getList();
},
methods: {
handleSearch() {
this.page = 1
this.getList()
},
init() {
this.list = [];
this.page = 1;
this.keyword = '';
this.loading = true;
this.getList(true);
},
getList(is_stop = false) {
if (this.loading2) {
return;
}
this.loading2 = true;
doctorArticleList({
page: this.page,
keyword: this.keyword
})
.then((res) => {
if (res.errcode == 0) {
this.list = this.page === 1 ? res.data : [...this.list, ...res.data];
this.noMore = res.data.length < 20;
if (this.page >= this.totalPage) {
this.status = "nomore";
} else {
this.status = "loadmore";
}
this.$nextTick(() => {
this.loading = false;
is_stop && uni.stopPullDownRefresh();
});
} else {
this.$toast(res.msg);
is_stop && uni.stopPullDownRefresh();
this.loading = false;
}
})
.finally(() => {
this.loading2 = false;
});
},
changeSelect(index) {
const current = this.list[index]
current.select = !current.select
this.list.splice(index, 1, current)
},
submit() {
uni.navigateBack({ delta: 1 })
const list = this.list.filter(item => item.select)
if (list.length < 0) {
uni.showToast({
title: '请选择发送的文章',
icon: 'none',
mask: true
})
return
}
if (list.length > 1) {
uni.showToast({
title: '只能选择一篇文章',
icon: 'none',
mask: true
})
return
}
uni.setStorageSync('articleList', list)
}
}
};
</script>
<style>
page {
background-color: #eeeeef;
}
</style>
<style lang="scss" scoped>
.select {
width: 44rpx;
height: 44rpx;
border: 2rpx solid #E8E9EB;
border-radius: 44rpx;
}
.search {
position: relative;
z-index: 982;
padding: 16rpx 32rpx;
}
.box {
position: relative;
z-index: 982;
}
.button {
position: fixed;
bottom: env(safe-area-inset-bottom);
left: 0;
width: 750rpx;
height: 110rpx;
background: #FFFFFF;
border-top: 2rpx solid #F2F3F5;
padding: 12rpx 32rpx;
box-sizing: border-box;
z-index: 999;
::v-deep button {
z-index: 999;
background: #6ACDBB;
color: #FFFFFF;
}
}
</style>

View File

@@ -1,228 +0,0 @@
<template>
<view>
<view class="white p-32" v-if="info&&!loading">
<view class="flex-row flex-ali-center m-b-48" v-if="info">
<u-image width="96rpx" height="96rpx" shape="circle" :src="getAvatar">
<u-loading slot="loading"></u-loading>
</u-image>
<view class="flex-col flex-jus-sp m-l-32">
<view class="flex-row flex-ali-center">
<d-text :text="info.patient.name" className="content-c fs-32"></d-text>
<d-text :text="info.patient.sex==1?'男':'女'" className="tips-c m-l-2"></d-text>
<d-text :text="info.age+'岁'" className="tips-c m-l-2"></d-text>
</view>
<d-text :text="getMobile(info.patient.mobile)"></d-text>
</view>
</view>
<view class="m-b-48 flex-row flex-jus-sp flex-ali-center">
<d-text text="备注" width="85rpx"></d-text>
<view class="flex-row flex-ali-center">
<d-text width="558" :text="info.remark.remark" className="content-c m-r-16" :lines="1">
</d-text>
<u-icon name="arrow-right" color="#A7ABB0" size="32"></u-icon>
</view>
</view>
<view class="m-b-48 flex-row flex-jus-sp flex-ali-center">
<d-text text="分组" width="85rpx"></d-text>
<view class="flex-row flex-ali-center flex-jus-end">
<d-text :text="getGroup(info.group)" width="558" align="right" className="content-c m-r-16"
:lines="1"></d-text>
<u-icon name="arrow-right" color="#A7ABB0" size="32"></u-icon>
</view>
</view>
</view>
<view class="m-t-2 white" v-if="!loading">
<u-tabs :list="tabs" :is-scroll="false" :current="current" @change="current=$event;loading2=true;getList()"
bar-width="20" inactive-color="#6C7380"></u-tabs>
<view class="p-32" v-if="list[current].length>0&&!loading2">
<block v-if="current==0">
<view class="flex-col" :class="{'m-b-32':(i+1)!=list[current].length}"
v-for="(item,i) in list[current]" :key="i">
<d-text text="2022-11-16"></d-text>
<view class="flex-row flex-ali-center m-t-16">
<d-text text="主诉:"></d-text>
<d-text :text="item.main_suit" className="content-c"></d-text>
</view>
<view class="flex-row flex-ali-center flex-jus-sp m-t08">
<d-text width="85rpx" text="诊断:" className=""></d-text>
<d-text :text="item.diagnose" className="flex-1 content-c" :lines="1">
</d-text>
</view>
</view>
</block>
<block v-else>
<view class="flex-col" v-for="(item,i) in list[current]" :key="i">
<view class="flex-row flex-jus-sp flex-ali-center">
<d-text :text="$u.timeFormat(item.created_at, 'yyyy-mm-dd hh:MM:ss')" color="#6C7380">
</d-text>
<d-text :text="states[item.status].label" :color="states[item.status].color"></d-text>
</view>
<view class="m-t-32 flex-col">
<view class="flex-row m-b-16">
<d-text text="医生" align="justify" width="112"></d-text>
<d-text :text="item.doctor_name" className="content-c m-l-48"></d-text>
</view>
<view class="flex-row m-b-16">
<d-text text="诊断" width="112" align="justify"></d-text>
<d-text :text="item.clinical_diagnose" className="content-c m-l-48 flex-1" :lines="1">
</d-text>
</view>
<view class="flex-row m-b-16">
<d-text text="药品" width="112" align="justify"></d-text>
<d-text :text="getDrug(item.drug_name)" className="content-c m-l-48 flex-1" :lines="1"></d-text>
</view>
<!-- <view class="flex-row">
<d-text text="问诊记录" width="112" align="justify"></d-text>
<d-text text="点击查看" :color="states[0].color" className="m-l-48"></d-text>
</view> -->
<!-- <view class="m-t-16 flex-row flex-jus-end">
<u-button :throttle-time="0" shape="circle" size="mini"
:custom-style="{backgroundColor: 'transparent !important'}" plain :ripple="true">
查看详情
</u-button>
</view> -->
</view>
</view>
</block>
</view>
<view class="flex-row flex-jus-center">
<d-empty text="无记录" v-if="!loading2&&list[current].length==0"></d-empty>
</view>
<d-loading-page :loading="loading2"></d-loading-page>
</view>
<d-loading-page :loading="loading"></d-loading-page>
</view>
</template>
<script>
import {
patientInfo,
recordList,
prescripRecord
} from '@/api/all.js'
export default {
data() {
return {
loading: false,
loading2: true,
info: {},
tabs: [{
name: '就诊记录'
}, {
name: '处方记录'
}],
list: {
0: [],
1: []
},
states: {
3: {
color: '#2979FF',
label: '待使用'
},
6: {
color: '#A7ABB0',
label: '已失效'
},
4: {
color: '#A7ABB0',
label: '已使用'
}
},
current: 0,
id: -1
}
},
computed: {
getAvatar() {
return require(`@/static/image/${this.info.patient.sex%2==0?'nv':'nan'}.png`)
}
},
onLoad(op) {
this.id = op['id'] || -1
this.id > 0 && this.getList()
this.id > 0 && this.patientInfo()
},
methods: {
getDrug(obj={}) {
let list = (obj.chinese.length>0?obj.chinese:obj.west)||[]
let drugs =[]
console.log(list);
for (let i = 0; i < list.length; i++) {
if (obj.chinese.length>0) {
for (let j = 0; j < JSON.parse(list[i].content).length; j++) {
drugs.push(JSON.parse(list[i].content)[j]['name'])
}
} else{
drugs.push(JSON.parse(list[i].content)['drug_name'])
}
}
return drugs.join()
},
getGroup(list) {
let groups = []
for (var i = 0; i < list.length; i++) {
groups.push(list[i].name)
}
return groups
},
getMobile(v) {
return v.replace(/^(\d{3})\d{4}(\d+)/, "$1****$2")
},
patientInfo() {
patientInfo({
up_id: this.id
}).then(res => {
if (res.errcode == 0) {
this.info = res.data
this.$nextTick(() => {
this.loading = false
})
} else {
this.$toast(res.msg);
this.loading = false
}
})
},
async getList() {
let res = {}
this.current == 0 && (res = await recordList({
up_id: this.id
}))
this.current == 1 && (res = await prescripRecord({
up_id: this.id
}))
if (res.errcode == 0) {
this.list[this.current] = res.data && res.data.list ? res.data.list : res.data || []
this.$nextTick(() => {
this.loading2 = false
})
} else {
this.$toast(res.msg);
this.loading2 = false
}
}
}
}
</script>
<style>
page {
background-color: #F9FAFB;
}
</style>
<style lang="scss" scoped>
.text {
width: 122rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
text-align-last: justify;
}
.text:after {
content: '';
width: 100%;
display: inline-block;
}
</style>

View File

@@ -158,10 +158,6 @@ import { sendDoctor } from '../../api/all';
this.select = true;
this.$go(`patient_select?list=${this.info.patient&&JSON.stringify(this.info.patient)}`)
},
// goArtcle(){
// this.select1 = true;
// this.$go(`article_select`)
// },
confirm(e) {
// console.log(e,'time');
let time = `${e.month}/${e.day} ${e.hour}:${e.minute}`

View File

@@ -1,103 +0,0 @@
<template>
<view class="change_password">
<view class="box">
<u-input
style="width: 100%"
placeholder="当前密码"
v-model="value"
type="password"
></u-input>
</view>
<view class="box">
<u-input
style="width: 100%"
placeholder="新密码(6-20位)"
v-model="pwd1"
type="password"
></u-input>
</view>
<view class="box">
<u-input
style="width: 100%"
placeholder="确认新密码"
v-model="pwd2"
type="password"
></u-input>
</view>
<view class="btn">
<u-button
:throttle-time="0"
type="primary"
:disabled="!disabled"
@click="updatePassword"
shape="circle"
:loading="loading"
>确认修改</u-button
>
</view>
</view>
</template>
<script>
import { updatePassword } from "@/api/all.js";
export default {
data() {
return {
value: null,
pwd1: null,
pwd2: null,
loading: false,
};
},
computed: {
disabled() {
return this.value && this.pwd1 && this.pwd2;
},
},
methods: {
async updatePassword() {
if (this.pwd1!==this.pwd2) {
this.$toast("两次密码不一致");
return;
}
if (this.loading) {
return;
}
this.loading = true;
try {
const res = await updatePassword({
password_old: this.value,
password_new: this.pwd1,
});
if (res.errcode != -1) {
this.$toast("修改成功");
} else {
this.$toast(res.msg);
}
} catch (error) {
this.$toast("系统错误");
} finally {
this.loading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.change_password {
height: 100vh;
background: #f8fafc;
padding-top: 20px;
.box {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 60rpx;
background: #fff;
}
.btn {
margin: 0 32rpx;
margin-top: 80rpx;
}
}
</style>

View File

@@ -1,42 +0,0 @@
<template>
<view class="">
</view>
</template>
<script>
export default {
components: {
},
data() {
return {
type: 0,
info: {},
page: 1,
record: "",
};
},
onLoad(op) {
},
methods: {
},
mounted() {
// this.a = setInterval(() => {
// this.messageList()
// }, 1000)
},
destroyed() {
// 取消监听键盘高度变化事件
uni.offKeyboardHeightChange();
// clearInterval(this.a)
// this.a = null
},
};
</script>
<style></style>

View File

@@ -258,11 +258,13 @@
</template>
<script>
// 审方详情页:老 Yii examine/detail、examine/use-way、examine/verify 已迁移到 xk-api /pharmacist-examine/*
import {
getPassed,
prescripRecordDetail,
getUseWay
} from "@/api/all.js";
getExamineDetail,
getExamineUseWay,
verifyExamine
} from "@/api/pharmacistExamine.js";
import { unwrapXkApi } from "@/utils/api-response.js";
import { withWxKey } from '@/utils/wxListKey.js'
export default {
data() {
@@ -298,16 +300,14 @@
}
},
onLoad(e) {
// console.log('12')
// console.log(e, "id");
this.id = e.id
this.prescription_no = e.prescription_no
this.getInfo()
getUseWay({
store_id: uni.getStorageSync('store_id') || 11001,
}).then((res) => {
if (res.errcode == 0) {
const useWay = ["无", ...(res.data.map(it => it.name))]
// 用法字典(xk-api):下标 0 占位「无」,药材 order 字段按下标映射煎法名称
getExamineUseWay().then((res) => {
const { ok, payload } = unwrapXkApi(res)
if (ok) {
const useWay = ["无", ...(payload.map(it => it.name))]
uni.setStorageSync('useWay', JSON.stringify(useWay))
this.useWay = useWay
}
@@ -322,21 +322,22 @@
// 如果url包含http,则返回url,反之加上 data:image/jpeg;base64,
return url.includes('http') ? url : 'data:image/jpeg;base64,' + url
},
/** 驳回:examine/verify 已迁 xk-api,驳回原因必填,成功后刷新详情展示最新状态 */
confirm() {
getPassed({
verifyExamine({
prescription_no: this.prescription_no,
status: 2,
reject_reason: this.reject_reason,
store_id: uni.getStorageSync('store_id') || '11001',
}).then((res) => {
if (res.errcode == 0) {
const { ok, payload, message } = unwrapXkApi(res)
if (ok) {
this.show = false
this.loading = true;
this.isshow = false
this.getInfo() && this.$go(1, 4)
this.$toast(res.data[0]);
this.getInfo()
this.$toast((payload && payload[0]) || '已驳回');
} else {
this.$toast(res.msg);
this.$toast(message);
this.loading = false
}
})
@@ -370,37 +371,38 @@
})
return withWxKey(normalized, 'id', 'rp')
},
/** 详情:examine/detail 已迁 xk-api,content 后端已解码,签名图/公章字段名与老接口一致 */
getInfo() {
prescripRecordDetail({
store_id: uni.getStorageSync('store_id') || '11001',
getExamineDetail({
prescription_no: this.prescription_no
}).then((res) => {
// console.log(res, 'deta');
if (res.errcode == 0) {
const content = res.data.content
const { ok, payload, message } = unwrapXkApi(res)
if (ok) {
const content = payload.content
content.repice = this.normalizeRepice(content.repice)
this.infoList = content
this.rpList = res.data.pharmacistInfo
this.lists = res.data
// console.log(this.infoList, 'info');
this.rpList = payload.pharmacistInfo
this.lists = payload
} else {
this.$toast(message);
}
})
},
/** 通过:examine/verify 已迁 xk-api(status=1),后端连带订单 ERP 同步与站内通知 */
pass(e) {
this.status = e
getPassed({
store_id: uni.getStorageSync('store_id') || '11001',
verifyExamine({
prescription_no: this.prescription_no,
status: this.status
}).then((res) => {
if (res.errcode == 0) {
const { ok, message } = unwrapXkApi(res)
if (ok) {
this.show = false
this.$toast('审核成功')
this.getInfo()
} else {
this.$toast(res.msg);
this.$toast(message);
}
})
},

View File

@@ -1,172 +0,0 @@
<template>
<view class="safe-area-inset-bottom p-2">
<view
class="p-32 white b-r-8 flex-col"
v-for="(item, i) in list"
:key="item.id"
>
<view class="flex-row flex-ali-center flex-jus-sp">
<view class="flex-row flex-ali-center flex-jus-sp">
<view style="position: relative">
<u-image
width="80rpx"
height="80rpx"
shape="circle"
:src="item.user.avatar || '../../static/image/djr.png'"
>
<u-loading slot="loading"></u-loading>
</u-image>
<!-- <u-badge :offset="[0,-8]" :count="2"></u-badge> -->
</view>
<view class="m-l-2 flex-col flex-jus-sp">
<view class="flex-row flex-ali-center">
<d-text
:text="item.user.name || '用户'"
className="fs-32 tips-c"
></d-text>
<d-text :text="timeago(item.last.created_at*1000)" color="#969799" className="m-l-2"></d-text>
</view>
<d-text :text="getMsg(item.last.content).data.text" className="fs-32 main-c"></d-text>
</view>
</view>
<view>
<u-button
size="mini"
shape="circle"
:ripple="true"
type="primary"
@click="sessionIn(item.id)"
>接入会话
</u-button>
</view>
</view>
</view>
<view class="flex-row flex-jus-center" v-if="!loading && list.length == 0">
<d-empty text="暂无待接入用户" mode="search"></d-empty>
</view>
<d-loading-page :loading="loading"></d-loading-page>
</view>
</template>
<script>
import { sessionIn,waitInList } from "@/api/all.js";
export default {
data() {
return {
loading: true,
list: [],
info: "",
};
},
onShow() {
this.waitInList()
},
methods: {
getMsg(val) {
return this.$u.test.jsonString(val)
? JSON.parse(val)
: {
type: "text",
data: {
text: val,
},
};
},
timeago(dateTimeStamp) {
let result = "";
var minute = 1000 * 60; //把分,时,天,周,半个月,一个月用毫秒表示
var hour = minute * 60;
var day = hour * 24;
var week = day * 7;
var halfamonth = day * 15;
var month = day * 30;
var now = new Date().getTime(); //获取当前时间毫秒
console.log(now);
var diffValue = now - dateTimeStamp; //时间差
if (diffValue < 0) {
return;
}
var minC = diffValue / minute; //计算时间差的分,时,天,周,月
var hourC = diffValue / hour;
var dayC = diffValue / day;
var weekC = diffValue / week;
var monthC = diffValue / month;
if (monthC >= 1 && monthC <= 3) {
result = " " + parseInt(monthC) + "月前";
} else if (weekC >= 1 && weekC <= 3) {
result = " " + parseInt(weekC) + "周前";
} else if (dayC >= 1 && dayC <= 6) {
result = " " + parseInt(dayC) + "天前";
} else if (hourC >= 1 && hourC <= 23) {
result = " " + parseInt(hourC) + "小时前";
} else if (minC >= 1 && minC <= 59) {
result = " " + parseInt(minC) + "分钟前";
} else if (diffValue >= 0 && diffValue <= minute) {
result = "刚刚";
} else {
var datetime = new Date();
datetime.setTime(dateTimeStamp);
var Nyear = datetime.getFullYear();
var Nmonth =
datetime.getMonth() + 1 < 10
? "0" + (datetime.getMonth() + 1)
: datetime.getMonth() + 1;
var Ndate =
datetime.getDate() < 10
? "0" + datetime.getDate()
: datetime.getDate();
var Nhour =
datetime.getHours() < 10
? "0" + datetime.getHours()
: datetime.getHours();
var Nminute =
datetime.getMinutes() < 10
? "0" + datetime.getMinutes()
: datetime.getMinutes();
var Nsecond =
datetime.getSeconds() < 10
? "0" + datetime.getSeconds()
: datetime.getSeconds();
result = Nyear + "-" + Nmonth + "-" + Ndate;
}
return result;
},
async waitInList() {
this.loading=true
let res = {};
res = await waitInList();
this.loading=false
if (res.errcode == 0) {
this.list = res.data;
} else {
this.$toast(res.msg);
}
},
sessionIn(id) {
console.log(id,'0090')
uni.showLoading({
title: "接入中...",
});
sessionIn({
ims_id: id,
}).then((res) => {
if (res.errcode == 0) {
this.info = res.data;
this.waitInList()
uni.hideLoading();
} else {
this.$toast(res.msg);
}
});
},
},
};
</script>
<style>
page {
background-color: #f9fafb;
}
</style>
<style lang="scss" scoped></style>

View File

@@ -1,631 +0,0 @@
<template>
<view class="safe-area-inset-bottom">
<u-navbar title="查看处方" :is-back="false" title-size="34" title-color="#fff" :background="{
background:'linear-gradient(90deg, #6ACDBB 0%, #6ACDBB 93%)'}">
</u-navbar>
<!-- 处方 -->
<view class="head">
<view class="head_record_top">
<view class="record">
处方编号:CF20222020202020
</view>
<view class="record_state">
普通
</view>
</view>
<!-- 医院 -->
<view class="head_record">
<image src="@/static/image/hlwyy.png" mode="" v-show="status===true"></image>
<view class="record_yard">
<view class="yard">
互联网医院
</view>
<view class="state">
处方笺
</view>
</view>
</view>
<!-- 时间 -->
<view class="head_record_time">
开具日期:2022-10-23
</view>
</view>
<!-- 信息 -->
<view class="my_info">
<view class="info">
<view class="info_item">
<view class="name">
姓名:
<text>李悦</text>
</view>
</view>
<view class="info_item">
<view class="name">
性别:
<text>女</text>
</view>
</view>
<view class="info_item">
<view class="name">
年龄:
<text>18</text>
</view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="name">
科室:
<text>中医科</text>
</view>
</view>
<view class="info_item">
<view class="name">
电话:
<text>18000000000</text>
</view>
</view>
</view>
<view class="info">
<view class="info_item">
<view class="name">
类别:
<text>自费</text>
</view>
</view>
<view class="info_item">
<view class="name">
诊断:
<text>胆囊结核</text>
</view>
</view>
</view>
</view>
<!-- 药品 -->
<view class="my_medical">
<view class="title">
Rp
</view>
<view class="item">
<view class="name">
<text>复方玄驹胶囊(施强)</text>
<text>1盒</text>
</view>
<view class="details">
<text>规格:0.42g*45s</text>
<text>用法:饭后口服,每日三次,每次3粒</text>
<text>可用天数:5天</text>
</view>
</view>
<view class="item">
<view class="name">
<text>生力雄丸(太极)</text>
<text>2盒</text>
</view>
<view class="details">
<text>规格:0.32g*12粒*2板</text>
<text>用法:口服,每日三次,每次3粒</text>
<text>可用天数:6天</text>
</view>
</view>
</view>
<!-- 医嘱 -->
<view class="doctor_order">
<view class="order_info">
<view class="title">
医嘱:
</view>
<view class="info">
7天一疗程,如无好转再来复查
</view>
</view>
<view class="titles">
处方开具已完毕
</view>
</view>
<!-- 医生 -->
<view class="my_doctor">
<view class="doctor_info">
<view class="info">
<text>医师</text>
<text class="name">李磊</text>
</view>
<view class="info">
<text>药师审核</text>
<text class="name">张三</text>
</view>
</view>
<view class="price">
价格
</view>
</view>
<!-- 温馨提示 -->
<view class="my_prompt">
<view class="title">
{{status||'温馨提示,请遵医嘱服药!处方48小时内有效!'}}
</view>
<!-- <view class="prompt">
<text>特别提示</text>
<ol>
<li>1、处方有效期为3天,请及时取药。</li>
<li>2、本处方限于互联网医院使用,自行下载配药不具有处方效力。</li>
<li>3、服用3天症状未见好转,建议及时复诊或线下就医。</li>
</ol>
</view> -->
</view>
<d-loading-page :loading="loading"></d-loading-page>
<u-popup v-model="show" mode="bottom" :closeable="true" @close="show=false" close-icon-pos='top-right'
:safe-area-inset-bottom="true" :mask-close-able="false">
<view class="flex-col flex-ali-center p-32">
<d-text text="手写签名" className="m-b-48 fs-32" color="#323233"></d-text>
<u-image width="414rpx" height="204rpx" shape="circle" :src="sign_image">
<u-loading slot="loading"></u-loading>
</u-image>
<view class="m-t-8">
<u-button shape="circle" :custom-style="{heigth:'86rpx',width:'646rpx'}"
@click="affirm" :throttle-time="0">确认通过</u-button>
</view>
<view class="m-t-48"></view>
</view>
</u-popup>
<view class="bottom flex-row flex-jus-sp">
<u-button type="primary" shape="circle" :disabled="!sign_image"
:custom-style="{heigth:'86rpx',width:'327rpx',backgroundColor: 'transparent !important'}" plain
@click="is_reject=true;show=true;" :throttle-time="0" :loading="loading" :ripple="true">拒绝
</u-button>
<u-button :throttle-time="0" :disabled="!sign_image" :loading="loading" shape="circle"
:custom-style="{heigth:'86rpx',width:'327rpx'}" @click="show=true">
通过</u-button>
</view>
</view>
</template>
<script>
import {
getRecordDetail,
getUseWay
} from '@/api/all.js'
export default {
data() {
return {
userInfo: uni.getStorageSync("userInfo") || {},
is_reject: false,
show: false,
id: '',
type: 0,
loading: true,
info: '',
sign_image: null
}
},
computed: {
// getList() {
// return this.info ? this.info.recipe.map(item => {
// return {
// ...item,
// content: item['content'] ? JSON.parse(item['content']) : {}
// }
// }) : []
// }
},
onLoad(op) {
this.id = op['id'];
},
methods: {
async getinfo() {
this.loading = false
let res = {}
res = await getRecordDetail({
store_id: uni.getStorageSync('store_id') || '11001',
prescription_id: this.id
})
this.info = ''
if (res.errcode == 0) {
console.log(res, 'xq');
this.info = res.data;
this.info.patient['age'] = this.$d.getAge(this.info['patient']['id_card'])
this.$nextTick(() => {
this.loading = false
})
} else {
this.$toast(res.msg);
this.loading = false
}
}
},
mounted() {
this.getinfo()
}
}
</script>
<style scoped lang="scss">
.min-w-140 {
min-width: 140rpx;
}
.rp {
grid-column-gap: 80rpx;
grid-row-gap: 8rpx;
}
.signature {
width: 96rpx;
height: 44rpx;
border-bottom: 1px solid #31353D;
font-size: 32rpx;
color: #000;
image {
width: 80rpx;
height: 38rpx;
}
}
.bottom {
box-sizing: border-box;
width: 100vw;
height: calc(110rpx + env(safe-area-inset-bottom));
padding: 12rpx 32rpx;
position: fixed;
left: 0;
bottom: 0;
border-top: 1px solid #F2F3F5;
padding-bottom: calc(12rpx + env(safe-area-inset-bottom)) !important;
background-color: #fff;
}
.hiht {
margin: 32rpx 0 calc(32rpx + env(safe-area-inset-bottom)) 0;
}
.head {
.head_record_top {
width: 710rpx;
height: 42rpx;
margin: 34rpx auto;
display: flex;
align-items: center;
justify-content: space-between;
.record {
height: 40rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
line-height: 33rpx;
}
.record_state {
width: 80rpx;
height: 42rpx;
line-height: 42rpx;
text-align: center;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
border: 1rpx solid #C4C7CC;
}
}
.head_record {
display: flex;
align-items: center;
justify-content: center;
position: relative;
image {
width: 195rpx;
height: 195rpx;
position: absolute;
left: 38%;
top: -50rpx;
}
.record_yard {
width: 240rpx;
height: 112rpx;
text-align: center;
.yard {
width: 240rpx;
height: 68rpx;
font-size: 48rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
line-height: 56rpx;
}
.state {
height: 44rpx;
font-size: 32rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
line-height: 38rpx;
}
}
}
.head_record_time {
text-align: right;
margin-top: 20rpx;
width: 750rpx;
padding-right: 32rpx;
height: 34rpx;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
line-height: 28rpx;
}
}
// 信息
.my_info {
width: 710rpx;
height: 232rpx;
margin: 8rpx auto;
border-top: 2rpx solid #31353D;
border-bottom: 2rpx solid #31353D;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 16rpx 4rpx;
.info {
width: 710rpx;
height: 40rpx;
display: flex;
align-items: center;
.info_item {
.name {
display: flex;
align-items: center;
justify-content: space-around;
height: 40rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
line-height: 40rpx;
text {
display: block;
width: 140rpx;
height: 40rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
line-height: 40rpx;
margin: 0 0rpx 0 8rpx;
}
}
}
}
}
// 药品
.my_medical {
width: 710rpx;
height: 492rpx;
margin: 32rpx auto;
.title {
height: 44rpx;
font-size: 32rpx;
font-family: PingFang SC-Semibold, PingFang SC;
font-weight: 600;
color: #31353D;
line-height: 44rpx;
}
.item {
width: 710rpx;
height: 228rpx;
border-bottom: 2rpx solid #E8E9EB;
padding-top: 32rpx;
.name {
width: 710rpx;
height: 40rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.details {
height: 120rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
display: flex;
flex-direction: column;
justify-content: space-around;
}
}
.item:last-child {
border: 0;
}
}
// 医嘱
.doctor_order {
width: 710rpx;
height: 128rpx;
margin: 16rpx auto;
.order_info {
width: 710rpx;
height: 88rpx;
.title {
height: 40rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
line-height: 40rpx;
}
.info {
height: 40rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
line-height: 40rpx;
}
}
.titles {
width: 710rpx;
height: 40rpx;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
line-height: 40rpx;
}
}
// 医生
.my_doctor {
width: 710rpx;
height: 172rpx;
margin: 8rpx auto;
border-top: 2rpx solid #31353D;
border-bottom: 2rpx solid #31353D;
padding: 32rpx 4rpx;
.doctor_info {
width: 710rpx;
height: 44rpx;
display: flex;
align-items: center;
margin-bottom: 24rpx;
.info {
width: 222rpx;
text {
height: 34rpx;
line-height: 28rpx;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
}
.name {
width: 106rpx;
height: 44rpx;
font-size: 32rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
line-height: 38rpx;
border-bottom: 2rpx solid #000000;
margin-left: 16rpx;
}
}
}
.price {
width: 710rpx;
height: 44rpx;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
line-height: 44rpx;
}
}
// 温馨提示
.my_prompt {
width: 710rpx;
height: 304rpx;
margin: 0 auto 30rpx;
padding: 0 4rpx;
.title {
width: 710rpx;
height: 60rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
line-height: 60rpx;
margin: 32rpx 0;
position: relative;
}
.prompt {
width: 710rpx;
height: 200rpx;
text {
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
}
ol {
padding: 0 0 0 32rpx;
li {
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
margin: 4rpx 0;
}
}
}
}
// 状态
.my_state {
width: 710rpx;
height: 40rpx;
margin: 12rpx auto;
padding-bottom: 110rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #F44336;
line-height: 40rpx;
}
</style>

View File

@@ -20,7 +20,7 @@
<view class="m-l-4 flex-1">
<view class="flex-row flex-ali-center flex-jus-sp">
<d-text text="就诊" className="fs-36 content-c"></d-text>
<d-text :text="$u.timeFormat(prescription.patients.updated_at, 'yyyy-mm-dd hh:MM')"></d-text>
<d-text :text="prescription.patients.updated_at"></d-text>
</view>
<view class="flex-row flex-ali-center flex-jus-sp m-t-32">
<d-text text="姓名"></d-text>
@@ -40,7 +40,7 @@
</view>
<view class="flex-row flex-ali-center flex-jus-sp m-t-48">
<d-text text="开具" className="fs-36 content-c"></d-text>
<d-text :text="$u.timeFormat(prescription.created_at, 'yyyy-mm-dd hh:MM')"></d-text>
<d-text :text="prescription.created_at"></d-text>
</view>
<view class="flex-row flex-ali-center flex-jus-sp m-t-32">
<d-text text="处方编号"></d-text>
@@ -65,11 +65,11 @@
</view>
<view class="flex-row flex-ali-center flex-jus-sp m-t-16">
<d-text text="开具时间"></d-text>
<d-text :text="$u.timeFormat(prescription.created_at, 'yyyy-mm-dd hh:MM')"></d-text>
<d-text :text="prescription.created_at"></d-text>
</view>
<view class="flex-row flex-ali-center flex-jus-sp m-t-48">
<d-text text="审核" className="fs-36 content-c"></d-text>
<d-text :text="$u.timeFormat(prescription.pharmacist_view_time, 'yyyy-mm-dd hh:MM')"></d-text>
<d-text :text="prescription.pharmacist_view_time"></d-text>
</view>
<view class="flex-row flex-ali-center flex-jus-sp m-t-32">
<d-text text="审核状态"></d-text>
@@ -92,9 +92,13 @@
</template>
<script>
// 处方溯源页:老 Yii examine/source 已迁移到 xk-api /pharmacist-examine/source
import {
source,
} from '@/api/all.js'
getExamineSource,
} from '@/api/pharmacistExamine.js'
import {
unwrapXkApi
} from '@/utils/api-response.js'
export default {
data() {
return {
@@ -114,22 +118,25 @@
},
onLoad(op) {
this.source({
prescription_id: op['prescription_id'],
prescription_no: op['prescription_no'],
store_id: uni.getStorageSync('store_id') || '11001',
})
},
methods: {
source(data) {
source(data).then(res => {
if (res.errcode == 0) {
this.info = res.data;
this.prescription = res.data.prescription
this.type = res.data.type
// this.info['age'] = this.$d.getAge(this.info.prescription['patients']['id_card'])
getExamineSource(data).then(res => {
const { ok, payload, message } = unwrapXkApi(res)
if (ok) {
// 时间字段后端已格式化为字符串,截取到分钟;patients 兜底空对象防模板取值报错
const prescription = payload.prescription || {}
prescription.patients = prescription.patients || {}
prescription.created_at = (prescription.created_at || '').slice(0, 16)
prescription.patients.updated_at = (prescription.patients.updated_at || '').slice(0, 16)
this.info = payload;
this.prescription = prescription
this.type = payload.type
this.loading = false
} else {
this.$toast(res.msg);
this.$toast(message);
this.loading = false
}
})

View File

@@ -1,197 +0,0 @@
<template>
<view class="safe-area-inset-bottom p-2">
<view class="search p-row-32">
<u-search
placeholder="请输入患者姓名、标签"
v-model="keyword"
@change="change"
:show-action="false"
></u-search>
</view>
<!-- <view class="p-32 white b-r-8 flex-col" v-if="!loading&&list.length>0"> -->
<view
class="p-32 white b-r-8 flex-col"
v-for="(item, i) in list"
:key="item.id"
>
<view class="flex-row flex-ali-center flex-jus-sp">
<view class="flex-row flex-ali-center flex-jus-sp">
<view style="position: relative">
<u-image
width="80rpx"
height="80rpx"
shape="circle"
:src="item.avator || '../../static/image/djr.png'"
>
<u-loading slot="loading"></u-loading>
</u-image>
</view>
<view class="m-l-2 flex-col flex-jus-sp">
<view class="flex-row flex-ali-center">
<d-text
:text="item.name"
className="fs-32 "
color="#31353D"
></d-text>
<d-text
:text="item.title"
color="#31353D"
className="m-l-2"
></d-text>
</view>
<d-text
:text="`${item.hospital}(${item.yard}) ${item.depart}`"
color="#6C7380"
className="fs-28"
></d-text>
</view>
</view>
<view>
<u-button
size="mini"
shape="circle"
:ripple="true"
type="primary"
@click="select(item)"
>选择
</u-button>
</view>
</view>
</view>
<view class="flex-row flex-jus-center" v-if="!loading && list.length == 0">
<d-empty text="暂无医生" mode="search"></d-empty>
</view>
<u-loadmore
:status="status"
:load-text="loadText"
v-if="!loading && list.length > 0"
/>
<d-loading-page :loading="loading"></d-loading-page>
</view>
</template>
<script>
import { doctorLists } from "@/api/all.js";
export default {
data() {
return {
list: [],
page: 1,
keyword: "",
loading: true,
loading2: false,
status: "loading",
loadText: {
loadmore: "轻轻上拉",
loading: "努力加载中",
nomore: "已加载全部",
},
noMore: false,
};
},
// onLoad(op) {
// this.loading = false
// this.list = op['list'] ? JSON.parse(op['list']) : []
// },
onShow() {
this.init();
},
onPullDownRefresh() {
this.init();
},
onReachBottom() {
if (this.noMore) {
this.$toast("数据已全部加载");
return;
}
this.page++;
this.getList();
},
methods: {
select(item) {
uni.showModal({
confirmText:'确定发送',
content:`发送 ${item.name} 医生的名片到当前聊天`,
success: function (res) {
if (res.confirm) {
uni.$emit("newAddPv", item);
uni.navigateBack({
delta: 1, //返回层数,2则上上页
});
} else if (res.cancel) {
console.log("用户点击取消");
}
},
});
},
change() {
this.list = [];
this.page = 1;
uni.showLoading({
title: "搜索中...",
});
uni.$u.debounce(this.getList, 600);
},
init() {
this.list = [];
this.page = 1;
this.keyword = "";
this.loading = true;
this.getList(true);
},
handleSearch() {
this.page = 1;
this.getList();
},
getList(is_stop = false) {
if (this.loading2) {
return;
}
this.loading2 = true;
doctorLists({
page: this.page,
keyword: this.keyword,
})
.then((res) => {
if (res.errcode == 0) {
this.list =
this.page === 1
? res.data.list
: [...this.list, ...res.data.list];
this.noMore = res.data.list.length < 20;
if (this.noMore) {
this.status = "nomore";
} else {
this.status = "loadmore";
}
this.$nextTick(() => {
this.loading = false;
is_stop && uni.stopPullDownRefresh();
});
} else {
this.$toast(res.msg);
is_stop && uni.stopPullDownRefresh();
this.loading = false;
}
})
.finally(() => {
uni.hideLoading();
this.loading2 = false;
});
},
},
};
</script>
<style>
page {
background-color: #f9fafb;
}
</style>
<style lang="scss" scoped>
.search {
position: relative;
z-index: 982;
}
</style>

View File

@@ -2,6 +2,7 @@ import { platformAdminApi } from '@/api/businessApi.js';
const BUSINESS_PACKAGE_ROOT = '/subPackages/sub_business_shared';
const PLATFORM_HOME = '/subPackages/sub_platform_admin/home/index';
const PLATFORM_FEATURES = '/subPackages/sub_platform_admin/features/index';
const PLATFORM_MY = '/subPackages/sub_platform_admin/my/index';
export function getPlatformAdminContext() {
@@ -29,6 +30,7 @@ export function getPlatformAdminContext() {
},
tabList: [
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: PLATFORM_HOME },
{ text: '功能', iconPath: 'grid', selectedIconPath: 'grid-fill', pagePath: PLATFORM_FEATURES },
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: PLATFORM_MY },
],
};

View File

@@ -10,10 +10,12 @@
<view class="clinic-body" :style="bodyStyle">
<slot />
</view>
<!-- 高度显式传 rpx(100 与 body 预留一致):默认 50px 固定像素在宽屏机型会与内部 rpx 定位的 icon/文字冲突挤压 -->
<u-tabbar
v-if="showTabbar"
:value="tabIndex"
:list="tabList"
:height="100"
active-color="#6ACDBB"
@change="onTabChange"
/>

View File

@@ -0,0 +1,39 @@
<template>
<business-page-layout title="功能" :show-tabbar="true" :tab-index="1">
<workbench-features ref="features" :api="featuresApi"></workbench-features>
</business-page-layout>
</template>
<script>
/**
* 平台管理员端「功能」tab 薄壳页
* 壳页只负责:套本端 layout(navbar/tabbar 配置)+ 注入本端角色接口,
* 搜索/分组滑动/长按收藏/使用上报等全部逻辑在主包共享组件 workbench-features(四端复用)
*/
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
import {
getPlatformAdminWorkbenchEntries,
getPlatformAdminEntryUserData,
togglePlatformAdminFavoriteEntry,
reportPlatformAdminEntryUsage,
} from '@/api/platformAdmin.js'
export default {
components: { BusinessPageLayout },
data() {
return {
/** 注入共享组件的本端接口集合(统一返回 { res, data, ok }) */
featuresApi: {
getEntries: getPlatformAdminWorkbenchEntries,
getUserData: getPlatformAdminEntryUserData,
toggleFavorite: togglePlatformAdminFavoriteEntry,
reportUsage: reportPlatformAdminEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在)
if (this.$refs.features) this.$refs.features.refreshUserData()
},
}
</script>

View File

@@ -2,7 +2,7 @@
<business-page-layout title="工作台" :show-tabbar="true" :tab-index="0">
<view class="workspace-container">
<!-- 极简流线型顶部个人信息区 -->
<!-- 极简流线型顶部个人信息区:右侧日期卡补齐视觉平衡(此前右侧空白失衡) -->
<view class="hero-section">
<view class="brand-info-wrap">
<view class="brand-text">
@@ -10,37 +10,65 @@
<text class="store-name">{{ displayName }}</text>
<text class="store-sub">欢迎回来,今天也要高效工作</text>
</view>
<view class="date-chip">
<text class="date-day">{{ todayDay }}</text>
<text class="date-month">{{ todayMonth }}</text>
</view>
</view>
</view>
<!-- 快捷服务区:多彩微拟物质感 (复用样式逻辑) -->
<view class="content-wrapper">
<view v-for="section in sections" :key="section.key" class="section-panel">
<view class="panel-header">
<text class="panel-title">{{ section.title }}</text>
<!-- 平台看板:hero 下沉浮卡,待办审核数 + 今日订单概览(统一工作台看板规格) -->
<view class="dashboard-wrapper">
<view class="dashboard-card">
<view class="stat-item" @click="go({ path: '/subPackages/sub_platform_admin/store-input/audit/index' })">
<text class="stat-label">待审核门店</text>
<view class="stat-value-group">
<text class="stat-value" :class="{ warn: dashboard.pending_store_audit > 0 }">{{ dashboard.pending_store_audit }}</text>
<text class="stat-unit">家</text>
</view>
</view>
<view class="service-grid">
<view
v-for="item in section.items"
:key="item.key"
class="service-card"
hover-class="service-card-hover"
@click="go(item)"
>
<view class="service-icon-box" :style="{ background: item.theme.bg }">
<!-- 使用 uView 1.x 的 u-icon 组件 -->
<u-icon :name="item.icon" size="38" :color="item.theme.color"></u-icon>
</view>
<view class="service-info">
<text class="service-name">{{ item.label }}</text>
<text class="service-desc">{{ item.desc || '快捷进入' }}</text>
</view>
<view class="stat-item" @click="go({ path: '/subPackages/sub_platform_admin/doctor-input/audit/index' })">
<text class="stat-label">待审核医生</text>
<view class="stat-value-group">
<text class="stat-value" :class="{ warn: dashboard.pending_doctor_audit > 0 }">{{ dashboard.pending_doctor_audit }}</text>
<text class="stat-unit">人</text>
</view>
</view>
<view class="stat-item" @click="go({ path: '/subPackages/sub_platform_admin/withdrawal/audit/index' })">
<text class="stat-label">待审核提现</text>
<view class="stat-value-group">
<text class="stat-value" :class="{ warn: dashboard.pending_withdrawal_count > 0 }">{{ dashboard.pending_withdrawal_count }}</text>
<text class="stat-unit">笔</text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">今日订单</text>
<view class="stat-value-group">
<text class="stat-value">{{ dashboard.today_order_count }}</text>
<text class="stat-unit">单</text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">今日交易额</text>
<view class="stat-value-group">
<text class="stat-value highlight">{{ dashboard.today_order_amount }}</text>
<text class="stat-unit">元</text>
</view>
</view>
</view>
</view>
<!-- 主体内容区:个人功能区(最近使用 / 我的常用 / 更多功能,全部入口收敛到功能 tab) -->
<view class="content-wrapper">
<workbench-user-entries
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
@item-click="onEntryTap"
@more="goFeatures"
></workbench-user-entries>
</view>
</view>
</business-page-layout>
</template>
@@ -49,165 +77,114 @@
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
import { navigateToInputForm } from '@/subPackages/sub_salesperson/common/inputNavigation.js'
import platformAdminApi from '@/api/platformAdmin.js'
const ROOT = '/subPackages/sub_platform_admin'
const SALESPERSON_ROOT = '/subPackages/sub_salesperson'
import { getPlatformAdminDashboardStats, getPlatformAdminEntryUserData, reportPlatformAdminEntryUsage } from '@/api/platformAdmin.js'
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
export default {
components: { BusinessPageLayout },
provide() { return { businessContext: getPlatformAdminContext() } },
data() {
const now = new Date()
return {
displayName: '',
warehouseMenus: [],
// 预定义的全局主题池,用于动态分配没有明确指定主题的项
globalThemes: [
{ bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' }, // 橙金
{ bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' }, // 科技蓝
{ bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' }, // 优雅紫
{ bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' }, // 安全绿
]
/** hero 右侧日期卡:日 + 月,纯展示用于视觉平衡 */
todayDay: String(now.getDate()),
todayMonth: (now.getMonth() + 1) + '月',
/** 平台看板数据(接口返回前展示 0,避免布局跳动) */
dashboard: {
pending_store_audit: 0,
pending_doctor_audit: 0,
pending_withdrawal_count: 0,
today_order_count: 0,
today_order_amount: 0,
},
/** 我的常用/最近使用原始入口(归一化后存储,行为装饰在 computed 注入保证角标响应式) */
favRaw: [],
recentRaw: [],
/** 卡片形态偏好:true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
}
},
computed: {
sections() {
const shared = '/subPackages/sub_business_shared'
const root = '/subPackages/sub_platform_admin'
return [
{
key: 'finance',
title: '财务管理',
items: [
{
key: 'withdrawal', label: '提现管理', desc: '提现流水查询', path: `${shared}/withdrawal/index`,
icon: 'red-packet-fill', // uView icon
theme: { bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' } // 橙金
},
{
key: 'withdrawal-audit', label: '提现审核', desc: '处理提现申请', path: `${root}/withdrawal/audit/index`,
icon: 'order',
theme: { bg: 'linear-gradient(135deg, #FEE2E2, #FECACA)', color: '#EF4444' } // 警戒红
},
{
key: 'reconciliation', label: '对账单', desc: '财务对账明细', path: `${shared}/reconciliation/index`,
icon: 'file-text-fill',
theme: { bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' } // 科技蓝
},
],
},
{
key: 'business',
title: '业务管理',
items: [
{
key: 'order', label: '商品订单', desc: '全平台订单概览', path: `${shared}/order/index`,
icon: 'bag-fill',
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`,
icon: 'home-fill',
theme: { bg: 'linear-gradient(135deg, #ECFEFF, #CFFAFE)', color: '#06B6D4' }
},
{
key: 'salesperson-manage', label: '推广员管理', desc: '全平台推广员', path: '/subPackages/sub_salesperson_manage/index?mode=platform',
icon: 'account-fill',
theme: { bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' }
}
],
},
{
key: 'warehouse',
title: '仓库管理',
// 动态加载的仓库数据,在 onShow 中会进行加工
items: this.warehouseMenus.length ? this.warehouseMenus : [
// 提供一个默认的占位符,防止异步加载完成前显示空白
{
key: 'warehouse-loading', label: '总仓库', desc: '库存管理中心', path: '/subPackages/sub_business_shared/warehouse/index',
icon: 'car-fill',
theme: { bg: 'linear-gradient(135deg, #F3F4F6, #E5E7EB)', color: '#6B7280' } // 占位灰
}
]
},
{
key: 'input-audit',
title: '信息审核',
items: [
{
key: 'store-audit', label: '门店审核', desc: '新入门店审批', path: `${root}/store-input/audit/index`,
icon: 'home-fill',
theme: { bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' } // 优雅紫
},
{
key: 'doctor-audit', label: '医生审核', desc: '入驻医生资质', path: `${root}/doctor-input/audit/index`,
icon: 'account-fill',
theme: { bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' } // 科技蓝
},
],
},
{
key: 'input-create',
title: '信息录入',
items: [
{
key: 'store-list', label: '门店列表', desc: '平台门店档案', path: `${SALESPERSON_ROOT}/store-input/index`,
icon: 'grid-fill',
theme: { bg: 'linear-gradient(135deg, #ECFEFF, #CFFAFE)', color: '#06B6D4' } // 青色
},
{
key: 'store-form', label: '门店录入', desc: '新增线下门店', path: `${SALESPERSON_ROOT}/store-input/form`, inputType: 'store', isForm: true,
icon: 'plus-square-fill',
theme: { bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' } // 绿色
},
{
key: 'doctor-list', label: '医生列表', desc: '平台医生名册', path: `${SALESPERSON_ROOT}/doctor-input/index`,
icon: 'list-dot',
theme: { bg: 'linear-gradient(135deg, #FFFBEB, #FEF3C7)', color: '#D97706' } // 琥珀色
},
{
key: 'doctor-form', label: '医生录入', desc: '新增合作医生', path: `${SALESPERSON_ROOT}/doctor-input/form`, inputType: 'doctor', isForm: true,
icon: 'plus-circle-fill',
theme: { bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' } // 橙金
},
],
},
]
/** 我的常用(角标/表单预填等行为装饰在这里注入,保证看板数据返回后角标响应式更新) */
favItems() {
return this.favRaw.map((item) => this.decorateItem(item))
},
/** 最近使用 */
recentItems() {
return this.recentRaw.map((item) => this.decorateItem(item))
},
},
onShow() {
const user = uni.getStorageSync('platform_admin_user') || {}
this.displayName = user.nick_name || '平台管理员'
// 加载仓库数据
platformAdminApi.getWarehouseProductTypeOptions().then(w => {
const options = Array.isArray(w.data) ? w.data : []
if (!options.length) return
this.warehouseMenus = options.map((opt, index) => {
const value = opt.value != null ? opt.value : opt.id
const label = opt.label || opt.name || '仓库'
// 为动态仓库分配不同的主题色
const theme = this.globalThemes[index % this.globalThemes.length]
return {
key: 'warehouse-' + value,
label,
desc: `${label}库存管理`, // 增强描述
icon: 'car-fill', // 仓库类目统一使用 uView 的车/物流图标
theme: theme,
path: `/subPackages/sub_business_shared/warehouse/index?type=${encodeURIComponent(value)}&label=${encodeURIComponent(label)}`,
}
})
}).catch(() => {})
// 卡片形态偏好每次 onShow 重读:我的页切换后返回立即生效
this.cardLong = getCardStyle() === 'long'
this.loadDashboard()
this.loadUserEntries()
},
methods: {
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底) */
async loadUserEntries() {
try {
const wrap = await getPlatformAdminEntryUserData()
if (wrap && wrap.ok && wrap.data) {
this.favRaw = this.normalizeList(wrap.data.favorites)
this.recentRaw = this.normalizeList(wrap.data.recents)
}
} catch (e) {
console.error('常用/最近使用加载失败', e)
}
},
/** 后端入口对象数组 → 渲染项数组(与功能页同一映射) */
normalizeList(list) {
if (!Array.isArray(list)) return []
const items = []
list.forEach((entry) => {
const item = normalizeEntry(entry)
if (item) items.push(item)
})
return items
},
/** 按 code 注入本端行为:审核类入口待办角标、录入表单预填标记 */
decorateItem(item) {
const badgeMap = {
platform_withdrawal_audit: this.dashboard.pending_withdrawal_count,
platform_store_audit: this.dashboard.pending_store_audit,
platform_doctor_audit: this.dashboard.pending_doctor_audit,
}
const formMap = { platform_store_form: 'store', platform_doctor_form: 'doctor' }
const next = { ...item }
if (badgeMap[item.key] != null) {
next.badge = badgeMap[item.key]
}
if (formMap[item.key]) {
next.isForm = true
next.inputType = formMap[item.key]
}
return next
},
/** 拉取平台看板统计:失败静默保持 0,不阻塞首页其他模块 */
async loadDashboard() {
try {
const wrap = await getPlatformAdminDashboardStats()
if (wrap && wrap.ok && wrap.data) {
this.dashboard = { ...this.dashboard, ...wrap.data }
}
} catch (e) {
console.error('看板统计加载失败', e)
}
},
/** 入口点击:静默上报使用(保持最近使用新鲜)→ 表单预填/普通跳转 */
onEntryTap(item) {
reportPlatformAdminEntryUsage(item.key).catch(() => {})
this.go(item)
},
/** 更多功能/空态引导:切到功能 tab(tabbar 页统一走 reLaunch) */
goFeatures() {
uni.reLaunch({ url: '/subPackages/sub_platform_admin/features/index' })
},
go(item) {
if (item.isForm && item.inputType) {
navigateToInputForm(item.path, item.inputType)
@@ -278,78 +255,93 @@ export default {
display: block;
}
/* ================= 主体内容区:快捷服务 ================= */
/* hero 右侧日期卡:毛玻璃白底小方卡,与推广员端二维码位同规格 */
.date-chip {
width: 110rpx;
height: 110rpx;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.date-day {
font-size: 44rpx;
font-weight: 700;
color: #fff;
line-height: 1.1;
font-family: "DIN Alternate", sans-serif;
}
.date-month {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.85);
}
/* ================= 平台看板:hero 下沉浮白卡(统一工作台看板规格) ================= */
.dashboard-wrapper {
margin: -30rpx 30rpx 0;
position: relative;
z-index: 11;
}
.dashboard-card {
background: #fff;
border-radius: 24rpx;
padding: 32rpx 20rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
row-gap: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-label {
font-size: 22rpx;
color: #94A3B8;
margin-bottom: 10rpx;
}
.stat-value-group {
display: flex;
align-items: baseline;
}
/* 数值:DIN 数字字体统一看板视觉语言 */
.stat-value {
font-size: 34rpx;
font-weight: 700;
color: #1E293B;
font-family: "DIN Alternate", sans-serif;
}
.stat-value.highlight {
color: #059669;
}
.stat-value.warn {
color: #F97316;
}
.stat-unit {
font-size: 20rpx;
color: #94A3B8;
margin-left: 4rpx;
}
/* ================= 主体内容区:个人功能区(分区/宫格样式在 workbench-user-entries 组件内) ================= */
.content-wrapper {
margin-top: 30rpx; /* 轻微向上偏移,与顶部区域产生连接感 */
position: relative;
z-index: 10;
}
.section-panel {
margin: 0 30rpx 30rpx; /* 增加底部间距 */
background: transparent;
}
.panel-header {
margin-bottom: 24rpx;
padding: 0 10rpx;
}
.panel-title {
font-size: 34rpx;
font-weight: 700;
color: #1E293B; /* 深石板灰 */
}
/* 采用与之前页面一致的宫格卡片布局 */
.service-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.service-card {
background: #FFFFFF;
border-radius: 28rpx;
padding: 30rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
transition: all 0.2s ease;
border: 1px solid rgba(0, 0, 0, 0.01);
}
.service-card-hover {
transform: translateY(-2rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.service-icon-box {
width: 76rpx;
height: 76rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.5); /* 顶部微高光,增加拟物质感 */
}
.service-info {
display: flex;
flex-direction: column;
}
.service-name {
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
margin-bottom: 6rpx;
}
.service-desc {
font-size: 20rpx;
color: #94A3B8; /* 银灰色 */
}
</style>

View File

@@ -4,13 +4,18 @@
<view v-if="!auditMode" class="toolbar">
<button class="btn" @click="goForm()">新建录入</button>
</view>
<view v-for="item in list" :key="item.id" class="card" @click="goDetail(item)">
<view class="row">
<text class="name">{{ item.name }}</text>
<text class="status" :class="'s' + item.status">{{ statusText(item.status) }}</text>
</view>
<view class="sub">{{ item.mobile }} · {{ displayTime(item.created_at) }}</view>
</view>
<!-- 录入/审核列表卡:统一 xk-list-card 规范(首字头像 + 状态色 + 查看操作) -->
<xk-list-card
v-for="item in list" :key="item.id"
:avatar-text="item.name"
:title="item.name"
:time="displayTime(item.created_at)"
:rows="[item.mobile || '-']"
:status="{ text: statusText(item.status), color: statusColor(item.status) }"
:actions="[{ key: 'detail', text: auditMode ? '去审核' : '查看详情', type: auditMode ? 'primary' : 'ghost' }]"
@action="goDetail(item)"
@click="goDetail(item)"
></xk-list-card>
<view v-if="!list.length" class="empty">暂无记录</view>
</view>
</business-page-layout>
@@ -72,6 +77,10 @@ export default {
statusText(s) {
return { 0: '待审核', 1: '已通过', 2: '已拒绝' }[Number(s)] || '-'
},
/** 状态色映射:与 statusText 对应(橙=待审核 / 绿=通过 / 红=拒绝) */
statusColor(s) {
return { 0: '#FF7D00', 1: '#00B42A', 2: '#F53F3F' }[Number(s)] || '#999'
},
displayTime(t) {
if (t == null || t === '') return '-'
if (typeof t === 'string' && (t.includes('-') || t.includes(':'))) {
@@ -98,13 +107,5 @@ export default {
.page-wrap { padding: 24rpx; }
.toolbar { margin-bottom: 24rpx; }
.btn { background: #6acdbb; color: #fff; font-size: 28rpx; }
.card { background: #fff; padding: 28rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 30rpx; font-weight: 600; }
.status { font-size: 24rpx; }
.s0 { color: #FF7D00; }
.s1 { color: #00B42A; }
.s2 { color: #F53F3F; }
.sub { margin-top: 12rpx; color: #86909C; font-size: 24rpx; }
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
</style>

View File

@@ -1,79 +1,38 @@
<template>
<business-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container">
<view class="profile-panel">
<view class="avatar-circle"><text>{{ avatarText }}</text></view>
<view class="user-info">
<text class="user-name">{{ displayName }}</text>
<view class="role-badge"><text class="role-text">{{ roleText }}</text></view>
</view>
</view>
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
</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>
<!-- 页面主体统一收进 admin-my-panel(资料区 + 个人中心入口 + 菜单卡 + 退出) -->
<admin-my-panel
ref="myPanel"
:display-name="displayName"
:avatar="avatar"
:role-text="roleText"
fallback-char="管"
/>
</business-page-layout>
</template>
<script>
/**
* 平台管理员「我的」:对齐医生(菜单卡 + 绿色退出),保留绑定微信
* 平台管理员「我的」:主体复用 admin-my-panel 共享组件
* 本页只保留 layout、businessContext 注入与本地缓存读取
*/
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { BusinessPageLayout, AccountSwitchEntry, WxBindEntry },
components: { BusinessPageLayout, AdminMyPanel },
provide() { return { businessContext: getPlatformAdminContext() } },
data() { return { displayName: '', roleText: '平台管理员' } },
computed: {
avatarText() { return (this.displayName || '管').slice(0, 1) },
data() {
return { displayName: '', avatar: '', roleText: '平台管理员' }
},
onShow() {
// 个人中心改完昵称/头像返回时重读缓存立即生效
const user = uni.getStorageSync('platform_admin_user') || {}
this.displayName = user.nick_name || '管理员'
this.avatar = user.avatar || ''
this.roleText = (user.roles && user.roles.name) || '平台管理员'
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
},
methods: {
logout() {
uni.showModal({
title: '提示', content: '确定退出登录?', confirmColor: '#6ACDBB',
success: (res) => {
if (!res.confirm) return
clearLoginStorage()
uni.reLaunch({ url: '/pages/login/index' })
},
})
},
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
}
</script>
<style lang="scss" scoped>
.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; }
.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-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; }
.setting-panel { background: #fff; padding: 8rpx 40rpx 16rpx; margin-bottom: 24rpx; }
.logout-wrap { padding: 0 40rpx 32rpx; }
</style>

View File

@@ -11,29 +11,31 @@
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!list.length" class="empty">暂无门店</view>
<view
<!-- 门店列表卡:统一 xk-list-card 规范(类型标签 + 地址/联系信息 + 权益标签 + 管理操作) -->
<xk-list-card
v-for="item in list"
:key="item.id"
class="card"
hover-class="card-hover"
:avatar-text="item.name"
:title="item.name"
:tags="[{ text: clinicTypeText(item.clinic_type), type: 'primary' }]"
:actions="[{ key: 'detail', text: '管理门店', type: 'primary' }]"
@action="goDetail(item)"
@click="goDetail(item)"
>
<view class="card-head">
<text class="name">{{ item.name }}</text>
<text class="type-tag">{{ clinicTypeText(item.clinic_type) }}</text>
</view>
<text class="addr">{{ item.position || '暂无地址' }}</text>
<view class="meta-row">
<text>ID {{ item.id }}</text>
<text v-if="item.mobile">电话 {{ item.mobile }}</text>
</view>
<view class="tag-row">
<text class="mini-tag" :class="{ on: Number(item.salesperson_see_price) === 1 }">推广员可见价格</text>
<text class="mini-tag" :class="{ on: Number(item.is_shipping_free) === 1 }">包邮</text>
<text v-if="Number(item.bank_card_report_status) === 0" class="mini-tag warn">需报备银行卡</text>
<text v-else-if="item.bank_card_report_status_text" class="mini-tag bank">{{ item.bank_card_report_status_text }}</text>
</view>
</view>
<template #body>
<text class="addr">{{ item.position || '暂无地址' }}</text>
<view class="meta-row">
<text>ID {{ item.id }}</text>
<text v-if="item.mobile">电话 {{ item.mobile }}</text>
</view>
<view class="tag-row">
<text class="mini-tag" :class="{ on: Number(item.salesperson_see_price) === 1 }">推广员可见价格</text>
<text class="mini-tag" :class="{ on: Number(item.is_shipping_free) === 1 }">包邮</text>
<text v-if="Number(item.bank_card_report_status) === 0" class="mini-tag warn">需报备银行卡</text>
<text v-else-if="item.bank_card_report_status_text" class="mini-tag bank">{{ item.bank_card_report_status_text }}</text>
</view>
</template>
</xk-list-card>
</view>
</store-page-layout>
</template>
@@ -110,14 +112,7 @@ export default {
background: #6ACDBB; color: #fff; padding: 16rpx 32rpx; border-radius: 12rpx; font-size: 26rpx;
}
.empty { text-align: center; color: #999; padding: 80rpx 0; font-size: 28rpx; }
.card {
background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx;
}
.card-hover { opacity: 0.92; }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
.name { font-size: 30rpx; font-weight: 600; color: #1f2937; }
.type-tag { font-size: 22rpx; color: #6ACDBB; background: #f0fbf8; padding: 4rpx 12rpx; border-radius: 8rpx; }
.addr { display: block; font-size: 24rpx; color: #6b7280; margin-bottom: 12rpx; }
.addr { display: block; font-size: 24rpx; color: #6b7280; margin: 8rpx 0 12rpx; }
.meta-row { display: flex; gap: 24rpx; font-size: 22rpx; color: #9ca3af; margin-bottom: 12rpx; }
.tag-row { display: flex; flex-wrap: wrap; gap: 8rpx; }
.mini-tag {

View File

@@ -3,6 +3,7 @@ import { getPlatformAdminContext } from '@/subPackages/sub_business_shared/commo
const BUSINESS_PACKAGE_ROOT = '/subPackages/sub_business_shared';
const SALESPERSON_HOME = '/subPackages/sub_salesperson/home/index';
const SALESPERSON_FEATURES = '/subPackages/sub_salesperson/features/index';
const SALESPERSON_MY = '/subPackages/sub_salesperson/my/index';
export function getSalespersonContext() {
@@ -24,6 +25,7 @@ export function getSalespersonContext() {
},
tabList: [
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: SALESPERSON_HOME },
{ text: '功能', iconPath: 'grid', selectedIconPath: 'grid-fill', pagePath: SALESPERSON_FEATURES },
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: SALESPERSON_MY },
],
};

View File

@@ -11,10 +11,12 @@
<slot />
</view>
<image-gallery-picker />
<!-- 高度显式传 rpx(100 与 body 预留一致):默认 50px 固定像素在宽屏机型会与内部 rpx 定位的 icon/文字冲突挤压 -->
<u-tabbar
v-if="showTabbar"
:value="tabIndex"
:list="tabList"
:height="100"
active-color="#6ACDBB"
@change="onTabChange"
/>

View File

@@ -10,10 +10,12 @@
<view class="clinic-body" :style="bodyStyle">
<slot />
</view>
<!-- 高度显式传 rpx(100 与 body 预留一致):默认 50px 固定像素在宽屏机型会与内部 rpx 定位的 icon/文字冲突挤压 -->
<u-tabbar
v-if="showTabbar"
:value="tabIndex"
:list="tabList"
:height="100"
active-color="#6ACDBB"
@change="onTabChange"
/>

View File

@@ -0,0 +1,39 @@
<template>
<business-page-layout title="功能" :show-tabbar="true" :tab-index="1">
<workbench-features ref="features" :api="featuresApi"></workbench-features>
</business-page-layout>
</template>
<script>
/**
* 业务员端「功能」tab 薄壳页
* 壳页只负责:套本端 layout(navbar/tabbar 配置)+ 注入本端角色接口,
* 搜索/分组滑动/长按收藏/使用上报等全部逻辑在主包共享组件 workbench-features(四端复用)
*/
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import {
getSalespersonWorkbenchEntries,
getSalespersonEntryUserData,
toggleSalespersonFavoriteEntry,
reportSalespersonEntryUsage,
} from '@/api/salesperson.js'
export default {
components: { BusinessPageLayout },
data() {
return {
/** 注入共享组件的本端接口集合(统一返回 { res, data, ok }) */
featuresApi: {
getEntries: getSalespersonWorkbenchEntries,
getUserData: getSalespersonEntryUserData,
toggleFavorite: toggleSalespersonFavoriteEntry,
reportUsage: reportSalespersonEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在)
if (this.$refs.features) this.$refs.features.refreshUserData()
},
}
</script>

View File

@@ -35,31 +35,15 @@
</view>
</view>
<!-- 多彩微拟物双列卡片区 -->
<!-- 主体内容区:个人功能区(最近使用 / 我的常用 / 更多功能,全部入口收敛到功能 tab) -->
<view class="content-wrapper">
<view v-for="section in sections" :key="section.key" class="section-panel">
<view class="panel-header">
<text class="panel-title">{{ section.title }}</text>
</view>
<view class="service-grid">
<view
v-for="item in section.items"
:key="item.key"
class="service-card"
hover-class="service-card-hover"
@click="go(item)"
>
<view class="service-icon-box" :style="{ background: item.theme.bg }">
<u-icon :name="item.icon" size="38" :color="item.theme.color"></u-icon>
</view>
<view class="service-info">
<text class="service-name">{{ item.label }}</text>
<text class="service-desc">{{ item.desc || '快捷进入' }}</text>
</view>
</view>
</view>
</view>
<workbench-user-entries
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
@item-click="onEntryTap"
@more="goFeatures"
></workbench-user-entries>
</view>
</view>
@@ -67,10 +51,11 @@
</template>
<script>
// ... (脚本逻辑保持不变)
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import { getSalespersonInputStats } from '@/api/salesperson.js'
import { getSalespersonInputStats, getSalespersonEntryUserData, reportSalespersonEntryUsage } from '@/api/salesperson.js'
import { navigateToInputForm } from '@/subPackages/sub_salesperson/common/inputNavigation.js'
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
export default {
components: { BusinessPageLayout },
@@ -78,34 +63,71 @@ export default {
return {
displayName: '',
stats: { storeCount: 0, storeApprovedCount: 0, doctorCount: 0, doctorApprovedCount: 0 },
themes: {
store: { bg: 'linear-gradient(135deg, #ECFEFF, #CFFAFE)', color: '#06B6D4' },
doctor: { bg: 'linear-gradient(135deg, #FFFBEB, #FEF3C7)', color: '#D97706' }
}
/** 我的常用/最近使用原始入口(归一化后存储,表单预填装饰在 computed 注入) */
favRaw: [],
recentRaw: [],
/** 卡片形态偏好:true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
}
},
// ... (computed 及 methods 保持原样)
computed: {
sections() {
return [
{ key: 'input', title: '信息录入', items: [
{ key: 'store-form', label: '门店录入', desc: '新增线下门店', path: '/subPackages/sub_salesperson/store-input/form', inputType: 'store', isForm: true, icon: 'plus-square-fill', theme: this.themes.store },
{ 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: '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: 'doctor-list', label: '医生列表', desc: '查看已录医生', path: '/subPackages/sub_salesperson/doctor-input/index', icon: 'account-fill', theme: this.themes.doctor },
]}
]
}
/** 我的常用(录入表单预填标记在这里注入) */
favItems() {
return this.favRaw.map((item) => this.decorateItem(item))
},
/** 最近使用 */
recentItems() {
return this.recentRaw.map((item) => this.decorateItem(item))
},
},
onShow() {
const user = uni.getStorageSync('salesperson_user') || {}
this.displayName = user.nick_name || '业务员'
// 卡片形态偏好每次 onShow 重读:我的页切换后返回立即生效
this.cardLong = getCardStyle() === 'long'
this.fetchDashboardData()
this.loadUserEntries()
},
methods: {
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底) */
async loadUserEntries() {
try {
const wrap = await getSalespersonEntryUserData()
if (wrap && wrap.ok && wrap.data) {
this.favRaw = this.normalizeList(wrap.data.favorites)
this.recentRaw = this.normalizeList(wrap.data.recents)
}
} catch (e) {
console.error('常用/最近使用加载失败', e)
}
},
/** 后端入口对象数组 → 渲染项数组(与功能页同一映射) */
normalizeList(list) {
if (!Array.isArray(list)) return []
const items = []
list.forEach((entry) => {
const item = normalizeEntry(entry)
if (item) items.push(item)
})
return items
},
/** 按 code 注入本端行为:录入表单预填标记 */
decorateItem(item) {
const formMap = { sp_store_form: 'store', sp_doctor_form: 'doctor' }
if (formMap[item.key]) {
return { ...item, isForm: true, inputType: formMap[item.key] }
}
return item
},
/** 入口点击:静默上报使用(保持最近使用新鲜)→ 表单预填/普通跳转 */
onEntryTap(item) {
reportSalespersonEntryUsage(item.key).catch(() => {})
this.go(item)
},
/** 更多功能/空态引导:切到功能 tab(tabbar 页统一走 reLaunch) */
goFeatures() {
uni.reLaunch({ url: '/subPackages/sub_salesperson/features/index' })
},
async fetchDashboardData() {
try {
const wrap = await getSalespersonInputStats()
@@ -141,25 +163,16 @@ export default {
.store-name { font-size: 42rpx; font-weight: 600; color: #fff; }
.store-sub { font-size: 26rpx; color: rgba(255, 255, 255, 0.85); }
/* 看板区域优化 */
/* 看板区域:与各端工作台看板统一规格(24rpx 圆角 + 轻阴影 + DIN 数字) */
.dashboard-wrapper { margin: -40rpx 30rpx 30rpx; }
.dashboard-card { background: #fff; border-radius: 32rpx; padding: 40rpx 30rpx; display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.05); }
.dashboard-card { background: #fff; border-radius: 24rpx; padding: 40rpx 30rpx; display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03); }
.stat-item { display: flex; flex-direction: column; align-items: center; }
.stat-label { font-size: 22rpx; color: #94A3B8; margin-bottom: 12rpx; }
.stat-value-group { display: flex; align-items: baseline; }
.stat-value { font-size: 42rpx; font-weight: 700; color: #1E293B; }
.stat-value { font-size: 42rpx; font-weight: 700; color: #1E293B; font-family: "DIN Alternate", sans-serif; }
.stat-slash { font-size: 32rpx; color: #CBD5E1; margin: 0 8rpx; }
.stat-value-highlight { font-size: 42rpx; font-weight: 700; color: #059669; }
.stat-value-highlight { font-size: 42rpx; font-weight: 700; color: #059669; font-family: "DIN Alternate", sans-serif; }
/* 业务卡片区保持原样 */
/* 个人功能区(分区/宫格样式在 workbench-user-entries 组件内) */
.content-wrapper { position: relative; z-index: 10; }
.section-panel { margin: 0 30rpx 30rpx; }
.panel-title { font-size: 34rpx; font-weight: 700; color: #1E293B; margin-bottom: 24rpx; display: block; padding-left: 10rpx;}
.service-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; }
.service-card { background: #FFFFFF; border-radius: 28rpx; padding: 30rpx 24rpx; display: flex; align-items: center; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02); }
.service-card-hover { transform: translateY(-2rpx); }
.service-icon-box { width: 76rpx; height: 76rpx; border-radius: 22rpx; display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.5); }
.service-info { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
.service-name { font-size: 28rpx; font-weight: 600; color: #1E293B; line-height: 1.2; }
.service-desc { font-size: 20rpx; color: #94A3B8; margin-top: 4rpx; line-height: 1.2; }
</style>

View File

@@ -6,18 +6,18 @@
:input-type="inputType"
@select="onDraftSelect"
/>
<view v-for="item in list" :key="item.id" class="card">
<view class="row" @click="goDetail(item)">
<text class="name">{{ item.name }}</text>
<text class="status" :class="'s' + item.status">{{ statusText(item.status) }}</text>
</view>
<view class="sub" @click="goDetail(item)">{{ item.mobile }} · {{ displayTime(item.created_at) }}</view>
<view class="card-actions" @click.stop>
<button class="mini-btn outline" @click="goDetail(item)">查看详情</button>
<button v-if="canEditItem(item)" class="mini-btn outline" @click="goEdit(item)">继续编辑</button>
<button v-if="showDoctorEntry" class="mini-btn" @click="goDoctorInput(item)">录入医生</button>
</view>
</view>
<!-- 录入列表卡:统一 xk-list-card 规范(首字头像 + 状态色 + 主/次操作按钮) -->
<xk-list-card
v-for="item in list" :key="item.id"
:avatar-text="item.name"
:title="item.name"
:time="displayTime(item.created_at)"
:rows="[item.mobile || '-']"
:status="{ text: statusText(item.status), color: statusColor(item.status) }"
:actions="cardActions(item)"
@action="onCardAction($event, item)"
@click="goDetail(item)"
></xk-list-card>
<view v-if="!list.length" class="empty">暂无记录</view>
</view>
<view v-if="showListFooter" class="list-footer">
@@ -100,6 +100,23 @@ export default {
statusText(s) {
return { 0: '待审核', 1: '已通过', 2: '已拒绝' }[Number(s)] || '-'
},
/** 状态色映射:与 statusText 对应(橙=待审核 / 绿=通过 / 红=拒绝) */
statusColor(s) {
return { 0: '#FF7D00', 1: '#00B42A', 2: '#F53F3F' }[Number(s)] || '#999'
},
/** 组装卡片操作按钮:主操作放最后(录入医生),次操作用浅底描边 */
cardActions(item) {
const actions = [{ key: 'detail', text: '查看详情', type: 'ghost' }]
if (this.canEditItem(item)) actions.push({ key: 'edit', text: '继续编辑', type: 'ghost' })
if (this.showDoctorEntry) actions.push({ key: 'doctor', text: '录入医生', type: 'primary' })
return actions
},
/** 卡片操作分发:按 key 路由到对应跳转 */
onCardAction(act, item) {
if (act.key === 'edit') return this.goEdit(item)
if (act.key === 'doctor') return this.goDoctorInput(item)
this.goDetail(item)
},
displayTime(t) {
if (t == null || t === '') return '-'
if (typeof t === 'string' && (t.includes('-') || t.includes(':'))) {
@@ -156,16 +173,5 @@ export default {
padding: 22rpx;
margin: 0;
}
.card { background: #fff; padding: 28rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 30rpx; font-weight: 600; }
.status { font-size: 24rpx; }
.s0 { color: #FF7D00; }
.s1 { color: #00B42A; }
.s2 { color: #F53F3F; }
.sub { margin-top: 12rpx; color: #86909C; font-size: 24rpx; }
.card-actions { margin-top: 16rpx; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 12rpx; }
.mini-btn { font-size: 24rpx; padding: 8rpx 24rpx; background: #6acdbb; color: #fff; line-height: 1.6; margin: 0; }
.mini-btn.outline { background: #fff; color: #6acdbb; border: 1rpx solid #6acdbb; }
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
</style>

View File

@@ -1,77 +1,38 @@
<template>
<business-page-layout title="我的" :show-tabbar="true" :tab-index="1">
<view class="mine-container">
<view class="profile-panel">
<view class="avatar-circle"><text>{{ avatarText }}</text></view>
<view class="user-info">
<text class="user-name">{{ displayName }}</text>
<view class="role-badge"><text class="role-text">{{ roleText }}</text></view>
</view>
</view>
<view class="setting-panel">
<wx-bind-entry ref="wxBindEntry" />
<account-switch-entry />
</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>
<!-- 页面主体统一收进 admin-my-panel(资料区 + 个人中心入口 + 菜单卡 + 退出) -->
<admin-my-panel
ref="myPanel"
:display-name="displayName"
:avatar="avatar"
:role-text="roleText"
fallback-char="业"
/>
</business-page-layout>
</template>
<script>
/**
* 业务员「我的」:对齐医生(菜单卡 + 绿色退出),保留绑定微信
* 业务员「我的」:主体复用 admin-my-panel 共享组件
* 本页只保留 layout、businessContext 注入与本地缓存读取
*/
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
import WxBindEntry from '@/components/wx-bind-entry/wx-bind-entry.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getSalespersonContext } from '@/subPackages/sub_salesperson/common/context.js'
import { clearLoginStorage } from '@/utils/loginSession.js'
export default {
components: { BusinessPageLayout, AccountSwitchEntry, WxBindEntry },
components: { BusinessPageLayout, AdminMyPanel },
provide() { return { businessContext: getSalespersonContext() } },
data() { return { displayName: '', roleText: '业务员' } },
computed: { avatarText() { return (this.displayName || '业').slice(0, 1) } },
data() {
return { displayName: '', avatar: '', roleText: '业务员' }
},
onShow() {
// 个人中心改完昵称/头像返回时重读缓存立即生效
const user = uni.getStorageSync('salesperson_user') || {}
this.displayName = user.nick_name || '业务员'
this.avatar = user.avatar || ''
this.roleText = (user.roles && user.roles.name) || '业务员'
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh()
},
methods: {
logout() {
uni.showModal({
title: '提示', content: '确定退出登录?', confirmColor: '#6ACDBB',
success: (res) => {
if (!res.confirm) return
clearLoginStorage()
uni.reLaunch({ url: '/pages/login/index' })
},
})
},
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
}
</script>
<style lang="scss" scoped>
.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; }
.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-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; }
.setting-panel { background: #fff; padding: 8rpx 40rpx 16rpx; margin-bottom: 24rpx; }
.logout-wrap { padding: 0 40rpx 32rpx; }
</style>

View File

@@ -20,42 +20,46 @@
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="!list.length" class="empty">暂无推广员</view>
<!-- 推广员卡:信息分层(姓名/门店/分成标签)+ 三列统计带 + 主/次操作胶囊按钮 -->
<view v-for="item in list" :key="item.id" class="card">
<view class="card-head">
<view>
<text class="name">{{ item.nick_name || '未知' }}</text>
<text v-if="item.store_name" class="store">{{ item.store_name }}</text>
<view class="head-left">
<view class="avatar-text">{{ (item.nick_name || '未')[0] }}</view>
<view class="head-info">
<text class="name">{{ item.nick_name || '未知' }}</text>
<text class="store">{{ item.store_name || item.phone }}</text>
</view>
</view>
<view class="tags">
<text class="tag">{{ splitLabel(item.split_type) }}</text>
<text v-if="Number(item.tcm_split) > 0" class="tag tcm">中药 {{ item.tcm_split }}%</text>
</view>
</view>
<view class="card-body">
<text class="phone">{{ item.phone }}</text>
<view class="stat-row">
<text>累计获客</text>
<text class="num">{{ (item.count && item.count.user_number) || 0 }}</text>
<!-- 统计带:三列并排,数值突出(对齐工作台看板视觉) -->
<view class="stat-strip">
<view class="stat-cell">
<text class="stat-num">{{ (item.count && item.count.user_number) || 0 }}</text>
<text class="stat-label">累计获客</text>
</view>
<view class="stat-row">
<text>待结算</text>
<text class="num pending">¥{{ (item.count && item.count.pending_amount) || '0.00' }}</text>
<view class="stat-cell">
<text class="stat-num pending">¥{{ (item.count && item.count.pending_amount) || '0.00' }}</text>
<text class="stat-label">待结算</text>
</view>
<view class="stat-row">
<text>后台账号</text>
<text :class="item.has_admin_account ? 'ok' : 'warn'">{{ item.has_admin_account ? '已开通' : '未开通' }}</text>
<view class="stat-cell">
<text class="stat-num" :class="item.has_admin_account ? 'ok' : 'warn'">{{ item.has_admin_account ? '已开通' : '未开通' }}</text>
<text class="stat-label">后台账号</text>
</view>
</view>
<view class="actions">
<view class="action-btn" @click="goEdit(item)">编辑</view>
<view class="action-btn primary" @click="goCommission(item)">服务费与结算</view>
<view v-if="item.qr_code && item.qr_code.qr_code" class="action-btn" @click="openQr(item)">二维码</view>
<view class="action-btn" @click="goLeads(item)">获客</view>
<view class="action-btn ghost" @click="goEdit(item)">编辑</view>
<view v-if="item.qr_code && item.qr_code.qr_code" class="action-btn ghost" @click="openQr(item)">二维码</view>
<view class="action-btn ghost" @click="goLeads(item)">获客</view>
<view
v-if="!item.has_admin_account"
class="action-btn warn"
@click="openAdminAccount(item)"
>开通后台</view>
<view class="action-btn primary" @click="goCommission(item)">服务费与结算</view>
</view>
</view>
</view>
@@ -200,27 +204,47 @@ export default {
text-align: center; padding: 20rpx; border-radius: 16rpx; font-size: 28rpx;
}
.loading-tip, .empty { text-align: center; color: #999; padding: 80rpx 0; }
/* 卡片容器:与 xk-list-card 同规格(24rpx 圆角 + 轻阴影) */
.card {
background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
background: #fff; border-radius: 24rpx; padding: 28rpx 24rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
}
.card-head { display: flex; justify-content: space-between; margin-bottom: 16rpx; }
.name { display: block; font-size: 32rpx; font-weight: 600; }
.store { font-size: 24rpx; color: #999; }
.tags { display: flex; flex-direction: column; gap: 8rpx; align-items: flex-end; }
.tag { font-size: 22rpx; background: #EFF6FF; color: #3B82F6; padding: 4rpx 12rpx; border-radius: 8rpx; }
.card-head { display: flex; justify-content: space-between; align-items: flex-start; }
.head-left { display: flex; align-items: center; min-width: 0; }
/* 首字头像:青绿浅底,与 xk-list-card 兜底头像一致 */
.avatar-text {
width: 80rpx; height: 80rpx; border-radius: 50%; flex-shrink: 0;
background: #e8f7f3; color: #4db6a8; font-size: 32rpx; font-weight: 600;
display: flex; align-items: center; justify-content: center; margin-right: 20rpx;
}
.head-info { display: flex; flex-direction: column; min-width: 0; }
.name { font-size: 32rpx; font-weight: 500; color: #333; }
.store { font-size: 24rpx; color: #999; margin-top: 4rpx; }
.tags { display: flex; flex-direction: column; gap: 8rpx; align-items: flex-end; flex-shrink: 0; }
.tag { font-size: 22rpx; background: #EFF6FF; color: #3B82F6; padding: 4rpx 12rpx; border-radius: 20rpx; }
.tag.tcm { background: #F0FDF4; color: #22C55E; }
.phone { font-size: 26rpx; color: #666; display: block; margin-bottom: 12rpx; }
.stat-row { display: flex; justify-content: space-between; font-size: 26rpx; margin-bottom: 8rpx; }
.num { font-weight: 600; }
/* 统计带:浅灰底圆角内嵌,三列均分,数值加粗突出 */
.stat-strip {
display: flex; background: #f8fafb; border-radius: 16rpx;
padding: 20rpx 0; margin-top: 20rpx;
}
.stat-cell {
flex: 1; display: flex; flex-direction: column; align-items: center;
border-right: 1rpx solid #eef1f3;
&:last-child { border-right: none; }
}
.stat-num { font-size: 30rpx; font-weight: 600; color: #333; }
.stat-label { font-size: 22rpx; color: #94A3B8; margin-top: 6rpx; }
.pending { color: #F97316; }
.ok { color: #22C55E; }
.warn { color: #EF4444; }
.actions { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 20rpx; }
/* 操作区:胶囊按钮,主操作实心青绿、次操作浅底描边、警示操作琥珀 */
.actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 16rpx; margin-top: 20rpx; }
.action-btn {
font-size: 24rpx; padding: 10rpx 20rpx; border-radius: 10rpx;
background: #f5f5f5; color: #333;
font-size: 24rpx; padding: 10rpx 28rpx; border-radius: 30rpx;
font-weight: 500; line-height: 1.4; text-align: center;
}
.action-btn.ghost { color: #6ACDBB; background: #f0f9f7; border: 1rpx solid #c2ece2; }
.action-btn.primary { background: #6ACDBB; color: #fff; }
.action-btn.warn { background: #FEF3C7; color: #D97706; }
.action-btn.warn { background: #FEF3C7; color: #D97706; border: 1rpx solid #fde68a; }
</style>

View File

@@ -1,198 +0,0 @@
<template>
<view class="box">
<view class="b-r-8 white p-32 flex-row flex-jus-sp flex-ali-center m-b-2">
<text class="left">停诊公告</text>
<u-switch v-model="form.checked" @change="disabled=form.id?false:!form.checked"></u-switch>
</view>
<view class="b-r-8 white p-32 " style="margin-bottom: calc(env(safe-area-inset-bottom) + 110rpx);"
v-show="form.checked">
<u-form :model="form" ref="announcement" label-width="150rpx">
<u-form-item label="公告内容" prop="msg" label-position="top">
<view class="flex-col bg b-r-8 p-row-16" style="width: 646rpx;">
<u-input v-model="form.msg" placeholder="请输入" type="textarea" :maxlength="500" auto-height />
<d-text :text="`${form['msg']?form['msg'].length:0}/500`" color="#C4C7CC" width="614"
align="right">
</d-text>
</view>
</u-form-item>
<u-form-item label="开始时间" prop="startTime">
<u-input input-align="right" v-model="form.startTime" type="select" :select-open="select1"
@click="select1=true;show=true" placeholder="请选择" />
</u-form-item>
<u-form-item label="结束时间" prop="endTime" :border-bottom="false">
<u-input input-align="right" v-model="form.endTime" type="select" :select-open="select2"
@click="select2=true;show=true" placeholder="请选择" />
</u-form-item>
</u-form>
</view>
<view class="button">
<u-button :throttle-time="0" type="primary" shape="circle" @click="submit" :ripple="true"
:disabled="form.id&&!form.checked?false:!form.checked">确定</u-button>
</view>
<u-picker :safe-area-inset-bottom="true" :mask-close-able="false" @cancel="select1 = false;select2 = false"
v-model="show" mode="time" :params="params" @confirm="confirm"></u-picker>
<d-loading-page :loading="loading" :loadingText="loadingText"></d-loading-page>
</view>
</template>
<script>
import {
queryNotice,
saveClose
} from '@/api/all.js'
export default {
data() {
return {
disabled: true,
loadingText: '正在加载中...',
info: '',
loading: true,
form: {},
show: false,
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true,
timestamp: true
},
select1: 0,
select2: 0,
rules: {
msg: [{
required: true,
validator: (rule, value, callback) => {
if (!this.form['msg']) {
callback(new Error('公告内容不能为空'));
} else {
callback();
}
},
trigger: ['change', 'blur']
}],
startTime: [{
required: true,
validator: (rule, value, callback) => {
if (!this.form['startTime']) {
callback(new Error('请选择开始时间'));
} else {
callback();
}
},
trigger: ['change', 'blur']
}],
endTime: [{
required: true,
validator: (rule, value, callback) => {
if (!this.form['endTime']) {
callback(new Error('请选择结束时间'));
return
}
if (this.endTimestamp <= this.startTimestamp) {
callback(new Error('结束时间不能小于等于开始时间'));
return
}
callback();
},
trigger: ['change', 'blur']
}]
}
}
},
created() {
this.$nextTick(() => {
this.$refs.announcement.setRules(this.rules);
})
this.queryNotice()
},
methods: {
queryNotice() {
queryNotice().then(res => {
if (res.errcode == 0) {
if (res.data.close_notice) {
this.form = {
checked: res.data.close_notice ? true : false,
msg: res.data.content,
startTime: this.$u.timeFormat(res.data.start_time, 'yyyy-mm-dd hh:MM:ss'),
endTime: this.$u.timeFormat(res.data.end_time, 'yyyy-mm-dd hh:MM:ss'),
}
this.startTimestamp = res.data.start_time
this.endTimestamp = res.data.end_time
}
this.form.id = res.data.id
this.loading = false
this.disabled = false
} else {
this.disabled = true
this.loading = false
}
})
},
saveClose(data) {
saveClose(data).then(res => {
if (res.errcode == 0) {
this.$toast('提交成功')
this.loading = false
} else {
this.$toast(res.msg)
this.loading = false
}
})
},
confirm(e) {
let time = `${e.month}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`
if (this.select1) {
this.form.startTime = this.$u.timeFormat(e.timestamp, 'yyyy-mm-dd hh:MM:ss')
this.startTimestamp = e.timestamp
this.select1 = false
}
if (this.select2) {
this.form.endTime = this.$u.timeFormat(e.timestamp, 'yyyy-mm-dd hh:MM:ss')
this.endTimestamp = e.timestamp
this.select2 = false
}
},
submit() {
this.$refs.announcement.validate(valid => {
if (valid) {
let y = new Date().getFullYear()
const data = {
close_notice: this.form.checked ? 1 : 0,
content: this.form.msg,
start_time: this.startTimestamp,
end_time: this.endTimestamp,
}
this.loadingText = '请稍后...'
this.loading = true
this.saveClose(data)
}
})
},
}
}
</script>
<style>
page {
background-color: #F9FAFB;
}
</style>
<style scoped lang="scss">
.box {
padding: 20rpx;
}
.button {
position: fixed;
bottom: env(safe-area-inset-bottom);
left: 0;
width: 750rpx;
height: 110rpx;
background: #FFFFFF;
border-top: 2rpx solid #F2F3F5;
padding: 12rpx 32rpx;
box-sizing: border-box;
z-index: 999;
}
</style>

View File

@@ -1,63 +0,0 @@
<template>
<view class="box">
<view class="p-row-32 white b-r-8 m-t-3" v-if="info">
<view class="p-col-32 bottom-border flex-row flex-jus-sp flex-ali-center">
<d-text text="肾功能" className="fs-3 tips-c"></d-text>
<d-text text="正常" className="fs-3 content-c"></d-text>
</view>
<view class="p-col-32 bottom-border flex-row flex-jus-sp flex-ali-center">
<d-text text="过敏史" className="fs-3 tips-c"></d-text>
<d-text text="阿莫西林" className="fs-3 content-c"></d-text>
</view>
<view class="p-col-32 bottom-border flex-row flex-jus-sp flex-ali-center">
<d-text text="个人病史" className="fs-3 tips-c"></d-text>
<d-text text="无" className="fs-3 content-c"></d-text>
</view>
<view class="p-col-32 bottom-border flex-row flex-jus-sp flex-ali-center">
<d-text text="家族病史" className="fs-3 tips-c"></d-text>
<d-text text="心脏病" className="fs-3 content-c"></d-text>
</view>
</view>
<d-loading-page :loading="loading"></d-loading-page>
</view>
</template>
<script>
import {
baseInfo
} from '@/api/all.js'
export default {
data() {
return {
loading: true,
info: ''
}
},
onLoad(op) {
op['id']&&this.getInfo(op['id'])
},
methods: {
getInfo(up_id=-1) {
baseInfo({up_id}).then(() => {
if (res.errcode == 0) {
this.info = res.data
this.loading = false
} else {
this.loading = false
}
})
}
}
}
</script>
<style>
page {
background-color: #F9FAFB;
}
</style>
<style scoped lang="scss">
.box {
padding: 0 20rpx;
padding-bottom: env(safe-area-inset-bottom);
}
</style>

View File

@@ -6,31 +6,20 @@
<u-tabs bar-width="20" :list="tabs.filter((item, index) => (type === 1 ? true : index !== 1))"
:current="current" :is-scroll="false" @change="change" active-color="#6ACDBB"></u-tabs>
</view>
<view class="p-row-2" style="margin-top: 114rpx" v-if="!loading && list[current].length >= 0">
<view class="p-32 white flex-col m-b-2" @click="toInfo(item.user_patient_id,item.id)"
v-for="item in list[current]" :key="item.id">
<view class="flex-row">
<!-- 接诊状态 -->
<!-- <d-text :color="tabs[current]['color']" :text="tabs[current]['name']"></d-text> -->
<u-image width="104rpx" height="104rpx" shape="circle"
:src="item.avatar || require(`../../static/image/${item.sex%2==0?'nv':'nan'}.png`)">
<u-loading slot="loading"></u-loading>
</u-image>
<view class="list">
<view class="names">
<text class="fs-32">{{item.patient}}</text>
<text class="fs-24">{{item.created_at}}</text>
</view>
<view class="flex-row">
<text>{{item.sex %2 != 0 ? '男' : '女'}}</text>
<text>{{item.age + '岁'}}</text>
</view>
<view class="flex-row m-t-1">
<text class="fs-24 color-gray">推广员:{{ item.salesperson_name || '无' }}</text>
</view>
</view>
</view>
</view>
<view style="margin-top: 114rpx; padding: 20rpx 24rpx 0;" v-if="!loading && list[current].length >= 0">
<!-- 到店接诊列表卡:统一走 xk-list-card(信息分层 + 状态色 + 主/次操作按钮) -->
<xk-list-card
v-for="item in list[current]" :key="item.id"
:avatar="item.avatar || require(`../../static/image/${item.sex%2==0?'nv':'nan'}.png`)"
:title="item.patient"
:meta="(item.sex % 2 != 0 ? '男' : '女') + ' · ' + item.age + '岁'"
:time="item.created_at"
:rows="['推广员:' + (item.salesperson_name || '无')]"
:status="{ text: tabs[current].name, color: tabs[current].color }"
:actions="[{ key: 'info', text: current === 0 ? '去接诊' : '查看详情', type: current === 0 ? 'primary' : 'ghost' }]"
@action="toInfo(item.user_patient_id, item.id)"
@click="toInfo(item.user_patient_id, item.id)"
></xk-list-card>
<u-loadmore :status="status" :load-text="loadText" v-if="!loading && list[current].length > 0" />
</view>
@@ -234,24 +223,6 @@
z-index: 99;
}
.list {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 30rpx;
.names {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
text {
margin-right: 20rpx;
}
}
.fun {
position: fixed;
left: 0;

View File

@@ -1,269 +0,0 @@
<template>
<view class="container">
<!-- 按钮 -->
<view class="head">
<view class="head_left">
<text :class="{
active:!type
}" @click="
type = null;
init();
">全部({{ data.all_count }})</text>
<text :class="{
active:type==='high'
}" @click="
type = 'high';
init();
">高分({{ data.high_count }})</text>
<text :class="{
active:type==='low'
}" @click="
type = 'low';
init();
">低分({{ data.low_count }})</text>
</view>
<view class="head_right">
<view class="score">
<u-icon name="star-fill" color="#FB8C00" size="36"></u-icon>
<text>{{ data.score_avg }}</text>
</view>
<text>患者评分</text>
</view>
</view>
<!-- 列表 -->
<view class="items" v-for="item in list" :key="item.id">
<!-- <image src="../../static/logo.png" mode=""></image> -->
<view class="items_right">
<view class="item_name">
<view class="title">
<text>{{ item.nickname || "匿名" }}</text>
<u-rate :count="count" v-model="item.score" size="28" active-color="#FB8C00"
inactive-color="#b2b2b2" :gutter="item.score"></u-rate>
</view>
<view class="time"> 2023-1-13 </view>
</view>
<view class="info">
非常非常耐心细致,是我最近遇到的最棒的医生,给了治疗建议,谢谢。
</view>
</view>
</view>
<u-loadmore :status="status" :load-text="loadText" v-if="!loading && list.length > 0" />
<view class="flex-row flex-jus-center" v-if="!loading && list.length == 0">
<d-empty text="暂无信息" mode="search"></d-empty>
</view>
<d-loading-page :loading="loading"></d-loading-page>
</view>
</template>
<script>
import {
doctorWorkComments,
commentMain
} from "@/api/all.js";
export default {
data() {
return {
count: 5,
type: null,
data: {
high_count: 0,
low_count: 0,
score_avg: 0,
},
page: 1,
list: [],
loading: true,
loading2: false,
status: "loading",
loadText: {
loadmore: "轻轻上拉",
loading: "努力加载中",
nomore: "实在没有了",
},
totalPage: 1,
};
},
onShow() {
this.getList();
this.commentMain()
},
onPullDownRefresh() {
this.init();
},
onReachBottom() {
if (this.page >= this.totalPage) {
this.$toast("数据已全部加载");
return;
}
this.page++;
this.getList();
},
methods: {
commentMain() {
commentMain({store_id: uni.getStorageSync('store_id') || '11001'})
.then((res) => {
if (res.errcode == 0) {
this.data = res.data
}
})
},
init() {
this.list = [];
this.page = 1;
(this.loading = true), this.getList(true);
},
getList(is_stop = false) {
if (this.loading2) {
return;
}
this.loading2 = true;
doctorWorkComments({
page: this.page,
type: this.type,
store_id: uni.getStorageSync('store_id') || '11001'
})
.then((res) => {
if (res.errcode == 0) {
this.list = [...res.data.list, ...this.list];
this.totalPage = res.data.pagination.totalPage;
if (this.page >= this.totalPage) {
this.status = "nomore";
} else {
this.status = "loadmore";
}
this.$nextTick(() => {
this.loading = false;
is_stop && uni.stopPullDownRefresh();
});
} else {
this.$toast(res.msg);
is_stop && uni.stopPullDownRefresh();
this.loading = false;
if (this.page >= this.totalPage) {
this.status = "nomore";
} else {
this.status = "loadmore";
}
}
})
.finally(() => {
this.loading2 = false;
});
},
},
};
</script>
<style lang="scss" scoped>
.container {
.head {
width: 750rpx;
height: 110rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 32rpx;
margin-bottom: 10rpx;
.head_left {
.active {
padding: 10rpx 20rpx;
background: #6ACDBB;
border-radius: 28rpx;
height: 44rpx;
line-height: 44rpx;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #ffffff;
}
text {
padding: 10rpx 20rpx;
border-radius: 28rpx;
background: #f1f5f9;
height: 44rpx;
line-height: 44rpx;
font-size: 24rpx;
color: #475569;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
margin: 0 20rpx 0 0;
}
}
.head_right {
.score {
text {
height: 50rpx;
font-size: 36rpx;
font-family: PingFang SC-Semibold, PingFang SC;
font-weight: 600;
color: #fb8c00;
line-height: 42rpx;
}
}
text {
height: 28rpx;
font-size: 20rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #94a3b8;
}
}
}
.items {
width: 706rpx;
display: flex;
margin: 10rpx auto;
background-color: #fff;
justify-content: space-between;
padding: 10rpx 10rpx;
image {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
}
.items_right {
.item_name {
display: flex;
justify-content: space-between;
align-items: center;
.title {
height: 34rpx;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #475569;
}
.time {
height: 34rpx;
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #94a3b8;
}
}
.info {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
margin: 16rpx 0 0 0;
}
}
}
}
</style>

View File

@@ -1,97 +0,0 @@
<template>
<view class="page">
<view class="bg">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="navbar" :style="[navbarHeight]"></view>
<view class="title">
</view>
</view>
<view class="box">
<label class="box_title">请选择你的身份</label>
<view class="box_item flex-row flex-ali-center flex-jus-sp" v-for="(item, index) in identitys" :key="index"
@click="checkId(item)">
<view class="flex-row flex-ali-center">
<image :src="item.src"></image>
<text>{{item.name}}</text>
</view>
<u-icon name="arrow-right" color="#A7ABB0" size="32"></u-icon>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
identitys: [{
name: '我是医生',
type: 1,
src: '../../static/image/doctor.png'
},
{
name: '我是药师',
type: 2,
src: '../../static/image/ys.png'
}
],
statusBarHeight:this.$statusBarHeight,
navbarHeight:this.$navbarHeight
};
},
methods: {
checkId(item) {
this.$go('/pages/login/register?role='+item.type)
uni.setStorageSync('role', item.type)
}
},
};
</script>
<style lang="scss" scoped>
.bg {
width: 100vw;
background: linear-gradient(180deg, #EAF2FF 0%, rgba(238, 245, 255, 0) 100%);
.title {
height: 200rpx;
line-height: 200rpx;
width: 100%;
text-align: center;
font-size: 48rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #0D111A;
}
}
.box {
padding: 0 52rpx;
&_title {
font-size: 36rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #6C7380;
}
&_item {
height: 152rpx;
background-color: #F9FAFB;
border-radius: 8rpx;
padding: 32rpx;
margin-top: 32rpx;
text{
font-size: 36rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #31353D;
margin-left: 20rpx;
}
image {
width: 88rpx;
height: 88rpx;
}
}
}
</style>

View File

@@ -1,83 +0,0 @@
<template>
<view class="inquiries safe-area-inset-bottom ff_pfsc p-row-32">
<view class="p-col-16">
<d-input prefixIcon="search" borderRadius="64rpx" v-model="keyword" type="text" placeholder="搜索疾病、症状等" height="66"></d-input>
</view>
<view class="list p-col-16 ">
<view class="m-b-48" v-for="(item,index) in list">
<view class="flex flex-jus-sp flex-ali-center">
<view class="ask_l">
问
</view>
<view class="ask_r content-c fs-32 text-twoline">
小孩子5岁,左脸湿疹溃烂,之前肿胀流水,左下肢也有一些皮屑,5月25日初诊小孩子5岁,左脸湿疹溃烂,之前肿胀流水,左下肢也有一些皮屑,5月25日初诊
</view>
</view>
<view class="answer text-hide">李磊:继续维持目前药物剂量,按照说明书的剂量服用</view>
<view class="text-align-end m-t-16 fs-28">
<text style="color: #2979FF;" v-if="item==1" @click.stop="setFine(1)">设为精选</text>
<text style="color: #A7ABB0;" @click.stop="setFine(0)" v-else>取消精选</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data(){
return{
keyword:'',
list:[1,2],
type:'',
}
},
mounted() {
},
methods:{
setFine(type){
// 1设为精选,0取消精选
this.type=type
},
getList(){
},
},
watch: {
keyword(oldValue, newValue) {
this.$u.debounce(this.getList, 500)
}
}
}
</script>
<style scoped lang="scss">
.list {
.ask_l {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background-color: #2979FF;
color: #FFFFFF;
font-size: 28rpx;
line-height: 48rpx;
text-align: center;
}
.ask_r {
width: 622rpx;
}
.answer {
margin-top: 16rpx;
background-color: #F3F4F5;
height: 76rpx;
font-size: 28rpx;
color: #6C7380;
padding: 16rpx;
border-radius: 8rpx;
}
}
</style>

View File

@@ -1,43 +0,0 @@
<template>
<view>
<!-- <d-chat :state="type" :info="info" :record="record" v-if="record"></d-chat> -->
</view>
</template>
<script>
export default {
data() {
return {
type:0,
info:{},
page:1,
record:'',
a:"",
}
},
methods:{
},
mounted() {
// this.a = setInterval(() => {
// this.messageList()
// }, 1000)
},
destroyed() {
// 取消监听键盘高度变化事件
uni.offKeyboardHeightChange()
// clearInterval(this.a)
// this.a = null
}
}
</script>
<style>
page {
background-color: #eeeeef;
}
</style>
<style>
</style>

View File

@@ -1,644 +0,0 @@
<template>
<view class="safe-area-inset-bottom white">
<view class="search">
<u-search :height="66" :inputStyle="{height:'66rpx'}" @custom="keyword=''" :show-action="false"
placeholder="请输入您想要的商品" v-model="keyword" @input="getDrugList"></u-search>
</view>
<view v-if="type==1">
<u-tabs :list="tabs" :is-scroll="false" :current="current"
@change="loading = true;current=$event;drugList($event)" bar-width="20" active-color="#6ACDBB"
inactive-color="#6C7380">
</u-tabs>
</view>
<scroll-view scroll-y="true" style="padding-bottom: 200rpx">
<!-- 西药 -->
<block v-if="type==1">
<view class="b-r-8 p-32" v-for="(item,i) in setList" :key="item.id">
<view class="flex-row mb40">
<u-image width="128rpx" height="144rpx" border-radius="8" :src="item.image">
<u-loading slot="loading"></u-loading>
</u-image>
<view class="m-l-24 flex-1">
<view>
<d-text :text="item.drug_name" className="fs-3 content-c"></d-text>
</view>
<d-text :text="'规格:'+item.specification" className="fs-24 tips-c mtb12"
style="margin-left: 5rpx;"></d-text>
<view class="flex-row flex-jus-sp flex-ali-center ">
<view class="flex-row flex-1">
<d-text :text="'库存:剩余'" className="fs-24 tips-c"></d-text>
<d-text :text="item.stock" className="colorG fz24"></d-text>
<d-text text="件" className="fs-24 tips-c"></d-text>
</view>
<view class="price">
¥{{ parseFloat(item.price).toFixed(2) }}
</view>
</view>
</view>
</view>
<view class="m-t08 flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-1 flex-jus-sp ">
<block v-if="!item.is_add">
<view class="flex-row flex-ali-center btnBox" v-if="!item.is_collect"
@click.prevent="hoarding(item.id,0)">
<u-icon name="plus" color="#6ACDBB" size="26"></u-icon>
<d-text :text="'常用药'" className="fs-24 ml14" color="#6ACDBB"></d-text>
</view>
<view v-else class="flex-row flex-ali-center btnBox del"
@click.prevent="hoarding(item.id,1)">
<u-icon name="minus-circle" color="#FC3636" size="30"></u-icon>
<d-text :text="'移除'" className="fs-24 ml14" color="#FC3636"></d-text>
</view>
<view class="m-l-32">
<u-button :custom-style="{ backgroundColor:'#6ACDBB',color:'#fff' }" shape="circle"
size="mini" @click="handleAdd(item, i)">
添加商品
</u-button>
</view>
</block>
<d-number-box v-else v-model="item.number" :min="0" :max="item.stock"
:asyncChange="true">
<view slot="minus" class="minus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(249, 250, 251,${item.number>0?1:0.6})`}"
@click="clickHandler('minus',item,item.number)">
<u-icon name="minus" size="22" :color="item.number>0?'#000':'#A7ABB0'"></u-icon>
</view>
<view slot="input" style="width: 80rpx;"
class="input flex-row flex-jus-center flex-ali-center">
<d-text :text="item.number" className="fs-3 content-c"></d-text>
</view>
<view slot="plus" class="plus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(41,121,255,${item.number>=item.stock?0.6:1})`}"
@click="clickHandler('plus',item,item.number)">
<u-icon name="plus" size="22" color="#fff"></u-icon>
</view>
</d-number-box>
</view>
</view>
</view>
</block>
<!-- 中药 颗粒 -->
<block v-if="type==2||type==3">
<u-checkbox-group shape="circle" icon-size="35" size="44">
<view v-for="(item, i) in list[0]" :key="item.id"
class="flex-row flex-jus-sp white p-32 flex-ali-center" style="width: 100vw;">
<u-checkbox :value="item.checked" @change="changeCheckbox(item,0)" :name="item.id">
</u-checkbox>
<view class="flex-col flex-1">
<view class="flex-row flex-jus-sp flex-ali-center">
<d-text :text="item.drug_name||item.name" className="content-c fs-3"></d-text>
<view class="flex-row flex-ali-end" v-if="item.price">
<d-text
:text="item.price.split('.').length>0?'¥':'¥'+item.price"
className="fs-24 error-c">
</d-text>
<block v-if="item.price.split('.').length>0">
<d-text :text="item.price.split('.')[0]" className="fs-3 error-c"
:bold="true">
</d-text>
<d-text text="." className="fs-24 error-c"></d-text>
<d-text :text="item.price.split('.')[1]"
className="fs-24 error-c"></d-text>
</block>
</view>
</view>
<d-text :text="'单位:克'"></d-text>
</view>
</view>
</u-checkbox-group>
<view class="bottom flex-row flex-jus-sp flex-ali-center" style="border-bottom: 2rpx solid #F2F3F5;">
<d-text :text="`已选(${getSelect})`" className="fs-32 content-c" @click="affirmShow=true"></d-text>
<u-button :throttle-time="0"
:custom-style="{heigth:'86rpx',width:'478rpx',backgroundColor:'#6ACDBB',color:'#fff'}"
shape="circle" @click="show=true;info=getList;" :disabled="!getSelect">
下一步
</u-button>
</view>
</block>
<view class="flex-row flex-jus-center box"
v-if="!loading&&((type==2||type==3)&&list[0].length==0||type==1&&setList.length==0)">
<d-empty :text="keyword?'换个名称试试':current==1?'去收藏药品吧!':'暂无药品'" mode="search"></d-empty>
</view>
</scroll-view>
<view class="btn" v-if="shownum">
<u-button :throttle-time="0" :custom-style="{ backgroundColor:'#6ACDBB',color:'#fff' }" shape="circle"
@click="toBack" :loading="loading">
添加到处方
</u-button>
</view>
<!-- 添加西药详情 -->
<add-set-usage :info="info" @close="show=false"
@add="index!=-1&&$set(list,index,$event);index=-1;show=false;type==1?drugList(current):$go(1, 4);recipe={}"
:type="type" v-if="type>0&&show" :idx='idx' :recipe="recipe" :is_set="recipe['pId']?true:false">
</add-set-usage>
<!-- 中药 颗粒 -->
<u-popup v-model="affirmShow" mode="bottom" :closeable="true" @close="$emit('close')" close-icon-pos='top-right'
:safe-area-inset-bottom="true" :mask-close-able="false">
<view class="flex-row flex-jus-center flex-ali-center m-t-3">
<d-text text="已选择" className="fs-32 tips-c"></d-text>
</view>
<view class="m-t-4 p-row-52 flex-col">
<view class="bottom-border p-col-32 flex-row flex-jus-sp flex-ali-center"
v-for="(item,index) in getList" :key="index">
<d-text :text="item.drug_name" className="fs-32 content-c"></d-text>
<d-text text="移除" className="fs-32 primary-c" @click="setChecked(item.id)"></d-text>
</view>
</view>
<view class="m-t-4 m-b-16">
<u-button :throttle-time="0"
:custom-style="{heigth:'86rpx',width:'646rpx',backgroundColor:'#6ACDBB',color:'#fff'}"
shape="circle" @click="affirmShow=false" :disabled="!getSelect">
确定
</u-button>
</view>
</u-popup>
</view>
</template>
<script>
import mixin from './mixin/mixin.js';
import addSetUsage from './components/add-setUsage.vue'
import {
westList,
chineseList,
searchChinese,
searchWest,
westCommon,
chineseCommon,
westCollection,
westUncollect,
granularList,
} from '@/api/all.js'
export default {
components: {
addSetUsage
},
mixins: [mixin],
data() {
return {
loading: true,
recipe: {},
affirmShow: false,
index: -1,
info: {},
show: false,
tabs: [{
id: 0,
name: '全部药品'
}, {
id: 1,
name: '我的常用药'
}],
list: {
0: [],
1: [],
},
keyword: '',
current: 0,
type: 0,
searchList: {
0: [],
1: [],
},
idx: 0,
shownum: false
}
},
computed: {
setList() {
this.getDrug();
let list = this.list[this.current].length > 0 ? this.list[this.current] : [];
let arr = [];
this.type == 1 && (arr = this.wests)
this.type == 2 && (arr = this.chineses)
this.type == 3 && (arr = this.granular)
// console.log(arr, 'this.granular')
list = list.map((item) => {
let it = arr.find(it => it['id'] == item.id) || {}
const obj = {
...item,
...it,
is_add: it['id'] ? true : false,
stock: item['stock'] - (it['number'] || 0)
}
return obj
});
return list
},
getList() {
return this.list[this.current].filter(item => item.checked).map(item => {
return {
...item,
grammage: item.grammage || 0
}
})
},
getSelect() {
return this.list[this.current].filter(item => item.checked).length
}
},
onLoad(op) {
this.current = op['current'] || 0;
this.type = op['type'] || 0;
this.idx = op['index'] || 0;
this.recipe = op['recipe'] ? JSON.parse(op['recipe']) : {}
uni.showLoading({
title: '加载中...'
})
this.drugList();
this.getDrug();
},
methods: {
toBack() {
uni.navigateBack(1)
},
changeCheckbox(e, cur) {
this.list[cur] = this.list[cur].map(item => {
if (item.id === e.id) {
return {
...item,
checked: !item.checked
}
}
return item
})
},
async hoarding(id, type = 0) {
let res = {}
uni.showLoading({
title: '请稍后...'
})
type == 0 && (res = await westCollection({
id,
store_id: uni.getStorageSync('store_id') || '11001'
}))
type == 1 && (res = await westUncollect({
id,
store_id: uni.getStorageSync('store_id') || '11001'
}))
if (res.errcode == 0) {
this.list[this.current] = this.list[this.current].map((it) => {
return {
...it,
is_collect: id == it['id'] ? !it.is_collect : it.is_collect
}
})
this.$nextTick(() => {
uni.hideLoading()
})
} else {
this.$toast(res.msg)
}
},
getDrugList() {
uni.showLoading({
title: '搜索中...'
})
this.loading = true
uni.$u.debounce(this.drugList, 1000);
},
// 药品列表
async drugList(current = 0) {
this.list[this.current] = []
let res = {}
if (current == 0) {
// 西药列表
this.type == 1 && (res = await westList({
name: this.keyword,
store_id: uni.getStorageSync('store_id') || '11001'
}))
// 中药列表
this.type == 2 && (res = await chineseList({
name: this.keyword,
store_id: uni.getStorageSync('store_id') || '11001'
}))
this.type == 3 && (res = await granularList({
name: this.keyword,
store_id: uni.getStorageSync('store_id') || '11001'
}))
} else {
// 常用药-西药
this.type == 1 && (res = await westCommon({
name: this.keyword,
store_id: uni.getStorageSync('store_id') || '11001'
}))
// 常用药-中药
this.type == 2 && (res = await chineseCommon({
store_id: uni.getStorageSync('store_id') || '11001'
}))
}
if (res.errcode == 0) {
if (this.type == 2) {
this.list[this.current] = res.data
for (var i = 0; i < this.list[this.current].length; i++) {
let id = this.list[this.current][i]['id']
let chinese = this.chineses[this.idx]['item'].find(it => it['id'] == id) || {}
if (id == chinese['id']) {
this.list[this.current][i] = chinese
}
}
}
if (this.type == 3) {
this.list[this.current] = res.data
for (var i = 0; i < this.list[this.current].length; i++) {
let id = this.list[this.current][i]['id']
let granular = this.granular[this.idx]['item'].find(it => it['id'] == id) || {}
if (id == granular['id']) {
this.list[this.current][i] = granular
}
}
}
if (this.type == 1) {
let collect = Array.isArray(res.data) ? res.data : res.data.collect || res.data.collected;
collect = (collect || []).map(item => {
return {
...item,
is_collect: true
}
})
let no_collect = res.data.no_collect || []
let uncollect = res.data.uncollect || []
uncollect = [...uncollect, ...no_collect].map(item => {
return {
...item,
is_collect: false
}
})
this.list[this.current] = collect.concat(this.current == 0 ? uncollect : [])
}
this.$nextTick(() => {
this.loading = false
uni.hideLoading()
})
} else {
this.loading = false
this.$toast(res.msg)
}
},
clickHandler(type, data, number) {
// TODO 这里有Bug
let that = this;
let item = JSON.parse(JSON.stringify(data));
item['number'] = number || item['number']
if (type === 'minus' && item['number'] == 1) {
uni.showModal({
title: '提示',
content: '是否确认从处方中删除该药品',
success: (res) => {
if (res.confirm) {
// 根据item的id找到对应的索引
let index = that.wests.findIndex(it => it['id'] == item['id'])
// 定义变量type,根据tabsActive的值决定数组操作的类型
let type = that.type;
switch (type) {
case 1:
index = that.wests.findIndex(it => it['id'] == item['id'])
break;
case 2:
index = that.chineses.findIndex(it => it['id'] == item['id'])
break;
case 3:
index = that.granular.findIndex(it => it['id'] == item['id'])
break;
}
console.log(index, 'sssssssssss')
// 如果index有效,则执行删除和存储操作
if (index >= 0) {
// 根据type的值,从对应的数组中删除指定索引的元素
type == 1 && that.wests.splice(index, 1);
type == 2 && that.chineses.splice(index, 1);
type == 3 && that.granular.splice(index, 1);
// 定义变量key用于存储缓存键名
let key = null
// 根据type和缓存标志决定key的值
if (type == 1) {
key = uni.getStorageSync('add') ? 'addWests' : 'wests'
}
if (type == 2) {
key = uni.getStorageSync('add') ? 'addChineses' : 'chineses'
}
if (type == 3) {
key = uni.getStorageSync('add') ? 'addGranular' : 'granular'
}
// 将修改后的数组存储到缓存中,如果数组为空则存储默认值
uni.setStorageSync(key, type == 1 ? this.wests : type == 2 ? this.chineses.length == 0 ? [{
item: [],
obj: {}
}] : this.chineses : this.granular.length == 0 ? [{
item: [],
obj: {}
}] : this.granular)
// 调用getDrug方法,可能用于更新药物数据
that.getDrug()
} else {
// 如果index无效,清空所有数组和相关缓存
this.wests = []
this.chineses = []
this.granular = []
uni.removeStorageSync(uni.getStorageSync('add') ? 'addWests' : 'wests')
// 日志输出,可能用于调试
console.log(1);
// 同样,设置缓存,存储空的数组结构
uni.setStorageSync(uni.getStorageSync('add') ? 'addChineses' : 'chineses', [{
item: [],
obj: {}
}])
uni.setStorageSync(uni.getStorageSync('add') ? 'addGranular' : 'granular', [{
item: [],
obj: {}
}])
}
}
}
});
this.$set(item, 'number', item['number'])
return
}
item['number'] != 0 && type === 'minus' && item['number']--;
item['number'] < item.stock && type === 'plus' && item['number']++
this.setDrug(item);
},
// 添加西药
handleAdd(item, i) {
// console.log(item, 'item')
this.show = true
this.info = {
...item
}
this.index = i
this.shownum = true
},
setChecked(id) {
this.list[this.current] = this.list[this.current].map(item => {
if (item.id == id) {
item.checked = !item.checked
}
return item
})
},
}
}
</script>
<style lang="scss">
.search {
padding: 16rpx 32rpx;
}
.btn {
width: 710rpx;
margin: 20rpx auto;
position: fixed;
left: 2.2%;
bottom: 20rpx;
z-index: 999;
}
.minus {
width: 64rpx;
height: 64rpx;
background-color: #F9FAFB;
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.plus {
width: 64rpx;
height: 64rpx;
background-color: #2979FF;
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.input {
width: 122rpx;
height: 64rpx;
background-color: #F3F4F5;
}
.bottom {
position: fixed;
width: 100vw;
box-sizing: border-box;
bottom: env(safe-area-inset-bottom);
left: 0;
background-color: #fff;
padding: 12rpx 32rpx;
::v-deep button {
z-index: 999;
background: #6ACDBB;
color: #FFFFFF;
}
}
.m-b-16 {
::v-deep button {
z-index: 999;
background: #6ACDBB;
color: #FFFFFF;
}
}
.text {
width: 122rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
text-align-last: justify;
}
.price {
font-size: 32rpx;
color: #F44437;
font-weight: bold;
}
.search {
padding: 16rpx 32rpx;
}
.colorG {
color: #6ACDBB !important;
}
.fz24 {
font-size: 24rpx !important;
}
.minus {
width: 64rpx;
height: 64rpx;
background-color: #F9FAFB;
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.plus {
width: 64rpx;
height: 64rpx;
background-color: #6ACDBB;
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.input {
width: 122rpx;
height: 64rpx;
background-color: #F3F4F5;
}
.mtb12 {
margin: 12rpx 0;
}
.bottom {
position: fixed;
width: 100vw;
box-sizing: border-box;
bottom: env(safe-area-inset-bottom);
left: 0;
background-color: #fff;
padding: 12rpx 32rpx;
}
.text {
width: 122rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
text-align-last: justify;
}
.btnBox {
height: 48rpx;
line-height: 48rpx;
padding: 0 20rpx;
border-radius: 24rpx;
border: 2rpx solid #6ACDBB;
&.del {
border: 2rpx solid #FC3636;
}
}
.mb40 {
margin-bottom: 40rpx;
}
.ml14 {
margin-left: 14rpx;
}
</style>

View File

@@ -1,476 +0,0 @@
<template>
<view class="safe-area-inset-bottom white">
<view class="search">
<u-search :height="66" :inputStyle="{height:'66rpx'}" @custom="keyword=''" :show-action="false"
placeholder="请输入您想要的商品" v-model="keyword" @input="getDrugList"></u-search>
</view>
<scroll-view scroll-y="true" style="padding-bottom: 200rpx">
<!-- 西药 -->
<block v-if="type==3">
<view class="b-r-8 p-32" v-for="(item,i) in setList" :key="item.id">
<view class="flex-row mb40">
<u-image width="128rpx" height="144rpx" border-radius="8" :src="item.image">
<u-loading slot="loading"></u-loading>
</u-image>
<view class="m-l-24 flex-1">
<view>
<d-text :text="item.drug_name" className="fs-3 content-c"></d-text>
</view>
<d-text :text="'规格:'+item.specification" className="fs-24 tips-c mtb12"
style="margin-left: 5rpx;"></d-text>
<view class="flex-row flex-jus-sp flex-ali-center ">
<view class="flex-row flex-1">
<d-text :text="'库存:剩余'" className="fs-24 tips-c"></d-text>
<d-text :text="item.stock" className="colorG fz24"></d-text>
<d-text text="件" className="fs-24 tips-c"></d-text>
</view>
<view class="price">
¥{{ parseFloat(item.price).toFixed(2) }}
</view>
</view>
</view>
</view>
<view class="m-t08 flex-row flex-jus-sp flex-ali-center">
<view class="flex-row flex-1 flex-jus-sp ">
<block v-if="!item.is_add">
<view class="flex-row flex-ali-center btnBox">
<u-icon name="plus" color="#6ACDBB" size="26"></u-icon>
<d-text :text="'未添加'" className="fs-24 ml14" color="#6ACDBB"></d-text>
</view>
<view class="m-l-32">
<u-button :custom-style="{ backgroundColor:'#6ACDBB',color:'#fff' }" shape="circle"
size="mini" @click="handleAdd(item, i)">
添加商品
</u-button>
</view>
</block>
<!-- <view v-else class="flex-row flex-ali-center btnBox">-->
<!-- <d-text :text="`已添加(${item.number})周期`" className="fs-24 ml14" color="#6ACDBB"></d-text>-->
<!-- </view>-->
<d-number-box v-else v-model="item.number" :min="0" :max="item.stock"
:asyncChange="true">
<view slot="minus" class="minus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(249, 250, 251,${item.number>0?1:0.6})`}"
@click="clickHandler('minus',item,item.number)">
<u-icon name="minus" size="22" :color="item.number>0?'#000':'#A7ABB0'"></u-icon>
</view>
<view slot="input" style="width: 80rpx;"
class="input flex-row flex-jus-center flex-ali-center">
<d-text :text="item.number" className="fs-3 content-c"></d-text>
</view>
</d-number-box>
</view>
</view>
</view>
</block>
<view class="flex-row flex-jus-center box"
v-if="!loading&&((type===2||type===3)&&list[0].length===0||type===1&&setList.length===0)">
<d-empty :text="keyword?'换个名称试试':current===1?'去收藏药品吧!':'暂无药品'" mode="search"></d-empty>
</view>
</scroll-view>
<view class="btn" v-if="shownum">
<u-button :throttle-time="0" :custom-style="{ backgroundColor:'#6ACDBB',color:'#fff' }" shape="circle"
@click="toBack" :loading="loading">
添加到处方
</u-button>
</view>
<!-- 添加西药详情 -->
<add-set-usage :info="info" @close="show=false"
@add="index!==-1&&$set(list,index,$event);index=-1;show=false;type===1?drugList(current):$go(1, 4);recipe={}"
:type="type" v-if="type>0&&show" :idx='idx' :recipe="recipe" :is_set="!!recipe['pId']">
</add-set-usage>
</view>
</template>
<script>
import mixin from './mixin/mixin.js';
import addSetUsage from './components/add-setUsage.vue'
import {servicePageList,} from '@/api/servicePackage.js'
export default {
components: {
addSetUsage
},
mixins: [mixin],
data() {
return {
loading: true,
recipe: {},
affirmShow: false,
index: -1,
info: {},
show: false,
tabs: [{
id: 0,
name: '全部药品'
}, {
id: 1,
name: '我的常用药'
}],
list: {
0: [],
1: [],
},
keyword: '',
current: 0,
type: 0,
searchList: {
0: [],
1: [],
},
idx: 0,
shownum: false
}
},
computed: {
setList() {
this.getDrug();
let list = this.list[this.current]?.length > 0 ? this.list[this.current] : [];
let arr = [];
arr = this.servicePackage
console.log(list, 'this.servicePackage')
console.log(arr, 'this.servicePackage')
list = list.map((item) => {
let it = arr.find(it => it['id'] === item.id) || {};
return {
...item,
...it,
is_add: !!it['id'],
stock: item['stock'] - (it['number'] || 0)
}
});
return list
},
getList() {
return this.list[this.current].filter(item => item.checked).map(item => {
return {
...item,
grammage: item.grammage || 0
}
})
},
getSelect() {
return this.list[this.current].filter(item => item.checked).length
}
},
onLoad(op) {
this.current = op['current'] || 0;
this.type = 3;
this.idx = op['index'] || 0;
this.recipe = op['recipe'] ? JSON.parse(op['recipe']) : {}
uni.showLoading({
title: '加载中...'
})
this.drugList();
this.getDrug();
},
methods: {
toBack() {
uni.navigateBack(1)
},
changeCheckbox(e, cur) {
this.list[cur] = this.list[cur].map(item => {
if (item.id === e.id) {
return {
...item,
checked: !item.checked
}
}
return item
})
},
async hoarding(id, type = 0) {
let res = {}
uni.showLoading({
title: '请稍后...'
})
// type == 0 && (res = await westCollection({
// id,
// store_id: uni.getStorageSync('store_id') || '11001'
// }))
// type == 1 && (res = await westUncollect({
// id,
// store_id: uni.getStorageSync('store_id') || '11001'
// }))
if (res.errcode === 0) {
this.list[this.current] = this.list[this.current].map((it) => {
return {
...it,
is_collect: id === it['id'] ? !it.is_collect : it.is_collect
}
})
this.$nextTick(() => {
uni.hideLoading()
})
} else {
this.$toast(res.msg)
}
},
getDrugList() {
uni.showLoading({
title: '搜索中...'
})
this.loading = true
uni.$u.debounce(this.drugList, 1000);
},
// 药品列表
async drugList() {
this.list[this.current] = [];
let res = {};
(res = await servicePageList({
name: this.keyword,
store_id: uni.getStorageSync('store_id') || '11001'
}))
if (res.errcode === 0) {
let collect = Array.isArray(res.data) ? res.data : res.data.collect || res.data.collected;
collect = (collect || []).map(item => {
return {
...item,
is_collect: true
}
})
let no_collect = res.data.no_collect || []
let uncollect = res.data.uncollect || []
uncollect = [...uncollect, ...no_collect].map(item => {
return {
...item,
is_collect: false
}
})
this.list[this.current] = collect.concat(this.current == 0 ? uncollect : [])
console.log(this.list[this.current], 'sssssssssssssssssssssss')
this.$nextTick(() => {
this.loading = false
uni.hideLoading()
})
} else {
this.loading = false
this.$toast(res.msg)
}
},
clickHandler(type, data, number) {
let that = this;
let item = JSON.parse(JSON.stringify(data));
item['number'] = number || item['number']
if (type === 'minus' && item['number'] == 1) {
uni.showModal({
title: '提示',
content: '是否确认从处方中删除该药品',
success: (res) => {
if (res.confirm) {
// 根据item的id找到对应的索引
let type = that.type;
let index = that.servicePackage.findIndex(it => it.id === item.id)
// 如果index有效,则执行删除和存储操作
if (index >= 0) {
// 根据type的值,从对应的数组中删除指定索引的元素
that.servicePackage.splice(index, 1);
// 定义变量key用于存储缓存键名
let key = null
key = uni.getStorageSync('add') ? 'addServicePackage' : 'servicePackage'
// 将修改后的数组存储到缓存中,如果数组为空则存储默认值
uni.setStorageSync(key, this.servicePackage)
// 调用getDrug方法,可能用于更新药物数据
that.getDrug()
} else {
// 如果index无效,清空所有数组和相关缓存
this.servicePackage = []
uni.removeStorageSync(uni.getStorageSync('add') ? 'addServicePackage' : 'servicePackage')
// 同样,设置缓存,存储空的数组结构
uni.setStorageSync(uni.getStorageSync('add') ? 'addServicePackage' : 'servicePackage', [{
item: [],
obj: {}
}])
}
}
}
});
this.$set(item, 'number', item['number'])
return
}
item['number'] != 0 && type === 'minus' && item['number']--;
item['number'] < item.stock && type === 'plus' && item['number']++
this.setDrug(item);
},
// 添加西药
handleAdd(item, i) {
// console.log(item, 'item')
this.show = true
this.info = {
...item
}
this.index = i
this.shownum = true
},
setChecked(id) {
this.list[this.current] = this.list[this.current].map(item => {
if (item.id == id) {
item.checked = !item.checked
}
return item
})
},
}
}
</script>
<style lang="scss">
.search {
padding: 16rpx 32rpx;
}
.btn {
width: 710rpx;
margin: 20rpx auto;
position: fixed;
left: 2.2%;
bottom: 20rpx;
z-index: 999;
}
.minus {
width: 64rpx;
height: 64rpx;
background-color: #F9FAFB;
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.plus {
width: 64rpx;
height: 64rpx;
background-color: #2979FF;
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.input {
width: 122rpx;
height: 64rpx;
background-color: #F3F4F5;
}
.bottom {
position: fixed;
width: 100vw;
box-sizing: border-box;
bottom: env(safe-area-inset-bottom);
left: 0;
background-color: #fff;
padding: 12rpx 32rpx;
::v-deep button {
z-index: 999;
background: #6ACDBB;
color: #FFFFFF;
}
}
.m-b-16 {
::v-deep button {
z-index: 999;
background: #6ACDBB;
color: #FFFFFF;
}
}
.text {
width: 122rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
text-align-last: justify;
}
.price {
font-size: 32rpx;
color: #F44437;
font-weight: bold;
}
.search {
padding: 16rpx 32rpx;
}
.colorG {
color: #6ACDBB !important;
}
.fz24 {
font-size: 24rpx !important;
}
.minus {
width: 64rpx;
height: 64rpx;
background-color: #F9FAFB;
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.plus {
width: 64rpx;
height: 64rpx;
background-color: #6ACDBB;
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.input {
width: 122rpx;
height: 64rpx;
background-color: #F3F4F5;
}
.mtb12 {
margin: 12rpx 0;
}
.bottom {
position: fixed;
width: 100vw;
box-sizing: border-box;
bottom: env(safe-area-inset-bottom);
left: 0;
background-color: #fff;
padding: 12rpx 32rpx;
}
.text {
width: 122rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #A7ABB0;
text-align-last: justify;
}
.btnBox {
height: 48rpx;
line-height: 48rpx;
padding: 0 20rpx;
border-radius: 24rpx;
border: 2rpx solid #6ACDBB;
&.del {
border: 2rpx solid #FC3636;
}
}
.mb40 {
margin-bottom: 40rpx;
}
.ml14 {
margin-left: 14rpx;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,804 +0,0 @@
<template>
<view class="box">
<u-popup v-model="show" mode="bottom" :closeable="true" @close="$emit('close')" close-icon-pos='top-right'
:safe-area-inset-bottom="true" :mask-close-able="false" z-index="10078">
<scroll-view scroll-y="true"
:style="{height: `calc(100vh - 110rpx - env(safe-area-inset-bottom) - var(--status-bar-height) - ${navbarHeight}px)`}">
<!-- 西药详情 -->
<view class="box flex-col white">
<view class="flex-row flex-jus-center">
<d-text text="用法用量" className="fs-32 main-c m-t-3"></d-text>
</view>
<view class="flex-col m-t-32 p-32" v-if="type==1">
<view class="flexBox">
<view class="item">
<view class="label">名称:</view>
<view class="val">{{obj.drug_name}}</view>
</view>
</view>
<view class="flexBox">
<view class="item">
<view class="label">规格:</view>
<view class="val">{{obj.specification}}</view>
</view>
<view class="item">
<view class="label">库存:</view>
<view class="val">剩余{{obj.stock}}件</view>
</view>
</view>
<view class="flexBox" v-if="see_rate==1">
<view class="item">
<view class="label">毛利率:</view>
<view class="val">{{getRate()}}%</view>
</view>
</view>
<view class="flexBox">
<view class="item">
<view class="label">厂家:</view>
<view class="val">{{obj.source}}</view>
</view>
</view>
<view class="flexBox">
<view class="item">
<view class="label">功能主治:</view>
<view class="val">{{obj.function}}</view>
</view>
</view>
<view class="m-t-24 flex-col bg p-col-08 iBox">
<d-text text="用法用量(说明书)" className="fs-24 content-c m-l-16"></d-text>
<d-text :text="obj.usage" className="fs-24 tips-c m-l-16"></d-text>
<image src="../../../../static/image/i4.png" class="icon" mode="scaleToFill"
@click="previewImage(obj.instruction)" />
</view>
<view class="flex-col flex-jus-sp m-t-32">
<d-text text="用法用量" className="fs-3 main-c"></d-text>
<d-text text="初始默认值为说明书用量,请根据患者实际情况调整剂量" className="fs-24 error-c m-t08"></d-text>
</view>
<view class="flex-row flex-jus-sp m-t-32">
<d-text text="药品数量" className="fs-3 main-c"></d-text>
<d-number-box v-model="obj.number" :min="1" :max="obj.stock">
<view slot="minus" class="minus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(249, 250, 251,${obj.number>1?1:0.6})`}">
<u-icon name="minus" size="22" :color="obj.number>1?'#000':'#A7ABB0'"></u-icon>
</view>
<view slot="plus" class="plus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(41,121,255,${obj.number>=obj.stock?0.6:1})`}">
<u-icon name="plus" size="22" color="#fff"></u-icon>
</view>
</d-number-box>
</view>
<view class="bottom-border"></view>
<view class="flex-col">
<view class="flex-row flex-ali-center m-t-32">
<text class="text">用法:</text>
<view class="flex-row flex-ali-center">
<view class="bg p-row-16">
<u-input v-model="obj.usages['0']['name']" type="select"
:select-open="key=='time'" @click="key='time';pickerShow=true"
placeholder="请选择" />
</view>
<view class="bg m-l-32 p-row-16">
<u-input v-model="obj.usages['1']['name']" type="select"
:select-open="key=='usage'" @click="key='usage';pickerShow=true"
placeholder="请选择" />
</view>
</view>
</view>
<view class="flex-row flex-ali-center m-t-32">
<text class="text">每次:</text>
<view class="flex-row flex-ali-center">
<d-number-box v-model="obj.count" :min="1">
<view slot="minus" class="minus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(249, 250, 251,${obj.count>1?1:0.6})`}">
<u-icon name="minus" size="22" :bold="true"
:color="`rgba(41,121,255,${obj.count>1?1:0.6})`"></u-icon>
</view>
<view slot="plus" class="plus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(249, 250, 251,${obj.count>=obj.stock?0.6:1})`}">
<u-icon name="plus" size="22" :bold="true"
:color="`rgba(41,121,255,${obj.count>=obj.stock?0.6:1})`"></u-icon>
</view>
</d-number-box>
<view class="bg m-l-32 p-row-16">
<u-input v-model="obj.describe['name']" type="select"
:select-open="key=='describe'" @click="key='describe';pickerShow=true;"
placeholder="请选择" />
</view>
</view>
</view>
<view class="flex-row flex-ali-center m-t-32">
<text class="text">频率:</text>
<view class="bg p-row-16">
<u-input v-model="obj.frequency['name']" type="select"
:select-open="key=='frequency'" @click="key='frequency';pickerShow=true;"
placeholder="请选择" />
</view>
</view>
</view>
</view>
服务包
<view class="flex-col m-t-32 p-32" v-if="type==3">
<view class="flexBox">
<view class="item">
<view class="label">名称:</view>
<view class="val">{{obj.drug_name}}</view>
</view>
</view>
<view class="flexBox">
<view class="item">
<view class="label">规格:</view>
<view class="val">{{obj.specification}}</view>
</view>
<view class="item">
<view class="label">库存:</view>
<view class="val">剩余{{obj.stock}}件</view>
</view>
</view>
<view class="flexBox" v-if="see_rate==1">
<view class="item">
<view class="label">毛利率:</view>
<view class="val">{{getRate()}}%</view>
</view>
</view>
<view class="flexBox">
<view class="item">
<view class="label">厂家:</view>
<view class="val">{{obj.source}}</view>
</view>
</view>
<view class="flexBox">
<view class="item">
<view class="label">功能主治:</view>
<view class="val">{{obj.function}}</view>
</view>
</view>
<view class="m-t-24 flex-col bg p-col-08 iBox">
<d-text text="用法用量(说明书)" className="fs-24 content-c m-l-16"></d-text>
<d-text :text="obj.usage" className="fs-24 tips-c m-l-16"></d-text>
<image src="../../../../static/image/i4.png" class="icon" mode="scaleToFill"
@click="previewImage(obj.instruction)" />
</view>
<view class="flex-col flex-jus-sp m-t-32">
<d-text text="用法用量" className="fs-3 main-c"></d-text>
<d-text text="初始默认值为说明书用量,请根据患者实际情况调整剂量" className="fs-24 error-c m-t08"></d-text>
</view>
<view class="flex-row flex-jus-sp m-t-32">
<d-text text="商品数量" className="fs-3 main-c"></d-text>
<d-number-box v-model="obj.number" :min="1" :max="1">
<view slot="minus" class="minus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(249, 250, 251,${obj.number>1?1:0.6})`}">
<u-icon name="minus" size="22" :color="obj.number>1?'#000':'#A7ABB0'"></u-icon>
</view>
<view slot="plus" class="plus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(41,121,255,${obj.number>=obj.stock?0.6:1})`}">
<u-icon name="plus" size="22" color="#fff"></u-icon>
</view>
</d-number-box>
</view>
<view class="bottom-border"></view>
<view class="flex-col">
</view>
</view>
<!-- 中药 -->
<view class="box flex-col white p-32" v-if="(type==2)&&is_next">
<view class="grid-tem-col-2 rp bg p-16">
<template v-for="item in obj">
<view class="flex-row flex-jus-sp flex-ali-center" :key="item.id" v-if="item.checked">
<d-text :text="item.drug_name" color="#31353D"></d-text>
<d-text :text="item.grammage+'g'" color="#6C7380"></d-text>
</view>
</template>
</view>
<view class="flex-row m-t-32 flex-ali-center">
<text class="text">调配:</text>
<view class="flex-row flex-ali-center">
<view class="subsection" v-if="show">
<u-subsection :height="50" active-color="#6ACDBB" font-size="24" :list="recipes"
:current="addRecipe.deploy||0" mode="subsection"
@change="addRecipe.deploy=$event.index"></u-subsection>
</view>
<view class="m-l-32">
<u-checkbox-group shape="circle" icon-size="28" size="36">
<u-checkbox v-model="addRecipe['frying']" name="浓煎"
@change="$set(addRecipe,'frying',$event.value)">浓煎</u-checkbox>
</u-checkbox-group>
</view>
</view>
</view>
<view class="flex-row flex-ali-center m-t-32">
<text class="text">剂数:</text>
<view class="bg p-row-16 flex-1">
<u-input v-model="addRecipe.dosage['name']" type="select" :select-open="key=='dosage'"
placeholder="请选择" @click="key='dosage';pickerShow=true" />
</view>
</view>
<view class="flex-row flex-ali-center m-t-32">
<text class="text">用量:</text>
<view class="bg p-row-16 flex-1">
<u-input v-model="addRecipe.nums['name']" type="select" :select-open="key=='nums'"
placeholder="请选择" @click="key='nums';pickerShow=true" />
</view>
</view>
<view class="flex-row flex-ali-center m-t-32">
<text class="text">用法:</text>
<view class="bg p-row-16 flex-1">
<u-input v-model="addRecipe.usage['name']" type="select" :select-open="key=='usage'"
placeholder="请选择" @click="key='usage';pickerShow=true" />
</view>
</view>
<view class="flex-row flex-ali-center m-t-32">
<text class="text">服法:</text>
<view class="bg p-row-16 flex-1">
<u-input v-model="addRecipe.time['name']" type="select" :select-open="key=='time'"
placeholder="请选择" @click="key='time';pickerShow=true" />
</view>
</view>
<view class="flex-row flex-ali-center m-t-32">
<text class="text">备注:</text>
<view class="bg p-row-16 flex-1">
<u-input v-model="addRecipe.remark" placeholder="请输入" />
</view>
</view>
</view>
<!-- 颗粒 -->
<view class="p-row-52 flex-col" v-if="(type==2||type==4)&&!is_next">
<view class="bottom-border p-col-32 flex-row flex-jus-sp flex-ali-center"
v-for="(item,index) in obj" :key="index">
<d-text :text="item.drug_name" className="fs-32 content-c"></d-text>
<d-number-box v-model="item.grammage" :min='0'>
<view slot="minus" class="minus flex-row flex-jus-center flex-ali-center"
:style="{backgroundColor:`rgba(249, 250, 251,${item.grammage>0?1:0.6})`}">
<u-icon name="minus" size="22" :color="item.grammage>0?'#000':'#A7ABB0'"></u-icon>
</view>
<view slot="plus" class="plus flex-row flex-jus-center flex-ali-center primary-c">
<u-icon name="plus" size="22" color="#fff"></u-icon>
</view>
</d-number-box>
</view>
</view>
</view>
</scroll-view>
<view class="add">
<u-button :throttle-time="0" :custom-style="{ backgroundColor:'#6ACDBB',color:'#fff' }" shape="circle"
@click="(type==2||type==4)&&!is_next?next():add()" :loading="loading">
{{(type==2||type==4)&&!is_next?'下一步':'添加到处方'}}
</u-button>
</view>
</u-popup>
<u-picker cancel-color="#6ACDBB" confirm-text="确定" :safe-area-inset-bottom="true" v-model="pickerShow"
@confirm="confirm" mode="selector" :default-selector="[0]" range-key="name" :range="range[key]"
@cancel="key='';pickerShow=false" :mask-close-able="false" :z-index="10079"></u-picker>
</view>
</template>
<script>
import mixin from '../mixin/mixin.js';
import {
useTime,
useType,
useFre,
westUnit,
useNum,
dosage,
addOneChinese,
addOneGranular
} from '@/api/all.js'
export default {
name: 'add-setUsage',
mixins: [mixin],
props: {
is_set: {
type: Boolean,
default: false
},
idx: {
type: [Number, String],
default: 0
},
info: {
type: [Object, Array],
default: () => {
return {}
}
},
type: {
type: Number,
default: 0
},
recipes: {
type: [Array],
default: () => {
return [{
id: 0,
name: '煎煮'
}, {
id: 1,
name: '外配'
}]
}
},
recipe: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
navbarHeight: 0,
cm_id: [],
is_next: false,
pickerShow: false,
obj: JSON.parse(JSON.stringify(this.info)),
addRecipe: this.recipe,
show: true,
canvas: '',
loading: false,
key: '',
range: {
time: [],
dosage: [],
frequency: [],
usage: [],
describe: [],
nums: []
},
see_rate: uni.getStorageSync('see_rate')
}
},
created() {
this.navbarHeight = this.$navbarHeight
this.useTime()
this.useType()
if (this.type == 1) {
this.useFre()
this.westUnit()
} else {
this.useNum()
this.dosage()
}
},
computed: {
},
methods: {
getRate() {
return ((Number((this.obj.price - this.obj.buy_price) / this.obj.buy_price)).toFixed(2)) * 100
},
next() {
this.cm_id = new Array(this.obj.length)
let grammage = this.obj.reduce((total, item) => {
total += item['grammage'];
return total
}, 0) || 0
if (grammage <= 0) {
this.$toast('请设置克数');
return
}
let index = 0;
this.loading = true
for (let i = 0; i < this.obj.length; i++) {
this.type == 2 && addOneChinese({
drug_id: this.obj[i]['drug_id'],
number: this.obj[i]['grammage'],
store_id: uni.getStorageSync('store_id') || '11001'
}).then(res => {
if (res.errcode == 0) {
this.cm_id[i] = res.data[0]
index++;
this.is_next = index >= this.obj.length
this.loading = !this.is_next
} else {
this.loading = false
this.$toast(res.msg);
}
})
this.type == 3 && addOneGranular({
drug_id: this.obj[i]['drug_id'],
number: this.obj[i]['grammage'],
store_id: uni.getStorageSync('store_id') || '11001'
}).then(res => {
console.log(res, 'addgramm');
if (res.errcode == 0) {
this.cm_id[i] = res.data[0]
index++;
this.is_next = index >= this.obj.length
this.loading = !this.is_next
} else {
this.loading = false
this.$toast(res.msg);
}
})
}
},
dosage() {
dosage({
store_id: uni.getStorageSync('store_id') || '11001'
}).then((res) => {
if (res.errcode == 0) {
this.range['dosage'] = res.data.map(item => {
return {
...item,
name: item.num + ""
}
})
for (var i = 0; i < this.range['dosage'].length; i++) {
if (this.range['dosage'][i].id == this.addRecipe['dosage']) {
this.addRecipe['dosage'] = this.range['dosage'][i]
}
}
}
})
},
useNum() {
useNum({
store_id: uni.getStorageSync('store_id') || '11001'
}).then((res) => {
if (res.errcode == 0) {
this.range['nums'] = res.data;
for (var i = 0; i < res.data.length; i++) {
if (res.data[i].id == this.addRecipe['consumption']) {
this.addRecipe['nums'] = res.data[i]
}
}
}
})
},
westUnit() {
westUnit({
store_id: uni.getStorageSync('store_id') || '11001'
}).then((res) => {
if (res.errcode == 0) {
this.range['describe'] = res.data;
for (var i = 0; i < res.data.length; i++) {
if (res.data[i].id == this.obj['unit_id']) {
this.obj['describe'] = res.data[i]
}
}
}
})
},
useTime() {
useTime({
store_id: uni.getStorageSync('store_id') || '11001'
}).then((res) => {
if (res.errcode == 0) {
this.range['time'] = res.data
for (var i = 0; i < res.data.length; i++) {
if (this.type == 1) {
if (res.data[i].id == this.obj['time_id']) {
!this.obj.usages && (this.obj['usages'] = {})
this.obj['usages'][0] = res.data[i]
}
} else {
if (res.data[i].id == this.addRecipe['usage']) {
this.addRecipe['usage'] = res.data[i]
}
}
}
}
})
},
useType() {
useType({
store_id: uni.getStorageSync('store_id') || '11001'
}).then((res) => {
if (res.errcode == 0) {
this.range['usage'] = res.data
for (var i = 0; i < res.data.length; i++) {
if (this.type == 1) {
if (res.data[i].id == this.obj['type_id']) {
!this.obj.usages && (this.obj['usages'] = {})
this.obj['usages'][1] = res.data[i]
}
} else {
if (res.data[i].id == this.addRecipe['fufa_id']) {
this.addRecipe['time'] = res.data[i]
}
}
}
}
})
},
useFre() {
useFre({
store_id: uni.getStorageSync('store_id') || '11001'
}).then((res) => {
if (res.errcode == 0) {
this.range['frequency'] = res.data
for (var i = 0; i < res.data.length; i++) {
if (res.data[i].id == this.obj['frequency_id']) {
this.obj['frequency'] = res.data[i]
}
}
}
})
},
confirm(e) {
switch (this.type) {
case 1:
if (this.key == 'time' || this.key == 'usage') {
let i = this.key == 'time' ? 0 : 1;
!this.obj.usages && (this.obj['usages'] = {})
this.obj.usages[i] = this.range[this.key][e[0]];
} else {
this.obj[this.key] = this.range[this.key][e[0]]
}
break;
case 2:
this.addRecipe[this.key] = this.range[this.key][e[0]]
break;
case 3:
this.addRecipe[this.key] = this.range[this.key][e[0]]
break;
}
this.key = '';
},
add() {
let obj = this.obj;
let addRecipe = this.addRecipe;
let data = {}
let instruction = '';
switch (this.type) {
case 1:
if (!obj['usages'] || (obj['usages'] && !obj['usages'][0] || !obj['usages'][1])) {
this.$toast('请设置用法');
return
}
if (!obj['describe']) {
this.$toast('请设置每次');
return
}
if (!obj['frequency']) {
this.$toast('请设置频率');
return
}
instruction = obj.usages['0']['name'] + obj.usages['1']['name'] + ',' + obj['frequency'][
'name'
] + ',每次' + (obj['count'] || 1)
instruction += obj['describe']['name']
data = {
store_id: uni.getStorageSync('store_id') || 11001,
content: obj['id'],
number: obj['number'] || 1,
usage: instruction,
instruction,
time_id: obj.usages['0']['id'],
type_id: obj.usages['1']['id'],
grain_number: obj['count'] || 1,
count: obj['count'] || 1,
available_days: 5,
f_id: obj['frequency']['id'],
wu_id: obj['describe']['id']
}
this.is_set ? this.setDrug({
...data,
pId: obj.pId
}, obj) : this.addDrug(data, obj)
break;
case 2:
if (!addRecipe.dosage) {
this.$toast('请设置剂数');
return
}
if (!addRecipe.nums) {
this.$toast('请设置用量');
return
}
if (!addRecipe.usage) {
this.$toast('请设置用法');
return
}
if (!addRecipe.time) {
this.$toast('请设置服法');
return
}
if (!addRecipe.remark) {
this.$toast('请输入备注');
return
}
let gram = obj.reduce((g, item) => {
g += item['grammage']
return g
}, 0)
data = {
store_id: uni.getStorageSync('store_id') || 11001,
deployment: addRecipe.deploy ? 2 : 1,
dosage: addRecipe.dosage['id'],
consumption: addRecipe.nums['id'],
usage: addRecipe.usage['id'],
fufa_id: addRecipe.time['id'],
remark: addRecipe.remark,
is_deepfry: addRecipe.frying ? 1 : 0,
cm_id: this.cm_id.join(),
directions: addRecipe.deploy ? '外配' : '煎煮' + addRecipe.nums['name'] + ',共' + addRecipe
.dosage['name'] + '剂' + Number(addRecipe.dosage['name']) * gram + '克,每剂' + obj.length +
'味' + gram + '克'
}
this.is_set ? this.setDrug({
...data,
pId: addRecipe.pId
}, obj) : this.addDrug(data, obj, this.idx)
break;
case 3:
// instruction = obj.usages['0']['name'] + obj.usages['1']['name'] + ',' + obj['frequency'][
// 'name'
// ] + ',每次' + (obj['count'] || 1)
// instruction += obj['describe']['name']
data = {
store_id: uni.getStorageSync('store_id') || 11001,
content: obj['id'],
number: obj['number'] || 1,
usage: instruction,
instruction,
time_id: obj.usages === undefined? '' : obj.usages['0']['id'],
type_id: obj.usages === undefined? '' : obj.usages['1']['id'],
grain_number: obj['count'] || 1,
count: obj['count'] || 1,
available_days: 5,
f_id: obj['frequency'] !== undefined? obj['frequency']['id']: '',
wu_id: obj['describe'] !== undefined? obj['describe']['id']: ''
}
// TODO 这里写加入逻辑
this.is_set ? this.setDrug({
...data,
pId: obj.pId
}, obj) : this.addDrug(data, obj)
break;
case 4:
if (!addRecipe.dosage) {
this.$toast('请设置剂数');
return
}
if (!addRecipe.nums) {
this.$toast('请设置用量');
return
}
if (!addRecipe.usage) {
this.$toast('请设置用法');
return
}
if (!addRecipe.time) {
this.$toast('请设置服法');
return
}
if (!addRecipe.remark) {
this.$toast('请输入备注');
return
}
let gram2 = obj.reduce((g, item) => {
g += item['grammage']
return g
}, 0)
data = {
store_id: uni.getStorageSync('store_id') || 11001,
deployment: addRecipe.deploy ? 2 : 1,
dosage: addRecipe.dosage['id'],
consumption: addRecipe.nums['id'],
usage: addRecipe.usage['id'],
fufa_id: addRecipe.time['id'],
remark: addRecipe.remark,
is_deepfry: addRecipe.frying ? 1 : 0,
gm_id: this.cm_id.join(),
directions: addRecipe.deploy ? '外配' : '煎煮' + addRecipe.nums['name'] + ',共' + addRecipe
.dosage['name'] + '剂' + Number(addRecipe.dosage['name']) * gram2 + '克,每剂' + obj
.length + '味' + gram2 + '克'
}
this.is_set ? this.setDrug({
...data,
pId: addRecipe.pId
}, obj) : this.addDrug(data, obj, this.idx)
break;
}
},
// 西药规格
// 说明书
previewImage(url) {
// console.log(url,'aaaaaaaaaaaaa');
let img=url.split(',')
uni.previewImage({
indicator: "none",
current: img,
urls: img,
});
},
}
}
</script>
<style lang="scss" scoped>
.box {
height: 100vh;
width: 100vw;
overflow: scroll;
padding-bottom: 100rpx;
padding-top: 40rpx;
}
.rp {
grid-column-gap: 64rpx;
grid-row-gap: 16rpx;
}
.add {
position: absolute;
width: 100vw;
box-sizing: border-box;
bottom: 0;
left: 0;
background-color: #fff;
height: 110rpx;
padding: 0 32rpx;
}
.minus {
width: 64rpx;
height: 64rpx;
background-color: #F9FAFB;
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.plus {
width: 64rpx;
height: 64rpx;
background-color: #2979FF;
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.flexBox {
display: flex;
align-items: center;
font-size: 28rpx !important;
flex-wrap: wrap;
.item {
display: flex;
align-items: flex-start;
margin-right: 100rpx;
margin-bottom: 16rpx;
}
.label {
color: rgba(0, 0, 0, 0.45)
}
.val {
flex: 1;
color: rgba(0, 0, 0, 0.85)
}
}
.iBox {
position: relative;
padding-right: 100rpx;
.icon {
width: 30rpx;
height: 30rpx;
position: absolute;
right: 40rpx;
top: 50%;
transform: translateY(-50%);
}
}
</style>

View File

@@ -1,55 +0,0 @@
<template>
<view class="box white" v-show="value">
<scroll-view scroll-y="true" :style="{height: `calc(100vh - 122rpx - ${height}px - env(safe-area-inset-bottom))`}">
<view class="p-32 flex-col">
<view class="flex-row flex-jus-sp flex-ali-center m-b-48" v-for="item in list" :key="item.id">
<u-icon :name="require('@/static/image/tag.png')" size="28" color="#6C7380" label-size="30" label-color="#6C7380"
:label="item.name"></u-icon>
<u-icon name="plus" size="32" color="#2979FF" v-if="!item.is_add" @click="$emit('add',item)">
</u-icon>
<d-text text="已添加" v-else></d-text>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
props: {
list: {
diagnose: Array,
default: () => {
return []
}
},
value: {
type: Boolean,
default: false
},
height: {
type: [Number],
default: 0
},
},
data() {
return {
show: false
};
},
watch: {
value() {
this.show = this.value
}
}
};
</script>
<style lang="scss" scoped>
.box{
position: fixed;
z-index: 999999999;
bottom: env(safe-area-inset-bottom);
left: 0;
width: 100vw;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,78 +0,0 @@
<template>
<view class="safe-area-inset-bottom notice p-2 ff_pfsc">
<view class="p-col-16">
<d-input prefixIcon="search" borderRadius="64rpx" v-model="keyword" type="text" placeholder="输入关键字搜索相关快速回复"
@handleInput="toSearch" height="66"></d-input>
</view>
<view class="flex">
<view class="tabs">
<u-tabs name="txt" :list="tabs" :is-scroll="true" :current="active" @change="setTabActive"></u-tabs>
</view>
<view class="flex flex-ali-center">
<u-icon label="管理" size="40" name="setting-fill"></u-icon>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
keyword: '',
tabs: [{
id: 1,
txt: '测试1'
}, {
id: 2,
txt: '测试2'
}, {
id: 3,
txt: '测试3'
}, {
id: 4,
txt: '测试4'
}, {
id: 5,
txt: '测试5'
}, {
id: 6,
txt: '测试6'
}, {
id: 7,
txt: '测试7'
}, {
id: 8,
txt: '测试8'
}, ],
active: 0,
list: [],
page: 1,
}
},
mounted() {
},
methods: {
getList() {
this.list.push('获取列表')
},
setTabActive(index) {
this.active = index
this.page = 1
this.getList()
}
},
watch: {
keyword(oldValue, newValue) {
this.$u.debounce(this.getList, 500)
}
}
}
</script>
<style lang="scss" scoped>
.tabs{
width: 590rpx;
}
</style>

View File

@@ -0,0 +1,23 @@
/**
* 工作台功能卡片形态偏好(长卡/短卡)
* 存本地 storage(已确认不入库):用户在「我的」页切换后,各工作台首页 onShow 时读取生效
* short = 双列短卡(默认);long = 整行长卡
*/
const STORAGE_KEY = 'wb_card_style'
/**
* 读取卡片形态偏好,异常值一律回落 short,保证布局不会因脏数据坏掉
*/
export function getCardStyle() {
const value = uni.getStorageSync(STORAGE_KEY)
return value === 'long' ? 'long' : 'short'
}
/**
* 写入卡片形态偏好
* @param {'short'|'long'} style
*/
export function setCardStyle(style) {
uni.setStorageSync(STORAGE_KEY, style === 'long' ? 'long' : 'short')
}

56
utils/workbenchEntries.js Normal file
View File

@@ -0,0 +1,56 @@
/**
* 小程序工作台功能入口下发数据的通用组装工具
* 后端 /xxx-dashboard/entries 返回扁平数组:[{ code, name, description, icon, theme, path, group_name }]
* 这里统一转换成四个工作台首页渲染用的 sections 结构:[{ key, title, items: [{ key, label, desc, icon, theme, path }] }]
* 行为字段(角标/表单预填/弹窗等)由各首页按 code 通过 decorate 回调注入,DB 只管展示与有无
*/
/** 未配置主题时的兜底配色(与各端硬编码菜单的占位灰一致) */
const FALLBACK_THEME = { bg: 'linear-gradient(135deg, #F3F4F6, #E5E7EB)', color: '#6B7280' };
/**
* 单条下发入口 → 渲染项归一化(首页 sections 与功能页常用/历史共用同一映射,保证字段一致)
* @param {Object} entry 后端下发的入口对象
* @returns {Object|null} { key, label, desc, icon, theme, path, group };非法入口返回 null
*/
export function normalizeEntry(entry) {
if (!entry || !entry.code) return null;
return {
key: entry.code,
label: entry.name || '',
desc: entry.description || '',
icon: entry.icon || 'grid-fill',
theme: entry.theme && entry.theme.bg ? entry.theme : FALLBACK_THEME,
path: entry.path || '',
group: entry.group_name || '',
};
}
/**
* 下发入口 → sections 结构
* @param {Array} entries 后端下发的入口数组(已按后台配置排序)
* @param {Function} [decorate] 可选行为装饰回调 (item, entry) => item|null,返回 null 表示丢弃该项
* @returns {Array} sections;entries 为空/非法时返回空数组(调用方据此回落硬编码菜单)
*/
export function groupEntriesToSections(entries, decorate) {
if (!Array.isArray(entries) || entries.length === 0) return [];
const sections = [];
entries.forEach((entry) => {
let item = normalizeEntry(entry);
if (!item) return;
if (typeof decorate === 'function') {
item = decorate(item, entry);
if (!item) return;
}
const title = entry.group_name || '快捷服务';
let section = sections.find((s) => s.title === title);
if (!section) {
// key 用分组标题即可(仅作 v-for key,首个出现顺序即分组展示顺序)
section = { key: title, title, items: [] };
sections.push(section);
}
section.items.push(item);
});
// 丢弃被 decorate 全部过滤掉的空分组
return sections.filter((s) => s.items.length > 0);
}

View File