保存视觉审核与文档流迁移进度
This commit is contained in:
@@ -192,6 +192,7 @@ const openSection = (key) => {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.feed-shortcut {
|
||||
position: relative;
|
||||
@@ -212,10 +213,14 @@ const openSection = (key) => {
|
||||
.feed-card,
|
||||
.feed-state-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 230rpx;
|
||||
min-height: 98px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-top: 17rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.feed-card__skin,
|
||||
.feed-state-card > image,
|
||||
@@ -224,11 +229,12 @@ const openSection = (key) => {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.feed-card__copy {
|
||||
position: absolute;
|
||||
inset: 15% 9%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 15% 9%;
|
||||
}
|
||||
.feed-card text {
|
||||
display: block;
|
||||
@@ -260,9 +266,10 @@ const openSection = (key) => {
|
||||
margin-top: 70rpx;
|
||||
}
|
||||
.feed-state-card > view {
|
||||
position: absolute;
|
||||
inset: 23% 10%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 23% 10%;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.feed-state-card text {
|
||||
|
||||
@@ -99,19 +99,22 @@ onUnmounted(() => {
|
||||
}
|
||||
.publish-panel {
|
||||
width: calc(100% - 32rpx);
|
||||
height: min(640px, calc((100vw - 16px) * 1.48));
|
||||
min-height: min(640px, calc((100vw - 16px) * 1.48));
|
||||
margin: 18rpx auto 0;
|
||||
padding: 9%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.publish-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.publish-form,
|
||||
.publish-result {
|
||||
position: absolute;
|
||||
inset: 9% 9%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.publish-form > text,
|
||||
.publish-result > text {
|
||||
@@ -133,20 +136,23 @@ onUnmounted(() => {
|
||||
}
|
||||
.publish-field {
|
||||
position: relative;
|
||||
height: 250rpx;
|
||||
display: flex;
|
||||
min-height: 250rpx;
|
||||
margin-top: 24rpx;
|
||||
padding: 25rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.publish-field image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.publish-field textarea {
|
||||
position: absolute;
|
||||
inset: 25rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: auto;
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
@@ -159,7 +165,7 @@ onUnmounted(() => {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.publish-result {
|
||||
top: 29%;
|
||||
margin-top: 20%;
|
||||
text-align: center;
|
||||
}
|
||||
.publish-result > .app-button {
|
||||
|
||||
@@ -1,5 +1,386 @@
|
||||
<!-- 页面编号:F-06;用途:新建与编辑谱文。 -->
|
||||
<template><ModulePage page-id="f06" /></template>
|
||||
<template>
|
||||
<view
|
||||
class="article-editor-page"
|
||||
:class="`article-editor-state--${editorState}`"
|
||||
>
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="article-editor-page__header">
|
||||
<PageHeader title="编辑谱文" />
|
||||
</view>
|
||||
|
||||
<view v-if="editorState === 'loading'" class="article-editor-loading">
|
||||
<AppLoading
|
||||
text="正在打开谱文编辑器"
|
||||
description="请稍候,草稿正在展开。"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-else-if="editorState === 'success'"
|
||||
class="article-editor-content"
|
||||
>
|
||||
<view class="editor-result-card">
|
||||
<view class="editor-result-card__body">
|
||||
<text class="editor-eyebrow">保存结果</text>
|
||||
<text class="editor-result-card__title">谱文已保存</text>
|
||||
<text class="editor-result-card__copy"
|
||||
>这篇谱文已收录,可返回谱文列表继续查看。</text
|
||||
>
|
||||
<AppButton block label="返回谱文列表" @click="returnToList" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="article-editor-content">
|
||||
<view class="editor-panel">
|
||||
<view class="editor-panel__body">
|
||||
<text class="editor-eyebrow">草稿 · 未发布</text>
|
||||
<text class="editor-title">把值得传承的故事写下来</text>
|
||||
<text class="editor-intro"
|
||||
>补充标题、分类和正文,保存后可返回谱文列表继续查看。</text
|
||||
>
|
||||
|
||||
<view class="editor-field">
|
||||
<text class="editor-field__label">文章标题</text>
|
||||
<view
|
||||
class="editor-control"
|
||||
:class="{ 'editor-control--error': fieldErrors.title }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<input
|
||||
v-model="form.title"
|
||||
maxlength="40"
|
||||
placeholder="请输入文章标题"
|
||||
placeholder-class="editor-placeholder"
|
||||
@input="clearFieldError('title')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.title" class="editor-field__error">{{
|
||||
fieldErrors.title
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<view class="editor-field">
|
||||
<text class="editor-field__label">文章分类</text>
|
||||
<view
|
||||
class="editor-control"
|
||||
:class="{ 'editor-control--error': fieldErrors.category }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<input
|
||||
v-model="form.category"
|
||||
maxlength="20"
|
||||
placeholder="如:家风家训"
|
||||
placeholder-class="editor-placeholder"
|
||||
@input="clearFieldError('category')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.category" class="editor-field__error">{{
|
||||
fieldErrors.category
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<view class="editor-field">
|
||||
<text class="editor-field__label">正文内容</text>
|
||||
<view
|
||||
class="editor-control editor-control--textarea"
|
||||
:class="{ 'editor-control--error': fieldErrors.content }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<textarea
|
||||
v-model="form.content"
|
||||
maxlength="1200"
|
||||
placeholder="记录家训、往事或想留给后人的话"
|
||||
placeholder-class="editor-placeholder"
|
||||
@input="clearFieldError('content')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.content" class="editor-field__error">{{
|
||||
fieldErrors.content
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<text class="editor-draft-note"
|
||||
>当前为未发布草稿;真实草稿保存将在接口阶段接入。</text
|
||||
>
|
||||
<text v-if="editorState === 'error'" class="editor-save-error"
|
||||
>保存失败,已填写内容仍保留,请稍后重试。</text
|
||||
>
|
||||
<AppButton
|
||||
block
|
||||
:label="actionLabel"
|
||||
:disabled="editorState === 'saving'"
|
||||
@click="submit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
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";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const fieldFrame =
|
||||
"/static/assets/modules/family/transparent/module-field-frame.png";
|
||||
const draftFixture = {
|
||||
title: "汤氏家训辑录",
|
||||
category: "家风家训",
|
||||
content:
|
||||
"孝友传家,勤俭立业;敬祖睦宗,诚实待人。愿后人常怀感恩,彼此扶持。",
|
||||
};
|
||||
const editorState = ref("form");
|
||||
const allowedStates = new Set([
|
||||
"draft",
|
||||
"loading",
|
||||
"validation",
|
||||
"saving",
|
||||
"error",
|
||||
"success",
|
||||
]);
|
||||
const form = reactive({ title: "", category: "", content: "" });
|
||||
const fieldErrors = reactive({ title: "", category: "", content: "" });
|
||||
let saveTimer = null;
|
||||
|
||||
const actionLabel = computed(() =>
|
||||
editorState.value === "saving"
|
||||
? "正在保存…"
|
||||
: editorState.value === "error"
|
||||
? "重新保存"
|
||||
: "保存谱文",
|
||||
);
|
||||
|
||||
const fillDraft = (failed = false) => {
|
||||
Object.assign(form, draftFixture, failed ? { title: "保存失败" } : {});
|
||||
};
|
||||
const showAllFieldErrors = () => {
|
||||
fieldErrors.title = "请填写文章标题";
|
||||
fieldErrors.category = "请填写文章分类";
|
||||
fieldErrors.content = "请填写正文内容";
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
const requestedState = allowedStates.has(query.state) ? query.state : "form";
|
||||
if (["draft", "saving", "error"].includes(requestedState)) {
|
||||
fillDraft(requestedState === "error");
|
||||
}
|
||||
if (requestedState === "validation") showAllFieldErrors();
|
||||
editorState.value = requestedState;
|
||||
});
|
||||
|
||||
const clearFieldError = (field) => {
|
||||
fieldErrors[field] = "";
|
||||
if (
|
||||
editorState.value === "error" ||
|
||||
editorState.value === "validation"
|
||||
) {
|
||||
editorState.value = "form";
|
||||
}
|
||||
};
|
||||
const validate = () => {
|
||||
fieldErrors.title = form.title.trim() ? "" : "请填写文章标题";
|
||||
fieldErrors.category = form.category.trim() ? "" : "请填写文章分类";
|
||||
fieldErrors.content = form.content.trim() ? "" : "请填写正文内容";
|
||||
return !fieldErrors.title && !fieldErrors.category && !fieldErrors.content;
|
||||
};
|
||||
const submit = () => {
|
||||
if (editorState.value === "saving") return;
|
||||
if (!validate()) {
|
||||
editorState.value = "validation";
|
||||
return;
|
||||
}
|
||||
editorState.value = "saving";
|
||||
saveTimer = setTimeout(() => {
|
||||
editorState.value =
|
||||
form.title.trim() === "保存失败" ? "error" : "success";
|
||||
saveTimer = null;
|
||||
}, 320);
|
||||
};
|
||||
const returnToList = () => uni.navigateBack();
|
||||
|
||||
onUnload(() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-editor-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.article-editor-page__header,
|
||||
.article-editor-loading,
|
||||
.article-editor-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.article-editor-loading {
|
||||
padding-top: 150rpx;
|
||||
}
|
||||
.article-editor-content {
|
||||
padding: 18rpx 20rpx calc(38rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.editor-panel,
|
||||
.editor-result-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 20rpx solid transparent;
|
||||
border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");
|
||||
border-image-slice: 72 fill;
|
||||
border-image-width: 64rpx;
|
||||
border-image-repeat: stretch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.editor-panel__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 38rpx 34rpx 42rpx;
|
||||
}
|
||||
.editor-eyebrow {
|
||||
display: block;
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
letter-spacing: 3rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.editor-title {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
.editor-intro {
|
||||
display: block;
|
||||
margin: 10rpx 8rpx 22rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.55;
|
||||
text-align: center;
|
||||
}
|
||||
.editor-field {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.editor-field__label {
|
||||
display: block;
|
||||
margin: 0 8rpx 8rpx;
|
||||
color: $ink;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.editor-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 82rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.editor-control > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.editor-control input,
|
||||
.editor-control textarea {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.editor-control input {
|
||||
width: 100%;
|
||||
height: 82rpx;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
.editor-control--textarea {
|
||||
height: 220rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.editor-control textarea {
|
||||
width: 100%;
|
||||
height: 180rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.editor-control--error {
|
||||
filter: sepia(0.18) saturate(1.2);
|
||||
}
|
||||
.editor-placeholder {
|
||||
color: #9e8e79;
|
||||
}
|
||||
.editor-field__error,
|
||||
.editor-save-error {
|
||||
display: block;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
}
|
||||
.editor-field__error {
|
||||
margin: 5rpx 8rpx 0;
|
||||
}
|
||||
.editor-draft-note {
|
||||
display: block;
|
||||
margin: 20rpx 10rpx 0;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.55;
|
||||
text-align: center;
|
||||
}
|
||||
.editor-save-error {
|
||||
margin: 12rpx 8rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
.editor-panel__body > .app-button {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.editor-result-card {
|
||||
min-height: 420rpx;
|
||||
}
|
||||
.editor-result-card__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 112rpx 52rpx 68rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.editor-result-card__title {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.editor-result-card__copy {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.editor-result-card__body > .app-button {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.article-editor-content {
|
||||
padding-right: 14rpx;
|
||||
padding-left: 14rpx;
|
||||
}
|
||||
.editor-panel__body {
|
||||
padding-right: 26rpx;
|
||||
padding-left: 26rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,342 @@
|
||||
<!-- 页面编号:F-08;用途:相册详情与照片墙。 -->
|
||||
<template><ModulePage page-id="f08" /></template>
|
||||
<template>
|
||||
<view class="album-detail-page" :class="`album-state--${albumState}`">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="album-detail-header"><PageHeader title="相册详情" /></view>
|
||||
|
||||
<view class="album-detail-content">
|
||||
<view v-if="albumState === 'expired'" class="album-expired-state">
|
||||
<view class="album-state-card__body">
|
||||
<text class="album-state-card__eyebrow">相册状态</text>
|
||||
<text class="album-state-card__title">相册已失效</text>
|
||||
<text class="album-state-card__copy">这个相册已无法查看</text>
|
||||
<AppButton block label="返回相册列表" @click="returnToAlbums" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="album-heading">
|
||||
<text class="album-heading__eyebrow">家族影像 · 2024</text>
|
||||
<text class="album-heading__title">2024 春节团圆</text>
|
||||
<text class="album-heading__copy"
|
||||
>一家人围坐团圆,让今朝欢聚与祖居旧影留在同一本相册里。</text
|
||||
>
|
||||
<text class="album-heading__count"
|
||||
>{{ albumState === "empty" ? "0 张照片" : "5 张照片" }}</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view v-if="albumState === 'empty'" class="album-empty-state">
|
||||
<view class="album-state-card__body">
|
||||
<text class="album-state-card__eyebrow">照片墙</text>
|
||||
<text class="album-state-card__title">相册里还没有照片</text>
|
||||
<text class="album-state-card__copy"
|
||||
>添加第一张团圆影像,把值得珍藏的时刻留在这里。</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="album-wall">
|
||||
<view
|
||||
class="album-photo-tile album-photo-tile--hero"
|
||||
@click="openPreview(0)"
|
||||
>
|
||||
<image
|
||||
class="album-hero-photo"
|
||||
:src="photos[0].src"
|
||||
:alt="photos[0].alt"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text class="album-photo-tile__caption">{{ photos[0].caption }}</text>
|
||||
</view>
|
||||
|
||||
<view class="album-photo-grid">
|
||||
<view
|
||||
v-for="(photo, index) in photos.slice(1)"
|
||||
:key="photo.src"
|
||||
class="album-photo-tile album-photo-tile--grid"
|
||||
@click="openPreview(index + 1)"
|
||||
>
|
||||
<image
|
||||
class="album-photo-tile__image"
|
||||
:src="photo.src"
|
||||
:alt="photo.alt"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text class="album-photo-tile__caption">{{ photo.caption }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="album-upload-action" @click="showUploadNotice">
|
||||
<AppButton block label="添加照片" />
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view v-if="previewVisible" class="album-preview">
|
||||
<view class="album-preview__toolbar">
|
||||
<text class="album-preview__title">照片预览</text>
|
||||
<view class="album-preview__close" @click="closePreview"><text>关闭</text></view>
|
||||
</view>
|
||||
<image
|
||||
class="album-preview__image"
|
||||
:src="photos[previewIndex].src"
|
||||
:alt="photos[previewIndex].alt"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="album-preview__footer">
|
||||
<text class="album-preview__position"
|
||||
>{{ previewIndex + 1 }} / {{ photos.length }}</text
|
||||
>
|
||||
<text class="album-preview__caption">{{ photos[previewIndex].caption }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppToast :visible="uploadNoticeVisible" :message="uploadNoticeMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const albumState = ref("normal");
|
||||
const previewVisible = ref(false);
|
||||
const previewIndex = ref(0);
|
||||
const uploadNoticeVisible = ref(false);
|
||||
const uploadNoticeMessage = "上传照片将在入口接通后开放";
|
||||
let uploadNoticeTimer;
|
||||
|
||||
const photos = [
|
||||
{ src: "/static/assets/modules/family/f08/f08-reunion-hero.png", alt: "春节团圆时三代家人的合影", caption: "除夕团圆 · 2024" },
|
||||
{ src: "/static/assets/modules/family/f08/f08-family-portrait.png", alt: "家人在院落前的春节合影", caption: "院前合影 · 2024" },
|
||||
{ src: "/static/assets/modules/family/f08/f08-reunion-table.png", alt: "家人围坐吃年夜饭", caption: "围桌守岁 · 2024" },
|
||||
{ src: "/static/assets/modules/family/f08/f08-ancestral-home.png", alt: "祖居院落的复古旧照", caption: "祖居旧影 · 1968" },
|
||||
{ src: "/static/assets/modules/family/f08/f08-ancestral-portrait.png", alt: "老一辈家人在祖居门前的合影", caption: "门前合影 · 1972" },
|
||||
];
|
||||
|
||||
const openPreview = (index) => {
|
||||
previewIndex.value = index;
|
||||
previewVisible.value = true;
|
||||
albumState.value = "preview";
|
||||
};
|
||||
|
||||
const closePreview = () => {
|
||||
previewVisible.value = false;
|
||||
albumState.value = "normal";
|
||||
};
|
||||
|
||||
const showUploadNotice = () => {
|
||||
uploadNoticeVisible.value = true;
|
||||
clearTimeout(uploadNoticeTimer);
|
||||
uploadNoticeTimer = setTimeout(() => {
|
||||
uploadNoticeVisible.value = false;
|
||||
}, 1800);
|
||||
};
|
||||
|
||||
const returnToAlbums = () => {
|
||||
uni.redirectTo({ url: "/pages/family/f07-album-list" });
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
albumState.value =
|
||||
query.state === "empty"
|
||||
? "empty"
|
||||
: query.state === "preview"
|
||||
? "preview"
|
||||
: query.state === "expired"
|
||||
? "expired"
|
||||
: "normal";
|
||||
previewVisible.value = albumState.value === "preview";
|
||||
});
|
||||
|
||||
onBackPress(() => {
|
||||
if (!previewVisible.value) return false;
|
||||
closePreview();
|
||||
return true;
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
clearTimeout(uploadNoticeTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.album-detail-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.album-detail-header,
|
||||
.album-detail-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.album-detail-content {
|
||||
padding: 22rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.album-heading {
|
||||
padding: 18rpx 12rpx 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.album-heading text,
|
||||
.album-state-card__body text,
|
||||
.album-preview__footer text {
|
||||
display: block;
|
||||
}
|
||||
.album-heading__eyebrow,
|
||||
.album-state-card__eyebrow {
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.album-heading__title,
|
||||
.album-state-card__title {
|
||||
margin-top: 8rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.album-heading__copy,
|
||||
.album-state-card__copy {
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.album-heading__count {
|
||||
margin-top: 12rpx;
|
||||
color: #806a51;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.album-wall {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.album-photo-tile {
|
||||
position: relative;
|
||||
min-height: 44px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(179, 133, 63, 0.62);
|
||||
background: rgba(245, 238, 226, 0.86);
|
||||
box-shadow: 0 8rpx 18rpx rgba(91, 60, 32, 0.12);
|
||||
}
|
||||
.album-photo-tile--hero {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
.album-photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14rpx;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.album-photo-tile--grid {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
.album-hero-photo,
|
||||
.album-photo-tile__image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.album-photo-tile__caption {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 10rpx 12rpx;
|
||||
background: rgba(48, 35, 25, 0.72);
|
||||
color: #fffaf0;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.album-upload-action {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.album-empty-state,
|
||||
.album-expired-state {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 430rpx;
|
||||
box-sizing: border-box;
|
||||
border: 20rpx solid transparent;
|
||||
border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");
|
||||
border-image-slice: 72 fill;
|
||||
border-image-width: 64rpx;
|
||||
border-image-repeat: stretch;
|
||||
}
|
||||
.album-state-card__body {
|
||||
padding: 104rpx 40rpx 62rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.album-state-card__body > .app-button {
|
||||
margin-top: 34rpx;
|
||||
}
|
||||
.album-preview {
|
||||
position: fixed;
|
||||
z-index: 80;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(24, 17, 13, 0.96);
|
||||
}
|
||||
.album-preview__toolbar {
|
||||
display: flex;
|
||||
min-height: 96rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(env(safe-area-inset-top) + 12rpx) 24rpx 12rpx;
|
||||
}
|
||||
.album-preview__title {
|
||||
color: #fffaf0;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.album-preview__close {
|
||||
display: flex;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fffaf0;
|
||||
font-size: 25rpx;
|
||||
}
|
||||
.album-preview__image {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.album-preview__footer {
|
||||
padding: 20rpx 28rpx calc(30rpx + env(safe-area-inset-bottom));
|
||||
color: #fffaf0;
|
||||
text-align: center;
|
||||
}
|
||||
.album-preview__position {
|
||||
font-size: 23rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.album-preview__caption {
|
||||
margin-top: 8rpx;
|
||||
color: rgba(255, 250, 240, 0.78);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.album-detail-content {
|
||||
padding-right: 16rpx;
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
.album-photo-grid {
|
||||
gap: 10rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,656 @@
|
||||
<!-- 页面编号:F-09;用途:图片预览、上传与失败状态。 -->
|
||||
<template><ModulePage page-id="f09" /></template>
|
||||
<!-- 页面编号:F-09;用途:照片选择、说明、上传进度与失败重试视觉候选。 -->
|
||||
<template>
|
||||
<view
|
||||
class="media-upload-page"
|
||||
:class="`media-upload-state--${uploadState}`"
|
||||
>
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="media-upload-header"><PageHeader title="上传照片" /></view>
|
||||
|
||||
<view class="media-upload-content">
|
||||
<view class="media-album-card">
|
||||
<image
|
||||
class="media-album-card__cover"
|
||||
:src="mockLibrary[0].src"
|
||||
:alt="mockLibrary[0].alt"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="media-album-card__copy">
|
||||
<text class="media-album-card__eyebrow">当前相册</text>
|
||||
<text class="media-album-card__title">2024 春节团圆</text>
|
||||
<text class="media-album-card__limit">最多可选择 9 张</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="uploadState === 'permission'" class="media-permission-card">
|
||||
<text class="media-state-card__eyebrow">照片访问权限</text>
|
||||
<text class="media-state-card__title">需要照片访问权限</text>
|
||||
<text class="media-state-card__copy"
|
||||
>授权后才能选择要加入当前相册的照片;当前仅展示 H5 审核状态。</text
|
||||
>
|
||||
<view class="media-permission-action" @click="selectMockPhotos">
|
||||
<AppButton block label="重新授权" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="uploadState === 'success'" class="media-success-card">
|
||||
<text class="media-state-card__eyebrow">上传结果</text>
|
||||
<text class="media-success-title">4 张照片已上传</text>
|
||||
<text class="media-state-card__copy"
|
||||
>照片已加入“2024 春节团圆”,可以返回相册继续查看。</text
|
||||
>
|
||||
<view class="media-success-action" @click="returnToAlbum">
|
||||
<AppButton block label="返回当前相册" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="media-section-heading">
|
||||
<text>选择照片</text>
|
||||
<text>{{ selectedPhotos.length }} / {{ MAX_PHOTOS }}</text>
|
||||
</view>
|
||||
|
||||
<view class="media-photo-grid">
|
||||
<view
|
||||
v-for="(photo, index) in selectedPhotos"
|
||||
:key="photo.id"
|
||||
class="media-photo-tile"
|
||||
:class="{
|
||||
'media-photo-tile--active': index === activePhotoIndex,
|
||||
'media-photo-tile--failed': photo.status === 'error',
|
||||
}"
|
||||
@click="selectPhoto(index)"
|
||||
>
|
||||
<image
|
||||
class="media-photo-tile__image"
|
||||
:src="photo.src"
|
||||
:alt="photo.alt"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text class="media-photo-order">{{ index + 1 }}</text>
|
||||
<text
|
||||
v-if="index === activePhotoIndex"
|
||||
class="media-photo-current"
|
||||
>当前</text
|
||||
>
|
||||
<view
|
||||
v-if="!isLocked"
|
||||
class="media-photo-remove"
|
||||
@click.stop="removePhoto(index)"
|
||||
>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
<text
|
||||
v-if="photo.status === 'error'"
|
||||
class="media-photo-status media-photo-status--error"
|
||||
>上传失败</text
|
||||
>
|
||||
<text
|
||||
v-else-if="photo.status === 'uploading'"
|
||||
class="media-photo-status media-photo-status--uploading"
|
||||
>{{ photo.progress }}%</text
|
||||
>
|
||||
<text
|
||||
v-else-if="photo.status === 'uploaded'"
|
||||
class="media-photo-status media-photo-status--uploaded"
|
||||
>已上传</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="selectedPhotos.length < MAX_PHOTOS && !isLocked"
|
||||
class="media-add-tile"
|
||||
@click="selectedPhotos.length ? addMockPhoto() : selectMockPhotos()"
|
||||
>
|
||||
<text class="media-add-tile__title">{{
|
||||
selectedPhotos.length ? "继续添加" : "选择照片"
|
||||
}}</text>
|
||||
<text class="media-add-tile__copy">{{
|
||||
selectedPhotos.length ? `还可选择 ${MAX_PHOTOS - selectedPhotos.length} 张` : "最多 9 张"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="selectedPhotos.length" class="media-description-panel">
|
||||
<view class="media-batch-field">
|
||||
<view class="media-field-heading">
|
||||
<text>整批说明</text><text>必填</text>
|
||||
</view>
|
||||
<textarea
|
||||
v-model="batchDescription"
|
||||
:disabled="isLocked"
|
||||
maxlength="120"
|
||||
placeholder="例如:2024 年春节全家团圆留影"
|
||||
placeholder-class="media-field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="validationMessage" class="media-field-error">{{
|
||||
validationMessage
|
||||
}}</text>
|
||||
|
||||
<view v-if="activePhoto" class="media-photo-field">
|
||||
<view class="media-photo-editor">
|
||||
<image
|
||||
class="media-photo-editor__thumb"
|
||||
:src="activePhoto.src"
|
||||
:alt="activePhoto.alt"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="media-photo-editor__copy">
|
||||
<text class="media-photo-editor__position"
|
||||
>第 {{ activePhotoIndex + 1 }} 张 / 共
|
||||
{{ selectedPhotos.length }} 张</text
|
||||
>
|
||||
<text class="media-photo-editor__hint"
|
||||
>正在编辑此照片的说明</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view class="media-field-heading">
|
||||
<text>当前照片说明</text><text>选填</text>
|
||||
</view>
|
||||
<textarea
|
||||
:value="activePhoto.note"
|
||||
:disabled="isLocked"
|
||||
maxlength="80"
|
||||
placeholder="补充人物、时间或拍摄地点"
|
||||
placeholder-class="media-field-placeholder"
|
||||
@input="updateActiveNote"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-if="uploadState === 'uploading'">
|
||||
<text class="media-progress-copy"
|
||||
>正在上传 2/{{ selectedPhotos.length }}</text
|
||||
>
|
||||
<view class="media-progress-action">
|
||||
<AppButton block label="正在上传" />
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
v-else-if="uploadState === 'error'"
|
||||
class="media-retry-action"
|
||||
@click="retryFailed"
|
||||
>
|
||||
<AppButton block label="重试 2 张失败照片" />
|
||||
</view>
|
||||
<view v-else class="media-primary-action" @click="startUpload">
|
||||
<AppButton
|
||||
block
|
||||
:label="
|
||||
selectedPhotos.length
|
||||
? `上传 ${selectedPhotos.length} 张照片`
|
||||
: '选择照片'
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const MAX_PHOTOS = 9;
|
||||
const uploadState = ref("initial");
|
||||
const activePhotoIndex = ref(0);
|
||||
const batchDescription = ref("");
|
||||
const validationMessage = ref("");
|
||||
|
||||
const mockLibrary = [
|
||||
{
|
||||
id: "reunion",
|
||||
src: "/static/assets/modules/family/f08/f08-reunion-hero.png",
|
||||
alt: "春节团圆时三代家人的合影",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
id: "portrait",
|
||||
src: "/static/assets/modules/family/f08/f08-family-portrait.png",
|
||||
alt: "家人在院落前的春节合影",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
id: "table",
|
||||
src: "/static/assets/modules/family/f08/f08-reunion-table.png",
|
||||
alt: "家人围坐吃年夜饭",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
id: "home",
|
||||
src: "/static/assets/modules/family/f08/f08-ancestral-home.png",
|
||||
alt: "祖居院落的复古旧照",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
id: "ancestor",
|
||||
src: "/static/assets/modules/family/f08/f08-ancestral-portrait.png",
|
||||
alt: "老一辈家人在祖居门前的合影",
|
||||
note: "",
|
||||
},
|
||||
];
|
||||
|
||||
const selectedPhotos = ref([]);
|
||||
const activePhoto = computed(
|
||||
() => selectedPhotos.value[activePhotoIndex.value] || null,
|
||||
);
|
||||
const isLocked = computed(() => uploadState.value === "uploading");
|
||||
|
||||
const makeSelectedPhotos = (state) =>
|
||||
mockLibrary.slice(0, 4).map((photo, index) => ({
|
||||
...photo,
|
||||
note: "",
|
||||
progress: state === "uploading" ? [100, 62, 35, 0][index] : 0,
|
||||
status:
|
||||
state === "error"
|
||||
? index < 2
|
||||
? "error"
|
||||
: "uploaded"
|
||||
: state,
|
||||
}));
|
||||
|
||||
const selectMockPhotos = () => {
|
||||
selectedPhotos.value = makeSelectedPhotos("selected");
|
||||
activePhotoIndex.value = 0;
|
||||
validationMessage.value = "";
|
||||
uploadState.value = "selected";
|
||||
};
|
||||
|
||||
const selectPhoto = (index) => {
|
||||
if (!isLocked.value) activePhotoIndex.value = index;
|
||||
};
|
||||
|
||||
const removePhoto = (index) => {
|
||||
if (isLocked.value) return;
|
||||
selectedPhotos.value.splice(index, 1);
|
||||
activePhotoIndex.value = Math.max(
|
||||
0,
|
||||
Math.min(activePhotoIndex.value, selectedPhotos.value.length - 1),
|
||||
);
|
||||
};
|
||||
|
||||
const addMockPhoto = () => {
|
||||
if (isLocked.value || selectedPhotos.value.length >= MAX_PHOTOS) return;
|
||||
const nextIndex = selectedPhotos.value.length;
|
||||
const next = mockLibrary[nextIndex % mockLibrary.length];
|
||||
selectedPhotos.value.push({
|
||||
...next,
|
||||
id: `${next.id}-${nextIndex + 1}`,
|
||||
note: "",
|
||||
progress: 0,
|
||||
status: "selected",
|
||||
});
|
||||
};
|
||||
|
||||
const updateActiveNote = (event) => {
|
||||
if (!isLocked.value && activePhoto.value) {
|
||||
activePhoto.value.note = event.detail.value;
|
||||
}
|
||||
};
|
||||
|
||||
const startUpload = () => {
|
||||
if (!selectedPhotos.value.length) {
|
||||
selectMockPhotos();
|
||||
return;
|
||||
}
|
||||
if (!batchDescription.value.trim()) {
|
||||
validationMessage.value = "请填写整批照片说明";
|
||||
return;
|
||||
}
|
||||
validationMessage.value = "";
|
||||
selectedPhotos.value = selectedPhotos.value.map((photo, index) => ({
|
||||
...photo,
|
||||
progress: [100, 62, 35, 0][index] || 0,
|
||||
status: "uploading",
|
||||
}));
|
||||
uploadState.value = "uploading";
|
||||
};
|
||||
|
||||
const retryFailed = () => {
|
||||
selectedPhotos.value = selectedPhotos.value.map((photo, index) => ({
|
||||
...photo,
|
||||
progress: [100, 62, 35, 0][index] || 0,
|
||||
status: "uploading",
|
||||
}));
|
||||
uploadState.value = "uploading";
|
||||
};
|
||||
|
||||
const returnToAlbum = () => {
|
||||
uni.redirectTo({ url: "/pages/family/f08-album-detail" });
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
uploadState.value = ["permission", "selected", "uploading", "error", "success"].includes(query.state)
|
||||
? query.state
|
||||
: "initial";
|
||||
|
||||
if (!["initial", "permission"].includes(uploadState.value)) {
|
||||
selectedPhotos.value = makeSelectedPhotos(uploadState.value);
|
||||
batchDescription.value = "春节团圆照片整理";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-upload-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.media-upload-header,
|
||||
.media-upload-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.media-upload-content {
|
||||
padding: 20rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.media-album-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 154rpx;
|
||||
box-sizing: border-box;
|
||||
grid-template-columns: 132rpx minmax(0, 1fr);
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
padding: 14rpx 20rpx;
|
||||
border: 20rpx solid transparent;
|
||||
border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");
|
||||
border-image-slice: 72 fill;
|
||||
border-image-width: 58rpx;
|
||||
border-image-repeat: stretch;
|
||||
}
|
||||
.media-album-card__cover {
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
border: 2rpx solid rgba(179, 133, 63, 0.62);
|
||||
box-shadow: 0 6rpx 14rpx rgba(91, 60, 32, 0.12);
|
||||
}
|
||||
.media-album-card__copy text,
|
||||
.media-state-card__eyebrow,
|
||||
.media-state-card__title,
|
||||
.media-state-card__copy,
|
||||
.media-success-title,
|
||||
.media-photo-status,
|
||||
.media-progress-copy {
|
||||
display: block;
|
||||
}
|
||||
.media-album-card__eyebrow {
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.media-album-card__title {
|
||||
margin-top: 7rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.media-album-card__limit {
|
||||
margin-top: 7rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.media-section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 24rpx 8rpx 12rpx;
|
||||
color: $brand-red;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.media-section-heading text:last-child {
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
.media-photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12rpx;
|
||||
}
|
||||
.media-photo-tile,
|
||||
.media-add-tile {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
aspect-ratio: 1;
|
||||
border: 2rpx solid rgba(179, 133, 63, 0.62);
|
||||
background: rgba(247, 241, 231, 0.88);
|
||||
}
|
||||
.media-photo-tile--active {
|
||||
outline: 4rpx solid rgba(159, 44, 35, 0.78);
|
||||
outline-offset: -4rpx;
|
||||
}
|
||||
.media-photo-tile--failed {
|
||||
border-style: dashed;
|
||||
border-color: #9f2c23;
|
||||
}
|
||||
.media-photo-tile__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.media-photo-order,
|
||||
.media-photo-current,
|
||||
.media-photo-status {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
color: #fffaf0;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.media-photo-order {
|
||||
top: 7rpx;
|
||||
left: 7rpx;
|
||||
min-width: 34rpx;
|
||||
padding: 3rpx 7rpx;
|
||||
border-radius: 999px;
|
||||
background: rgba(43, 31, 23, 0.78);
|
||||
text-align: center;
|
||||
}
|
||||
.media-photo-current {
|
||||
bottom: 7rpx;
|
||||
left: 7rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
background: rgba(74, 45, 27, 0.84);
|
||||
}
|
||||
.media-photo-remove {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
color: #fffaf0;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.media-photo-remove text {
|
||||
padding: 7rpx 8rpx;
|
||||
background: rgba(145, 36, 29, 0.88);
|
||||
}
|
||||
.media-photo-status {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 7rpx 8rpx;
|
||||
background: rgba(58, 43, 32, 0.82);
|
||||
text-align: center;
|
||||
}
|
||||
.media-photo-status--error {
|
||||
background: rgba(132, 35, 29, 0.92);
|
||||
font-weight: 700;
|
||||
}
|
||||
.media-photo-status--uploaded {
|
||||
background: rgba(55, 82, 51, 0.88);
|
||||
}
|
||||
.media-add-tile {
|
||||
display: flex;
|
||||
min-height: 44px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-style: dashed;
|
||||
color: $brand-red;
|
||||
text-align: center;
|
||||
}
|
||||
.media-add-tile__title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.media-add-tile__copy {
|
||||
margin-top: 6rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 19rpx;
|
||||
}
|
||||
.media-description-panel {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.media-batch-field,
|
||||
.media-photo-field {
|
||||
padding: 18rpx 20rpx 20rpx;
|
||||
border: 2rpx solid rgba(179, 133, 63, 0.55);
|
||||
background: rgba(250, 246, 237, 0.76);
|
||||
}
|
||||
.media-photo-field {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.media-photo-editor {
|
||||
display: grid;
|
||||
grid-template-columns: 78rpx minmax(0, 1fr);
|
||||
gap: 14rpx;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 14rpx;
|
||||
border-bottom: 1px solid rgba(179, 133, 63, 0.32);
|
||||
}
|
||||
.media-photo-editor__thumb {
|
||||
width: 78rpx;
|
||||
height: 78rpx;
|
||||
border: 2rpx solid rgba(159, 44, 35, 0.62);
|
||||
}
|
||||
.media-photo-editor__copy text {
|
||||
display: block;
|
||||
}
|
||||
.media-photo-editor__position {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.media-photo-editor__hint {
|
||||
margin-top: 6rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.media-field-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.media-field-heading text:last-child {
|
||||
color: $brand-red;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.media-batch-field textarea,
|
||||
.media-photo-field textarea {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
min-height: 92rpx;
|
||||
margin-top: 12rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.media-field-placeholder {
|
||||
color: #9e8e79;
|
||||
}
|
||||
.media-field-error {
|
||||
display: block;
|
||||
margin: 9rpx 6rpx 0;
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.media-primary-action,
|
||||
.media-retry-action,
|
||||
.media-progress-action {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.media-progress-copy {
|
||||
margin-top: 22rpx;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.media-permission-card,
|
||||
.media-success-card {
|
||||
position: relative;
|
||||
min-height: 410rpx;
|
||||
margin-top: 24rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 98rpx 46rpx 58rpx;
|
||||
border: 20rpx solid transparent;
|
||||
border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");
|
||||
border-image-slice: 72 fill;
|
||||
border-image-width: 64rpx;
|
||||
border-image-repeat: stretch;
|
||||
text-align: center;
|
||||
}
|
||||
.media-state-card__eyebrow {
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.media-state-card__title,
|
||||
.media-success-title {
|
||||
margin-top: 12rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.media-state-card__copy {
|
||||
margin-top: 16rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.media-permission-action,
|
||||
.media-success-action {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.media-upload-content {
|
||||
padding-right: 16rpx;
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
.media-photo-grid {
|
||||
gap: 8rpx;
|
||||
}
|
||||
.media-album-card {
|
||||
grid-template-columns: 116rpx minmax(0, 1fr);
|
||||
gap: 14rpx;
|
||||
}
|
||||
.media-album-card__cover {
|
||||
width: 116rpx;
|
||||
height: 116rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,143 @@
|
||||
<!-- 页面编号:F-10;用途:家族视频列表、详情与待开放状态。 -->
|
||||
<template><ModulePage page-id="f10" /></template>
|
||||
<!-- 页面编号:F-10;用途:家族视频待开放状态。 -->
|
||||
<template>
|
||||
<view class="video-status-page">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="video-status-header"><PageHeader title="家族视频" /></view>
|
||||
|
||||
<view class="video-status-content">
|
||||
<view class="video-status-lead">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>记录会动的家族记忆</text>
|
||||
</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>
|
||||
<text class="video-status-card__copy"
|
||||
>开放后可在这里浏览家族影像与纪念视频</text
|
||||
>
|
||||
<view class="video-return-action" @click="returnToFamily">
|
||||
<AppButton block label="返回家族首页" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const returnToFamily = () => {
|
||||
uni.reLaunch({ url: "/pages/family/f01-family-feed" });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.video-status-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.video-status-header,
|
||||
.video-status-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.video-status-content {
|
||||
padding: 16rpx 26rpx 56rpx;
|
||||
}
|
||||
.video-status-lead {
|
||||
position: relative;
|
||||
height: 56rpx;
|
||||
margin: 0 12rpx 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.video-status-lead image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-lead text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 330rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.video-status-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 70rpx 60rpx 48rpx;
|
||||
}
|
||||
.video-status-card__eyebrow,
|
||||
.video-status-card__title,
|
||||
.video-status-card__copy {
|
||||
display: block;
|
||||
}
|
||||
.video-status-card__eyebrow {
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.video-status-card__title {
|
||||
margin-top: 14rpx;
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.video-status-card__copy {
|
||||
margin-top: 17rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.video-return-action {
|
||||
margin: 28rpx auto 0;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.video-status-content {
|
||||
padding-right: 34rpx;
|
||||
padding-left: 34rpx;
|
||||
}
|
||||
}
|
||||
@media (max-width: 340px) {
|
||||
.video-status-content {
|
||||
padding-right: 20rpx;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
.video-status-card__body {
|
||||
padding-right: 44rpx;
|
||||
padding-left: 44rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -194,8 +194,14 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
}
|
||||
.profile-hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 42rpx minmax(0, 1fr);
|
||||
width: 100%;
|
||||
min-height: 252rpx;
|
||||
align-items: center;
|
||||
gap: 30rpx;
|
||||
padding: 34rpx 52rpx 32rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-hero__frame {
|
||||
@@ -203,6 +209,7 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-hero__hall {
|
||||
position: absolute;
|
||||
@@ -211,6 +218,7 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
width: 330rpx;
|
||||
height: 122rpx;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-hero__cloud {
|
||||
position: absolute;
|
||||
@@ -219,11 +227,10 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
width: 96rpx;
|
||||
height: 52rpx;
|
||||
opacity: 0.32;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-hero__label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 40rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
width: 42rpx;
|
||||
@@ -240,7 +247,6 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.12;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.profile-identity {
|
||||
position: relative;
|
||||
@@ -250,7 +256,6 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 34rpx 52rpx 32rpx 112rpx;
|
||||
}
|
||||
.profile-identity__seal {
|
||||
width: 92rpx;
|
||||
@@ -291,6 +296,7 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-scroll-notice__copy {
|
||||
position: relative;
|
||||
@@ -395,6 +401,7 @@ const openItem = (item) => uni.navigateTo({ url: item.url });
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-error__copy {
|
||||
position: relative;
|
||||
|
||||
@@ -201,24 +201,28 @@ onUnmounted(() => {
|
||||
}
|
||||
.people-search {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 126rpx;
|
||||
width: 100%;
|
||||
height: 82rpx;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
}
|
||||
.people-search > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.people-search input {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 126rpx;
|
||||
bottom: 0;
|
||||
left: 31rpx;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding-left: 31rpx;
|
||||
box-sizing: border-box;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
@@ -226,10 +230,8 @@ onUnmounted(() => {
|
||||
color: #8a7965;
|
||||
}
|
||||
.people-search__action {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
width: 126rpx;
|
||||
height: 100%;
|
||||
@@ -245,9 +247,13 @@ onUnmounted(() => {
|
||||
}
|
||||
.person-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: clamp(92px, 190rpx, 108px);
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding: 16% 10%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.person-card:first-child {
|
||||
margin-top: 0;
|
||||
@@ -257,12 +263,13 @@ onUnmounted(() => {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.person-card__copy {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
inset: 16% 10%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -300,6 +307,7 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
min-height: 118px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.people-result-empty > view,
|
||||
.people-state-card > view {
|
||||
|
||||
@@ -1,5 +1,155 @@
|
||||
<!-- 页面编号:R-02;用途:人物录详情与编辑。 -->
|
||||
<template><ModulePage page-id="r02" /></template>
|
||||
<!-- 页面编号:R-02;用途:人物录详情、同页编辑与受控状态。 -->
|
||||
<template>
|
||||
<view class="person-detail-page" :class="`person-detail-state--${personState}`">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="person-detail-header"><PageHeader :title="personState === 'edit' ? '编辑人物' : '人物详情'" /></view>
|
||||
|
||||
<view v-if="personState === 'loading'" class="person-detail-loading">
|
||||
<AppLoading text="正在读取人物档案" description="请稍候,正在整理人物资料。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="person-detail-content">
|
||||
<template v-if="['detail', 'edit', 'privacy'].includes(personState)">
|
||||
<view class="person-identity-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/r01-person-name-card.png" mode="scaleToFill" />
|
||||
<view class="person-identity-card__copy">
|
||||
<text class="person-identity-card__name">{{ person.name }}</text>
|
||||
<text class="person-identity-card__meta">{{ person.role }} · 第 {{ person.generation }} 世</text>
|
||||
<text class="person-identity-card__hint">人物录档案</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-if="personState === 'detail'">
|
||||
<view v-for="item in detailSections" :key="item.title" class="person-archive-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<view><text>{{ item.title }}</text><text>{{ item.copy }}</text></view>
|
||||
</view>
|
||||
<view class="person-edit-action" @click="enterEdit"><AppButton block label="编辑人物" /></view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="personState === 'edit'">
|
||||
<view v-for="field in shortFields" :key="field.key" class="person-field">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-field-frame.png" mode="scaleToFill" />
|
||||
<text class="person-field__label">{{ field.label }}</text>
|
||||
<input v-model="draft[field.key]" :type="field.key === 'generation' ? 'number' : 'text'" :placeholder="`请输入${field.label}`" />
|
||||
<text v-if="errors[field.key]" class="person-field-error">{{ errors[field.key] }}</text>
|
||||
</view>
|
||||
<view v-for="field in longFields" :key="field.key" class="person-long-field">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<text class="person-long-field__label">{{ field.label }}</text>
|
||||
<textarea v-model="draft[field.key]" :placeholder="`请输入${field.label}`" />
|
||||
</view>
|
||||
<view class="person-edit-actions">
|
||||
<view class="person-save-action" @click="savePerson"><AppButton block label="保存人物" /></view>
|
||||
<view class="person-cancel-action" @click="cancelEdit"><AppButton type="secondary" block label="取消编辑" /></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else-if="personState === 'privacy'" class="person-state-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<view><text>部分资料未公开</text><text>人物小传与家族印记受隐私设置保护,当前只展示公开身份。</text></view>
|
||||
<view class="person-state-action" @click="returnToPeople"><AppButton block label="返回人物录" /></view>
|
||||
</view>
|
||||
|
||||
<view v-else class="person-state-card">
|
||||
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
|
||||
<view>
|
||||
<text>{{ personState === 'expired' ? '人物档案已失效' : '人物档案暂不可用' }}</text>
|
||||
<text>{{ personState === 'expired' ? '这份人物资料已无法查看,请返回人物录选择其他档案。' : '请稍后重新查看,已有资料不会受到影响。' }}</text>
|
||||
</view>
|
||||
<view class="person-state-action" @click="personState === 'expired' ? returnToPeople() : restoreDetail()">
|
||||
<AppButton :type="personState === 'error' ? 'secondary' : 'primary'" block :label="personState === 'expired' ? '返回人物录' : '重新查看'" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" message="人物资料已保存" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const people = [
|
||||
{ id: "1", name: "汤文正", role: "家谱管理员", generation: "18", biography: "勤于修谱,常年整理族中旧照片与口述资料。", legacy: "参与维护汤氏家谱与家风家训。" },
|
||||
{ id: "2", name: "汤淑华", role: "家族长辈", generation: "17", biography: "熟悉家族往事,愿意为后辈讲述旧时记忆。", legacy: "长期参与家族节庆与敬老活动。" },
|
||||
{ id: "3", name: "汤文清", role: "青年代表", generation: "19", biography: "协助整理电子家谱和家族影像资料。", legacy: "推动年轻成员共同参与家谱维护。" },
|
||||
];
|
||||
const person = reactive({ ...people[0] });
|
||||
const draft = reactive({ name: "", role: "", generation: "", biography: "", legacy: "" });
|
||||
const errors = reactive({ name: "", role: "", generation: "" });
|
||||
const personState = ref("loading");
|
||||
const toastVisible = ref(false);
|
||||
let toastTimer = null;
|
||||
const shortFields = [{ key: "name", label: "姓名" }, { key: "role", label: "身份" }, { key: "generation", label: "世代" }];
|
||||
const longFields = [{ key: "biography", label: "人物小传" }, { key: "legacy", label: "家族印记" }];
|
||||
const detailSections = computed(() => [{ title: "人物小传", copy: person.biography }, { title: "家族印记", copy: person.legacy }]);
|
||||
const copyToDraft = () => Object.assign(draft, { name: person.name, role: person.role, generation: person.generation, biography: person.biography, legacy: person.legacy });
|
||||
const clearErrors = () => Object.assign(errors, { name: "", role: "", generation: "" });
|
||||
const enterEdit = () => { copyToDraft(); clearErrors(); personState.value = "edit"; };
|
||||
const cancelEdit = () => { copyToDraft(); clearErrors(); personState.value = "detail"; };
|
||||
const savePerson = () => {
|
||||
clearErrors();
|
||||
if (!String(draft.name).trim()) errors.name = "请填写姓名";
|
||||
if (!String(draft.role).trim()) errors.role = "请填写身份";
|
||||
if (!String(draft.generation).trim()) errors.generation = "请填写世代";
|
||||
if (errors.name || errors.role || errors.generation) return;
|
||||
Object.assign(person, { ...draft, name: draft.name.trim(), role: draft.role.trim(), generation: String(draft.generation).trim() });
|
||||
personState.value = "detail";
|
||||
toastVisible.value = true;
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { toastVisible.value = false; toastTimer = null; }, 1800);
|
||||
};
|
||||
const returnToPeople = () => uni.reLaunch({ url: "/pages/records/r01-people-list" });
|
||||
const restoreDetail = () => { personState.value = "detail"; };
|
||||
onLoad((query) => {
|
||||
const selected = people.find((item) => item.id === String(query.personId || ""));
|
||||
if (selected) Object.assign(person, selected);
|
||||
copyToDraft();
|
||||
const requested = ["loading", "privacy", "expired", "error", "edit"].includes(query.state) ? query.state : "detail";
|
||||
personState.value = selected ? requested : "error";
|
||||
});
|
||||
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.person-detail-page { min-height: 100vh; overflow-x: hidden; background: $paper; }
|
||||
.person-detail-loading { min-height: calc(100vh - 100rpx); }
|
||||
.person-detail-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.person-identity-card,.person-archive-card,.person-field,.person-long-field,.person-state-card { position: relative; }
|
||||
.person-card-skin { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
|
||||
.person-identity-card { display: flex; min-height: 190rpx; padding: 30rpx 10%; box-sizing: border-box; }
|
||||
.person-identity-card__copy { position: relative; z-index: 1; display: flex; flex: 1; flex-direction: column; justify-content: center; }
|
||||
.person-identity-card__copy text { display: block; }
|
||||
.person-identity-card__name { color: $ink; font-family: STKaiti,KaiTi,serif; font-size: 38rpx; font-weight: 700; }
|
||||
.person-identity-card__meta { margin-top: 8rpx; color: $ink-muted; font-size: 25rpx; font-weight: 600; }
|
||||
.person-identity-card__hint { margin-top: 8rpx; color: #806a51; font-size: 21rpx; }
|
||||
.person-archive-card { min-height: 154rpx; margin-top: 14rpx; padding: 32rpx 42rpx; box-sizing: border-box; }
|
||||
.person-archive-card > view { position: relative; z-index: 1; }
|
||||
.person-archive-card text { display: block; }
|
||||
.person-archive-card text:first-child,.person-long-field__label { color: $brand-red; font-size: 24rpx; font-weight: 700; }
|
||||
.person-archive-card text:last-child { margin-top: 11rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
|
||||
.person-edit-action { margin-top: 20rpx; }
|
||||
.person-field { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8rpx 24rpx; min-height: 92rpx; margin-top: 12rpx; padding: 18rpx 28rpx; box-sizing: border-box; }
|
||||
.person-field__label { position: relative; z-index: 1; color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.person-field input { position: relative; z-index: 1; width: 100%; min-width: 0; height: 56rpx; color: $ink; font-size: 23rpx; text-align: right; }
|
||||
.person-field-error { position: relative; z-index: 1; grid-column: 1 / -1; display: block; color: $brand-red; font-size: 21rpx; text-align: right; }
|
||||
.person-long-field { display: flex; flex-direction: column; min-height: 210rpx; margin-top: 14rpx; padding: 28rpx 38rpx; box-sizing: border-box; }
|
||||
.person-long-field__label,.person-long-field textarea { position: relative; z-index: 1; }
|
||||
.person-long-field textarea { width: 100%; height: 112rpx; min-height: 112rpx; margin-top: 14rpx; color: $ink; font-size: 23rpx; line-height: 1.5; }
|
||||
.person-edit-actions { display: flex; flex-direction: column; gap: 14rpx; margin-top: 20rpx; }
|
||||
.person-state-card { display: flex; flex-direction: column; min-height: 300rpx; margin-top: 26rpx; padding: 72rpx 54rpx 42rpx; box-sizing: border-box; text-align: center; }
|
||||
.person-state-card > view { position: relative; z-index: 1; }
|
||||
.person-state-card text { display: block; }
|
||||
.person-state-card text:first-child { color: $ink; font-family: STKaiti,KaiTi,serif; font-size: 34rpx; font-weight: 700; }
|
||||
.person-state-card text:last-child { margin-top: 16rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
|
||||
.person-state-action { margin: 24rpx 48rpx 0; }
|
||||
@media (min-width: 400px) { .person-detail-content { padding-right: 32rpx; padding-left: 32rpx; } }
|
||||
@media (max-width: 340px) { .person-detail-content { padding-right: 18rpx; padding-left: 18rpx; } .person-identity-card__name { font-size: 35rpx; } }
|
||||
</style>
|
||||
|
||||
+130
-138
@@ -25,48 +25,46 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="tree-scroll"
|
||||
scroll-x
|
||||
:scroll-left="initialScrollLeft"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view
|
||||
class="tree-canvas"
|
||||
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
|
||||
<view class="tree-stage">
|
||||
<view v-if="treeState === 'tree'" class="generation-rail">
|
||||
<view
|
||||
v-for="row in generationRows"
|
||||
:key="row.generation"
|
||||
class="generation-band"
|
||||
:style="generationRowStyle(row)"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="generation-band__copy">
|
||||
<text>{{ row.label }}</text>
|
||||
<text>{{ row.summary }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="tree-scroll"
|
||||
:class="{ 'tree-scroll--lineage': treeState === 'tree' }"
|
||||
scroll-x
|
||||
:scroll-left="initialScrollLeft"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<AppLoading
|
||||
v-if="treeState === 'loading'"
|
||||
text="正在整理世系"
|
||||
description="请稍候,正在准备家谱成员关系。"
|
||||
/>
|
||||
<view
|
||||
class="tree-canvas"
|
||||
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
|
||||
>
|
||||
<AppLoading
|
||||
v-if="treeState === 'loading'"
|
||||
text="正在整理世系"
|
||||
description="请稍候,正在准备家谱成员关系。"
|
||||
/>
|
||||
|
||||
<template v-else-if="treeState === 'tree'">
|
||||
<view class="lineage-pan-cue">
|
||||
<text>同代分支可左右查看</text>
|
||||
</view>
|
||||
|
||||
<view class="generation-band generation-band--twelve">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>第十二世 · 始祖</text>
|
||||
</view>
|
||||
<view class="generation-band generation-band--thirteen">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>第十三世 · 两房</text>
|
||||
</view>
|
||||
<view class="generation-band generation-band--fourteen">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<text>第十四世 · 三支</text>
|
||||
</view>
|
||||
<template v-else-if="treeState === 'tree'">
|
||||
<view class="lineage-pan-cue">
|
||||
<text>同代分支可左右查看</text>
|
||||
</view>
|
||||
|
||||
<view class="lineage-connector lineage-connector--root" />
|
||||
<view class="lineage-connector lineage-connector--root-branch" />
|
||||
@@ -103,31 +101,32 @@
|
||||
>
|
||||
<text class="node-years">{{ member.years }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<view v-else class="tree-state-card">
|
||||
<image
|
||||
class="tree-state-card__skin"
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="tree-state-card__content">
|
||||
<text class="tree-state-card__eyebrow">{{
|
||||
stateCopy.eyebrow
|
||||
}}</text>
|
||||
<text class="tree-state-card__title">{{ stateCopy.title }}</text>
|
||||
<text class="tree-state-card__copy">{{ stateCopy.copy }}</text>
|
||||
<view class="tree-state-card__action" @click="handleStateAction">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{ stateCopy.action }}</text>
|
||||
<view v-else class="tree-state-card">
|
||||
<image
|
||||
class="tree-state-card__skin"
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="tree-state-card__content">
|
||||
<text class="tree-state-card__eyebrow">{{
|
||||
stateCopy.eyebrow
|
||||
}}</text>
|
||||
<text class="tree-state-card__title">{{ stateCopy.title }}</text>
|
||||
<text class="tree-state-card__copy">{{ stateCopy.copy }}</text>
|
||||
<view class="tree-state-card__action" @click="handleStateAction">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{ stateCopy.action }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view v-if="treeState === 'tree' && selected" class="member-sheet">
|
||||
<image
|
||||
@@ -143,20 +142,17 @@
|
||||
>
|
||||
</view>
|
||||
<view class="sheet-actions">
|
||||
<view class="sheet-action sheet-member-action" @click="toMember">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-secondary-button-v2.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>查看资料</text>
|
||||
</view>
|
||||
<view class="sheet-action" @click="toAddRelative">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-primary-button-v2.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>添加亲属</text>
|
||||
</view>
|
||||
<AppButton
|
||||
class="sheet-action"
|
||||
type="secondary"
|
||||
label="查看资料"
|
||||
@click="toMember"
|
||||
/>
|
||||
<AppButton
|
||||
class="sheet-action"
|
||||
label="添加亲属"
|
||||
@click="toAddRelative"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -165,6 +161,7 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
@@ -173,7 +170,12 @@ import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
const genealogyId = ref("");
|
||||
const treeState = ref("loading");
|
||||
const selected = ref(null);
|
||||
const initialScrollLeft = ref(20);
|
||||
const initialScrollLeft = ref(90);
|
||||
const generationRows = [
|
||||
{ generation: 12, label: "第十二世", summary: "始祖", y: 135 },
|
||||
{ generation: 13, label: "第十三世", summary: "两房", y: 350 },
|
||||
{ generation: 14, label: "第十四世", summary: "三支", y: 650 },
|
||||
];
|
||||
const members = ref([
|
||||
{
|
||||
id: 101,
|
||||
@@ -287,6 +289,9 @@ const nodeStyle = (member) => ({
|
||||
left: `${member.x}rpx`,
|
||||
top: `${member.y}rpx`,
|
||||
});
|
||||
const generationRowStyle = (row) => ({
|
||||
top: `${row.y - 88}rpx`,
|
||||
});
|
||||
const handleStateAction = () => {
|
||||
if (treeState.value === "empty") {
|
||||
uni.navigateTo({
|
||||
@@ -324,7 +329,7 @@ const toAddRelative = () =>
|
||||
}
|
||||
.tree-page__header,
|
||||
.tree-toolbar,
|
||||
.tree-scroll,
|
||||
.tree-stage,
|
||||
.member-sheet {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -355,11 +360,29 @@ const toAddRelative = () =>
|
||||
color: $brand-red;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.tree-stage {
|
||||
position: relative;
|
||||
height: calc(100vh - 272rpx);
|
||||
}
|
||||
.tree-scroll {
|
||||
width: 100%;
|
||||
height: calc(100vh - 272rpx);
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.generation-rail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
width: 190rpx;
|
||||
box-sizing: border-box;
|
||||
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
|
||||
}
|
||||
.tree-scroll--lineage {
|
||||
width: calc(100% - 190rpx);
|
||||
margin-left: 190rpx;
|
||||
}
|
||||
.tree-canvas {
|
||||
position: relative;
|
||||
width: 900rpx;
|
||||
@@ -380,10 +403,10 @@ const toAddRelative = () =>
|
||||
}
|
||||
.generation-band {
|
||||
position: absolute;
|
||||
left: 24rpx;
|
||||
left: 12rpx;
|
||||
z-index: 3;
|
||||
width: 184rpx;
|
||||
height: 38rpx;
|
||||
width: 166rpx;
|
||||
height: 58rpx;
|
||||
color: #8f6c3f;
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
@@ -394,22 +417,18 @@ const toAddRelative = () =>
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.generation-band text {
|
||||
.generation-band__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.generation-band--twelve {
|
||||
top: 48rpx;
|
||||
}
|
||||
.generation-band--thirteen {
|
||||
top: 250rpx;
|
||||
}
|
||||
.generation-band--fourteen {
|
||||
top: 522rpx;
|
||||
.generation-band__copy text:last-child {
|
||||
margin-top: 1rpx;
|
||||
font-size: 19rpx;
|
||||
}
|
||||
.lineage-connector {
|
||||
position: absolute;
|
||||
@@ -528,12 +547,11 @@ const toAddRelative = () =>
|
||||
color: $brand-red;
|
||||
}
|
||||
.tree-state-card {
|
||||
position: absolute;
|
||||
top: 150rpx;
|
||||
right: 24rpx;
|
||||
left: 24rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: calc(100% - 48rpx);
|
||||
height: 360rpx;
|
||||
margin: 150rpx 24rpx 0;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
@@ -597,18 +615,21 @@ const toAddRelative = () =>
|
||||
right: 18rpx;
|
||||
bottom: calc(14rpx + env(safe-area-inset-bottom));
|
||||
left: 18rpx;
|
||||
height: 190rpx;
|
||||
display: flex;
|
||||
height: 240rpx;
|
||||
flex-direction: column;
|
||||
padding: 38rpx 44rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.member-sheet__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.member-sheet__copy {
|
||||
position: absolute;
|
||||
top: 38rpx;
|
||||
left: 44rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.sheet-name {
|
||||
@@ -625,38 +646,16 @@ const toAddRelative = () =>
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.sheet-actions {
|
||||
position: absolute;
|
||||
right: 36rpx;
|
||||
bottom: 30rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
margin-top: auto;
|
||||
}
|
||||
.sheet-action {
|
||||
position: relative;
|
||||
width: 148rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
.sheet-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.sheet-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: $ink;
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.sheet-action:last-child text {
|
||||
color: #fff9ed;
|
||||
width: 250rpx;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
@@ -668,18 +667,11 @@ const toAddRelative = () =>
|
||||
gap: 14rpx;
|
||||
}
|
||||
.member-sheet {
|
||||
height: 206rpx;
|
||||
}
|
||||
.member-sheet__copy {
|
||||
top: 32rpx;
|
||||
left: 36rpx;
|
||||
}
|
||||
.sheet-actions {
|
||||
right: 28rpx;
|
||||
bottom: 26rpx;
|
||||
}
|
||||
.sheet-action {
|
||||
width: 160rpx;
|
||||
height: 240rpx;
|
||||
padding-top: 32rpx;
|
||||
padding-right: 36rpx;
|
||||
padding-left: 36rpx;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user