Files
jiapuapp/pages/genealogy/g10-application-review.vue
T
2026-07-23 08:24:07 +08:00

479 lines
13 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.
<!-- 页面编号G-10用途管理员入谱申请审核与空/失败状态 -->
<template>
<view class="review-page">
<GenealogyPageBackground />
<view class="review-page__header">
<PageHeader
title="入谱审核"
action="说明"
custom-back
@action="showHelp"
@back="requestBack"
/>
</view>
<view
class="review-content"
:class="{
'review-state--list': reviewState === 'list',
'review-state--empty': reviewState === 'empty',
'review-state--error': reviewState === 'error',
'review-state--no-permission': reviewState === 'no-permission',
}"
>
<view v-if="reviewState !== 'loading'" class="review-intro">
<text>核实亲属关系后再决定</text>
<text>当前只预览审核交互不会提交服务器</text>
</view>
<template v-if="reviewState === 'list'">
<view
v-for="item in applications"
:key="item.id"
class="application-card"
>
<view class="application-card__body">
<text class="application-card__name">{{ item.name }}</text>
<text class="application-card__phone">{{ item.phone }}</text>
<text class="application-card__time">{{ item.appliedAt }}</text>
<text class="application-card__relation">{{ item.relation }}</text>
<view v-if="item.status === 'PENDING'" class="review-actions">
<AppButton
compact
type="secondary"
label="预览拒绝"
@click="confirmAudit(item, false)"
/>
<AppButton
compact
label="预览通过"
@click="confirmAudit(item, true)"
/>
</view>
<view
v-else
class="review-result"
:class="
item.status === 'APPROVED'
? 'review-result--approved'
: 'review-result--rejected'
"
>
<text class="application-card__status">{{
item.status === "APPROVED" ? "已通过" : "已拒绝"
}}</text>
</view>
</view>
</view>
</template>
<AppLoading
v-else-if="reviewState === 'loading'"
text="正在整理审核申请"
description="请稍候,正在读取待审核记录。"
/>
<view v-else class="review-state-card">
<view class="review-state-card__copy">
<text>{{ stateTitle }}</text>
<text>{{ stateCopy }}</text>
</view>
</view>
<AppButton
v-if="reviewState === 'error'"
class="review-page__retry"
label="重新查看"
@click="loadApplications({ genealogyId })"
/>
</view>
<AppDialog
:visible="!!confirmation || helpVisible"
:title="
helpVisible
? '审核说明'
: confirmation?.approved
? '预览通过效果?'
: '预览拒绝效果?'
"
:message="
helpVisible
? '请核对申请人的姓名、亲属关系和补充说明,仅确认与本家谱存在真实关系的申请。'
: confirmation?.approved
? '当前只更新本页本地预览,不会让申请人成为成员,也不会提交服务器。'
: '当前只更新本页本地预览,不会通知申请人,也不会提交服务器。'
"
:confirm-text="
helpVisible
? '我知道了'
: confirmation?.approved
? '查看通过效果'
: '查看拒绝效果'
"
:show-cancel="!!confirmation"
compact-actions
:close-on-mask="false"
@confirm="helpVisible ? closeDialog() : applyAudit()"
@cancel="closeDialog"
>
<view v-if="confirmation && !confirmation.approved" class="rejection-field">
<text>拒绝原因</text>
<textarea
id="g10-rejection-reason"
v-model="rejectionReason"
auto-height
maxlength="120"
placeholder="请说明需要补充或核实的信息"
:aria-invalid="!!rejectionError"
aria-describedby="g10-rejection-error"
:focus="rejectionFocused"
@input="clearRejectionError"
/>
<text
v-if="rejectionError"
id="g10-rejection-error"
class="rejection-field__error"
role="alert"
>{{ rejectionError }}</text
>
</view>
</AppDialog>
<view v-if="feedbackVisible" class="review-feedback">
<text>{{ feedbackMessage }}</text>
</view>
</view>
</template>
<script setup>
import { computed, nextTick, ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppDialog from "@/components/AppDialog.vue";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { getGenealogyFixtureAccess } from "@/data/mock.js";
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
const applications = ref([]);
const reviewSamples = [
{
id: "review-1",
name: "汤志成",
phone: "139****6421",
relation: "自述为汤正华堂侄 · 祖居洛阳",
appliedAt: "今天 10:24",
status: "PENDING",
},
{
id: "review-2",
name: "汤雨薇",
phone: "136****2798",
relation: "自述为汤正国之女 · 已补充长辈姓名",
appliedAt: "昨天 18:02",
status: "PENDING",
},
];
const genealogyId = ref("");
const reviewState = ref("loading");
const errorMessage = ref("");
const confirmation = ref(null);
const rejectionReason = ref("");
const rejectionError = ref("");
const rejectionFocused = ref(false);
const helpVisible = ref(false);
const feedbackVisible = ref(false);
const feedbackMessage = ref("");
let feedbackTimer = null;
onUnload(() => {
const timer = feedbackTimer;
feedbackTimer = null;
if (timer) clearTimeout(timer);
});
const stateTitle = computed(() =>
reviewState.value === "empty"
? "暂无待审核申请"
: reviewState.value === "no-permission"
? "当前账号无审核权限"
: "审核列表暂时无法读取",
);
const stateCopy = computed(() =>
reviewState.value === "empty"
? "新的入谱申请会在这里出现,并同步发送消息提醒。"
: reviewState.value === "no-permission"
? "只有家谱所有者或具备审核权限的管理员可以处理申请。"
: errorMessage.value || "请检查网络或家谱权限后重试。",
);
const loadApplications = (query = {}) => {
reviewState.value = "loading";
errorMessage.value = "";
genealogyId.value = String(query.genealogyId || "");
if (!genealogyId.value) {
errorMessage.value = "没有找到当前家谱,请从家谱总览进入。";
reviewState.value = "error";
return;
}
if (
getGenealogyFixtureAccess(genealogyId.value).accessRole !== "owner"
) {
reviewState.value = "no-permission";
return;
}
if (query.state === "loading") {
reviewState.value = "loading";
return;
}
if (query.state === "empty") {
reviewState.value = "empty";
return;
}
if (query.state === "error") {
reviewState.value = "error";
return;
}
if (query.state === "no-permission") {
reviewState.value = "no-permission";
return;
}
applications.value = reviewSamples.map((item) => ({ ...item }));
reviewState.value = "list";
};
onLoad(loadApplications);
const confirmAudit = (item, approved) => {
rejectionReason.value = "";
rejectionError.value = "";
rejectionFocused.value = false;
confirmation.value = { item, approved };
};
const closeDialog = () => {
confirmation.value = null;
rejectionReason.value = "";
rejectionError.value = "";
rejectionFocused.value = false;
helpVisible.value = false;
};
const requestBack = () =>
runBackGuard({
transientOpen: Boolean(confirmation.value) || helpVisible.value,
"close-transient": closeDialog,
});
onBackPress((event) => handleBackPress(event, requestBack));
const showFeedback = (message) => {
feedbackMessage.value = message;
feedbackVisible.value = true;
if (feedbackTimer) clearTimeout(feedbackTimer);
const timer = setTimeout(() => {
if (feedbackTimer !== timer) return;
feedbackVisible.value = false;
feedbackTimer = null;
}, 1800);
feedbackTimer = timer;
};
const clearRejectionError = () => {
rejectionError.value = "";
};
const applyAudit = async () => {
const current = confirmation.value;
if (!current) return;
if (!current.approved && !rejectionReason.value.trim()) {
rejectionError.value = "请填写拒绝原因,方便申请人补充资料";
rejectionFocused.value = false;
await nextTick();
rejectionFocused.value = true;
return;
}
current.item.status = current.approved ? "APPROVED" : "REJECTED";
if (!current.approved) current.item.rejectionReason = rejectionReason.value.trim();
closeDialog();
showFeedback(
`本地审核预览已更新,尚未提交服务器 · ${current.approved ? "已通过" : "已拒绝"}`,
);
};
const showHelp = () => {
helpVisible.value = true;
};
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.review-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.review-page__header {
z-index: 1;
}
.rejection-field { width: 100%; margin: 20rpx 0; text-align: left; }
.rejection-field > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }
.rejection-field textarea { @include adaptive.adaptive-genealogy-form-field; width: 100%; min-height: 110rpx; margin-top: 10rpx; padding: 18rpx; color: $ink; font-size: 23rpx; line-height: 1.55; }
.rejection-field__error { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; }
.review-content {
z-index: 1;
padding: 24rpx 24rpx 60rpx;
}
.review-intro {
margin: 0 8rpx 24rpx;
padding-bottom: 22rpx;
background: url("/static/assets/modules/genealogy/transparent/section-divider.png") bottom center / 100% 14rpx no-repeat;
}
.review-intro text {
display: block;
}
.review-intro text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
font-weight: 700;
}
.review-intro text:nth-child(2) {
margin-top: 8rpx;
color: $ink-muted;
font-size: 24rpx;
}
.application-card {
@include adaptive.adaptive-genealogy-list-card;
width: 100%;
min-height: 218rpx;
margin-bottom: 12rpx;
}
.application-card__body {
display: grid;
width: 100%;
min-height: 182rpx;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-rows: auto auto auto;
gap: 8rpx 0;
padding: 28rpx 28rpx 22rpx 40rpx;
box-sizing: border-box;
}
.application-card__name {
grid-column: 1;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 30rpx;
font-weight: 700;
}
.application-card__phone {
grid-column: 2;
margin-left: 14rpx;
color: $ink-muted;
font-size: 23rpx;
}
.application-card__time {
grid-column: 3;
justify-self: end;
margin-top: 3rpx;
color: #998873;
font-size: 23rpx;
}
.application-card__relation {
display: block;
grid-column: 1 / span 2;
grid-row: 2;
margin-top: 8rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.35;
}
.review-actions {
display: flex;
grid-column: 2 / 4;
grid-row: 3;
justify-self: end;
gap: 12rpx;
}
.review-actions .app-button {
width: 148rpx;
min-height: 68rpx;
}
.review-result {
@include adaptive.adaptive-scroll-button(primary);
display: grid;
grid-column: 2 / 4;
grid-row: 3;
justify-self: end;
width: 308rpx;
min-height: 68rpx;
}
.application-card__status {
z-index: 1;
display: flex;
grid-area: 1 / 1;
align-items: center;
justify-content: center;
height: 100%;
color: $brand-red;
font-size: 24rpx;
font-weight: 700;
}
.review-result--approved .application-card__status {
color: #fff9ed;
}
.review-result--approved {
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png");
}
.review-result--rejected .application-card__status {
color: $ink;
}
.review-result--rejected {
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png");
}
.review-state-card {
@include adaptive.adaptive-genealogy-list-card;
width: 100%;
min-height: 228rpx;
margin-top: 70rpx;
}
.review-state-card__copy {
display: flex;
min-height: 228rpx;
flex-direction: column;
justify-content: center;
padding: 48rpx 12%;
box-sizing: border-box;
text-align: center;
}
.review-state-card__copy text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
}
.review-state-card__copy text:last-child {
margin-top: 15rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.6;
}
.review-page__retry {
width: 420rpx;
min-height: 82rpx;
margin: 32rpx auto 0;
}
.review-feedback {
@include adaptive.adaptive-feedback-toast;
position: fixed;
z-index: 40;
top: 140rpx;
left: 50%;
display: flex;
min-width: 250rpx;
min-height: 76rpx;
align-items: center;
justify-content: center;
padding: 0 36rpx;
transform: translateX(-50%);
}
.review-feedback text {
color: $ink;
font-size: 23rpx;
}
</style>