630 lines
18 KiB
Vue
630 lines
18 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="card">
|
||
<view class="title">担架运送申请</view>
|
||
|
||
<!-- 申报科室 -->
|
||
<view class="form-item">
|
||
<text class="label required">申报科室</text>
|
||
<picker mode="multiSelector" :range="deptMultiRange"
|
||
:value="deptMultiValue" @columnchange="onDeptColumnChange"
|
||
@change="onDeptChange">
|
||
<view class="picker-value" :class="{ placeholder: !form.decDept }">
|
||
{{ currentDeptName || '请选择所在科室' }}
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 申报床号 -->
|
||
<view class="form-item">
|
||
<text class="label required">申报床号</text>
|
||
<input class="input" type="text" v-model="form.decBedNo"
|
||
placeholder="请输入床号" />
|
||
</view>
|
||
|
||
<!-- 申报手机 -->
|
||
<view class="form-item">
|
||
<text class="label required">申报手机</text>
|
||
<input class="input" type="number" maxlength="11" v-model="form.decTel"
|
||
placeholder="请输入手机号码" />
|
||
</view>
|
||
|
||
<!-- 运送工具 -->
|
||
<view class="form-item">
|
||
<text class="label required">运送工具</text>
|
||
<picker mode="selector" :range="toolNames" @change="onPickTool">
|
||
<view class="picker-value"
|
||
:class="{ placeholder: !form.carryingtools }">
|
||
{{ currentToolName || '请选择所需运送工具' }}
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 工具数量 -->
|
||
<view class="form-item">
|
||
<text class="label required">工具数量</text>
|
||
<input class="input" type="number"
|
||
v-model.number="form.carryingtoolsCount" placeholder="请输入工具数量" />
|
||
</view>
|
||
|
||
<!-- 承运人数量 -->
|
||
<view class="form-item">
|
||
<text class="label required">承运人数量</text>
|
||
<input class="input" type="number" v-model.number="form.outnumber"
|
||
placeholder="请输入所需运送人员数量" />
|
||
</view>
|
||
|
||
<!-- 是否预约 -->
|
||
<view class="form-item">
|
||
<text class="label">是否预约</text>
|
||
<switch :checked="form.isOrdered" @change="onToggleOrdered" />
|
||
</view>
|
||
|
||
<!-- 预约时间 -->
|
||
<view v-if="form.isOrdered" class="form-item">
|
||
<text class="label">预约时间</text>
|
||
<picker mode="time" :value="form.orderedDatetime || ''"
|
||
@change="onPickTime">
|
||
<view class="picker-value"
|
||
:class="{ placeholder: !form.orderedDatetime }">
|
||
{{ form.orderedDatetime || '请选择' }}
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 支付方式 -->
|
||
<view class="form-item">
|
||
<text class="label required">支付方式</text>
|
||
<picker mode="selector" :range="payNames" @change="onPickPay">
|
||
<view class="picker-value"
|
||
:class="{ placeholder: !form.paymentMethod }">
|
||
{{ currentPayName || '请选择支付方式' }}
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="btns">
|
||
<button class="btn outline" :disabled="!isCancelSubmit"
|
||
@tap="onCancelAgent">取消申请</button>
|
||
<button class="btn primary" :disabled="!isSubmit"
|
||
@tap="onSubmit">申请</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 等待接单浮层 -->
|
||
<view v-if="isShowProgress" class="overlay">
|
||
<view class="overlay-card">
|
||
<text class="overlay-title">等待骑手接单</text>
|
||
<text class="overlay-time">{{ countdownText }}</text>
|
||
<button class="btn outline" @tap="onCancelAgent">取消申请</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||
// JS 服务以 any 导入,避免类型声明缺失导致报错
|
||
// @ts-ignore
|
||
import util from '@/utils/util.js'
|
||
// @ts-ignore
|
||
import queryService from '@/service/queryService.js'
|
||
// @ts-ignore
|
||
import getService from '@/service/getService.js'
|
||
// @ts-ignore
|
||
import config from '@/config'
|
||
|
||
// 表单数据(字段需与旧版保持一致)
|
||
const form = reactive({
|
||
needcontactDept: null as any,
|
||
needcontact: null as any,
|
||
needcontactTel: null as any,
|
||
decChannel: null as any,
|
||
decDepts: null as any,
|
||
decDept: null as any,
|
||
decBedNo: null as any,
|
||
decTel: null as any,
|
||
openid: null as any,
|
||
carryingtools: null as any,
|
||
carryingtoolsCount: 1,
|
||
outnumber: 1,
|
||
isOrdered: false,
|
||
orderedDatetime: null as any,
|
||
paymentMethod: '01'
|
||
})
|
||
|
||
// 选项数据
|
||
const formData = reactive({
|
||
decDeptDatas: [] as Array<any>,
|
||
carryingtoolsDatas: [] as Array<any>,
|
||
paymentMethodDatas: [] as Array<any>
|
||
})
|
||
|
||
// UI & 轮询相关
|
||
const isSubmit = ref(true)
|
||
const isCancelSubmit = ref(false)
|
||
const isShowProgress = ref(false)
|
||
const list = ref<any[] | null>(null)
|
||
|
||
const COUNTDOWN_MAX = 5 * 60 * 1000 // 5 分钟
|
||
const countdown = ref(COUNTDOWN_MAX)
|
||
let pollTimer: any = null
|
||
let cdTimer: any = null
|
||
|
||
// 申报科室两列联动:左列父级、右列子级(无子项则显示占位)
|
||
const deptMultiRange = ref<string[][]>([[], []])
|
||
const deptParents = ref<any[]>([])
|
||
const deptChildrenMap = ref<Record<string, any[]>>({}) // parentId -> children[]
|
||
const deptMultiValue = ref<number[]>([0, 0])
|
||
const toolNames = computed(() => formData.carryingtoolsDatas.map(d => d.name || d.label || ''))
|
||
const payNames = computed(() => formData.paymentMethodDatas.map(d => d.name || d.label || ''))
|
||
|
||
const currentDeptName = computed(() => {
|
||
if (!form.decDept) return ''
|
||
// 优先在子集里找
|
||
for (const p of deptParents.value) {
|
||
const children = deptChildrenMap.value[p.id] || []
|
||
const c = children.find((it: any) => it.id === form.decDept)
|
||
if (c) return c.gridFullname || c.gridName || c.name || c.label || ''
|
||
}
|
||
// 子集中没有,则在父集中找
|
||
const p = deptParents.value.find((it: any) => it.id === form.decDept)
|
||
return p ? (p.gridFullname || p.gridName || p.name || p.label || '') : ''
|
||
})
|
||
const currentToolName = computed(() => {
|
||
const found = formData.carryingtoolsDatas.find(d => d.id === form.carryingtools)
|
||
return found ? (found.name || found.label) : ''
|
||
})
|
||
const currentPayName = computed(() => {
|
||
const found = formData.paymentMethodDatas.find(d => d.id === form.paymentMethod)
|
||
return found ? (found.name || found.label) : ''
|
||
})
|
||
|
||
const countdownText = computed(() => {
|
||
const total = Math.max(0, countdown.value)
|
||
const h = Math.floor(total / 3600000)
|
||
const m = Math.floor((total % 3600000) / 60000)
|
||
const s = Math.floor((total % 60000) / 1000)
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
return `${pad(h)}:${pad(m)}:${pad(s)}`
|
||
})
|
||
|
||
function onDeptColumnChange(e: any) {
|
||
const { column, value } = e.detail
|
||
// 切换父列,联动更新子列
|
||
if (column === 0) {
|
||
deptMultiValue.value[0] = value
|
||
const parent = deptParents.value[value]
|
||
const children = (parent && deptChildrenMap.value[parent.id]) || []
|
||
deptMultiRange.value[1] = children.length > 0
|
||
? children.map((c: any) => c.gridFullname || c.gridName || c.name || c.label || '')
|
||
: ['- 无子项 -']
|
||
deptMultiValue.value[1] = 0
|
||
}
|
||
}
|
||
|
||
function onDeptChange(e: any) {
|
||
const [pi, ci] = e.detail.value as number[]
|
||
const parent = deptParents.value[pi]
|
||
const children = (parent && deptChildrenMap.value[parent.id]) || []
|
||
if (children.length > 0) {
|
||
const child = children[ci]
|
||
if (!child) return
|
||
form.decDept = child.id
|
||
form.decDepts = ['', child.id]
|
||
} else {
|
||
if (!parent) return
|
||
form.decDept = parent.id
|
||
form.decDepts = ['', parent.id]
|
||
}
|
||
}
|
||
|
||
function onPickTool(e: any) {
|
||
const idx = Number(e.detail.value)
|
||
const item = formData.carryingtoolsDatas[idx]
|
||
if (!item) return
|
||
form.carryingtools = item.id
|
||
}
|
||
|
||
function onPickPay(e: any) {
|
||
const idx = Number(e.detail.value)
|
||
const item = formData.paymentMethodDatas[idx]
|
||
if (!item) return
|
||
form.paymentMethod = item.id
|
||
}
|
||
|
||
function onPickTime(e: any) {
|
||
form.orderedDatetime = e.detail.value
|
||
}
|
||
|
||
function onToggleOrdered(e: any) {
|
||
form.isOrdered = !!e.detail.value
|
||
}
|
||
|
||
function validate(): boolean {
|
||
// 简单校验,保持与旧版规则一致
|
||
if (!form.decDept && (!form.decDepts || (Array.isArray(form.decDepts) && form.decDepts.length <= 1))) {
|
||
uni.showToast({ title: '申报科室不能为空', icon: 'none' })
|
||
return false
|
||
}
|
||
if (!form.decBedNo) {
|
||
uni.showToast({ title: '床号不能为空', icon: 'none' })
|
||
return false
|
||
}
|
||
if (!form.decTel) {
|
||
uni.showToast({ title: '申报电话不能为空', icon: 'none' })
|
||
return false
|
||
}
|
||
if (!form.carryingtools) {
|
||
uni.showToast({ title: '运送工具不能为空', icon: 'none' })
|
||
return false
|
||
}
|
||
if (!form.carryingtoolsCount) {
|
||
uni.showToast({ title: '工具数量不能为空', icon: 'none' })
|
||
return false
|
||
}
|
||
if (!form.outnumber) {
|
||
uni.showToast({ title: '所需运送人员数量不能为空', icon: 'none' })
|
||
return false
|
||
}
|
||
if (!form.paymentMethod) {
|
||
uni.showToast({ title: '支付方式不能为空', icon: 'none' })
|
||
return false
|
||
}
|
||
if (form.isOrdered && !form.orderedDatetime) {
|
||
uni.showToast({ title: '请选择预约时间', icon: 'none' })
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function onSubmit() {
|
||
if (!validate()) return
|
||
|
||
// 兼容旧逻辑:若 decDepts 长度>1,则取第二位作为 decDept
|
||
if (form.decDepts && Array.isArray(form.decDepts) && form.decDepts.length > 1) {
|
||
form.decDept = form.decDepts[1]
|
||
}
|
||
|
||
const openid = uni.getStorageSync('openid')
|
||
form.openid = openid ? openid : config.defaultOpenId
|
||
|
||
// 组装后端要求的预约时间格式:yyyy-MM-dd HH:mm:ss
|
||
const payload: any = { ...form }
|
||
if (form.isOrdered) {
|
||
payload.orderedDatetime = toFullDateTime(form.orderedDatetime as any)
|
||
} else {
|
||
payload.orderedDatetime = null
|
||
}
|
||
|
||
try {
|
||
const res = await util.request({ url: '/API/stretchertransport/create', method: 'POST', data: payload })
|
||
if (res.data && res.data.code === 0) {
|
||
isSubmit.value = false
|
||
isCancelSubmit.value = true
|
||
isShowProgress.value = true
|
||
startPoll()
|
||
startCountdown()
|
||
uni.showToast({ title: '担架运送申请成功,等待骑手接单!', icon: 'none' })
|
||
} else {
|
||
throw new Error('create failed')
|
||
}
|
||
} catch (err) {
|
||
uni.showToast({ title: '担架运送申请失败,请重新申请!', icon: 'none' })
|
||
isShowProgress.value = false
|
||
isSubmit.value = true
|
||
isCancelSubmit.value = false
|
||
}
|
||
}
|
||
|
||
async function onCancel() {
|
||
try {
|
||
const res = await util.request({ url: `/API/stretchertransport/redis/delete/${form.decTel}`, method: 'GET' })
|
||
if (res.data && res.data.code === 0) {
|
||
isSubmit.value = true
|
||
isCancelSubmit.value = false
|
||
isShowProgress.value = false
|
||
stopPoll()
|
||
stopCountdown()
|
||
uni.showToast({ title: '担架运送申请取消成功!', icon: 'none' })
|
||
} else {
|
||
throw new Error('cancel failed')
|
||
}
|
||
} catch (err) {
|
||
uni.showToast({ title: '担架运送申请取消失败!', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function onCancelAgent() {
|
||
const res = await uni.showModal({ title: '确认取消申请提示', content: '您确定要取消本次申请吗?' })
|
||
// res: [error, result] in promise? In uni-app promise, returns [err, res]? Using any below
|
||
const result: any = Array.isArray(res) ? res[1] : (res as any)
|
||
if (result && result.confirm) {
|
||
onCancel()
|
||
}
|
||
}
|
||
|
||
function startPoll() {
|
||
if (pollTimer) return
|
||
pollTimer = setInterval(async () => {
|
||
const data = { decDept: form.decDept, decBedNo: form.decBedNo, decTel: form.decTel }
|
||
try {
|
||
const res = await queryService.queryStretcherReceiptTask(data)
|
||
if (res.data && res.data.code === 0) {
|
||
list.value = res.data.data
|
||
if (list.value && list.value.length > 0) {
|
||
// 接单成功
|
||
isShowProgress.value = false
|
||
isCancelSubmit.value = false
|
||
isSubmit.value = true
|
||
stopPoll()
|
||
stopCountdown()
|
||
resetForm()
|
||
uni.showToast({ title: '申请已被骑手接单!', icon: 'success', duration: 3000 })
|
||
}
|
||
}
|
||
} catch { }
|
||
}, 1000)
|
||
}
|
||
|
||
function stopPoll() {
|
||
if (pollTimer) clearInterval(pollTimer)
|
||
pollTimer = null
|
||
}
|
||
|
||
function startCountdown() {
|
||
countdown.value = COUNTDOWN_MAX
|
||
if (cdTimer) clearInterval(cdTimer)
|
||
cdTimer = setInterval(() => {
|
||
countdown.value -= 1000
|
||
if (countdown.value <= 0) {
|
||
onCountdownEnd()
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
function stopCountdown() {
|
||
if (cdTimer) clearInterval(cdTimer)
|
||
cdTimer = null
|
||
}
|
||
|
||
function onCountdownEnd() {
|
||
stopPoll()
|
||
stopCountdown()
|
||
uni.showToast({ title: '申请未被接单,请重新申请!', icon: 'success', duration: 4000 })
|
||
isShowProgress.value = false
|
||
isCancelSubmit.value = false
|
||
isSubmit.value = true
|
||
}
|
||
|
||
function resetForm() {
|
||
form.decDepts = null
|
||
form.decDept = null
|
||
form.decBedNo = null
|
||
form.decTel = null
|
||
form.carryingtools = null
|
||
form.carryingtoolsCount = 1
|
||
form.outnumber = 1
|
||
form.isOrdered = false
|
||
form.orderedDatetime = null
|
||
}
|
||
|
||
async function getCarryToolsType() {
|
||
try {
|
||
const res = await queryService.getDataDictionary('db_tasksheetcarryingtools_type')
|
||
if (res.data && res.data.code === 0) {
|
||
formData.carryingtoolsDatas = res.data.data.dictionaryList || []
|
||
if (formData.carryingtoolsDatas.length > 0) {
|
||
form.carryingtools = formData.carryingtoolsDatas[0].id
|
||
}
|
||
} else {
|
||
uni.showToast({ title: '获取运送工具类别失败!', icon: 'none' })
|
||
}
|
||
} catch {
|
||
uni.showToast({ title: '获取运送工具类别失败!', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function getPaymentMethodType() {
|
||
try {
|
||
const res = await queryService.getDataDictionary('db_payment_mode_type')
|
||
if (res.data && res.data.code === 0) {
|
||
formData.paymentMethodDatas = res.data.data.dictionaryList || []
|
||
if (formData.paymentMethodDatas.length > 0) {
|
||
form.paymentMethod = formData.paymentMethodDatas[0].id
|
||
}
|
||
} else {
|
||
uni.showToast({ title: '获取支付方式列表失败!', icon: 'none' })
|
||
}
|
||
} catch {
|
||
uni.showToast({ title: '获取支付方式列表失败!', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function getGrids() {
|
||
try {
|
||
const res = await queryService.getGrids()
|
||
if (res.data && res.data.code === 0) {
|
||
formData.decDeptDatas = res.data.data || []
|
||
// 构建父子结构
|
||
deptParents.value = formData.decDeptDatas
|
||
deptChildrenMap.value = {}
|
||
deptParents.value.forEach((p: any) => {
|
||
deptChildrenMap.value[p.id] = Array.isArray(p.children) ? p.children : []
|
||
})
|
||
// 初始化两列
|
||
deptMultiRange.value[0] = deptParents.value.map((p: any) => p.gridFullname || p.gridName || p.name || p.label || '')
|
||
const firstParent = deptParents.value[0]
|
||
const firstChildren = firstParent ? (deptChildrenMap.value[firstParent.id] || []) : []
|
||
deptMultiRange.value[1] = firstChildren.length > 0
|
||
? firstChildren.map((c: any) => c.gridFullname || c.gridName || c.name || c.label || '')
|
||
: ['无子项']
|
||
deptMultiValue.value = [0, 0]
|
||
|
||
// 默认选择:若有子项选第一个子项,否则选第一个父项
|
||
if (firstParent) {
|
||
if (firstChildren.length > 0) {
|
||
form.decDept = firstChildren[0].id
|
||
form.decDepts = ['', firstChildren[0].id]
|
||
} else {
|
||
form.decDept = firstParent.id
|
||
form.decDepts = ['', firstParent.id]
|
||
}
|
||
}
|
||
} else {
|
||
uni.showToast({ title: '获取网格列表失败!', icon: 'none' })
|
||
}
|
||
} catch {
|
||
uni.showToast({ title: '获取网格列表失败!', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
// 获取 openid(仅在微信小程序)
|
||
// #ifdef MP-WEIXIN
|
||
const openid = uni.getStorageSync('openid')
|
||
if (!openid) {
|
||
try { getService.getOpenId && getService.getOpenId() } catch { }
|
||
}
|
||
// #endif
|
||
|
||
await Promise.all([getCarryToolsType(), getPaymentMethodType(), getGrids()])
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopPoll()
|
||
stopCountdown()
|
||
})
|
||
|
||
function toFullDateTime(timeStr: string): string {
|
||
const now = new Date()
|
||
const yyyy = now.getFullYear()
|
||
const MM = String(now.getMonth() + 1).padStart(2, '0')
|
||
const dd = String(now.getDate()).padStart(2, '0')
|
||
if (!timeStr) return `${yyyy}-${MM}-${dd} 00:00:00`
|
||
const parts = String(timeStr).split(':')
|
||
const h = String(parts[0] ?? '00').padStart(2, '0')
|
||
const m = String(parts[1] ?? '00').padStart(2, '0')
|
||
const s = String(parts[2] ?? '00').padStart(2, '0')
|
||
return `${yyyy}-${MM}-${dd} ${h}:${m}:${s}`
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background: #f5f7fb;
|
||
}
|
||
|
||
.card {
|
||
margin: 24rpx;
|
||
padding: 24rpx;
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.title {
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
margin-bottom: 12rpx;
|
||
color: #111;
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.label {
|
||
color: #333;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.required::before {
|
||
content: '*';
|
||
color: #ff4d4f;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.input {
|
||
text-align: right;
|
||
flex: 1;
|
||
margin-left: 24rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.picker-value {
|
||
flex: 1;
|
||
text-align: right;
|
||
margin-left: 24rpx;
|
||
color: #333;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.picker-value.placeholder {
|
||
color: #999;
|
||
}
|
||
|
||
.btns {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.btn {
|
||
flex: 1;
|
||
padding: 0 20rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.btn.primary {
|
||
background: #007aff;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn.outline {
|
||
background: #fff;
|
||
color: #007aff;
|
||
border: 2rpx solid #007aff;
|
||
}
|
||
|
||
.overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.35);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 999;
|
||
}
|
||
|
||
.overlay-card {
|
||
width: 80%;
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.overlay-title {
|
||
display: block;
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.overlay-time {
|
||
display: block;
|
||
font-size: 40rpx;
|
||
color: #007aff;
|
||
margin-bottom: 16rpx;
|
||
font-weight: 600;
|
||
}
|
||
</style>
|