完成全项目响应式审核与换机交接
This commit is contained in:
@@ -150,6 +150,10 @@
|
||||
import { ref } from "vue";
|
||||
import { onUnload } from "@dcloudio/uni-app";
|
||||
import AuthPageShell from "@/components/AuthPageShell.vue";
|
||||
import {
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
validatePassword,
|
||||
} from "@/utils/validation.js";
|
||||
|
||||
const phone = ref("");
|
||||
const password = ref("");
|
||||
@@ -200,7 +204,8 @@ const clearFieldError = (field) => {
|
||||
const validateForm = () => {
|
||||
const nextErrors = { phone: "", password: "", confirmPassword: "" };
|
||||
if (!/^1\d{10}$/.test(phone.value)) nextErrors.phone = "请输入正确手机号";
|
||||
if (!password.value) nextErrors.password = "请设置密码";
|
||||
const passwordResult = validatePassword(password.value);
|
||||
if (!passwordResult.valid) nextErrors.password = PASSWORD_POLICY_MESSAGE;
|
||||
if (!confirmPassword.value) nextErrors.confirmPassword = "请再次输入密码";
|
||||
else if (password.value !== confirmPassword.value)
|
||||
nextErrors.confirmPassword = "两次输入的密码不一致";
|
||||
|
||||
@@ -179,6 +179,10 @@
|
||||
import { ref } from "vue";
|
||||
import { onUnload } from "@dcloudio/uni-app";
|
||||
import AuthPageShell from "@/components/AuthPageShell.vue";
|
||||
import {
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
validatePassword,
|
||||
} from "@/utils/validation.js";
|
||||
|
||||
const phone = ref("");
|
||||
const verificationCode = ref("");
|
||||
@@ -237,7 +241,8 @@ const validateForm = () => {
|
||||
if (!/^1\d{10}$/.test(phone.value)) nextErrors.phone = "请输入正确手机号";
|
||||
if (!/^\d{6}$/.test(verificationCode.value))
|
||||
nextErrors.verificationCode = "请输入 6 位验证码";
|
||||
if (!password.value) nextErrors.password = "请设置新密码";
|
||||
const passwordResult = validatePassword(password.value);
|
||||
if (!passwordResult.valid) nextErrors.password = PASSWORD_POLICY_MESSAGE;
|
||||
if (!confirmPassword.value) nextErrors.confirmPassword = "请再次输入新密码";
|
||||
else if (password.value !== confirmPassword.value)
|
||||
nextErrors.confirmPassword = "两次密码输入不一致";
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
><view class="publish-field"
|
||||
><textarea
|
||||
v-model="content"
|
||||
auto-height
|
||||
maxlength="300"
|
||||
placeholder="写下想对家人说的话"
|
||||
placeholder-class="publish-placeholder"
|
||||
@@ -95,7 +96,6 @@ onUnmounted(() => {
|
||||
}
|
||||
.publish-panel {
|
||||
width: calc(100% - 32rpx);
|
||||
min-height: min(640px, calc((100vw - 16px) * 1.48));
|
||||
margin: 18rpx auto 0;
|
||||
padding: 9%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
>
|
||||
<textarea
|
||||
v-model="form.content"
|
||||
auto-height
|
||||
maxlength="1200"
|
||||
placeholder="记录家训、往事或想留给后人的话"
|
||||
placeholder-class="editor-placeholder"
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
</view>
|
||||
<textarea
|
||||
v-model="batchDescription"
|
||||
auto-height
|
||||
:disabled="isLocked"
|
||||
maxlength="120"
|
||||
placeholder="例如:2024 年春节全家团圆留影"
|
||||
@@ -151,6 +152,7 @@
|
||||
</view>
|
||||
<textarea
|
||||
:value="activePhoto.note"
|
||||
auto-height
|
||||
:disabled="isLocked"
|
||||
maxlength="80"
|
||||
placeholder="补充人物、时间或拍摄地点"
|
||||
|
||||
@@ -14,11 +14,6 @@
|
||||
</view>
|
||||
|
||||
<view class="video-status-card">
|
||||
<image
|
||||
class="video-status-card__skin"
|
||||
src="/static/assets/modules/family/transparent/module-content-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="video-status-card__body">
|
||||
<text class="video-status-card__eyebrow">视频 · 服务说明</text>
|
||||
<text class="video-status-card__title">视频服务暂未开放</text>
|
||||
@@ -45,6 +40,8 @@ const returnToFamily = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
|
||||
.video-status-page {
|
||||
min-height: 100vh;
|
||||
background: $paper;
|
||||
@@ -74,19 +71,13 @@ const returnToFamily = () => {
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card {
|
||||
display: grid;
|
||||
@include adaptive.adaptive-family-panel;
|
||||
width: 100%;
|
||||
min-height: 330rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.video-status-card__skin {
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card__body {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
padding: 70rpx 60rpx 48rpx;
|
||||
}
|
||||
.video-status-card__eyebrow,
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad } from "@dcloudio/uni-app";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppTabbar from "@/components/AppTabbar.vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
@@ -365,19 +365,12 @@ const selectedGenealogyId = ref(
|
||||
const listScrollCommand = ref(0);
|
||||
const currentListScrollTop = ref(0);
|
||||
|
||||
const readPresentationState = () => {
|
||||
if (typeof location !== "undefined") {
|
||||
const query = new URLSearchParams(location.hash.split("?")[1] || "");
|
||||
return query.get("state") || "default";
|
||||
}
|
||||
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage?.options?.state || "default";
|
||||
};
|
||||
|
||||
const syncEmptyStateFromRoute = () => {
|
||||
const presentationState = readPresentationState();
|
||||
const syncEmptyStateFromRoute = (query = {}) => {
|
||||
const presentationState = ["empty", "loading", "error"].includes(
|
||||
query?.state,
|
||||
)
|
||||
? query.state
|
||||
: "default";
|
||||
forceEmptyState.value = presentationState === "empty";
|
||||
isLoading.value = presentationState === "loading";
|
||||
hasError.value = presentationState === "error";
|
||||
@@ -391,11 +384,7 @@ onLoad((query) => {
|
||||
} else if (selectedGenealogyId.value) {
|
||||
genealogyContext.setCurrentGenealogyId(selectedGenealogyId.value);
|
||||
}
|
||||
syncEmptyStateFromRoute();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
syncEmptyStateFromRoute();
|
||||
syncEmptyStateFromRoute(query);
|
||||
});
|
||||
|
||||
const unreadCount = computed(
|
||||
|
||||
@@ -235,8 +235,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
@@ -287,48 +287,20 @@ const changeAncestorBirthDate = (event) => {
|
||||
|
||||
const isAncestorStep = computed(() => currentStep.value === "ancestor");
|
||||
|
||||
const getFlowQuery = () => {
|
||||
if (typeof location !== "undefined") {
|
||||
return new URLSearchParams(location.hash.split("?")[1] || "");
|
||||
}
|
||||
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage?.options || {};
|
||||
};
|
||||
|
||||
const syncFlowFromRoute = () => {
|
||||
const query = getFlowQuery();
|
||||
const isAncestorRoute = query.get
|
||||
? query.get("step") === "ancestor"
|
||||
: query.step === "ancestor";
|
||||
const routeGenealogyId = query.get
|
||||
? query.get("genealogyId")
|
||||
: query.genealogyId;
|
||||
const syncFlowFromRoute = (query = {}) => {
|
||||
const isAncestorRoute = query?.step === "ancestor";
|
||||
const routeGenealogyId = query?.genealogyId || "";
|
||||
currentStep.value = isAncestorRoute ? "ancestor" : "create";
|
||||
genealogyId.value =
|
||||
routeGenealogyId || (isAncestorStep.value ? "local-created-genealogy" : "");
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
syncFlowFromRoute();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
syncFlowFromRoute();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("hashchange", syncFlowFromRoute);
|
||||
}
|
||||
onLoad((query) => {
|
||||
syncFlowFromRoute(query);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (submitTimer) clearTimeout(submitTimer);
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("hashchange", syncFlowFromRoute);
|
||||
}
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onUnload, onShow } from "@dcloudio/uni-app";
|
||||
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";
|
||||
@@ -332,26 +332,13 @@ const resultFixtures = [
|
||||
|
||||
const inviteTarget = computed(() => resultFixtures[0]);
|
||||
|
||||
const syncModeFromRoute = () => {
|
||||
let requestedMode = "search";
|
||||
let requestedState = "";
|
||||
if (typeof location !== "undefined") {
|
||||
const routeParams = new URLSearchParams(location.hash.split("?")[1] || "");
|
||||
requestedMode = routeParams.get("mode") || "search";
|
||||
requestedState = routeParams.get("state") || "";
|
||||
} else {
|
||||
const pages = getCurrentPages();
|
||||
const routeOptions = pages[pages.length - 1]?.options || {};
|
||||
requestedMode = routeOptions.mode || "search";
|
||||
requestedState = routeOptions.state || "";
|
||||
}
|
||||
mode.value = requestedMode === "invite" ? "invite" : "search";
|
||||
if (mode.value === "search" && requestedState === "loading")
|
||||
const syncModeFromRoute = (query = {}) => {
|
||||
mode.value = query?.mode === "invite" ? "invite" : "search";
|
||||
if (mode.value === "search" && query?.state === "loading")
|
||||
searchState.value = "loading";
|
||||
};
|
||||
|
||||
onLoad(syncModeFromRoute);
|
||||
onShow(syncModeFromRoute);
|
||||
onLoad((query) => syncModeFromRoute(query));
|
||||
onUnload(() => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
});
|
||||
|
||||
@@ -112,15 +112,23 @@
|
||||
<view v-if="confirmation && !confirmation.approved" class="rejection-field">
|
||||
<text>拒绝原因</text>
|
||||
<textarea
|
||||
id="g10-rejection-reason"
|
||||
v-model="rejectionReason"
|
||||
auto-height
|
||||
maxlength="120"
|
||||
placeholder="请说明需要补充或核实的信息"
|
||||
@input="rejectionError = ''"
|
||||
:aria-invalid="!!rejectionError"
|
||||
aria-describedby="g10-rejection-error"
|
||||
:focus="rejectionFocused"
|
||||
@input="clearRejectionError"
|
||||
/>
|
||||
<text v-if="rejectionError" class="rejection-field__error">{{
|
||||
rejectionError
|
||||
}}</text>
|
||||
<text
|
||||
v-if="rejectionError"
|
||||
id="g10-rejection-error"
|
||||
class="rejection-field__error"
|
||||
role="alert"
|
||||
>{{ rejectionError }}</text
|
||||
>
|
||||
</view>
|
||||
</AppDialog>
|
||||
|
||||
@@ -131,7 +139,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
@@ -165,6 +173,7 @@ 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("");
|
||||
@@ -225,12 +234,14 @@ 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 showFeedback = (message) => {
|
||||
@@ -242,11 +253,17 @@ const showFeedback = (message) => {
|
||||
feedbackTimer = null;
|
||||
}, 1800);
|
||||
};
|
||||
const applyAudit = () => {
|
||||
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";
|
||||
|
||||
@@ -223,7 +223,6 @@ const saveSettings = () => {
|
||||
@include adaptive.adaptive-genealogy-state-panel;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
min-height: min(620px, calc((100vw - 16px) * 1.43));
|
||||
margin: 18rpx auto 0;
|
||||
padding: 70rpx 8%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -251,7 +251,6 @@ const savePoems = () => {
|
||||
@include adaptive.adaptive-genealogy-state-panel;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
min-height: min(650px, calc((100vw - 16px) * 1.5));
|
||||
margin: 18rpx auto 0;
|
||||
padding: 76rpx 8%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -27,6 +27,10 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
validatePassword,
|
||||
} from "@/utils/validation.js";
|
||||
|
||||
const passwordForm = reactive({ current: "", next: "", confirm: "" });
|
||||
const passwordVisible = reactive({ current: false, next: false, confirm: false });
|
||||
@@ -40,14 +44,17 @@ const passwordFields = [
|
||||
const toastVisible = ref(false); const toastMessage = ref(""); let timer = null;
|
||||
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(timer); timer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||||
const togglePassword = (key) => { passwordVisible[key] = !passwordVisible[key]; };
|
||||
const validatePassword = () => {
|
||||
const validateForm = () => {
|
||||
errors.current = passwordForm.current ? "" : "请输入当前密码";
|
||||
errors.next = /^(?=.*[A-Za-z])(?=.*\d).{8,32}$/.test(passwordForm.next) ? "" : "新密码需为 8–32 位,并同时包含字母和数字";
|
||||
const nextPassword = validatePassword(passwordForm.next);
|
||||
errors.next = nextPassword.valid ? "" : PASSWORD_POLICY_MESSAGE;
|
||||
if (!errors.next && passwordForm.next === passwordForm.current)
|
||||
errors.next = "新密码不能与当前密码相同";
|
||||
errors.confirm = !passwordForm.confirm ? "请再次输入新密码" : passwordForm.confirm !== passwordForm.next ? "两次输入的新密码不一致" : "";
|
||||
return !Object.values(errors).some(Boolean);
|
||||
};
|
||||
const savePassword = () => {
|
||||
if (!validatePassword() || passwordState.value === "saving") return;
|
||||
if (!validateForm() || passwordState.value === "saving") return;
|
||||
passwordState.value = "saving";
|
||||
timer = setTimeout(() => { passwordState.value = "ready"; passwordForm.current = ""; passwordForm.next = ""; passwordForm.confirm = ""; showToast("密码已修改,请妥善保管新密码"); }, 500);
|
||||
};
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { computed, onUnmounted, ref } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
@@ -104,22 +105,7 @@ const people = [
|
||||
{ id: 3, name: "汤文清", role: "青年代表", generation: 19 },
|
||||
];
|
||||
|
||||
const query = (() => {
|
||||
if (typeof location !== "undefined")
|
||||
return Object.fromEntries(
|
||||
new URLSearchParams(location.hash.split("?")[1] || ""),
|
||||
);
|
||||
const pages = getCurrentPages();
|
||||
return pages[pages.length - 1]?.options || {};
|
||||
})();
|
||||
|
||||
const peopleState = ref(
|
||||
query.state === "empty"
|
||||
? "empty"
|
||||
: query.state === "error"
|
||||
? "error"
|
||||
: "ready",
|
||||
);
|
||||
const peopleState = ref("ready");
|
||||
const keywordInput = ref("");
|
||||
const keyword = ref("");
|
||||
const toastVisible = ref(false);
|
||||
@@ -157,6 +143,12 @@ const showCreateNotice = () => {
|
||||
uni.navigateTo({ url: "/pages/records/r02-person-detail?mode=create" });
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
peopleState.value = ["empty", "error"].includes(query.state)
|
||||
? query.state
|
||||
: "ready";
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
});
|
||||
@@ -219,10 +211,9 @@ onUnmounted(() => {
|
||||
@include adaptive.adaptive-records-person;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: clamp(92px, 190rpx, 108px);
|
||||
min-height: clamp(92px, 190rpx, 108px);
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding: 16% 10%;
|
||||
}
|
||||
.person-card:first-child {
|
||||
margin-top: 0;
|
||||
|
||||
@@ -20,6 +20,15 @@
|
||||
<text>主支 · 第 12—14 世</text>
|
||||
</view>
|
||||
<view class="tree-toolbar__actions">
|
||||
<text
|
||||
v-if="treeState === 'tree' && treeHasDrifted && selected"
|
||||
class="tree-recenter"
|
||||
role="button"
|
||||
aria-label="回到当前成员"
|
||||
hover-class="tree-action--pressed"
|
||||
@click="recenterSelectedMember"
|
||||
>回到当前</text
|
||||
>
|
||||
<text @click="treeState = 'landscape'">阅读提示</text>
|
||||
<text @click="toRelationship">关系维护</text>
|
||||
</view>
|
||||
@@ -51,18 +60,23 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="treeState === 'tree'" class="lineage-pan-cue">
|
||||
<text>同代分支可左右查看</text>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="tree-scroll"
|
||||
:class="{ 'tree-scroll--lineage': treeState === 'tree' }"
|
||||
scroll-x
|
||||
:scroll-left="initialScrollLeft"
|
||||
:scroll-left="treeState === 'tree' ? treeScrollLeft : 0"
|
||||
:show-scrollbar="false"
|
||||
:style="treeScrollStyle"
|
||||
@scroll="handleTreeScroll"
|
||||
>
|
||||
<view
|
||||
class="tree-canvas"
|
||||
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
|
||||
:style="treeMetricsStyle"
|
||||
:style="treeState === 'tree' ? treeMetricsStyle : undefined"
|
||||
>
|
||||
<AppLoading
|
||||
v-if="treeState === 'loading'"
|
||||
@@ -71,10 +85,6 @@
|
||||
/>
|
||||
|
||||
<template v-else-if="treeState === 'tree'">
|
||||
<view class="lineage-pan-cue">
|
||||
<text>同代分支可左右查看</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="connector in lineageConnectors"
|
||||
:key="connector.id"
|
||||
@@ -85,6 +95,7 @@
|
||||
<view
|
||||
v-for="member in layoutMembers"
|
||||
:key="member.id"
|
||||
:id="`tree-member-${member.id}`"
|
||||
class="member-node"
|
||||
:class="{ 'member-node--selected': selected?.id === member.id }"
|
||||
:style="nodeGridStyle(member)"
|
||||
@@ -166,7 +177,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
@@ -176,7 +187,12 @@ import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
const genealogyId = ref("");
|
||||
const treeState = ref("loading");
|
||||
const selected = ref(null);
|
||||
const initialScrollLeft = ref(90);
|
||||
const treeScrollLeft = ref(90);
|
||||
const currentTreeScrollLeft = ref(90);
|
||||
const centeredTreeScrollLeft = ref(90);
|
||||
const treeHasDrifted = ref(false);
|
||||
let ignoreTreeScroll = false;
|
||||
let recenterTimer = null;
|
||||
const GRID_UNIT = 5;
|
||||
const NODE_HALF_HEIGHT = 47;
|
||||
const MEMBER_GAP = 250;
|
||||
@@ -409,6 +425,46 @@ onLoad((query) => {
|
||||
treeState.value = "tree";
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
if (recenterTimer) clearTimeout(recenterTimer);
|
||||
});
|
||||
|
||||
const handleTreeScroll = (event) => {
|
||||
if (ignoreTreeScroll || treeState.value !== "tree") return;
|
||||
const scrollLeft = Number(event?.detail?.scrollLeft || 0);
|
||||
currentTreeScrollLeft.value = scrollLeft;
|
||||
treeHasDrifted.value = Math.abs(scrollLeft - centeredTreeScrollLeft.value) > 80;
|
||||
};
|
||||
|
||||
const recenterSelectedMember = async () => {
|
||||
if (!selected.value) return;
|
||||
ignoreTreeScroll = true;
|
||||
const [scrollRect, nodeRect] = await new Promise((resolve) => {
|
||||
const query = uni.createSelectorQuery();
|
||||
query.select(".tree-scroll").boundingClientRect();
|
||||
query
|
||||
.select(`#tree-member-${selected.value.id}`)
|
||||
.boundingClientRect();
|
||||
query.exec(resolve);
|
||||
});
|
||||
if (scrollRect && nodeRect) {
|
||||
const centeredScrollLeft = Math.max(
|
||||
0,
|
||||
currentTreeScrollLeft.value +
|
||||
nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2),
|
||||
);
|
||||
treeScrollLeft.value = centeredScrollLeft;
|
||||
currentTreeScrollLeft.value = centeredScrollLeft;
|
||||
centeredTreeScrollLeft.value = centeredScrollLeft;
|
||||
}
|
||||
treeHasDrifted.value = false;
|
||||
if (recenterTimer) clearTimeout(recenterTimer);
|
||||
recenterTimer = setTimeout(() => {
|
||||
ignoreTreeScroll = false;
|
||||
recenterTimer = null;
|
||||
}, 320);
|
||||
};
|
||||
|
||||
const nodeGridStyle = (member) => ({
|
||||
gridColumn: `${member.x / 5 + 1}`,
|
||||
gridRow: `${member.y / 5 + 1}`,
|
||||
@@ -484,6 +540,9 @@ const toAddRelative = () =>
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.tree-action--pressed {
|
||||
opacity: 0.72;
|
||||
}
|
||||
.tree-stage--lineage {
|
||||
display: grid;
|
||||
grid-template-columns: 190rpx minmax(0, 1fr);
|
||||
@@ -503,6 +562,7 @@ const toAddRelative = () =>
|
||||
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
|
||||
}
|
||||
.tree-scroll--lineage {
|
||||
grid-area: 1 / 2;
|
||||
min-width: 0;
|
||||
}
|
||||
.tree-canvas {
|
||||
@@ -514,7 +574,7 @@ const toAddRelative = () =>
|
||||
width: calc(100vw - 32rpx);
|
||||
}
|
||||
.lineage-pan-cue {
|
||||
grid-area: 1 / 1 / -1 / -1;
|
||||
grid-area: 1 / 2;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
z-index: 3;
|
||||
|
||||
@@ -141,7 +141,7 @@ const toTree = () => uni.navigateBack();
|
||||
.member-context { @include adaptive.adaptive-tree-field; width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; }
|
||||
.member-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
|
||||
.member-panel { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); min-height: min(620px, calc((100vw - 16px) * 1.42)); margin: 16rpx auto 0; padding: 9% 8%; }
|
||||
.member-panel { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); margin: 16rpx auto 0; padding: 9% 8%; }
|
||||
.member-panel > .app-loading { margin-top: 30%; }
|
||||
.member-heading { display: flex; align-items: center; gap: 20rpx; }
|
||||
.member-heading__seal { display: flex; width: 74rpx; height: 74rpx; flex: 0 0 74rpx; align-items: center; justify-content: center; background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat; }
|
||||
|
||||
@@ -224,7 +224,7 @@ const discardAndBack = () => {
|
||||
|
||||
.add-relative-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.add-relative-page__header { z-index: 3; }
|
||||
.add-relative-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); min-height: min(680px, calc((100vw - 16px) * 1.5)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
|
||||
.add-relative-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
|
||||
@@ -173,7 +173,7 @@ const discardAndBack = () => {
|
||||
|
||||
.edit-member-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.edit-member-page__header { z-index: 3; }
|
||||
.edit-member-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); min-height: min(690px, calc((100vw - 16px) * 1.52)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
|
||||
.edit-member-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
|
||||
@@ -192,7 +192,7 @@ const handleResultAction = () => {
|
||||
|
||||
.relationship-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.relationship-page__header { z-index: 3; }
|
||||
.relationship-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); min-height: min(650px, calc((100vw - 16px) * 1.46)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
|
||||
.relationship-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
|
||||
Reference in New Issue
Block a user