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.

738 lines
21 KiB
Vue

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>
<div class="page-container bg-white">
<TopDivBar @back="backToIndex">
<template #left>
<div class="tabs-div">
<!-- 一级菜单 -->
<div class="first-level-tabs">
<tn-tabs
v-model="currentFirstLevelTab"
:bottom-shadow="false"
:bar="false"
height="20px"
>
<TnTabsItem
v-for="(item, index) in firstLevelTabs"
:key="index"
:title="item.text"
@click="changeFirstLevelTab(item)"
/>
</tn-tabs>
</div>
<!-- 二级菜单 -->
<div class="second-level-tabs">
<tn-tabs
v-model="currentSecondLevelTab"
:bottom-shadow="false"
:bar="false"
height="25px"
>
<TnTabsItem
v-for="(item, index) in secondLevelTabs"
:key="index"
:title="item.text"
@click="changeSecondLevelTab(item)"
/>
</tn-tabs>
</div>
</div>
</template>
</TopDivBar>
<div class="tools-div">
<div class="tool-input">
<tn-button
width="11vw"
height="32px"
:plain="true"
text-color="#0099ff"
@click="openDateTimePicker = !openDateTimePicker"
>
时间筛选<uni-icons
type="down"
size="12"
color="#0099ff"
style="margin-left: 5px"
></uni-icons>
</tn-button>
<div style="width: 1vw"></div>
<tn-button
width="11vw"
height="32px"
:plain="true"
text-color="#0099ff"
@click="shareToggle"
>
业务筛选<uni-icons
type="down"
size="12"
color="#0099ff"
style="margin-left: 5px"
></uni-icons>
</tn-button>
<div style="width: 1vw"></div>
<tn-input
placeholder="请输入票号搜索"
height="36px"
v-model="searchVal"
>
<template #prefix>
<uni-icons type="search" size="18" color="#ccc"></uni-icons>
</template>
</tn-input>
</div>
<div class="tools-btn">
<tn-button width="84px" height="32px" text-color="#fff" @click="search">
<uni-icons
type="search"
size="18"
color="#fff"
style="margin-right: 5px"
></uni-icons
>搜索
</tn-button>
</div>
</div>
<div>
<uni-table
ref="table"
:loading="loading"
border
stripe
emptyText="暂无更多数据"
>
<uni-tr>
<uni-th width="100" align="center">姓名</uni-th>
<uni-th width="120" align="center">手机号码</uni-th>
<uni-th width="120" align="center">业务类型</uni-th>
<uni-th width="100" align="center">票号(办理状态)</uni-th>
<uni-th width="120" align="center">等候时长</uni-th>
<uni-th width="180" align="center">呼叫时间</uni-th>
<uni-th width="120" align="center">办理窗口</uni-th>
<uni-th width="120" align="center">操作</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in tableData" :key="index">
<uni-td align="center">{{
item.customerName + "(" + item.roleType + ")"
}}</uni-td>
<uni-td align="center">{{ item.phoneNumber }}</uni-td>
<uni-td align="center">{{ item.bizName }}</uni-td>
<uni-td align="center">
<span class="click-able" @click="showDetail(item)">{{
item.tktId
}}</span>
<span>{{ "(" + item.status + ")" }}</span>
</uni-td>
<uni-td align="center">{{ item.waitingDuration }}</uni-td>
<uni-td align="center">{{ item.startTime || "--" }}</uni-td>
<uni-td align="center">{{ item.winName || "--" }}</uni-td>
<uni-td>
<view class="uni-group">
<button
v-show="!['5', '6'].includes(item.statusCode.toString())"
class="uni-button"
size="mini"
type="primary"
@click="confirmPriorityCall(item)"
>
优先
</button>
</view>
</uni-td>
</uni-tr>
</uni-table>
<div class="pagination-div">
<p>
共<span class="page-span">{{ total }}</span
>条数据,每页显示<span class="page-span">{{ pageSize }}</span
>条
</p>
<view class="uni-pagination-box"
><uni-pagination
show-icon
:page-size="pageSize"
:current="pageCurrent"
:total="total"
@change="change"
/></view>
</div>
</div>
<!-- 税务健康检查报告组件 -->
<TaxHealthReport
v-model:show="showReport"
:report-data="customReportData"
:company-info="customCompanyInfo"
title="自定义税务报告"
@close="onClose"
@confirm="onConfirm"
/>
<!-- 票号详情弹窗 -->
<QueueTicketDetail
v-model:show="isShowDetail"
:ticket-data="ticketDetail"
title="排队票号详情"
@close="onDetailClose"
/>
<!-- 筛选弹窗 -->
<uni-popup ref="share" type="share" safeArea backgroundColor="#fff">
<div class="popup-div">
<div class="filter-btn">
<tn-button
width="80px"
height="32px"
:plain="true"
:text="true"
text-color="#999"
border-color="#999"
@click="resetSelectValue"
>
重置
</tn-button>
<tn-button
width="80px"
height="32px"
:plain="true"
:text="true"
text-color="#0099ff"
@click="closeShareToggle"
>
确定
</tn-button>
</div>
<div class="fiter-items">
<TnCheckboxGroup v-model="selectValue">
<TnCheckbox
v-for="item in bizData"
:label="item.uid"
size="lg"
:border="true"
>{{ item.name }}
</TnCheckbox>
</TnCheckboxGroup>
</div>
</div>
</uni-popup>
<!-- 日期筛选弹窗 -->
<TnDateTimePicker
v-model="dateTimeValue"
v-model:open="openDateTimePicker"
mode="timeNoSecond"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import TopDivBar from "@/components/TopDivBar.vue";
import TnCheckboxGroup from "@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox-group.vue";
import TnCheckbox from "@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox.vue";
import TnDateTimePicker from "@/uni_modules/tuniaoui-vue3/components/date-time-picker/src/date-time-picker.vue";
import TnTabsItem from "@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs-item.vue";
import TnTabs from "@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs.vue";
import type { ReportData } from "@/components/TaxHealthReport";
import {
TaxHealthReport,
defaultReportData,
} from "@/components/TaxHealthReport";
import type { TicketDetail } from "@/components/QueueTicketDetail";
import {
QueueTicketDetail,
RatingResult,
TicketStatus,
} from "@/components/QueueTicketDetail";
import { getBizList, getTicket } from "@/api/index.js";
import { onLoad } from "@dcloudio/uni-app";
onLoad(() => {
getData();
});
/*一级菜单变量*/
let currentFirstLevelTab = ref(0);
let firstLevelTabs = [
{
text: "企业用户",
value: "enterprise",
},
{
text: "个人用户",
value: "personal",
},
];
/*二级菜单变量*/
let currentSecondLevelTab = ref(0);
let secondLevelTabs = computed(() => {
const baseTabs = [
{
text: "全部",
status: "-1",
},
{
text: "等候中",
status: "0",
},
{
text: "已完成",
status: "5,6",
},
];
// 如果是企业用户,添加预约号选项
if (firstLevelTabs[currentFirstLevelTab.value]?.value === "enterprise") {
return [
...baseTabs,
{
text: "预约号",
status: "appointment",
},
];
}
return baseTabs;
});
/*表格变量*/
let searchVal = ref("");
const tableData = ref<any[]>([]);
// 每页数据量
let pageSize = ref(10);
// 当前页
let pageCurrent = ref(1);
// 数据总量
let total = ref(0);
let loading = ref(false);
// 数据类型
let statusType = ref("0");
// 用户类型
let userType = ref("enterprise");
/* 健康度报告模块 */
const showReport = ref(false);
/*弹窗模块*/
const share = ref(null);
// 业务列表
const bizData = ref<any[]>([]);
// 已选业务列表
const selectValue = ref<Number[]>([]);
/* 日期选择模块 */
const openDateTimePicker = ref(false);
const dateTimeValue = ref("");
const toDateTimeText = (date: string, time: string) => {
const rawDate = String(date || "").trim();
const rawTime = String(time || "").trim();
if (!rawDate || rawDate === "--" || !rawTime || rawTime === "--") return "--";
// 仅取 tktDate 的日期部分(兼容 "YYYY-MM-DD" 或 "YYYY-MM-DD HH:mm:ss"
const datePart = rawDate.split(/[ T]/)[0];
// 仅取 tktTime 的时间部分(兼容 "HH:mm:ss" / "YYYY-MM-DD HH:mm:ss" / "YYYY-MM-DDTHH:mm:ss"
const normalizedTime = rawTime.replace("T", " ");
const timeMatch = normalizedTime.match(/(\d{2}:\d{2}:\d{2})/);
const timePart = timeMatch ? timeMatch[1] : "";
if (!datePart || !timePart) return "--";
return `${datePart} ${timePart}`;
};
const getDatePart = (date: string) => {
const rawDate = String(date || "").trim();
if (!rawDate || rawDate === "--") return "--";
return rawDate.split(/[ T]/)[0] || "--";
};
const parseDateTime = (dateTimeText: string) => {
if (!dateTimeText || dateTimeText === "--") return null;
const normalized = dateTimeText.replace(/-/g, "/");
const date = new Date(normalized);
return Number.isNaN(date.getTime()) ? null : date;
};
const formatDuration = (diffMs: number) => {
if (diffMs < 0) return "--";
const totalMinutes = Math.floor(diffMs / 60000);
return `${totalMinutes}分钟`;
};
// 分页触发
const change = (e) => {
// $refs.table.clearSelection()
// console.log(e)
pageCurrent.value = e.current;
getData();
};
// 获取所有数据
const getData = async () => {
const params = {
page: pageCurrent.value,
size: pageSize.value,
status: statusType.value,
bizUid: selectValue.value.join(","),
keyword: searchVal.value,
userType: userType.value,
};
console.log(params);
try {
const res = await getTicket(params);
console.log(res);
loading.value = true;
if (res && res.list) {
total.value = res.total || 0;
pageCurrent.value = res.page || 1;
// 修复:直接赋值给响应式变量
tableData.value = res.list.map((item: any) => ({
// 纳税人信息
customerName: item.customerName || "--",
companyName: item.companyName || "--",
taxNumber: item.taxNumber || "--",
phoneNumber: item.phoneNumber || "--",
roleType: item.roleType || "--",
// 票号信息
tktId: item.tktId || "--",
tktDate: item.tktDate || "--",
tktTime: item.tktTime || "--",
startTime: item.startTime || "--",
endTime: item.endTime || "--",
window: item.window || -1,
winName: item.winName || "--",
bizUid: item.bizUid || -1,
bizName: item.bizName || "--",
businessType: item.businessType || "--",
id_code: item.id_code || "--",
status: item.status || "--",
statusCode: item.statusCode || 0,
queueDateTime: toDateTimeText(item.tktDate || "--", item.tktTime || "--"),
waitingDuration: item.waitingDuration || item.waitDuration || "",
processingDuration: "--",
}));
tableData.value = tableData.value.map((item: any) => {
console.log("[waitingDuration][raw]", {
tktId: item.tktId,
tktDate: item.tktDate,
tktTime: item.tktTime,
startTime: item.startTime,
queueDateTime: item.queueDateTime,
waitingDurationFromApi: item.waitingDuration,
});
// 后端有 waitingDuration 时优先使用;没有再用 呼叫时间-取号时间 计算
if (item.waitingDuration && String(item.waitingDuration).trim()) {
console.log("[waitingDuration][use-api]", {
tktId: item.tktId,
waitingDuration: item.waitingDuration,
});
return item;
}
const queueDateTime = item.queueDateTime;
const queueDate = parseDateTime(queueDateTime);
const startDate = parseDateTime(item.startTime);
console.log("[waitingDuration][parsed]", {
tktId: item.tktId,
queueDateTime,
startTime: item.startTime,
queueDateValid: !!queueDate,
startDateValid: !!startDate,
queueTs: queueDate?.getTime?.(),
startTs: startDate?.getTime?.(),
});
if (!queueDate || !startDate || startDate.getTime() < queueDate.getTime()) {
// 兜底1如果 startTime 的日期与 tktTime 更匹配改用「startTime日期 + tktTime时间」
if (startDate && item.tktTime && item.tktTime !== "--") {
const fallbackQueueDateTime = toDateTimeText(item.startTime, item.tktTime);
const fallbackQueueDate = parseDateTime(fallbackQueueDateTime);
if (fallbackQueueDate && startDate.getTime() >= fallbackQueueDate.getTime()) {
const fallbackDiffMs = startDate.getTime() - fallbackQueueDate.getTime();
const fallbackCalculated = formatDuration(fallbackDiffMs);
console.log("[waitingDuration][fallback-startDate+tktTime]", {
tktId: item.tktId,
fallbackQueueDateTime,
diffMs: fallbackDiffMs,
waitingDuration: fallbackCalculated,
});
const endDate = parseDateTime(item.endTime);
const processingDuration =
endDate && startDate && endDate.getTime() >= startDate.getTime()
? formatDuration(endDate.getTime() - startDate.getTime())
: "--";
return {
...item,
queueDateTime: fallbackQueueDateTime,
waitingDuration: fallbackCalculated,
processingDuration,
};
}
}
// 兜底2仍异常时用绝对值差避免持续 '--'
if (queueDate && startDate) {
const absDiffMs = Math.abs(startDate.getTime() - queueDate.getTime());
const absCalculated = formatDuration(absDiffMs);
console.log("[waitingDuration][fallback-abs-diff]", {
tktId: item.tktId,
diffMs: absDiffMs,
waitingDuration: absCalculated,
});
const endDate = parseDateTime(item.endTime);
const processingDuration =
endDate && startDate && endDate.getTime() >= startDate.getTime()
? formatDuration(endDate.getTime() - startDate.getTime())
: "--";
return { ...item, waitingDuration: absCalculated, processingDuration };
}
console.log("[waitingDuration][fallback--]", {
tktId: item.tktId,
reason: !queueDate
? "queueDate invalid"
: !startDate
? "startDate invalid"
: "startTime < queueDateTime",
});
const endDate = parseDateTime(item.endTime);
const processingDuration =
endDate && startDate && endDate.getTime() >= startDate.getTime()
? formatDuration(endDate.getTime() - startDate.getTime())
: "--";
return { ...item, waitingDuration: "--", processingDuration };
}
const diffMs = startDate.getTime() - queueDate.getTime();
const calculated = formatDuration(diffMs);
const endDate = parseDateTime(item.endTime);
const processingDuration =
endDate && endDate.getTime() >= startDate.getTime()
? formatDuration(endDate.getTime() - startDate.getTime())
: "--";
console.log("[waitingDuration][calculated]", {
tktId: item.tktId,
diffMs,
waitingDuration: calculated,
processingDuration,
});
return { ...item, waitingDuration: calculated, processingDuration };
});
} else {
tableData.value = [];
total.value = 0;
}
} catch (error) {
console.log("获取数据失败!", error);
uni.showToast({
title: "数据加载失败",
icon: "none",
});
tableData.value = [];
} finally {
loading.value = false;
}
};
// 使用默认数据或自定义数据
const customReportData: ReportData = {
...defaultReportData,
registration: {
...defaultReportData.registration,
basicInfoConsistency: "自定义检查结果",
},
};
// 事件处理
const onClose = () => {
console.log("弹窗关闭");
};
const onConfirm = () => {
console.log("确认操作");
};
const search = () => {
pageCurrent.value = 1;
getData();
};
const shareToggle = async () => {
try {
const res = await getBizList();
console.log("业务列表:", res);
bizData.value = Array.isArray(res) ? res : [];
share.value?.open();
} catch (error) {
console.error("获取业务列表失败:", error);
uni.showToast({
title: "获取筛选选项失败",
icon: "none",
});
}
};
const resetSelectValue = () => {
selectValue.value = [];
};
const closeShareToggle = () => {
share.value?.close();
};
/* 票号详情 */
const isShowDetail = ref(false);
// 票号详情数据
const ticketDetail: TicketDetail = {
ticketNumber: "B025",
status: TicketStatus.PROCESSING,
queueDate: "2024-01-15",
queueTime: "14:20:15",
serviceWindow: "5号窗口",
businessType: "发票领用",
staffName: "王工",
waitingDuration: "8分钟",
processingDuration: "进行中",
// 办理中的票号还没有评价
rating: RatingResult.EXCELLENT,
ratingPerson: "张先生",
ratingContact: "139****6666",
};
// 票号详情
const showDetail = (item: any) => {
isShowDetail.value = !isShowDetail.value;
ticketDetail.ticketNumber = item.tktId;
ticketDetail.queueDate = getDatePart(item.tktDate);
// 展示 tktDate 日期部分 + tktTime 时间部分拼接后的取号时间
ticketDetail.queueTime = toDateTimeText(item.tktDate, item.tktTime) || "--";
ticketDetail.waitingDuration = item.waitingDuration || "--";
// 办理时长使用 startTime 与 endTime 计算结果
ticketDetail.processingDuration = item.processingDuration || "--";
ticketDetail.serviceWindow = item.winName;
ticketDetail.businessType = item.bizName;
ticketDetail.ratingPerson = item.customerName;
ticketDetail.ratingContact = item.phoneNumber;
};
const onDetailClose = () => {
console.log("票号详情弹窗已关闭");
};
// 切换一级菜单
const changeFirstLevelTab = (item: any) => {
console.log("切换一级菜单:", item);
userType.value = item.value;
// 重置二级菜单到第一个选项
currentSecondLevelTab.value = 0;
statusType.value = secondLevelTabs.value[0]?.status || "0";
getData();
};
// 切换二级菜单
const changeSecondLevelTab = (item: any) => {
console.log("切换二级菜单:", item);
statusType.value = item.status;
getData();
};
const confirmPriorityCall = (item: any) => {
uni.showModal({
title: "确认优先",
content: `确认对票号 ${item.tktId || "--"} 执行优先吗?`,
success: (res) => {
if (!res.confirm) return;
uni.showToast({
title: "已确认优先",
icon: "success",
});
},
});
};
const backToIndex = () => {
uni.navigateTo({
url: "/pages/index/index",
});
};
</script>
<style lang="scss">
.page-container {
min-height: 100vh;
padding-top: calc(6vh + 64px);
}
.bg-white {
background-color: #fff;
}
.tabs-div {
.first-level-tabs {
width: 100%;
}
.second-level-tabs {
width: 100%;
}
}
.tools-div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
.tool-input {
display: flex;
}
.tools-btn {
display: flex;
}
}
.click-able {
color: #0033ff;
text-decoration: underline;
}
.pagination-div {
display: flex;
justify-content: space-between;
padding: 10px 20px;
.page-span {
display: inline;
color: #0099ff;
margin: 0 4px;
}
}
.uni-group {
display: flex;
align-items: center;
}
.popup-div {
.fiter-items {
padding: 50px 300px;
}
.filter-btn {
height: 6vh;
padding: 10px 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
</style>