Compare commits
2 Commits
2643f637a9
...
ca70138ad7
| Author | SHA1 | Date |
|---|---|---|
|
|
ca70138ad7 | |
|
|
141f6708b8 |
5882
docs/api.md
5882
docs/api.md
File diff suppressed because it is too large
Load Diff
|
|
@ -32,6 +32,8 @@ export const getReceptionTaskPageUrl = () => `${baseUrl}/api/checkin/reception/p
|
|||
export const getReceptionTaskClaimUrl = () => `${baseUrl}/api/checkin/reception/claim`;
|
||||
/** 取消本人已领取或已分配的接待任务 */
|
||||
export const getReceptionTaskCancelUrl = () => `${baseUrl}/api/checkin/reception/cancel`;
|
||||
/** 完成接待任务 */
|
||||
export const getReceptionTaskCompleteUrl = () => `${baseUrl}/api/checkin/reception/complete`;
|
||||
/** 查询接待任务详情 */
|
||||
export const getReceptionTaskDetailUrl = () => `${baseUrl}/api/checkin/reception/detail`;
|
||||
/** 保存已完成未缴费任务的接待记录 */
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
getReceptionSummaryUrl,
|
||||
getReceptionTaskCancelUrl,
|
||||
getReceptionTaskClaimUrl,
|
||||
getReceptionTaskCompleteUrl,
|
||||
getReceptionTaskDetailUrl,
|
||||
getReceptionTaskPageUrl,
|
||||
} from "./fetchUrl";
|
||||
|
|
@ -61,6 +62,9 @@ export interface ReceptionTaskDTO {
|
|||
receptionResult: string | null;
|
||||
receptionRecord: string | null;
|
||||
followSuggestion: string | null;
|
||||
unpaidReason: string | null;
|
||||
unpaidStatus: number | null;
|
||||
unpaidStatusName: string | null;
|
||||
nextFollowTime: string | null;
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +116,8 @@ export interface ReceptionDetailDTO {
|
|||
receptionResult: string | null;
|
||||
receptionRecord: string | null;
|
||||
unpaidReason: string | null;
|
||||
unpaidStatus: number | null;
|
||||
unpaidStatusName: string | null;
|
||||
followSuggestion: string | null;
|
||||
nextFollowTime: string | null;
|
||||
}
|
||||
|
|
@ -124,6 +130,7 @@ export interface ReceptionDetailResult {
|
|||
export interface SaveReceptionRecordParams {
|
||||
taskId: number;
|
||||
reason: string;
|
||||
unpaidStatus: number;
|
||||
}
|
||||
|
||||
export interface ReceptionStatisticsParams {
|
||||
|
|
@ -193,6 +200,16 @@ export const cancelReceptionTask = async (taskId: number): Promise<ReceptionActi
|
|||
};
|
||||
};
|
||||
|
||||
/** 完成接待;请求字段按文档保持为 taskId。 */
|
||||
export const completeReceptionTask = async (taskId: number, receptionResult: string = "已完成接待"): Promise<ReceptionActionResult> => {
|
||||
const response = await postRequest(getReceptionTaskCompleteUrl(), { taskId, receptionResult }, { silent: true });
|
||||
|
||||
return {
|
||||
success: response.code === 200,
|
||||
message: response.code === 200 ? "" : getFailureMessage(response, "完成接待失败"),
|
||||
};
|
||||
};
|
||||
|
||||
/** 根据分页项 taskId 查询接待详情;接口 Query 参数名固定为 id。 */
|
||||
export const fetchReceptionDetail = async (taskId: number): Promise<ReceptionDetailResult> => {
|
||||
const response = await getRequest(getReceptionTaskDetailUrl(), { id: taskId }, { silent: true });
|
||||
|
|
|
|||
|
|
@ -105,6 +105,16 @@ export function useReceptionData(pageSize = DEFAULT_PAGE_SIZE) {
|
|||
queryVersion += 1;
|
||||
});
|
||||
|
||||
const resetFilters = (): void => {
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = null;
|
||||
}
|
||||
keyword.value = "";
|
||||
activeTab.value = "all";
|
||||
reload();
|
||||
};
|
||||
|
||||
return {
|
||||
keyword,
|
||||
activeTab,
|
||||
|
|
@ -115,5 +125,6 @@ export function useReceptionData(pageSize = DEFAULT_PAGE_SIZE) {
|
|||
showEmpty,
|
||||
loadNextPage,
|
||||
reload,
|
||||
resetFilters,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { showToast } from "vant";
|
|||
import "vant/es/toast/style";
|
||||
import {
|
||||
cancelReceptionTask,
|
||||
completeReceptionTask,
|
||||
fetchReceptionSummary,
|
||||
fetchReceptionTaskPage,
|
||||
type ReceptionPaymentStatusCode,
|
||||
|
|
@ -91,6 +92,7 @@ export function useReceptionMine() {
|
|||
const loading = ref(false);
|
||||
const error = ref(false);
|
||||
const cancellingTaskId = ref<number | null>(null);
|
||||
const completingTaskId = ref<number | null>(null);
|
||||
|
||||
const loadMine = async (): Promise<void> => {
|
||||
if (loading.value) return;
|
||||
|
|
@ -136,6 +138,26 @@ export function useReceptionMine() {
|
|||
}
|
||||
};
|
||||
|
||||
const completeTask = async (taskId: number): Promise<boolean> => {
|
||||
if (completingTaskId.value !== null) return false;
|
||||
|
||||
completingTaskId.value = taskId;
|
||||
try {
|
||||
const result = await completeReceptionTask(taskId, "已完成接待");
|
||||
if (!result.success) {
|
||||
if (result.message) showToast(result.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
ongoingTasks.value = ongoingTasks.value.filter((task) => task.taskId !== taskId);
|
||||
await loadMine();
|
||||
showToast("接待已完成");
|
||||
return true;
|
||||
} finally {
|
||||
completingTaskId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
summary,
|
||||
overview,
|
||||
|
|
@ -143,7 +165,9 @@ export function useReceptionMine() {
|
|||
loading,
|
||||
error,
|
||||
cancellingTaskId,
|
||||
completingTaskId,
|
||||
loadMine,
|
||||
cancelTask,
|
||||
completeTask,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export function useReceptionRecord() {
|
|||
const router = useRouter();
|
||||
const detail = ref<ReceptionDetailDTO | null>(null);
|
||||
const reason = ref("");
|
||||
const unpaidStatus = ref<number | null>(null);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const error = ref(false);
|
||||
|
|
@ -53,6 +54,7 @@ export function useReceptionRecord() {
|
|||
|
||||
detail.value = result.data;
|
||||
reason.value = result.data.unpaidReason ?? "";
|
||||
unpaidStatus.value = result.data.unpaidStatus ?? null;
|
||||
};
|
||||
|
||||
const save = async (): Promise<void> => {
|
||||
|
|
@ -77,9 +79,12 @@ export function useReceptionRecord() {
|
|||
}
|
||||
if (saving.value) return;
|
||||
|
||||
// 页面仅支持填写未缴费原因,跟进状态按接口要求提供默认值
|
||||
const statusToSend = unpaidStatus.value ?? 1;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const result = await saveReceptionRecord({ taskId: id, reason: normalizedReason });
|
||||
const result = await saveReceptionRecord({ taskId: id, reason: normalizedReason, unpaidStatus: statusToSend });
|
||||
if (!result.success) {
|
||||
if (result.message) showToast(result.message);
|
||||
return;
|
||||
|
|
@ -87,6 +92,8 @@ export function useReceptionRecord() {
|
|||
|
||||
reason.value = normalizedReason;
|
||||
detail.value.unpaidReason = normalizedReason;
|
||||
detail.value.unpaidStatus = statusToSend;
|
||||
unpaidStatus.value = statusToSend;
|
||||
showToast("保存成功");
|
||||
router.back();
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,52 @@
|
|||
// 通用格式化 / 校验纯函数(无状态,供 composable 与页面复用)。
|
||||
|
||||
/** Date → `YYYY-MM-DD` */
|
||||
export const formatDate = (date: Date | string): string => {
|
||||
/** Date 格式化。支持自定义格式,默认 `YYYY-MM-DD`。
|
||||
*
|
||||
* 支持的 token:
|
||||
* - YYYY / MM / DD / HH / mm / ss(补零)
|
||||
* - M / D / H / m / s(不补零)
|
||||
*
|
||||
* @example
|
||||
* formatDate(new Date()) // '2026-08-12'
|
||||
* formatDate('2025-12-05', 'YYYY/MM/DD') // '2025/12/05'
|
||||
* formatDate(date, 'YYYY-MM-DD HH:mm:ss') // '2026-08-12 09:05:30'
|
||||
*/
|
||||
export const formatDate = (
|
||||
date: Date | string,
|
||||
format: string = "YYYY-MM-DD"
|
||||
): string => {
|
||||
const transformedDate = typeof date === "string" ? new Date(date) : date;
|
||||
if (Number.isNaN(transformedDate.getTime())) return "";
|
||||
|
||||
const year = transformedDate.getFullYear();
|
||||
const month = String(transformedDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(transformedDate.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
const month = transformedDate.getMonth() + 1;
|
||||
const day = transformedDate.getDate();
|
||||
const hours = transformedDate.getHours();
|
||||
const minutes = transformedDate.getMinutes();
|
||||
const seconds = transformedDate.getSeconds();
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
// 按长度从长到短替换,避免短 token 干扰
|
||||
const replacements: Array<[string, string]> = [
|
||||
["YYYY", String(year)],
|
||||
["MM", pad(month)],
|
||||
["DD", pad(day)],
|
||||
["HH", pad(hours)],
|
||||
["mm", pad(minutes)],
|
||||
["ss", pad(seconds)],
|
||||
["M", String(month)],
|
||||
["D", String(day)],
|
||||
["H", String(hours)],
|
||||
["m", String(minutes)],
|
||||
["s", String(seconds)],
|
||||
];
|
||||
|
||||
let result = format;
|
||||
for (const [token, value] of replacements) {
|
||||
result = result.split(token).join(value);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** 手机号输入格式化:仅保留数字并截断到 11 位。 */
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@
|
|||
<VanList v-model:loading="loading" :finished="finished" :error="error"
|
||||
:finished-text="records.length ? '没有更多了' : ''" error-text="加载失败,点击重试" @load="loadNextPage"
|
||||
@update:error="(value: boolean) => (error = value)">
|
||||
<VanEmpty v-if="showEmpty" image="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_zanwu.png"
|
||||
:image-size="[186, 144]" description="暂无接待数据" />
|
||||
<VanEmpty v-if="showEmpty" image="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_kong2.png"
|
||||
:image-size="[136, 136]" description="暂无接待数据" >
|
||||
<VanButton color="#1580FF" block class="" plain @click="resetFilters">查看全部接待</VanButton>
|
||||
</VanEmpty>
|
||||
|
||||
<div v-else class="flex flex-col gap-12 px-15 py-15">
|
||||
<article v-for="record in records" :key="record.taskId" class="rounded-8 bg-white p-15">
|
||||
|
|
@ -60,7 +62,8 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { Empty as VanEmpty, List as VanList, Search as VanSearch, Tab as VanTab, Tabs as VanTabs } from "vant";
|
||||
import { Empty as VanEmpty, List as VanList, Search as VanSearch, Tab as VanTab, Tabs as VanTabs, Button as VanButton } from "vant";
|
||||
import "vant/es/button/style";
|
||||
import "vant/es/empty/style";
|
||||
import "vant/es/list/style";
|
||||
import "vant/es/search/style";
|
||||
|
|
@ -85,7 +88,7 @@ const tabs: ReadonlyArray<{ label: string; value: ReceptionDataTab }> = [
|
|||
];
|
||||
|
||||
const router = useRouter();
|
||||
const { keyword, activeTab, records, loading, finished, error, showEmpty, loadNextPage } = useReceptionData();
|
||||
const { keyword, activeTab, records, loading, finished, error, showEmpty, loadNextPage, resetFilters } = useReceptionData();
|
||||
|
||||
const formatCheckinTime = (value: string): string => {
|
||||
const normalized = value.replace("T", " ");
|
||||
|
|
@ -136,6 +139,12 @@ const openRecord = (record: ReceptionTaskDTO): void => {
|
|||
}
|
||||
|
||||
:deep(.van-empty) {
|
||||
--van-empty-padding: 50% 0;
|
||||
--van-empty-padding: 20% 0;
|
||||
}
|
||||
|
||||
:deep(.van-empty__bottom){
|
||||
--van-empty-bottom-margin-top: 30px;
|
||||
width: 260px;
|
||||
--van-button-normal-font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -90,17 +90,16 @@
|
|||
v-if="item.canCancel"
|
||||
class="h-32 min-w-50 rounded-4 border border-solid border-[#1580ff] bg-white px-10 text-[3.25rem] leading-none text-[#1580ff] disabled:opacity-60"
|
||||
type="button"
|
||||
:disabled="cancellingTaskId !== null"
|
||||
:disabled="cancellingTaskId === item.taskId || completingTaskId === item.taskId"
|
||||
@click="openCancelDialog(item)">
|
||||
{{ cancellingTaskId === item.taskId ? "取消中" : "取消" }}
|
||||
</button>
|
||||
<button
|
||||
v-if="item.canComplete"
|
||||
class="h-32 min-w-50 rounded-4 border-0 bg-[#1580ff] px-10 text-[3.25rem] leading-none text-white disabled:opacity-60"
|
||||
class="h-32 min-w-50 rounded-4 border-0 bg-[#1580ff] px-10 text-[3.25rem] leading-none text-white disabled:bg-[#CCCCCC] disabled:text-white"
|
||||
type="button"
|
||||
:disabled="cancellingTaskId !== null"
|
||||
:disabled="!item.canComplete || cancellingTaskId === item.taskId || completingTaskId === item.taskId"
|
||||
@click="completeReception(item)">
|
||||
完成
|
||||
{{ completingTaskId === item.taskId ? "完成中" : "完成" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -120,8 +119,8 @@
|
|||
overlay-class="reception-cancel-dialog__overlay"
|
||||
show-cancel-button
|
||||
:close-on-click-overlay="false"
|
||||
:cancel-button-disabled="cancellingTaskId !== null"
|
||||
:confirm-button-disabled="cancellingTaskId !== null"
|
||||
:cancel-button-disabled="cancellingTaskId === pendingCancelTask?.taskId"
|
||||
:confirm-button-disabled="cancellingTaskId === pendingCancelTask?.taskId"
|
||||
:before-close="beforeCancelDialogClose" />
|
||||
</main>
|
||||
</template>
|
||||
|
|
@ -138,7 +137,7 @@
|
|||
import { useReceptionMine } from "@/composables/useReceptionMine";
|
||||
|
||||
const router = useRouter();
|
||||
const { summary, overview, ongoingTasks, loading, error, cancellingTaskId, loadMine, cancelTask } = useReceptionMine();
|
||||
const { summary, overview, ongoingTasks, loading, error, cancellingTaskId, completingTaskId, loadMine, cancelTask, completeTask } = useReceptionMine();
|
||||
const showCancelDialog = ref(false);
|
||||
const pendingCancelTask = ref<ReceptionTaskDTO | null>(null);
|
||||
type CancelDialogAction = "confirm" | "cancel";
|
||||
|
|
@ -163,10 +162,6 @@
|
|||
router.push({ name: "reception-data" });
|
||||
};
|
||||
|
||||
const completeReception = (item: ReceptionTaskDTO): void => {
|
||||
void router.push({ name: "reception-record", params: { id: String(item.taskId) } });
|
||||
};
|
||||
|
||||
const openCancelDialog = (item: ReceptionTaskDTO): void => {
|
||||
pendingCancelTask.value = item;
|
||||
showCancelDialog.value = true;
|
||||
|
|
@ -185,6 +180,10 @@
|
|||
if (success) pendingCancelTask.value = null;
|
||||
return success;
|
||||
};
|
||||
|
||||
const completeReception = (item: ReceptionTaskDTO): void => {
|
||||
void completeTask(item.taskId);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
<div class="flex items-center gap-8">
|
||||
<span class="min-w-0 truncate text-[4rem] font-600 leading-none text-[#000]">{{ item.studentName || "-" }}</span>
|
||||
<span class="shrink-0 rounded-2 bg-[#E7F2FF] px-4 py-3 text-[2.5rem] leading-none text-[#1580FF]">
|
||||
签到:{{ item.checkinTime }}
|
||||
签到:{{ formatDate(item.checkinTime, "MM-DD HH:mm") }}
|
||||
</span>
|
||||
<span
|
||||
v-if="activeTab === 'available' && item.waitingMinutes > 0"
|
||||
|
|
@ -89,6 +89,7 @@
|
|||
import "vant/es/tabs/style";
|
||||
import type { ReceptionTaskDTO } from "@/api/reception";
|
||||
import { useReceptionTasks, type ReceptionTaskTab } from "@/composables/useReceptionTasks";
|
||||
import { formatDate } from "@/utils/format";
|
||||
|
||||
const tabs: ReadonlyArray<{ label: string; value: ReceptionTaskTab }> = [
|
||||
{ label: "待接待", value: "available" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue