feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -35,9 +35,16 @@
|
||||
:key="item.id"
|
||||
class="ceremony-card"
|
||||
@click="openCeremony(item)"
|
||||
><view
|
||||
><image
|
||||
v-if="item.coverFile?.accessUrl"
|
||||
class="ceremony-card__cover"
|
||||
:src="item.coverFile.accessUrl"
|
||||
mode="aspectFill"
|
||||
aria-hidden="true"
|
||||
/><view
|
||||
><text>{{ item.title }}</text
|
||||
><text>{{ item.typeLabel }}{{ item.time ? ` · ${item.time}` : "" }}</text
|
||||
><text v-if="item.createTime">创建于 {{ formatMinuteTimestamp(item.createTime) }}</text
|
||||
><text v-if="item.description" class="ceremony-card__description">{{ item.description }}</text></view
|
||||
><text>{{ item.giftCount }} 笔献礼</text></view
|
||||
></view
|
||||
@@ -58,6 +65,7 @@ import {
|
||||
isRequestCancelled
|
||||
} from "@/services/api/request-controller.js";
|
||||
import { ceremonyApi } from "@/services/api/ceremony-service.js";
|
||||
import { formatMinuteTimestamp } from "@/utils/display-time.js";
|
||||
import { goBack, openPage, returnTo } from "@/utils/navigation/gateway.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
@@ -120,11 +128,13 @@ onUnload(() => {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
flex: 1;
|
||||
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.state-card,
|
||||
.ceremony-card {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.state-card {
|
||||
display: flex;
|
||||
@@ -144,7 +154,7 @@ onUnload(() => {
|
||||
.ceremony-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.ceremony-card {
|
||||
display: flex;
|
||||
@@ -158,6 +168,13 @@ onUnload(() => {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.ceremony-card__cover {
|
||||
width: 150rpx;
|
||||
height: 118rpx;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 10rpx;
|
||||
background: rgba(128, 89, 49, 0.12);
|
||||
}
|
||||
.ceremony-card text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
</view>
|
||||
<view v-else>
|
||||
<view class="detail-card">
|
||||
<image
|
||||
v-if="detail.coverFile?.accessUrl"
|
||||
class="detail-card__cover"
|
||||
:src="detail.coverFile.accessUrl"
|
||||
mode="aspectFill"
|
||||
role="button"
|
||||
aria-label="查看礼仪活动封面"
|
||||
@click="previewCover"
|
||||
/>
|
||||
<text>{{ detail.title }}</text>
|
||||
<text>{{ detail.typeLabel }}{{ detail.time ? ` · ${detail.time}` : "" }}</text>
|
||||
<text v-if="detail.location || detail.locationAddress">地点:{{ detail.location || detail.locationAddress }}</text>
|
||||
@@ -165,9 +174,9 @@
|
||||
:visible="ceremonyDeleteConfirmVisible"
|
||||
:close-on-mask="false"
|
||||
eyebrow="删除确认"
|
||||
title="删除这项礼仪活动?"
|
||||
message="活动和相关内容会一并删除,删除后无法恢复。"
|
||||
confirm-text="确认删除"
|
||||
title="将这项礼仪活动移至回收站?"
|
||||
message="活动和相关内容将不再展示,家谱管理员可在保留期内恢复。"
|
||||
confirm-text="移至回收站"
|
||||
cancel-text="保留活动"
|
||||
show-cancel
|
||||
@confirm="deleteCeremony"
|
||||
@@ -258,6 +267,11 @@ const valid = computed(
|
||||
() =>
|
||||
/^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(ceremonyId.value),
|
||||
);
|
||||
const previewCover = () => {
|
||||
const url = detail.value?.coverFile?.accessUrl;
|
||||
if (!url || typeof uni?.previewImage !== "function") return;
|
||||
uni.previewImage({ current: url, urls: [url] });
|
||||
};
|
||||
const giftSnapshot = computed(() => JSON.stringify(giftForm));
|
||||
const giftDirty = computed(
|
||||
() => giftFormVisible.value && giftSnapshot.value !== giftBaseline.value,
|
||||
@@ -337,7 +351,12 @@ const toggleGiftForm = async () => {
|
||||
return true;
|
||||
};
|
||||
const requestGiftSubmit = () => {
|
||||
giftError.value = giftForm.giftAmount.trim() ? "" : "请填写献礼金额";
|
||||
const giftAmount = giftForm.giftAmount.trim();
|
||||
giftError.value = !giftAmount
|
||||
? "请填写献礼金额"
|
||||
: /^(?:0|[1-9]\d{0,9})(?:\.\d{1,2})?$/.test(giftAmount)
|
||||
? ""
|
||||
: "献礼金额应为 0 至 9999999999.99,最多保留两位小数";
|
||||
if (!giftError.value && !giftSubmitting.value) giftConfirmVisible.value = true;
|
||||
};
|
||||
const confirmGiftSubmit = async () => {
|
||||
@@ -544,7 +563,8 @@ onUnload(() => {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
flex: 1;
|
||||
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.state-card,
|
||||
.detail-card,
|
||||
@@ -552,11 +572,19 @@ onUnload(() => {
|
||||
.gift-form-card,
|
||||
.gift-result {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.gift-form-card {
|
||||
margin-top: 18rpx;
|
||||
padding: 34rpx 32rpx;
|
||||
}
|
||||
.detail-card__cover {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
margin-bottom: 22rpx;
|
||||
border-radius: 10rpx;
|
||||
background: rgba(128, 89, 49, 0.12);
|
||||
}
|
||||
.gift-form-card__title,
|
||||
.gift-form-card__note,
|
||||
.gift-field > text,
|
||||
@@ -604,7 +632,7 @@ onUnload(() => {
|
||||
}
|
||||
.gift-field input {
|
||||
width: auto;
|
||||
min-height: 70rpx;
|
||||
min-height: 80rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.gift-message-field {
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
<text v-if="coverFileName" class="upload-receipt"
|
||||
>已上传:{{ coverFileName }}</text
|
||||
>
|
||||
<button v-if="coverOssId" class="remove-cover-button" :disabled="uploading || submitting" @click="clearCover">移除封面</button>
|
||||
</view>
|
||||
<text v-if="uploadError" class="form-error">{{ uploadError }}</text>
|
||||
<text v-if="submitError" class="form-error">{{ submitError }}</text>
|
||||
@@ -370,6 +371,12 @@ const uploadCover = async () => {
|
||||
if (pageActive) uploading.value = false;
|
||||
}
|
||||
};
|
||||
const clearCover = () => {
|
||||
if (uploading.value || submitting.value) return;
|
||||
coverOssId.value = null;
|
||||
coverFileName.value = "";
|
||||
uploadError.value = "";
|
||||
};
|
||||
const saveCeremony = async () => {
|
||||
if (uploading.value || submitting.value || !hasValidContext.value) return;
|
||||
if (!form.ceremonyType.trim() || !form.ceremonyTitle.trim()) {
|
||||
@@ -382,7 +389,7 @@ const saveCeremony = async () => {
|
||||
const payload = {
|
||||
...ceremonyForm,
|
||||
ceremonyTime: ceremonyTime.value,
|
||||
...(coverOssId.value ? { coverOssId: coverOssId.value } : {}),
|
||||
coverOssId: coverOssId.value,
|
||||
...(isEdit.value ? preservedUpdateFields.value : {}),
|
||||
};
|
||||
const createAttempt = isEdit.value ? null : ceremonyCreateGuard.begin(payload);
|
||||
@@ -463,12 +470,13 @@ onUnload(() => {
|
||||
}
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 22rpx 28rpx 72rpx;
|
||||
padding: 22rpx 28rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.editor-card,
|
||||
.state-card {
|
||||
@include adaptive-records-content;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.editor-card {
|
||||
padding: 38rpx 32rpx 42rpx;
|
||||
@@ -593,6 +601,18 @@ onUnload(() => {
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.remove-cover-button {
|
||||
justify-self: start;
|
||||
min-height: 72rpx;
|
||||
margin: 0;
|
||||
padding: 0 18rpx;
|
||||
border: 1rpx solid rgba($brand-red, 0.38);
|
||||
border-radius: 8rpx;
|
||||
background: transparent;
|
||||
color: $brand-red;
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
}
|
||||
.remove-cover-button::after { border: 0; }
|
||||
.form-error {
|
||||
margin-top: 10rpx;
|
||||
color: $brand-red;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -444,12 +444,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,
|
||||
.event-card {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.form-card {
|
||||
box-sizing: border-box;
|
||||
@@ -491,7 +493,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);
|
||||
@@ -586,7 +588,7 @@ onUnload(() => {
|
||||
.event-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.save-notice {
|
||||
display: block;
|
||||
@@ -619,5 +621,5 @@ onUnload(() => {
|
||||
justify-content: flex-end;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.event-card__actions .app-button { width: 154rpx; min-height: 66rpx; }
|
||||
.event-card__actions .app-button { width: 154rpx; min-height: 80rpx; }
|
||||
</style>
|
||||
|
||||
+42
-28
@@ -3,7 +3,7 @@
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-header"
|
||||
><PageHeader
|
||||
title="家族备忘"
|
||||
:title="pageTitle"
|
||||
:action="valid && view === 'list' ? '新建' : ''"
|
||||
custom-back
|
||||
@back="requestBack"
|
||||
@@ -11,14 +11,14 @@
|
||||
/></view>
|
||||
<view class="page-content">
|
||||
<view v-if="view === 'form'" class="form-card">
|
||||
<text>{{ isEdit ? '编辑家族备忘' : '新建家族备忘' }}</text>
|
||||
<text>{{ isEdit ? `编辑${recordName}` : `新建${recordName}` }}</text>
|
||||
<text class="form-copy">{{ isEdit ? '原有关联图片、完成状态和排序会随本次保存保留。' : '红色 * 为必填项,其余内容可按需补充。' }}</text>
|
||||
<view class="field"
|
||||
><text><text class="required-mark">*</text>备忘标题</text
|
||||
><text><text class="required-mark">*</text>{{ isBenefactorMode ? "恩人姓名" : "备忘标题" }}</text
|
||||
><input
|
||||
v-model="form.memoTitle"
|
||||
maxlength="40"
|
||||
placeholder="请输入备忘标题"
|
||||
:placeholder="isBenefactorMode ? '请输入恩人姓名' : '请输入备忘标题'"
|
||||
@input="error = ''"
|
||||
/></view>
|
||||
<picker mode="date" :value="form.remindDate" @change="selectRemindDate"
|
||||
@@ -37,12 +37,12 @@
|
||||
></picker
|
||||
>
|
||||
<view class="field field--textarea"
|
||||
><text>备忘内容</text
|
||||
><text>{{ isBenefactorMode ? "恩人事迹" : "备忘内容" }}</text
|
||||
><textarea
|
||||
v-model="form.memoContent"
|
||||
auto-height
|
||||
maxlength="1200"
|
||||
placeholder="记录需要提醒的事情"
|
||||
:placeholder="isBenefactorMode ? '记录恩人事迹与家族渊源' : '记录需要提醒的事情'"
|
||||
@input="error = ''"
|
||||
/>
|
||||
</view>
|
||||
@@ -50,7 +50,7 @@
|
||||
<view
|
||||
><text>相关图片</text
|
||||
><text
|
||||
>图片上传成功后,会随备忘一起保存。</text
|
||||
>图片上传成功后,会随{{ recordName }}一起保存。</text
|
||||
></view
|
||||
>
|
||||
<button
|
||||
@@ -74,34 +74,35 @@
|
||||
label="取消"
|
||||
@click="cancelCreate" /><AppButton
|
||||
:disabled="submitting || uploading"
|
||||
:label="submitting ? '正在提交' : '提交备忘'"
|
||||
:label="submitting ? '正在提交' : `提交${recordName}`"
|
||||
@click="saveMemo"
|
||||
/></view>
|
||||
</view>
|
||||
<view v-else-if="!valid" class="state-card"
|
||||
><text>暂时无法打开家族备忘</text
|
||||
><text>暂时无法打开{{ recordName }}</text
|
||||
><AppButton block label="返回上一页" @click="requestBack"
|
||||
/></view>
|
||||
<view v-else-if="listState === 'loading'" class="state-card"
|
||||
><AppLoading text="正在读取家族备忘"
|
||||
><AppLoading :text="`正在读取${recordName}`"
|
||||
/></view>
|
||||
<view v-else-if="listState === 'error'" class="state-card"
|
||||
><text>暂时无法读取家族备忘</text
|
||||
><text>暂时无法读取{{ recordName }}</text
|
||||
><AppButton block type="secondary" label="重新加载" @click="loadMemos"
|
||||
/></view>
|
||||
<view v-else-if="listState === 'empty'" class="state-card"
|
||||
><text>还没有家族备忘</text
|
||||
><AppButton block label="新建备忘" @click="openCreate"
|
||||
><text>还没有{{ recordName }}</text
|
||||
><AppButton block :label="`新建${recordName}`" @click="openCreate"
|
||||
/></view>
|
||||
<view v-else class="memo-list">
|
||||
<text v-if="saveNotice" class="save-notice"
|
||||
>备忘已保存。</text
|
||||
>{{ recordName }}已保存。</text
|
||||
>
|
||||
<text v-if="deleteError" class="delete-error">{{ deleteError }}</text>
|
||||
<view v-for="item in memos" :key="item.id" class="memo-card" role="button" :aria-label="`查看${item.title}详情`" @click="openMemoDetail(item)">
|
||||
<view class="memo-card__copy">
|
||||
<text>{{ item.title }}</text>
|
||||
<text v-if="item.remindTime">{{ item.remindTime }}</text>
|
||||
<text v-if="item.createTime">创建于 {{ formatMinuteTimestamp(item.createTime) }}</text>
|
||||
<text v-if="item.content" class="memo-card__content">{{ item.content }}</text>
|
||||
</view>
|
||||
<view v-if="item.canEdit || item.canDelete" class="memo-card__actions">
|
||||
@@ -125,29 +126,30 @@
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="Boolean(detailTarget)"
|
||||
eyebrow="备忘详情"
|
||||
:title="detailTarget?.title || '家族备忘'"
|
||||
:eyebrow="`${recordName}详情`"
|
||||
:title="detailTarget?.title || recordName"
|
||||
confirm-text="关闭"
|
||||
:close-on-mask="detailState !== 'loading'"
|
||||
@confirm="closeMemoDetail"
|
||||
@cancel="closeMemoDetail"
|
||||
>
|
||||
<view class="detail-content">
|
||||
<AppLoading v-if="detailState === 'loading'" text="正在读取完整备忘" />
|
||||
<AppLoading v-if="detailState === 'loading'" :text="`正在读取完整${recordName}`" />
|
||||
<text v-else-if="detailState === 'error'" class="detail-error">{{ detailError }}</text>
|
||||
<template v-else>
|
||||
<text>提醒时间:{{ detailTarget?.remindTime || "未设置" }}</text>
|
||||
<text>完成状态:{{ detailTarget?.completed === "1" ? "已完成" : "未完成" }}</text>
|
||||
<text class="detail-content__body">{{ detailTarget?.content || "未填写备忘内容" }}</text>
|
||||
<text v-if="detailTarget?.createTime">创建时间:{{ formatMinuteTimestamp(detailTarget.createTime) }}</text>
|
||||
<text class="detail-content__body">{{ detailTarget?.content || `未填写${recordName}内容` }}</text>
|
||||
<view v-if="detailTarget?.mediaFiles?.length" class="detail-media">
|
||||
<image v-for="file in detailTarget.mediaFiles" :key="file.fileId" :src="file.accessUrl" mode="aspectFill" role="button" aria-label="查看备忘图片" @click="previewDetailMedia(file)" />
|
||||
<image v-for="file in detailTarget.mediaFiles" :key="file.fileId" :src="file.accessUrl" mode="aspectFill" role="button" :aria-label="`查看${recordName}图片`" @click="previewDetailMedia(file)" />
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃家族备忘?"
|
||||
:title="`放弃${recordName}?`"
|
||||
message="尚未提交的内容将被清除。"
|
||||
confirm-text="放弃并返回"
|
||||
cancel-text="继续填写"
|
||||
@@ -158,10 +160,10 @@
|
||||
/>
|
||||
<AppDialog
|
||||
:visible="deleteConfirmationVisible"
|
||||
title="删除这条家族备忘?"
|
||||
message="删除后无法恢复,请确认当前内容不再需要。"
|
||||
confirm-text="确认删除"
|
||||
cancel-text="保留备忘"
|
||||
:title="`将这条${recordName}移至回收站?`"
|
||||
message="移入回收站后不再展示,家谱管理员可在保留期内恢复。"
|
||||
confirm-text="移至回收站"
|
||||
:cancel-text="`保留${recordName}`"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@confirm="deleteMemo"
|
||||
@@ -182,9 +184,11 @@ import {
|
||||
createRequestController,
|
||||
isRequestCancelled
|
||||
} from "@/services/api/request-controller.js";
|
||||
import { MEMO_TYPE } from "@/services/api/life-record-contract.js";
|
||||
import { lifeRecordApi } from "@/services/api/life-record-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,
|
||||
@@ -193,6 +197,7 @@ import {
|
||||
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation/gateway.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const memoType = ref(MEMO_TYPE.GENERAL);
|
||||
const view = ref("list");
|
||||
const listState = ref("loading");
|
||||
const memos = ref([]);
|
||||
@@ -228,6 +233,9 @@ const memoDetailRequestController = createRequestController();
|
||||
const memoCreateGuard = createNonIdempotentWriteGuard();
|
||||
let pageActive = true;
|
||||
const valid = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
|
||||
const isBenefactorMode = computed(() => memoType.value === MEMO_TYPE.BENEFACTOR);
|
||||
const recordName = computed(() => isBenefactorMode.value ? "家族恩人" : "家族备忘");
|
||||
const pageTitle = computed(() => recordName.value);
|
||||
const isEdit = computed(() => Boolean(editingMemo.value));
|
||||
const remindTime = computed(() =>
|
||||
form.remindDate ? `${form.remindDate} ${form.remindClock || "00:00"}:00` : "",
|
||||
@@ -276,7 +284,7 @@ const loadMemos = async () => {
|
||||
requestController: memoListRequestController,
|
||||
});
|
||||
if (!pageActive) return;
|
||||
memos.value = rows;
|
||||
memos.value = rows.filter((memo) => memo.memoType === memoType.value);
|
||||
listState.value = memos.value.length ? "ready" : "empty";
|
||||
if (pendingMemoId.value) {
|
||||
const target = memos.value.find((item) => item.id === pendingMemoId.value);
|
||||
@@ -413,6 +421,7 @@ const saveMemo = async () => {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
memoType: memoType.value,
|
||||
memoTitle: form.memoTitle,
|
||||
remindTime: remindTime.value,
|
||||
memoContent: form.memoContent,
|
||||
@@ -510,6 +519,9 @@ const requestBack = () =>
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = String(query?.genealogyId || "");
|
||||
memoType.value = query?.memoType === MEMO_TYPE.BENEFACTOR
|
||||
? MEMO_TYPE.BENEFACTOR
|
||||
: MEMO_TYPE.GENERAL;
|
||||
pendingMemoId.value = /^[1-9]\d*$/.test(String(query?.memoId || "")) ? String(query.memoId) : "";
|
||||
if (valid.value) loadMemos();
|
||||
else listState.value = "invalid";
|
||||
@@ -544,12 +556,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,
|
||||
.memo-card {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.form-card {
|
||||
box-sizing: border-box;
|
||||
@@ -674,7 +688,7 @@ onUnload(() => {
|
||||
.memo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.save-notice {
|
||||
display: block;
|
||||
@@ -707,7 +721,7 @@ onUnload(() => {
|
||||
}
|
||||
.memo-card__actions .app-button {
|
||||
width: 140rpx;
|
||||
min-height: 68rpx;
|
||||
min-height: 80rpx;
|
||||
}
|
||||
.memo-card text {
|
||||
display: block;
|
||||
|
||||
+133
-22
@@ -59,13 +59,27 @@
|
||||
}}</view></picker
|
||||
></view
|
||||
>
|
||||
<view class="upload-field">
|
||||
<view>
|
||||
<text>相关图片</text>
|
||||
<text>图片上传成功后,会随这条功德记录一起保存。</text>
|
||||
</view>
|
||||
<button class="upload-button" :disabled="uploading || submitting" @click="uploadImage">
|
||||
{{ uploading ? "上传中…" : "添加图片" }}
|
||||
</button>
|
||||
<view v-for="(receipt, index) in mediaReceipts" :key="`${receipt.ossId}-${index}`" class="upload-receipt">
|
||||
<text>已上传:{{ receipt.fileName || "图片" }}</text>
|
||||
<button :disabled="uploading || submitting" @click="removeImage(index)">移除</button>
|
||||
</view>
|
||||
<text v-if="uploadError" class="error">{{ uploadError }}</text>
|
||||
</view>
|
||||
<text v-if="error" class="error">{{ error }}</text>
|
||||
<view class="form-actions"
|
||||
><AppButton
|
||||
type="secondary"
|
||||
label="取消"
|
||||
@click="cancelCreate" /><AppButton
|
||||
:disabled="submitting"
|
||||
:disabled="submitting || uploading"
|
||||
:label="submitting ? '正在提交' : isEdit ? '保存修改' : '提交功德记录'"
|
||||
@click="saveMeritRecord"
|
||||
/></view>
|
||||
@@ -98,6 +112,7 @@
|
||||
<text>{{ item.title }}</text>
|
||||
<text>{{ item.donor }}{{ item.typeLabel ? ` · ${item.typeLabel}` : "" }}</text>
|
||||
<text v-if="item.time">{{ item.time }}</text>
|
||||
<text v-if="item.createTime">创建于 {{ formatMinuteTimestamp(item.createTime) }}</text>
|
||||
<text v-if="item.content">{{ item.content }}</text>
|
||||
</view>
|
||||
<view class="merit-card__amount">
|
||||
@@ -136,8 +151,20 @@
|
||||
<text>捐赠人:{{ detailTarget?.donor || "未署名" }}</text>
|
||||
<text>功德类型:{{ detailTarget?.typeLabel || "未填写" }}</text>
|
||||
<text>记录时间:{{ detailTarget?.time || "未填写" }}</text>
|
||||
<text v-if="detailTarget?.createTime">创建时间:{{ formatMinuteTimestamp(detailTarget.createTime) }}</text>
|
||||
<text>金额:¥{{ detailTarget?.amount || "0.00" }}</text>
|
||||
<text class="detail-content__body">{{ detailTarget?.content || "未填写记录内容" }}</text>
|
||||
<view v-if="detailTarget?.mediaFiles?.length" class="detail-media">
|
||||
<image
|
||||
v-for="file in detailTarget.mediaFiles"
|
||||
:key="file.fileId"
|
||||
:src="file.accessUrl"
|
||||
mode="aspectFill"
|
||||
role="button"
|
||||
:aria-label="file.fileName || '查看功德记录图片'"
|
||||
@click="previewMeritImage(file)"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</AppDialog>
|
||||
@@ -154,9 +181,9 @@
|
||||
/>
|
||||
<AppDialog
|
||||
:visible="deleteConfirmationVisible"
|
||||
title="删除这笔功德记录?"
|
||||
message="删除后无法恢复,请确认当前内容不再需要。"
|
||||
confirm-text="确认删除"
|
||||
title="将这笔功德记录移至回收站?"
|
||||
message="移入回收站后不再展示,家谱管理员可在保留期内恢复。"
|
||||
confirm-text="移至回收站"
|
||||
cancel-text="保留记录"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@@ -174,9 +201,7 @@ import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
MERIT_TYPE_OPTIONS as meritTypeOptions
|
||||
} from "@/services/api/life-record-contract.js";
|
||||
import { businessDictionaryApi } from "@/services/api/business-dictionary-service.js";
|
||||
import {
|
||||
createRequestController,
|
||||
isRequestCancelled
|
||||
@@ -184,16 +209,25 @@ import {
|
||||
import { lifeRecordApi } from "@/services/api/life-record-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,
|
||||
pickAndUploadImage,
|
||||
} from "@/utils/media-upload.js";
|
||||
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation/gateway.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const meritTypeOptions = ref([]);
|
||||
const view = ref("list");
|
||||
const listState = ref("loading");
|
||||
const merits = ref([]);
|
||||
const saveNotice = ref("");
|
||||
const submitting = ref(false);
|
||||
const uploading = ref(false);
|
||||
const error = ref("");
|
||||
const uploadError = ref("");
|
||||
const mediaReceipts = ref([]);
|
||||
const discardVisible = ref(false);
|
||||
const deleteConfirmationVisible = ref(false);
|
||||
const deleteTarget = ref(null);
|
||||
@@ -224,15 +258,18 @@ const meritEditorDetailRequestController = createRequestController();
|
||||
const meritSaveRequestController = createRequestController();
|
||||
const meritDeletionRequestController = createRequestController();
|
||||
const meritDetailRequestController = createRequestController();
|
||||
const meritImageUploadRequestController = createRequestController();
|
||||
const meritTypeRequestController = createRequestController();
|
||||
const meritRecordCreateGuard = createNonIdempotentWriteGuard();
|
||||
let pageActive = true;
|
||||
const valid = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
|
||||
const isEdit = computed(() => Boolean(editingMerit.value));
|
||||
const formSnapshot = computed(() => JSON.stringify(form));
|
||||
const mediaOssIds = computed(() => mediaReceipts.value.map((item) => item.ossId).join(","));
|
||||
const formSnapshot = computed(() => JSON.stringify({ ...form, mediaOssIds: mediaOssIds.value }));
|
||||
const dirty = computed(() =>
|
||||
isEdit.value
|
||||
? Boolean(formBaseline.value) && formSnapshot.value !== formBaseline.value
|
||||
: Object.values(form).some((value) => String(value).trim()),
|
||||
: Object.values(form).some((value) => String(value).trim()) || mediaReceipts.value.length > 0,
|
||||
);
|
||||
const meritTime = computed(() =>
|
||||
form.meritDate ? `${form.meritDate} ${form.meritClock || "00:00"}:00` : "",
|
||||
@@ -240,11 +277,11 @@ const meritTime = computed(() =>
|
||||
const meritTypeIndex = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
meritTypeOptions.findIndex((item) => item.value === form.type),
|
||||
meritTypeOptions.value.findIndex((item) => item.value === form.type),
|
||||
),
|
||||
);
|
||||
const meritTypeLabel = computed(
|
||||
() => meritTypeOptions.find((item) => item.value === form.type)?.label || "",
|
||||
() => meritTypeOptions.value.find((item) => item.value === form.type)?.label || "",
|
||||
);
|
||||
const addCurrencyAmounts = (left, right) => {
|
||||
const [leftWhole, leftFraction] = left.split(".");
|
||||
@@ -285,9 +322,11 @@ const resetForm = () => {
|
||||
meritClock: "",
|
||||
content: "",
|
||||
});
|
||||
mediaReceipts.value = [];
|
||||
editingMerit.value = null;
|
||||
formBaseline.value = "";
|
||||
error.value = "";
|
||||
uploadError.value = "";
|
||||
};
|
||||
const loadMerits = async () => {
|
||||
if (!valid.value) return;
|
||||
@@ -305,8 +344,23 @@ const loadMerits = async () => {
|
||||
listState.value = "error";
|
||||
}
|
||||
};
|
||||
const openCreate = () => {
|
||||
const loadMeritTypes = async () => {
|
||||
meritTypeRequestController.abort();
|
||||
meritTypeOptions.value = await businessDictionaryApi.getBusinessDictionaryOptions(
|
||||
"gen_merit_type",
|
||||
{ requestController: meritTypeRequestController },
|
||||
);
|
||||
};
|
||||
const openCreate = async () => {
|
||||
if (!valid.value) return;
|
||||
if (!meritTypeOptions.value.length) {
|
||||
try {
|
||||
await loadMeritTypes();
|
||||
} catch (cause) {
|
||||
if (!isRequestCancelled(cause)) error.value = getRequestErrorMessage(cause, "功德类型暂时无法读取。");
|
||||
return;
|
||||
}
|
||||
}
|
||||
saveNotice.value = "";
|
||||
resetForm();
|
||||
view.value = "form";
|
||||
@@ -329,7 +383,7 @@ const openEditMerit = async (merit) => {
|
||||
!detail.canEdit ||
|
||||
!detail.donor ||
|
||||
!detail.title ||
|
||||
!meritTypeOptions.some((item) => item.value === detail.type) ||
|
||||
!meritTypeOptions.value.some((item) => item.value === detail.type) ||
|
||||
!detail.amount ||
|
||||
!["0", "1"].includes(detail.status) ||
|
||||
!Number.isSafeInteger(detail.sortOrder)
|
||||
@@ -347,6 +401,10 @@ const openEditMerit = async (merit) => {
|
||||
meritClock: timeParts.clock,
|
||||
content: detail.content,
|
||||
});
|
||||
mediaReceipts.value = detail.mediaFiles.map((file) => ({
|
||||
ossId: String(file.ossId),
|
||||
fileName: file.fileName,
|
||||
}));
|
||||
editingMerit.value = {
|
||||
id: detail.id,
|
||||
sortOrder: detail.sortOrder,
|
||||
@@ -385,6 +443,11 @@ const closeMeritDetail = () => {
|
||||
detailState.value = "idle";
|
||||
detailError.value = "";
|
||||
};
|
||||
const previewMeritImage = (file) => {
|
||||
const urls = detailTarget.value?.mediaFiles?.map((mediaFile) => mediaFile.accessUrl).filter(Boolean) || [];
|
||||
if (!file?.accessUrl || !urls.length || typeof uni?.previewImage !== "function") return;
|
||||
uni.previewImage({ current: file.accessUrl, urls });
|
||||
};
|
||||
const cancelCreate = () => {
|
||||
resetForm();
|
||||
view.value = "list";
|
||||
@@ -398,11 +461,33 @@ const selectMeritClock = (event) => {
|
||||
error.value = "";
|
||||
};
|
||||
const selectMeritType = (event) => {
|
||||
form.type = meritTypeOptions[Number(event.detail.value)]?.value || "";
|
||||
form.type = meritTypeOptions.value[Number(event.detail.value)]?.value || "";
|
||||
error.value = "";
|
||||
};
|
||||
const uploadImage = async () => {
|
||||
if (uploading.value || submitting.value) return;
|
||||
uploading.value = true;
|
||||
uploadError.value = "";
|
||||
try {
|
||||
const receipt = await pickAndUploadImage({
|
||||
requestController: meritImageUploadRequestController,
|
||||
});
|
||||
if (!pageActive) return;
|
||||
mediaReceipts.value = [...mediaReceipts.value, receipt];
|
||||
} catch (cause) {
|
||||
if (pageActive && !isImagePickCancelled(cause) && !isRequestCancelled(cause))
|
||||
uploadError.value = getRequestErrorMessage(cause, "图片上传失败,请稍后重试。");
|
||||
} finally {
|
||||
if (pageActive) uploading.value = false;
|
||||
}
|
||||
};
|
||||
const removeImage = (index) => {
|
||||
if (uploading.value || submitting.value) return;
|
||||
mediaReceipts.value = mediaReceipts.value.filter((_, receiptIndex) => receiptIndex !== index);
|
||||
uploadError.value = "";
|
||||
};
|
||||
const saveMeritRecord = async () => {
|
||||
if (submitting.value || !valid.value) return;
|
||||
if (submitting.value || uploading.value || !valid.value) return;
|
||||
const donorName = form.donor.trim();
|
||||
const meritTitle = form.title.trim();
|
||||
const amountText = form.amount.trim();
|
||||
@@ -410,8 +495,8 @@ const saveMeritRecord = async () => {
|
||||
error.value = !donorName ? "请填写捐赠人" : "请填写功德标题";
|
||||
return;
|
||||
}
|
||||
if (amountText && !Number.isFinite(Number(amountText))) {
|
||||
error.value = "金额必须是数字";
|
||||
if (amountText && !/^(?:0|[1-9]\d{0,9})(?:\.\d{1,2})?$/.test(amountText)) {
|
||||
error.value = "金额应为 0 至 9999999999.99,最多保留两位小数";
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
@@ -420,6 +505,7 @@ const saveMeritRecord = async () => {
|
||||
meritType: form.type,
|
||||
meritContent: form.content,
|
||||
meritTime: meritTime.value,
|
||||
mediaOssIds: mediaOssIds.value,
|
||||
...(amountText ? { amount: Number(amountText) } : {}),
|
||||
...(editingMerit.value
|
||||
? {
|
||||
@@ -521,7 +607,10 @@ const requestBack = () =>
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = String(query?.genealogyId || "");
|
||||
if (valid.value) loadMerits();
|
||||
if (valid.value) {
|
||||
void loadMeritTypes().catch(() => {});
|
||||
loadMerits();
|
||||
}
|
||||
else listState.value = "invalid";
|
||||
});
|
||||
onShow(() => {
|
||||
@@ -536,6 +625,8 @@ onUnload(() => {
|
||||
meritSaveRequestController.abort();
|
||||
meritDeletionRequestController.abort();
|
||||
meritDetailRequestController.abort();
|
||||
meritImageUploadRequestController.abort();
|
||||
meritTypeRequestController.abort();
|
||||
confirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
@@ -553,12 +644,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,
|
||||
.merit-card {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.form-card {
|
||||
box-sizing: border-box;
|
||||
@@ -600,7 +693,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);
|
||||
@@ -614,6 +707,22 @@ onUnload(() => {
|
||||
.field--picker .placeholder {
|
||||
color: $ink-muted;
|
||||
}
|
||||
.upload-field {
|
||||
display: grid;
|
||||
gap: 12rpx;
|
||||
margin-top: 16rpx;
|
||||
padding: 18rpx 22rpx;
|
||||
@include adaptive-records-field;
|
||||
}
|
||||
.upload-field > view:first-child > text { display: block; }
|
||||
.upload-field > view:first-child > text:first-child { color: $ink; font-size: clamp(14px, 23rpx, 17px); font-weight: 700; }
|
||||
.upload-field > view:first-child > text:last-child { margin-top: 6rpx; color: $ink-muted; font-size: clamp(13px, 20rpx, 16px); line-height: 1.45; }
|
||||
.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(184, 35, 35, .38); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: clamp(14px, 22rpx, 17px); }
|
||||
.upload-field > text { color: $ink-muted; font-size: clamp(13px, 21rpx, 16px); overflow-wrap: anywhere; }
|
||||
.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; }
|
||||
.error {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
@@ -648,7 +757,7 @@ onUnload(() => {
|
||||
.merit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.save-notice {
|
||||
display: block;
|
||||
@@ -707,9 +816,11 @@ onUnload(() => {
|
||||
}
|
||||
.merit-card__amount .app-button {
|
||||
width: 140rpx;
|
||||
min-height: 68rpx;
|
||||
min-height: 80rpx;
|
||||
}
|
||||
.detail-content { width: 100%; margin-top: 18rpx; text-align: left; }
|
||||
.detail-media { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 16rpx; gap: 10rpx; }
|
||||
.detail-media image { width: 100%; height: 150rpx; border-radius: 8rpx; background: rgba(128, 89, 49, .12); }
|
||||
.detail-content > text { display: block; margin-top: 9rpx; color: $ink-muted; font-size: clamp(14px, 23rpx, 17px); line-height: 1.55; overflow-wrap: anywhere; }
|
||||
.detail-content__body { padding-top: 10rpx; border-top: 1rpx solid rgba(142, 95, 41, .2); color: $ink !important; white-space: pre-wrap; }
|
||||
.detail-error { color: $brand-red !important; }
|
||||
|
||||
+22
-11
@@ -55,7 +55,7 @@
|
||||
class="people-primary-action"
|
||||
type="secondary"
|
||||
block
|
||||
:label="loadingMore ? '正在加载…' : '加载更多人物'"
|
||||
:label="loadingMore ? '正在加载…' : loadMoreError ? '加载失败,重新加载' : '加载更多人物'"
|
||||
@click="loadMore"
|
||||
/>
|
||||
</view>
|
||||
@@ -125,9 +125,10 @@ const keyword = ref("");
|
||||
const total = ref(0);
|
||||
const pageNum = ref(1);
|
||||
const loadingMore = ref(false);
|
||||
const loadMoreError = ref(false);
|
||||
const peopleListRequestController = createRequestController();
|
||||
let loadSequence = 0;
|
||||
const hasValidContext = computed(() => Boolean(genealogyId.value));
|
||||
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
|
||||
const hasMore = computed(() => people.value.length < total.value);
|
||||
|
||||
const applySearch = () => {
|
||||
@@ -136,41 +137,48 @@ const applySearch = () => {
|
||||
return;
|
||||
}
|
||||
keyword.value = keywordInput.value.trim();
|
||||
pageNum.value = 1;
|
||||
loadMoreError.value = false;
|
||||
void loadPeople();
|
||||
};
|
||||
const clearSearch = () => {
|
||||
keywordInput.value = "";
|
||||
keyword.value = "";
|
||||
pageNum.value = 1;
|
||||
loadMoreError.value = false;
|
||||
void loadPeople();
|
||||
};
|
||||
const loadPeople = async ({ append = false } = {}) => {
|
||||
if (!hasValidContext.value) return;
|
||||
const activeLoad = ++loadSequence;
|
||||
if (append) loadingMore.value = true;
|
||||
const requestedPage = append ? pageNum.value + 1 : 1;
|
||||
if (append) {
|
||||
loadingMore.value = true;
|
||||
loadMoreError.value = false;
|
||||
}
|
||||
else peopleState.value = "loading";
|
||||
try {
|
||||
const personPage = await lineageApi.getPersonPage(
|
||||
genealogyId.value,
|
||||
{ pageNum: pageNum.value, pageSize: 10, keyword: keyword.value },
|
||||
{ pageNum: requestedPage, pageSize: 10, keyword: keyword.value },
|
||||
{ requestController: peopleListRequestController },
|
||||
);
|
||||
if (activeLoad !== loadSequence) return;
|
||||
people.value = append ? [...people.value, ...personPage.rows] : personPage.rows;
|
||||
pageNum.value = requestedPage;
|
||||
total.value = personPage.total;
|
||||
peopleState.value = people.value.length ? "ready" : "empty";
|
||||
} catch (error) {
|
||||
if (activeLoad !== loadSequence || isRequestCancelled(error)) return;
|
||||
if (!append) people.value = [];
|
||||
peopleState.value = "error";
|
||||
if (append) loadMoreError.value = true;
|
||||
else {
|
||||
people.value = [];
|
||||
peopleState.value = "error";
|
||||
}
|
||||
} finally {
|
||||
if (activeLoad === loadSequence) loadingMore.value = false;
|
||||
}
|
||||
};
|
||||
const loadMore = () => {
|
||||
if (loadingMore.value || !hasMore.value) return;
|
||||
pageNum.value += 1;
|
||||
void loadPeople({ append: true });
|
||||
};
|
||||
const openPerson = (person) =>
|
||||
@@ -188,7 +196,7 @@ const openPerson = (person) =>
|
||||
const handleStateAction = () => {
|
||||
if (peopleState.value === "invalid") return goBack();
|
||||
if (peopleState.value === "error") {
|
||||
pageNum.value = 1;
|
||||
loadMoreError.value = false;
|
||||
return loadPeople();
|
||||
}
|
||||
return goBack();
|
||||
@@ -224,7 +232,7 @@ onUnload(() => {
|
||||
}
|
||||
.people-content {
|
||||
flex: 1;
|
||||
padding: 22rpx 24rpx 100rpx;
|
||||
padding: 22rpx 24rpx calc(100rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.people-search {
|
||||
@include adaptive-records-field;
|
||||
@@ -269,6 +277,7 @@ onUnload(() => {
|
||||
min-height: clamp(92px, 190rpx, 108px);
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.person-card:first-child {
|
||||
margin-top: 0;
|
||||
@@ -284,11 +293,13 @@ onUnload(() => {
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: clamp(17px, 31rpx, 22px);
|
||||
font-weight: 700;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.person-card__meta {
|
||||
margin-top: 7rpx;
|
||||
color: $ink-muted;
|
||||
font-size: clamp(14px, 23rpx, 17px);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.person-card__hint {
|
||||
margin-top: 8rpx;
|
||||
|
||||
@@ -32,15 +32,15 @@
|
||||
</template>
|
||||
|
||||
<template v-if="personState === 'detail'">
|
||||
<view
|
||||
v-for="item in detailSections"
|
||||
:key="item.title"
|
||||
class="person-archive-card"
|
||||
>
|
||||
<view class="person-archive-card">
|
||||
<view
|
||||
><text>{{ item.title }}</text
|
||||
><text>{{ item.copy || "未填写" }}</text></view
|
||||
v-for="item in detailSections"
|
||||
:key="item.title"
|
||||
class="person-archive-row"
|
||||
>
|
||||
<text>{{ item.title }}</text>
|
||||
<text>{{ item.copy || "未填写" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="person-related-actions">
|
||||
<AppButton
|
||||
@@ -200,8 +200,8 @@ onLoad((query) => {
|
||||
personId.value = String(query.personId || "");
|
||||
if (
|
||||
query.mode !== "view" ||
|
||||
!genealogyId.value ||
|
||||
!personId.value
|
||||
!/^[1-9]\d*$/.test(genealogyId.value) ||
|
||||
!/^[1-9]\d*$/.test(personId.value)
|
||||
) {
|
||||
personState.value = "error";
|
||||
return;
|
||||
@@ -234,13 +234,14 @@ onUnload(() => {
|
||||
min-height: calc(100vh - 100rpx);
|
||||
}
|
||||
.person-detail-content {
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.person-identity-card {
|
||||
@include adaptive-records-person;
|
||||
display: flex;
|
||||
min-height: 190rpx;
|
||||
padding: 30rpx 10%;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.person-identity-card__copy {
|
||||
display: flex;
|
||||
@@ -269,28 +270,41 @@ onUnload(() => {
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
}
|
||||
.person-archive-card {
|
||||
min-height: 154rpx;
|
||||
margin-top: 14rpx;
|
||||
padding: 32rpx 42rpx;
|
||||
padding: 22rpx 42rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.person-archive-card,
|
||||
.person-state-card {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.person-archive-card text {
|
||||
display: block;
|
||||
.person-archive-row {
|
||||
display: grid;
|
||||
min-height: 68rpx;
|
||||
align-items: start;
|
||||
padding: 15rpx 0;
|
||||
border-bottom: 1rpx solid rgba($gold, 0.24);
|
||||
grid-template-columns: 176rpx minmax(0, 1fr);
|
||||
column-gap: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.person-archive-card text:first-child {
|
||||
.person-archive-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.person-archive-row text {
|
||||
min-width: 0;
|
||||
}
|
||||
.person-archive-row text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: clamp(15px, 24rpx, 18px);
|
||||
font-weight: 700;
|
||||
}
|
||||
.person-archive-card text:last-child {
|
||||
margin-top: 11rpx;
|
||||
.person-archive-row text:last-child {
|
||||
color: $ink;
|
||||
font-size: clamp(15px, 24rpx, 18px);
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.person-edit-action {
|
||||
margin-top: 20rpx;
|
||||
@@ -342,5 +356,8 @@ onUnload(() => {
|
||||
.person-identity-card__name {
|
||||
font-size: clamp(19px, 35rpx, 24px);
|
||||
}
|
||||
.person-archive-row {
|
||||
grid-template-columns: 146rpx minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<view class="documents-page">
|
||||
<ModulePageBackground module="records" />
|
||||
<view class="page-layer">
|
||||
<PageHeader title="重要证件" custom-back @back="requestBack" />
|
||||
</view>
|
||||
<view class="documents-content page-layer">
|
||||
<view v-if="!valid" class="documents-card">
|
||||
<text>暂时无法打开重要证件</text>
|
||||
<text>未找到家谱信息,请返回后重新进入。</text>
|
||||
</view>
|
||||
<view v-else class="documents-card">
|
||||
<text>家谱证件档案</text>
|
||||
<text>集中查看当前家谱中有权访问的重要证件;新增证件仍从对应人物资料进入。</text>
|
||||
<AppButton block label="查看全部证件" :disabled="documentBusy" @click="openDocuments" />
|
||||
</view>
|
||||
</view>
|
||||
<PersonDocumentDialog
|
||||
ref="documentDialog"
|
||||
:genealogy-id="genealogyId"
|
||||
@busy-change="documentBusy = $event"
|
||||
@transient-change="documentTransientOpen = $event"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onBackPress, onLoad, onReady } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import PersonDocumentDialog from "@/components/tree/PersonDocumentDialog.vue";
|
||||
import { goBack, handleBackPress } from "@/utils/navigation/gateway.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const documentDialog = ref(null);
|
||||
const documentBusy = ref(false);
|
||||
const documentTransientOpen = ref(false);
|
||||
const valid = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
|
||||
|
||||
const openDocuments = () => {
|
||||
if (!valid.value || documentBusy.value) return;
|
||||
documentDialog.value?.open();
|
||||
};
|
||||
const requestBack = () => {
|
||||
if (documentTransientOpen.value) return documentDialog.value?.closeTransient() ?? true;
|
||||
return goBack();
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = String(query?.genealogyId || "");
|
||||
});
|
||||
onReady(() => {
|
||||
if (valid.value) openDocuments();
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/adaptive-frame-profiles.scss";
|
||||
.documents-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.page-layer {
|
||||
z-index: 1;
|
||||
}
|
||||
.documents-content {
|
||||
flex: 1;
|
||||
padding: 24rpx 28rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.documents-card {
|
||||
@include adaptive-records-content;
|
||||
padding: 48rpx 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.documents-card text {
|
||||
display: block;
|
||||
color: $ink-muted;
|
||||
font-size: clamp(14px, 23rpx, 17px);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.documents-card text:first-child {
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: clamp(19px, 34rpx, 24px);
|
||||
font-weight: 700;
|
||||
}
|
||||
.documents-card text + text,
|
||||
.documents-card .app-button {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -351,6 +351,11 @@ const saveRelative = async () => {
|
||||
"上次提交结果暂时无法确认,请先返回贺礼簿检查,避免重复创建。";
|
||||
return;
|
||||
}
|
||||
const giftAmount = form.giftAmount.trim();
|
||||
if (giftAmount && !/^(?:0|[1-9]\d{0,9})(?:\.\d{1,2})?$/.test(giftAmount)) {
|
||||
submitError.value = "礼金金额应为 0 至 9999999999.99,最多保留两位小数";
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
submitError.value = "";
|
||||
@@ -423,13 +428,15 @@ 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 {
|
||||
box-sizing: border-box;
|
||||
padding: 46rpx;
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.form-card > text:first-child,
|
||||
.state-card > text:first-child {
|
||||
@@ -485,9 +492,10 @@ onUnload(() => {
|
||||
}
|
||||
.field-row--picker picker {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.field-row--picker picker > view {
|
||||
min-height: 48rpx;
|
||||
min-height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@@ -512,7 +520,7 @@ onUnload(() => {
|
||||
}
|
||||
.upload-button {
|
||||
justify-self: start;
|
||||
min-height: 60rpx;
|
||||
min-height: 80rpx;
|
||||
margin: 0;
|
||||
padding: 0 20rpx;
|
||||
border: 1rpx solid rgba(159, 23, 15, 0.42);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
title="贺礼簿"
|
||||
:action="valid ? '新建' : ''"
|
||||
custom-back
|
||||
@back="returnToFamily"
|
||||
@back="requestBack"
|
||||
@action="createRelative"
|
||||
/></view>
|
||||
<view class="page-content">
|
||||
@@ -32,12 +32,20 @@
|
||||
<view v-else class="record-list">
|
||||
<text v-if="deleteError" class="delete-error">{{ deleteError }}</text>
|
||||
<view v-for="item in records" :key="item.id" class="record-card" role="button" :aria-label="`查看${item.name}的往来详情`" @click="openRecordDetail(item)">
|
||||
<image
|
||||
v-if="item.mediaFiles?.[0]?.accessUrl"
|
||||
class="record-card__cover"
|
||||
:src="item.mediaFiles[0].accessUrl"
|
||||
mode="aspectFill"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<view
|
||||
><text>{{ item.name }}</text
|
||||
><text
|
||||
>{{ item.relation
|
||||
}}{{ item.event ? ` · ${item.event}` : "" }}</text
|
||||
><text v-if="item.time">{{ item.time }}</text
|
||||
><text v-if="item.createTime">创建于 {{ formatMinuteTimestamp(item.createTime) }}</text
|
||||
><text v-if="item.content">{{ item.content }}</text></view
|
||||
>
|
||||
<view class="record-card__amount">
|
||||
@@ -76,6 +84,7 @@
|
||||
<text>关系:{{ detailTarget?.relation || "未填写" }}</text>
|
||||
<text>事项:{{ detailTarget?.event || "未填写" }}</text>
|
||||
<text>时间:{{ detailTarget?.time || "未填写" }}</text>
|
||||
<text v-if="detailTarget?.createTime">创建时间:{{ formatMinuteTimestamp(detailTarget.createTime) }}</text>
|
||||
<text>金额:{{ detailTarget?.amount ? `¥${detailTarget.amount}` : "未填写" }}</text>
|
||||
<text class="detail-content__body">{{ detailTarget?.content || "未填写记录内容" }}</text>
|
||||
<view v-if="detailTarget?.mediaFiles?.length" class="detail-media">
|
||||
@@ -86,9 +95,9 @@
|
||||
</AppDialog>
|
||||
<AppDialog
|
||||
:visible="deleteConfirmationVisible"
|
||||
title="删除这条亲友往来?"
|
||||
message="删除后无法恢复,请确认当前内容不再需要。"
|
||||
confirm-text="确认删除"
|
||||
title="将这条亲友往来移至回收站?"
|
||||
message="移入回收站后不再展示,家谱管理员可在保留期内恢复。"
|
||||
confirm-text="移至回收站"
|
||||
cancel-text="保留记录"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@@ -100,7 +109,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
@@ -112,7 +121,8 @@ import {
|
||||
} from "@/services/api/request-controller.js";
|
||||
import { lifeRecordApi } from "@/services/api/life-record-service.js";
|
||||
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
|
||||
import { goBack, openPage, returnTo } from "@/utils/navigation/gateway.js";
|
||||
import { formatMinuteTimestamp } from "@/utils/display-time.js";
|
||||
import { goBack, handleBackPress, openPage, returnTo } from "@/utils/navigation/gateway.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const records = ref([]);
|
||||
@@ -200,6 +210,18 @@ const closeDeleteConfirmation = () => {
|
||||
deleteConfirmationVisible.value = false;
|
||||
deleteTarget.value = null;
|
||||
};
|
||||
const requestBack = () => {
|
||||
if (deleting.value || detailState.value === "loading") return true;
|
||||
if (deleteConfirmationVisible.value) {
|
||||
closeDeleteConfirmation();
|
||||
return true;
|
||||
}
|
||||
if (detailTarget.value) {
|
||||
closeRecordDetail();
|
||||
return true;
|
||||
}
|
||||
return returnToFamily();
|
||||
};
|
||||
const deleteRecord = async () => {
|
||||
const record = deleteTarget.value;
|
||||
if (!record?.canDelete || deleting.value) return;
|
||||
@@ -234,6 +256,7 @@ onUnload(() => {
|
||||
relativeRecordDeleteController.abort();
|
||||
relativeRecordDetailController.abort();
|
||||
});
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -249,11 +272,13 @@ onUnload(() => {
|
||||
z-index: 1;
|
||||
}
|
||||
.page-content {
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
flex: 1;
|
||||
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.state-card,
|
||||
.record-card {
|
||||
@include adaptive-records-content;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.state-card {
|
||||
display: flex;
|
||||
@@ -273,7 +298,7 @@ onUnload(() => {
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.record-card {
|
||||
display: flex;
|
||||
@@ -310,6 +335,12 @@ onUnload(() => {
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.record-card__cover {
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
border-radius: 10rpx;
|
||||
background: rgba(128, 89, 49, 0.12);
|
||||
}
|
||||
.record-card__amount > text {
|
||||
margin-right: auto;
|
||||
color: $brand-red;
|
||||
@@ -317,7 +348,7 @@ onUnload(() => {
|
||||
}
|
||||
.record-card__amount .app-button {
|
||||
width: 140rpx;
|
||||
min-height: 68rpx;
|
||||
min-height: 80rpx;
|
||||
}
|
||||
.delete-error {
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user