fix: 错误的文档导致文案错误
parent
94f7adc407
commit
b1072b825b
|
|
@ -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 请求失败)时,打开弹窗再补一次;
|
||||||
// 补齐后若列表确已空且结束(说明首次自动加载因无区域落空),再触发一次加载。
|
// 补齐后若列表确已空且结束(说明首次自动加载因无区域落空),再触发一次加载。
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue