修改完成
This commit is contained in:
@@ -22,11 +22,6 @@
|
||||
><text>汤氏家族圈</text><text>家宴、通知与共同记忆</text></view
|
||||
>
|
||||
<view v-if="feedState !== 'loading'" class="feed-shortcuts">
|
||||
<image
|
||||
class="feed-shortcuts__skin"
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
v-for="item in shortcuts"
|
||||
:key="item.key"
|
||||
@@ -42,11 +37,6 @@
|
||||
class="feed-card"
|
||||
@click="openDetail(item)"
|
||||
>
|
||||
<image
|
||||
class="feed-card__skin"
|
||||
src="/static/assets/modules/family/transparent/f01-family-letter-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="feed-card__copy">
|
||||
<text class="feed-card__meta"
|
||||
>{{ item.tag }} · {{ item.time }}</text
|
||||
@@ -58,11 +48,7 @@
|
||||
</view>
|
||||
</template>
|
||||
<view v-else-if="feedState !== 'loading'" class="feed-state-card">
|
||||
<image
|
||||
src="/static/assets/modules/family/transparent/f01-family-letter-card.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
<view class="feed-state-card__copy"
|
||||
><text>{{
|
||||
feedState === "empty" ? "还没有家族动态" : "家族动态暂不可用"
|
||||
}}</text
|
||||
@@ -77,10 +63,7 @@
|
||||
v-if="feedState !== 'loading'"
|
||||
class="feed-action"
|
||||
@click="feedState === 'error' ? (feedState = 'list') : toPublish()"
|
||||
><image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>{{
|
||||
><text>{{
|
||||
feedState === "error" ? "重新查看" : "发布家族动态"
|
||||
}}</text></view
|
||||
>
|
||||
@@ -121,6 +104,10 @@ const shortcuts = [
|
||||
{ key: "albums", label: "相册" },
|
||||
{ key: "rituals", label: "礼仪" },
|
||||
{ key: "memos", label: "备忘" },
|
||||
{ key: "people", label: "人物录" },
|
||||
{ key: "gifts", label: "贺礼簿" },
|
||||
{ key: "merits", label: "功德录" },
|
||||
{ key: "videos", label: "家族视频" },
|
||||
];
|
||||
onLoad((query) => {
|
||||
genealogyId.value =
|
||||
@@ -146,23 +133,29 @@ const openSection = (key) => {
|
||||
albums: "/pages/family/f07-album-list",
|
||||
rituals: "/pages/records/r05-ritual-list",
|
||||
memos: "/pages/records/r10-memo-list",
|
||||
people: "/pages/records/r01-people-list",
|
||||
gifts: "/pages/records/r03-gift-list",
|
||||
merits: "/pages/records/r11-merit-records",
|
||||
videos: "/pages/family/f10-video-list",
|
||||
};
|
||||
uni.navigateTo({ url: routes[key] });
|
||||
uni.navigateTo({
|
||||
url: `${routes[key]}?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.family-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.family-page__header,
|
||||
.feed-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.feed-content {
|
||||
flex: 1;
|
||||
padding: 24rpx 24rpx 190rpx;
|
||||
}
|
||||
.feed-heading text {
|
||||
@@ -180,23 +173,14 @@ const openSection = (key) => {
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.feed-shortcuts {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
.feed-shortcuts__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.feed-shortcut {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
min-height: 44px;
|
||||
@@ -212,28 +196,17 @@ const openSection = (key) => {
|
||||
}
|
||||
.feed-card,
|
||||
.feed-state-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 230rpx;
|
||||
min-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,
|
||||
.feed-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/family/transparent/f01-family-letter-card.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.feed-card__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 15% 9%;
|
||||
}
|
||||
.feed-card text {
|
||||
@@ -265,9 +238,7 @@ const openSection = (key) => {
|
||||
.feed-state-card {
|
||||
margin-top: 70rpx;
|
||||
}
|
||||
.feed-state-card > view {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.feed-state-card__copy {
|
||||
padding: 23% 10%;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
@@ -288,14 +259,13 @@ const openSection = (key) => {
|
||||
line-height: 1.45;
|
||||
}
|
||||
.feed-action {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 76rpx;
|
||||
margin-top: 19rpx;
|
||||
width: 514rpx;
|
||||
max-width: 100%;
|
||||
min-height: 76rpx;
|
||||
margin: 19rpx auto 0;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.feed-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -10,19 +10,13 @@
|
||||
><ModulePageBackground module="family" /><view class="publish-page__header"
|
||||
><PageHeader title="发布动态" /></view
|
||||
><view class="publish-panel"
|
||||
><image
|
||||
class="publish-panel__skin"
|
||||
src="/static/assets/modules/family/transparent/module-content-frame.png"
|
||||
mode="scaleToFill" /><view
|
||||
><view
|
||||
v-if="publishState === 'form'"
|
||||
class="publish-form"
|
||||
><text>记录此刻</text
|
||||
><text>分享通知、活动、家族故事或一段共同记忆。</text
|
||||
><view class="publish-field"
|
||||
><image
|
||||
src="/static/assets/modules/family/transparent/module-field-frame.png"
|
||||
mode="scaleToFill"
|
||||
/><textarea
|
||||
><textarea
|
||||
v-model="content"
|
||||
maxlength="300"
|
||||
placeholder="写下想对家人说的话"
|
||||
@@ -87,15 +81,14 @@ onUnmounted(() => {
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.publish-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.publish-page__header,
|
||||
.publish-panel {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.publish-panel {
|
||||
width: calc(100% - 32rpx);
|
||||
@@ -103,18 +96,7 @@ onUnmounted(() => {
|
||||
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: relative;
|
||||
z-index: 1;
|
||||
background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.publish-form > text,
|
||||
.publish-result > text {
|
||||
@@ -135,25 +117,16 @@ onUnmounted(() => {
|
||||
line-height: 1.6;
|
||||
}
|
||||
.publish-field {
|
||||
position: relative;
|
||||
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;
|
||||
background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.publish-field textarea {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
min-height: 200rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
|
||||
@@ -1,5 +1,153 @@
|
||||
<!-- 页面编号:F-03;用途:动态详情、评论、点赞与删除确认。 -->
|
||||
<template><ModulePage page-id="f03" /></template>
|
||||
<!-- 页面编号:F-03;用途:动态详情、评论与内容失效状态。 -->
|
||||
<template>
|
||||
<view class="feed-detail-page" :class="{ 'feed-state--expired': feedState === 'expired', 'feed-state--error': feedState === 'error', 'feed-state--ready': feedState === 'ready' }">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="feed-detail-header"><PageHeader title="动态详情" /></view>
|
||||
|
||||
<view v-if="feedState === 'loading'" class="feed-detail-loading">
|
||||
<AppLoading text="正在读取家族动态" description="请稍候,正在整理正文与家人评论。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="feed-detail-content">
|
||||
<template v-if="feedState === 'ready'">
|
||||
<view class="feed-article-card">
|
||||
<text class="feed-article-card__meta">{{ currentFeed.tag }} · {{ currentFeed.time }}</text>
|
||||
<text class="feed-article-card__title">{{ currentFeed.title }}</text>
|
||||
<text class="feed-article-card__body">{{ currentFeed.content }}</text>
|
||||
<text class="feed-article-card__author">发布人:{{ currentFeed.author }}</text>
|
||||
</view>
|
||||
|
||||
<view class="feed-comments-panel">
|
||||
<view class="feed-comments-heading">
|
||||
<text>家人评论</text><text>{{ feedComments.length }} 条</text>
|
||||
</view>
|
||||
<view v-for="comment in feedComments" :key="comment.id" class="feed-comment-card">
|
||||
<view><text>{{ comment.author }}</text><text>{{ comment.time }}</text></view>
|
||||
<text>{{ comment.content }}</text>
|
||||
</view>
|
||||
<view v-if="!feedComments.length" class="feed-comments-empty">
|
||||
<text>还没有评论</text><text>写下第一句祝福或共同记忆。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="feed-comment-form" :class="{ 'comment-state--saving': commentState === 'saving', 'comment-state--error': commentState === 'error' }">
|
||||
<text>写下评论</text>
|
||||
<textarea v-model="commentDraft" auto-height maxlength="240" placeholder="对家人说点什么" />
|
||||
<text v-if="commentError" class="feed-comment-error">{{ commentError }}</text>
|
||||
<AppButton block :disabled="commentState === 'saving'" :label="commentState === 'saving' ? '正在发送' : '发送评论'" @click="submitComment" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="feed-state-card">
|
||||
<text>{{ feedState === 'expired' ? '动态已失效' : '动态暂不可用' }}</text>
|
||||
<text>{{ feedState === 'expired' ? '这条动态可能已被发布人删除,请返回家族圈查看其他内容。' : '请稍后重新查看,已有家族记录不会受到影响。' }}</text>
|
||||
<AppButton :type="feedState === 'error' ? 'secondary' : 'primary'" block :label="feedState === 'error' ? '重新查看' : '返回家族圈'" @click="feedState === 'error' ? restoreFeed() : backToFamily()" />
|
||||
</view>
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" message="评论已发送" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { onUnmounted, 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 feedRecords = [
|
||||
{ id: "1", tag: "团圆记忆", time: "今天 10:24", title: "端午家宴", content: "今年端午全家相聚,长辈讲起祖居旧事,孩子们也为大家拍下了新的全家福。饭后我们把照片和口述片段整理进家族档案,让这份热闹成为往后仍能翻看的共同记忆。", author: "汤正国" },
|
||||
{ id: "2", tag: "家族通知", time: "昨天 18:02", title: "修谱资料征集", content: "请家人补充老照片中的人物姓名、拍摄时间和地点。无法确认的信息也可以先写下线索,由熟悉往事的长辈共同核对。", author: "谱主" },
|
||||
];
|
||||
const feedId = ref("1");
|
||||
const currentFeed = ref(feedRecords[0]);
|
||||
const feedState = ref("loading");
|
||||
const commentState = ref("idle");
|
||||
const commentDraft = ref("");
|
||||
const commentError = ref("");
|
||||
const toastVisible = ref(false);
|
||||
const forceCommentFailure = ref(false);
|
||||
const feedComments = ref([
|
||||
{ id: 1, author: "汤淑华", time: "今天 10:42", content: "一家人能常常相聚,就是最珍贵的福气。" },
|
||||
{ id: 2, author: "汤文清", time: "今天 11:08", content: "照片已经整理好了,晚些时候放进春节团圆相册。" },
|
||||
]);
|
||||
let submitTimer = null;
|
||||
let toastTimer = null;
|
||||
|
||||
onLoad((query) => {
|
||||
feedId.value = String(query.feedId || "1");
|
||||
const selected = feedRecords.find((item) => item.id === feedId.value);
|
||||
currentFeed.value = selected || feedRecords[0];
|
||||
forceCommentFailure.value = query.commentResult === "error";
|
||||
feedState.value = ["loading", "error", "expired"].includes(query.state)
|
||||
? query.state
|
||||
: selected
|
||||
? "ready"
|
||||
: "expired";
|
||||
});
|
||||
|
||||
const submitComment = () => {
|
||||
if (commentState.value === "saving") return;
|
||||
const content = commentDraft.value.trim();
|
||||
if (!content) {
|
||||
commentError.value = "请先写下评论内容";
|
||||
return;
|
||||
}
|
||||
commentError.value = "";
|
||||
commentState.value = "saving";
|
||||
submitTimer = setTimeout(() => {
|
||||
if (forceCommentFailure.value) {
|
||||
commentState.value = "error";
|
||||
commentError.value = "评论发送失败,请保留文字后重试";
|
||||
forceCommentFailure.value = false;
|
||||
return;
|
||||
}
|
||||
feedComments.value.push({ id: Date.now(), author: "我", time: "刚刚", content });
|
||||
commentDraft.value = "";
|
||||
commentState.value = "idle";
|
||||
toastVisible.value = true;
|
||||
toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
|
||||
}, 320);
|
||||
};
|
||||
const restoreFeed = () => { feedState.value = "ready"; };
|
||||
const backToFamily = () => uni.redirectTo({ url: "/pages/family/f01-family-feed" });
|
||||
|
||||
onUnmounted(() => {
|
||||
if (submitTimer) clearTimeout(submitTimer);
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.feed-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.feed-detail-header, .feed-detail-loading, .feed-detail-content { z-index: 1; }
|
||||
.feed-detail-loading { min-height: calc(100vh - 100rpx); }
|
||||
.feed-detail-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.feed-article-card, .feed-comments-panel, .feed-comment-form, .feed-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.feed-article-card { padding: 46rpx 48rpx 42rpx; }
|
||||
.feed-article-card text, .feed-state-card > text { display: block; }
|
||||
.feed-article-card__meta, .feed-article-card__author { color: $ink-muted; font-size: 22rpx; }
|
||||
.feed-article-card__title { margin-top: 12rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 36rpx; font-weight: 700; }
|
||||
.feed-article-card__body { margin-top: 18rpx; color: $ink; font-size: 25rpx; line-height: 1.75; }
|
||||
.feed-article-card__author { margin-top: 22rpx; }
|
||||
.feed-comments-panel { margin-top: 18rpx; padding: 38rpx 38rpx 34rpx; }
|
||||
.feed-comments-heading { display: flex; align-items: center; justify-content: space-between; color: $brand-red; font-size: 24rpx; font-weight: 700; }
|
||||
.feed-comments-heading text:last-child { color: $ink-muted; font-size: 21rpx; font-weight: 400; }
|
||||
.feed-comment-card { margin-top: 16rpx; padding: 20rpx 22rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.feed-comment-card > view { display: flex; justify-content: space-between; gap: 18rpx; color: $ink-muted; font-size: 20rpx; }
|
||||
.feed-comment-card > text { display: block; margin-top: 9rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
|
||||
.feed-comments-empty { padding: 34rpx 10rpx 20rpx; text-align: center; }
|
||||
.feed-comments-empty text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.6; }
|
||||
.feed-comments-empty text:first-child { color: $ink; font-size: 28rpx; font-weight: 700; }
|
||||
.feed-comment-form { margin-top: 18rpx; padding: 38rpx 40rpx 42rpx; }
|
||||
.feed-comment-form > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
|
||||
.feed-comment-form textarea { width: auto; min-width: 0; min-height: 126rpx; margin-top: 16rpx; padding: 20rpx 22rpx; box-sizing: border-box; color: $ink; font-size: 24rpx; line-height: 1.55; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.feed-comment-error { display: block; margin-top: 10rpx; color: $brand-red; font-size: 22rpx; }
|
||||
.feed-comment-form .app-button { margin-top: 22rpx; }
|
||||
.feed-state-card { min-height: 340rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.feed-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.feed-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.feed-state-card .app-button { margin-top: 30rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,117 @@
|
||||
<!-- 页面编号:F-04;用途:谱文分类与文章列表。 -->
|
||||
<template><ModulePage page-id="f04" /></template>
|
||||
<!-- 页面编号:F-04;用途:谱文分类、搜索、列表与新建入口。 -->
|
||||
<template>
|
||||
<view class="article-list-page" :class="{ 'article-list-state--loading': listState === 'loading', 'article-list-state--empty': listState === 'empty', 'article-list-state--error': listState === 'error' }">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="article-list-header"><PageHeader title="谱文" action="新建" @action="createArticle" /></view>
|
||||
|
||||
<view v-if="listState === 'loading'" class="article-list-loading">
|
||||
<AppLoading text="正在整理家族谱文" description="请稍候,正在读取家训、往事与序言。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="article-list-content">
|
||||
<template v-if="listState === 'ready'">
|
||||
<view class="article-search">
|
||||
<input v-model="keyword" placeholder="搜索标题、作者或正文" confirm-type="search" />
|
||||
</view>
|
||||
<view class="article-categories">
|
||||
<view class="article-categories__row">
|
||||
<view v-for="category in articleCategories" :key="category" class="article-category" :class="{ 'article-category--active': activeCategory === category }" @click="activeCategory = category"><text>{{ category }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="filteredArticles.length" class="article-list">
|
||||
<view v-for="article in filteredArticles" :key="article.id" class="article-card" role="button" :aria-label="`查看谱文${article.title}`" @click="openArticle(article)">
|
||||
<text class="article-card__category">{{ article.category }}</text>
|
||||
<text class="article-card__title">{{ article.title }}</text>
|
||||
<text class="article-card__summary">{{ article.summary }}</text>
|
||||
<view class="article-card__meta"><text>{{ article.author }}</text><text>{{ article.updatedAt }}</text></view>
|
||||
</view>
|
||||
<AppButton block label="新建谱文" @click="createArticle" />
|
||||
</view>
|
||||
|
||||
<view v-else class="article-list-state-card">
|
||||
<text>{{ keyword || activeCategory !== '全部' ? '没有找到相关谱文' : '还没有谱文' }}</text>
|
||||
<text>{{ keyword || activeCategory !== '全部' ? '换一个关键词或分类继续查找。' : '从第一篇家训、序言或家族往事开始记录。' }}</text>
|
||||
<AppButton block :label="keyword || activeCategory !== '全部' ? '清空筛选' : '新建谱文'" @click="keyword || activeCategory !== '全部' ? resetFilters() : createArticle()" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="article-list-state-card">
|
||||
<text>{{ listState === 'empty' ? '还没有谱文' : '谱文列表暂不可用' }}</text>
|
||||
<text>{{ listState === 'empty' ? '记录第一篇家风家训或家族往事。' : '请稍后重新查看,已有谱文不会受到影响。' }}</text>
|
||||
<AppButton :type="listState === 'error' ? 'secondary' : 'primary'" block :label="listState === 'error' ? '重新查看' : '新建谱文'" @click="listState === 'error' ? restoreArticles() : createArticle()" />
|
||||
</view>
|
||||
</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 AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
|
||||
const articleCategories = ["全部", "家风家训", "家族往事", "族谱序言"];
|
||||
const baseArticles = [
|
||||
{ id: "101", category: "家风家训", title: "孝友传家的日常", summary: "从敬老、睦亲与守信的小事里,看见家风如何代代相传。", author: "汤文正", updatedAt: "今天更新" },
|
||||
{ id: "102", category: "家族往事", title: "祖居门前的那棵桂花树", summary: "长辈口述的旧居记忆,以及每年中秋一家人相聚的故事。", author: "汤淑华", updatedAt: "昨天更新" },
|
||||
{ id: "103", category: "族谱序言", title: "续修族谱序", summary: "说明本次续修的缘起、资料来源与共同参与的家人。", author: "谱主", updatedAt: "5 月 12 日" },
|
||||
];
|
||||
const articles = ref([...baseArticles]);
|
||||
const activeCategory = ref("全部");
|
||||
const keyword = ref("");
|
||||
const listState = ref("loading");
|
||||
const filteredArticles = computed(() => {
|
||||
const term = keyword.value.trim().toLowerCase();
|
||||
return articles.value.filter((article) => {
|
||||
const categoryMatched = activeCategory.value === "全部" || article.category === activeCategory.value;
|
||||
const keywordMatched = !term || `${article.title} ${article.summary} ${article.author}`.toLowerCase().includes(term);
|
||||
return categoryMatched && keywordMatched;
|
||||
});
|
||||
});
|
||||
|
||||
onLoad((query) => {
|
||||
const count = Math.max(1, Math.min(Number(query.count) || baseArticles.length, 50));
|
||||
articles.value = Array.from({ length: count }, (_, index) => ({
|
||||
...baseArticles[index % baseArticles.length],
|
||||
id: String(101 + index),
|
||||
title: count > baseArticles.length ? `${baseArticles[index % baseArticles.length].title}(第 ${index + 1} 篇)` : baseArticles[index].title,
|
||||
}));
|
||||
listState.value = ["loading", "empty", "error"].includes(query.state) ? query.state : "ready";
|
||||
});
|
||||
|
||||
const openArticle = (article) => uni.navigateTo({ url: `/pages/family/f05-article-detail?articleId=${article.id}` });
|
||||
const createArticle = () => uni.navigateTo({ url: "/pages/family/f06-article-editor?mode=create" });
|
||||
const restoreArticles = () => { listState.value = "ready"; };
|
||||
const resetFilters = () => { keyword.value = ""; activeCategory.value = "全部"; };
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.article-list-header, .article-list-loading, .article-list-content { z-index: 1; }
|
||||
.article-list-loading { min-height: calc(100vh - 100rpx); }
|
||||
.article-list-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.article-search { min-height: 82rpx; padding: 12rpx 26rpx; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-search input { width: 100%; min-height: 58rpx; color: $ink; font-size: 24rpx; }
|
||||
.article-categories { width: 100%; margin-top: 16rpx; }
|
||||
.article-categories__row { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 2rpx 4rpx 8rpx; }
|
||||
.article-category { min-height: 58rpx; padding: 0 28rpx; color: $ink-muted; font-size: 23rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-category text { display: flex; min-height: 58rpx; align-items: center; }
|
||||
.article-category--active { color: $brand-red; font-weight: 700; }
|
||||
.article-list { display: flex; flex-direction: column; gap: 16rpx; margin-top: 10rpx; }
|
||||
.article-card, .article-list-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-card { min-height: 196rpx; padding: 34rpx 46rpx 30rpx; }
|
||||
.article-card > text { display: block; }
|
||||
.article-card__category { color: $brand-red; font-size: 21rpx; font-weight: 700; letter-spacing: 2rpx; }
|
||||
.article-card__title { margin-top: 7rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 31rpx; font-weight: 700; }
|
||||
.article-card__summary { margin-top: 10rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.55; }
|
||||
.article-card__meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6rpx 18rpx; margin-top: 12rpx; color: $ink-muted; font-size: 20rpx; }
|
||||
.article-list > .app-button { margin-top: 8rpx; }
|
||||
.article-list-state-card { min-height: 340rpx; margin-top: 30rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.article-list-state-card > text { display: block; }
|
||||
.article-list-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.article-list-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.article-list-state-card .app-button { margin-top: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,113 @@
|
||||
<!-- 页面编号:F-05;用途:谱文详情。 -->
|
||||
<template><ModulePage page-id="f05" /></template>
|
||||
<!-- 页面编号:F-05;用途:谱文正文、收藏、编辑与受控状态。 -->
|
||||
<template>
|
||||
<view class="article-detail-page" :class="articleStateClasses">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="article-detail-header"><PageHeader title="谱文详情" /></view>
|
||||
|
||||
<view v-if="articleState === 'loading'" class="article-detail-loading">
|
||||
<AppLoading text="正在读取谱文" description="请稍候,正在整理正文与收录信息。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="article-detail-content">
|
||||
<template v-if="articleState === 'ready'">
|
||||
<view class="article-paper">
|
||||
<text class="article-paper__category">{{ article.category }}</text>
|
||||
<text class="article-paper__title">{{ article.title }}</text>
|
||||
<view class="article-paper__meta"><text>{{ article.author }}</text><text>{{ article.updatedAt }}</text></view>
|
||||
<view class="article-paper__body">
|
||||
<text v-for="(paragraph, index) in articleParagraphs" :key="index">{{ paragraph }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="article-actions">
|
||||
<AppButton block :label="favorite ? '已收藏谱文' : '收藏谱文'" @click="toggleFavorite" />
|
||||
<AppButton type="secondary" block label="编辑谱文" @click="editArticle" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="article-state-card">
|
||||
<text>{{ stateCopy.title }}</text>
|
||||
<text>{{ stateCopy.copy }}</text>
|
||||
<AppButton :type="articleState === 'error' ? 'secondary' : 'primary'" block :label="stateCopy.action" @click="handleStateAction" />
|
||||
</view>
|
||||
</view>
|
||||
<AppToast :visible="toastVisible" :message="favorite ? '已收藏谱文' : '已取消收藏'" />
|
||||
</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 articleRecords = [
|
||||
{ id: "101", category: "家风家训", title: "孝友传家的日常", author: "汤文正", updatedAt: "2024 年 5 月 12 日", paragraphs: ["孝友传家,不只在族谱序言里,也在一家人每日的言行中。长辈以宽厚待晚辈,晚辈以耐心照料长辈,亲友之间守信互助,便是最朴素也最长久的家风。", "勤俭并非一味节省,而是珍惜所得、量入为出,也愿意在家人需要时伸出援手。家中每一代人都可以用自己的方式,把这份分寸与担当继续传下去。", "敬祖睦宗,最终是为了让今天的家人彼此认识、彼此关心。记录姓名与世代之外,也应留下真实的生活、共同经历和温暖记忆。"] },
|
||||
{ id: "102", category: "家族往事", title: "祖居门前的那棵桂花树", author: "汤淑华", updatedAt: "2024 年 5 月 10 日", paragraphs: ["祖居门前曾有一棵桂花树。每到中秋,院里都是清甜的香气,远道回来的家人也总能循着那股味道找到家门。", "后来房屋几经修缮,桂花树仍被大家小心保留下来。它见过孩子长大,也见过长辈把往事一遍遍讲给后来人。"] },
|
||||
{ id: "103", category: "族谱序言", title: "续修族谱序", author: "谱主", updatedAt: "2024 年 5 月 8 日", paragraphs: ["本次续修以旧谱、碑记、户籍资料和长辈口述为基础,由家人共同核对补充。凡暂不能确认之处,均保留来源和疑问,留待后续查证。", "愿这份记录不仅理清世系,也能保存家风、人物与共同记忆。"] },
|
||||
];
|
||||
const articleId = ref("101");
|
||||
const article = reactive({ ...articleRecords[0] });
|
||||
const articleState = ref("loading");
|
||||
const favorite = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
let toastTimer = null;
|
||||
const articleParagraphs = computed(() => article.paragraphs || []);
|
||||
const articleStateClasses = computed(() => ({
|
||||
[`article-state--${articleState.value}`]: true,
|
||||
"article-state--expired": articleState.value === "expired",
|
||||
"article-state--privacy": articleState.value === "privacy",
|
||||
"article-state--error": articleState.value === "error",
|
||||
}));
|
||||
const stateCopy = computed(() => ({
|
||||
expired: { title: "这篇谱文已无法查看", copy: "内容可能已被作者删除或取消公开,请返回谱文列表查看其他内容。", action: "返回谱文列表" },
|
||||
privacy: { title: "这篇谱文暂未公开", copy: "作者仅向有权限的家人开放正文,请返回谱文列表查看其他内容。", action: "返回谱文列表" },
|
||||
error: { title: "谱文暂不可用", copy: "请稍后重新查看,已有谱文不会受到影响。", action: "重新查看" },
|
||||
}[articleState.value] || {}));
|
||||
|
||||
onLoad((query) => {
|
||||
articleId.value = String(query.articleId || "101");
|
||||
const selected = articleRecords.find((item) => item.id === articleId.value);
|
||||
if (selected) Object.assign(article, selected);
|
||||
articleState.value = ["loading", "error", "expired", "privacy"].includes(query.state)
|
||||
? query.state
|
||||
: selected
|
||||
? "ready"
|
||||
: "expired";
|
||||
});
|
||||
|
||||
const toggleFavorite = () => {
|
||||
favorite.value = !favorite.value;
|
||||
toastVisible.value = true;
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
|
||||
};
|
||||
const editArticle = () => uni.navigateTo({ url: `/pages/family/f06-article-editor?mode=edit&articleId=${articleId.value}` });
|
||||
const backToArticles = () => uni.redirectTo({ url: "/pages/family/f04-article-list" });
|
||||
const handleStateAction = () => { if (articleState.value === "error") articleState.value = "ready"; else backToArticles(); };
|
||||
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.article-detail-header, .article-detail-loading, .article-detail-content { z-index: 1; }
|
||||
.article-detail-loading { min-height: calc(100vh - 100rpx); }
|
||||
.article-detail-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.article-paper, .article-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.article-paper { padding: 50rpx 48rpx 54rpx; }
|
||||
.article-paper > text { display: block; }
|
||||
.article-paper__category { color: $brand-red; font-size: 22rpx; font-weight: 700; letter-spacing: 3rpx; }
|
||||
.article-paper__title { margin-top: 12rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 39rpx; font-weight: 700; line-height: 1.35; }
|
||||
.article-paper__meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6rpx 18rpx; margin-top: 16rpx; color: $ink-muted; font-size: 21rpx; }
|
||||
.article-paper__body { margin-top: 28rpx; }
|
||||
.article-paper__body text { display: block; margin-top: 18rpx; color: $ink; font-size: 25rpx; line-height: 1.85; text-align: justify; }
|
||||
.article-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14rpx; margin-top: 18rpx; }
|
||||
.article-state-card { min-height: 350rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.article-state-card > text { display: block; }
|
||||
.article-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.article-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.article-state-card .app-button { margin-top: 30rpx; }
|
||||
@media (max-width: 340px) { .article-actions { grid-template-columns: minmax(0, 1fr); } }
|
||||
</style>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
class="editor-control"
|
||||
:class="{ 'editor-control--error': fieldErrors.title }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<input
|
||||
v-model="form.title"
|
||||
maxlength="40"
|
||||
@@ -67,7 +66,6 @@
|
||||
class="editor-control"
|
||||
:class="{ 'editor-control--error': fieldErrors.category }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<input
|
||||
v-model="form.category"
|
||||
maxlength="20"
|
||||
@@ -87,7 +85,6 @@
|
||||
class="editor-control editor-control--textarea"
|
||||
:class="{ 'editor-control--error': fieldErrors.content }"
|
||||
>
|
||||
<image :src="fieldFrame" mode="scaleToFill" />
|
||||
<textarea
|
||||
v-model="form.content"
|
||||
maxlength="1200"
|
||||
@@ -127,8 +124,6 @@ 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: "家风家训",
|
||||
@@ -136,6 +131,9 @@ const draftFixture = {
|
||||
"孝友传家,勤俭立业;敬祖睦宗,诚实待人。愿后人常怀感恩,彼此扶持。",
|
||||
};
|
||||
const editorState = ref("form");
|
||||
const articleId = ref("");
|
||||
const editorMode = ref("create");
|
||||
const simulateSaveFailure = ref(false);
|
||||
const allowedStates = new Set([
|
||||
"draft",
|
||||
"loading",
|
||||
@@ -156,8 +154,8 @@ const actionLabel = computed(() =>
|
||||
: "保存谱文",
|
||||
);
|
||||
|
||||
const fillDraft = (failed = false) => {
|
||||
Object.assign(form, draftFixture, failed ? { title: "保存失败" } : {});
|
||||
const fillDraft = () => {
|
||||
Object.assign(form, draftFixture);
|
||||
};
|
||||
const showAllFieldErrors = () => {
|
||||
fieldErrors.title = "请填写文章标题";
|
||||
@@ -166,9 +164,15 @@ const showAllFieldErrors = () => {
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
articleId.value = query.articleId || "";
|
||||
editorMode.value = articleId.value || query.mode === "edit" ? "edit" : "create";
|
||||
const requestedState = allowedStates.has(query.state) ? query.state : "form";
|
||||
if (["draft", "saving", "error"].includes(requestedState)) {
|
||||
fillDraft(requestedState === "error");
|
||||
simulateSaveFailure.value = requestedState === "error";
|
||||
if (
|
||||
editorMode.value === "edit" ||
|
||||
["draft", "saving", "error"].includes(requestedState)
|
||||
) {
|
||||
fillDraft();
|
||||
}
|
||||
if (requestedState === "validation") showAllFieldErrors();
|
||||
editorState.value = requestedState;
|
||||
@@ -197,12 +201,18 @@ const submit = () => {
|
||||
}
|
||||
editorState.value = "saving";
|
||||
saveTimer = setTimeout(() => {
|
||||
editorState.value =
|
||||
form.title.trim() === "保存失败" ? "error" : "success";
|
||||
editorState.value = simulateSaveFailure.value ? "error" : "success";
|
||||
simulateSaveFailure.value = false;
|
||||
saveTimer = null;
|
||||
}, 320);
|
||||
};
|
||||
const returnToList = () => uni.navigateBack();
|
||||
const returnToList = () =>
|
||||
uni.redirectTo({
|
||||
url:
|
||||
editorMode.value === "edit" && articleId.value
|
||||
? `/pages/family/f05-article-detail?articleId=${articleId.value}`
|
||||
: "/pages/family/f04-article-list",
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
@@ -211,16 +221,15 @@ onUnload(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-editor-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.article-editor-page__header,
|
||||
.article-editor-loading,
|
||||
.article-editor-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.article-editor-loading {
|
||||
padding-top: 150rpx;
|
||||
@@ -230,7 +239,6 @@ onUnload(() => {
|
||||
}
|
||||
.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");
|
||||
@@ -240,8 +248,6 @@ onUnload(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.editor-panel__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 38rpx 34rpx 42rpx;
|
||||
}
|
||||
.editor-eyebrow {
|
||||
@@ -279,22 +285,13 @@ onUnload(() => {
|
||||
font-weight: 700;
|
||||
}
|
||||
.editor-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.editor-control > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat;
|
||||
}
|
||||
.editor-control input,
|
||||
.editor-control textarea {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: $ink;
|
||||
@@ -302,17 +299,17 @@ onUnload(() => {
|
||||
}
|
||||
.editor-control input {
|
||||
width: 100%;
|
||||
height: 82rpx;
|
||||
min-height: 82rpx;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
.editor-control--textarea {
|
||||
height: 220rpx;
|
||||
min-height: 220rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.editor-control textarea {
|
||||
width: 100%;
|
||||
height: 180rpx;
|
||||
min-height: 180rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.editor-control--error {
|
||||
@@ -350,8 +347,6 @@ onUnload(() => {
|
||||
min-height: 420rpx;
|
||||
}
|
||||
.editor-result-card__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 112rpx 52rpx 68rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,131 @@
|
||||
<!-- 页面编号:F-07;用途:家族相册列表。 -->
|
||||
<template><ModulePage page-id="f07" /></template>
|
||||
<!-- 页面编号:F-07;用途:家族相册列表、创建与受控状态。 -->
|
||||
<template>
|
||||
<view class="album-list-page" :class="albumStateClasses">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="album-list-header"><PageHeader title="家族相册" action="新建" @action="createAlbum" /></view>
|
||||
|
||||
<view v-if="albumState === 'loading'" class="album-list-loading">
|
||||
<AppLoading text="正在整理家族相册" description="请稍候,正在读取照片与更新时间。" />
|
||||
</view>
|
||||
|
||||
<view v-else class="album-list-content">
|
||||
<template v-if="albumState === 'ready'">
|
||||
<view class="album-list-lead"><text>让每一张照片都回到家人身边</text><text>共 {{ albums.length }} 本相册</text></view>
|
||||
<view v-if="albums.length" class="album-list">
|
||||
<view v-for="album in albums" :key="album.id" class="album-card" role="button" :aria-label="`打开相册${album.name}`" @click="openAlbum(album)">
|
||||
<view class="album-card__media"><image class="album-card__cover" :src="album.cover" mode="aspectFill" :alt="album.name" /></view>
|
||||
<view class="album-card__copy">
|
||||
<text>{{ album.name }}</text>
|
||||
<text>{{ album.photoCount }} 张照片 · {{ album.updatedAt }}</text>
|
||||
<text>{{ album.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<AppButton block label="新建相册" @click="createAlbum" />
|
||||
</view>
|
||||
<view v-else class="album-state-card">
|
||||
<text>还没有相册</text><text>创建一本相册,把团圆、成长与祖居旧影整理在一起。</text>
|
||||
<AppButton block label="新建相册" @click="createAlbum" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="album-state-card">
|
||||
<text>{{ albumState === 'empty' ? '还没有相册' : '相册暂不可用' }}</text>
|
||||
<text>{{ albumState === 'empty' ? '从第一本团圆相册开始收集家族影像。' : '请稍后重新查看,已有照片不会受到影响。' }}</text>
|
||||
<AppButton :type="albumState === 'error' ? 'secondary' : 'primary'" block :label="albumState === 'error' ? '重新查看' : '新建相册'" @click="albumState === 'error' ? restoreAlbums() : createAlbum()" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog :visible="dialogVisible" eyebrow="新建相册" title="为家人整理一段影像" message="相册创建后可继续添加照片和说明。" confirm-text="创建相册" cancel-text="取消" show-cancel @confirm="confirmCreateAlbum" @cancel="closeCreateDialog">
|
||||
<view class="album-dialog-field">
|
||||
<text>相册名称</text>
|
||||
<input v-model="albumNameDraft" maxlength="30" placeholder="例如:春节团圆" />
|
||||
<text v-if="albumNameError">{{ albumNameError }}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppToast :visible="toastVisible" message="相册已创建" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModulePage from "@/components/ModulePage.vue";
|
||||
import { computed, onUnmounted, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.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 baseAlbums = [
|
||||
{ id: "201", name: "2024 春节团圆", photoCount: 18, updatedAt: "今天更新", description: "三代家人的团圆饭与院前合影", cover: "/static/assets/modules/family/f08/f08-reunion-hero.png" },
|
||||
{ id: "202", name: "祖居旧影", photoCount: 32, updatedAt: "5 月 10 日更新", description: "祖居、旧物与长辈珍藏的老照片", cover: "/static/assets/modules/family/f08/f08-ancestral-home.png" },
|
||||
{ id: "203", name: "儿童成长", photoCount: 46, updatedAt: "持续更新", description: "记录孩子们每一个值得珍藏的瞬间", cover: "/static/assets/modules/family/f08/f08-family-portrait.png" },
|
||||
];
|
||||
const albums = ref([...baseAlbums]);
|
||||
const albumState = ref("loading");
|
||||
const dialogVisible = ref(false);
|
||||
const albumNameDraft = ref("");
|
||||
const albumNameError = ref("");
|
||||
const toastVisible = ref(false);
|
||||
let toastTimer = null;
|
||||
const albumStateClasses = computed(() => ({
|
||||
[`album-list-state--${albumState.value}`]: true,
|
||||
"album-state--empty": albumState.value === "empty",
|
||||
"album-list-state--loading": albumState.value === "loading",
|
||||
"album-list-state--error": albumState.value === "error",
|
||||
}));
|
||||
|
||||
onLoad((query) => {
|
||||
const count = Math.max(1, Math.min(Number(query.count) || baseAlbums.length, 30));
|
||||
albums.value = Array.from({ length: count }, (_, index) => ({
|
||||
...baseAlbums[index % baseAlbums.length],
|
||||
id: String(201 + index),
|
||||
name: count > baseAlbums.length ? `${baseAlbums[index % baseAlbums.length].name}(${index + 1})` : baseAlbums[index].name,
|
||||
}));
|
||||
albumState.value = ["loading", "empty", "error"].includes(query.state) ? query.state : "ready";
|
||||
});
|
||||
|
||||
const openAlbum = (album) => uni.navigateTo({ url: `/pages/family/f08-album-detail?albumId=${album.id}` });
|
||||
const createAlbum = () => { albumNameDraft.value = ""; albumNameError.value = ""; dialogVisible.value = true; };
|
||||
const closeCreateDialog = () => { dialogVisible.value = false; };
|
||||
const confirmCreateAlbum = () => {
|
||||
const name = albumNameDraft.value.trim();
|
||||
if (!name) { albumNameError.value = "请填写相册名称"; return; }
|
||||
albums.value.unshift({ id: String(Date.now()), name, photoCount: 0, updatedAt: "刚刚创建", description: "等待添加第一张照片", cover: "/static/assets/modules/family/f08/f08-reunion-hero.png" });
|
||||
albumState.value = "ready";
|
||||
dialogVisible.value = false;
|
||||
toastVisible.value = true;
|
||||
toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
|
||||
};
|
||||
const restoreAlbums = () => { albumState.value = "ready"; };
|
||||
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.album-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.album-list-header, .album-list-loading, .album-list-content { z-index: 1; }
|
||||
.album-list-loading { min-height: calc(100vh - 100rpx); }
|
||||
.album-list-content { padding: 18rpx 24rpx 72rpx; }
|
||||
.album-list-lead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8rpx 18rpx; min-height: 62rpx; padding: 0 20rpx; color: $ink-muted; font-size: 22rpx; background: url("/static/assets/modules/genealogy/transparent/section-divider.png") center / 100% 100% no-repeat; }
|
||||
.album-list { display: flex; flex-direction: column; gap: 16rpx; margin-top: 16rpx; }
|
||||
.album-card, .album-state-card { width: 100%; box-sizing: border-box; background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat; }
|
||||
.album-card { display: grid; grid-template-columns: minmax(150rpx, 0.7fr) minmax(0, 1.3fr); min-height: 210rpx; gap: 22rpx; padding: 30rpx 38rpx; }
|
||||
.album-card__media { width: 100%; aspect-ratio: 4 / 3; align-self: center; }
|
||||
.album-card__cover { display: block; width: 100%; height: 100%; }
|
||||
.album-card__copy { align-self: center; min-width: 0; }
|
||||
.album-card__copy text { display: block; overflow-wrap: anywhere; }
|
||||
.album-card__copy text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
|
||||
.album-card__copy text:nth-child(2) { margin-top: 9rpx; color: $brand-red; font-size: 21rpx; }
|
||||
.album-card__copy text:last-child { margin-top: 8rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.5; }
|
||||
.album-list > .app-button { margin-top: 8rpx; }
|
||||
.album-state-card { min-height: 340rpx; margin-top: 30rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||||
.album-state-card > text { display: block; }
|
||||
.album-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||||
.album-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||||
.album-state-card .app-button { margin-top: 28rpx; }
|
||||
.album-dialog-field { width: 100%; margin: 24rpx 0; text-align: left; }
|
||||
.album-dialog-field > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }
|
||||
.album-dialog-field input { width: 100%; min-height: 76rpx; margin-top: 10rpx; padding: 0 22rpx; box-sizing: border-box; color: $ink; font-size: 24rpx; background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat; }
|
||||
.album-dialog-field > text:last-child { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; }
|
||||
@media (max-width: 340px) { .album-card { grid-template-columns: 130rpx minmax(0, 1fr); gap: 16rpx; padding-right: 30rpx; padding-left: 30rpx; } }
|
||||
</style>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="album-upload-action" @click="showUploadNotice">
|
||||
<view class="album-upload-action" @click="toUpload">
|
||||
<AppButton block label="添加照片" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -93,24 +93,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppToast :visible="uploadNoticeVisible" :message="uploadNoticeMessage" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad } 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 albumId = ref("");
|
||||
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" },
|
||||
@@ -131,19 +127,17 @@ const closePreview = () => {
|
||||
albumState.value = "normal";
|
||||
};
|
||||
|
||||
const showUploadNotice = () => {
|
||||
uploadNoticeVisible.value = true;
|
||||
clearTimeout(uploadNoticeTimer);
|
||||
uploadNoticeTimer = setTimeout(() => {
|
||||
uploadNoticeVisible.value = false;
|
||||
}, 1800);
|
||||
};
|
||||
const toUpload = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/family/f09-media-upload?albumId=${albumId.value}`,
|
||||
});
|
||||
|
||||
const returnToAlbums = () => {
|
||||
uni.redirectTo({ url: "/pages/family/f07-album-list" });
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
albumId.value = query.albumId || "reunion";
|
||||
albumState.value =
|
||||
query.state === "empty"
|
||||
? "empty"
|
||||
@@ -161,22 +155,18 @@ onBackPress(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
clearTimeout(uploadNoticeTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.album-detail-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.album-detail-header,
|
||||
.album-detail-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.album-detail-content {
|
||||
padding: 22rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
@@ -265,7 +255,6 @@ onUnload(() => {
|
||||
}
|
||||
.album-empty-state,
|
||||
.album-expired-state {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 430rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -237,6 +237,7 @@ const mockLibrary = [
|
||||
];
|
||||
|
||||
const selectedPhotos = ref([]);
|
||||
const albumId = ref("");
|
||||
const activePhoto = computed(
|
||||
() => selectedPhotos.value[activePhotoIndex.value] || null,
|
||||
);
|
||||
@@ -322,10 +323,13 @@ const retryFailed = () => {
|
||||
};
|
||||
|
||||
const returnToAlbum = () => {
|
||||
uni.redirectTo({ url: "/pages/family/f08-album-detail" });
|
||||
uni.redirectTo({
|
||||
url: `/pages/family/f08-album-detail?albumId=${albumId.value}`,
|
||||
});
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
albumId.value = query.albumId || "reunion";
|
||||
uploadState.value = ["permission", "selected", "uploading", "error", "success"].includes(query.state)
|
||||
? query.state
|
||||
: "initial";
|
||||
@@ -339,21 +343,19 @@ onLoad((query) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-upload-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.media-upload-header,
|
||||
.media-upload-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
}
|
||||
.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;
|
||||
@@ -420,7 +422,6 @@ onLoad((query) => {
|
||||
}
|
||||
.media-photo-tile,
|
||||
.media-add-tile {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
overflow: hidden;
|
||||
@@ -429,6 +430,9 @@ onLoad((query) => {
|
||||
border: 2rpx solid rgba(179, 133, 63, 0.62);
|
||||
background: rgba(247, 241, 231, 0.88);
|
||||
}
|
||||
.media-photo-tile {
|
||||
position: relative;
|
||||
}
|
||||
.media-photo-tile--active {
|
||||
outline: 4rpx solid rgba(159, 44, 35, 0.78);
|
||||
outline-offset: -4rpx;
|
||||
@@ -570,7 +574,6 @@ onLoad((query) => {
|
||||
.media-batch-field textarea,
|
||||
.media-photo-field textarea {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
min-height: 92rpx;
|
||||
margin-top: 12rpx;
|
||||
box-sizing: border-box;
|
||||
@@ -601,7 +604,6 @@ onLoad((query) => {
|
||||
}
|
||||
.media-permission-card,
|
||||
.media-success-card {
|
||||
position: relative;
|
||||
min-height: 410rpx;
|
||||
margin-top: 24rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -46,56 +46,47 @@ const returnToFamily = () => {
|
||||
|
||||
<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;
|
||||
display: grid;
|
||||
min-height: 56rpx;
|
||||
margin: 0 12rpx 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.video-status-lead image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-lead text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 330rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.video-status-card__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-status-card__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1;
|
||||
padding: 70rpx 60rpx 48rpx;
|
||||
}
|
||||
.video-status-card__eyebrow,
|
||||
|
||||
Reference in New Issue
Block a user