ykt-team-wxapp/pages/record/appointment-record.vue
2026-09-02 14:50:14 +08:00

539 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<full-page pageClass="record-page" :customScroll="true">
<scroll-view
class="record-scroll"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="refreshRecords"
>
<view class="page-body">
<view class="profile-card" @click="switchArchive">
<view class="avatar-wrap">
<view class="avatar-placeholder">{{ avatarText }}</view>
</view>
<view class="profile-main">
<view class="profile-title">{{ customerName || "请选择档案" }}</view>
<view class="profile-sub">所属机构{{ corpName || "-" }}</view>
</view>
<uni-icons type="right" size="16" color="#c0c4cc" />
</view>
<picker mode="date" fields="month" :value="selectedMonth" @change="handleMonthChange">
<view class="month-bar">
<text>{{ monthText }}</text>
<text class="month-arrow"></text>
</view>
</picker>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!records.length" class="empty">暂无预约记录</view>
<view v-else class="record-list">
<view v-for="item in records" :key="item._id" class="record-card">
<view class="record-status">{{ statusText(item) }}</view>
<view class="record-row">
<text class="row-label">预约类型</text>
<text class="row-value strong">{{ appointmentTypeText(item) }}</text>
</view>
<view class="record-row">
<text class="row-label">{{ practitionerLabel(item) }}</text>
<text class="row-value strong">{{ practitionerText(item) }}</text>
</view>
<view class="record-row">
<text class="row-label">预约日期</text>
<text class="row-value">{{ appointmentDateText(item) }}</text>
</view>
<view class="record-row">
<text class="row-label">预约时间</text>
<text class="row-value">{{ item.timeRange || "-" }}</text>
</view>
<view class="record-row">
<text class="row-label">预约项目</text>
<text class="row-value">{{ projectText(item) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useAccount from "@/store/account";
import api from "@/utils/api";
import { normalizeCorpId } from "@/utils/api-base-config";
import { useTeamAccess } from "@/hooks/use-team-access";
import { hideLoading, loading as showLoading, toast } from "@/utils/widget";
import FullPage from "@/components/full-page.vue";
const env = __VITE_ENV__;
const appid = env.MP_WX_APP_ID;
const { account, externalUserId } = storeToRefs(useAccount());
const { getTeams, getExternalUserId, login } = useAccount();
const { openTeamLogin: openTeamInviteLogin, ensureTeamAdded: ensureJoinedTeam } = useTeamAccess();
const corpId = ref("");
const teamId = ref("");
const customerId = ref("");
const customerName = ref("");
const corpName = ref("");
const selectedMonth = ref(formatMonth(Date.now()));
const records = ref([]);
const staffNameMap = ref({});
const loading = ref(false);
const refreshing = ref(false);
const corpUserId = ref("");
const routeExternalUserId = ref("");
const authPending = ref(false);
const initialized = ref(false);
const statusMap = {
pending: { label: "未到院" },
confirmed: { label: "已到院" },
no_show: { label: "爽约" },
};
const avatarText = computed(() => (customerName.value || "档案").slice(0, 1));
const monthText = computed(() => selectedMonth.value.replace("-", "年") + "月");
function buildCurrentUrl() {
const params = [
`corpId=${encodeURIComponent(corpId.value || "")}`,
`teamId=${encodeURIComponent(teamId.value || "")}`,
customerId.value ? `customerId=${encodeURIComponent(customerId.value)}` : "",
customerName.value ? `name=${encodeURIComponent(customerName.value)}` : "",
corpName.value ? `corpName=${encodeURIComponent(corpName.value)}` : "",
corpUserId.value ? `corpUserId=${encodeURIComponent(corpUserId.value)}` : "",
routeExternalUserId.value ? `externalUserId=${encodeURIComponent(routeExternalUserId.value)}` : "",
selectedMonth.value ? `month=${encodeURIComponent(selectedMonth.value)}` : "",
].filter(Boolean).join("&");
return `/pages/record/appointment-record?${params}`;
}
async function ensureLogin() {
if (!account.value) await login();
if (!account.value) {
await ensureCustomerContext();
await openTeamLogin();
return false;
}
return true;
}
async function ensurePhoneAuthorized() {
if (account.value?.mobile) return true;
await ensureCustomerContext();
return openTeamLogin();
}
async function openTeamLogin() {
if (!corpId.value || !teamId.value) {
toast("预约团队信息缺失,暂无法查看预约记录");
return false;
}
const res = await openTeamInviteLogin({
corpId: corpId.value,
teamId: teamId.value,
corpUserId: corpUserId.value || "",
externalUserId: routeExternalUserId.value || externalUserId.value || "",
fallbackCorpName: corpName.value || "",
redirectUrl: buildCurrentUrl(),
});
if (!res.success) toast(res.message);
return false;
}
async function ensureCustomerContext() {
if (!corpId.value || !customerId.value) return null;
const res = await api("getCustomerByCustomerId", { corpId: corpId.value, customerId: customerId.value }, false);
const customer = res?.success && res.data ? res.data : null;
if (!customer) return null;
if (!customerName.value && customer.name) customerName.value = customer.name;
const teamIds = Array.isArray(customer.teamId) ? customer.teamId : [customer.teamId].filter(Boolean);
if (!teamId.value) teamId.value = teamIds.find(Boolean) || "";
return customer;
}
async function ensureTeamAdded() {
await ensureCustomerContext();
if (!corpId.value || !teamId.value) {
toast("预约团队信息缺失,暂无法查看预约记录");
return false;
}
showLoading("添加团队中...");
try {
const res = await ensureJoinedTeam({
appid,
account: account.value,
getTeams,
corpId: corpId.value,
teamId: teamId.value,
});
if (!res.success) {
toast(res.message);
return false;
}
return true;
} finally {
hideLoading();
}
}
async function ensureArchiveBound() {
const miniAppId = account.value?.openid || "";
if (!miniAppId || !corpId.value || !customerId.value) return false;
const customer = await ensureCustomerContext();
if (customer && String(customer.miniAppId || "") === String(miniAppId)) return true;
authPending.value = true;
uni.navigateTo({
url: `/pages/archive/edit-archive?corpId=${encodeURIComponent(corpId.value)}&teamId=${encodeURIComponent(teamId.value)}&bindCustomerId=${encodeURIComponent(customerId.value)}&source=appointmentRegistration`,
});
return false;
}
async function ensureAccessReady() {
await ensureCustomerContext();
const okLogin = await ensureLogin();
if (!okLogin) return false;
const okPhone = await ensurePhoneAuthorized();
if (!okPhone) return false;
await ensureCustomerContext();
if (corpId.value) await getExternalUserId(corpId.value);
const okTeam = await ensureTeamAdded();
if (!okTeam) return false;
const okArchive = await ensureArchiveBound();
if (!okArchive) return false;
return true;
}
async function loadCorpName() {
if (!corpId.value) {
corpName.value = "";
return;
}
const res = await api("getCorpInfo", { corpId: corpId.value }, false);
const corp = Array.isArray(res?.data) ? res.data[0] : null;
corpName.value = corp?.corp_name || corp?.corpName || "";
}
function pad(value) {
return String(value).padStart(2, "0");
}
function toDate(ts) {
if (!ts) return null;
const d = new Date(Number(ts));
return Number.isNaN(d.getTime()) ? null : d;
}
function formatMonth(ts) {
const d = toDate(ts) || new Date();
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
}
function formatDate(ts) {
const d = toDate(ts);
if (!d) return "";
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
function monthRange(month) {
const [year, monthIndex] = String(month || "").split("-").map(Number);
const start = new Date(year, monthIndex - 1, 1);
const end = new Date(year, monthIndex, 0);
return {
start: `${start.getFullYear()}-${pad(start.getMonth() + 1)}-01`,
end: `${end.getFullYear()}-${pad(end.getMonth() + 1)}-${pad(end.getDate())}`,
};
}
function appointmentDateText(item) {
return item?.appointmentDateStr || formatDate(item?.appointmentDate || item?.startTime || item?.registrationTime) || "-";
}
function statusText(item) {
return statusMap[item?.status || "pending"]?.label || "未到院";
}
function appointmentTypeText(item) {
if (item?.appointmentType === "followup") return "复诊预约";
if (item?.appointmentType === "treatment") return "治疗预约";
return "其他预约";
}
function practitionerLabel(item) {
return item?.appointmentType === "treatment" ? "治疗师" : "医生";
}
function practitionerText(item) {
const id = item?.therapistUserId || "";
if (item?.therapistName) return item.therapistName;
if (id === "none-therapist" || id === "none-doctor") return "未指定人员";
return staffNameMap.value[id] || id || "未指定人员";
}
function projectText(item) {
const projects = item?.treatmentProject;
if (Array.isArray(projects)) {
const names = projects
.map((project) => {
if (!project) return "";
if (typeof project === "string") return project;
return project.projectName || project.name || project.title || "";
})
.filter(Boolean);
return names.length ? names.join("、") : "无";
}
if (typeof projects === "string" && projects) return projects;
return "无";
}
function switchArchive() {
const corp = encodeURIComponent(corpId.value || "");
const team = encodeURIComponent(teamId.value || "");
const customer = encodeURIComponent(customerId.value || "");
uni.navigateTo({
url: `/pages/experience-coupon/select-rights-archive?mode=target&target=appointment&corpId=${corp}&teamId=${team}&customerId=${customer}`,
});
}
async function loadStaffNameMap() {
if (!corpId.value) {
staffNameMap.value = {};
return;
}
const res = await api("getAllCorpMemberIncludeDeleted", { corpId: corpId.value }, false);
const list = res?.success && Array.isArray(res.data) ? res.data : [];
staffNameMap.value = list.reduce((map, staff) => {
if (staff?.userid) {
map[staff.userid] = staff.anotherName || staff.name || staff.userid;
}
return map;
}, {});
}
async function loadRecords(options = {}) {
const { silent = false } = options;
if (!corpId.value || !customerId.value) {
records.value = [];
return;
}
if (!silent) loading.value = true;
try {
const range = monthRange(selectedMonth.value);
const res = await api(
"getAppointmentRegistration",
{
corpId: corpId.value,
customerId: customerId.value,
appointmentDateStart: range.start,
appointmentDateEnd: range.end,
page: 1,
pageSize: 200,
sortBy: "appointmentDate",
sortOrder: "desc",
},
false
);
if (!res?.success) {
toast(res?.message || "加载预约记录失败");
records.value = [];
return;
}
records.value = (Array.isArray(res.data) ? res.data : []).sort((a, b) => {
const aTime = Number(a.appointmentDate || a.startTime || a.registrationTime || 0);
const bTime = Number(b.appointmentDate || b.startTime || b.registrationTime || 0);
return bTime - aTime;
});
} finally {
loading.value = false;
}
}
async function refreshRecords() {
if (refreshing.value) return;
refreshing.value = true;
try {
await loadStaffNameMap();
await loadRecords({ silent: true });
} finally {
refreshing.value = false;
}
}
async function handleMonthChange(event) {
selectedMonth.value = event?.detail?.value || selectedMonth.value;
await loadRecords();
}
onLoad(async (options = {}) => {
uni.setNavigationBarTitle({ title: "预约记录" });
corpId.value = normalizeCorpId(options.corpId || "");
teamId.value = options.teamId || "";
customerId.value = options.customerId || options.id || "";
customerName.value = options.name ? decodeURIComponent(options.name) : "";
corpName.value = options.corpName ? decodeURIComponent(options.corpName) : "";
corpUserId.value = options.corpUserId || "";
routeExternalUserId.value = options.externalUserId || "";
if (options.month) selectedMonth.value = options.month;
await loadCorpName();
const ready = await ensureAccessReady();
if (!ready) return;
initialized.value = true;
await loadStaffNameMap();
await loadRecords();
});
onShow(async () => {
if (authPending.value) {
authPending.value = false;
const ready = await ensureAccessReady();
if (!ready) return;
initialized.value = true;
await loadStaffNameMap();
await loadRecords();
return;
}
if (initialized.value && corpId.value && customerId.value) loadRecords();
});
</script>
<style scoped>
.record-page :deep(.page-scroll) {
overflow: hidden;
}
.record-scroll {
height: 100%;
background: #e9edf3;
}
.page-body {
min-height: 100%;
box-sizing: border-box;
background: #e9edf3;
border-top: 8rpx solid #0b63d8;
}
.profile-card {
display: flex;
align-items: center;
gap: 18rpx;
padding: 20rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #d7dce5;
}
.avatar-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #d2d5da;
overflow: hidden;
flex-shrink: 0;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 30rpx;
font-weight: 600;
}
.profile-main {
flex: 1;
min-width: 0;
}
.profile-title {
font-size: 31rpx;
font-weight: 700;
color: #333;
line-height: 40rpx;
}
.profile-sub {
margin-top: 4rpx;
font-size: 24rpx;
color: #9aa0a8;
line-height: 34rpx;
}
.profile-arrow {
flex-shrink: 0;
color: #b4b7bf;
font-size: 32rpx;
}
.month-bar {
display: flex;
align-items: center;
gap: 8rpx;
height: 70rpx;
padding: 0 24rpx;
background: #e2e6ec;
color: #333;
font-size: 30rpx;
font-weight: 700;
border-bottom: 1rpx solid #d4d9e2;
}
.month-arrow {
font-size: 18rpx;
color: #333;
}
.record-list {
padding: 0rpx 10rpx 12rpx 10rpx;
}
.record-card {
position: relative;
padding: 22rpx 24rpx 22rpx 34rpx;
background: #fff;
border-top: 14rpx solid #e9edf3;
border-bottom: 1rpx solid #d7dce5;
}
.record-card::before {
display: none;
}
.record-status {
position: absolute;
top: 24rpx;
right: 24rpx;
color: #333;
background: transparent;
padding: 0;
font-size: 26rpx;
}
.record-row {
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 0;
padding: 7rpx 112rpx 7rpx 0;
font-size: 27rpx;
line-height: 38rpx;
}
.record-row:first-of-type {
padding-top: 0;
}
.row-label {
flex-shrink: 0;
color: #6b7280;
font-weight: 700;
}
.row-value {
min-width: 0;
text-align: left;
color: #333;
word-break: break-all;
}
.strong {
color: #333;
font-weight: 700;
}
.empty {
padding: 120rpx 0;
text-align: center;
font-size: 28rpx;
color: #999;
}
</style>