|
|
<!-- pages/queue/index.vue -->
|
|
|
<template>
|
|
|
<view class="page-container">
|
|
|
<view class="page-toolbar">
|
|
|
<view class="toolbar-left">
|
|
|
<button class="btn btn-ghost back-btn" @click="goBack">
|
|
|
<uni-icons type="arrow-left" size="18" color="#1e3a8a"></uni-icons>
|
|
|
返回
|
|
|
</button>
|
|
|
</view>
|
|
|
<view class="hall-overview">
|
|
|
<view class="hall-stat" @click="goToHallInfo">
|
|
|
<view class="hall-stat-icon hall-stat-wait">
|
|
|
<uni-icons type="person-filled" size="20" color="#fff"></uni-icons>
|
|
|
</view>
|
|
|
<view class="hall-stat-meta">
|
|
|
<text class="hall-stat-label">当前等候人数</text>
|
|
|
<text class="hall-stat-value">{{ waitingCount }}人</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="hall-stat" @click="goToHallInfo">
|
|
|
<view class="hall-stat-icon hall-stat-appoint">
|
|
|
<uni-icons type="calendar" size="20" color="#fff"></uni-icons>
|
|
|
</view>
|
|
|
<view class="hall-stat-meta">
|
|
|
<text class="hall-stat-label">今日预约人数</text>
|
|
|
<text class="hall-stat-value">{{ todayAppointmentCount }}人</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="hall-stat" @click="goToHallInfo">
|
|
|
<view class="hall-stat-icon hall-stat-window">
|
|
|
<uni-icons type="home-filled" size="20" color="#fff"></uni-icons>
|
|
|
</view>
|
|
|
<view class="hall-stat-meta">
|
|
|
<text class="hall-stat-label">空闲窗口</text>
|
|
|
<text class="hall-stat-value">{{ idleWindowCount }}个</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="hall-stat" @click="goToHallInfo">
|
|
|
<view class="hall-stat-icon hall-stat-kiosk">
|
|
|
<uni-icons type="settings-filled" size="20" color="#fff"></uni-icons>
|
|
|
</view>
|
|
|
<view class="hall-stat-meta">
|
|
|
<text class="hall-stat-label">空闲自助机</text>
|
|
|
<text class="hall-stat-value">{{ idleKioskCount }}台</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="tool-input">
|
|
|
<view class="search-field">
|
|
|
<uni-icons type="search" size="16" color="#94a3b8"></uni-icons>
|
|
|
<input
|
|
|
v-model="queryForm.keyword"
|
|
|
class="search-input"
|
|
|
confirm-type="search"
|
|
|
placeholder="请输入关键字"
|
|
|
@confirm="handleSearch"
|
|
|
/>
|
|
|
</view>
|
|
|
<button
|
|
|
class="btn btn-primary action-btn"
|
|
|
:disabled="loading"
|
|
|
@click="handleSearch"
|
|
|
>
|
|
|
搜索
|
|
|
</button>
|
|
|
<button
|
|
|
class="btn action-btn take-btn"
|
|
|
@click="openNormalTakeTicket"
|
|
|
>
|
|
|
普通取号
|
|
|
</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<scroll-view class="table-wrapper" scroll-y :style="tableRowStyle">
|
|
|
<view class="table-card">
|
|
|
<uni-table :loading="loading" border stripe emptyText="暂无更多数据">
|
|
|
<uni-tr>
|
|
|
<uni-th width="76" align="center">现场照片</uni-th>
|
|
|
<uni-th width="96" align="center">姓名</uni-th>
|
|
|
<uni-th width="140" align="center">身份证</uni-th>
|
|
|
<uni-th width="100" align="center">手机号</uni-th>
|
|
|
<uni-th width="72" align="center">票号</uni-th>
|
|
|
<uni-th width="130" align="center">核验时间</uni-th>
|
|
|
<uni-th width="240" align="left">一键体检</uni-th>
|
|
|
<uni-th width="200" align="center">操作</uni-th>
|
|
|
</uni-tr>
|
|
|
<uni-tr v-for="item in records" :key="item.uid">
|
|
|
<uni-td align="center">
|
|
|
<view
|
|
|
class="photo-cell"
|
|
|
:class="{ 'photo-cell-click': item._faceSrc && item.uid }"
|
|
|
@click="openFaceImgPopup(item)"
|
|
|
>
|
|
|
<img
|
|
|
v-if="item._faceSrc && useNativeFaceImg"
|
|
|
:src="item._faceSrc"
|
|
|
class="row-face-img"
|
|
|
alt=""
|
|
|
/>
|
|
|
<image
|
|
|
v-else-if="item._faceSrc"
|
|
|
:src="item._faceSrc"
|
|
|
mode="aspectFill"
|
|
|
class="row-face-img"
|
|
|
/>
|
|
|
<text v-else>-</text>
|
|
|
</view>
|
|
|
</uni-td>
|
|
|
<uni-td align="center">
|
|
|
<view class="name-cell">
|
|
|
<text class="name-text">{{ item.name || "-" }}</text>
|
|
|
<view v-if="item._verifyTags?.length" class="name-tags">
|
|
|
<text
|
|
|
v-for="(tag, idx) in item._verifyTags"
|
|
|
:key="idx"
|
|
|
class="name-tag"
|
|
|
:class="'name-tag-' + tag.tone"
|
|
|
>{{ tag.label }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</uni-td>
|
|
|
<uni-td align="center">{{ maskIdCard(item.idCard) }}</uni-td>
|
|
|
<uni-td align="center">{{ maskPhone(item.phone) }}</uni-td>
|
|
|
<uni-td align="center">{{ item.ticketId || "-" }}</uni-td>
|
|
|
<uni-td align="center">{{ item.verifyTime || "-" }}</uni-td>
|
|
|
<uni-td align="left">
|
|
|
<view class="health-cell" @click="openHealthDetail(item)">
|
|
|
<text v-if="item._health?.loading" class="health-muted">体检生成中...</text>
|
|
|
<text
|
|
|
v-else-if="item._health?.unavailable || item._health?.queryFail"
|
|
|
class="health-muted"
|
|
|
>{{ item._health.reason || "暂无法体检" }}</text>
|
|
|
<view v-else class="health-summary">
|
|
|
<text>{{ item._health?.companyCount || 0 }}户关联企业</text>
|
|
|
<text v-if="item._health?.abnormal" class="health-warn">;{{ item._health.abnormal }}户非正常</text>
|
|
|
<text v-if="item._health?.violation" class="health-warn">;{{ item._health.violation }}条未处理违法违章</text>
|
|
|
<text v-if="item._health?.undeclare">;{{ item._health.undeclare }}条未申报</text>
|
|
|
<text v-if="item._health?.owing" class="health-warn">;{{ item._health.owing }}条欠税</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</uni-td>
|
|
|
<uni-td align="left">
|
|
|
<view class="row-actions">
|
|
|
<button
|
|
|
class="btn btn-primary table-action-btn"
|
|
|
:disabled="ticketLoading"
|
|
|
@click="openTakeTicketPopup(item)"
|
|
|
>
|
|
|
取号
|
|
|
</button>
|
|
|
<button
|
|
|
v-if="needSmzCollect(item)"
|
|
|
class="btn btn-default table-action-btn"
|
|
|
:disabled="smzSaving"
|
|
|
@click="openSmzCollectPopup(item)"
|
|
|
>
|
|
|
采集
|
|
|
</button>
|
|
|
<button
|
|
|
v-if="SHOW_APPOINTMENT_SIGN_IN"
|
|
|
class="btn btn-default table-action-btn"
|
|
|
@click="openAppointmentPopup(item)"
|
|
|
>
|
|
|
预约签到
|
|
|
</button>
|
|
|
</view>
|
|
|
</uni-td>
|
|
|
</uni-tr>
|
|
|
<uni-tr v-for="n in emptyRowCount" :key="'empty-row-' + n">
|
|
|
<uni-td align="center"></uni-td>
|
|
|
<uni-td align="center"></uni-td>
|
|
|
<uni-td align="center"></uni-td>
|
|
|
<uni-td align="center"></uni-td>
|
|
|
<uni-td align="center"></uni-td>
|
|
|
<uni-td align="center"></uni-td>
|
|
|
<uni-td align="center"></uni-td>
|
|
|
<uni-td align="center"></uni-td>
|
|
|
</uni-tr>
|
|
|
</uni-table>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
|
|
|
<view class="pagination-div">
|
|
|
<view class="page-info-wrap">
|
|
|
<text class="page-info">
|
|
|
共<span class="page-span">{{ total }}</span>条
|
|
|
</text>
|
|
|
<picker
|
|
|
mode="selector"
|
|
|
:range="sizeOptions"
|
|
|
:value="sizeOptionIndex"
|
|
|
:disabled="loading"
|
|
|
@change="onSizeChange"
|
|
|
>
|
|
|
<view class="size-picker">
|
|
|
<text>每页 {{ size }} 条</text>
|
|
|
<uni-icons type="bottom" size="12" color="#64748b"></uni-icons>
|
|
|
</view>
|
|
|
</picker>
|
|
|
</view>
|
|
|
<view class="page-actions">
|
|
|
<button
|
|
|
class="page-btn"
|
|
|
:disabled="page <= 1 || loading"
|
|
|
@click="handlePrevPage"
|
|
|
>
|
|
|
上一页
|
|
|
</button>
|
|
|
<view class="page-pill">
|
|
|
<text class="page-pill-num">{{ page }}</text>
|
|
|
<text class="page-pill-sep">/</text>
|
|
|
<text>{{ totalPages }}</text>
|
|
|
</view>
|
|
|
<button
|
|
|
class="page-btn page-btn-primary"
|
|
|
:disabled="page >= totalPages || loading"
|
|
|
@click="handleNextPage"
|
|
|
>
|
|
|
下一页
|
|
|
</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<uni-popup ref="faceImgPopup" type="center" :is-mask-click="true">
|
|
|
<view class="face-img-popup">
|
|
|
<view class="popup-header">
|
|
|
<text class="popup-title">现场照片</text>
|
|
|
<view class="report-close" @click="closeFaceImgPopup">×</view>
|
|
|
</view>
|
|
|
<view class="face-img-body">
|
|
|
<view v-if="faceImgLoading" class="report-loading">照片加载中...</view>
|
|
|
<img
|
|
|
v-else-if="previewFaceSrc && previewUseNative"
|
|
|
:src="previewFaceSrc"
|
|
|
class="face-img-full"
|
|
|
alt=""
|
|
|
/>
|
|
|
<image
|
|
|
v-else-if="previewFaceSrc"
|
|
|
:src="previewFaceSrc"
|
|
|
mode="aspectFit"
|
|
|
class="face-img-full"
|
|
|
/>
|
|
|
<view v-else class="report-empty">暂无照片</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</uni-popup>
|
|
|
|
|
|
<uni-popup ref="healthPopup" type="center" :is-mask-click="true">
|
|
|
<view class="health-popup">
|
|
|
<view class="popup-header">
|
|
|
<text class="popup-title">一键体检 · {{ healthDetailName || "-" }}</text>
|
|
|
<view class="report-close" @click="closeHealthDetail">×</view>
|
|
|
</view>
|
|
|
<scroll-view class="health-popup-body" scroll-y>
|
|
|
<view v-if="healthDetail?.loading" class="health-popup-empty">体检生成中...</view>
|
|
|
<view
|
|
|
v-else-if="healthDetail?.unavailable || healthDetail?.queryFail"
|
|
|
class="health-popup-empty"
|
|
|
>{{ healthDetail.reason || "暂无法体检" }}</view>
|
|
|
<view v-else>
|
|
|
<view class="health-kv">
|
|
|
<text class="health-kv-k">关联企业</text>
|
|
|
<text class="health-kv-v">{{ healthDetail?.companyCount || 0 }} 户</text>
|
|
|
</view>
|
|
|
<view class="health-kv">
|
|
|
<text class="health-kv-k">非正常户</text>
|
|
|
<text class="health-kv-v" :class="{ 'health-warn': healthDetail?.abnormal }">{{ healthDetail?.abnormal || 0 }} 户</text>
|
|
|
</view>
|
|
|
<view class="health-kv">
|
|
|
<text class="health-kv-k">未处理违法违章</text>
|
|
|
<text class="health-kv-v" :class="{ 'health-warn': healthDetail?.violation }">{{ healthDetail?.violation || 0 }} 条</text>
|
|
|
</view>
|
|
|
<view class="health-kv">
|
|
|
<text class="health-kv-k">未申报</text>
|
|
|
<text class="health-kv-v">{{ healthDetail?.undeclare || 0 }} 条</text>
|
|
|
</view>
|
|
|
<view class="health-kv">
|
|
|
<text class="health-kv-k">欠税</text>
|
|
|
<text class="health-kv-v" :class="{ 'health-warn': healthDetail?.owing }">{{ healthDetail?.owing || 0 }} 条</text>
|
|
|
</view>
|
|
|
<view
|
|
|
v-for="(firm, idx) in healthDetail?.details || []"
|
|
|
:key="firm.djxh || idx"
|
|
|
class="health-firm"
|
|
|
>
|
|
|
<text class="health-firm-name">{{ firm.nsrmc || "-" }}</text>
|
|
|
<text class="health-firm-sub">{{ firm.nsrsbh || "-" }} · {{ firm.nsrztmc || firm.nsrztDm || "-" }}</text>
|
|
|
<text class="health-firm-sub">违法违章 {{ firm.violation || 0 }} · 未申报 {{ firm.undeclare || 0 }} · 欠税 {{ firm.owing || 0 }}</text>
|
|
|
</view>
|
|
|
<view v-if="!(healthDetail?.details || []).length" class="health-popup-empty">无关联企业</view>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
</view>
|
|
|
</uni-popup>
|
|
|
|
|
|
<TakeTicketPopup
|
|
|
ref="takeTicketPopupRef"
|
|
|
@success="fetchTableData"
|
|
|
@loading="(value) => (ticketLoading = value)"
|
|
|
@visible-change="onTakeTicketVisibleChange"
|
|
|
/>
|
|
|
|
|
|
<uni-popup ref="smzCollectPopup" type="center" :is-mask-click="false">
|
|
|
<view class="smz-collect-popup">
|
|
|
<view class="popup-header">
|
|
|
<text class="popup-title">实名采集</text>
|
|
|
<view class="report-close" @click="closeSmzCollectPopup">×</view>
|
|
|
</view>
|
|
|
<view class="smz-collect-body">
|
|
|
<view class="smz-field">
|
|
|
<text class="smz-label">手机号码<span class="smz-required">*</span></text>
|
|
|
<input
|
|
|
v-model="smzForm.sjhm"
|
|
|
class="smz-input"
|
|
|
type="number"
|
|
|
maxlength="11"
|
|
|
placeholder="请输入手机号码"
|
|
|
/>
|
|
|
</view>
|
|
|
<view class="smz-field">
|
|
|
<text class="smz-label">电子邮箱</text>
|
|
|
<input
|
|
|
v-model="smzForm.dzyx"
|
|
|
class="smz-input"
|
|
|
placeholder="请输入电子邮箱(可选)"
|
|
|
/>
|
|
|
</view>
|
|
|
<view class="smz-field">
|
|
|
<text class="smz-label">固定电话</text>
|
|
|
<input
|
|
|
v-model="smzForm.lxdh"
|
|
|
class="smz-input"
|
|
|
placeholder="请输入固定电话(可选)"
|
|
|
/>
|
|
|
</view>
|
|
|
<view class="appoint-actions">
|
|
|
<button
|
|
|
class="btn btn-primary report-btn"
|
|
|
:disabled="smzSaving"
|
|
|
@click="submitSmzCollect"
|
|
|
>
|
|
|
保存
|
|
|
</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</uni-popup>
|
|
|
|
|
|
<uni-popup ref="appointmentPopup" type="center">
|
|
|
<view class="appoint-popup">
|
|
|
<view class="popup-header">
|
|
|
<text class="popup-title">预约信息</text>
|
|
|
<view class="report-close" @click="closeAppointmentPopup">×</view>
|
|
|
</view>
|
|
|
<view class="appoint-content">
|
|
|
<view class="appoint-row">
|
|
|
<text class="appoint-label">姓名</text>
|
|
|
<text class="appoint-value">{{ currentRow?.name || "-" }}</text>
|
|
|
</view>
|
|
|
<view class="appoint-row">
|
|
|
<text class="appoint-label">身份证</text>
|
|
|
<text class="appoint-value">{{
|
|
|
maskIdCard(currentRow?.idCard)
|
|
|
}}</text>
|
|
|
</view>
|
|
|
<view class="appoint-row">
|
|
|
<text class="appoint-label">手机号</text>
|
|
|
<text class="appoint-value">{{
|
|
|
maskPhone(currentRow?.phone)
|
|
|
}}</text>
|
|
|
</view>
|
|
|
<view class="appoint-row">
|
|
|
<text class="appoint-label">预约时间段</text>
|
|
|
<text class="appoint-value">{{
|
|
|
getAppointmentTimeRange(currentRow)
|
|
|
}}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="appoint-actions">
|
|
|
<button
|
|
|
class="btn btn-primary report-btn"
|
|
|
@click="confirmAppointmentSignIn"
|
|
|
>
|
|
|
签到
|
|
|
</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</uni-popup>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { saveSmzcjxx } from "@/api/smz.js";
|
|
|
import { getOverview } from "@/api/system.js";
|
|
|
import { getFaceImage, searchTicketVerify } from "@/api/ticket.js";
|
|
|
import TakeTicketPopup from "@/components/TakeTicketPopup/TakeTicketPopup.vue";
|
|
|
import {
|
|
|
extractImageBase64FromResponse,
|
|
|
fixBase64ForDecode,
|
|
|
getMimeTypeFromBase64,
|
|
|
isWebPlatform,
|
|
|
loadFaceImageDisplaySrc,
|
|
|
normalizeBase64Pure,
|
|
|
} from "@/utils/faceImage.js";
|
|
|
import { getHealthState, loadHealthForRecords } from "@/utils/healthCheck.js";
|
|
|
import { waitSessionReady } from "@/utils/request.js";
|
|
|
import { validatePhoneNumber } from "@/utils/validator.js";
|
|
|
import { onHide, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
|
|
import { computed, getCurrentInstance, nextTick, reactive, ref } from "vue";
|
|
|
|
|
|
/** 暂时隐藏预约签到入口,恢复时改为 true */
|
|
|
const SHOW_APPOINTMENT_SIGN_IN = false;
|
|
|
|
|
|
const loading = ref(false);
|
|
|
const page = ref(1);
|
|
|
const size = ref(5);
|
|
|
const sizeOptions = Array.from({ length: 13 }, (_, index) => index + 3);
|
|
|
const sizeOptionIndex = computed(() => {
|
|
|
const index = sizeOptions.indexOf(size.value);
|
|
|
return index >= 0 ? index : 2;
|
|
|
});
|
|
|
const total = ref(0);
|
|
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / size.value) || 1));
|
|
|
const records = ref([]);
|
|
|
const emptyRowCount = computed(() => {
|
|
|
if (!records.value.length) return 0;
|
|
|
return Math.max(0, size.value - records.value.length);
|
|
|
});
|
|
|
const queueRowHeight = ref(56);
|
|
|
const tableRowStyle = computed(() => ({
|
|
|
"--queue-row-height": `${queueRowHeight.value}px`,
|
|
|
}));
|
|
|
const useNativeFaceImg = isWebPlatform();
|
|
|
const healthPopup = ref(null);
|
|
|
const healthDetailKey = ref("");
|
|
|
const healthDetailName = ref("");
|
|
|
const healthDetail = computed(() => {
|
|
|
const key = String(healthDetailKey.value || "").trim();
|
|
|
if (!key) return { unavailable: true, reason: "无身份证,暂无法体检" };
|
|
|
const row = records.value.find((item) => String(item?.idCard || "").trim() === key);
|
|
|
return row?._health || getHealthState({ idCard: key });
|
|
|
});
|
|
|
const faceImgPopup = ref(null);
|
|
|
const faceImgLoading = ref(false);
|
|
|
const previewFaceSrc = ref("");
|
|
|
const previewUseNative = ref(false);
|
|
|
const faceImgRevoke = ref(null);
|
|
|
const instance = getCurrentInstance();
|
|
|
const smzCollectPopup = ref(null);
|
|
|
const smzSaving = ref(false);
|
|
|
const smzForm = reactive({
|
|
|
sjhm: "",
|
|
|
dzyx: "",
|
|
|
lxdh: "",
|
|
|
});
|
|
|
const preChecked = 0;
|
|
|
const takeTicketPopupRef = ref(null);
|
|
|
const takeTicketPopupOpen = ref(false);
|
|
|
const appointmentPopup = ref(null);
|
|
|
const ticketLoading = ref(false);
|
|
|
const currentRow = ref(null);
|
|
|
|
|
|
const queryForm = reactive({
|
|
|
keyword: "",
|
|
|
});
|
|
|
|
|
|
const waitingCount = ref(0);
|
|
|
const todayAppointmentCount = ref(0);
|
|
|
const idleWindowCount = ref(0);
|
|
|
const idleKioskCount = ref(0);
|
|
|
|
|
|
const loadOverview = async () => {
|
|
|
try {
|
|
|
const res = await getOverview();
|
|
|
waitingCount.value = Number(res?.waitingCount ?? 0);
|
|
|
todayAppointmentCount.value = Number(res?.todayAppointmentCount ?? 0);
|
|
|
idleWindowCount.value = Number(res?.idleWindowCount ?? 0);
|
|
|
idleKioskCount.value = Number(res?.idleKioskCount ?? 0);
|
|
|
} catch (error) {
|
|
|
console.log("获取大厅概况失败", error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const goToHallInfo = () => {
|
|
|
uni.navigateTo({
|
|
|
url: "/pages/mod/hallInfo",
|
|
|
});
|
|
|
};
|
|
|
|
|
|
const revokePreviewFace = () => {
|
|
|
if (typeof faceImgRevoke.value === "function") {
|
|
|
faceImgRevoke.value();
|
|
|
}
|
|
|
faceImgRevoke.value = null;
|
|
|
};
|
|
|
|
|
|
const openFaceImgPopup = async (item) => {
|
|
|
const uid = String(item?.uid ?? "").trim();
|
|
|
if (!uid) {
|
|
|
uni.showToast({
|
|
|
title: "缺少记录 uid",
|
|
|
icon: "none",
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
revokePreviewFace();
|
|
|
previewFaceSrc.value = "";
|
|
|
previewUseNative.value = useNativeFaceImg;
|
|
|
faceImgLoading.value = true;
|
|
|
faceImgPopup.value?.open();
|
|
|
try {
|
|
|
const result = await getFaceImage(uid);
|
|
|
const base64 = extractImageBase64FromResponse(result);
|
|
|
if (!base64) {
|
|
|
uni.showToast({
|
|
|
title: "暂无照片",
|
|
|
icon: "none",
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
const { src, revoke } = await loadFaceImageDisplaySrc(base64);
|
|
|
faceImgRevoke.value = revoke;
|
|
|
previewUseNative.value =
|
|
|
isWebPlatform() || src.startsWith("blob:") || src.startsWith("data:");
|
|
|
previewFaceSrc.value = src;
|
|
|
} catch (error) {
|
|
|
console.log("获取人脸照片失败", error);
|
|
|
uni.showToast({
|
|
|
title: "照片加载失败",
|
|
|
icon: "none",
|
|
|
});
|
|
|
} finally {
|
|
|
faceImgLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const closeFaceImgPopup = () => {
|
|
|
faceImgPopup.value?.close();
|
|
|
faceImgLoading.value = false;
|
|
|
previewFaceSrc.value = "";
|
|
|
revokePreviewFace();
|
|
|
};
|
|
|
|
|
|
const matchRowKeyword = (item, keyword) => {
|
|
|
const kw = String(keyword || "").trim().toLowerCase();
|
|
|
if (!kw) return true;
|
|
|
const parts = [
|
|
|
item?.name,
|
|
|
item?.idCard,
|
|
|
maskIdCard(item?.idCard),
|
|
|
item?.phone,
|
|
|
maskPhone(item?.phone),
|
|
|
item?.ticketId,
|
|
|
item?.baishuiResp,
|
|
|
item?.verifyTime,
|
|
|
item?.ticketUid,
|
|
|
item?.uid,
|
|
|
item?.IS_VIP,
|
|
|
item?.is_vip,
|
|
|
item?.isVip,
|
|
|
];
|
|
|
return parts.some((part) => String(part ?? "").toLowerCase().includes(kw));
|
|
|
};
|
|
|
|
|
|
const isVipRecord = (item) => {
|
|
|
const vip = item?.IS_VIP ?? item?.is_vip ?? item?.isVip ?? item?.vip;
|
|
|
return vip === true || vip === 1 || vip === "1" || String(vip).toLowerCase() === "true";
|
|
|
};
|
|
|
|
|
|
const getVerifyTags = (item) => {
|
|
|
const text = String(item?.baishuiResp || "");
|
|
|
const tags = text
|
|
|
? text
|
|
|
.split(/[,,;;\s]+/)
|
|
|
.map((part) => part.trim())
|
|
|
.filter(Boolean)
|
|
|
.map((label) => ({
|
|
|
label,
|
|
|
tone: label.includes("未") ? "fail" : label.includes("已") ? "pass" : "other",
|
|
|
}))
|
|
|
: [];
|
|
|
if (isVipRecord(item)) {
|
|
|
tags.unshift({ label: "VIP", tone: "vip" });
|
|
|
}
|
|
|
return tags;
|
|
|
};
|
|
|
|
|
|
const formatFaceImgSrc = (item) => {
|
|
|
const raw = String(item?.imageBase64 ?? "").trim();
|
|
|
if (!raw) return "";
|
|
|
if (/^data:image\//i.test(raw)) return raw;
|
|
|
const pure = normalizeBase64Pure(raw);
|
|
|
if (!pure) return "";
|
|
|
const mime = getMimeTypeFromBase64(pure);
|
|
|
return `data:${mime};base64,${fixBase64ForDecode(pure)}`;
|
|
|
};
|
|
|
|
|
|
const attachFaceSrc = (list) =>
|
|
|
(Array.isArray(list) ? list : []).map((item) => ({
|
|
|
...item,
|
|
|
_faceSrc: formatFaceImgSrc(item),
|
|
|
_verifyTags: getVerifyTags(item),
|
|
|
_health: getHealthState(item),
|
|
|
}));
|
|
|
|
|
|
const applyHealthToRecords = () => {
|
|
|
records.value = records.value.map((item) => ({
|
|
|
...item,
|
|
|
_health: getHealthState(item),
|
|
|
}));
|
|
|
};
|
|
|
|
|
|
const syncPageHealth = () => {
|
|
|
loadHealthForRecords(records.value, applyHealthToRecords);
|
|
|
};
|
|
|
|
|
|
const needSmzCollect = (item) => {
|
|
|
if (!item || item._smzCollected) return false;
|
|
|
return String(item.baishuiResp || "").includes("未采集");
|
|
|
};
|
|
|
|
|
|
const getRxsfzBdxh = (row) =>
|
|
|
String(row?.rxsfzBdxh || row?.compareCode || row?.CompareCode || "").trim();
|
|
|
|
|
|
const maskIdCard = (value) => {
|
|
|
const text = String(value || "").trim();
|
|
|
if (!text) return "-";
|
|
|
if (text.length <= 8) return text;
|
|
|
return `${text.slice(0, 4)}********${text.slice(-4)}`;
|
|
|
};
|
|
|
|
|
|
const maskPhone = (value) => {
|
|
|
const text = String(value || "").trim();
|
|
|
if (!text) return "-";
|
|
|
if (text.length < 7) return text;
|
|
|
return `${text.slice(0, 3)}****${text.slice(-4)}`;
|
|
|
};
|
|
|
|
|
|
const formatDateYMD = (date) => {
|
|
|
const y = date.getFullYear();
|
|
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
const d = String(date.getDate()).padStart(2, "0");
|
|
|
return `${y}-${m}-${d}`;
|
|
|
};
|
|
|
|
|
|
// 固定查询当天至过去三天(含当天共 4 天)
|
|
|
const getVerifySearchDateRange = () => {
|
|
|
const endDate = new Date();
|
|
|
const startDate = new Date();
|
|
|
startDate.setDate(startDate.getDate() - 3);
|
|
|
return {
|
|
|
startDate: formatDateYMD(startDate),
|
|
|
endDate: formatDateYMD(endDate),
|
|
|
};
|
|
|
};
|
|
|
|
|
|
const keywordFilteredCache = ref([]);
|
|
|
const keywordCacheValue = ref("");
|
|
|
|
|
|
const fetchVerifyPage = async (pageNum, pageSize, silent) => {
|
|
|
const { startDate, endDate } = getVerifySearchDateRange();
|
|
|
return searchTicketVerify(
|
|
|
{
|
|
|
name: "",
|
|
|
idCard: "",
|
|
|
phone: "",
|
|
|
preChecked,
|
|
|
startDate,
|
|
|
endDate,
|
|
|
page: pageNum,
|
|
|
size: pageSize,
|
|
|
},
|
|
|
silent
|
|
|
? {
|
|
|
loading: false,
|
|
|
quiet: true,
|
|
|
}
|
|
|
: {},
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const fetchTableData = async (options = {}) => {
|
|
|
const silent = options.silent === true;
|
|
|
const keyword = String(queryForm.keyword || "").trim();
|
|
|
if (silent && takeTicketPopupOpen.value) {
|
|
|
return;
|
|
|
}
|
|
|
if (silent && keyword) {
|
|
|
return;
|
|
|
}
|
|
|
if (!silent) {
|
|
|
loading.value = true;
|
|
|
}
|
|
|
try {
|
|
|
if (!keyword) {
|
|
|
const result = await fetchVerifyPage(page.value, size.value, silent);
|
|
|
console.log("[queue-table] 查询参数:", {
|
|
|
keyword,
|
|
|
preChecked,
|
|
|
page: page.value,
|
|
|
size: size.value,
|
|
|
});
|
|
|
console.log("[queue-table] 接口原始返回:", result);
|
|
|
records.value = attachFaceSrc(
|
|
|
Array.isArray(result.records) ? result.records : [],
|
|
|
);
|
|
|
total.value = Number(result.total) || 0;
|
|
|
keywordFilteredCache.value = [];
|
|
|
keywordCacheValue.value = "";
|
|
|
console.log("[queue-table] 表格数据 records:", records.value);
|
|
|
console.log("[queue-table] 总条数 total:", total.value);
|
|
|
syncPageHealth();
|
|
|
return;
|
|
|
}
|
|
|
const canReuse =
|
|
|
options.fromPager === true &&
|
|
|
keywordCacheValue.value === keyword &&
|
|
|
Array.isArray(keywordFilteredCache.value);
|
|
|
let filtered = keywordFilteredCache.value;
|
|
|
if (!canReuse) {
|
|
|
const pageSize = 50;
|
|
|
let pageNum = 1;
|
|
|
let all = [];
|
|
|
let serverTotal = 0;
|
|
|
while (pageNum <= 20) {
|
|
|
const result = await fetchVerifyPage(pageNum, pageSize, true);
|
|
|
const list = Array.isArray(result.records) ? result.records : [];
|
|
|
serverTotal = Number(result.total) || 0;
|
|
|
all = all.concat(list);
|
|
|
if (list.length < pageSize || (serverTotal > 0 && all.length >= serverTotal)) {
|
|
|
break;
|
|
|
}
|
|
|
pageNum += 1;
|
|
|
}
|
|
|
filtered = all.filter((item) => matchRowKeyword(item, keyword));
|
|
|
keywordFilteredCache.value = filtered;
|
|
|
keywordCacheValue.value = keyword;
|
|
|
}
|
|
|
total.value = filtered.length;
|
|
|
const start = (page.value - 1) * size.value;
|
|
|
records.value = attachFaceSrc(filtered.slice(start, start + size.value));
|
|
|
console.log("[queue-table] 关键字过滤:", {
|
|
|
keyword,
|
|
|
matched: filtered.length,
|
|
|
page: page.value,
|
|
|
});
|
|
|
syncPageHealth();
|
|
|
} catch (error) {
|
|
|
console.log("实名核验查询失败:", error);
|
|
|
} finally {
|
|
|
if (!silent) {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
updateQueueRowHeight();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const updateQueueRowHeight = () => {
|
|
|
nextTick(() => {
|
|
|
const query = uni.createSelectorQuery();
|
|
|
if (instance?.proxy) {
|
|
|
query.in(instance.proxy);
|
|
|
}
|
|
|
query.select(".table-wrapper").boundingClientRect();
|
|
|
query.select(".table-wrapper .uni-table-th").boundingClientRect();
|
|
|
query.exec((rects) => {
|
|
|
const wrap = rects?.[0];
|
|
|
const th = rects?.[1];
|
|
|
if (!wrap?.height) return;
|
|
|
const paddingY = 24;
|
|
|
const headerH = th?.height || 44;
|
|
|
const bodyH = wrap.height - paddingY - headerH;
|
|
|
queueRowHeight.value = Math.max(44, Math.floor(bodyH / size.value));
|
|
|
});
|
|
|
});
|
|
|
};
|
|
|
|
|
|
const onSizeChange = (event) => {
|
|
|
const index = Number(event?.detail?.value);
|
|
|
const next = sizeOptions[index];
|
|
|
if (!next || next === size.value) return;
|
|
|
size.value = next;
|
|
|
page.value = 1;
|
|
|
fetchTableData();
|
|
|
};
|
|
|
|
|
|
const handleSearch = () => {
|
|
|
page.value = 1;
|
|
|
fetchTableData();
|
|
|
};
|
|
|
|
|
|
const handlePrevPage = () => {
|
|
|
if (page.value <= 1) return;
|
|
|
page.value -= 1;
|
|
|
fetchTableData({ fromPager: true });
|
|
|
};
|
|
|
|
|
|
const handleNextPage = () => {
|
|
|
if (page.value >= totalPages.value) return;
|
|
|
page.value += 1;
|
|
|
fetchTableData({ fromPager: true });
|
|
|
};
|
|
|
|
|
|
const openHealthDetail = (item) => {
|
|
|
healthDetailKey.value = String(item?.idCard || "").trim();
|
|
|
healthDetailName.value = String(item?.name || "").trim();
|
|
|
healthPopup.value?.open();
|
|
|
};
|
|
|
|
|
|
const closeHealthDetail = () => {
|
|
|
healthPopup.value?.close();
|
|
|
};
|
|
|
|
|
|
const openTakeTicketPopup = (row) => {
|
|
|
takeTicketPopupRef.value?.openWithRow(row);
|
|
|
};
|
|
|
|
|
|
const openNormalTakeTicket = () => {
|
|
|
takeTicketPopupRef.value?.open();
|
|
|
};
|
|
|
|
|
|
const onTakeTicketVisibleChange = (visible) => {
|
|
|
takeTicketPopupOpen.value = Boolean(visible);
|
|
|
if (takeTicketPopupOpen.value) {
|
|
|
stopRefreshTimer();
|
|
|
} else {
|
|
|
startRefreshTimer();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const getAppointmentTimeRange = (row) => {
|
|
|
if (!row) return "-";
|
|
|
return (
|
|
|
row.appointmentTimeRange ||
|
|
|
row.appointmentTimeSlot ||
|
|
|
row.appointmentRange ||
|
|
|
"暂无预约时间段"
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const openAppointmentPopup = (row) => {
|
|
|
currentRow.value = row;
|
|
|
appointmentPopup.value?.open();
|
|
|
};
|
|
|
|
|
|
const closeAppointmentPopup = () => {
|
|
|
appointmentPopup.value?.close();
|
|
|
};
|
|
|
|
|
|
const openSmzCollectPopup = (row) => {
|
|
|
currentRow.value = row;
|
|
|
smzForm.sjhm = String(row?.phone || "").trim();
|
|
|
smzForm.dzyx = "";
|
|
|
smzForm.lxdh = "";
|
|
|
smzCollectPopup.value?.open();
|
|
|
};
|
|
|
|
|
|
const closeSmzCollectPopup = () => {
|
|
|
smzCollectPopup.value?.close();
|
|
|
};
|
|
|
|
|
|
const submitSmzCollect = async () => {
|
|
|
if (!validatePhoneNumber(smzForm.sjhm)) {
|
|
|
uni.showToast({ title: "请输入正确手机号", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
const rxsfzBdxh = getRxsfzBdxh(currentRow.value);
|
|
|
if (!rxsfzBdxh) {
|
|
|
uni.showToast({ title: "缺少比对序号", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
smzSaving.value = true;
|
|
|
try {
|
|
|
await saveSmzcjxx({
|
|
|
rxsfzBdxh,
|
|
|
sjhm: smzForm.sjhm,
|
|
|
dzyx: smzForm.dzyx,
|
|
|
lxdh: smzForm.lxdh,
|
|
|
});
|
|
|
if (currentRow.value) currentRow.value._smzCollected = true;
|
|
|
records.value = records.value.map((item) =>
|
|
|
item?.uid === currentRow.value?.uid ? { ...item, _smzCollected: true } : item,
|
|
|
);
|
|
|
uni.showToast({ title: "采集成功", icon: "success" });
|
|
|
closeSmzCollectPopup();
|
|
|
} catch (error) {
|
|
|
uni.showToast({ title: "采集失败", icon: "none" });
|
|
|
} finally {
|
|
|
smzSaving.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const confirmAppointmentSignIn = () => {
|
|
|
if (!currentRow.value) return;
|
|
|
uni.showModal({
|
|
|
title: "确认签到",
|
|
|
content: `确认对 ${currentRow.value.name || "该用户"} 执行预约签到吗?`,
|
|
|
success: (res) => {
|
|
|
if (!res.confirm) return;
|
|
|
uni.showToast({
|
|
|
title: "签到成功",
|
|
|
icon: "success",
|
|
|
});
|
|
|
closeAppointmentPopup();
|
|
|
},
|
|
|
});
|
|
|
};
|
|
|
|
|
|
const goBack = () => {
|
|
|
uni.navigateBack();
|
|
|
};
|
|
|
|
|
|
let refreshTimer = null;
|
|
|
|
|
|
const startRefreshTimer = () => {
|
|
|
stopRefreshTimer();
|
|
|
refreshTimer = setInterval(() => {
|
|
|
loadOverview();
|
|
|
fetchTableData({ silent: true });
|
|
|
}, 15000);
|
|
|
};
|
|
|
|
|
|
const stopRefreshTimer = () => {
|
|
|
if (refreshTimer) {
|
|
|
clearInterval(refreshTimer);
|
|
|
refreshTimer = null;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
onLoad(() => {
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
|
queryForm.keyword = "";
|
|
|
}
|
|
|
});
|
|
|
|
|
|
onShow(async () => {
|
|
|
await waitSessionReady();
|
|
|
loadOverview();
|
|
|
fetchTableData();
|
|
|
startRefreshTimer();
|
|
|
updateQueueRowHeight();
|
|
|
if (typeof uni !== "undefined" && typeof uni.onWindowResize === "function") {
|
|
|
uni.onWindowResize(updateQueueRowHeight);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
onHide(() => {
|
|
|
stopRefreshTimer();
|
|
|
if (typeof uni !== "undefined" && typeof uni.offWindowResize === "function") {
|
|
|
uni.offWindowResize(updateQueueRowHeight);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
onUnload(() => {
|
|
|
stopRefreshTimer();
|
|
|
revokePreviewFace();
|
|
|
if (typeof uni !== "undefined" && typeof uni.offWindowResize === "function") {
|
|
|
uni.offWindowResize(updateQueueRowHeight);
|
|
|
}
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.page-container {
|
|
|
height: var(--pad-h);
|
|
|
overflow: hidden;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
background: #eef3f9;
|
|
|
}
|
|
|
|
|
|
.page-toolbar {
|
|
|
flex-shrink: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 16px;
|
|
|
padding: calc(env(safe-area-inset-top) + 12px) 20px 12px;
|
|
|
background: #fff;
|
|
|
border-bottom: 1px solid #d7e2ee;
|
|
|
box-shadow: 0 8px 24px rgba(30, 58, 138, 0.06);
|
|
|
z-index: 20;
|
|
|
}
|
|
|
|
|
|
.toolbar-left {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.back-btn {
|
|
|
width: auto;
|
|
|
min-width: 84px;
|
|
|
height: 40px;
|
|
|
padding: 0 14px;
|
|
|
gap: 4px;
|
|
|
}
|
|
|
|
|
|
.hall-overview {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.hall-stat {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
padding: 4px 8px 4px 4px;
|
|
|
}
|
|
|
|
|
|
.hall-stat-icon {
|
|
|
width: 40px;
|
|
|
height: 40px;
|
|
|
border-radius: 10px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.hall-stat-wait {
|
|
|
background: #2563eb;
|
|
|
}
|
|
|
|
|
|
.hall-stat-appoint {
|
|
|
background: #f59e0b;
|
|
|
}
|
|
|
|
|
|
.hall-stat-window {
|
|
|
background: #14b8a6;
|
|
|
}
|
|
|
|
|
|
.hall-stat-kiosk {
|
|
|
background: #0ea5e9;
|
|
|
}
|
|
|
|
|
|
.hall-stat-meta {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.hall-stat-label {
|
|
|
font-size: 12px;
|
|
|
color: #64748b;
|
|
|
line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
.hall-stat-value {
|
|
|
margin-top: 2px;
|
|
|
font-size: 16px;
|
|
|
font-weight: 700;
|
|
|
color: #0f172a;
|
|
|
line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
.tool-input {
|
|
|
display: flex;
|
|
|
flex: 0 1 480px;
|
|
|
min-width: 320px;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.search-field {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
height: 42px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
padding: 0 12px;
|
|
|
background: #f4f7fb;
|
|
|
border: 1px solid #d7e2ee;
|
|
|
border-radius: 10px;
|
|
|
}
|
|
|
|
|
|
.search-input {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
height: 40px;
|
|
|
font-size: 14px;
|
|
|
color: #1e293b;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.btn {
|
|
|
min-width: 84px;
|
|
|
height: 32px;
|
|
|
border-radius: 10px;
|
|
|
font-size: 14px;
|
|
|
border: none;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.action-btn {
|
|
|
width: 88px;
|
|
|
height: 42px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.take-btn {
|
|
|
width: auto;
|
|
|
min-width: 96px;
|
|
|
padding: 0 14px;
|
|
|
background: #16a34a !important;
|
|
|
color: #fff !important;
|
|
|
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.22);
|
|
|
}
|
|
|
|
|
|
.report-btn {
|
|
|
width: 84px;
|
|
|
min-width: 84px;
|
|
|
height: 28px;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
background: #2563eb;
|
|
|
color: #fff;
|
|
|
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.22);
|
|
|
}
|
|
|
|
|
|
.btn-default {
|
|
|
background: #e5e7eb;
|
|
|
color: #111827;
|
|
|
}
|
|
|
|
|
|
.btn-ghost {
|
|
|
background: #fff;
|
|
|
color: #1e3a8a;
|
|
|
border: 1px solid #c7d7ea;
|
|
|
}
|
|
|
|
|
|
.table-wrapper {
|
|
|
flex: 1;
|
|
|
height: 0;
|
|
|
min-height: 0;
|
|
|
width: 100%;
|
|
|
padding: 12px 16px;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.table-card {
|
|
|
height: 100%;
|
|
|
min-height: 100%;
|
|
|
background: #fff;
|
|
|
border: 1px solid #d7e2ee;
|
|
|
border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
box-shadow: 0 10px 28px rgba(30, 58, 138, 0.05);
|
|
|
}
|
|
|
|
|
|
.table-wrapper :deep(.uni-table) {
|
|
|
min-width: 100%;
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
.table-wrapper :deep(.uni-table-th) {
|
|
|
background: #f3f6fb;
|
|
|
color: #475569;
|
|
|
font-size: 13px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.table-wrapper :deep(.uni-table-td) {
|
|
|
color: #1e293b;
|
|
|
font-size: 13px;
|
|
|
vertical-align: middle;
|
|
|
height: var(--queue-row-height, 56px);
|
|
|
box-sizing: border-box;
|
|
|
padding-top: 4px;
|
|
|
padding-bottom: 4px;
|
|
|
}
|
|
|
|
|
|
.pagination-div {
|
|
|
flex-shrink: 0;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
padding: 10px 20px calc(env(safe-area-inset-bottom) + 12px);
|
|
|
background: #fff;
|
|
|
border-top: 1px solid #d7e2ee;
|
|
|
z-index: 20;
|
|
|
}
|
|
|
|
|
|
.page-info-wrap {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.page-info {
|
|
|
font-size: 14px;
|
|
|
color: #64748b;
|
|
|
}
|
|
|
|
|
|
.page-span {
|
|
|
color: #1d4ed8;
|
|
|
margin: 0 4px;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.size-picker {
|
|
|
min-width: 108px;
|
|
|
height: 36px;
|
|
|
padding: 0 12px;
|
|
|
border-radius: 18px;
|
|
|
background: #eef3f9;
|
|
|
border: 1px solid #d7e2ee;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
font-size: 14px;
|
|
|
font-weight: 600;
|
|
|
color: #1e3a8a;
|
|
|
}
|
|
|
|
|
|
.page-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.page-pill {
|
|
|
min-width: 92px;
|
|
|
height: 42px;
|
|
|
padding: 0 16px;
|
|
|
border-radius: 21px;
|
|
|
background: #eef3f9;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
font-size: 16px;
|
|
|
color: #64748b;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.page-pill-num {
|
|
|
color: #1e3a8a;
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
.page-pill-sep {
|
|
|
margin: 0 4px;
|
|
|
color: #94a3b8;
|
|
|
}
|
|
|
|
|
|
.page-btn {
|
|
|
min-width: 112px;
|
|
|
height: 42px;
|
|
|
padding: 0 18px;
|
|
|
border: 1px solid #c7d7ea;
|
|
|
border-radius: 21px;
|
|
|
background: #fff;
|
|
|
color: #1e3a8a;
|
|
|
font-size: 15px;
|
|
|
font-weight: 600;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
|
|
|
.page-btn-primary {
|
|
|
background: #2563eb;
|
|
|
border-color: #2563eb;
|
|
|
color: #fff;
|
|
|
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.22);
|
|
|
}
|
|
|
|
|
|
.page-btn[disabled] {
|
|
|
opacity: 0.4;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.report-popup {
|
|
|
width: calc(96 * var(--pad-vw));
|
|
|
max-width: none;
|
|
|
height: calc(92 * var(--pad-vh));
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.report-header {
|
|
|
height: 48px;
|
|
|
padding: 0 14px;
|
|
|
border-bottom: 1px solid #eee;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
.report-title {
|
|
|
font-size: 16px;
|
|
|
color: #111827;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.report-close {
|
|
|
width: 28px;
|
|
|
height: 28px;
|
|
|
line-height: 28px;
|
|
|
text-align: center;
|
|
|
border-radius: 50%;
|
|
|
background: #f3f4f6;
|
|
|
color: #6b7280;
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
.report-loading,
|
|
|
.report-empty {
|
|
|
height: 100%;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
color: #6b7280;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.health-cell {
|
|
|
width: 100%;
|
|
|
text-align: left;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.health-summary {
|
|
|
font-size: 12px;
|
|
|
line-height: 1.45;
|
|
|
color: #334155;
|
|
|
}
|
|
|
|
|
|
.health-muted {
|
|
|
font-size: 12px;
|
|
|
color: #94a3b8;
|
|
|
}
|
|
|
|
|
|
.health-warn {
|
|
|
color: #dc2626;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.health-popup {
|
|
|
width: 640px;
|
|
|
max-width: 92vw;
|
|
|
max-height: 80vh;
|
|
|
background: #fff;
|
|
|
border-radius: 16px;
|
|
|
overflow: hidden;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.health-popup-body {
|
|
|
height: 64vh;
|
|
|
padding: 16px 20px 20px;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.health-popup-empty {
|
|
|
padding: 24px 0;
|
|
|
text-align: center;
|
|
|
color: #94a3b8;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.health-kv {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
padding: 8px 0;
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.health-kv-k {
|
|
|
color: #475569;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.health-kv-v {
|
|
|
color: #1e293b;
|
|
|
}
|
|
|
|
|
|
.health-firm {
|
|
|
margin-top: 12px;
|
|
|
padding: 10px 12px;
|
|
|
background: #f8fafc;
|
|
|
border-radius: 10px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 4px;
|
|
|
}
|
|
|
|
|
|
.health-firm-name {
|
|
|
font-size: 14px;
|
|
|
font-weight: 700;
|
|
|
color: #1e3a8a;
|
|
|
}
|
|
|
|
|
|
.health-firm-sub {
|
|
|
font-size: 12px;
|
|
|
color: #64748b;
|
|
|
}
|
|
|
|
|
|
.photo-cell,
|
|
|
.name-cell {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
min-height: 0;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.photo-cell {
|
|
|
flex-direction: row;
|
|
|
}
|
|
|
|
|
|
.photo-cell-click {
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.name-cell {
|
|
|
gap: 4px;
|
|
|
}
|
|
|
|
|
|
.row-face-img {
|
|
|
width: auto;
|
|
|
height: min(56px, calc(var(--queue-row-height, 56px) - 8px));
|
|
|
aspect-ratio: 3 / 4;
|
|
|
border-radius: 4px;
|
|
|
object-fit: cover;
|
|
|
display: block;
|
|
|
background: #e2e8f0;
|
|
|
}
|
|
|
|
|
|
.face-img-popup {
|
|
|
width: 720rpx;
|
|
|
max-width: calc(92 * var(--pad-vw));
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.face-img-body {
|
|
|
padding: 12px;
|
|
|
min-height: 320px;
|
|
|
max-height: calc(70 * var(--pad-vh));
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
background: #f9fafb;
|
|
|
}
|
|
|
|
|
|
.face-img-full {
|
|
|
width: 100%;
|
|
|
max-height: calc(65 * var(--pad-vh));
|
|
|
object-fit: contain;
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.name-text {
|
|
|
font-size: 13px;
|
|
|
font-weight: 600;
|
|
|
color: #1e293b;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.name-tags {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
gap: 4px;
|
|
|
}
|
|
|
|
|
|
.name-tag {
|
|
|
padding: 1px 6px;
|
|
|
border-radius: 4px;
|
|
|
font-size: 11px;
|
|
|
line-height: 1.4;
|
|
|
color: #fff;
|
|
|
}
|
|
|
|
|
|
.name-tag-pass {
|
|
|
background: #16a34a;
|
|
|
}
|
|
|
|
|
|
.name-tag-fail {
|
|
|
background: #dc2626;
|
|
|
}
|
|
|
|
|
|
.name-tag-vip {
|
|
|
background: #c9a227;
|
|
|
}
|
|
|
|
|
|
.name-tag-other {
|
|
|
background: #64748b;
|
|
|
}
|
|
|
|
|
|
.row-actions {
|
|
|
width: 100%;
|
|
|
display: flex;
|
|
|
flex-wrap: nowrap;
|
|
|
gap: 4px;
|
|
|
justify-content: flex-start;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
.table-action-btn {
|
|
|
width: auto;
|
|
|
min-width: 56px;
|
|
|
height: 28px;
|
|
|
padding: 0 10px;
|
|
|
margin: 0;
|
|
|
font-size: 12px;
|
|
|
border-radius: 8px;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.row-actions :deep(button),
|
|
|
.row-actions :deep(.uni-button) {
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
.appoint-popup {
|
|
|
width: 680rpx;
|
|
|
max-width: calc(90 * var(--pad-vw));
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.popup-header {
|
|
|
height: 48px;
|
|
|
padding: 0 14px;
|
|
|
border-bottom: 1px solid #eee;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
.popup-title {
|
|
|
font-size: 15px;
|
|
|
color: #111827;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.appoint-content {
|
|
|
padding: 14px;
|
|
|
}
|
|
|
|
|
|
.appoint-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
padding: 10px 0;
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
}
|
|
|
|
|
|
.appoint-label {
|
|
|
color: #6b7280;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.appoint-value {
|
|
|
color: #111827;
|
|
|
font-size: 14px;
|
|
|
text-align: right;
|
|
|
}
|
|
|
|
|
|
.appoint-actions {
|
|
|
padding: 12px 14px 14px;
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
.smz-collect-popup {
|
|
|
width: 680rpx;
|
|
|
max-width: 90vw;
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.smz-collect-body {
|
|
|
padding: 14px;
|
|
|
}
|
|
|
|
|
|
.smz-field {
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.smz-label {
|
|
|
display: block;
|
|
|
margin-bottom: 6px;
|
|
|
font-size: 13px;
|
|
|
color: #475569;
|
|
|
}
|
|
|
|
|
|
.smz-required {
|
|
|
color: #dc2626;
|
|
|
margin-left: 2px;
|
|
|
}
|
|
|
|
|
|
.smz-input {
|
|
|
width: 100%;
|
|
|
height: 40px;
|
|
|
padding: 0 12px;
|
|
|
border: 1px solid #d7e2ee;
|
|
|
border-radius: 10px;
|
|
|
background: #f8fafc;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
.page-toolbar {
|
|
|
flex-wrap: wrap;
|
|
|
align-items: stretch;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.toolbar-left {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.hall-overview {
|
|
|
width: 100%;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.tool-input {
|
|
|
flex-wrap: nowrap;
|
|
|
width: 100%;
|
|
|
flex: none;
|
|
|
max-width: none;
|
|
|
}
|
|
|
|
|
|
.search-field {
|
|
|
flex: 1;
|
|
|
width: auto;
|
|
|
}
|
|
|
|
|
|
.pagination-div {
|
|
|
flex-direction: column;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|