fix: 错误的文档导致文案错误

master
xjs 2026-07-29 16:14:38 +08:00
parent 94f7adc407
commit b1072b825b
3 changed files with 105 additions and 23 deletions

View File

@ -18,7 +18,7 @@
</button> </button>
</header> </header>
<div class="flex shrink-0 items-center bg-white"> <div class="flex shrink-0 items-center bg-white" @compositionstart="onCompositionStart" @compositionend="onCompositionEnd">
<label class="sr-only" :for="searchInputId">{{ searchPlaceholder }}</label> <label class="sr-only" :for="searchInputId">{{ searchPlaceholder }}</label>
<VanSearch <VanSearch
ref="searchRef" ref="searchRef"
@ -30,6 +30,7 @@
:id="searchInputId" :id="searchInputId"
:placeholder="searchPlaceholder" :placeholder="searchPlaceholder"
@clear="handleClear" @clear="handleClear"
@update:model-value="handleSearchInput"
@search="handleSearch" /> @search="handleSearch" />
<button <button
class="shrink-0 border-0 bg-transparent py-[1.5rem] pl-[2.5rem] pr-[3.75rem] text-[4rem] text-[#1580FF] leading-none" class="shrink-0 border-0 bg-transparent py-[1.5rem] pl-[2.5rem] pr-[3.75rem] text-[4rem] text-[#1580FF] leading-none"
@ -96,7 +97,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onBeforeMount, ref, useId, watch, type HTMLAttributes } from "vue"; import { computed, nextTick, onBeforeMount, onBeforeUnmount, ref, useId, watch, type HTMLAttributes } from "vue";
import { import {
Empty as VanEmpty, Empty as VanEmpty,
Highlight as VanHighlight, Highlight as VanHighlight,
@ -169,6 +170,7 @@
const searchKeyword = ref(""); const searchKeyword = ref("");
const submittedKeyword = ref(""); const submittedKeyword = ref("");
const isComposing = ref(false);
const activeRegionIndex = ref(0); const activeRegionIndex = ref(0);
const visibleSchools = ref<SchoolOption[]>([]); const visibleSchools = ref<SchoolOption[]>([]);
const currentPage = ref(0); const currentPage = ref(0);
@ -186,6 +188,8 @@
const regions = ref<SchoolRegion[]>([]); const regions = ref<SchoolRegion[]>([]);
let regionsLoaded: Promise<void> = Promise.resolve(); let regionsLoaded: Promise<void> = Promise.resolve();
let regionsLoading = false; let regionsLoading = false;
let searchTimer: ReturnType<typeof setTimeout> | null = null;
const SEARCH_DEBOUNCE_MS = 300;
// Promise // Promise
const loadRegions = (): Promise<void> => { const loadRegions = (): Promise<void> => {
@ -271,14 +275,46 @@
loading.value = false; loading.value = false;
}; };
const clearSearchTimer = (): void => {
if (searchTimer) {
clearTimeout(searchTimer);
searchTimer = null;
}
};
const handleSearch = (value: string | number = searchKeyword.value): void => { const handleSearch = (value: string | number = searchKeyword.value): void => {
if (isComposing.value) return;
clearSearchTimer();
const keyword = String(value).trim(); const keyword = String(value).trim();
searchKeyword.value = keyword; searchKeyword.value = keyword;
submittedKeyword.value = keyword; submittedKeyword.value = keyword;
emit("search", keyword); emit("search", keyword);
}; };
const handleSearchInput = (value: string | number): void => {
if (isComposing.value) return;
clearSearchTimer();
searchTimer = setTimeout(() => {
searchTimer = null;
handleSearch(value);
}, SEARCH_DEBOUNCE_MS);
};
const onCompositionStart = (): void => {
isComposing.value = true;
clearSearchTimer();
};
const onCompositionEnd = (): void => {
isComposing.value = false;
// Vue compositionend
void nextTick(() => handleSearch(searchKeyword.value));
};
const showAllSchools = (): void => { const showAllSchools = (): void => {
clearSearchTimer();
searchKeyword.value = ""; searchKeyword.value = "";
submittedKeyword.value = ""; submittedKeyword.value = "";
emit("search", ""); emit("search", "");
@ -313,7 +349,9 @@
}; };
const handleOpened = (): void => { const handleOpened = (): void => {
nextTick(() => searchRef.value?.focus()); if (searchKeyword.value.trim()) {
void nextTick(() => searchRef.value?.focus());
}
}; };
const handleCancel = (): void => { const handleCancel = (): void => {
@ -327,6 +365,7 @@
// onBeforeMount watch(show) // onBeforeMount watch(show)
// keep-alive VanList // keep-alive VanList
onBeforeMount(loadRegions); onBeforeMount(loadRegions);
onBeforeUnmount(clearSearchTimer);
// onBeforeMount // onBeforeMount
// //
@ -406,5 +445,5 @@
:deep(.van-empty) { :deep(.van-empty) {
--van-empty-padding: 0; --van-empty-padding: 0;
} }
</style> </style>

View File

@ -3,10 +3,9 @@ import { showToast } from "vant";
import "vant/es/toast/style"; import "vant/es/toast/style";
import { import {
cancelReceptionTask, cancelReceptionTask,
fetchReceptionStatistics,
fetchReceptionSummary, fetchReceptionSummary,
fetchReceptionTaskPage, fetchReceptionTaskPage,
type ReceptionStatisticsDTO, type ReceptionPaymentStatusCode,
type ReceptionTaskDTO, type ReceptionTaskDTO,
type ReceptionTaskStatus, type ReceptionTaskStatus,
} from "@/api/reception"; } from "@/api/reception";
@ -14,12 +13,27 @@ import { useUserStore } from "@/store/user";
const ACTIVE_TASK_PAGE_SIZE = 100; const ACTIVE_TASK_PAGE_SIZE = 100;
const ACTIVE_STATUSES: ReadonlyArray<ReceptionTaskStatus> = [2, 3]; const ACTIVE_STATUSES: ReadonlyArray<ReceptionTaskStatus> = [2, 3];
const OVERVIEW_PAGE_SIZE = 1;
const PAYMENT_STATUS_CODES: readonly ReceptionPaymentStatusCode[] = ["paid", "unpaid", "refunded", "toVerify"];
interface ActiveTaskResult { interface ActiveTaskResult {
tasks: ReceptionTaskDTO[]; tasks: ReceptionTaskDTO[];
message: string; message: string;
} }
interface ReceptionOverview {
total: number;
paid: number;
unpaid: number;
refunded: number;
toVerify: number;
}
interface ReceptionOverviewResult {
data: ReceptionOverview | null;
message: string;
}
const fetchActiveTasks = async (): Promise<ActiveTaskResult> => { const fetchActiveTasks = async (): Promise<ActiveTaskResult> => {
const result = await fetchReceptionTaskPage({ const result = await fetchReceptionTaskPage({
page: 1, page: 1,
@ -35,11 +49,44 @@ const fetchActiveTasks = async (): Promise<ActiveTaskResult> => {
}; };
}; };
/** 使用接待数据页同一分页接口的 total 构建累计缴费概览。 */
const fetchReceptionOverview = async (): Promise<ReceptionOverviewResult> => {
const [allResult, ...paymentResults] = await Promise.all([
fetchReceptionTaskPage({ page: 1, pageSize: OVERVIEW_PAGE_SIZE, scope: "completed" }),
...PAYMENT_STATUS_CODES.map((paymentStatusCode) =>
fetchReceptionTaskPage({
page: 1,
pageSize: OVERVIEW_PAGE_SIZE,
scope: "completed",
paymentStatusCode,
}),
),
]);
const results = [allResult, ...paymentResults];
const failedResult = results.find((result) => !result.data);
if (failedResult) {
return { data: null, message: failedResult.message };
}
const [paidResult, unpaidResult, refundedResult, toVerifyResult] = paymentResults;
return {
data: {
total: allResult.data?.total ?? 0,
paid: paidResult?.data?.total ?? 0,
unpaid: unpaidResult?.data?.total ?? 0,
refunded: refundedResult?.data?.total ?? 0,
toVerify: toVerifyResult?.data?.total ?? 0,
},
message: "",
};
};
/** /reception/mine 的个人摘要、累计统计和进行中任务。 */ /** /reception/mine 的个人摘要、累计统计和进行中任务。 */
export function useReceptionMine() { export function useReceptionMine() {
const userStore = useUserStore(); const userStore = useUserStore();
const summary = computed(() => userStore.getReceptionSummary); const summary = computed(() => userStore.getReceptionSummary);
const statistics = ref<ReceptionStatisticsDTO | null>(null); const overview = ref<ReceptionOverview | null>(null);
const ongoingTasks = ref<ReceptionTaskDTO[]>([]); const ongoingTasks = ref<ReceptionTaskDTO[]>([]);
const loading = ref(false); const loading = ref(false);
const error = ref(false); const error = ref(false);
@ -52,17 +99,13 @@ export function useReceptionMine() {
error.value = false; error.value = false;
try { try {
const [summaryResult, statisticsResult, activeTaskResult] = await Promise.all([ const [summaryResult, overviewResult, activeTaskResult] = await Promise.all([fetchReceptionSummary(), fetchReceptionOverview(), fetchActiveTasks()]);
fetchReceptionSummary(),
fetchReceptionStatistics({ scope: "mine" }),
fetchActiveTasks(),
]);
if (summaryResult.data) userStore.setReceptionSummary(summaryResult.data); if (summaryResult.data) userStore.setReceptionSummary(summaryResult.data);
if (statisticsResult.data) statistics.value = statisticsResult.data; if (overviewResult.data) overview.value = overviewResult.data;
ongoingTasks.value = activeTaskResult.tasks; ongoingTasks.value = activeTaskResult.tasks;
const messages = [summaryResult.message, statisticsResult.message, activeTaskResult.message].filter(Boolean); const messages = [summaryResult.message, overviewResult.message, activeTaskResult.message].filter(Boolean);
error.value = messages.length > 0; error.value = messages.length > 0;
if (messages[0]) showToast(messages[0]); if (messages[0]) showToast(messages[0]);
} finally { } finally {
@ -95,7 +138,7 @@ export function useReceptionMine() {
return { return {
summary, summary,
statistics, overview,
ongoingTasks, ongoingTasks,
loading, loading,
error, error,

View File

@ -34,7 +34,7 @@
:key="item.label" :key="item.label"
class="flex flex-1 flex-col items-center" class="flex flex-1 flex-col items-center"
:class="{ 'border-l border-solid border-[#EEF0F2]': index > 0 }"> :class="{ 'border-l border-solid border-[#EEF0F2]': index > 0 }">
<span class="din-bold text-[5rem] font-700 leading-none text-[#000]">{{ item.value }}</span> <span class="din-bold text-[5rem] font-700 leading-none" :style="{ color: item.color }">{{ item.value }}</span>
<span class="mt-4 text-[3.25rem] leading-none text-[#666]">{{ item.label }}</span> <span class="mt-4 text-[3.25rem] leading-none text-[#666]">{{ item.label }}</span>
</div> </div>
</div> </div>
@ -138,18 +138,18 @@
import { useReceptionMine } from "@/composables/useReceptionMine"; import { useReceptionMine } from "@/composables/useReceptionMine";
const router = useRouter(); const router = useRouter();
const { summary, statistics, ongoingTasks, loading, error, cancellingTaskId, loadMine, cancelTask } = useReceptionMine(); const { summary, overview, ongoingTasks, loading, error, cancellingTaskId, loadMine, cancelTask } = useReceptionMine();
const showCancelDialog = ref(false); const showCancelDialog = ref(false);
const pendingCancelTask = ref<ReceptionTaskDTO | null>(null); const pendingCancelTask = ref<ReceptionTaskDTO | null>(null);
type CancelDialogAction = "confirm" | "cancel"; type CancelDialogAction = "confirm" | "cancel";
const receptionistGreeting = computed(() => (summary.value?.receptionistName ? `${summary.value.receptionistName}老师,辛苦了` : "辛苦了")); const receptionistGreeting = computed(() => (summary.value?.receptionistName ? `${summary.value.receptionistName}老师,辛苦了` : "辛苦了"));
const stats = computed(() => [ const stats = computed(() => [
{ label: "待接待", value: summary.value?.waitingCount ?? statistics.value?.waitingCount ?? 0 }, { label: "总接待", value: overview.value?.total ?? 0, color: "#000" },
{ label: "接待中", value: statistics.value?.inProgressCount ?? summary.value?.inProgressCount ?? 0 }, { label: "已缴费", value: overview.value?.paid ?? 0, color: "#000" },
{ label: "已完成", value: statistics.value?.completedCount ?? summary.value?.totalCompletedCount ?? 0 }, { label: "未缴费", value: overview.value?.unpaid ?? 0, color: "#FF2928" },
{ label: "已报名", value: statistics.value?.registeredCount ?? 0 }, { label: "已退费", value: overview.value?.refunded ?? 0, color: "#000" },
{ label: "已缴费", value: statistics.value?.paidCount ?? 0 }, { label: "待核销", value: overview.value?.toVerify ?? 0, color: "#000" },
]); ]);
const formatCheckinTime = (value: string): string => { const formatCheckinTime = (value: string): string => {