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

802 lines
17 KiB
Vue

<template>
<view class="page-container">
<div class="top-div">
<div></div>
<view class="top-actions">
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="refreshMonitorList">
<uni-icons type="refresh" size="16" color="#0099ff" style="margin-right: 5px"></uni-icons>
</tn-button>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px"></uni-icons>
</tn-button>
</view>
</div>
<view class="content">
<!-- 窗口状态监控 -->
<view class="status-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">窗口状态监控</text>
<view class="status-summary">
<text class="summary-item">空闲窗口: {{ freeWindowsCount }}</text>
<text class="summary-item">忙碌窗口: {{ busyWindowsCount }}</text>
</view>
</view>
<!-- 人工窗口 -->
<view class="window-group">
<view class="group-title">
<uni-icons type="staff" size="20" color="#1890ff"></uni-icons>
<text>人工服务窗口</text>
</view>
<view class="windows-grid">
<view v-for="window in manualWindows" :key="window.id" class="window-item"
:class="getWindowStatusClass(window.status)" @click="handleWindowClick(window)">
<view class="window-header">
<text class="window-number">{{ window.number }}</text>
<view class="window-status" :class="'status-' + window.status">
{{ getWindowStatusText(window.status) }}
</view>
</view>
<view class="window-info">
<text class="window-type">{{ window.type }}</text>
<text class="waiting-count">等候: {{ window.waitingCount }}人</text>
</view>
<view class="window-current">
<text class="current-task" v-if="window.currentTask">
当前: {{ window.currentTask }}
</text>
<text class="no-task" v-else>暂无业务</text>
</view>
</view>
</view>
</view>
<!-- 自助设备 -->
<!-- <view class="window-group">
<view class="group-title">
<uni-icons type="settings" size="20" color="#52c41a"></uni-icons>
<text>自助办税设备</text>
</view>
<view class="devices-grid">
<view v-for="device in selfServiceDevices" :key="device.id" class="device-item"
:class="getDeviceStatusClass(device.status)">
<view class="device-icon">
<uni-icons :type="device.icon" size="30"
:color="getDeviceIconColor(device.status)"></uni-icons>
</view>
<view class="device-info">
<text class="device-name">{{ device.name }}</text>
<text class="device-status" :class="'status-' + device.status">
{{ getDeviceStatusText(device.status) }}
</text>
<text class="device-usage" v-if="device.status === 'busy'">
使用中
</text>
<text class="device-usage" v-else>
空闲
</text>
</view>
</view>
</view>
</view> -->
</view>
</view>
</view>
<!-- 窗口详情弹窗 -->
<uni-popup ref="windowPopup" type="center" background-color="#fff">
<view class="popup-content" v-if="selectedWindow">
<view class="popup-header">
<text class="popup-title">窗口详情 - {{ selectedWindow.number }}</text>
<uni-icons type="close" size="20" color="#999" @click="closeWindowPopup"></uni-icons>
</view>
<view class="popup-body">
<view class="detail-grid">
<view class="detail-item">
<text class="detail-label">窗口编号:</text>
<text class="detail-value">{{ selectedWindow.number }}</text>
</view>
<view class="detail-item">
<text class="detail-label">窗口类型:</text>
<text class="detail-value">{{ selectedWindow.type }}</text>
</view>
<view class="detail-item">
<text class="detail-label">当前状态:</text>
<view class="detail-value">
<view class="status-badge" :class="'status-' + selectedWindow.status">
{{ getWindowStatusText(selectedWindow.status) }}
</view>
</view>
</view>
<view class="detail-item">
<text class="detail-label">等候人数:</text>
<text class="detail-value">{{ selectedWindow.waitingCount }}人</text>
</view>
<view class="detail-item">
<text class="detail-label">当前业务:</text>
<text class="detail-value">{{
selectedWindow.currentTask || "无"
}}</text>
</view>
<view class="detail-item">
<text class="detail-label">工作人员:</text>
<text class="detail-value">{{
selectedWindow.operator || "未分配"
}}</text>
</view>
</view>
<view class="waiting-list" v-if="selectedWindow.waitingCount > 0">
<view class="list-title">等候队列</view>
<view class="list-items">
<view v-for="(person, index) in getWaitingList(
selectedWindow.waitingCount,
)" :key="index" class="list-item">
<text class="queue-number">第{{ index + 1 }}位</text>
<text class="estimate-time">预计等待: {{ (index + 1) * 5 }}分钟</text>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<tn-button type="primary" @click="handleWindowAction(selectedWindow)">
<uni-icons type="gear" size="16" color="#fff"></uni-icons>
窗口操作
</tn-button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup lang="ts">
import { getMonitorList } from "@/api/window.js";
import { onLoad } from "@dcloudio/uni-app";
import { computed, ref } from "vue";
//
const windowPopup = ref();
//
const manualWindows = ref([
{
id: 1,
number: "A001",
type: "",
status: "busy",
waitingCount: 5,
currentTask: "",
operator: "",
},
{
id: 2,
number: "A002",
type: "",
status: "free",
waitingCount: 2,
currentTask: "",
operator: "",
},
{
id: 3,
number: "A003",
type: "",
status: "busy",
waitingCount: 3,
currentTask: "",
operator: "",
},
{
id: 4,
number: "A004",
type: "",
status: "free",
waitingCount: 0,
currentTask: "",
operator: "",
},
{
id: 5,
number: "A005",
type: "",
status: "pause",
waitingCount: 4,
currentTask: "",
operator: "",
},
{
id: 6,
number: "A006",
type: "",
status: "free",
waitingCount: 1,
currentTask: "",
operator: "",
},
]);
//
const selfServiceDevices = ref([
{
id: 1,
name: "",
status: "free",
icon: "printer",
},
{
id: 2,
name: "",
status: "busy",
icon: "document",
},
{
id: 3,
name: "",
status: "free",
icon: "search",
},
{
id: 4,
name: "",
status: "maintenance",
icon: "cloud-upload",
},
{
id: 5,
name: "",
status: "free",
icon: "paperplane",
},
{
id: 6,
name: "",
status: "busy",
icon: "card",
},
]);
//
const freeWindowsCount = computed(() => {
return manualWindows.value.filter((window) => window.status === "空闲")
.length;
});
const busyWindowsCount = computed(() => {
return manualWindows.value.filter((window) => window.status === "忙碌")
.length;
});
// // 计算属性
// const editing = computed(() => {
// return hallParams.value.some(param => param.editing)
// })
// 选中的窗口
const selectedWindow = ref<any>(null);
// 窗口状态处理
const getWindowStatusClass = (status : string) => {
const classMap : any = {
free: "window-free",
busy: "window-busy",
pause: "window-pause",
空闲: "window-free",
忙碌: "window-busy",
暂停: "window-pause",
};
return classMap[status] || "window-free";
};
const getWindowStatusText = (status : string) => {
const textMap : any = {
free: "空闲",
busy: "忙碌",
pause: "暂停",
空闲: "空闲",
忙碌: "忙碌",
暂停: "暂停",
};
return textMap[status] || "未知";
};
// 设备状态处理
const getDeviceStatusClass = (status : string) => {
const classMap : any = {
free: "device-free",
busy: "device-busy",
maintenance: "device-maintenance",
};
return classMap[status] || "device-free";
};
const getDeviceStatusText = (status : string) => {
const textMap : any = {
free: "空闲",
busy: "使用中",
maintenance: "维护中",
};
return textMap[status] || "未知";
};
const getDeviceIconColor = (status : string) => {
const colorMap : any = {
free: "#52c41a",
busy: "#fa541c",
maintenance: "#faad14",
};
return colorMap[status] || "#52c41a";
};
// 调用监控列表接口并回填页面数据
const refreshMonitorList = async () => {
try {
const res = await getMonitorList();
console.log("大厅监控列表", res);
// 兼容后端不同字段命名
const windows = res?.list || res?.windowList || res?.windows;
const devices = res?.selfServiceDevices || res?.deviceList || res?.devices;
if (Array.isArray(windows)) {
manualWindows.value = windows.map((item : any, index : number) => {
const businesses = Array.isArray(item?.supportedBusinesses)
? item.supportedBusinesses
: [];
const type = businesses
.slice()
.sort((a : any, b : any) => (a?.priority ?? 0) - (b?.priority ?? 0))
.map((b : any) => b?.businessName)
.filter(Boolean)
.join(",");
return {
...item,
id: item?.windowId ?? index,
number: item?.windowName || "",
type,
currentTask: item?.currentTicketNo || "",
status: item?.windowStatus || "",
waitingCount: Number(item?.waitingCount ?? 0),
};
});
}
if (Array.isArray(devices)) {
selfServiceDevices.value = devices;
}
} catch (error) {
console.log("获取大厅监控列表失败", error);
}
};
// 窗口点击处理
const handleWindowClick = (window : any) => {
selectedWindow.value = window;
windowPopup.value.open();
};
const closeWindowPopup = () => {
windowPopup.value.close();
};
// 获取等候列表(模拟数据)
const getWaitingList = (count : number) => {
return Array.from(
{
length: count,
},
(_, index) => ({
id: index + 1,
queueNumber: index + 1,
}),
);
};
// 窗口操作
const handleWindowAction = (window : any) => {
uni.showActionSheet({
itemList: ["暂停服务", "重置窗口", "呼叫下一位", "查看统计"],
success: (res) => {
const actions = ["pause", "reset", "callNext", "viewStats"];
const action = actions[res.tapIndex];
// 执行相应操作
uni.showToast({
title: `执行操作: ${action}`,
icon: "success",
});
},
});
};
const backToIndex = () => {
uni.navigateTo({
url: "/pages/index/index",
});
};
onLoad(() => {
refreshMonitorList();
});
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.top-div {
display: flex;
justify-content: space-between;
background-color: #fff;
padding: 6vh 20px 10px 20px;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
box-sizing: border-box;
}
.top-actions {
display: flex;
gap: 10px;
}
.content {
padding: 20rpx;
margin-top: 100px;
}
// 通用卡片样式
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.status-summary {
display: flex;
gap: 20rpx;
.summary-item {
font-size: 24rpx;
color: #666;
}
}
}
// 窗口组样式
.window-group {
margin-bottom: 40rpx;
.group-title {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
// 人工窗口网格
.windows-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.window-item {
padding: 24rpx;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
transition: all 0.3s;
&.window-free {
border-color: #52c41a;
background: #f6ffed;
}
&.window-busy {
border-color: #fa541c;
background: #fff2e8;
}
&.window-pause {
border-color: #faad14;
background: #fffbe6;
}
&:active {
transform: scale(0.98);
}
.window-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
.window-number {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.window-status {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
font-weight: 500;
&.status-free {
background: #52c41a;
color: #fff;
}
&.status-busy {
background: #fa541c;
color: #fff;
}
&.status-pause {
background: #faad14;
color: #fff;
}
}
}
.window-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
.window-type {
font-size: 24rpx;
color: #666;
}
.waiting-count {
font-size: 22rpx;
color: #fa541c;
font-weight: 500;
}
}
.window-current {
.current-task {
font-size: 22rpx;
color: #333;
background: #f0f0f0;
padding: 4rpx 8rpx;
border-radius: 6rpx;
}
.no-task {
font-size: 22rpx;
color: #999;
font-style: italic;
}
}
}
}
// 自助设备网格
.devices-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
.device-item {
padding: 24rpx;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
text-align: center;
&.device-free {
border-color: #52c41a;
background: #f6ffed;
}
&.device-busy {
border-color: #fa541c;
background: #fff2e8;
}
&.device-maintenance {
border-color: #faad14;
background: #fffbe6;
}
.device-icon {
margin-bottom: 15rpx;
}
.device-info {
.device-name {
display: block;
font-size: 24rpx;
color: #333;
margin-bottom: 8rpx;
}
.device-status {
display: block;
font-size: 20rpx;
margin-bottom: 5rpx;
&.status-free {
color: #52c41a;
}
&.status-busy {
color: #fa541c;
}
&.status-maintenance {
color: #faad14;
}
}
.device-usage {
font-size: 18rpx;
color: #999;
}
}
}
}
// 参数表格样式
.param-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.param-value {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
.param-unit {
font-size: 22rpx;
color: #999;
}
}
.param-edit {
display: flex;
justify-content: center;
}
.param-actions {
display: flex;
justify-content: center;
.edit-actions {
display: flex;
gap: 10rpx;
}
}
// 弹窗样式
.popup-content {
width: 650rpx;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.popup-body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
.detail-grid {
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.detail-label {
font-size: 26rpx;
color: #666;
}
.detail-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.status-badge {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
color: #fff;
&.status-free {
background: #52c41a;
}
&.status-busy {
background: #fa541c;
}
&.status-pause {
background: #faad14;
}
}
}
}
.waiting-list {
margin-top: 30rpx;
.list-title {
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
}
.list-items {
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.queue-number {
font-size: 24rpx;
color: #333;
}
.estimate-time {
font-size: 22rpx;
color: #999;
}
}
}
}
.popup-footer {
padding: 30rpx;
border-top: 1rpx solid #f0f0f0;
text-align: center;
}
</style>