feat: 调整了字段的展示

This commit is contained in:
2026-07-27 11:42:53 +08:00
parent 59362e37e5
commit 6d629181ce
5 changed files with 46 additions and 64 deletions

View File

@@ -3,7 +3,7 @@
<view class="queue-ticket" :class="`queue-ticket--${ticketTone}`"> <view class="queue-ticket" :class="`queue-ticket--${ticketTone}`">
<view class="queue-ticket__topline"> <view class="queue-ticket__topline">
<view> <view>
<text class="queue-ticket__project">{{ ticket.project_name }}</text> <text class="queue-ticket__project">{{ ticket.projectName }}</text>
<text class="queue-ticket__title">{{ ticketLabel }}</text> <text class="queue-ticket__title">{{ ticketLabel }}</text>
</view> </view>
<text class="queue-ticket__status-mark" /> <text class="queue-ticket__status-mark" />
@@ -11,24 +11,24 @@
<view class="queue-ticket__number"> <view class="queue-ticket__number">
<text>我的号码</text> <text>我的号码</text>
<text class="queue-ticket__number-value">{{ ticket.ticket_number }}</text> <text class="queue-ticket__number-value">{{ ticket.ticketNumber }}</text>
<text class="queue-ticket__number-meta">本号 {{ ticket.party_size }} </text> <text class="queue-ticket__number-meta">本号 {{ ticket.partySize }} </text>
</view> </view>
<view class="queue-ticket__metrics"> <view class="queue-ticket__metrics">
<view class="queue-ticket__metric"> <view class="queue-ticket__metric">
<text>当前最新到号</text> <text>当前最新到号</text>
<text class="queue-ticket__metric-value">{{ ticket.latest_called_number || "暂无" }}</text> <text class="queue-ticket__metric-value">{{ ticket.latestCalledNumber || "暂无" }}</text>
</view> </view>
<template v-if="ticketStatus === 'WAITING' && !projectPaused"> <template v-if="ticketStatus === 'WAITING' && !projectPaused">
<view class="queue-ticket__metric"> <view class="queue-ticket__metric">
<text>预计等待时间</text> <text>预计等待时间</text>
<text class="queue-ticket__metric-value">{{ formatEstimatedWait(ticket.estimated_wait) }}</text> <text class="queue-ticket__metric-value">{{ formatEstimatedWait(ticket.estimatedWait) }}</text>
</view> </view>
<view class="queue-ticket__metric queue-ticket__metric--wide"> <view class="queue-ticket__metric queue-ticket__metric--wide">
<text>前方排队</text> <text>前方排队</text>
<text class="queue-ticket__metric-value">{{ ticket.tickets_ahead == null ? "未知" : ticket.tickets_ahead + " 个号码" }}</text> <text class="queue-ticket__metric-value">{{ ticket.ticketsAhead == null ? "未知" : ticket.ticketsAhead + " 个号码" }}</text>
<text class="queue-ticket__metric-meta">{{ ticket.people_ahead == null ? "人数未知" : "共 " + ticket.people_ahead + " 人" }}</text> <text class="queue-ticket__metric-meta">{{ ticket.peopleAhead == null ? "人数未知" : "共 " + ticket.peopleAhead + " 人" }}</text>
</view> </view>
</template> </template>
</view> </view>
@@ -41,7 +41,7 @@
<view v-if="ticketStatus === 'CALLED'" class="queue-call-action"> <view v-if="ticketStatus === 'CALLED'" class="queue-call-action">
<text>现在请前往</text> <text>现在请前往</text>
<text class="queue-call-action__entrance">{{ ticket.entrance || "现场入口" }}</text> <text class="queue-call-action__entrance">{{ ticket.entrance || "现场入口" }}</text>
<text class="queue-call-action__desc">请出示排队号码叫号时间 {{ formatDateTime(ticket.called_at) }}</text> <text class="queue-call-action__desc">请出示排队号码叫号时间 {{ formatDateTime(ticket.calledAt) }}</text>
</view> </view>
<text v-if="ticketStatus !== 'WAITING' && ticketStatus !== 'CALLED'" class="queue-ticket__guidance"> <text v-if="ticketStatus !== 'WAITING' && ticketStatus !== 'CALLED'" class="queue-ticket__guidance">
@@ -51,7 +51,7 @@
<view class="queue-ticket__metadata"> <view class="queue-ticket__metadata">
<text>手机号尾号 {{ phoneLast4 }}</text> <text>手机号尾号 {{ phoneLast4 }}</text>
<text>更新于 {{ formatDateTime(lastUpdatedAt || ticket.last_updated_at) }}</text> <text>更新于 {{ formatDateTime(lastUpdatedAt || ticket.lastUpdatedAt) }}</text>
</view> </view>
</view> </view>
@@ -78,13 +78,13 @@ const props = defineProps({
}); });
const ticketStatus = computed(() => String(props.ticket?.status || "").toUpperCase()); const ticketStatus = computed(() => String(props.ticket?.status || "").toUpperCase());
const statusMeta = computed(() => visitorStatusMeta(props.ticket.status, props.ticket.people_ahead)); const statusMeta = computed(() => visitorStatusMeta(props.ticket.status, props.ticket.peopleAhead));
const projectPaused = computed(() => ticketStatus.value === "WAITING" && String(props.ticket?.project?.status || "").toUpperCase() === "PAUSED"); const projectPaused = computed(() => ticketStatus.value === "WAITING" && String(props.ticket?.project?.status || "").toUpperCase() === "PAUSED");
const ticketTone = computed(() => projectPaused.value ? "warning" : statusMeta.value.tone); const ticketTone = computed(() => projectPaused.value ? "warning" : statusMeta.value.tone);
const ticketLabel = computed(() => projectPaused.value ? "项目暂时暂停" : statusMeta.value.label); const ticketLabel = computed(() => projectPaused.value ? "项目暂时暂停" : statusMeta.value.label);
const phoneLast4 = computed(() => String(props.ticket?.phone_last4 || "").replace(/\D/g, "").slice(-4) || "未知"); const phoneLast4 = computed(() => String(props.ticket?.phoneLast4 || "").replace(/\D/g, "").slice(-4) || "未知");
const visitorNotice = computed(() => { const visitorNotice = computed(() => {
if (props.ticket?.visitor_notice === null) return ""; if (props.ticket?.visitorNotice === null) return "";
return props.ticket?.visitor_notice?.trim() || "请您在景区附近等候,注意听从工作人员指引。"; return props.ticket?.visitorNotice?.trim() || "请您在景区附近等候,注意听从工作人员指引。";
}); });
</script> </script>

