|
|
<template>
|
|
|
<view class="assign-order-page">
|
|
|
<u-navbar :is-back="true" title="分配订单" :background="{'background':'#1479ff'}" title-color="#fff"
|
|
|
:border-bottom="false">
|
|
|
</u-navbar>
|
|
|
<view class="b-border"></view>
|
|
|
|
|
|
<scroll-view scroll-y class="main-scroll" @scrolltolower="loadMoreNurses">
|
|
|
<!-- 当前订单信息 -->
|
|
|
<view v-if="orderBrief.no" class="order-brief glass-card">
|
|
|
<view class="order-brief__title">待分配订单</view>
|
|
|
<view class="order-brief__row"><text class="label">订单号</text><text class="value">{{ orderBrief.no }}</text></view>
|
|
|
<view class="order-brief__row" v-if="orderBrief.accompany_product && orderBrief.accompany_product.name">
|
|
|
<text class="label">服务项目</text><text class="value">{{ orderBrief.accompany_product.name }}</text>
|
|
|
</view>
|
|
|
<view class="order-brief__row" v-if="orderBrief.time"><text class="label">服务时间</text><text class="value">{{ orderBrief.time }}</text></view>
|
|
|
<view class="order-brief__row" v-if="orderBrief.user_archive && orderBrief.user_archive.name">
|
|
|
<text class="label">被服务人</text><text class="value">{{ orderBrief.user_archive.name }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view v-else-if="orderLoadFailed" class="order-brief order-brief--empty glass-card">
|
|
|
<text class="muted">未能加载订单信息,仍可分配(请核对订单号)</text>
|
|
|
</view>
|
|
|
|
|
|
<view class="nurse-panel">
|
|
|
<view class="section-title">
|
|
|
选择护工
|
|
|
<text v-if="staffTotalHint" class="section-hint">共 {{ staffTotalHint }} 人</text>
|
|
|
</view>
|
|
|
<view class="search-box">
|
|
|
<u-search v-model="searchKeyword" placeholder="搜索姓名或手机号"
|
|
|
show-action shape="square" bg-color="#f0f2f5"
|
|
|
@search="searchNurse" @clear="searchNurse" action-text="搜索" @custom="searchNurse" />
|
|
|
</view>
|
|
|
|
|
|
<view v-if="hasNurseList" class="nurse-items">
|
|
|
<view
|
|
|
v-for="nurse in nurseList"
|
|
|
:key="nurse.id"
|
|
|
:class="['nurse-item', { active: selectedNurseId === nurse.id }]"
|
|
|
@click="selectNurseById(nurse.id)"
|
|
|
>
|
|
|
<view class="nurse-radio-wrap">
|
|
|
<view class="nurse-radio" :class="{ on: selectedNurseId === nurse.id }">
|
|
|
<view v-if="selectedNurseId === nurse.id" class="nurse-radio__dot"></view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="nurse-avatar">
|
|
|
<u-avatar :src="getNurseAvatar(nurse)" size="88"></u-avatar>
|
|
|
</view>
|
|
|
<view class="nurse-info">
|
|
|
<view class="nurse-name-row">
|
|
|
<text class="nurse-name">{{ nurse.name || '—' }}</text>
|
|
|
</view>
|
|
|
<view class="nurse-phone">{{ maskMobile(nurse.mobile) }}</view>
|
|
|
<view v-if="nurseSexText(nurse)" class="nurse-sub">{{ nurseSexText(nurse) }}</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<u-loadmore :margin-top="20" :margin-bottom="120" :status="loadStatus" />
|
|
|
</view>
|
|
|
<view v-else class="empty-state">
|
|
|
<u-empty mode="list" text="暂无可指派护工,请更换关键词或不限制站点后重试" />
|
|
|
</view>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
|
|
|
<!-- 底部操作 -->
|
|
|
<view class="footer-bar safe-area">
|
|
|
<u-button shape="circle" class="confirm-btn"
|
|
|
:custom-style="confirmBtnStyle"
|
|
|
:disabled="!selectedNurseId" @click="assignOrder">
|
|
|
确认分配
|
|
|
</u-button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import { ROOTPATH } from '@/common/config.js'
|
|
|
|
|
|
export default {
|
|
|
data() {
|
|
|
return {
|
|
|
orderId: '',
|
|
|
nurseList: [],
|
|
|
selectedNurseId: null,
|
|
|
searchKeyword: '',
|
|
|
loadStatus: 'loadmore',
|
|
|
page: 1,
|
|
|
pageSize: 10,
|
|
|
loginRole: '',
|
|
|
orderBrief: {},
|
|
|
orderLoadFailed: false,
|
|
|
staffTotal: 0,
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
hasNurseList() {
|
|
|
return this.nurseList.length > 0;
|
|
|
},
|
|
|
confirmBtnStyle() {
|
|
|
const dis = !this.selectedNurseId;
|
|
|
return {
|
|
|
width: '100%',
|
|
|
height: '88rpx',
|
|
|
lineHeight: '88rpx',
|
|
|
fontSize: '32rpx',
|
|
|
color: '#fff',
|
|
|
opacity: dis ? '0.55' : '1',
|
|
|
background: dis ? '#c5ced9' : 'linear-gradient(to right, #476de4, #7bb9f7)',
|
|
|
border: 'none'
|
|
|
};
|
|
|
},
|
|
|
staffTotalHint() {
|
|
|
if (this.loginRole !== 'staff' || !this.hasNurseList) return ''
|
|
|
if (this.staffTotal > 0) return this.staffTotal
|
|
|
return this.nurseList.length
|
|
|
}
|
|
|
},
|
|
|
onLoad(options) {
|
|
|
this.loginRole = uni.getStorageSync('login_role') || ''
|
|
|
if (options.id) {
|
|
|
this.orderId = options.id;
|
|
|
}
|
|
|
this.fetchOrderBrief();
|
|
|
this.getNurseList();
|
|
|
},
|
|
|
methods: {
|
|
|
/** 解压分页中的一条 nurses 数组 */
|
|
|
extractPaginatedRows(payload) {
|
|
|
if (!payload) return { rows: [], total: 0, last_page: 1 }
|
|
|
let rows = []
|
|
|
let total = 0
|
|
|
let last_page = 1
|
|
|
|
|
|
const tryArr = payload.data ?? payload.items ?? payload.list
|
|
|
if (Array.isArray(tryArr)) {
|
|
|
rows = tryArr
|
|
|
total = payload.total ?? rows.length
|
|
|
last_page = payload.last_page ?? 1
|
|
|
return { rows, total, last_page }
|
|
|
}
|
|
|
if (Array.isArray(payload)) {
|
|
|
return { rows: payload, total: payload.length, last_page: 1 }
|
|
|
}
|
|
|
return { rows: [], total: 0, last_page: 1 }
|
|
|
},
|
|
|
async fetchOrderBrief() {
|
|
|
if (!this.orderId) return
|
|
|
try {
|
|
|
let detail = {}
|
|
|
if (this.loginRole === 'staff') {
|
|
|
detail = await this.$u.api.accompanyOrderDetail({ id: this.orderId })
|
|
|
} else {
|
|
|
detail = await this.$u.api.operatorOrderShow({
|
|
|
id: this.orderId,
|
|
|
'show_relation[0]': 'userArchive',
|
|
|
'show_relation[1]': 'accompanyProduct',
|
|
|
'show_relation[2]': 'hospital'
|
|
|
})
|
|
|
}
|
|
|
const err = detail && (detail.errcode || detail.errCode)
|
|
|
if (!err || err === 0) {
|
|
|
this.orderBrief = (detail && detail.data != null ? detail.data : detail) || {}
|
|
|
} else {
|
|
|
this.orderLoadFailed = true
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error(e)
|
|
|
this.orderLoadFailed = true
|
|
|
}
|
|
|
},
|
|
|
async getNurseList(isLoadMore = false) {
|
|
|
try {
|
|
|
if (!isLoadMore) {
|
|
|
this.page = 1;
|
|
|
this.nurseList = [];
|
|
|
this.selectedNurseId = null;
|
|
|
}
|
|
|
|
|
|
let res;
|
|
|
|
|
|
if (this.loginRole === 'staff') {
|
|
|
const params = {
|
|
|
page: this.page,
|
|
|
page_size: this.pageSize,
|
|
|
sort_name: 'name',
|
|
|
sort_type: 'asc'
|
|
|
};
|
|
|
const kw = (this.searchKeyword || '').trim();
|
|
|
if (kw) params.keyword = kw;
|
|
|
if (this.orderId) params.order_id = this.orderId;
|
|
|
res = await this.$u.api.staffNursesForAssign(params);
|
|
|
} else {
|
|
|
const params = {
|
|
|
page: this.page,
|
|
|
page_size: this.pageSize,
|
|
|
sort_name: 'name',
|
|
|
sort_type: 'asc'
|
|
|
};
|
|
|
if (this.searchKeyword && this.searchKeyword.trim()) {
|
|
|
const kw = this.searchKeyword.trim();
|
|
|
params['filter[0][key]'] = 'name';
|
|
|
params['filter[0][op]'] = 'like';
|
|
|
params['filter[0][value]'] = kw;
|
|
|
}
|
|
|
params['filter[1][key]'] = 'status';
|
|
|
params['filter[1][op]'] = 'eq';
|
|
|
params['filter[1][value]'] = 1;
|
|
|
res = await this.$u.api.nurseIndex(params);
|
|
|
}
|
|
|
|
|
|
const { rows, total, last_page } = this.extractPaginatedRows(res);
|
|
|
|
|
|
if (rows.length === 0) {
|
|
|
this.loadStatus = 'nomore';
|
|
|
if (isLoadMore && this.page > 1) this.page--;
|
|
|
if (!isLoadMore) this.nurseList = [];
|
|
|
if (this.loginRole === 'staff') this.staffTotal = 0;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const dedupe = [];
|
|
|
const seen = new Set();
|
|
|
rows.forEach((n) => {
|
|
|
const id = n && n.id;
|
|
|
if (!id || seen.has(id)) return;
|
|
|
seen.add(id);
|
|
|
dedupe.push(n);
|
|
|
});
|
|
|
|
|
|
if (isLoadMore) this.nurseList.push(...dedupe);
|
|
|
else this.nurseList = dedupe;
|
|
|
|
|
|
if (this.loginRole === 'staff' && total != null && Number.isFinite(Number(total)) && Number(total) >= 0) {
|
|
|
this.staffTotal = Number(total);
|
|
|
}
|
|
|
|
|
|
this.loadStatus =
|
|
|
last_page !== undefined && last_page !== null
|
|
|
? (this.page >= last_page ? 'nomore' : 'loadmore')
|
|
|
: (rows.length >= this.pageSize ? 'loadmore' : 'nomore');
|
|
|
} catch (error) {
|
|
|
console.error('获取护工列表失败:', error);
|
|
|
this.loadStatus = 'nomore';
|
|
|
uni.showToast({
|
|
|
title: '获取护工列表失败',
|
|
|
icon: 'none'
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
|
|
|
searchNurse() {
|
|
|
this.page = 1;
|
|
|
this.getNurseList(false);
|
|
|
},
|
|
|
|
|
|
loadMoreNurses() {
|
|
|
if (this.loadStatus !== 'loadmore') return;
|
|
|
this.page++;
|
|
|
this.getNurseList(true);
|
|
|
},
|
|
|
|
|
|
/** 仅用 id(原始类型),避免小程序端 key/事件编译生成非法 data-event-opts */
|
|
|
selectNurseById(id) {
|
|
|
if (id != null && id !== '') this.selectedNurseId = id;
|
|
|
},
|
|
|
|
|
|
maskMobile(m) {
|
|
|
if (!m || m.length < 7) return m || '';
|
|
|
const s = String(m);
|
|
|
return s.slice(0, 3) + '****' + s.slice(-4);
|
|
|
},
|
|
|
|
|
|
nurseSexText(nurse) {
|
|
|
if (!nurse) return '';
|
|
|
const s = nurse.sex;
|
|
|
if (s === 1 || s === '1' || s === '男') return '性别 · 男';
|
|
|
if (s === 0 || s === '0' || s === '女') return '性别 · 女';
|
|
|
if (typeof s === 'string' && s) return `性别 · ${s}`;
|
|
|
return '';
|
|
|
},
|
|
|
|
|
|
async assignOrder() {
|
|
|
if (!this.selectedNurseId) {
|
|
|
uni.showToast({ title: '请选择护工', icon: 'none' });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const res =
|
|
|
this.loginRole === 'staff'
|
|
|
? await this.$u.api.staffOrderSave({
|
|
|
id: this.orderId,
|
|
|
nurse_id: this.selectedNurseId
|
|
|
})
|
|
|
: await this.$u.api.operatorOrderSave({
|
|
|
id: this.orderId,
|
|
|
nurse_id: this.selectedNurseId
|
|
|
});
|
|
|
|
|
|
const err = res.errcode ?? res.errCode ?? res.errorCode;
|
|
|
if (err && err !== 0) {
|
|
|
uni.showToast({ title: res.errmsg || res.message || '分配失败', icon: 'none' });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
uni.showToast({ title: '分配成功', icon: 'success' });
|
|
|
setTimeout(() => uni.navigateBack(), 1500);
|
|
|
} catch (error) {
|
|
|
console.error('分配订单失败:', error);
|
|
|
uni.showToast({
|
|
|
title: '分配失败',
|
|
|
icon: 'none'
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
|
|
|
getNurseAvatar(nurse) {
|
|
|
const wrap = nurse.avatar_detail || nurse.avatarDetail;
|
|
|
let raw = '';
|
|
|
if (wrap) {
|
|
|
if (typeof wrap === 'object' && wrap.url) raw = wrap.url;
|
|
|
}
|
|
|
if (!raw && typeof nurse.avatar === 'string' && /^https?:\/\//.test(nurse.avatar)) {
|
|
|
raw = nurse.avatar;
|
|
|
}
|
|
|
if (!raw) return '/static/default-avatar.png'
|
|
|
if (/^https?:\/\//i.test(raw)) return raw;
|
|
|
const base = ROOTPATH.replace(/\/$/, '');
|
|
|
return raw.startsWith('/') ? `${base}${raw}` : `${base}/${raw}`;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.assign-order-page {
|
|
|
min-height: 100vh;
|
|
|
height: 100vh;
|
|
|
background: #f6f8fb;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.main-scroll {
|
|
|
flex: 1;
|
|
|
height: 0;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.b-border {
|
|
|
flex-shrink: 0;
|
|
|
width: 100%;
|
|
|
height: 30rpx;
|
|
|
border-radius: 0 0 120rpx 120rpx;
|
|
|
background-color: #1479ff;
|
|
|
}
|
|
|
|
|
|
.glass-card {
|
|
|
background: #fff;
|
|
|
border-radius: 20rpx;
|
|
|
box-shadow: 0 6rpx 24rpx rgba(20, 121, 255, 0.08);
|
|
|
}
|
|
|
|
|
|
.order-brief {
|
|
|
margin: 24rpx 24rpx 0;
|
|
|
padding: 28rpx 30rpx;
|
|
|
|
|
|
&--empty {
|
|
|
padding: 24rpx 30rpx;
|
|
|
.muted {
|
|
|
font-size: 26rpx;
|
|
|
color: #999;
|
|
|
line-height: 1.5;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&__title {
|
|
|
font-size: 28rpx;
|
|
|
font-weight: 600;
|
|
|
color: #1479ff;
|
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
|
|
|
|
&__row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
font-size: 28rpx;
|
|
|
padding: 12rpx 0;
|
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
|
&:last-child {
|
|
|
border-bottom: none;
|
|
|
padding-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.label {
|
|
|
color: #888;
|
|
|
flex-shrink: 0;
|
|
|
width: 160rpx;
|
|
|
}
|
|
|
|
|
|
.value {
|
|
|
color: #333;
|
|
|
text-align: right;
|
|
|
flex: 1;
|
|
|
word-break: break-all;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.nurse-panel {
|
|
|
margin: 24rpx;
|
|
|
padding-bottom: 24rpx;
|
|
|
}
|
|
|
|
|
|
.section-title {
|
|
|
font-size: 32rpx;
|
|
|
font-weight: bold;
|
|
|
color: #333;
|
|
|
margin-bottom: 20rpx;
|
|
|
display: flex;
|
|
|
align-items: baseline;
|
|
|
gap: 16rpx;
|
|
|
}
|
|
|
|
|
|
.section-hint {
|
|
|
font-size: 24rpx;
|
|
|
font-weight: normal;
|
|
|
color: #999;
|
|
|
}
|
|
|
|
|
|
.search-box {
|
|
|
margin-bottom: 24rpx;
|
|
|
}
|
|
|
|
|
|
.nurse-items {
|
|
|
background: #fff;
|
|
|
border-radius: 20rpx;
|
|
|
overflow: hidden;
|
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
|
|
}
|
|
|
|
|
|
.nurse-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
padding: 28rpx 24rpx 28rpx 20rpx;
|
|
|
border-bottom: 1rpx solid #f0f2f5;
|
|
|
|
|
|
&:last-of-type {
|
|
|
border-bottom: none;
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
background: linear-gradient(90deg, #f2f8ff 0%, #fff 56%);
|
|
|
box-shadow: inset 4rpx 0 0 #1479ff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.nurse-radio-wrap {
|
|
|
width: 44rpx;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
margin-right: 8rpx;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.nurse-radio {
|
|
|
width: 36rpx;
|
|
|
height: 36rpx;
|
|
|
border-radius: 50%;
|
|
|
border: 2rpx solid #c8ccd4;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
&.on {
|
|
|
border-color: #1479ff;
|
|
|
background: #e8f1ff;
|
|
|
}
|
|
|
|
|
|
&__dot {
|
|
|
width: 18rpx;
|
|
|
height: 18rpx;
|
|
|
border-radius: 50%;
|
|
|
background: #1479ff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.nurse-avatar {
|
|
|
margin-right: 22rpx;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.nurse-info {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.nurse-name-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12rpx;
|
|
|
flex-wrap: wrap;
|
|
|
margin-bottom: 10rpx;
|
|
|
}
|
|
|
|
|
|
.nurse-name {
|
|
|
font-size: 28rpx;
|
|
|
color: #666;
|
|
|
letter-spacing: 1rpx;
|
|
|
font-family: system-ui;
|
|
|
margin-bottom: 6rpx;
|
|
|
}
|
|
|
|
|
|
.nurse-sub {
|
|
|
font-size: 22rpx;
|
|
|
color: #999;
|
|
|
line-height: 1.4;
|
|
|
}
|
|
|
|
|
|
.empty-state {
|
|
|
background: #fff;
|
|
|
border-radius: 20rpx;
|
|
|
padding: 80rpx 20rpx 120rpx;
|
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
|
|
}
|
|
|
|
|
|
.footer-bar {
|
|
|
flex-shrink: 0;
|
|
|
padding: 20rpx 30rpx 40rpx;
|
|
|
background: #fff;
|
|
|
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.06);
|
|
|
}
|
|
|
|
|
|
.confirm-btn {
|
|
|
opacity: 1 !important;
|
|
|
border: none !important;
|
|
|
position: relative;
|
|
|
z-index: 2;
|
|
|
|
|
|
&::after {
|
|
|
border: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.safe-area {
|
|
|
padding-bottom: calc(40rpx + constant(safe-area-inset-bottom));
|
|
|
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
|
|
}
|
|
|
</style>
|