完成50%

This commit is contained in:
2026-07-23 08:23:59 +08:00
parent 9b0ad62df4
commit f1edc6b533
218 changed files with 24318 additions and 5514 deletions
+302 -71
View File
@@ -4,9 +4,11 @@
<GenealogyPageBackground />
<view class="poem-page__header"
><PageHeader
title="字辈诗"
:action="poemState === 'list' ? '维护' : ''"
:title="pageTitle"
:action="canManage && poemState === 'list' ? '维护' : ''"
custom-back
@action="openEditor"
@back="requestBack"
/></view>
<view
@@ -25,26 +27,38 @@
description="请稍候,正在读取家谱字序。"
/>
<view v-else-if="poemState === 'list'" class="poem-list">
<text class="poem-list__eyebrow">汤氏家谱 · 传承字序</text>
<text class="poem-list__title">启宗敦本继世传芳</text>
<text class="poem-list__copy"
>按世代查看字辈当前家谱使用到字辈</text
>
<text class="poem-list__eyebrow">{{ genealogyName }} · 传承字序</text>
<text class="poem-list__title">{{ generationRangeTitle }}</text>
<text class="poem-list__copy">{{ currentGenerationCopy }}</text>
<view class="poem-rows">
<view
v-for="item in poemRows"
v-for="item in visiblePoemRows"
:key="item.generationNo"
class="poem-row"
:class="{ 'poem-row--current': item.current }"
:class="{
'poem-row--current': item.current,
'poem-row--disabled': item.status === GENERATION_POEM_STATUS.DISABLED,
}"
>
<text class="poem-row__number"> {{ item.generationNo }} </text>
<text class="poem-row__character">{{ item.character }}</text>
<text class="poem-row__character">{{ item.generationText }}</text>
<text class="poem-row__status">{{
item.current ? "当前字辈" : "传承字序"
item.current
? "当前字辈"
: item.status === GENERATION_POEM_STATUS.DISABLED
? "已停用·记录保留"
: "传承字序"
}}</text>
</view>
</view>
<view class="poem-action" @click="openEditor">
<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"
@@ -54,31 +68,33 @@
<view v-else-if="poemState === 'edit'" class="poem-editor">
<text class="poem-list__eyebrow">批量维护</text>
<text class="poem-list__title">录入连续字辈</text>
<text class="poem-list__title">录入完整字辈序列</text>
<text class="poem-list__copy"
>按照接口支持的连续文本录入每个汉字对应一代保存前可预览新字序</text
>无分隔符时每个字符对应一代也可用空格逗号分号顿号斜杠或竖线分隔多字字辈一次最多
{{ MAX_GENERATION_COUNT }} 每代最多
{{ MAX_GENERATION_TEXT_LENGTH }} 个字符本页只更新本地预览尚未提交服务器</text
>
<view class="poem-field">
<text>字辈内容</text>
<textarea
v-model="poemDraft"
auto-height
maxlength="50"
placeholder="例如:启宗敦本继世传芳"
:maxlength="MAX_GENERATION_POEM_INPUT_LENGTH * 2"
placeholder="例如:启宗敦本,或 克勤 克俭 承先 启后"
placeholder-class="poem-placeholder"
@input="poemError = ''"
/>
</view>
<text v-if="poemError" class="poem-field-error">{{ poemError }}</text>
<view class="poem-policy">
<text>缺失旧世代</text>
<text>未被新文本覆盖的后续世代</text>
<view
class="poem-policy__option"
@click="stopMissingOldGeneration = !stopMissingOldGeneration"
@click="disableMissing = !disableMissing"
>
<image
:src="
stopMissingOldGeneration
disableMissing
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
"
@@ -86,17 +102,17 @@
/>
<text
:class="{
'poem-policy__option-text--active': stopMissingOldGeneration,
'poem-policy__option-text--active': disableMissing,
}"
>{{ stopMissingOldGeneration ? "停止并提醒" : "继续补录" }}</text
>{{ disableMissing ? "停用并保留记录" : "保持原状态" }}</text
>
</view>
</view>
<text class="poem-preview"
>预览{{ previewCharacters || "尚未录入字辈" }}</text
>预览{{ previewSummary }}</text
>
<view class="poem-editor__actions">
<view class="poem-action" @click="poemState = 'list'">
<view class="poem-action" @click="requestLeaveEditor">
<image
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
mode="aspectFit"
@@ -134,56 +150,171 @@
: "请从家谱总览重新进入,或稍后再试。"
}}</text>
<view
v-if="poemState !== 'empty' || canManage"
class="poem-action"
@click="poemState === 'empty' ? openEditor() : (poemState = 'list')"
@click="handleStateAction"
>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/><text>{{ poemState === "empty" ? "开始录入" : "重新查看" }}</text>
/><text>{{
poemState === "empty"
? "开始录入"
: poemState === "no-permission"
? "返回家谱总览"
: "重新查看"
}}</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="poem-feedback">
<text>字辈预览已更新</text>
<text>本地字辈预览已更新</text>
</view>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
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 {
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 poemState = ref("loading");
const poemError = ref("");
const feedbackVisible = ref(false);
const discardVisible = ref(false);
const canManage = ref(false);
let feedbackTimer = null;
const poemDraft = ref("启宗敦本继世传芳");
const stopMissingOldGeneration = ref(true);
const startGeneration = ref(12);
const currentGeneration = ref(14);
const poemRows = ref([
{ generationNo: 12, character: "启", current: false },
{ generationNo: 13, character: "宗", current: false },
{ generationNo: 14, character: "敦", current: true },
{ generationNo: 15, character: "本", current: false },
]);
const previewCharacters = computed(() =>
poemDraft.value.trim().split("").join(" · "),
const poemDraft = ref(INITIAL_POEM_TEXT);
// OpenAPI 的 disableMissing 示例为 false;本地同样采用安全默认 false。停用后续世代是高影响动作,
// 必须由谱主主动选择,不能把缩短一次本地草稿解释为默认停用历史记录。
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 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 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;
});
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;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
onLoad((query) => {
genealogyId.value = query.genealogyId || "";
startGeneration.value = Math.max(1, Number(query.startGeneration) || 12);
currentGeneration.value = Math.max(
startGeneration.value,
Number(query.currentGeneration) || 14,
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,
});
visiblePoemCount.value = POEM_RENDER_BATCH_SIZE;
};
const loadMorePoems = () => {
visiblePoemCount.value = Math.min(
poemRows.value.length,
visiblePoemCount.value + POEM_RENDER_BATCH_SIZE,
);
poemState.value =
};
const loadPoems = (query = {}) => {
genealogyId.value = String(query.genealogyId || "");
const fixture = findGenealogyFixture(genealogyId.value);
if (!fixture) {
canManage.value = false;
poemState.value = "error";
return;
}
genealogyName.value = fixture.name || "家谱";
const accessRole = getGenealogyFixtureAccess(genealogyId.value).accessRole;
canManage.value = accessRole === "owner";
if (accessRole === "guest") {
poemState.value = "no-permission";
return;
}
resetPoemRows();
const requestedState =
query.state === "loading"
? "loading"
: query.state === "empty"
@@ -192,47 +323,123 @@ onLoad((query) => {
? "edit"
: query.state === "no-permission"
? "no-permission"
: query.state === "error" || !genealogyId.value
: 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(() => {
if (feedbackTimer) clearTimeout(feedbackTimer);
const timer = feedbackTimer;
feedbackTimer = null;
if (timer) clearTimeout(timer);
discardConfirmation.dispose();
});
const openEditor = () => {
const openEditor = (origin = poemState.value) => {
if (!canManage.value) return false;
editorOrigin.value = origin === "empty" ? "empty" : "list";
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 requestLeaveEditor = async () => {
if (isDirty.value) {
const confirmed = await requestDiscardConfirmation();
if (!confirmed) return false;
}
restoreEditorSnapshot();
poemState.value = editorOrigin.value;
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 characters = poemDraft.value.trim().split("").filter(Boolean);
if (!characters.length) {
poemError.value = "请录入字辈内容";
const validation = validateGenerationPoemText(poemDraft.value);
if (!validation.valid) {
poemError.value = validation.message;
return;
}
if (poemDraft.value.trim() === "失败") {
poemError.value = "字辈保存失败,请稍后重试";
const lastGeneration =
LOCAL_PREVIEW_START_GENERATION + validation.generations.length - 1;
if (LOCAL_CURRENT_GENERATION > lastGeneration) {
poemError.value = `请至少录入到当前第 ${LOCAL_CURRENT_GENERATION}`;
return;
}
const preservedRows = stopMissingOldGeneration.value
? []
: poemRows.value.filter(
(item) => item.generationNo < startGeneration.value,
);
const nextRows = characters.map((character, index) => {
const generationNo = startGeneration.value + index;
return {
generationNo,
character,
current: generationNo === currentGeneration.value,
};
const nextRows = mergeGenerationPoemRows({
existingRows: poemRows.value,
generationTexts: validation.generations,
startGeneration: LOCAL_PREVIEW_START_GENERATION,
currentGeneration: LOCAL_CURRENT_GENERATION,
disableMissing: disableMissing.value,
});
poemRows.value = [...preservedRows, ...nextRows];
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;
feedbackTimer = setTimeout(() => {
if (feedbackTimer) clearTimeout(feedbackTimer);
const timer = setTimeout(() => {
if (feedbackTimer !== timer) return;
feedbackVisible.value = false;
feedbackTimer = null;
}, 1800);
feedbackTimer = timer;
};
</script>
@@ -287,12 +494,26 @@ const savePoems = () => {
.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: 48% auto 1fr auto;
grid-template-columns: minmax(116rpx, 38%) minmax(0, 1fr) auto;
align-items: center;
}
.poem-row__number {
@@ -305,14 +526,19 @@ const savePoems = () => {
.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: 4;
grid-column: 3;
margin-right: 22rpx;
color: $ink-muted;
font-size: 22rpx;
@@ -321,6 +547,11 @@ const savePoems = () => {
.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%;