完成80%

This commit is contained in:
2026-07-27 06:50:07 +08:00
parent 8475bbd19a
commit 1eae3bbef4
63 changed files with 13664 additions and 280 deletions
+11 -5
View File
@@ -14,7 +14,10 @@
</view>
<view class="publish-field">
<text class="publish-field__label">动态类型</text>
<input v-model="form.feedType" placeholder="留空时服务端默认为 text" placeholder-class="publish-placeholder" @input="submitError = ''" />
<view class="publish-field__value publish-field__value--readonly">
<text>文字动态</text>
<text>当前仅支持此类型</text>
</view>
</view>
<view class="publish-field publish-field--media">
<view>
@@ -65,7 +68,7 @@ import { isImagePickCancelled, pickAndUploadImage } from "@/utils/resumable-imag
import { goBack, handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const form = reactive({ feedContent: "", feedType: "", sortOrder: "" });
const form = reactive({ feedContent: "", feedType: "text", sortOrder: "" });
const mediaReceipts = ref([]);
const publishState = ref("form");
const isSubmitting = ref(false);
@@ -76,7 +79,7 @@ const discardVisible = ref(false);
const requestController = createRequestController();
const mediaOssIds = computed(() => mediaReceipts.value.map((item) => item.ossId).join(","));
const isDirty = computed(() => Boolean(
form.feedContent.trim() || form.feedType.trim() || form.sortOrder.trim() || mediaReceipts.value.length,
form.feedContent.trim() || form.sortOrder.trim() || mediaReceipts.value.length,
));
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const resultCopy = computed(() => ({
@@ -139,7 +142,7 @@ const submit = async () => {
mediaOssIds: mediaOssIds.value,
sortOrder: form.sortOrder,
}, { requestController });
Object.assign(form, { feedContent: "", feedType: "", sortOrder: "" });
Object.assign(form, { feedContent: "", feedType: "text", sortOrder: "" });
mediaReceipts.value = [];
publishState.value = "success";
} catch (error) {
@@ -191,10 +194,13 @@ onUnmounted(() => {
.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; line-height: 1; transform: translateY(-3rpx); }
.publish-field input, .publish-field textarea { display: block; box-sizing: border-box; width: 100%; margin-top: 12rpx; color: $ink; font-size: 24rpx; }
.publish-field input { min-height: 64rpx; }
.publish-field__value--readonly { display:flex; min-height:76rpx; align-items:center; justify-content:space-between; margin-top:12rpx; padding:0 20rpx; border:1rpx solid rgba(128,89,49,.22); border-radius:10rpx; background:rgba(255,252,245,.48); }
.publish-field__value--readonly text:first-child { color:$ink; font-size:24rpx; }
.publish-field__value--readonly text:last-child { color:$ink-muted; font-size:20rpx; }
.publish-field--content textarea { min-height: 200rpx; line-height: 1.7; }
.publish-field--media { display: grid; gap: 12rpx; }
.publish-field__hint { display: block; margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }
.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; }
.publish-placeholder { color: #8e806e; }
.publish-error { margin-top: 12rpx; color: $brand-red; font-size: 22rpx; line-height: 1.5; }
+33
View File
@@ -18,6 +18,15 @@
<text>发布{{ feed.publisher }}</text>
<text>{{ feed.likeCount }} 次点赞 · {{ feed.commentCount }} 条评论</text>
</view>
<AppButton
compact
type="secondary"
:disabled="isTogglingLike || !hasLikeState"
:label="isTogglingLike ? '正在提交' : (hasLikeState ? (feed.likedByMe ? '取消点赞' : '点赞') : '点赞状态不可用')"
@click="toggleLike"
/>
<text v-if="!hasLikeState" class="like-note">点赞状态暂不可用页面不会猜测下一次操作</text>
<text v-if="likeError" class="like-error">{{ likeError }}</text>
</view>
<view class="comment-section">
@@ -82,6 +91,8 @@ const comments = ref([]);
const commentDraft = ref("");
const commentError = ref("");
const isSubmittingComment = ref(false);
const isTogglingLike = ref(false);
const likeError = ref("");
const controller = createRequestController();
let pageActive = true;
const feedTypeLabel = (type) => String(type || "").trim().toLowerCase() === "text" ? "文字动态" : "家族动态";
@@ -89,6 +100,7 @@ const feedTypeLabel = (type) => String(type || "").trim().toLowerCase() === "tex
const hasValidContext = computed(() =>
/^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(feedId.value),
);
const hasLikeState = computed(() => typeof feed.value?.likedByMe === "boolean");
const stateCopy = computed(() => {
if (!hasValidContext.value) {
return {
@@ -151,6 +163,23 @@ const refresh = async () => {
if (feedState.value === "ready") await loadComments();
};
const toggleLike = async () => {
if (isTogglingLike.value || !hasValidContext.value || !feed.value || !hasLikeState.value) return;
const nextLiked = !feed.value.likedByMe;
isTogglingLike.value = true;
likeError.value = "";
try {
await appApi.setFeedLike(genealogyId.value, feedId.value, nextLiked, { requestController: controller });
if (!pageActive) return;
await loadFeed();
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
likeError.value = error?.message || "点赞操作失败,请稍后重试";
} finally {
if (pageActive) isTogglingLike.value = false;
}
};
const submitComment = async () => {
if (isSubmittingComment.value || !hasValidContext.value) return;
const commentContent = commentDraft.value.trim();
@@ -208,6 +237,10 @@ onBackPress((event) => handleBackPress(event, requestBack));
.feed-card__heading text:last-child, .comment-card__heading text:last-child { color: $ink-muted; font-size: 21rpx; text-align: right; }
.feed-card__content { display: block; margin-top: 20rpx; color: $ink; font-size: 29rpx; line-height: 1.7; white-space: pre-wrap; }
.feed-card__meta { margin-top: 22rpx; color: $ink-muted; font-size: 21rpx; }
.feed-card .app-button { margin-top: 18rpx; }
.like-note, .like-error { display: block; margin-top: 10rpx; font-size: 22rpx; }
.like-note { color: $ink-muted; }
.like-error { color: $brand-red; }
.comment-section { margin-top: 18rpx; padding: 28rpx; }
.section-title { display: block; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 32rpx; font-weight: 700; }
.comment-list { margin-top: 18rpx; }
+1 -1
View File
@@ -208,7 +208,7 @@ onUnmounted(() => {
.editor-field--cover { display: grid; gap: 12rpx; padding: 18rpx 22rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); }
.editor-field--cover .editor-field__label { margin: 0; }
.editor-field__hint { display: block; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }
.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; }
.editor-placeholder { color: #9e8e79; }
.editor-save-error { display: block; margin: 12rpx 8rpx 0; color: $brand-red; font-size: 24rpx; line-height: 34rpx; text-align: center; }
+1 -1
View File
@@ -101,5 +101,5 @@ onUnmounted(() => { active = false; controller.abort(); });
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.album-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }.album-list-header, .album-list-content { z-index: 1; }.album-list-content { padding: 18rpx 24rpx 72rpx; }.album-state-card, .album-card { @include adaptive.adaptive-family-content; }.album-state-card { min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }.album-state-card text, .album-card text { display: block; }.album-state-card text:first-child, .album-card text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }.album-state-card text:nth-child(2), .album-card text:not(:first-child) { margin-top: 12rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.55; }.album-state-card .app-button { margin-top: 28rpx; }.album-list { display: flex; flex-direction: column; gap: 16rpx; }.album-card { padding: 28rpx 30rpx; }.album-dialog-field { width: 100%; margin: 20rpx 0; text-align: left; }.album-dialog-field > text:first-child, .album-dialog-field--cover > view > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; }.album-dialog-field input, .album-dialog-field textarea { @include adaptive.adaptive-family-field; box-sizing: border-box; display: block; width: 100%; margin-top: 10rpx; padding: 16rpx 22rpx; color: $ink; font-size: 24rpx; }.album-dialog-field input { min-height: 76rpx; padding-top: 0; padding-bottom: 0; }.album-dialog-field textarea { min-height: 112rpx; line-height: 1.55; }.album-dialog-field--cover { display: grid; gap: 12rpx; padding: 18rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); }.album-field-hint { display: block; margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }.upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; }.album-field-error { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; line-height: 1.45; }
.album-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }.album-list-header, .album-list-content { z-index: 1; }.album-list-content { padding: 18rpx 24rpx 72rpx; }.album-state-card, .album-card { @include adaptive.adaptive-family-content; }.album-state-card { min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }.album-state-card text, .album-card text { display: block; }.album-state-card text:first-child, .album-card text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }.album-state-card text:nth-child(2), .album-card text:not(:first-child) { margin-top: 12rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.55; }.album-state-card .app-button { margin-top: 28rpx; }.album-list { display: flex; flex-direction: column; gap: 16rpx; }.album-card { padding: 28rpx 30rpx; }.album-dialog-field { width: 100%; margin: 20rpx 0; text-align: left; }.album-dialog-field > text:first-child, .album-dialog-field--cover > view > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; }.album-dialog-field input, .album-dialog-field textarea { @include adaptive.adaptive-family-field; box-sizing: border-box; display: block; width: 100%; margin-top: 10rpx; padding: 16rpx 22rpx; color: $ink; font-size: 24rpx; }.album-dialog-field input { min-height: 76rpx; padding-top: 0; padding-bottom: 0; }.album-dialog-field textarea { min-height: 112rpx; line-height: 1.55; }.album-dialog-field--cover { display: grid; gap: 12rpx; padding: 18rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); }.album-field-hint { display: block; margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }.upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; }.album-field-error { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; line-height: 1.45; }
</style>
+21 -6
View File
@@ -27,9 +27,17 @@
<text class="media-field__label">拍摄人</text>
<input v-model="form.photographer" maxlength="30" placeholder="请输入拍摄人" placeholder-class="placeholder" @input="submitError = ''" />
</view>
<view class="media-field">
<view class="media-field media-field--picker">
<text class="media-field__label">拍摄日期</text>
<picker mode="date" :value="form.shootDate" @change="selectShootDate">
<view :class="{ placeholder: !form.shootDate }">{{ form.shootDate || '请选择' }}</view>
</picker>
</view>
<view class="media-field media-field--picker">
<text class="media-field__label">拍摄时间</text>
<input v-model="form.shootTime" maxlength="30" placeholder="例如:2026-07-24 10:00:00" placeholder-class="placeholder" @input="submitError = ''" />
<picker mode="time" :value="form.shootClock" @change="selectShootClock">
<view :class="{ placeholder: !form.shootClock }">{{ form.shootClock || '请选择' }}</view>
</picker>
</view>
<view class="media-field">
<text class="media-field__label">排序值</text>
@@ -79,13 +87,14 @@ const uploadError = ref("");
const submitError = ref("");
const uploading = ref(false);
const submitting = ref(false);
const form = reactive({ photoTitle: "", photoDesc: "", photographer: "", shootTime: "", sortOrder: "" });
const form = reactive({ photoTitle: "", photoDesc: "", photographer: "", shootDate: "", shootClock: "", sortOrder: "" });
const controller = createRequestController();
const discardVisible = ref(false);
const confirmation = createDiscardConfirmation((visible) => { discardVisible.value = visible; });
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(albumId.value));
const isDirty = computed(() => receipt.value || Object.values(form).some((value) => value.trim()));
const isDirty = computed(() => Boolean(receipt.value) || Object.values(form).some((value) => value.trim()));
const shootTime = computed(() => form.shootDate ? `${form.shootDate} ${form.shootClock || "00:00"}:00` : "");
const stateCopy = computed(() => pageState.value === "success"
? { title: "照片已提交服务端", copy: "服务端已返回成功结果。", action: "返回相册" }
: { title: "照片入口无效", copy: "没有取得有效家谱或相册标识。", action: "返回上一页" });
@@ -110,6 +119,8 @@ const selectPhoto = async () => {
uploading.value = false;
}
};
const selectShootDate = (event) => { form.shootDate = event.detail.value || ""; submitError.value = ""; };
const selectShootClock = (event) => { form.shootClock = event.detail.value || ""; submitError.value = ""; };
const submitPhoto = async () => {
if (uploading.value || submitting.value || !hasValidContext.value) return;
@@ -120,8 +131,10 @@ const submitPhoto = async () => {
submitting.value = true;
submitError.value = "";
try {
const { shootDate, shootClock, ...photoForm } = form;
await appApi.createAlbumPhoto(genealogyId.value, albumId.value, {
...form,
...photoForm,
shootTime: shootTime.value,
ossId: toConsumerOssId(receipt.value.ossId),
}, { requestController: controller });
pageState.value = "success";
@@ -167,11 +180,13 @@ onUnmounted(() => {
.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; line-height: 1; transform: translateY(-3rpx); }
.media-field input, .media-field textarea { @include adaptive.adaptive-family-field; box-sizing: border-box; display: block; width: 100%; color: $ink; font-size: 24rpx; }
.media-field input { min-height: 78rpx; padding: 0 22rpx; }
.media-field--picker picker { display: block; min-height: 78rpx; }
.media-field--picker picker > view { display: flex; min-height: 78rpx; align-items: center; padding: 0 22rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); color: $ink; font-size: 24rpx; }
.media-field textarea { min-height: 116rpx; padding: 18rpx 22rpx; line-height: 1.55; }
.placeholder { color: #9e8e79; }
.media-field--upload { display: grid; gap: 12rpx; padding: 18rpx 20rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); }
.media-field--upload .media-field__label { margin: 0; }
.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; }
.field-error { margin-top: 10rpx; color: $brand-red; font-size: 22rpx; line-height: 1.45; }
.media-panel .app-button { margin-top: 28rpx; }
+1 -1
View File
@@ -1,6 +1,6 @@
<!-- 页面编号F-10用途家族视频当前缺读取发布和播放业务 owner -->
<template>
<view class="video-page"><ModulePageBackground module="family" /><view class="page-header"><PageHeader title="家族视频" custom-back @back="returnToFamily" /></view><view class="page-content"><view class="state-card"><text>视频服务暂未开放</text><text>Apifox 当前只有删除视频 operation未声明视频列表详情播放地址发布编辑评论点赞或分享 owner页面不以相册参考项目或本地数据补造视频能力</text><AppButton block :label="hasValidContext ? '返回家族动态' : '返回上一页'" @click="returnToFamily" /></view></view></view>
<view class="video-page"><ModulePageBackground module="family" /><view class="page-header"><PageHeader title="家族视频" custom-back @back="returnToFamily" /></view><view class="page-content"><view class="state-card"><text>家族视频暂未开放</text><text>视频功能正在准备中开放后家人的影像会在这里展示</text><AppButton block :label="hasValidContext ? '返回家族动态' : '返回上一页'" @click="returnToFamily" /></view></view></view>
</template>
<script setup>
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"; import { goBack,returnTo } from "@/utils/navigation.js"; const genealogyId=ref("");const hasValidContext=computed(()=>/^[1-9]\d*$/.test(genealogyId.value));onLoad((query)=>{genealogyId.value=String(query?.genealogyId||"");});const returnToFamily=()=>hasValidContext.value?returnTo("F01",{genealogyId:genealogyId.value}):goBack();