630 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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