feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
<view class="page-content">
|
||||
<view v-if="view === 'form'" class="form-card">
|
||||
<text>{{ isEdit ? '编辑成长记录' : '新建成长记录' }}</text>
|
||||
<text class="form-copy">{{ isEdit ? '原有图片和相关设置会保留。' : '红色 * 为必填项,其余内容可按需补充。' }}</text>
|
||||
<text class="form-copy">{{ isEdit ? '原有图片、视频和相关设置会保留,也可以逐项移除。' : '红色 * 为必填项,其余内容可按需补充。' }}</text>
|
||||
<view class="field field--picker">
|
||||
<text>关联人物</text>
|
||||
<picker
|
||||
@@ -106,23 +106,32 @@
|
||||
>
|
||||
<view class="upload-field">
|
||||
<view
|
||||
><text>相关图片</text
|
||||
><text>相关图片或视频</text
|
||||
><text
|
||||
>图片上传成功后,会随这条记录一起保存。</text
|
||||
>媒体上传成功后,会随这条记录一起保存。</text
|
||||
></view
|
||||
>
|
||||
<button
|
||||
class="upload-button"
|
||||
:disabled="uploading || submitting"
|
||||
@click="uploadImage"
|
||||
>
|
||||
{{ uploading ? "上传中…" : "添加图片" }}
|
||||
</button>
|
||||
<text
|
||||
<view class="upload-actions">
|
||||
<button
|
||||
class="upload-button"
|
||||
:disabled="uploading || submitting"
|
||||
@click="uploadImage"
|
||||
>添加图片</button>
|
||||
<button
|
||||
class="upload-button"
|
||||
:disabled="uploading || submitting"
|
||||
@click="uploadVideo"
|
||||
>添加视频</button>
|
||||
</view>
|
||||
<text v-if="uploading">正在上传媒体…</text>
|
||||
<view
|
||||
v-for="(receipt, index) in mediaReceipts"
|
||||
:key="`${receipt.ossId}-${index}`"
|
||||
>已上传:{{ receipt.fileName || "图片" }}</text
|
||||
class="upload-receipt"
|
||||
>
|
||||
<text>已上传:{{ receipt.fileName || "媒体文件" }}</text>
|
||||
<button :disabled="uploading || submitting" @click="removeMedia(index)">移除</button>
|
||||
</view>
|
||||
<text v-if="uploadError" class="error">{{ uploadError }}</text>
|
||||
</view>
|
||||
<text v-if="error" class="error">{{ error }}</text>
|
||||
@@ -184,6 +193,7 @@
|
||||
><text
|
||||
>{{ growthRecordTypeLabel(item)
|
||||
}}{{ item.date ? ` · ${item.date}` : "" }}</text
|
||||
><text v-if="item.createTime">创建于 {{ formatMinuteTimestamp(item.createTime) }}</text
|
||||
><text v-if="item.content">{{ item.content }}</text></view
|
||||
>
|
||||
<AppButton
|
||||
@@ -222,9 +232,9 @@
|
||||
/>
|
||||
<AppDialog
|
||||
:visible="deleteConfirmationVisible"
|
||||
title="删除这条成长记录?"
|
||||
message="删除后无法恢复,请确认当前内容不再需要。"
|
||||
confirm-text="确认删除"
|
||||
title="将这条成长记录移至回收站?"
|
||||
message="移入回收站后不再展示,家谱管理员可在保留期内恢复。"
|
||||
confirm-text="移至回收站"
|
||||
cancel-text="保留记录"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@@ -252,10 +262,13 @@ import { lifeRecordApi } from "@/services/api/life-record-service.js";
|
||||
import { lineageApi } from "@/services/api/lineage-service.js";
|
||||
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import { formatMinuteTimestamp } from "@/utils/display-time.js";
|
||||
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
|
||||
import {
|
||||
isImagePickCancelled,
|
||||
isVideoPickCancelled,
|
||||
pickAndUploadImage,
|
||||
pickAndUploadVideo,
|
||||
} from "@/utils/media-upload.js";
|
||||
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation/gateway.js";
|
||||
|
||||
@@ -358,7 +371,11 @@ const formSnapshot = computed(() =>
|
||||
const dirty = computed(() =>
|
||||
isEdit.value
|
||||
? Boolean(formBaseline.value) && formSnapshot.value !== formBaseline.value
|
||||
: Object.values(form).some((value) => String(value).trim()) ||
|
||||
: Object.entries(form).some(
|
||||
([field, value]) =>
|
||||
field !== "lineagePersonId" && String(value).trim(),
|
||||
) ||
|
||||
form.lineagePersonId !== defaultLineagePersonId.value ||
|
||||
mediaReceipts.value.length > 0,
|
||||
);
|
||||
const confirmation = createDiscardConfirmation((visible) => {
|
||||
@@ -636,6 +653,28 @@ const saveGrowthRecord = async () => {
|
||||
if (pageActive) submitting.value = false;
|
||||
}
|
||||
};
|
||||
const uploadVideo = async () => {
|
||||
if (uploading.value || submitting.value) return;
|
||||
uploading.value = true;
|
||||
uploadError.value = "";
|
||||
try {
|
||||
const uploadReceipt = await pickAndUploadVideo({
|
||||
requestController: growthMediaUploadRequestController,
|
||||
});
|
||||
if (!pageActive) return;
|
||||
mediaReceipts.value = [...mediaReceipts.value, uploadReceipt];
|
||||
} catch (cause) {
|
||||
if (pageActive && !isVideoPickCancelled(cause) && !isRequestCancelled(cause))
|
||||
uploadError.value = getRequestErrorMessage(cause, "视频上传失败,请稍后重试。");
|
||||
} finally {
|
||||
if (pageActive) uploading.value = false;
|
||||
}
|
||||
};
|
||||
const removeMedia = (index) => {
|
||||
if (uploading.value || submitting.value) return;
|
||||
mediaReceipts.value = mediaReceipts.value.filter((_, receiptIndex) => receiptIndex !== index);
|
||||
uploadError.value = "";
|
||||
};
|
||||
const requestDeleteRecord = (record) => {
|
||||
if (!record?.canDelete || deleting.value) return;
|
||||
deleteError.value = "";
|
||||
@@ -726,12 +765,14 @@ onUnload(() => {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
flex: 1;
|
||||
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.form-card,
|
||||
.state-card,
|
||||
.record-card {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.form-card {
|
||||
box-sizing: border-box;
|
||||
@@ -773,7 +814,7 @@ onUnload(() => {
|
||||
@include adaptive-records-field;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 76rpx;
|
||||
min-height: 80rpx;
|
||||
padding: 16rpx 22rpx;
|
||||
color: $ink;
|
||||
font-size: clamp(14px, 23rpx, 17px);
|
||||
@@ -801,15 +842,15 @@ onUnload(() => {
|
||||
padding: 18rpx 22rpx;
|
||||
@include adaptive-records-field;
|
||||
}
|
||||
.upload-field > view > text {
|
||||
.upload-field > view:first-child > text {
|
||||
display: block;
|
||||
}
|
||||
.upload-field > view > text:first-child {
|
||||
.upload-field > view:first-child > text:first-child {
|
||||
color: $ink;
|
||||
font-size: clamp(14px, 23rpx, 17px);
|
||||
font-weight: 700;
|
||||
}
|
||||
.upload-field > view > text:last-child {
|
||||
.upload-field > view:first-child > text:last-child {
|
||||
margin-top: 6rpx;
|
||||
color: $ink-muted;
|
||||
font-size: clamp(13px, 20rpx, 16px);
|
||||
@@ -875,13 +916,40 @@ onUnload(() => {
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.save-notice {
|
||||
display: block;
|
||||
color: $brand-red;
|
||||
font-size: clamp(14px, 22rpx, 17px);
|
||||
}
|
||||
.upload-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.upload-receipt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.upload-receipt > text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: $ink-muted;
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.upload-receipt > button {
|
||||
min-height: 72rpx;
|
||||
margin: 0;
|
||||
padding: 0 16rpx;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: $brand-red;
|
||||
font-size: clamp(13px, 20rpx, 16px);
|
||||
}
|
||||
.upload-receipt > button::after { border: 0; }
|
||||
.delete-error {
|
||||
display: block;
|
||||
color: $brand-red;
|
||||
@@ -892,7 +960,7 @@ onUnload(() => {
|
||||
@include adaptive-records-field;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
min-height: 78rpx;
|
||||
min-height: 80rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14rpx 18rpx;
|
||||
|
||||
Reference in New Issue
Block a user