You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

555 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>