Files
jiapuapp/pages/records/r10-memo-list.vue
T

498 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 页面编号R-10用途读取新建当前家谱的家族备忘 -->
<template>
<view class="memo-page">
<ModulePageBackground module="records" />
<view class="page-header"
><PageHeader
title="家族备忘"
:action="valid && view === 'list' ? '新建' : ''"
custom-back
@back="requestBack"
@action="openCreate"
/></view>
<view class="page-content">
<view v-if="view === 'form'" class="form-card">
<text>新建家族备忘</text>
<text class="form-copy">红色 * 为必填项其余内容可按需补充</text>
<view class="field"
><text><text class="required-mark">*</text>备忘标题</text
><input
v-model="form.memoTitle"
maxlength="40"
placeholder="请输入备忘标题"
@input="error = ''"
/></view>
<picker mode="date" :value="form.remindDate" @change="selectRemindDate"
><view class="field field--picker"
><text>提醒日期</text
><text>{{ form.remindDate || "请选择" }}</text></view
></picker
>
<picker
mode="time"
:value="form.remindClock"
@change="selectRemindClock"
><view class="field field--picker"
><text>提醒时间</text
><text>{{ form.remindClock || "请选择" }}</text></view
></picker
>
<view class="field field--textarea"
><text>备忘内容</text
><textarea
v-model="form.memoContent"
auto-height
maxlength="1200"
placeholder="记录需要提醒的事情"
@input="error = ''"
/>
</view>
<view class="upload-field">
<view
><text>相关图片</text
><text
>图片选定后会先取得真实上传回执并在提交备忘时关联</text
></view
>
<button
class="upload-button"
:disabled="uploading || submitting"
@click="uploadImage"
>
{{ uploading ? "上传中…" : "添加图片" }}
</button>
<text
v-for="(receipt, index) in mediaReceipts"
:key="`${receipt.ossId}-${index}`"
>已上传{{ receipt.fileName || "图片" }}</text
>
<text v-if="uploadError" class="error">{{ uploadError }}</text>
</view>
<view class="field"
><text>排序值</text
><input
v-model="form.sortOrder"
type="number"
placeholder="数值越小越靠前"
@input="error = ''"
/></view>
<text v-if="error" class="error">{{ error }}</text>
<view class="form-actions"
><AppButton
type="secondary"
label="取消"
@click="cancelCreate" /><AppButton
:disabled="submitting || uploading"
:label="submitting ? '正在提交' : '提交备忘'"
@click="submit"
/></view>
</view>
<view v-else-if="!valid" class="state-card"
><text>家族备忘入口无效</text
><AppButton block label="返回上一页" @click="requestBack"
/></view>
<view v-else-if="listState === 'loading'" class="state-card"
><AppLoading text="正在读取家族备忘"
/></view>
<view v-else-if="listState === 'error'" class="state-card"
><text>暂时无法读取家族备忘</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"
/></view>
<view v-else class="memo-list">
<text v-if="saveNotice" class="save-notice"
>备忘已提交并已从服务端重新读取</text
>
<view v-for="item in memos" :key="item.id" class="memo-card"
><view
><text>{{ item.title }}</text
><text v-if="item.remindTime">{{ item.remindTime }}</text
><text v-if="item.content" class="memo-card__content">{{ item.content }}</text></view
></view
>
</view>
</view>
<AppDialog
:visible="discardVisible"
title="放弃家族备忘?"
message="尚未提交的内容将被清除。"
confirm-text="放弃并返回"
cancel-text="继续填写"
show-cancel
:close-on-mask="false"
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
</view>
</template>
<script setup>
import { computed, onUnmounted, reactive, ref } from "vue";
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
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 {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
isImagePickCancelled,
pickAndUploadImage,
} from "@/utils/resumable-image-upload.js";
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const view = ref("list");
const listState = ref("loading");
const memos = ref([]);
const saveNotice = ref(false);
const submitting = ref(false);
const uploading = ref(false);
const error = ref("");
const uploadError = ref("");
const discardVisible = ref(false);
const form = reactive({
memoTitle: "",
remindDate: "",
remindClock: "",
memoContent: "",
sortOrder: "",
});
const mediaReceipts = ref([]);
const controller = createRequestController();
let active = true;
const valid = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const remindTime = computed(() =>
form.remindDate ? `${form.remindDate} ${form.remindClock || "00:00"}:00` : "",
);
const mediaOssIds = computed(() =>
mediaReceipts.value.map((item) => item.ossId).join(","),
);
const dirty = computed(
() =>
Object.values(form).some((value) => String(value).trim()) ||
mediaReceipts.value.length > 0,
);
const confirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const confirmDiscard = () => {
confirmation.confirm();
resetForm();
view.value = "list";
};
const cancelDiscard = confirmation.cancel;
const resetForm = () => {
Object.assign(form, {
memoTitle: "",
remindDate: "",
remindClock: "",
memoContent: "",
sortOrder: "",
});
mediaReceipts.value = [];
error.value = "";
uploadError.value = "";
};
const loadMemos = async () => {
if (!valid.value) return;
controller.abort();
listState.value = "loading";
try {
const rows = await appApi.getMemos(genealogyId.value, {
requestController: controller,
});
if (!active) return;
memos.value = rows
.map((item) => ({
id: String(item.memoId || ""),
title: String(item.memoTitle || "未命名备忘"),
remindTime: String(item.remindTime || ""),
content: String(item.memoContent || ""),
}))
.filter((item) => /^[1-9]\d*$/.test(item.id));
listState.value = memos.value.length ? "ready" : "empty";
} catch (cause) {
if (!active || isRequestCancelled(cause)) return;
listState.value = "error";
}
};
const openCreate = () => {
if (!valid.value) return;
saveNotice.value = false;
resetForm();
view.value = "form";
};
const cancelCreate = () => {
resetForm();
view.value = "list";
};
const selectRemindDate = (event) => {
form.remindDate = event.detail.value || "";
};
const selectRemindClock = (event) => {
form.remindClock = event.detail.value || "";
};
const uploadImage = async () => {
if (uploading.value || submitting.value) return;
uploading.value = true;
uploadError.value = "";
try {
mediaReceipts.value = [
...mediaReceipts.value,
await pickAndUploadImage({ requestController: controller }),
];
} catch (cause) {
if (!isImagePickCancelled(cause) && !isRequestCancelled(cause))
uploadError.value = cause?.message || "图片上传失败,请稍后重试。";
} finally {
uploading.value = false;
}
};
const submit = async () => {
if (submitting.value || uploading.value || !valid.value) return;
if (!form.memoTitle.trim()) {
error.value = "请填写备忘标题";
return;
}
submitting.value = true;
error.value = "";
try {
await appApi.createMemo(
genealogyId.value,
{
memoTitle: form.memoTitle,
remindTime: remindTime.value,
memoContent: form.memoContent,
mediaOssIds: mediaOssIds.value,
sortOrder: form.sortOrder,
},
{ requestController: controller },
);
resetForm();
view.value = "list";
saveNotice.value = true;
await loadMemos();
} catch (cause) {
if (!isRequestCancelled(cause))
error.value = cause?.message || "备忘提交失败,请稍后重试。";
} finally {
submitting.value = false;
}
};
const requestBack = () =>
view.value !== "form"
? goBack()
: runBackGuard({
transientOpen: discardVisible.value,
dirty: dirty.value,
submitting: submitting.value || uploading.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": confirmation.request,
});
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
if (valid.value) loadMemos();
else listState.value = "invalid";
});
onShow(() => {
if (valid.value && view.value === "list" && listState.value !== "loading")
loadMemos();
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => {
active = false;
controller.abort();
confirmation.dispose();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.memo-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.page-header,
.page-content {
z-index: 1;
}
.page-content {
padding: 18rpx 24rpx 72rpx;
}
.form-card,
.state-card,
.memo-card {
@include adaptive-records-content;
}
.form-card {
box-sizing: border-box;
padding: 46rpx;
}
.form-card > text:first-child {
display: block;
color: $ink;
font-size: clamp(19px, 34rpx, 24px);
font-weight: 700;
}
.form-copy {
display: block;
margin-top: 12rpx;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.55;
}
.field {
display: grid;
grid-template-columns: 142rpx minmax(0, 1fr);
align-items: center;
gap: 12rpx 20rpx;
min-height: 82rpx;
margin-top: 14rpx;
padding: 14rpx 24rpx;
box-sizing: border-box;
@include adaptive-records-field;
}
.field > text,
.upload-field > view > text:first-child {
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
font-weight: 700;
}
.required-mark {
display: inline-block;
margin-right: 4rpx;
color: $brand-red;
font-size: clamp(16px, 26rpx, 20px);
transform: translateY(-3rpx);
}
.field input,
.field > text:last-child {
min-width: 0;
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
text-align: right;
}
.field--textarea {
align-items: start;
}
.field textarea {
min-height: 116rpx;
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.55;
}
.upload-field {
display: grid;
gap: 12rpx;
margin-top: 14rpx;
padding: 18rpx 24rpx;
@include adaptive-records-field;
}
.upload-field > view > text {
display: block;
}
.upload-field > view > 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(159, 23, 15, 0.42);
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;
}
.error {
display: block;
margin-top: 12rpx;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
}
.form-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.45fr);
gap: 16rpx;
margin-top: 20rpx;
}
.form-actions .app-button {
width: 100%;
min-width: 0;
}
.state-card {
display: flex;
min-height: 320rpx;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 42rpx;
color: $ink;
font-size: clamp(16px, 28rpx, 20px);
text-align: center;
}
.state-card .app-button {
width: 100%;
margin-top: 24rpx;
}
.memo-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.save-notice {
display: block;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
}
.memo-card {
min-height: 128rpx;
padding: 28rpx 32rpx;
}
.memo-card text {
display: block;
}
.memo-card text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(17px, 30rpx, 22px);
font-weight: 700;
}
.memo-card text:not(:first-child) {
margin-top: 7rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.45;
overflow-wrap: anywhere;
}
.memo-card__content {
display: -webkit-box !important;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
</style>