View File

@@ -87,18 +87,18 @@
</picker> </picker>
</label> </label>
<text v-if="selectedProject?.visitor_notice" class="queue-notice">{{ selectedProject.visitor_notice }}</text> <text v-if="selectedProject?.visitorNotice" class="queue-notice">{{ selectedProject.visitorNotice }}</text>
<label class="queue-field"> <label class="queue-field">
<text>同行人数</text> <text>同行人数</text>
<input <input
type="number" type="number"
inputmode="numeric" inputmode="numeric"
:value="ticketForm.party_size" :value="ticketForm.partySize"
:disabled="createBusy" :disabled="createBusy"
:min="minPartySize" :min="minPartySize"
:max="maxPartySize" :max="maxPartySize"
@input="handleTicketFormInput('party_size', $event)" @input="handleTicketFormInput('partySize', $event)"
/> />
<text class="queue-field__hint">本项目每个号码可绑定 {{ minPartySize }} {{ maxPartySize }} </text> <text class="queue-field__hint">本项目每个号码可绑定 {{ minPartySize }} {{ maxPartySize }} </text>
</label> </label>
@@ -120,9 +120,9 @@
<label class="queue-field"> <label class="queue-field">
<text>姓氏选填</text> <text>姓氏选填</text>
<input <input
:value="ticketForm.last_name" :value="ticketForm.lastName"
:disabled="createBusy" :disabled="createBusy"
@input="handleTicketFormInput('last_name', $event)" @input="handleTicketFormInput('lastName', $event)"
/> />
</label> </label>
@@ -216,7 +216,7 @@
:show-retry="Boolean(currentStatusResource.error && currentStatusResource.data)" :show-retry="Boolean(currentStatusResource.error && currentStatusResource.data)"
@retry="refreshCurrentStatus" @retry="refreshCurrentStatus"
/> />
<TicketCard :ticket="tokenResource.data" :last-updated-at="tokenResource.data.last_updated_at || tokenResource.lastSuccessAt" /> <TicketCard :ticket="tokenResource.data" :last-updated-at="tokenResource.data.lastUpdatedAt || tokenResource.lastSuccessAt" />
<button class="queue-button queue-button--secondary queue-refresh" :disabled="tokenResource.refreshing" hover-class="none" @click="refreshCurrentStatus"> <button class="queue-button queue-button--secondary queue-refresh" :disabled="tokenResource.refreshing" hover-class="none" @click="refreshCurrentStatus">
{{ tokenResource.refreshing ? "正在刷新" : "刷新状态" }} {{ tokenResource.refreshing ? "正在刷新" : "刷新状态" }}
</button> </button>
@@ -284,9 +284,9 @@ const createIntent = ref(null);
const ticketForm = reactive({ const ticketForm = reactive({
phone: "", phone: "",
last_name: "", lastName: "",
honorific: "", honorific: "",
party_size: 1, partySize: 1,
}); });
const projectsResource = reactive({ const projectsResource = reactive({
@@ -319,11 +319,11 @@ const statusRequestGate = createSingleFlight();
const selectedProject = computed(() => projectsResource.projects.find((project) => project.id === selectedProjectId.value) || null); const selectedProject = computed(() => projectsResource.projects.find((project) => project.id === selectedProjectId.value) || null);
const projectNames = computed(() => projectsResource.projects.map((project) => project.name)); const projectNames = computed(() => projectsResource.projects.map((project) => project.name));
const selectedProjectIndex = computed(() => Math.max(0, projectsResource.projects.findIndex((project) => project.id === selectedProjectId.value))); const selectedProjectIndex = computed(() => Math.max(0, projectsResource.projects.findIndex((project) => project.id === selectedProjectId.value)));
const minPartySize = computed(() => Number(selectedProject.value?.min_party_size) || 1); const minPartySize = computed(() => Number(selectedProject.value?.minPartySize) || 1);
const maxPartySize = computed(() => Number(selectedProject.value?.max_party_size) || minPartySize.value); const maxPartySize = computed(() => Number(selectedProject.value?.maxPartySize) || minPartySize.value);
const canSubmitTicket = computed(() => !createBusy.value && Boolean(selectedProjectId.value) && Boolean(projectsResource.projects.length)); const canSubmitTicket = computed(() => !createBusy.value && Boolean(selectedProjectId.value) && Boolean(projectsResource.projects.length));
const createdTicketTarget = computed(() => createdTicket.value ? resolveStatusTarget(createdTicket.value) : null); const createdTicketTarget = computed(() => createdTicket.value ? resolveStatusTarget(createdTicket.value) : null);
const createdTicketProjectName = computed(() => selectedProject.value?.name || createdTicket.value?.ticket?.project_name || "当前项目"); const createdTicketProjectName = computed(() => selectedProject.value?.name || createdTicket.value?.ticket?.projectName || "当前项目");
const currentStatusResource = computed(() => tokenResource); const currentStatusResource = computed(() => tokenResource);
const currentStale = computed(() => Boolean(currentStatusResource.value.data) && isTimestampStale(currentStatusResource.value.lastSuccessAt, 30_000)); const currentStale = computed(() => Boolean(currentStatusResource.value.data) && isTimestampStale(currentStatusResource.value.lastSuccessAt, 30_000));
const headerTone = computed(() => { const headerTone = computed(() => {
@@ -356,7 +356,7 @@ watch(selectedProject, () => normalizePartySize());
onLoad((options = {}) => { onLoad((options = {}) => {
const sceneToken = extractVisitorToken(parseRouteValue(options.scene)); const sceneToken = extractVisitorToken(parseRouteValue(options.scene));
const token = parseRouteValue(options.token || options.public_token) || sceneToken; const token = parseRouteValue(options.token || options.publicToken) || sceneToken;
const phone = parseRouteValue(options.phone); const phone = parseRouteValue(options.phone);
const tab = parseRouteValue(options.tab); const tab = parseRouteValue(options.tab);
@@ -504,9 +504,9 @@ const handleProjectChange = (event) => {
const normalizePartySize = () => { const normalizePartySize = () => {
if (!selectedProject.value) return; if (!selectedProject.value) return;
const current = Number(ticketForm.party_size); const current = Number(ticketForm.partySize);
if (!Number.isInteger(current) || current < minPartySize.value || current > maxPartySize.value) { if (!Number.isInteger(current) || current < minPartySize.value || current > maxPartySize.value) {
ticketForm.party_size = minPartySize.value; ticketForm.partySize = minPartySize.value;
} }
} }
@@ -516,9 +516,9 @@ const handleTicketFormInput = (field, event) => {
const updateTicketForm = (field, value) => { const updateTicketForm = (field, value) => {
let nextValue = value; let nextValue = value;
if (field === "party_size") { if (field === "partySize") {
const digits = digitsOnly(value); const digits = digitsOnly(value);
ticketForm.party_size = digits === "" ? "" : Number(digits); ticketForm.partySize = digits === "" ? "" : Number(digits);
nextValue = digits; nextValue = digits;
} else if (field === "phone") { } else if (field === "phone") {
const phone = digitsOnly(value); const phone = digitsOnly(value);
@@ -560,7 +560,7 @@ const submitTicket = async () => {
return; return;
} }
const partySize = Number(ticketForm.party_size); const partySize = Number(ticketForm.partySize);
if (!Number.isInteger(partySize) || partySize < minPartySize.value || partySize > maxPartySize.value) { if (!Number.isInteger(partySize) || partySize < minPartySize.value || partySize > maxPartySize.value) {
formError.value = `本项目每个号码可绑定 ${minPartySize.value}${maxPartySize.value}`; formError.value = `本项目每个号码可绑定 ${minPartySize.value}${maxPartySize.value}`;
return; return;
@@ -576,7 +576,7 @@ const submitTicket = async () => {
const payload = { const payload = {
phone, phone,
party_size: partySize, party_size: partySize,
last_name: ticketForm.last_name.trim(), last_name: ticketForm.lastName.trim(),
honorific: ticketForm.honorific, honorific: ticketForm.honorific,
allow_duplicate: confirmDuplicatePhone.value, allow_duplicate: confirmDuplicatePhone.value,
}; };
@@ -612,9 +612,9 @@ const submitTicket = async () => {
const resetTicketForm = () => { const resetTicketForm = () => {
ticketForm.phone = ""; ticketForm.phone = "";
ticketForm.last_name = ""; ticketForm.lastName = "";
ticketForm.honorific = ""; ticketForm.honorific = "";
ticketForm.party_size = minPartySize.value; ticketForm.partySize = minPartySize.value;
phoneError.value = ""; phoneError.value = "";
formError.value = ""; formError.value = "";
} }
@@ -700,7 +700,7 @@ const refreshTokenStatus = async ({ initial = false } = {}) => {
try { try {
const data = await getPublicStatus(currentToken.value); const data = await getPublicStatus(currentToken.value);
if (requestId !== statusRequestId || viewMode.value !== VIEW_TOKEN) return; if (requestId !== statusRequestId || viewMode.value !== VIEW_TOKEN) return;
applyStatusSuccess(tokenResource, data); applyStatusSuccess(tokenResource, Array.isArray(data?.tickets) ? data.tickets[0] : data);
} catch (error) { } catch (error) {
if (requestId !== statusRequestId || viewMode.value !== VIEW_TOKEN) return; if (requestId !== statusRequestId || viewMode.value !== VIEW_TOKEN) return;
applyStatusError(tokenResource, error); applyStatusError(tokenResource, error);

View File

@@ -1,4 +1,3 @@
import { normalizePublicStatus } from "../../utils/queueUtils.mjs";
import request from "../base/request"; import request from "../base/request";
export class QueueApiError extends Error { export class QueueApiError extends Error {
@@ -74,22 +73,13 @@ export const createPublicTicket = (projectId, payload, idempotencyKey = newQueue
project_id: projectId, project_id: projectId,
idempotency_key: idempotencyKey, idempotency_key: idempotencyKey,
...payload, ...payload,
})).then((response) => ({
...response,
ticket: normalizePublicStatus(response.ticket),
})); }));
} }
export const getPublicStatus = () => { export const getPublicStatus = () => {
return normalizeQueueRequest(request.get("/hotelBiz/visitorQueue/status", {})) return normalizeQueueRequest(request.get("/hotelBiz/visitorQueue/status", {}));
.then((response) => normalizePublicStatus(response));
} }
export const searchPublicStatusByPhone = () => { export const searchPublicStatusByPhone = () => {
return normalizeQueueRequest(request.get("/hotelBiz/visitorQueue/status", {})).then((response) => { return getPublicStatus();
if (Array.isArray(response.tickets)) {
return { tickets: response.tickets.map(normalizePublicStatus) };
}
return { tickets: [normalizePublicStatus(response)] };
});
} }

View File

@@ -118,7 +118,6 @@ let requestId = 0;
const hasLookupTarget = computed(() => Boolean(phone.value || token.value)); const hasLookupTarget = computed(() => Boolean(phone.value || token.value));
const tickets = computed(() => { const tickets = computed(() => {
if (token.value) return resource.data ? [resource.data] : [];
return resource.data?.tickets || []; return resource.data?.tickets || [];
}); });
const selectedTicket = computed(() => { const selectedTicket = computed(() => {
@@ -153,7 +152,7 @@ watch(tickets, () => syncSelectedTicket(), { deep: true });
onLoad((options = {}) => { onLoad((options = {}) => {
phone.value = parseRouteValue(options.phone); phone.value = parseRouteValue(options.phone);
token.value = parseRouteValue(options.token || options.public_token); token.value = parseRouteValue(options.token || options.publicToken);
}); });
onShow(() => { onShow(() => {

View File

@@ -26,11 +26,13 @@ export const formatEstimatedWait = (value) => {
return Number.isFinite(numeric) ? `${Math.max(0, Math.round(numeric))} 分钟` : value; return Number.isFinite(numeric) ? `${Math.max(0, Math.round(numeric))} 分钟` : value;
} }
if (typeof value === "object") { if (typeof value === "object") {
const estimate = Number(value.estimate_minutes); if (value.available === false && value.reason) return value.reason;
const estimate = Number(value.estimateMinutes);
if (Number.isFinite(estimate)) return `${Math.max(0, Math.round(estimate))} 分钟`; if (Number.isFinite(estimate)) return `${Math.max(0, Math.round(estimate))} 分钟`;
const min = Number(value.min ?? value.min_minutes ?? value.lower ?? value.lower_minutes); const min = Number(value.min ?? value.minMinutes ?? value.lower ?? value.lowerMinutes);
const max = Number(value.max ?? value.max_minutes ?? value.upper ?? value.upper_minutes); const max = Number(value.max ?? value.maxMinutes ?? value.upper ?? value.upperMinutes);
if (Number.isFinite(min) && Number.isFinite(max)) { if (Number.isFinite(min) && Number.isFinite(max)) {
return `预计 ${Math.max(0, Math.round(min))}-${Math.max(0, Math.round(max))} 分钟`; return `预计 ${Math.max(0, Math.round(min))}-${Math.max(0, Math.round(max))} 分钟`;
} }
@@ -78,23 +80,14 @@ export const visitorStatusMeta = (status, peopleAhead) => {
}; };
} }
export const normalizePublicStatus = (ticket = {}) => {
const partySize = Number(ticket.party_size);
return {
...ticket,
party_size: Number.isFinite(partySize) && partySize > 0 ? partySize : 1,
ticket_number: ticket.ticket_number || ticket.display_number || "暂无",
};
}
export const buildTicketKey = (ticket = {}) => { export const buildTicketKey = (ticket = {}) => {
return `${ticket.project?.id ?? ticket.project_name}:${ticket.ticket_number}`; return `${ticket.project?.id ?? ticket.projectName}:${ticket.ticketNumber}`;
} }
export const resolveStatusTarget = (response = {}) => { export const resolveStatusTarget = (response = {}) => {
if (response.public_token) return { token: response.public_token }; if (response.publicToken) return { token: response.publicToken };
const url = response.public_url || response.status_path || ""; const url = response.publicUrl || response.statusPath || "";
const match = String(url).match(/\/visitor\/([^/?#]+)/); const match = String(url).match(/\/visitor\/([^/?#]+)/);
if (match?.[1]) return { token: decodeURIComponent(match[1]) }; if (match?.[1]) return { token: decodeURIComponent(match[1]) };
@@ -105,7 +98,7 @@ export const extractVisitorToken = (value) => {
const text = String(value ?? "").trim(); const text = String(value ?? "").trim();
if (!text) return ""; if (!text) return "";
const tokenMatch = text.match(/(?:^|[?&#])(?:token|public_token)=([^&#]+)/); const tokenMatch = text.match(/(?:^|[?&#])(?:token|publicToken)=([^&#]+)/);
if (tokenMatch?.[1]) return decodeMaybe(tokenMatch[1]); if (tokenMatch?.[1]) return decodeMaybe(tokenMatch[1]);
const visitorMatch = text.match(/\/visitor\/([^/?#]+)/); const visitorMatch = text.match(/\/visitor\/([^/?#]+)/);