Files
gao-customer-h5/src/pages/customer/QuickSignInPage.vue
T
2026-08-07 11:32:48 +08:00

365 lines
8.4 KiB
Vue

<template>
<div v-can="permissionCodes.eventRecord.create" class="quick-sign-in-page">
<button class="registration-event-select" type="button" @click="eventSelectorVisible = true">
<span class="required-label">登记事件</span>
<strong v-text="selectedEventName" />
<t-icon name="chevron-right" />
</button>
<code-scanner
visible
:session-key="scannerSessionKey"
:status-text="scanStatus"
active-text="请将客户二维码移入取景框"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
<customer-profile-card
v-if="matchedCustomer"
:customer="matchedCustomer"
/>
<t-cell-group
v-if="recentRecordList.length"
title="最近登记记录"
theme="card"
>
<t-cell
v-for="item in recentRecordList"
:key="item.id"
:title="item.event?.name || '未知事件'"
:description="Time.toPassedDateTime(item.registeredAt)"
:note="item.remark || '无备注'"
arrow
hover
@click="toRecordDetail(item.id || '')"
>
<template #leftIcon>
<t-image
v-if="getEventRecordPhotoURL(item)"
:src="getEventRecordPhotoURL(item)"
:alt="item.event?.name || '未知事件'"
fit="cover"
class="record-photo"
/>
</template>
</t-cell>
</t-cell-group>
<t-button
class="submit"
theme="primary"
:disabled="!canSubmitRegister"
:loading="submitting"
@click="doSubmit"
>登记</t-button>
</div>
<t-popup v-model="eventSelectorVisible" placement="bottom">
<div class="registration-event-panel">
<div class="title">登记事件</div>
<t-cell-group v-if="eventList.length" theme="card">
<t-cell
v-for="item in eventList"
:key="item.id"
:title="item.name"
:note="item.id === selectedEventId ? '当前使用' : ''"
@click="selectEvent(item.id)"
/>
</t-cell-group>
<div v-else class="empty-block">暂无可用登记事件</div>
</div>
</t-popup>
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import CustomerAPI from "@/api/CustomerAPI";
import EventAPI from "@/api/EventAPI";
import EventRecordAPI from "@/api/EventRecordAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem";
import type { EventRecord } from "@/types/EventRecord";
import { permissionCodes } from "@/utils/permission";
import { getEventRecordPhotoURL } from "@/utils/eventRecord";
import { Storage, Time } from "timi-web";
defineOptions({
name: "QuickSignInPage"
});
const router = useRouter();
const settingStore = useSettingStore();
// ---------- 登记事件 ----------
const EventItemStorageKey = "quick-scan-event-id";
const eventSelectorVisible = ref(false);
const selectedEventId = ref("");
const eventList = ref<EventItem[]>([]);
const selectedEventName = computed(() => eventList.value.find(item => item.id === selectedEventId.value)?.name || "请选择登记事件");
async function loadActions(): Promise<void> {
eventList.value = await EventAPI.listValid();
const storedEventId = Storage.getString(EventItemStorageKey);
const storedAction = eventList.value.find(item => item.id === storedEventId);
selectedEventId.value = storedAction?.id || eventList.value[0]?.id || "";
if (selectedEventId.value) {
Storage.setString(EventItemStorageKey, selectedEventId);
}
}
function selectEvent(eventId?: string): void {
if (!eventId) {
return;
}
selectedEventId.value = eventId;
Storage.setString(EventItemStorageKey, selectedEventId);
eventSelectorVisible.value = false;
Toast.success("登记事件已切换");
}
onMounted(loadActions);
// ---------- 扫码 ----------
const scanCode = ref("");
const scanStatus = ref("正在启动扫码预览");
const scannerSessionKey = ref(0);
const matchedCustomer = ref<Customer>();
let latestDecodedText = "";
async function handleDecodedText(rawText: string): Promise<void> {
const normalizedText = rawText.trim();
if (!normalizedText || normalizedText === latestDecodedText) {
return;
}
latestDecodedText = normalizedText;
scanCode.value = normalizedText;
await settingStore.playScanSuccessSound();
await loadCustomer(normalizedText);
if (matchedCustomer.value) {
scanStatus.value = `已识别 ${normalizedText},可直接登记`;
return;
}
scanStatus.value = `已识别 ${normalizedText},但未匹配客户档案`;
}
function handleScanStatusChange(value: string): void {
scanStatus.value = value;
}
// ---------- 读取客户 ----------
const recentRecordList = ref<EventRecord[]>([]);
async function loadCustomer(code: string): Promise<void> {
const customer = await CustomerAPI.findByCode(code);
if (!customer) {
matchedCustomer.value = undefined;
recentRecordList.value = [];
return;
}
matchedCustomer.value = customer;
recentRecordList.value = (await EventRecordAPI.list({
index: 0,
size: 5,
customerId: matchedCustomer.value.id!
})).list;
}
async function toRecordDetail(id: string): Promise<void> {
if (!id) {
return;
}
await router.push(`/management/registration-event-records/${id}`);
}
// ---------- 表单 ----------
const submitting = ref(false);
const canSubmitRegister = computed(() => !!matchedCustomer.value && !!selectedEventId.value);
async function doSubmit(): Promise<void> {
const customer = matchedCustomer.value;
if (!customer) {
return;
}
submitting.value = true;
try {
await EventRecordAPI.create({
customerId: customer.id,
eventId: selectedEventId.value,
remark: "客户快速扫码登记"
});
scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`;
scanCode.value = "";
matchedCustomer.value = undefined;
recentRecordList.value = [];
latestDecodedText = "";
scannerSessionKey.value += 1;
await settingStore.playRegisterSuccessSound();
} finally {
submitting.value = false;
}
}
</script>
<style scoped lang="less">
.quick-sign-in-page {
gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem;
display: flex;
flex-direction: column;
.panel,
.bottom-bar {
left: 1rem;
right: 1rem;
}
.panel {
padding: 1rem;
background: rgba(255, 255, 255, .94);
border-radius: 1.4rem;
box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
}
.registration-event-select {
gap: .5rem;
border: 0;
padding: .875rem 1rem;
display: grid;
min-width: 0;
text-align: left;
background: rgba(255, 255, 255, .94);
box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
align-items: center;
border-radius: .875rem;
grid-template-columns: auto minmax(0, 1fr) auto;
span {
color: #52607a;
font-size: .8125rem;
}
strong {
color: #0f172a;
overflow: hidden;
font-size: 1rem;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.code-box {
padding: .875rem;
background: #f7faff;
border: 1px solid rgba(148, 163, 184, .2);
border-radius: 1rem;
}
.required-label::before {
color: #e34d59;
content: "*";
margin-right: .25rem;
}
.code-box {
display: grid;
margin-top: .875rem;
gap: .375rem;
span {
color: #52607a;
font-size: .8125rem;
}
strong {
font-size: 1.125rem;
line-height: 1.5;
word-break: break-all;
}
}
.submit {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
.record-photo {
width: 3.5rem;
height: 3.5rem;
border-radius: .875rem;
}
}
.registration-event-panel {
gap: .875rem;
padding: 1rem 0 calc(1rem + env(safe-area-inset-bottom, 0)) 0;
display: flex;
background: #f5f5f8;
border-radius: 1rem 1rem 0 0;
flex-direction: column;
.title {
margin: 0;
color: #0f172a;
font-size: 1rem;
text-align: center;
font-weight: 700;
}
.empty-block {
color: #52607a;
padding: 1rem;
text-align: center;
}
}
:global(html.theme-dark) {
.quick-sign-in-page {
.panel,
.bottom-bar {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}
.registration-event-select {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
.code-box {
background: rgba(30, 41, 59, .9);
border-color: rgba(148, 163, 184, .16);
}
.code-box span {
color: #94a3b8;
}
}
.registration-event-panel {
background: #10161f;
.title {
color: #e2e8f0;
}
.empty-block {
color: #94a3b8;
}
}
}
</style>