完成70%
This commit is contained in:
@@ -1,735 +1,4 @@
|
||||
<!-- 页面编号:G-03;用途:创建家谱与录入首代人物的同路由两步流程。 -->
|
||||
<template>
|
||||
<view class="flow-page">
|
||||
<GenealogyPageBackground />
|
||||
|
||||
<PageHeader
|
||||
:title="isAncestorStep ? '录入首代人物' : '创建家谱'"
|
||||
custom-back
|
||||
@back="requestBack"
|
||||
/>
|
||||
|
||||
<view class="flow-content">
|
||||
<view class="create-flow-panel">
|
||||
<view v-if="!isAncestorStep" class="create-flow-panel__content">
|
||||
<view class="flow-step-label"><text>第一步 · 立谱信息</text></view>
|
||||
<text class="flow-heading">为家族立一部可传承的谱</text>
|
||||
<text class="flow-note"
|
||||
>家谱建立后可继续完善,名称与访问规则由创建者维护。</text
|
||||
>
|
||||
|
||||
<view class="flow-fields">
|
||||
<view class="field-row">
|
||||
<text>姓氏</text>
|
||||
<input
|
||||
v-model="createForm.surname"
|
||||
maxlength="4"
|
||||
placeholder="请输入姓氏"
|
||||
placeholder-class="placeholder"
|
||||
@input="clearFieldError('surname')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.surname" class="field-error">{{
|
||||
fieldErrors.surname
|
||||
}}</text>
|
||||
<view class="field-row">
|
||||
<text>谱名</text>
|
||||
<input
|
||||
v-model="createForm.name"
|
||||
maxlength="24"
|
||||
placeholder="请输入家谱名称"
|
||||
placeholder-class="placeholder"
|
||||
@input="clearFieldError('name')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.name" class="field-error">{{
|
||||
fieldErrors.name
|
||||
}}</text>
|
||||
<view class="field-row">
|
||||
<text>堂号</text>
|
||||
<input
|
||||
v-model="createForm.hall"
|
||||
maxlength="12"
|
||||
placeholder="选填"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>所在地</text>
|
||||
<input
|
||||
v-model="createForm.location"
|
||||
maxlength="30"
|
||||
placeholder="请选择或输入"
|
||||
placeholder-class="placeholder"
|
||||
@input="clearFieldError('location')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.location" class="field-error">{{
|
||||
fieldErrors.location
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<view class="flow-rule">
|
||||
<text class="flow-rule__label">访问规则</text>
|
||||
<view class="flow-rule__options">
|
||||
<view
|
||||
v-for="option in GENEALOGY_ACCESS_PRESET_OPTIONS"
|
||||
:key="option.value"
|
||||
class="flow-rule__option"
|
||||
:class="{
|
||||
'flow-rule__option--active':
|
||||
createForm.accessPreset === option.value,
|
||||
}"
|
||||
@click="createForm.accessPreset = option.value"
|
||||
>{{ option.label }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<text class="flow-rule__note"
|
||||
>保护族人资料;创建后可在家谱设置中开放搜索与申请加入。</text
|
||||
>
|
||||
<text v-if="createState === 'error'" class="flow-error"
|
||||
>创建样式模拟失败,请检查信息后重试。</text
|
||||
>
|
||||
|
||||
<view
|
||||
class="flow-primary-action"
|
||||
hover-class="action-hover"
|
||||
@click="submitCreate"
|
||||
>
|
||||
<text class="flow-primary-action__copy">{{
|
||||
createState === "submitting" ? "正在创建…" : "创建并录入首代"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="create-flow-panel__content">
|
||||
<view class="flow-step-label"><text>第二步 · 首代人物</text></view>
|
||||
<text class="flow-heading">家谱从这一位开始</text>
|
||||
<text class="flow-note"
|
||||
>可先录入姓名、世代和简要生平,其他资料后续随时补充。</text
|
||||
>
|
||||
|
||||
<view class="flow-fields">
|
||||
<view class="field-row">
|
||||
<text>姓名</text>
|
||||
<input
|
||||
v-model="ancestorForm.personName"
|
||||
maxlength="20"
|
||||
placeholder="请输入首代姓名"
|
||||
placeholder-class="placeholder"
|
||||
@input="clearFieldError('personName')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.personName" class="field-error">{{
|
||||
fieldErrors.personName
|
||||
}}</text>
|
||||
<view class="field-row">
|
||||
<text>世代</text>
|
||||
<text class="fixed-value">第一世</text>
|
||||
</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="sexOptions"
|
||||
range-key="label"
|
||||
@change="changeAncestorSex"
|
||||
>
|
||||
<view class="field-row">
|
||||
<text>性别</text>
|
||||
<text class="fixed-value">{{ sexLabel }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="field-row">
|
||||
<text>出生日期</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="ancestorForm.birthDate"
|
||||
start="1800-01-01"
|
||||
:end="new Date().toISOString().slice(0, 10)"
|
||||
@change="changeAncestorBirthDate"
|
||||
>
|
||||
<text class="fixed-value">{{
|
||||
ancestorForm.birthDate || "请选择日期"
|
||||
}}</text>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="ancestorState === 'error'" class="flow-error"
|
||||
>首代人物保存失败,请稍后重试。</text
|
||||
>
|
||||
|
||||
<view class="intro-field">
|
||||
<text>生平简述</text>
|
||||
<textarea
|
||||
v-model="ancestorForm.introduction"
|
||||
auto-height
|
||||
maxlength="200"
|
||||
placeholder="可选,记录家训、迁徙或重要经历"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="flow-primary-action"
|
||||
hover-class="action-hover"
|
||||
@click="submitAncestor"
|
||||
>
|
||||
<text class="flow-primary-action__copy">{{
|
||||
ancestorState === "submitting" ? "正在保存…" : "保存并进入家谱"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="duplicateReminderVisible"
|
||||
class="duplicate-reminder-layer"
|
||||
@click="closeDuplicateReminder"
|
||||
>
|
||||
<view class="duplicate-reminder" @click.stop>
|
||||
<view class="duplicate-reminder__content">
|
||||
<text class="duplicate-reminder__title">先确认是否已有家谱</text>
|
||||
<text class="duplicate-reminder__copy"
|
||||
>同一家族可能已经建谱。建议先搜索谱名、地区和堂号,避免重复创建。</text
|
||||
>
|
||||
<view
|
||||
class="duplicate-reminder__search"
|
||||
@click="searchExistingGenealogy"
|
||||
>
|
||||
<text>先搜索已有家谱</text>
|
||||
</view>
|
||||
<view class="duplicate-reminder__confirm" @click="confirmCreate">
|
||||
<text>确认没有,继续创建</text>
|
||||
</view>
|
||||
<view
|
||||
class="duplicate-reminder__cancel"
|
||||
@click="closeDuplicateReminder"
|
||||
>返回修改</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="ancestorState === 'success'" class="flow-success-layer">
|
||||
<view class="flow-success-dialog">
|
||||
<view class="flow-success-dialog__content">
|
||||
<text class="flow-success-dialog__title">家谱创建完成</text>
|
||||
<text class="flow-success-dialog__copy"
|
||||
>当前为本地流程预览,资料尚未提交服务器,可进入总览继续检查页面。</text
|
||||
>
|
||||
<view class="flow-success-dialog__action" @click="enterOverview">
|
||||
<text>进入家谱总览</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃创建?"
|
||||
message="当前填写内容尚未保存,确认返回后将清空。"
|
||||
confirm-text="放弃并返回"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
createLocalGenealogyPreview,
|
||||
removeLocalGenealogyPreview,
|
||||
updateLocalGenealogyPreview,
|
||||
updateLocalGenealogyPreviewAncestor,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
GENEALOGY_ACCESS_PRESET,
|
||||
GENEALOGY_ACCESS_PRESET_OPTIONS,
|
||||
} from "@/utils/genealogy-contracts.js";
|
||||
import { handleBackPress, openPage, returnTo, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const currentStep = ref("create");
|
||||
const genealogyId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const createState = ref("form");
|
||||
const ancestorState = ref("form");
|
||||
const duplicateReminderVisible = ref(false);
|
||||
const discardVisible = ref(false);
|
||||
const fieldErrors = reactive({
|
||||
surname: "",
|
||||
name: "",
|
||||
location: "",
|
||||
personName: "",
|
||||
});
|
||||
let submitTimer = null;
|
||||
const createForm = reactive({
|
||||
surname: "",
|
||||
name: "",
|
||||
hall: "",
|
||||
location: "",
|
||||
accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY,
|
||||
});
|
||||
const ancestorForm = reactive({
|
||||
personName: "",
|
||||
generationNo: 1,
|
||||
generationName: "一世",
|
||||
sex: "0",
|
||||
birthDate: "",
|
||||
introduction: "",
|
||||
});
|
||||
const sexOptions = [
|
||||
{ value: "0", label: "男" },
|
||||
{ value: "1", label: "女" },
|
||||
{ value: "2", label: "暂不填写" },
|
||||
];
|
||||
const sexLabel = computed(
|
||||
() =>
|
||||
sexOptions.find((option) => option.value === ancestorForm.sex)?.label ||
|
||||
"请选择",
|
||||
);
|
||||
const changeAncestorSex = (event) => {
|
||||
ancestorForm.sex = sexOptions[Number(event.detail.value)]?.value || "2";
|
||||
};
|
||||
const changeAncestorBirthDate = (event) => {
|
||||
ancestorForm.birthDate = event.detail.value;
|
||||
};
|
||||
|
||||
const isAncestorStep = computed(() => currentStep.value === "ancestor");
|
||||
const isDirty = computed(() =>
|
||||
isAncestorStep.value
|
||||
? Boolean(
|
||||
ancestorForm.personName ||
|
||||
ancestorForm.birthDate ||
|
||||
ancestorForm.introduction ||
|
||||
ancestorForm.sex !== "0",
|
||||
)
|
||||
: Boolean(
|
||||
createForm.surname ||
|
||||
createForm.name ||
|
||||
createForm.hall ||
|
||||
createForm.location ||
|
||||
createForm.accessPreset !== GENEALOGY_ACCESS_PRESET.MEMBER_ONLY,
|
||||
),
|
||||
);
|
||||
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestRawDiscardConfirmation = discardConfirmation.request;
|
||||
const requestDiscardConfirmation = async () => {
|
||||
const confirmed = await requestRawDiscardConfirmation();
|
||||
if (confirmed && currentStep.value === "create" && genealogyId.value) {
|
||||
removeLocalGenealogyPreview(genealogyId.value);
|
||||
genealogyId.value = "";
|
||||
}
|
||||
return confirmed;
|
||||
};
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
onUnload(() => {
|
||||
const timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const closeActiveTransient = () => {
|
||||
if (duplicateReminderVisible.value) closeDuplicateReminder();
|
||||
else cancelDiscard();
|
||||
};
|
||||
const popInternalTrail = () => {
|
||||
currentStep.value = "create";
|
||||
ancestorState.value = "form";
|
||||
return true;
|
||||
};
|
||||
const requestBack = () => {
|
||||
if (ancestorState.value === "success") return enterOverview();
|
||||
return runBackGuard({
|
||||
transientOpen: duplicateReminderVisible.value || discardVisible.value,
|
||||
internalTrail: isAncestorStep.value && !isSubmitting.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": closeActiveTransient,
|
||||
"pop-internal-trail": popInternalTrail,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
};
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const clearFieldError = (field) => {
|
||||
fieldErrors[field] = "";
|
||||
};
|
||||
|
||||
const validateCreateForm = () => {
|
||||
fieldErrors.surname = createForm.surname.trim() ? "" : "请填写姓氏";
|
||||
fieldErrors.name = createForm.name.trim() ? "" : "请填写谱名";
|
||||
fieldErrors.location = createForm.location.trim() ? "" : "请填写所在地";
|
||||
return !fieldErrors.surname && !fieldErrors.name && !fieldErrors.location;
|
||||
};
|
||||
|
||||
const submitCreate = () => {
|
||||
if (isSubmitting.value) return;
|
||||
createState.value = "form";
|
||||
if (!validateCreateForm()) return;
|
||||
duplicateReminderVisible.value = true;
|
||||
};
|
||||
|
||||
const closeDuplicateReminder = () => {
|
||||
duplicateReminderVisible.value = false;
|
||||
};
|
||||
const searchExistingGenealogy = () => {
|
||||
closeDuplicateReminder();
|
||||
return openPage("G06", {}, "G03");
|
||||
};
|
||||
const confirmCreate = () => {
|
||||
if (isSubmitting.value) return;
|
||||
closeDuplicateReminder();
|
||||
const createSnapshot = Object.freeze({ ...createForm });
|
||||
isSubmitting.value = true;
|
||||
createState.value = "submitting";
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
submitTimer = null;
|
||||
isSubmitting.value = false;
|
||||
if (createSnapshot.name.trim() === "失败") {
|
||||
createState.value = "error";
|
||||
return;
|
||||
}
|
||||
genealogyId.value =
|
||||
updateLocalGenealogyPreview(genealogyId.value, createSnapshot) ||
|
||||
createLocalGenealogyPreview(createSnapshot);
|
||||
currentStep.value = "ancestor";
|
||||
createState.value = "form";
|
||||
}, 320);
|
||||
submitTimer = timer;
|
||||
};
|
||||
|
||||
const submitAncestor = () => {
|
||||
if (isSubmitting.value) return;
|
||||
if (!genealogyId.value) {
|
||||
ancestorState.value = "error";
|
||||
return;
|
||||
}
|
||||
if (!ancestorForm.personName.trim()) {
|
||||
fieldErrors.personName = "请填写首代姓名";
|
||||
return;
|
||||
}
|
||||
|
||||
const ancestorSnapshot = Object.freeze({ ...ancestorForm });
|
||||
isSubmitting.value = true;
|
||||
ancestorState.value = "submitting";
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
submitTimer = null;
|
||||
isSubmitting.value = false;
|
||||
if (ancestorSnapshot.personName.trim() === "失败") {
|
||||
ancestorState.value = "error";
|
||||
return;
|
||||
}
|
||||
ancestorState.value = updateLocalGenealogyPreviewAncestor(
|
||||
genealogyId.value,
|
||||
ancestorSnapshot,
|
||||
)
|
||||
? "success"
|
||||
: "error";
|
||||
}, 320);
|
||||
submitTimer = timer;
|
||||
};
|
||||
|
||||
const enterOverview = () =>
|
||||
returnTo("G05", { genealogyId: genealogyId.value });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.flow-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: #f9f6ef;
|
||||
}
|
||||
|
||||
.flow-content {
|
||||
z-index: 2;
|
||||
padding: 24rpx 32rpx 48rpx;
|
||||
}
|
||||
|
||||
.create-flow-panel {
|
||||
@include adaptive.adaptive-genealogy-state-panel;
|
||||
min-height: 1000rpx;
|
||||
}
|
||||
|
||||
.create-flow-panel__content {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 1000rpx;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding: 48rpx 50rpx 42rpx;
|
||||
}
|
||||
|
||||
.flow-step-label {
|
||||
color: #a33b2b;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.flow-heading {
|
||||
display: block;
|
||||
margin-top: 13rpx;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 39rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.flow-note,
|
||||
.flow-rule__note {
|
||||
display: block;
|
||||
color: #786654;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.flow-note {
|
||||
min-height: 56rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.flow-fields {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
min-height: 86rpx;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid rgba(181, 138, 75, 0.36);
|
||||
}
|
||||
|
||||
.field-row > text {
|
||||
width: 126rpx;
|
||||
flex: 0 0 auto;
|
||||
color: #786654;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
}
|
||||
|
||||
.field-row input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #b7aa97;
|
||||
}
|
||||
.field-error {
|
||||
display: block;
|
||||
margin-top: 3rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fixed-value {
|
||||
color: #a33b2b !important;
|
||||
}
|
||||
|
||||
.flow-rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.flow-rule__label {
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.flow-rule__options {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.flow-rule__option {
|
||||
display: flex;
|
||||
min-height: 54rpx;
|
||||
align-items: center;
|
||||
padding: 0 10rpx;
|
||||
color: #786654;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.flow-rule__option--active {
|
||||
color: $brand-red;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.flow-rule__note {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.flow-error {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: $brand-red;
|
||||
font-size: 25rpx;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro-field {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.intro-field > text {
|
||||
display: block;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.intro-field textarea {
|
||||
width: 100%;
|
||||
min-height: 142rpx;
|
||||
margin-top: 8rpx;
|
||||
color: #392719;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.flow-primary-action {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 96rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
|
||||
.flow-primary-action__copy {
|
||||
z-index: 1;
|
||||
color: #fff9ec;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.action-hover {
|
||||
opacity: 0.82;
|
||||
}
|
||||
.duplicate-reminder-layer,
|
||||
.flow-success-layer {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 42rpx;
|
||||
background: rgba(34, 20, 12, 0.6);
|
||||
}
|
||||
.duplicate-reminder,
|
||||
.flow-success-dialog {
|
||||
width: 100%;
|
||||
max-width: 670rpx;
|
||||
min-height: 650rpx;
|
||||
background: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.duplicate-reminder__content,
|
||||
.flow-success-dialog__content {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 650rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 92rpx 64rpx 56rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.duplicate-reminder__title,
|
||||
.flow-success-dialog__title {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 39rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.duplicate-reminder__copy,
|
||||
.flow-success-dialog__copy {
|
||||
margin-top: 24rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
.duplicate-reminder__search,
|
||||
.duplicate-reminder__confirm,
|
||||
.flow-success-dialog__action {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 92rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.duplicate-reminder__search {
|
||||
margin-top: 38rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.duplicate-reminder__confirm {
|
||||
margin-top: 14rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.duplicate-reminder__search text,
|
||||
.duplicate-reminder__confirm text,
|
||||
.flow-success-dialog__action text {
|
||||
z-index: 1;
|
||||
color: #fff9ec;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.duplicate-reminder__search text {
|
||||
color: #7b4e24;
|
||||
}
|
||||
.duplicate-reminder__cancel {
|
||||
display: flex;
|
||||
min-height: 68rpx;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.flow-success-dialog__action {
|
||||
margin-top: 44rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
</style>
|
||||
<!-- 页面编号:G-03;用途:创建家谱。两阶段创建的结果恢复链未闭合时保持关闭。 -->
|
||||
<template><view class="create-page"><GenealogyPageBackground /><view class="page-header"><PageHeader title="创建家谱" custom-back @back="backToGenealogies" /></view><view class="page-content"><view class="state-card"><text>创建家谱待结果恢复合同</text><text>创建家谱后还必须用真实响应中的 `genealogyId` 创建首位人物;当前没有可靠的创建结果恢复查询 owner,且不能从泛型“我的家谱”列表按名称猜 ID。页面不再生成本地家谱或本地首位人物预览。</text><AppButton block label="返回我的家谱" @click="backToGenealogies" /></view></view></view></template>
|
||||
<script setup>import AppButton from "@/components/AppButton.vue";import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";import PageHeader from "@/components/PageHeader.vue";import {returnTo} from "@/utils/navigation.js";const backToGenealogies=()=>returnTo("G01");</script>
|
||||
<style scoped lang="scss">@use "../../styles/adaptive-frame-profiles.scss" as adaptive;.create-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-genealogy-state-panel}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}</style>
|
||||
|
||||
@@ -1,718 +1,4 @@
|
||||
<!-- 页面编号:G-06;用途:搜索家谱/邀请码加入双模式定位目标家谱。 -->
|
||||
<template>
|
||||
<view class="search-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="search-page__header"><PageHeader title="加入家谱" /></view>
|
||||
|
||||
<view class="search-page__content">
|
||||
<view class="mode-tabs">
|
||||
<view
|
||||
class="mode-tab"
|
||||
:class="{ 'mode-tab--active': mode === 'search' }"
|
||||
@click="switchMode('search')"
|
||||
>搜索家谱</view
|
||||
>
|
||||
<view
|
||||
class="mode-tab"
|
||||
:class="{ 'mode-tab--active': mode === 'invite' }"
|
||||
@click="switchMode('invite')"
|
||||
>邀请码加入</view
|
||||
>
|
||||
</view>
|
||||
|
||||
<template v-if="mode === 'search'">
|
||||
<view class="search-controls">
|
||||
<view class="search-field">
|
||||
<input
|
||||
v-model.trim="keyword"
|
||||
class="search-input"
|
||||
placeholder="姓氏、谱名、地区或堂号"
|
||||
placeholder-class="search-input__placeholder"
|
||||
@confirm="search"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @click="clearSearch"
|
||||
>清空</view
|
||||
>
|
||||
</view>
|
||||
<view class="search-action" @click="search">
|
||||
<text class="search-action__label">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-row">
|
||||
<text class="filter-label">地区</text>
|
||||
<view
|
||||
v-for="area in areas"
|
||||
:key="area"
|
||||
class="filter-chip"
|
||||
:class="{ 'filter-chip--active': selectedArea === area }"
|
||||
@click="selectedArea = area"
|
||||
>{{ area }}</view
|
||||
>
|
||||
</view>
|
||||
<image
|
||||
class="search-divider"
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<AppLoading
|
||||
v-if="searchState === 'loading'"
|
||||
variant="section"
|
||||
text="正在检索公开家谱"
|
||||
description="请稍候,正在整理匹配结果。"
|
||||
/>
|
||||
<view
|
||||
v-else-if="searchState === 'initial'"
|
||||
class="search-status search-initial"
|
||||
>
|
||||
<text class="search-status__lead">先确认家族是否已有家谱</text>
|
||||
<text class="search-status__copy"
|
||||
>可按姓氏、谱名、地区或堂号查找,避免重复创建。</text
|
||||
>
|
||||
<image
|
||||
class="search-status__hall"
|
||||
src="/static/assets/foundation/transparent/root-header-hall.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view v-else-if="searchState === 'results'" class="search-results">
|
||||
<view class="search-results__head"
|
||||
><text>检索到 {{ results.length }} 部家谱</text
|
||||
><text>请核对支系与管理信息</text></view
|
||||
>
|
||||
<view
|
||||
v-for="item in results"
|
||||
:key="item.id"
|
||||
class="result-card genealogy-card"
|
||||
@click="openPreview(item)"
|
||||
>
|
||||
<view class="result-card__content">
|
||||
<view class="result-card__head">
|
||||
<view>
|
||||
<text class="result-card__name">{{ item.name }}</text>
|
||||
<text class="result-card__identity"
|
||||
>{{ item.surname }}氏 · {{ item.hall }}</text
|
||||
>
|
||||
</view>
|
||||
<text
|
||||
class="result-card__relation"
|
||||
:class="`result-card__relation--${item.relation}`"
|
||||
>{{ resultRelationLabel(item) }}</text
|
||||
>
|
||||
</view>
|
||||
<view class="result-card__facts">
|
||||
<text>地区:{{ item.location }}</text>
|
||||
<text>所属上级谱:{{ item.parentName }}</text>
|
||||
<text>当前支系:{{ item.branchName }}</text>
|
||||
<text
|
||||
>管理信息:{{ item.manager }} · {{ item.certification }}</text
|
||||
>
|
||||
<text
|
||||
>{{ item.memberCount }} 位成员 · 更新于
|
||||
{{ item.updatedAt }}</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
v-if="resultActionLabel(item)"
|
||||
class="result-card__action"
|
||||
@click.stop="handleResultAction(item)"
|
||||
>{{ resultActionLabel(item) }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="searchState === 'empty'"
|
||||
class="search-status search-empty"
|
||||
>
|
||||
<image
|
||||
class="search-status__cloud"
|
||||
src="/static/assets/modules/genealogy/transparent/create-cloud.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="search-status__lead">暂未找到匹配家谱</text>
|
||||
<text class="search-status__copy"
|
||||
>可调整地区或关键词后重试,也可使用邀请码加入。</text
|
||||
>
|
||||
</view>
|
||||
<view v-else class="search-status search-error">
|
||||
<text class="search-status__lead">检索暂时失败</text>
|
||||
<text class="search-status__copy"
|
||||
>当前仅展示失败样式,请稍后重新搜索。</text
|
||||
>
|
||||
<view class="status-retry" @click="search">
|
||||
<text>重新搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="invite-block">
|
||||
<text class="invite-title">输入家谱邀请码</text>
|
||||
<text class="invite-copy"
|
||||
>邀请码用于定位指定家谱,加入前仍需确认真实姓名和关系。</text
|
||||
>
|
||||
<view class="invite-controls">
|
||||
<view class="invite-field">
|
||||
<input
|
||||
v-model.trim="inviteCode"
|
||||
class="invite-input"
|
||||
maxlength="12"
|
||||
placeholder="请输入邀请码"
|
||||
placeholder-class="search-input__placeholder"
|
||||
@input="resetInvite"
|
||||
/>
|
||||
</view>
|
||||
<view class="search-action" @click="verifyInvite">
|
||||
<text class="search-action__label">验证</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="invite-demo">样式验证可输入 JP2026</text>
|
||||
</view>
|
||||
<image
|
||||
class="search-divider"
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
|
||||
<view
|
||||
v-if="inviteState === 'initial'"
|
||||
class="search-status invite-initial"
|
||||
>
|
||||
<text class="search-status__lead">通过邀请码直接定位家谱</text>
|
||||
<text class="search-status__copy"
|
||||
>验证有效后仅显示目标家谱,仍需填写身份关系;本地验证不会变更成员身份。</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="inviteState === 'invalid'"
|
||||
class="search-status invite-invalid"
|
||||
>
|
||||
<text class="search-status__lead">邀请码无效或已过期</text>
|
||||
<text class="search-status__copy"
|
||||
>请核对邀请码,或向家谱管理员重新获取。</text
|
||||
>
|
||||
</view>
|
||||
<view v-else class="invite-result">
|
||||
<text class="invite-result__label">已定位目标家谱</text>
|
||||
<view class="result-card genealogy-card">
|
||||
<view class="result-card__content">
|
||||
<text class="result-card__name">{{ inviteTarget.name }}</text>
|
||||
<view class="result-card__facts">
|
||||
<text
|
||||
>{{ inviteTarget.surname }}氏 · {{ inviteTarget.hall }} ·
|
||||
{{ inviteTarget.location }}</text
|
||||
>
|
||||
<text>所属上级谱:{{ inviteTarget.parentName }}</text>
|
||||
<text>当前支系:{{ inviteTarget.branchName }}</text>
|
||||
<text
|
||||
>{{ inviteTarget.manager }} ·
|
||||
{{ inviteTarget.certification }}</text
|
||||
>
|
||||
</view>
|
||||
<view class="result-card__action" @click="confirmInvite"
|
||||
>填写关系信息</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<text class="invite-result__note">当前为样式验证,不会变更成员身份</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
getGenealogyFixtureAccess,
|
||||
isGenealogySearchVisible,
|
||||
publicGenealogies,
|
||||
} from "@/data/mock.js";
|
||||
import { goRoot, openPage } from "@/utils/navigation.js";
|
||||
|
||||
const mode = ref("search");
|
||||
const keyword = ref("");
|
||||
const selectedArea = ref("全部");
|
||||
const searchState = ref("initial");
|
||||
const inviteCode = ref("");
|
||||
const inviteState = ref("initial");
|
||||
const results = ref([]);
|
||||
let searchTimer = null;
|
||||
const invalidateSearch = () => {
|
||||
const timer = searchTimer;
|
||||
searchTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
|
||||
const areas = ["全部", "河南", "山东"];
|
||||
const inviteTarget = computed(() => publicGenealogies[0]);
|
||||
|
||||
const syncModeFromRoute = (query = {}) => {
|
||||
mode.value = query?.mode === "invite" ? "invite" : "search";
|
||||
if (mode.value === "search" && query?.state === "loading")
|
||||
searchState.value = "loading";
|
||||
};
|
||||
|
||||
onLoad((query) => syncModeFromRoute(query));
|
||||
onUnload(invalidateSearch);
|
||||
|
||||
const switchMode = (nextMode) => {
|
||||
invalidateSearch();
|
||||
mode.value = nextMode;
|
||||
results.value = [];
|
||||
searchState.value = "initial";
|
||||
inviteState.value = "initial";
|
||||
};
|
||||
|
||||
const search = () => {
|
||||
invalidateSearch();
|
||||
searchState.value = "loading";
|
||||
const value = keyword.value.trim();
|
||||
const area = selectedArea.value;
|
||||
const timer = setTimeout(() => {
|
||||
if (searchTimer !== timer || mode.value !== "search") return;
|
||||
searchTimer = null;
|
||||
if (value === "失败") {
|
||||
searchState.value = "error";
|
||||
return;
|
||||
}
|
||||
results.value = publicGenealogies.filter((item) => {
|
||||
const matchesKeyword =
|
||||
!value ||
|
||||
`${item.name}${item.surname}${item.location}${item.hall}`.includes(
|
||||
value,
|
||||
);
|
||||
const matchesArea =
|
||||
area === "全部" || item.location.includes(area);
|
||||
return isGenealogySearchVisible(String(item.id)) && matchesKeyword && matchesArea;
|
||||
});
|
||||
searchState.value = results.value.length ? "results" : "empty";
|
||||
}, 260);
|
||||
searchTimer = timer;
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
invalidateSearch();
|
||||
keyword.value = "";
|
||||
results.value = [];
|
||||
searchState.value = "initial";
|
||||
};
|
||||
|
||||
const openPreview = (item) => {
|
||||
if (
|
||||
item.relation === "available" &&
|
||||
getGenealogyFixtureAccess(String(item.id)).canApply
|
||||
) {
|
||||
return openPage(
|
||||
"G05",
|
||||
{ genealogyId: String(item.id) },
|
||||
"G06",
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const resultActionLabel = (item) => {
|
||||
if (item.relation === "pending") return "查看申请进度";
|
||||
if (item.relation === "joined") return "切换到该家谱";
|
||||
if (item.relation === "owned") return "进入我的家谱";
|
||||
if (!getGenealogyFixtureAccess(String(item.id)).canApply) return "";
|
||||
return {
|
||||
available: "申请加入",
|
||||
rejected: "修改后重新申请",
|
||||
removed: "重新申请",
|
||||
}[item.relation] || "";
|
||||
};
|
||||
const resultRelationLabel = (item) =>
|
||||
({
|
||||
available: "可申请",
|
||||
joined: "已加入",
|
||||
pending: "审核中",
|
||||
rejected: "已拒绝",
|
||||
removed: "已退出",
|
||||
owned: "我创建的",
|
||||
})[item.relation] || "关系待确认";
|
||||
|
||||
const handleResultAction = (item) => {
|
||||
if (item.relation === "pending")
|
||||
return openPage("G09", { status: "pending" }, "G06");
|
||||
if (
|
||||
["available", "rejected", "removed"].includes(item.relation) &&
|
||||
getGenealogyFixtureAccess(String(item.id)).canApply
|
||||
)
|
||||
return openPage(
|
||||
"G08",
|
||||
{ genealogyId: String(item.id), source: "search" },
|
||||
"G06",
|
||||
);
|
||||
if (item.relation === "joined" || item.relation === "owned")
|
||||
return goRoot("G01", { genealogyId: String(item.id) });
|
||||
return false;
|
||||
};
|
||||
|
||||
const verifyInvite = () => {
|
||||
inviteState.value =
|
||||
inviteCode.value.toUpperCase() === "JP2026" ? "valid" : "invalid";
|
||||
};
|
||||
const resetInvite = () => {
|
||||
inviteState.value = "initial";
|
||||
};
|
||||
const confirmInvite = () => {
|
||||
if (inviteState.value !== "valid") return false;
|
||||
if (inviteCode.value.trim().toUpperCase() !== "JP2026") return false;
|
||||
return openPage(
|
||||
"G08",
|
||||
{
|
||||
genealogyId: String(inviteTarget.value.id),
|
||||
source: "invite",
|
||||
},
|
||||
"G06",
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.search-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.search-page__header {
|
||||
z-index: 2;
|
||||
}
|
||||
.search-page__content {
|
||||
z-index: 3;
|
||||
min-height: calc(100vh - 104rpx);
|
||||
padding: 30rpx 24rpx calc(58rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
width: 500rpx;
|
||||
margin: 30rpx auto 0;
|
||||
border-bottom: 1rpx solid rgba(181, 138, 75, 0.45);
|
||||
}
|
||||
.mode-tab {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 70rpx;
|
||||
color: $ink-muted;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.mode-tab--active {
|
||||
margin-bottom: -2rpx;
|
||||
background: linear-gradient($brand-red, $brand-red) center bottom /
|
||||
calc(100% - 84rpx) 4rpx no-repeat;
|
||||
color: $brand-red;
|
||||
font-weight: 700;
|
||||
}
|
||||
.search-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
.invite-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
.search-field {
|
||||
@include adaptive.adaptive-g06-search-field;
|
||||
display: grid;
|
||||
width: 430rpx;
|
||||
min-height: 95rpx;
|
||||
}
|
||||
.search-input {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 82rpx 0 30rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 25rpx;
|
||||
}
|
||||
.search-input__placeholder {
|
||||
color: #ab9c83;
|
||||
}
|
||||
.search-clear {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
justify-self: end;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.search-action {
|
||||
@include adaptive.adaptive-g06-search-action;
|
||||
display: flex;
|
||||
width: 200rpx;
|
||||
min-height: 88rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.search-action__label {
|
||||
z-index: 1;
|
||||
color: #fff4dc;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 5rpx;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin: 16rpx 14rpx 0;
|
||||
}
|
||||
.filter-label {
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.filter-chip {
|
||||
display: flex;
|
||||
min-width: 82rpx;
|
||||
min-height: 52rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.filter-chip--active {
|
||||
color: $brand-red;
|
||||
font-weight: 700;
|
||||
}
|
||||
.search-divider {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 30rpx;
|
||||
margin: 24rpx 0 24rpx;
|
||||
}
|
||||
.search-status {
|
||||
display: flex;
|
||||
min-height: 240rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16rpx 28rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.search-status__cloud {
|
||||
width: 92rpx;
|
||||
height: 46rpx;
|
||||
margin-bottom: 4rpx;
|
||||
opacity: 0.78;
|
||||
}
|
||||
.search-status__lead {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.search-status__copy {
|
||||
margin-top: 12rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
.search-status__hall {
|
||||
width: 330rpx;
|
||||
height: 132rpx;
|
||||
margin-top: 210rpx;
|
||||
opacity: 0.22;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.status-retry {
|
||||
display: flex;
|
||||
width: 300rpx;
|
||||
min-height: 76rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 24rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.status-retry text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.search-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.search-results__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.search-results__head text:first-child {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.result-card {
|
||||
@include adaptive.adaptive-genealogy-list-card;
|
||||
min-height: 314rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx 30rpx;
|
||||
}
|
||||
.result-card__content {
|
||||
z-index: 1;
|
||||
}
|
||||
.result-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.result-card__name,
|
||||
.result-card__identity {
|
||||
display: block;
|
||||
}
|
||||
.result-card__name {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 33rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.result-card__identity {
|
||||
margin-top: 5rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.result-card__relation {
|
||||
flex: 0 0 auto;
|
||||
max-width: 190rpx;
|
||||
margin-left: 12rpx;
|
||||
color: #9a641f;
|
||||
font-size: 21rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.result-card__relation--rejected,
|
||||
.result-card__relation--removed {
|
||||
color: $brand-red;
|
||||
}
|
||||
.result-card__facts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8rpx 18rpx;
|
||||
margin-top: 16rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
.result-card__facts text:nth-child(4),
|
||||
.result-card__facts text:nth-child(5) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.result-card__action {
|
||||
display: flex;
|
||||
min-height: 58rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 14rpx;
|
||||
color: $brand-red;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.invite-block {
|
||||
padding: 24rpx 18rpx 0;
|
||||
}
|
||||
.invite-title {
|
||||
display: block;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.invite-copy {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 35rpx;
|
||||
}
|
||||
.invite-field {
|
||||
@include adaptive.adaptive-g06-search-field;
|
||||
display: grid;
|
||||
flex: 1;
|
||||
min-height: 95rpx;
|
||||
}
|
||||
.invite-input {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 30rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 25rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.invite-demo {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: #9b876f;
|
||||
font-size: 19rpx;
|
||||
}
|
||||
.invite-result__label {
|
||||
display: block;
|
||||
margin: 0 12rpx 12rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.invite-result__note {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 340px) {
|
||||
.search-page__content {
|
||||
padding-right: 20rpx;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
.search-field {
|
||||
width: 430rpx;
|
||||
}
|
||||
.result-card {
|
||||
padding-right: 24rpx;
|
||||
padding-left: 24rpx;
|
||||
}
|
||||
.result-card__facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.result-card__facts text {
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- 页面编号:G-06;用途:搜索公开家谱。公开列表 item DTO 未声明时保持关闭。 -->
|
||||
<template><view class="search-page"><GenealogyPageBackground /><view class="page-header"><PageHeader title="搜索家谱" custom-back @back="backToGenealogies" /></view><view class="page-content"><view class="state-card"><text>公开家谱搜索待后端字段合同</text><text>公开家谱读取没有声明家谱 ID、名称、地区、简介、成员数或可申请状态字段;页面不再展示 fixture 搜索结果,也不从猜测条目进入申请。</text><AppButton block label="返回我的家谱" @click="backToGenealogies" /></view></view></view></template>
|
||||
<script setup>import AppButton from "@/components/AppButton.vue";import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";import PageHeader from "@/components/PageHeader.vue";import {returnTo} from "@/utils/navigation.js";const backToGenealogies=()=>returnTo("G01");</script>
|
||||
<style scoped lang="scss">@use "../../styles/adaptive-frame-profiles.scss" as adaptive;.search-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-genealogy-state-panel}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}</style>
|
||||
|
||||
@@ -1,462 +1,4 @@
|
||||
<!-- 页面编号:G-08;用途:申请加入家谱与提交成功/失败状态。 -->
|
||||
<template>
|
||||
<view class="join-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="join-page__header">
|
||||
<PageHeader :title="sourceContract.headerTitle" custom-back @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="join-panel"
|
||||
:class="{
|
||||
'join-state--form': joinState === 'form',
|
||||
'join-state--success': joinState === 'success',
|
||||
'join-state--error': joinState === 'error',
|
||||
'join-state--ineligible': joinState === 'ineligible',
|
||||
}"
|
||||
>
|
||||
<view v-if="joinState === 'form'" class="join-form">
|
||||
<text class="join-form__eyebrow">{{ sourceContract.eyebrow }}</text>
|
||||
<text class="join-form__title"
|
||||
>{{ sourceContract.formTitle }} {{ genealogyName }}</text
|
||||
>
|
||||
<text class="join-form__copy">{{ sourceContract.formCopy }}</text>
|
||||
<view class="join-field">
|
||||
<text>真实姓名</text
|
||||
><input
|
||||
v-model="form.realName"
|
||||
placeholder="请输入真实姓名"
|
||||
placeholder-class="join-placeholder"
|
||||
@input="clearFieldError('realName')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.realName" class="join-field-error">{{
|
||||
fieldErrors.realName
|
||||
}}</text>
|
||||
<view class="join-field">
|
||||
<text>与家谱关系</text
|
||||
><input
|
||||
v-model="form.relation"
|
||||
placeholder="例如:某某某堂侄"
|
||||
placeholder-class="join-placeholder"
|
||||
@input="clearFieldError('relation')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.relation" class="join-field-error">{{
|
||||
fieldErrors.relation
|
||||
}}</text>
|
||||
<text class="relation-help"
|
||||
>请以家谱中一位已知长辈为参照,例如:某某某堂侄</text
|
||||
>
|
||||
<view class="join-field join-field--message">
|
||||
<text>{{ sourceContract.thirdFieldLabel }}</text
|
||||
><textarea
|
||||
v-model="form.message"
|
||||
auto-height
|
||||
maxlength="80"
|
||||
:placeholder="sourceContract.thirdFieldPlaceholder"
|
||||
placeholder-class="join-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<text class="join-form__note">{{ sourceContract.note }}</text>
|
||||
<view class="join-action" @click="submitJoin">
|
||||
<text>{{
|
||||
isSubmitting
|
||||
? sourceContract.submittingLabel
|
||||
: sourceContract.submitLabel
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="join-result">
|
||||
<text class="join-result__eyebrow">{{
|
||||
joinState === "success"
|
||||
? sourceContract.successEyebrow
|
||||
: joinState === "ineligible"
|
||||
? "当前不可申请"
|
||||
: sourceContract.errorEyebrow
|
||||
}}</text>
|
||||
<text class="join-result__title">{{
|
||||
joinState === "success"
|
||||
? sourceContract.successTitle
|
||||
: joinState === "ineligible"
|
||||
? ineligibleTitle
|
||||
: sourceContract.errorTitle
|
||||
}}</text>
|
||||
<text class="join-result__copy">{{ resultCopy }}</text>
|
||||
<view
|
||||
class="join-action"
|
||||
@click="
|
||||
joinState === 'success'
|
||||
? completeFlow()
|
||||
: joinState === 'ineligible'
|
||||
? handleIneligibleAction()
|
||||
: retryForm()
|
||||
"
|
||||
>
|
||||
<text>{{
|
||||
joinState === "success"
|
||||
? sourceContract.nextLabel
|
||||
: joinState === "ineligible"
|
||||
? ineligibleActionLabel
|
||||
: "重新填写"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃填写?"
|
||||
message="当前身份关系尚未保存,确认返回后将清空。"
|
||||
confirm-text="放弃并返回"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
findGenealogyFixture,
|
||||
getGenealogyFixtureAccess,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
goBack,
|
||||
goRoot,
|
||||
handleBackPress,
|
||||
openPage,
|
||||
returnTo,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const source = ref("search");
|
||||
const genealogyName = ref("这部家谱");
|
||||
const joinState = ref("form");
|
||||
const isSubmitting = ref(false);
|
||||
const errorMessage = ref("");
|
||||
const ineligibleRelation = ref("unknown");
|
||||
const discardVisible = ref(false);
|
||||
const form = reactive({ realName: "", relation: "", message: "" });
|
||||
const fieldErrors = reactive({ realName: "", relation: "" });
|
||||
let submitTimer = null;
|
||||
const isDirty = computed(() =>
|
||||
Boolean(form.realName || form.relation || form.message),
|
||||
);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
const sourceContract = computed(() =>
|
||||
source.value === "invite"
|
||||
? {
|
||||
headerTitle: "确认身份关系",
|
||||
eyebrow: "邀请码定位预览",
|
||||
formTitle: "核对家谱",
|
||||
formCopy:
|
||||
"请填写真实身份和亲属关系;当前仅验证页面流程,不会变更成员身份。",
|
||||
thirdFieldLabel: "补充信息",
|
||||
thirdFieldPlaceholder: "选填:补充祖居地、长辈姓名等信息",
|
||||
note: "后端尚未提供邀请码验证与直接入谱接口。",
|
||||
submitLabel: "完成本地校验",
|
||||
submittingLabel: "正在校验…",
|
||||
successEyebrow: "本地校验完成",
|
||||
successTitle: "信息尚未提交服务器",
|
||||
errorEyebrow: "校验未完成",
|
||||
errorTitle: "暂时无法完成校验",
|
||||
nextLabel: "返回我的家谱",
|
||||
successCopy: `本地流程预览已完成“${genealogyName.value}”的身份填写;返回后不会选中或加入这部家谱。`,
|
||||
}
|
||||
: {
|
||||
headerTitle: "申请加入家谱",
|
||||
eyebrow: "公开家谱入谱申请",
|
||||
formTitle: "申请加入",
|
||||
formCopy: "请填写真实身份和亲属关系,管理员审核后会通过消息告知结果。",
|
||||
thirdFieldLabel: "申请说明",
|
||||
thirdFieldPlaceholder: "补充祖居地、长辈姓名等核验信息",
|
||||
note: "当前仅验证页面流程,后端申请接口接入后才能正式提交。",
|
||||
submitLabel: "完成本地校验",
|
||||
submittingLabel: "正在校验…",
|
||||
successEyebrow: "本地校验完成",
|
||||
successTitle: "申请尚未提交服务器",
|
||||
errorEyebrow: "申请未提交",
|
||||
errorTitle: "暂时无法完成校验",
|
||||
nextLabel: "查看我的申请",
|
||||
successCopy: `本地流程预览已完成“${genealogyName.value}”的申请填写,当前不会新增审核记录。`,
|
||||
},
|
||||
);
|
||||
const resultCopy = computed(() =>
|
||||
joinState.value === "success"
|
||||
? sourceContract.value.successCopy
|
||||
: joinState.value === "ineligible"
|
||||
? ({
|
||||
owned: "这是你创建的家谱,无需重复提交加入申请。",
|
||||
joined: "你已经是这部家谱的成员,无需重复申请。",
|
||||
pending: "这部家谱已有待审核申请,请先查看申请进度。",
|
||||
})[ineligibleRelation.value] ||
|
||||
"当前家谱不存在、未公开或不可申请,请返回后重新选择。"
|
||||
: errorMessage.value ||
|
||||
"请检查网络后重新填写;未成功提交的内容不会进入审核列表。",
|
||||
);
|
||||
const ineligibleTitle = computed(
|
||||
() =>
|
||||
({
|
||||
owned: "你已拥有这部家谱",
|
||||
joined: "你已加入这部家谱",
|
||||
pending: "申请正在审核中",
|
||||
})[ineligibleRelation.value] || "无法打开申请表",
|
||||
);
|
||||
const ineligibleActionLabel = computed(
|
||||
() =>
|
||||
ineligibleRelation.value === "pending"
|
||||
? "查看申请进度"
|
||||
: ["owned", "joined"].includes(ineligibleRelation.value)
|
||||
? "返回我的家谱"
|
||||
: "返回上一页",
|
||||
);
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
source.value = query.source === "invite" ? "invite" : "search";
|
||||
if (!genealogyId.value) {
|
||||
ineligibleRelation.value = "unknown";
|
||||
joinState.value = "ineligible";
|
||||
return;
|
||||
}
|
||||
const fixture = findGenealogyFixture(genealogyId.value);
|
||||
genealogyName.value = fixture?.name || "这部家谱";
|
||||
const access = getGenealogyFixtureAccess(genealogyId.value);
|
||||
if (!fixture || !access.canApply) {
|
||||
ineligibleRelation.value = access.relation;
|
||||
joinState.value = "ineligible";
|
||||
return;
|
||||
}
|
||||
if (query.state === "success") joinState.value = "success";
|
||||
});
|
||||
|
||||
const requestBack = () => {
|
||||
if (joinState.value === "success") return completeFlow();
|
||||
return runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
};
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
const timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const submitJoin = () => {
|
||||
if (isSubmitting.value) return;
|
||||
fieldErrors.realName = form.realName.trim() ? "" : "请填写真实姓名";
|
||||
fieldErrors.relation = form.relation.trim() ? "" : "请填写与家谱的关系";
|
||||
if (fieldErrors.realName || fieldErrors.relation) return;
|
||||
const submitSnapshot = Object.freeze({ ...form });
|
||||
isSubmitting.value = true;
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
submitTimer = null;
|
||||
joinState.value =
|
||||
submitSnapshot.realName.trim() === "失败" ? "error" : "success";
|
||||
if (joinState.value === "error")
|
||||
errorMessage.value =
|
||||
source.value === "invite"
|
||||
? "邀请码加入暂未完成,请稍后重试。"
|
||||
: "申请暂未提交,请稍后重试。";
|
||||
isSubmitting.value = false;
|
||||
}, 280);
|
||||
submitTimer = timer;
|
||||
};
|
||||
const clearFieldError = (field) => {
|
||||
fieldErrors[field] = "";
|
||||
};
|
||||
const retryForm = () => {
|
||||
joinState.value = "form";
|
||||
errorMessage.value = "";
|
||||
};
|
||||
const handleIneligibleAction = () => {
|
||||
if (ineligibleRelation.value === "pending")
|
||||
return openPage("G09", { status: "pending" }, "G08");
|
||||
if (["owned", "joined"].includes(ineligibleRelation.value))
|
||||
return goRoot("G01", { genealogyId: genealogyId.value });
|
||||
return goBack();
|
||||
};
|
||||
const completeFlow = () =>
|
||||
source.value === "invite" ? returnTo("G01", {}) : returnTo("G09", {});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.join-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.join-page__header {
|
||||
z-index: 3;
|
||||
}
|
||||
.join-panel {
|
||||
@include adaptive.adaptive-genealogy-state-panel;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
min-height: min(590px, calc((100vw - 16px) * 1.337));
|
||||
margin: 22rpx auto 0;
|
||||
padding: 70rpx 8%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.join-form {
|
||||
min-width: 0;
|
||||
}
|
||||
.join-form__eyebrow,
|
||||
.join-result__eyebrow {
|
||||
display: block;
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.join-form__title,
|
||||
.join-result__title {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.join-form__copy,
|
||||
.join-result__copy {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.join-field {
|
||||
@include adaptive.adaptive-genealogy-form-field;
|
||||
display: grid;
|
||||
min-height: 92rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.join-form__previous {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.join-field > text {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
align-self: center;
|
||||
margin-left: 24rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.join-field input,
|
||||
.join-field textarea {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 92rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 170rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
line-height: 92rpx;
|
||||
}
|
||||
.join-field textarea {
|
||||
box-sizing: border-box;
|
||||
padding-top: 26rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.join-placeholder {
|
||||
color: #a79884;
|
||||
}
|
||||
.join-field-error {
|
||||
display: block;
|
||||
margin-top: 3rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.relation-help {
|
||||
display: block;
|
||||
margin-top: 5rpx;
|
||||
color: #8e7b67;
|
||||
font-size: 19rpx;
|
||||
line-height: 29rpx;
|
||||
}
|
||||
.join-form__note {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.join-action {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.join-action text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.join-result {
|
||||
text-align: center;
|
||||
}
|
||||
.join-state--success,
|
||||
.join-state--error {
|
||||
padding: 170rpx 12% 70rpx;
|
||||
}
|
||||
.join-result__eyebrow {
|
||||
text-align: center;
|
||||
}
|
||||
.join-result__copy {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.join-result .join-action {
|
||||
width: 420rpx;
|
||||
max-width: 100%;
|
||||
margin: 34rpx auto 0;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.join-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- 页面编号:G-08;用途:加入申请。公开家谱详情/权限投影不完整时保持关闭。 -->
|
||||
<template><view class="join-page"><GenealogyPageBackground /><view class="page-header"><PageHeader title="申请加入家谱" custom-back @back="backToSearch" /></view><view class="page-content"><view class="state-card"><text>加入申请待后端字段合同</text><text>申请提交 operation 不等于可安全发起申请:当前没有公开家谱详情、可申请权限或稳定家谱条目 ID 的消费投影。页面不再使用 fixture 家谱或本地申请成功提示。</text><AppButton block label="返回搜索家谱" @click="backToSearch" /></view></view></view></template>
|
||||
<script setup>import AppButton from "@/components/AppButton.vue";import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";import PageHeader from "@/components/PageHeader.vue";import {returnTo} from "@/utils/navigation.js";const backToSearch=()=>returnTo("G06");</script>
|
||||
<style scoped lang="scss">@use "../../styles/adaptive-frame-profiles.scss" as adaptive;.join-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-genealogy-state-panel}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}</style>
|
||||
|
||||
@@ -1,394 +1,11 @@
|
||||
<!-- 页面编号:G-09;用途:我的家谱申请列表与空/失败状态。 -->
|
||||
<!-- 页面编号:G-09;用途:我的申请。列表 DTO 未声明时不展示 fixture 申请。 -->
|
||||
<template>
|
||||
<view class="application-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="application-page__header">
|
||||
<PageHeader title="我的申请" custom-back @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="application-content"
|
||||
:class="{
|
||||
'application-state--list': applicationState === 'list',
|
||||
'application-state--empty': applicationState === 'empty',
|
||||
'application-state--error': applicationState === 'error',
|
||||
}"
|
||||
>
|
||||
<template v-if="applicationState === 'list'">
|
||||
<view class="application-intro">
|
||||
<text>入谱申请进度</text><text>审核结果会同步到消息中心</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in applications"
|
||||
:key="item.id"
|
||||
class="application-card"
|
||||
>
|
||||
<view class="application-card__body">
|
||||
<text class="application-card__name">{{ item.genealogyName }}</text>
|
||||
<text class="application-card__time">{{ item.appliedAt }}</text>
|
||||
<text class="application-card__relation">{{ item.relation }}</text>
|
||||
<text
|
||||
class="application-card__status"
|
||||
:class="`application-card__status--${item.status.toLowerCase()}`"
|
||||
>{{ statusLabel(item.status) }}</text
|
||||
>
|
||||
<text class="application-card__hint">{{
|
||||
statusHint(item.status)
|
||||
}}</text>
|
||||
<view
|
||||
v-if="actionFor(item)"
|
||||
class="application-card__action"
|
||||
@click="handleApplicationAction(item)"
|
||||
>{{ actionFor(item) }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<AppLoading
|
||||
v-else-if="applicationState === 'loading'"
|
||||
text="正在整理申请记录"
|
||||
description="请稍候,正在同步审核状态。"
|
||||
/>
|
||||
<view v-else class="application-state-card">
|
||||
<view class="application-state-card__copy">
|
||||
<text>{{ stateTitle }}</text>
|
||||
<text>{{ stateCopy }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="applicationState !== 'list' && applicationState !== 'loading'"
|
||||
class="application-page__action"
|
||||
@click="
|
||||
applicationState === 'error' ? loadApplications({}) : toSearch()
|
||||
"
|
||||
>
|
||||
<text>{{
|
||||
applicationState === "error" ? "重新查看" : "查找公开家谱"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="!!withdrawTarget"
|
||||
title="预览撤回效果"
|
||||
:message="
|
||||
withdrawTarget
|
||||
? `当前只更新本页对“${withdrawTarget.genealogyName}”的撤回预览,不会向服务器提交;真实申请仍可能处于审核中。`
|
||||
: ''
|
||||
"
|
||||
confirm-text="查看本地效果"
|
||||
cancel-text="暂不撤回"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmWithdraw"
|
||||
@cancel="cancelWithdraw"
|
||||
/>
|
||||
</view>
|
||||
<view class="applications-page"><GenealogyPageBackground /><view class="page-header"><PageHeader title="我的申请" custom-back @back="returnToGenealogies" /></view><view class="page-content"><view class="state-card"><text>申请列表待后端字段合同</text><text>申请读取响应没有声明申请 ID、家谱名称、申请时间、状态或审核备注字段;页面已停止展示本地申请和本地撤回结果。</text><AppButton block label="返回我的家谱" @click="returnToGenealogies" /></view></view></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onBackPress, onLoad } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { findGenealogyFixture } from "@/data/mock.js";
|
||||
import { handleBackPress, openPage, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const applications = ref([]);
|
||||
const applicationState = ref("loading");
|
||||
const errorMessage = ref("");
|
||||
const withdrawTarget = ref(null);
|
||||
const genealogyNameFor = (genealogyId) =>
|
||||
findGenealogyFixture(genealogyId)?.name || "未知家谱";
|
||||
const applicationSamples = [
|
||||
{
|
||||
id: "pending-2003",
|
||||
genealogyId: "2003",
|
||||
genealogyName: genealogyNameFor("2003"),
|
||||
relation: "自述为汤正华堂侄",
|
||||
appliedAt: "今天 10:24",
|
||||
status: "PENDING",
|
||||
},
|
||||
{
|
||||
id: "approved-1002",
|
||||
genealogyId: "1002",
|
||||
genealogyName: genealogyNameFor("1002"),
|
||||
relation: "祖居河南汝南",
|
||||
appliedAt: "昨天 18:02",
|
||||
status: "APPROVED",
|
||||
},
|
||||
{
|
||||
id: "rejected-2004",
|
||||
genealogyId: "2004",
|
||||
genealogyName: genealogyNameFor("2004"),
|
||||
relation: "补充材料不足",
|
||||
appliedAt: "7月12日 09:18",
|
||||
status: "REJECTED",
|
||||
},
|
||||
];
|
||||
|
||||
const statusLabel = (status) =>
|
||||
({
|
||||
PENDING: "审核中",
|
||||
APPROVED: "已通过",
|
||||
REJECTED: "未通过",
|
||||
LOCAL_WITHDRAWN: "本地撤回预览",
|
||||
})[status] || "状态未知";
|
||||
|
||||
const statusHint = (status) =>
|
||||
({
|
||||
PENDING: "管理员尚未处理,可在审核前撤回",
|
||||
APPROVED: "申请已通过,可进入这部家谱",
|
||||
REJECTED: "请修改关系说明后重新提交",
|
||||
LOCAL_WITHDRAWN: "尚未提交服务器,真实申请仍可能处于审核中",
|
||||
})[status] || "";
|
||||
|
||||
const actionFor = (item) =>
|
||||
({ PENDING: "预览撤回效果", APPROVED: "进入家谱", REJECTED: "修改后重新提交" })[
|
||||
item.status
|
||||
] || "";
|
||||
const stateTitle = computed(() =>
|
||||
applicationState.value === "empty"
|
||||
? "还没有入谱申请"
|
||||
: applicationState.value === "loading"
|
||||
? "正在整理申请记录"
|
||||
: "申请记录暂时无法读取",
|
||||
);
|
||||
const stateCopy = computed(() =>
|
||||
applicationState.value === "empty"
|
||||
? "从家谱搜索提交的申请会显示在这里;邀请码本地校验不会生成申请记录。"
|
||||
: applicationState.value === "loading"
|
||||
? "请稍候,正在同步审核状态。"
|
||||
: errorMessage.value || "请检查网络后重新查看。",
|
||||
);
|
||||
|
||||
const loadApplications = (query = {}) => {
|
||||
applicationState.value = "loading";
|
||||
errorMessage.value = "";
|
||||
if (query.state === "empty") {
|
||||
applicationState.value = "empty";
|
||||
return;
|
||||
}
|
||||
if (query.state === "error") {
|
||||
applicationState.value = "error";
|
||||
return;
|
||||
}
|
||||
if (query.state === "loading") {
|
||||
applicationState.value = "loading";
|
||||
return;
|
||||
}
|
||||
applications.value = applicationSamples.map((item) => ({ ...item }));
|
||||
if (query.status) {
|
||||
const requestedStatus = String(query.status).toUpperCase();
|
||||
applications.value = applications.value.filter(
|
||||
(item) => item.status === requestedStatus,
|
||||
);
|
||||
if (!applications.value.length) applicationState.value = "empty";
|
||||
}
|
||||
if (applicationState.value !== "empty") applicationState.value = "list";
|
||||
};
|
||||
|
||||
onLoad(loadApplications);
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: Boolean(withdrawTarget.value),
|
||||
"close-transient": cancelWithdraw,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const handleApplicationAction = (item) => {
|
||||
if (item.status === "APPROVED")
|
||||
return openPage(
|
||||
"G05",
|
||||
{ genealogyId: String(item.genealogyId) },
|
||||
"G09",
|
||||
);
|
||||
if (item.status === "REJECTED")
|
||||
return openPage(
|
||||
"G08",
|
||||
{ genealogyId: String(item.genealogyId), source: "search" },
|
||||
"G09",
|
||||
);
|
||||
if (item.status === "PENDING") withdrawTarget.value = item;
|
||||
};
|
||||
const cancelWithdraw = () => {
|
||||
withdrawTarget.value = null;
|
||||
};
|
||||
const confirmWithdraw = () => {
|
||||
const target = applications.value.find(
|
||||
(item) => item.id === withdrawTarget.value?.id,
|
||||
);
|
||||
if (target) target.status = "LOCAL_WITHDRAWN";
|
||||
cancelWithdraw();
|
||||
};
|
||||
const toSearch = () => openPage("G06", {}, "G09");
|
||||
import AppButton from "@/components/AppButton.vue"; import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue"; import PageHeader from "@/components/PageHeader.vue"; import { returnTo } from "@/utils/navigation.js"; const returnToGenealogies=()=>returnTo("G01");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
|
||||
.application-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.application-page__header {
|
||||
z-index: 3;
|
||||
}
|
||||
.application-content {
|
||||
z-index: 2;
|
||||
padding: 28rpx 24rpx 60rpx;
|
||||
}
|
||||
.application-intro {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin: 0 8rpx 20rpx;
|
||||
}
|
||||
.application-intro text:first-child {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.application-intro text:last-child {
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.application-card {
|
||||
@include adaptive.adaptive-genealogy-list-card;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 210rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.application-card__body {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 174rpx;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8rpx 14rpx;
|
||||
padding: 26rpx 28rpx 22rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.application-card__name {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.application-card__time {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
margin-top: 3rpx;
|
||||
color: #998873;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.application-card__relation {
|
||||
display: block;
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
margin-top: 0;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.application-card__status {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
margin-right: 3%;
|
||||
color: $brand-red;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.application-card__status--approved {
|
||||
color: #537368;
|
||||
}
|
||||
.application-card__status--rejected {
|
||||
color: #7e6f62;
|
||||
}
|
||||
.application-card__hint {
|
||||
grid-row: 3;
|
||||
grid-column: 1;
|
||||
align-self: center;
|
||||
justify-self: start;
|
||||
margin-bottom: 4rpx;
|
||||
color: #766653;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.application-card__action {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
grid-row: 3;
|
||||
grid-column: 2;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
min-height: 54rpx;
|
||||
align-items: center;
|
||||
margin-right: 2%;
|
||||
margin-bottom: 0;
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.application-state-card {
|
||||
@include adaptive.adaptive-genealogy-list-card;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 228rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 80rpx;
|
||||
padding: 48rpx 12%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.application-state-card__copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.application-state-card__copy text:first-child {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.application-state-card__copy text:last-child {
|
||||
margin-top: 16rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.application-page__action {
|
||||
display: flex;
|
||||
width: 420rpx;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 34rpx auto 0;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.application-page__action text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.applications-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-genealogy-state-panel}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}
|
||||
</style>
|
||||
|
||||
@@ -1,478 +1,11 @@
|
||||
<!-- 页面编号:G-10;用途:管理员入谱申请审核与空/失败状态。 -->
|
||||
<!-- 页面编号:G-10;用途:加入申请审核。缺申请列表 DTO/ID 来源时保持关闭。 -->
|
||||
<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>
|
||||
<view class="review-page"><GenealogyPageBackground /><view class="page-header"><PageHeader title="申请审核" custom-back @back="returnToGenealogies" /></view><view class="page-content"><view class="state-card"><text>申请审核待后端字段合同</text><text>审核 operation 存在,但没有可消费的申请列表 DTO 或稳定申请 ID 来源;页面不再展示 fixture 申请或本地通过/拒绝预览。</text><AppButton block label="返回我的家谱" @click="returnToGenealogies" /></view></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;
|
||||
};
|
||||
import AppButton from "@/components/AppButton.vue"; import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue"; import PageHeader from "@/components/PageHeader.vue"; import { returnTo } from "@/utils/navigation.js"; const returnToGenealogies=()=>returnTo("G01");
|
||||
</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;
|
||||
}
|
||||
.review-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-genealogy-state-panel}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}
|
||||
</style>
|
||||
|
||||
@@ -1,485 +1,11 @@
|
||||
<!-- 页面编号:G-11;用途:家谱设置、访问预设与家谱简介(页面设计阶段使用本地模拟交互)。 -->
|
||||
<!-- 页面编号:G-11;用途:家谱设置。缺可靠读取 DTO 与封面上传 owner 时保持关闭。 -->
|
||||
<template>
|
||||
<view class="settings-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="settings-page__header">
|
||||
<PageHeader title="家谱设置" custom-back @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="settings-panel"
|
||||
:class="{
|
||||
'settings-state--form': settingsState === 'form',
|
||||
'settings-state--success': settingsState === 'success',
|
||||
'settings-state--error': settingsState === 'error',
|
||||
'settings-state--no-permission': settingsState === 'no-permission',
|
||||
}"
|
||||
>
|
||||
<AppLoading
|
||||
v-if="settingsState === 'loading'"
|
||||
text="正在读取家谱设置"
|
||||
description="请稍候,正在准备名称与访问规则。"
|
||||
/>
|
||||
<view v-else-if="settingsState === 'form'" class="settings-form">
|
||||
<text class="settings-form__eyebrow">谱主可见 · 基础设置</text>
|
||||
<text class="settings-form__title">完善家谱访问规则</text>
|
||||
<text class="settings-form__copy"
|
||||
>这里维护名称、访问规则与家谱简介;访问规则会同时决定公开范围和加入方式,管理权转让在成员场景中单独处理。</text
|
||||
>
|
||||
|
||||
<view class="settings-field">
|
||||
<text>家谱名称</text>
|
||||
<input
|
||||
v-model="genealogyDraft.name"
|
||||
maxlength="20"
|
||||
placeholder="请输入家谱名称"
|
||||
placeholder-class="settings-placeholder"
|
||||
@input="nameError = ''"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="nameError" class="settings-field-error">{{
|
||||
nameError
|
||||
}}</text>
|
||||
|
||||
<view class="visibility-block">
|
||||
<text class="visibility-block__label">访问规则</text>
|
||||
<view class="visibility-options">
|
||||
<view
|
||||
v-for="option in accessPresetOptions"
|
||||
:key="option.value"
|
||||
class="visibility-option"
|
||||
@click="genealogyDraft.accessPreset = option.value"
|
||||
>
|
||||
<image
|
||||
:src="
|
||||
genealogyDraft.accessPreset === option.value
|
||||
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
|
||||
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
|
||||
"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text
|
||||
:class="{
|
||||
'visibility-option__text--active':
|
||||
genealogyDraft.accessPreset === option.value,
|
||||
}"
|
||||
>{{ option.label }}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<text class="visibility-block__hint">{{ visibilityHint }}</text>
|
||||
</view>
|
||||
|
||||
<view class="settings-field settings-field--note">
|
||||
<text>家谱简介</text>
|
||||
<textarea
|
||||
v-model="genealogyDraft.intro"
|
||||
auto-height
|
||||
maxlength="80"
|
||||
placeholder="简要介绍家谱来源与支系"
|
||||
placeholder-class="settings-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="settings-action" @click="saveSettings">
|
||||
<text>保存设置</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="settings-result">
|
||||
<text class="settings-result__eyebrow">{{
|
||||
settingsState === "success"
|
||||
? "设置已保存"
|
||||
: settingsState === "no-permission"
|
||||
? "权限不足"
|
||||
: "设置未保存"
|
||||
}}</text>
|
||||
<text class="settings-result__title">{{
|
||||
settingsState === "success"
|
||||
? "本页设置草稿已更新"
|
||||
: settingsState === "no-permission"
|
||||
? "当前账号不能修改家谱"
|
||||
: "暂时无法打开家谱设置"
|
||||
}}</text>
|
||||
<text class="settings-result__copy">{{
|
||||
settingsState === "success"
|
||||
? `本地预览已更新,尚未提交服务器;当前只保留在本页,返回总览不会改变原资料 · ${genealogyDraft.name} · ${accessPresetLabel}`
|
||||
: settingsState === "no-permission"
|
||||
? "只有家谱所有者可以修改名称、访问规则和家谱简介。"
|
||||
: "请从家谱总览重新进入,当前修改不会被保留。"
|
||||
}}</text>
|
||||
<view class="settings-action" @click="handleResultAction">
|
||||
<text>{{
|
||||
settingsState === "success" ? "继续调整" : "重新查看"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃设置修改?"
|
||||
message="当前修改尚未保存,确认返回后将清空。"
|
||||
confirm-text="放弃并返回"
|
||||
cancel-text="继续修改"
|
||||
show-cancel
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
<view v-if="feedbackVisible" class="settings-feedback">
|
||||
<text>本页草稿已更新</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-page"><GenealogyPageBackground /><view class="page-header"><PageHeader title="家谱设置" custom-back @back="returnToOverview" /></view><view class="page-content"><view class="state-card"><text>家谱设置待字段合同</text><text>更新 operation 已声明名称、姓氏、祠堂、地区、简介、可见性等字段,但当前没有可消费的概览 DTO 来安全预填,也没有封面 `coverOssId` 的上传闭环。页面不再以 fixture 编辑或本地预览保存。</text><AppButton block :label="hasValidContext ? '返回家谱总览' : '返回上一页'" @click="returnToOverview" /></view></view></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
findGenealogyFixture,
|
||||
getGenealogyFixtureAccess,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
GENEALOGY_ACCESS_PRESET,
|
||||
GENEALOGY_ACCESS_PRESET_OPTIONS,
|
||||
isGenealogyAccessPreset,
|
||||
} from "@/utils/genealogy-contracts.js";
|
||||
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const settingsState = ref("loading");
|
||||
const nameError = ref("");
|
||||
const feedbackVisible = ref(false);
|
||||
const discardVisible = ref(false);
|
||||
let feedbackTimer = null;
|
||||
const genealogyDraft = reactive({
|
||||
name: "",
|
||||
accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY,
|
||||
intro: "",
|
||||
});
|
||||
const originalDraft = ref("");
|
||||
const isDirty = computed(
|
||||
() =>
|
||||
settingsState.value === "form" &&
|
||||
JSON.stringify(genealogyDraft) !== originalDraft.value,
|
||||
);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
const accessPresetOptions = GENEALOGY_ACCESS_PRESET_OPTIONS;
|
||||
const accessPresetLabel = computed(
|
||||
() =>
|
||||
accessPresetOptions.find((item) => item.value === genealogyDraft.accessPreset)
|
||||
?.label || "",
|
||||
);
|
||||
const visibilityHint = computed(() =>
|
||||
genealogyDraft.accessPreset === GENEALOGY_ACCESS_PRESET.MEMBER_ONLY
|
||||
? "只有已加入本家谱的成员可以查看谱系和家族资料。"
|
||||
: "访客可检索到家谱并提交入谱申请,资料仍需审核后查看。",
|
||||
);
|
||||
|
||||
const loadSettings = (query = {}) => {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
if (!genealogyId.value) {
|
||||
settingsState.value = "error";
|
||||
return;
|
||||
}
|
||||
if (
|
||||
getGenealogyFixtureAccess(genealogyId.value).accessRole !== "owner"
|
||||
) {
|
||||
settingsState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
const fixture = findGenealogyFixture(genealogyId.value);
|
||||
if (!fixture) {
|
||||
settingsState.value = "error";
|
||||
return;
|
||||
}
|
||||
if (!isGenealogyAccessPreset(fixture.accessPreset)) {
|
||||
settingsState.value = "error";
|
||||
return;
|
||||
}
|
||||
Object.assign(genealogyDraft, {
|
||||
name: fixture.name,
|
||||
accessPreset: fixture.accessPreset,
|
||||
intro: fixture.publicDescription || "家族资料,请妥善保存",
|
||||
});
|
||||
originalDraft.value = JSON.stringify(genealogyDraft);
|
||||
settingsState.value =
|
||||
query.state === "loading"
|
||||
? "loading"
|
||||
: query.state === "success"
|
||||
? "success"
|
||||
: query.state === "no-permission"
|
||||
? "no-permission"
|
||||
: query.state === "error" || !genealogyId.value
|
||||
? "error"
|
||||
: "form";
|
||||
};
|
||||
|
||||
onLoad(loadSettings);
|
||||
onUnload(() => {
|
||||
const timer = feedbackTimer;
|
||||
feedbackTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const continueEditing = () => {
|
||||
if (
|
||||
getGenealogyFixtureAccess(genealogyId.value).accessRole === "owner"
|
||||
) {
|
||||
settingsState.value = "form";
|
||||
}
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (settingsState.value === "success") return continueEditing();
|
||||
if (settingsState.value === "error")
|
||||
return loadSettings({ genealogyId: genealogyId.value });
|
||||
return goBack();
|
||||
};
|
||||
|
||||
const saveSettings = () => {
|
||||
if (!genealogyDraft.name.trim()) {
|
||||
nameError.value = "请填写家谱名称";
|
||||
return;
|
||||
}
|
||||
if (genealogyDraft.name.trim().length > 20) {
|
||||
nameError.value = "家谱名称不能超过 20 个字";
|
||||
return;
|
||||
}
|
||||
originalDraft.value = JSON.stringify(genealogyDraft);
|
||||
settingsState.value = "success";
|
||||
feedbackVisible.value = true;
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
const timer = setTimeout(() => {
|
||||
if (feedbackTimer !== timer) return;
|
||||
feedbackVisible.value = false;
|
||||
feedbackTimer = null;
|
||||
}, 1800);
|
||||
feedbackTimer = timer;
|
||||
};
|
||||
import { computed,ref } from "vue"; import { onLoad } from "@dcloudio/uni-app"; import AppButton from "@/components/AppButton.vue"; import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue"; import PageHeader from "@/components/PageHeader.vue"; import { goBack,returnTo } from "@/utils/navigation.js"; const genealogyId=ref("");const hasValidContext=computed(()=>/^[1-9]\d*$/.test(genealogyId.value));onLoad((query)=>{genealogyId.value=String(query?.genealogyId||"");});const returnToOverview=()=>hasValidContext.value?returnTo("G05",{genealogyId:genealogyId.value}):goBack();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.settings-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.settings-page__header {
|
||||
z-index: 3;
|
||||
}
|
||||
.settings-panel {
|
||||
@include adaptive.adaptive-genealogy-state-panel;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
margin: 18rpx auto 0;
|
||||
padding: 70rpx 8%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.settings-panel > .app-loading {
|
||||
grid-area: 1 / 1 / -1 / -1;
|
||||
}
|
||||
.settings-form {
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-form__eyebrow,
|
||||
.settings-result__eyebrow {
|
||||
display: block;
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.settings-form__title,
|
||||
.settings-result__title {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.settings-form__copy,
|
||||
.settings-result__copy {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.settings-field {
|
||||
@include adaptive.adaptive-genealogy-form-field;
|
||||
display: grid;
|
||||
min-height: 92rpx;
|
||||
margin-top: 17rpx;
|
||||
}
|
||||
.settings-field > text {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
align-self: center;
|
||||
margin-left: 24rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.settings-field input,
|
||||
.settings-field textarea {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 92rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 164rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
line-height: 92rpx;
|
||||
}
|
||||
.settings-field textarea {
|
||||
box-sizing: border-box;
|
||||
padding-top: 26rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.settings-placeholder {
|
||||
color: #a79884;
|
||||
}
|
||||
.settings-field-error {
|
||||
display: block;
|
||||
margin-top: 3rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.visibility-block {
|
||||
margin-top: 17rpx;
|
||||
}
|
||||
.visibility-block__label {
|
||||
display: block;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.visibility-options {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.visibility-option {
|
||||
display: grid;
|
||||
width: calc(50% - 7rpx);
|
||||
min-height: 64rpx;
|
||||
}
|
||||
.visibility-option image {
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.visibility-option text {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.visibility-option .visibility-option__text--active {
|
||||
color: #fff9ed;
|
||||
}
|
||||
.visibility-block__hint {
|
||||
display: block;
|
||||
margin-top: 9rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.settings-field--note {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.settings-action {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 78rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 19rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.settings-action text {
|
||||
z-index: 1;
|
||||
color: #fff9ed;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.settings-result {
|
||||
text-align: center;
|
||||
}
|
||||
.settings-state--success,
|
||||
.settings-state--error,
|
||||
.settings-state--no-permission {
|
||||
padding: 180rpx 12% 70rpx;
|
||||
}
|
||||
.settings-result__eyebrow {
|
||||
text-align: center;
|
||||
}
|
||||
.settings-result__copy {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.settings-result .settings-action {
|
||||
width: 420rpx;
|
||||
max-width: 100%;
|
||||
margin: 34rpx auto 0;
|
||||
}
|
||||
.settings-feedback {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
top: 138rpx;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
min-width: 240rpx;
|
||||
min-height: 74rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 34rpx;
|
||||
transform: translateX(-50%);
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.settings-feedback text {
|
||||
z-index: 1;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.settings-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
}
|
||||
.settings-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-genealogy-state-panel}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}
|
||||
</style>
|
||||
|
||||
@@ -1,176 +1,76 @@
|
||||
<!-- 页面编号:G-12;用途:字辈诗列表、空状态与批量维护(页面设计阶段使用本地模拟交互)。 -->
|
||||
<!-- 页面编号:G-12;用途:按 Apifox GenerationPoemView 读取字辈,并以服务端预览维护。 -->
|
||||
<template>
|
||||
<view class="poem-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="poem-page__header"
|
||||
><PageHeader
|
||||
:title="pageTitle"
|
||||
:action="canManage && poemState === 'list' ? '维护' : ''"
|
||||
custom-back
|
||||
@action="openEditor"
|
||||
@back="requestBack"
|
||||
/></view>
|
||||
<view class="poem-page__header">
|
||||
<PageHeader :title="pageTitle" :action="poemState === 'list' || poemState === 'empty' ? '维护' : ''" custom-back @action="enterManagement" @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view class="poem-panel" :class="`poem-state--${poemState}`">
|
||||
<AppLoading v-if="poemState === 'loading'" text="正在读取字辈诗" description="正在读取后端已声明的世代、字辈和状态字段。" />
|
||||
|
||||
<view
|
||||
class="poem-panel"
|
||||
:class="{
|
||||
'poem-state--list': poemState === 'list',
|
||||
'poem-state--empty': poemState === 'empty',
|
||||
'poem-state--edit': poemState === 'edit',
|
||||
'poem-state--error': poemState === 'error',
|
||||
'poem-state--no-permission': poemState === 'no-permission',
|
||||
}"
|
||||
>
|
||||
<AppLoading
|
||||
v-if="poemState === 'loading'"
|
||||
text="正在整理字辈诗"
|
||||
description="请稍候,正在读取家谱字序。"
|
||||
/>
|
||||
<view v-else-if="poemState === 'list'" class="poem-list">
|
||||
<text class="poem-list__eyebrow">{{ genealogyName }} · 传承字序</text>
|
||||
<text class="poem-list__title">{{ generationRangeTitle }}</text>
|
||||
<text class="poem-list__copy">{{ currentGenerationCopy }}</text>
|
||||
<text class="poem-list__copy">接口未声明“当前世代”字段,页面只按后端返回的世代、文字和状态展示。</text>
|
||||
<view class="poem-rows">
|
||||
<view
|
||||
v-for="item in visiblePoemRows"
|
||||
:key="item.generationNo"
|
||||
class="poem-row"
|
||||
:class="{
|
||||
'poem-row--current': item.current,
|
||||
'poem-row--disabled': item.status === GENERATION_POEM_STATUS.DISABLED,
|
||||
}"
|
||||
>
|
||||
<view v-for="item in visiblePoemRows" :key="item.poemId" class="poem-row" :class="{ 'poem-row--disabled': item.status === GENERATION_POEM_STATUS.DISABLED }">
|
||||
<text class="poem-row__number">第 {{ item.generationNo }} 世</text>
|
||||
<text class="poem-row__character">{{ item.generationText }}</text>
|
||||
<text class="poem-row__status">{{
|
||||
item.current
|
||||
? "当前字辈"
|
||||
: item.status === GENERATION_POEM_STATUS.DISABLED
|
||||
? "已停用·记录保留"
|
||||
: "传承字序"
|
||||
}}</text>
|
||||
<text class="poem-row__status">{{ item.status === GENERATION_POEM_STATUS.DISABLED ? '已停用·记录保留' : '传承字序' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="remainingPoemCount > 0"
|
||||
class="poem-load-more"
|
||||
@click="loadMorePoems"
|
||||
>
|
||||
<view v-if="remainingPoemCount > 0" class="poem-load-more" @click="loadMorePoems">
|
||||
<text>继续加载后续字辈(剩余 {{ remainingPoemCount }} 代)</text>
|
||||
</view>
|
||||
<view v-if="canManage" class="poem-action" @click="openEditor">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>维护字辈诗</text>
|
||||
<view class="poem-action" @click="enterManagement">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /><text>{{ canManage ? '继续维护字辈诗' : '读取维护列表' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="poemState === 'edit'" class="poem-editor">
|
||||
<text class="poem-list__eyebrow">批量维护</text>
|
||||
<text class="poem-list__eyebrow">服务端批量维护</text>
|
||||
<text class="poem-list__title">录入完整字辈序列</text>
|
||||
<text class="poem-list__copy"
|
||||
>无分隔符时每个字符对应一代;也可用空格、逗号、分号、顿号、斜杠或竖线分隔多字字辈。一次最多
|
||||
{{ MAX_GENERATION_COUNT }} 代,每代最多
|
||||
{{ MAX_GENERATION_TEXT_LENGTH }} 个字符。本页只更新本地预览,尚未提交服务器。</text
|
||||
>
|
||||
<text class="poem-list__copy">提交前会先请求服务端批量预览。无分隔符时每个字符对应一代;也可用空格、逗号、分号、顿号、斜杠或竖线分隔多字字辈。</text>
|
||||
<view class="poem-field">
|
||||
<text>字辈内容</text>
|
||||
<textarea
|
||||
v-model="poemDraft"
|
||||
auto-height
|
||||
:maxlength="MAX_GENERATION_POEM_INPUT_LENGTH * 2"
|
||||
placeholder="例如:启宗敦本,或 克勤 克俭 承先 启后"
|
||||
placeholder-class="poem-placeholder"
|
||||
@input="poemError = ''"
|
||||
/>
|
||||
<textarea v-model="poemDraft" auto-height :maxlength="MAX_GENERATION_POEM_INPUT_LENGTH" placeholder="例如:启宗敦本,或 克勤 克俭 承先 启后" placeholder-class="poem-placeholder" @input="invalidatePreview" />
|
||||
</view>
|
||||
<text v-if="poemError" class="poem-field-error">{{ poemError }}</text>
|
||||
<view class="poem-policy">
|
||||
<text>未被新文本覆盖的后续世代</text>
|
||||
<view
|
||||
class="poem-policy__option"
|
||||
@click="disableMissing = !disableMissing"
|
||||
>
|
||||
<image
|
||||
:src="
|
||||
disableMissing
|
||||
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
|
||||
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
|
||||
"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text
|
||||
:class="{
|
||||
'poem-policy__option-text--active': disableMissing,
|
||||
}"
|
||||
>{{ disableMissing ? "停用并保留记录" : "保持原状态" }}</text
|
||||
>
|
||||
<view class="poem-policy__option" @click="toggleDisableMissing">
|
||||
<image :src="disableMissing ? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png' : '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'" mode="aspectFit" />
|
||||
<text :class="{ 'poem-policy__option-text--active': disableMissing }">{{ disableMissing ? '停用并保留记录' : '保持原状态' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="poem-preview"
|
||||
>预览:{{ previewSummary }}</text
|
||||
>
|
||||
<text class="poem-preview">{{ previewSummary }}</text>
|
||||
<view class="poem-editor__actions">
|
||||
<view class="poem-action" @click="requestLeaveEditor">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text class="poem-action__secondary">取消</text>
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png" mode="aspectFit" /><text class="poem-action__secondary">取消</text>
|
||||
</view>
|
||||
<view class="poem-action" @click="savePoems">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>保存字辈</text>
|
||||
<view class="poem-action" :class="{ 'poem-action--disabled': previewing || saving }" @click="previewPoems">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png" mode="aspectFit" /><text class="poem-action__secondary">{{ previewing ? '正在预览' : '服务端预览' }}</text>
|
||||
</view>
|
||||
<view class="poem-action" :class="{ 'poem-action--disabled': !canSave || saving }" @click="savePoems">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /><text>{{ saving ? '正在保存' : '保存字辈' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="poem-state-card">
|
||||
<text class="poem-list__eyebrow">{{
|
||||
poemState === "empty"
|
||||
? "尚未建立字辈"
|
||||
: poemState === "no-permission"
|
||||
? "权限不足"
|
||||
: "字辈暂不可用"
|
||||
}}</text>
|
||||
<text class="poem-list__title">{{
|
||||
poemState === "empty"
|
||||
? "从第一段传承字序开始"
|
||||
: poemState === "no-permission"
|
||||
? "当前账号不能维护字辈"
|
||||
: "暂时无法读取字辈诗"
|
||||
}}</text>
|
||||
<text class="poem-list__copy">{{
|
||||
poemState === "empty"
|
||||
? "管理员可以一次录入连续字辈,系统会按世代拆分展示。"
|
||||
: poemState === "no-permission"
|
||||
? "普通成员可以查看字辈,但只有具备管理权限的成员可以维护。"
|
||||
: "请从家谱总览重新进入,或稍后再试。"
|
||||
}}</text>
|
||||
<view
|
||||
v-if="poemState !== 'empty' || canManage"
|
||||
class="poem-action"
|
||||
@click="handleStateAction"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>{{
|
||||
poemState === "empty"
|
||||
? "开始录入"
|
||||
: poemState === "no-permission"
|
||||
? "返回家谱总览"
|
||||
: "重新查看"
|
||||
}}</text>
|
||||
<text class="poem-list__eyebrow">{{ poemState === 'empty' ? '尚未建立字辈' : '字辈暂不可用' }}</text>
|
||||
<text class="poem-list__title">{{ poemState === 'empty' ? '当前读取没有正常状态字辈' : '暂时无法读取字辈诗' }}</text>
|
||||
<text class="poem-list__copy">{{ stateCopy }}</text>
|
||||
<view class="poem-action" @click="handleStateAction">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /><text>{{ poemState === 'empty' ? '读取维护列表' : '重新读取' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃字辈修改?"
|
||||
message="当前字辈草稿尚未保存,确认后将恢复进入编辑器前的内容。"
|
||||
message="当前草稿尚未保存,确认后不会提交服务端。"
|
||||
confirm-text="放弃修改"
|
||||
cancel-text="继续编辑"
|
||||
show-cancel
|
||||
@@ -179,9 +79,7 @@
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
<view v-if="feedbackVisible" class="poem-feedback">
|
||||
<text>本地字辈预览已更新</text>
|
||||
</view>
|
||||
<view v-if="feedbackMessage" class="poem-feedback"><text>{{ feedbackMessage }}</text></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -192,85 +90,61 @@ import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
findGenealogyFixture,
|
||||
getGenealogyFixtureAccess,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import {
|
||||
MAX_GENERATION_COUNT,
|
||||
MAX_GENERATION_POEM_INPUT_LENGTH,
|
||||
MAX_GENERATION_TEXT_LENGTH,
|
||||
GENERATION_POEM_STATUS,
|
||||
findFirstGenerationGap,
|
||||
mergeGenerationPoemRows,
|
||||
validateGenerationPoemText,
|
||||
} from "@/utils/generation-poem.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const INITIAL_POEM_TEXT = "启宗敦本";
|
||||
const POEM_RENDER_BATCH_SIZE = 50;
|
||||
const PREVIEW_GENERATION_LIMIT = 12;
|
||||
const LOCAL_PREVIEW_START_GENERATION = 1;
|
||||
const LOCAL_CURRENT_GENERATION = 3;
|
||||
const genealogyId = ref("");
|
||||
const genealogyName = ref("家谱");
|
||||
const genealogyName = ref("当前家谱");
|
||||
const poemState = ref("loading");
|
||||
const poemError = ref("");
|
||||
const feedbackVisible = ref(false);
|
||||
const feedbackMessage = ref("");
|
||||
const discardVisible = ref(false);
|
||||
const canManage = ref(false);
|
||||
let feedbackTimer = null;
|
||||
const poemDraft = ref(INITIAL_POEM_TEXT);
|
||||
// OpenAPI 的 disableMissing 示例为 false;本地同样采用安全默认 false。停用后续世代是高影响动作,
|
||||
// 必须由谱主主动选择,不能把缩短一次本地草稿解释为默认停用历史记录。
|
||||
const previewing = ref(false);
|
||||
const saving = ref(false);
|
||||
const poemDraft = ref("");
|
||||
const disableMissing = ref(false);
|
||||
const editorOrigin = ref("list");
|
||||
const editorSnapshot = ref(null);
|
||||
const poemRows = ref([]);
|
||||
const visiblePoemCount = ref(POEM_RENDER_BATCH_SIZE);
|
||||
const visiblePoemRows = computed(() =>
|
||||
poemRows.value.slice(0, visiblePoemCount.value),
|
||||
);
|
||||
const remainingPoemCount = computed(() =>
|
||||
poemRows.value.length > visiblePoemCount.value
|
||||
? poemRows.value.length - visiblePoemCount.value
|
||||
: 0,
|
||||
);
|
||||
const pageTitle = computed(() =>
|
||||
poemState.value === "edit" ? "维护字辈诗" : "字辈诗",
|
||||
);
|
||||
const preview = ref(null);
|
||||
const previewSignature = ref("");
|
||||
const requestController = createRequestController();
|
||||
let requestSequence = 0;
|
||||
|
||||
const visiblePoemRows = computed(() => poemRows.value.slice(0, visiblePoemCount.value));
|
||||
const remainingPoemCount = computed(() => Math.max(0, poemRows.value.length - visiblePoemCount.value));
|
||||
const pageTitle = computed(() => poemState.value === "edit" ? "维护字辈诗" : "字辈诗");
|
||||
const generationRangeTitle = computed(() => {
|
||||
if (!poemRows.value.length) return "尚未建立字辈";
|
||||
const first = poemRows.value[0].generationNo;
|
||||
const last = poemRows.value[poemRows.value.length - 1].generationNo;
|
||||
return first === last ? `第 ${first} 世字辈` : `第 ${first}—${last} 世字辈`;
|
||||
});
|
||||
const currentGenerationCopy = computed(() => {
|
||||
const current = poemRows.value.find((item) => item.current);
|
||||
return current
|
||||
? `当前为第 ${current.generationNo} 世“${current.generationText}”字辈。`
|
||||
: `当前第 ${LOCAL_CURRENT_GENERATION} 世尚未被有效字辈覆盖。`;
|
||||
});
|
||||
const draftSignature = computed(() => `${poemDraft.value}\u0000${disableMissing.value}`);
|
||||
const canSave = computed(() => Boolean(preview.value) && previewSignature.value === draftSignature.value);
|
||||
const previewSummary = computed(() => {
|
||||
const validation = validateGenerationPoemText(poemDraft.value);
|
||||
if (!validation.valid) {
|
||||
return poemDraft.value.trim() ? validation.message : "尚未录入字辈";
|
||||
}
|
||||
const visible = validation.generations
|
||||
.slice(0, PREVIEW_GENERATION_LIMIT)
|
||||
.join(" · ");
|
||||
const remaining = validation.generations.length - PREVIEW_GENERATION_LIMIT;
|
||||
return remaining > 0 ? `${visible} · …另 ${remaining} 代` : visible;
|
||||
if (previewing.value) return "正在请求服务端预览。";
|
||||
if (!preview.value) return "尚未请求服务端预览;预览成功后才可保存。";
|
||||
return `服务端预览:新增 ${preview.value.createCount} 条,更新 ${preview.value.updateCount} 条,保留 ${preview.value.keepCount} 条,停用 ${preview.value.disableCount} 条。`;
|
||||
});
|
||||
const isDirty = computed(() =>
|
||||
Boolean(
|
||||
poemState.value === "edit" &&
|
||||
editorSnapshot.value &&
|
||||
(poemDraft.value !== editorSnapshot.value.poemDraft ||
|
||||
disableMissing.value !== editorSnapshot.value.disableMissing),
|
||||
),
|
||||
const stateCopy = computed(() => poemState.value === "empty"
|
||||
? "正常字辈读取未返回条目。维护列表由后端单独鉴权,点击后才会判断是否可维护。"
|
||||
: "未取得可消费的服务端响应,页面不会用本地字辈或本地权限替代。",
|
||||
);
|
||||
const isDirty = computed(() => Boolean(
|
||||
poemState.value === "edit" && editorSnapshot.value && (
|
||||
poemDraft.value !== editorSnapshot.value.poemDraft ||
|
||||
disableMissing.value !== editorSnapshot.value.disableMissing
|
||||
),
|
||||
));
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
@@ -278,439 +152,186 @@ const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
const resetPoemRows = () => {
|
||||
const seed = validateGenerationPoemText(INITIAL_POEM_TEXT).generations;
|
||||
poemDraft.value = INITIAL_POEM_TEXT;
|
||||
poemRows.value = mergeGenerationPoemRows({
|
||||
existingRows: [],
|
||||
generationTexts: seed,
|
||||
startGeneration: LOCAL_PREVIEW_START_GENERATION,
|
||||
currentGeneration: LOCAL_CURRENT_GENERATION,
|
||||
disableMissing: false,
|
||||
});
|
||||
const applyRows = (rows) => {
|
||||
poemRows.value = rows;
|
||||
visiblePoemCount.value = POEM_RENDER_BATCH_SIZE;
|
||||
const named = rows.find((item) => item.genealogyName);
|
||||
genealogyName.value = named?.genealogyName || "当前家谱";
|
||||
};
|
||||
|
||||
const loadMorePoems = () => {
|
||||
visiblePoemCount.value = Math.min(
|
||||
poemRows.value.length,
|
||||
visiblePoemCount.value + POEM_RENDER_BATCH_SIZE,
|
||||
);
|
||||
};
|
||||
|
||||
const loadPoems = (query = {}) => {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
const fixture = findGenealogyFixture(genealogyId.value);
|
||||
if (!fixture) {
|
||||
canManage.value = false;
|
||||
const loadPoems = async ({ management = false } = {}) => {
|
||||
if (!genealogyId.value) {
|
||||
poemState.value = "error";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
genealogyName.value = fixture.name || "家谱";
|
||||
const accessRole = getGenealogyFixtureAccess(genealogyId.value).accessRole;
|
||||
canManage.value = accessRole === "owner";
|
||||
if (accessRole === "guest") {
|
||||
poemState.value = "no-permission";
|
||||
return;
|
||||
const sequence = ++requestSequence;
|
||||
poemState.value = "loading";
|
||||
poemError.value = "";
|
||||
try {
|
||||
const rows = management
|
||||
? await appApi.getGenerationPoemManagement(genealogyId.value, { requestController })
|
||||
: await appApi.getGenerationPoems(genealogyId.value, { requestController });
|
||||
if (sequence !== requestSequence) return false;
|
||||
applyRows(rows);
|
||||
canManage.value = management;
|
||||
poemState.value = rows.length ? "list" : "empty";
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isRequestCancelled(error) || sequence !== requestSequence) return false;
|
||||
poemState.value = "error";
|
||||
return false;
|
||||
}
|
||||
resetPoemRows();
|
||||
const requestedState =
|
||||
query.state === "loading"
|
||||
? "loading"
|
||||
: query.state === "empty"
|
||||
? "empty"
|
||||
: query.state === "edit"
|
||||
? "edit"
|
||||
: query.state === "no-permission"
|
||||
? "no-permission"
|
||||
: query.state === "error"
|
||||
? "error"
|
||||
: "list";
|
||||
if (requestedState === "empty") {
|
||||
poemDraft.value = "";
|
||||
poemRows.value = [];
|
||||
}
|
||||
if (requestedState === "edit") {
|
||||
if (!canManage.value) {
|
||||
poemState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
poemState.value = "list";
|
||||
openEditor("list");
|
||||
return;
|
||||
}
|
||||
poemState.value = requestedState;
|
||||
};
|
||||
|
||||
onLoad(loadPoems);
|
||||
onUnload(() => {
|
||||
const timer = feedbackTimer;
|
||||
feedbackTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const openEditor = (origin = poemState.value) => {
|
||||
const loadMorePoems = () => {
|
||||
visiblePoemCount.value = Math.min(poemRows.value.length, visiblePoemCount.value + POEM_RENDER_BATCH_SIZE);
|
||||
};
|
||||
const invalidatePreview = () => {
|
||||
poemError.value = "";
|
||||
preview.value = null;
|
||||
previewSignature.value = "";
|
||||
};
|
||||
const toggleDisableMissing = () => {
|
||||
disableMissing.value = !disableMissing.value;
|
||||
invalidatePreview();
|
||||
};
|
||||
const openEditor = () => {
|
||||
if (!canManage.value) return false;
|
||||
editorOrigin.value = origin === "empty" ? "empty" : "list";
|
||||
editorSnapshot.value = Object.freeze({
|
||||
poemDraft: poemDraft.value,
|
||||
disableMissing: disableMissing.value,
|
||||
});
|
||||
poemDraft.value = poemRows.value
|
||||
.filter((item) => item.status === GENERATION_POEM_STATUS.ACTIVE)
|
||||
.map((item) => item.generationText)
|
||||
.join(" ");
|
||||
disableMissing.value = false;
|
||||
preview.value = null;
|
||||
previewSignature.value = "";
|
||||
editorSnapshot.value = Object.freeze({ poemDraft: poemDraft.value, disableMissing: disableMissing.value });
|
||||
poemState.value = "edit";
|
||||
return true;
|
||||
};
|
||||
const restoreEditorSnapshot = () => {
|
||||
if (!editorSnapshot.value) return;
|
||||
poemDraft.value = editorSnapshot.value.poemDraft;
|
||||
disableMissing.value = editorSnapshot.value.disableMissing;
|
||||
const enterManagement = async () => {
|
||||
if (previewing.value || saving.value) return;
|
||||
const loaded = await loadPoems({ management: true });
|
||||
if (loaded) openEditor();
|
||||
};
|
||||
const requestLeaveEditor = async () => {
|
||||
if (isDirty.value) {
|
||||
const confirmed = await requestDiscardConfirmation();
|
||||
if (!confirmed) return false;
|
||||
}
|
||||
restoreEditorSnapshot();
|
||||
poemState.value = editorOrigin.value;
|
||||
poemState.value = poemRows.value.length ? "list" : "empty";
|
||||
editorSnapshot.value = null;
|
||||
poemError.value = "";
|
||||
return true;
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
internalTrail: poemState.value === "edit",
|
||||
"close-transient": cancelDiscard,
|
||||
"pop-internal-trail": requestLeaveEditor,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const handleStateAction = () => {
|
||||
if (poemState.value === "empty") return openEditor("empty");
|
||||
if (poemState.value === "error")
|
||||
return loadPoems({ genealogyId: genealogyId.value });
|
||||
return requestBack();
|
||||
};
|
||||
const savePoems = () => {
|
||||
const validateDraft = () => {
|
||||
const validation = validateGenerationPoemText(poemDraft.value);
|
||||
if (!validation.valid) {
|
||||
poemError.value = validation.message;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const lastGeneration =
|
||||
LOCAL_PREVIEW_START_GENERATION + validation.generations.length - 1;
|
||||
if (LOCAL_CURRENT_GENERATION > lastGeneration) {
|
||||
poemError.value = `请至少录入到当前第 ${LOCAL_CURRENT_GENERATION} 世`;
|
||||
return;
|
||||
}
|
||||
const nextRows = mergeGenerationPoemRows({
|
||||
existingRows: poemRows.value,
|
||||
generationTexts: validation.generations,
|
||||
startGeneration: LOCAL_PREVIEW_START_GENERATION,
|
||||
currentGeneration: LOCAL_CURRENT_GENERATION,
|
||||
disableMissing: disableMissing.value,
|
||||
});
|
||||
const activeRows = nextRows.filter(
|
||||
(item) => item.status === GENERATION_POEM_STATUS.ACTIVE,
|
||||
);
|
||||
const lastActiveGeneration = activeRows.length
|
||||
? activeRows[activeRows.length - 1].generationNo
|
||||
: null;
|
||||
const firstGap = lastActiveGeneration === null
|
||||
? null
|
||||
: findFirstGenerationGap(
|
||||
nextRows,
|
||||
lastActiveGeneration + 1,
|
||||
LOCAL_PREVIEW_START_GENERATION,
|
||||
);
|
||||
if (firstGap !== null) {
|
||||
poemError.value = `第 ${firstGap} 世字辈缺失;请补齐完整序列,或选择停用未覆盖的后续记录`;
|
||||
return;
|
||||
}
|
||||
poemRows.value = nextRows;
|
||||
visiblePoemCount.value = POEM_RENDER_BATCH_SIZE;
|
||||
editorSnapshot.value = null;
|
||||
poemState.value = "list";
|
||||
feedbackVisible.value = true;
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
const timer = setTimeout(() => {
|
||||
if (feedbackTimer !== timer) return;
|
||||
feedbackVisible.value = false;
|
||||
feedbackTimer = null;
|
||||
}, 1800);
|
||||
feedbackTimer = timer;
|
||||
return true;
|
||||
};
|
||||
const previewPoems = async () => {
|
||||
if (previewing.value || saving.value || !validateDraft()) return;
|
||||
previewing.value = true;
|
||||
poemError.value = "";
|
||||
try {
|
||||
preview.value = await appApi.previewGenerationPoemBatch(genealogyId.value, {
|
||||
poemText: poemDraft.value,
|
||||
disableMissing: disableMissing.value,
|
||||
}, { requestController });
|
||||
previewSignature.value = draftSignature.value;
|
||||
} catch (error) {
|
||||
if (!isRequestCancelled(error)) poemError.value = error?.message || "服务端预览失败,请稍后重试。";
|
||||
} finally {
|
||||
previewing.value = false;
|
||||
}
|
||||
};
|
||||
const savePoems = async () => {
|
||||
if (!canSave.value || saving.value || !validateDraft()) return;
|
||||
saving.value = true;
|
||||
poemError.value = "";
|
||||
try {
|
||||
await appApi.saveGenerationPoemBatch(genealogyId.value, {
|
||||
poemText: poemDraft.value,
|
||||
disableMissing: disableMissing.value,
|
||||
}, { requestController });
|
||||
feedbackMessage.value = "服务端字辈已保存,正在刷新维护列表。";
|
||||
const loaded = await loadPoems({ management: true });
|
||||
if (!loaded) {
|
||||
poemError.value = "字辈已提交,但维护列表刷新失败;请稍后重新查看。";
|
||||
return;
|
||||
}
|
||||
editorSnapshot.value = null;
|
||||
feedbackMessage.value = "服务端字辈已保存并已刷新。";
|
||||
} catch (error) {
|
||||
if (!isRequestCancelled(error)) poemError.value = error?.message || "字辈保存失败,请稍后重试。";
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
const requestBack = () => runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
internalTrail: poemState.value === "edit",
|
||||
submitting: previewing.value || saving.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"pop-internal-trail": requestLeaveEditor,
|
||||
"block-submitting": () => true,
|
||||
});
|
||||
const handleStateAction = () => poemState.value === "empty" ? enterManagement() : loadPoems();
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = String(query?.genealogyId || "");
|
||||
loadPoems();
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
++requestSequence;
|
||||
requestController.abort();
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
.poem-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.poem-page__header {
|
||||
z-index: 3;
|
||||
}
|
||||
.poem-panel {
|
||||
@include adaptive.adaptive-genealogy-state-panel;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
margin: 18rpx auto 0;
|
||||
padding: 76rpx 8%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.poem-panel > .app-loading {
|
||||
grid-area: 1 / 1 / -1 / -1;
|
||||
}
|
||||
.poem-list,
|
||||
.poem-editor,
|
||||
.poem-state-card {
|
||||
min-width: 0;
|
||||
}
|
||||
.poem-list__eyebrow {
|
||||
display: block;
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.poem-list__title {
|
||||
display: block;
|
||||
margin-top: 11rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-list__copy {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.poem-rows {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.poem-load-more {
|
||||
display: flex;
|
||||
min-height: 64rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 14rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.poem-load-more text {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-row {
|
||||
@include adaptive.adaptive-genealogy-form-field;
|
||||
display: grid;
|
||||
min-height: 72rpx;
|
||||
margin-top: 10rpx;
|
||||
grid-template-columns: minmax(116rpx, 38%) minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
.poem-row__number {
|
||||
z-index: 1;
|
||||
grid-column: 1;
|
||||
margin-left: 24rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.poem-row__character {
|
||||
z-index: 1;
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
padding: 12rpx 16rpx 12rpx 0;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
.poem-row__status {
|
||||
z-index: 1;
|
||||
grid-column: 3;
|
||||
margin-right: 22rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.poem-row--current .poem-row__character,
|
||||
.poem-row--current .poem-row__status {
|
||||
color: $brand-red;
|
||||
}
|
||||
.poem-row--disabled .poem-row__character,
|
||||
.poem-row--disabled .poem-row__status {
|
||||
color: $ink-muted;
|
||||
opacity: 0.62;
|
||||
}
|
||||
.poem-action {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 76rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.poem-action image {
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.poem-action text {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #fff9ed;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.poem-field {
|
||||
@include adaptive.adaptive-genealogy-form-field;
|
||||
display: grid;
|
||||
min-height: 118rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.poem-field > text {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
align-self: center;
|
||||
margin-left: 24rpx;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-field textarea {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
min-height: 118rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 164rpx;
|
||||
padding-top: 34rpx;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.poem-placeholder {
|
||||
color: #a79884;
|
||||
}
|
||||
.poem-field-error {
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.poem-policy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.poem-policy > text {
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-policy__option {
|
||||
display: grid;
|
||||
width: 248rpx;
|
||||
min-height: 62rpx;
|
||||
}
|
||||
.poem-policy__option image {
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.poem-policy__option text {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-policy__option .poem-policy__option-text--active {
|
||||
color: #fff9ed;
|
||||
}
|
||||
.poem-preview {
|
||||
display: block;
|
||||
margin-top: 22rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.poem-editor__actions {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.poem-editor__actions .poem-action {
|
||||
width: calc(50% - 7rpx);
|
||||
margin-top: 0;
|
||||
}
|
||||
.poem-action .poem-action__secondary {
|
||||
color: $ink;
|
||||
}
|
||||
.poem-state-card {
|
||||
text-align: center;
|
||||
}
|
||||
.poem-state--empty,
|
||||
.poem-state--error,
|
||||
.poem-state--no-permission {
|
||||
padding: 180rpx 4% 70rpx;
|
||||
}
|
||||
.poem-state-card .poem-list__eyebrow {
|
||||
text-align: center;
|
||||
}
|
||||
.poem-state-card .poem-list__copy {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.poem-state-card .poem-action {
|
||||
width: 420rpx;
|
||||
max-width: 100%;
|
||||
margin: 34rpx auto 0;
|
||||
}
|
||||
.poem-feedback {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
top: 138rpx;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
min-width: 260rpx;
|
||||
min-height: 74rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 34rpx;
|
||||
transform: translateX(-50%);
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.poem-feedback text {
|
||||
z-index: 1;
|
||||
color: $ink;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.poem-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
}
|
||||
.poem-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.poem-page__header { z-index: 3; }
|
||||
.poem-panel { @include adaptive.adaptive-genealogy-state-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 76rpx 8%; box-sizing: border-box; }
|
||||
.poem-panel > .app-loading { grid-area: 1 / 1 / -1 / -1; }
|
||||
.poem-list, .poem-editor, .poem-state-card { min-width: 0; }
|
||||
.poem-list__eyebrow { display: block; color: $brand-red; font-size: 23rpx; letter-spacing: 3rpx; }
|
||||
.poem-list__title { display: block; margin-top: 11rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 35rpx; font-weight: 700; }
|
||||
.poem-list__copy { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
|
||||
.poem-rows { margin-top: 18rpx; }
|
||||
.poem-load-more { display: flex; min-height: 64rpx; align-items: center; justify-content: center; margin-top: 14rpx; background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / contain no-repeat; }
|
||||
.poem-load-more text { color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.poem-row { @include adaptive.adaptive-genealogy-form-field; display: grid; min-height: 72rpx; margin-top: 10rpx; grid-template-columns: minmax(116rpx, 38%) minmax(0, 1fr) auto; align-items: center; }
|
||||
.poem-row__number { z-index: 1; grid-column: 1; margin-left: 24rpx; color: $ink-muted; font-size: 23rpx; }
|
||||
.poem-row__character { z-index: 1; grid-column: 2; min-width: 0; padding: 12rpx 16rpx 12rpx 0; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 32rpx; font-weight: 700; line-height: 1.4; overflow-wrap: anywhere; word-break: break-word; }
|
||||
.poem-row__status { z-index: 1; grid-column: 3; margin-right: 22rpx; color: $ink-muted; font-size: 22rpx; }
|
||||
.poem-row--disabled .poem-row__character, .poem-row--disabled .poem-row__status { color: $ink-muted; opacity: .62; }
|
||||
.poem-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 20rpx; }
|
||||
.poem-action--disabled { opacity: .55; }
|
||||
.poem-action image { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.poem-action text { z-index: 1; display: flex; grid-area: 1 / 1; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 25rpx; font-weight: 700; letter-spacing: 2rpx; }
|
||||
.poem-field { @include adaptive.adaptive-genealogy-form-field; display: grid; min-height: 118rpx; margin-top: 22rpx; }
|
||||
.poem-field > text { z-index: 1; grid-area: 1 / 1; align-self: center; margin-left: 24rpx; color: $ink; font-size: 22rpx; font-weight: 700; }
|
||||
.poem-field textarea { z-index: 1; grid-area: 1 / 1; box-sizing: border-box; width: auto; min-width: 0; min-height: 118rpx; margin-right: 20rpx; margin-left: 164rpx; padding-top: 34rpx; color: $ink; font-size: 22rpx; line-height: 1.5; }
|
||||
.poem-placeholder { color: #a79884; }
|
||||
.poem-field-error { display: block; margin-top: 4rpx; color: $brand-red; font-size: 24rpx; line-height: 34rpx; text-align: right; }
|
||||
.poem-policy { display: flex; align-items: center; justify-content: space-between; margin-top: 22rpx; }
|
||||
.poem-policy > text { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.poem-policy__option { display: grid; width: 248rpx; min-height: 62rpx; }
|
||||
.poem-policy__option image { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.poem-policy__option text { z-index: 1; display: flex; grid-area: 1 / 1; align-items: center; justify-content: center; height: 100%; color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.poem-policy__option .poem-policy__option-text--active { color: #fff9ed; }
|
||||
.poem-preview { display: block; margin-top: 22rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.6; }
|
||||
.poem-editor__actions { display: flex; gap: 14rpx; margin-top: 18rpx; }
|
||||
.poem-editor__actions .poem-action { width: calc((100% - 28rpx) / 3); margin-top: 0; }
|
||||
.poem-action .poem-action__secondary { color: $ink; }
|
||||
.poem-state-card { padding: 180rpx 4% 70rpx; text-align: center; }
|
||||
.poem-state-card .poem-list__eyebrow { text-align: center; }
|
||||
.poem-state-card .poem-list__copy { margin-top: 22rpx; }
|
||||
.poem-state-card .poem-action { width: 420rpx; max-width: 100%; margin: 34rpx auto 0; }
|
||||
.poem-feedback { position: fixed; z-index: 30; top: 138rpx; left: 50%; display: flex; min-width: 260rpx; min-height: 74rpx; align-items: center; justify-content: center; padding: 0 34rpx; transform: translateX(-50%); background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / contain no-repeat; }
|
||||
.poem-feedback text { z-index: 1; color: $ink; font-size: 22rpx; }
|
||||
@media (min-width: 400px) { .poem-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user