personnel_management_mobile/pages/callTicket/callTicket.vue

748 lines
18 KiB
Vue
Raw Permalink 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>
<!-- 外层容器使用 vw/vh 自适应屏幕 -->
<view class="wrap">
<!-- 顶部蓝色头部 -->
<view class="header-bar">
<view class="header-left">
<text class="win-no">窗口号{{ windowName }}</text>
<view class="user-dept">
<text>{{ handlerUserName }}</text>
<text>{{ handlerDeptName }}</text>
</view>
</view>
<view class="exit-btn" @click="handleExit">
<image class="icon-exit" src="../../static/images/home/logout_icon.png" />
<text>退出</text>
</view>
</view>
<!-- 中间内容区域 -->
<view class="content-box">
<view class="item-col">
<text class="col-title">当前叫号</text>
<text class="call-no">{{ currentCallNo }}</text>
</view>
<view class="item-col">
<text class="col-title">当前等候</text>
<text class="wait-num">{{ waitingCount }}</text>
</view>
</view>
<!-- 底部功能按钮区 -->
<view class="btn-group">
<view class="btn-item green" @click="handleCall" :class="{ disabled: callDisabled }">
<image class="btn-icon" src="../../static/images/takeTicket/hj_icon.png" mode="aspectFit" />
<text>呼叫</text>
</view>
<view class="btn-item blue" @click="handleReCall" :class="{ disabled: recallDisabled }">
<image class="btn-icon" src="../../static/images/takeTicket/ch_icon.png" mode="aspectFit" />
<text>重呼</text>
</view>
<view class="btn-item sky" @click="handleTransfer" :class="{ disabled: transferDisabled }">
<image class="btn-icon" src="../../static/images/takeTicket/zy_icon.png" mode="aspectFit" />
<text>转移</text>
</view>
<view class="btn-item orange" @click="handlePass" :class="{ disabled: passDisabled }">
<image class="btn-icon" src="../../static/images/takeTicket/gh_icon.png" mode="aspectFit" />
<text>过号</text>
</view>
<view class="btn-item red" @click="handleSuspend" v-if="serviceStatus === 1">
<image class="btn-icon" src="../../static/images/takeTicket/gq_icon.png" mode="aspectFit" />
<text>挂起</text>
</view>
</view>
<!-- 挂起遮罩层 -->
<view class="suspend-overlay" v-if="serviceStatus === 2">
<text class="suspend-tip">窗口已挂起,无法进行叫号操作</text>
<view class="resume-btn" @click="handleResume">
<image class="resume-btn-icon" src="../../static/images/takeTicket/hj_icon.png" mode="aspectFit" />
<text class="resume-btn-text">恢复</text>
</view>
</view>
<!-- 转移弹窗:选择目标窗口 -->
<view class="transfer-mask" v-if="showTransfer" @click="showTransfer = false">
<view class="transfer-dialog" @click.stop>
<text class="dialog-title">请选择目标窗口</text>
<scroll-view class="dialog-list" scroll-y>
<view class="transfer-item" v-for="win in targetWindowList" :key="win.id" @click="confirmTransfer(win)">
<text class="transfer-name">{{ win.windowName }}</text>
</view>
<view class="dialog-empty" v-if="targetWindowList.length === 0">
<text>暂无可转移的窗口</text>
</view>
</scroll-view>
<view class="dialog-cancel" @click="showTransfer = false">
<text>取消</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { getWindowWorkbench, callNext, recall, transferRecord, passRecord, suspendWindow, resumeWindow, getWindowList, finishHandle } from '@/api/ticket.js'
export default {
name: 'CallWindow',
data() {
return {
// 窗口信息(从本地存储读取)
windowId: '',
windowName: '',
serviceStatus: 1, // 1=接号中, 2=挂起中
handlerUserName: '',
handlerDeptName: '',
handlerUserId: '',
// 当前叫号信息
currentRecord: null, // 当前叫号记录对象
waitingCount: 0, // 等候人数
currentDeptName: '', // 当前办理的部门名称
// 转移弹窗
showTransfer: false,
targetWindowList: [],
// 操作锁(防止重复点击)
actionLock: false
}
},
computed: {
// 当前叫号显示
currentCallNo() {
return this.currentRecord ? this.currentRecord.queueNo : '----'
},
// 按钮禁用逻辑:挂起中全部禁用操作按钮
suspended() {
return this.serviceStatus === 2
},
// 呼叫按钮:挂起中 或 等候人数为0 或 操作锁时禁用
callDisabled() {
return this.suspended || this.waitingCount === 0 || this.actionLock
},
// 重呼按钮:挂起中 或 无当前记录 或 状态不是已叫号/办理中 时禁用
recallDisabled() {
if (this.suspended || !this.currentRecord || this.actionLock) return true
return ![2, 7].includes(this.currentRecord.status)
},
// 转移按钮:挂起中 或 无当前记录时禁用
transferDisabled() {
return this.suspended || !this.currentRecord || this.actionLock
},
// 过号按钮:挂起中 或 无当前记录 或 状态不是已叫号/办理中 时禁用
passDisabled() {
if (this.suspended || !this.currentRecord || this.actionLock) return true
return ![2, 7].includes(this.currentRecord.status)
}
},
onLoad() {
// 读取窗口信息
const winInfo = uni.getStorageSync('windowInfo')
if (!winInfo || !winInfo.windowId) {
uni.showToast({ title: '请先选择窗口', icon: 'none' })
setTimeout(() => {
uni.redirectTo({ url: '/pages/callTicket/chooseWindow' })
}, 1000)
return
}
this.windowId = winInfo.windowId
this.windowName = winInfo.windowName
this.serviceStatus = winInfo.serviceStatus || 1
this.handlerUserName = winInfo.handlerUserName || ''
this.handlerDeptName = winInfo.handlerDeptName || ''
this.handlerUserId = winInfo.handlerUserId || ''
// 首次加载工作台
this.fetchWorkbench()
},
methods: {
// ========== 数据获取 ==========
// 获取窗口工作台数据
async fetchWorkbench() {
try {
const res = await getWindowWorkbench({ windowId: this.windowId })
if (res.success && res.data) {
const data = res.data
// 更新窗口状态
this.serviceStatus = data.serviceStatus
this.waitingCount = data.waitingCount || 0
this.currentRecord = data.currentRecord || null
// 同步更新本地存储中的窗口状态
const winInfo = uni.getStorageSync('windowInfo') || {}
winInfo.serviceStatus = data.serviceStatus
uni.setStorageSync('windowInfo', winInfo)
// 提取部门名称
if (data.currentRecord && data.currentRecord.itemDeptName) {
this.currentDeptName = data.currentRecord.itemDeptName
}
}
} catch (e) {
console.error('获取工作台数据失败', e)
}
},
// 操作后刷新
async refreshAfterAction(delay = 500) {
await new Promise(resolve => setTimeout(resolve, delay))
await this.fetchWorkbench()
},
// ========== 按钮操作 ==========
// 退出
handleExit() {
uni.showModal({
title: '确认退出',
content: '确定要退出当前窗口吗?退出后需重新选择窗口。',
success: res => {
if (res.confirm) {
uni.removeStorageSync('windowInfo')
uni.redirectTo({ url: '/pages/callTicket/chooseWindow' })
}
}
})
},
// 呼叫下一位(自动完成当前记录)
async handleCall() {
if (this.callDisabled) return
this.actionLock = true
// 如果当前有记录,先自动完成
if (this.currentRecord) {
uni.showLoading({ title: '完成当前办理...', mask: true })
try {
await finishHandle({
recordId: this.currentRecord.id,
windowId: this.windowId
})
} catch (e) {
// 完成失败不阻塞,继续呼叫(后端可能已处理)
console.error('自动完成失败', e)
}
}
uni.showLoading({ title: '呼叫中...', mask: true })
try {
const res = await callNext({ windowId: this.windowId })
if (res.success && res.data) {
this.currentRecord = res.data
this.waitingCount = Math.max(0, this.waitingCount - 1)
if (res.data.itemDeptName) {
this.currentDeptName = res.data.itemDeptName
}
uni.showToast({ title: `已呼叫 ${res.data.queueNo}`, icon: 'success', duration: 1500 })
await this.refreshAfterAction(1000)
}
} catch (e) {
console.error('呼叫失败', e)
} finally {
uni.hideLoading()
this.actionLock = false
}
},
// 重呼
async handleReCall() {
if (this.recallDisabled) return
if (!this.currentRecord) return
this.actionLock = true
uni.showLoading({ title: '重呼中...', mask: true })
try {
const res = await recall({
recordId: this.currentRecord.id,
windowId: this.windowId
})
if (res.success && res.data) {
this.currentRecord = res.data
uni.showToast({ title: `已重呼 ${res.data.queueNo}`, icon: 'success', duration: 1500 })
await this.refreshAfterAction()
}
} catch (e) {
console.error('重呼失败', e)
} finally {
uni.hideLoading()
this.actionLock = false
}
},
// 转移 - 打开目标窗口选择弹窗
async handleTransfer() {
if (this.transferDisabled) return
// 获取可选目标窗口(排除自身,只选启用未挂起的)
uni.showLoading({ title: '加载窗口...', mask: true })
try {
const res = await getWindowList({ status: 1, serviceStatus: 1 })
if (res.success && res.data) {
const allWindows = res.data.records || []
this.targetWindowList = allWindows.filter(w => w.id !== this.windowId)
if (this.targetWindowList.length === 0) {
uni.showToast({ title: '暂无可转移的窗口', icon: 'none' })
return
}
this.showTransfer = true
}
} catch (e) {
console.error('获取窗口列表失败', e)
uni.showToast({ title: '获取窗口列表失败', icon: 'none' })
} finally {
uni.hideLoading()
}
},
// 确认转移
async confirmTransfer(win) {
if (!this.currentRecord) return
this.showTransfer = false
this.actionLock = true
uni.showLoading({ title: '转移中...', mask: true })
try {
const res = await transferRecord({
recordId: this.currentRecord.id,
windowId: this.windowId,
targetWindowId: win.id
})
if (res.success) {
uni.showToast({ title: `已转移至 ${win.windowName}`, icon: 'success', duration: 1500 })
await this.refreshAfterAction(1000)
}
} catch (e) {
console.error('转移失败', e)
} finally {
uni.hideLoading()
this.actionLock = false
}
},
// 过号
async handlePass() {
if (this.passDisabled) return
if (!this.currentRecord) return
uni.showModal({
title: '确认过号',
content: `确定将 ${this.currentRecord.queueNo} 标记为过号吗?`,
success: async modalRes => {
if (!modalRes.confirm) return
this.actionLock = true
uni.showLoading({ title: '处理中...', mask: true })
try {
const res = await passRecord({
recordId: this.currentRecord.id,
windowId: this.windowId
})
if (res.success) {
uni.showToast({ title: '已标记过号', icon: 'success', duration: 1500 })
await this.refreshAfterAction(1000)
}
} catch (e) {
console.error('过号失败', e)
} finally {
uni.hideLoading()
this.actionLock = false
}
}
})
},
// 挂起 - 直接挂起
async handleSuspend() {
this.actionLock = true
uni.showLoading({ title: '挂起中...', mask: true })
try {
const res = await suspendWindow({
windowId: this.windowId,
reason: '临时离岗'
})
if (res.success && res.data) {
this.serviceStatus = res.data.serviceStatus
// 同步更新本地存储
const winInfo = uni.getStorageSync('windowInfo') || {}
winInfo.serviceStatus = res.data.serviceStatus
uni.setStorageSync('windowInfo', winInfo)
uni.showToast({ title: '窗口已挂起', icon: 'success', duration: 1500 })
await this.refreshAfterAction()
}
} catch (e) {
console.error('挂起失败', e)
} finally {
uni.hideLoading()
this.actionLock = false
}
},
// 恢复窗口
async handleResume() {
this.actionLock = true
uni.showLoading({ title: '恢复中...', mask: true })
try {
const res = await resumeWindow({ windowId: this.windowId })
if (res.success && res.data) {
this.serviceStatus = res.data.serviceStatus
// 同步更新本地存储
const winInfo = uni.getStorageSync('windowInfo') || {}
winInfo.serviceStatus = res.data.serviceStatus
uni.setStorageSync('windowInfo', winInfo)
uni.showToast({ title: '窗口已恢复', icon: 'success', duration: 1500 })
await this.refreshAfterAction()
}
} catch (e) {
console.error('恢复失败', e)
} finally {
uni.hideLoading()
this.actionLock = false
}
}
}
}
</script>
<style scoped>
* {
box-sizing: border-box;
}
.wrap {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background: url('../../static/images/home/bg.png') no-repeat center center;
background-size: cover;
}
/* 顶部头部 */
.header-bar {
width: 100%;
background-color: rgba(19, 114, 255, 1);
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.944vh 2.083vw 1.296vh;
box-sizing: border-box;
}
.header-left {
display: flex;
flex-direction: column;
justify-content: center;
}
.win-no {
font-size: 3.125vw;
color: #fff;
font-weight: bold;
}
.user-dept {
display: flex;
margin-top: 1.111vh;
}
.user-dept text {
font-size: 2.083vw;
color: #fff;
margin-right: 2.604vw;
}
.exit-btn {
background: #fff;
border-radius: 2.604vw;
display: flex;
align-items: center;
padding: 1.389vh 1.302vw;
cursor: pointer;
}
.icon-exit {
width: 2.604vw;
height: 2.604vw;
margin-right: 1.042vw;
}
.exit-btn text {
font-size: 2.083vw;
}
/* 中间内容 */
.content-box {
flex: 1;
display: flex;
}
.item-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.col-title {
font-size: 3.125vw;
color: rgba(51, 51, 51, 1);
/* margin-bottom: 2.778vh; */
}
.call-no {
font-size: 11.458vw;
color: rgba(19, 114, 255, 1);
font-weight: bold;
}
.call-name {
font-size: 2.5vw;
color: #333;
margin-top: 2vh;
}
.call-item {
font-size: 2vw;
color: #666;
margin-top: 1vh;
}
/* 状态标签 */
.status-tag {
margin-top: 2vh;
padding: 1vh 2vw;
border-radius: 2vw;
}
.status-tag text {
font-size: 2vw;
color: #fff;
}
.tag-called {
background-color: #1976f2;
}
.tag-handling {
background-color: #28c958;
}
.wait-num {
font-size: 11.458vw;
color: #ff5522;
font-weight: bold;
}
/* 挂起全屏遮罩 */
.suspend-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 998;
}
.suspend-tip {
font-size: 3.125vw;
color: #fff;
font-weight: bold;
margin-bottom: 4vh;
}
/* 底部按钮 */
.btn-group {
display: flex;
padding: 0 2.083vw 5.556vh;
gap: 1.667vw;
}
.btn-item {
flex: 1;
height: 22.222vh;
border-radius: 1.667vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: opacity 0.2s ease;
}
.btn-item:active {
opacity: 0.8;
}
.btn-icon {
width: 3.646vw;
height: 3.646vw;
margin-bottom: 1.667vh;
}
.btn-item text {
font-size: 3.021vw;
color: #fff;
}
.green {
background-color: #28c958;
}
.blue {
background-color: #1976f2;
}
.sky {
background-color: #18a0f0;
}
.orange {
background-color: #ff9922;
}
.red {
background-color: #ff6633;
}
.resume-btn {
width: 18vw;
height: 22.222vh;
border-radius: 1.667vw;
background-color: #28c958;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.resume-btn:active {
opacity: 0.8;
}
.resume-btn-icon {
width: 3.646vw;
height: 3.646vw;
margin-bottom: 1.667vh;
}
.resume-btn-text {
font-size: 3.021vw;
color: #fff;
}
/* 按钮禁用态 */
.btn-item.disabled {
opacity: 0.4;
pointer-events: none;
}
/* ========== 弹窗通用 ========== */
.transfer-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
/* 转移弹窗 */
.transfer-dialog {
width: 55vw;
max-height: 70vh;
background: #fff;
border-radius: 2vw;
padding: 3vh 2vw;
display: flex;
flex-direction: column;
}
.dialog-title {
font-size: 3vw;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 3vh;
flex-shrink: 0;
}
.dialog-list {
flex: 1;
max-height: 45vh;
overflow-y: auto;
margin-bottom: 2vh;
}
.transfer-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3vh 2vw;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.transfer-item:active {
background: #f0f4ff;
}
.transfer-name {
font-size: 2.4vw;
font-weight: bold;
color: #1976f2;
}
.transfer-desc {
font-size: 2vw;
color: #999;
}
.dialog-empty {
text-align: center;
padding: 6vh 0;
font-size: 2.2vw;
color: #999;
}
.dialog-cancel {
text-align: center;
padding: 2vh 0;
font-size: 2.5vw;
color: #999;
cursor: pointer;
}
.dialog-cancel {
text-align: center;
padding: 2vh 0;
font-size: 2.5vw;
color: #999;
cursor: pointer;
}
</style>