修改完成待测试

This commit is contained in:
2026-09-13 17:45:47 +08:00
parent 9ad572907b
commit bbc024f7f2
80 changed files with 3844 additions and 1672 deletions
+131 -13
View File
@@ -134,6 +134,24 @@
</view>
<text v-if="uploadError" class="error">{{ uploadError }}</text>
</view>
<view v-if="!isEdit" class="field field--password">
<text>内容密码选填</text>
<text class="person-option-note">填写后会先创建记录再立即启用密码保护设置失败时会保留记录并明确提示</text>
<input
v-model="contentPassword"
password
maxlength="128"
placeholder="请输入8至128位内容密码"
@input="error = ''"
/>
<input
v-model="contentPasswordConfirm"
password
maxlength="128"
placeholder="请再次输入内容密码"
@input="error = ''"
/>
</view>
<text v-if="error" class="error">{{ error }}</text>
<view class="form-actions"
><AppButton
@@ -167,9 +185,21 @@
><AppButton block label="新建成长记录" @click="openCreate"
/></view>
<view v-else class="record-list">
<text v-if="saveNotice" class="save-notice"
>成长记录已保存</text
>
<BatchManagementBar
v-if="deletableGrowthRecords.length"
resource-name="成长记录"
:active="growthBatch.selectionMode.value"
:selected-count="growthBatch.selectedCount.value"
:all-selected="growthBatch.allSelected.value"
:busy="growthBatch.deleting.value"
@start="growthBatch.enterSelectionMode"
@finish="growthBatch.exitSelectionMode"
@toggle-all="growthBatch.toggleAll"
@delete="growthBatch.requestDelete"
/>
<text v-if="growthBatch.notice.value" class="save-notice" role="status">{{ growthBatch.notice.value }}</text>
<text v-if="growthBatch.error.value" class="delete-error" role="alert">{{ growthBatch.error.value }}</text>
<text v-if="saveNotice" class="save-notice">{{ saveNotice }}</text>
<text v-if="deleteError" class="delete-error">{{ deleteError }}</text>
<view class="record-filters">
<picker :range="filterPersonLabels" :value="filterPersonIndex" @change="selectFilterPerson">
@@ -185,9 +215,15 @@
:key="item.id"
class="record-card"
role="button"
:aria-label="`查看${item.title}详情`"
@click="openRecordDetail(item)"
:aria-label="growthBatch.selectionMode.value && item.canDelete ? `${growthBatch.isSelected(item) ? '取消选择' : '选择'}${item.title}` : `查看${item.title}详情`"
@click="handleGrowthRecordClick(item)"
>
<BatchSelectionMark
v-if="growthBatch.selectionMode.value && item.canDelete"
:selected="growthBatch.isSelected(item)"
:label="`成长记录:${item.title}`"
@toggle="growthBatch.toggleSelection(item)"
/>
<view
><text>{{ item.title }}</text
><text
@@ -197,14 +233,14 @@
><text v-if="item.content">{{ item.content }}</text></view
>
<AppButton
v-if="item.canEdit && personOptionsState === 'ready' && growthTypeOptionsState === 'ready'"
v-if="!growthBatch.selectionMode.value && item.canEdit && personOptionsState === 'ready' && growthTypeOptionsState === 'ready'"
compact
type="secondary"
label="编辑"
@click.stop="openEdit(item)"
/>
<AppButton
v-if="item.canDelete"
v-if="!growthBatch.selectionMode.value && item.canDelete"
compact
type="secondary"
label="删除"
@@ -219,6 +255,17 @@
@busy-change="detailBusy = $event"
@transient-change="detailTransientOpen = $event"
/>
<AppDialog
:visible="growthBatch.confirmationVisible.value"
title="将选中的成长记录移入回收站?"
:message="growthBatch.confirmationMessage.value"
:confirm-text="growthBatch.deleting.value ? '正在删除' : '移入回收站'"
cancel-text="继续选择"
show-cancel
:close-on-mask="false"
@confirm="growthBatch.confirmDelete"
@cancel="growthBatch.cancelDelete"
/>
<AppDialog
:visible="discardVisible"
title="放弃成长记录?"
@@ -250,6 +297,8 @@ 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";
import BatchManagementBar from "@/components/BatchManagementBar.vue";
import BatchSelectionMark from "@/components/BatchSelectionMark.vue";
import GrowthRecordDetailDialog from "@/components/records/GrowthRecordDetailDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
@@ -261,6 +310,7 @@ import { businessDictionaryApi } from "@/services/api/business-dictionary-servic
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 { useBatchDeletion } from "@/composables/use-batch-deletion.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { formatMinuteTimestamp } from "@/utils/display-time.js";
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
@@ -277,7 +327,7 @@ const personId = ref("");
const view = ref("list");
const listState = ref("loading");
const records = ref([]);
const saveNotice = ref(false);
const saveNotice = ref("");
const submitting = ref(false);
const uploading = ref(false);
const error = ref("");
@@ -299,6 +349,8 @@ const personOptions = ref([{ value: "", label: "不关联人物" }]);
const defaultLineagePersonId = ref("");
const growthTypeOptionsState = ref("loading");
const growthTypeOptions = ref([]);
const contentPassword = ref("");
const contentPasswordConfirm = ref("");
const form = reactive({
lineagePersonId: "",
recordType: "",
@@ -314,6 +366,7 @@ const recordListRequestController = createRequestController();
const growthEditorDetailRequestController = createRequestController();
const growthMediaUploadRequestController = createRequestController();
const growthRecordSaveRequestController = createRequestController();
const growthPasswordRequestController = createRequestController();
const personOptionsController = createRequestController();
const growthTypeRequestController = createRequestController();
const growthRecordDeleteController = createRequestController();
@@ -356,6 +409,20 @@ const filteredRecords = computed(() => records.value.filter((item) =>
(!filterPersonId.value || (filterPersonId.value === "NONE" ? !item.lineagePersonId : item.lineagePersonId === filterPersonId.value)) &&
(!filterType.value || item.type === filterType.value),
));
const growthBatch = useBatchDeletion({
items: records,
visibleItems: filteredRecords,
deleteOne: (record) =>
lifeRecordApi.deleteGrowthRecord(genealogyId.value, record.id, {
requestController: growthRecordDeleteController,
}),
resourceName: "成长记录",
isActive: () => pageActive,
onEmpty: () => {
listState.value = "empty";
},
});
const deletableGrowthRecords = growthBatch.deletableItems;
const mediaOssIds = computed(() =>
mediaReceipts.value.map((item) => item.ossId).join(","),
);
@@ -376,7 +443,9 @@ const dirty = computed(() =>
field !== "lineagePersonId" && String(value).trim(),
) ||
form.lineagePersonId !== defaultLineagePersonId.value ||
mediaReceipts.value.length > 0,
mediaReceipts.value.length > 0 ||
contentPassword.value ||
contentPasswordConfirm.value,
);
const confirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
@@ -400,6 +469,8 @@ const resetForm = () => {
remindClock: "",
});
mediaReceipts.value = [];
contentPassword.value = "";
contentPasswordConfirm.value = "";
editingRecord.value = null;
formBaseline.value = "";
error.value = "";
@@ -456,6 +527,7 @@ const loadRecords = async () => {
});
if (!pageActive) return;
records.value = rows;
growthBatch.exitSelectionMode();
listState.value = records.value.length ? "ready" : "empty";
} catch (cause) {
if (!pageActive || isRequestCancelled(cause)) return;
@@ -464,7 +536,7 @@ const loadRecords = async () => {
};
const openCreate = () => {
if (!valid.value) return;
saveNotice.value = false;
saveNotice.value = "";
resetForm();
view.value = "form";
};
@@ -563,15 +635,24 @@ const selectGrowthType = (event) => {
};
const selectFilterPerson = (event) => {
filterPersonId.value = filterPersonOptions.value[Number(event.detail.value)]?.value || "";
growthBatch.exitSelectionMode();
};
const selectFilterType = (event) => {
filterType.value = filterTypeOptions.value[Number(event.detail.value)]?.value || "";
growthBatch.exitSelectionMode();
};
const openRecordDetail = (record) =>
growthRecordDetailDialog.value?.open({
...record,
typeLabel: growthRecordTypeLabel(record),
});
const handleGrowthRecordClick = (record) => {
if (growthBatch.selectionMode.value && record?.canDelete) {
growthBatch.toggleSelection(record);
return;
}
openRecordDetail(record);
};
const uploadImage = async () => {
if (uploading.value || submitting.value) return;
uploading.value = true;
@@ -595,6 +676,16 @@ const saveGrowthRecord = async () => {
error.value = "请填写记录标题";
return;
}
if (contentPassword.value || contentPasswordConfirm.value) {
if (contentPassword.value.length < 8 || contentPassword.value.length > 128) {
error.value = "内容密码必须为8至128位。";
return;
}
if (contentPassword.value !== contentPasswordConfirm.value) {
error.value = "两次输入的内容密码不一致。";
return;
}
}
const { recordClock, remindDate, remindClock, ...recordForm } = form;
const payload = {
...recordForm,
@@ -619,6 +710,7 @@ const saveGrowthRecord = async () => {
submitting.value = true;
error.value = "";
let passwordSetupNotice = "";
try {
if (editingRecord.value) {
await lifeRecordApi.updateGrowthRecord(
@@ -628,14 +720,30 @@ const saveGrowthRecord = async () => {
{ requestController: growthRecordSaveRequestController },
);
} else {
await lifeRecordApi.createGrowthRecord(genealogyId.value, payload, {
const createdRecord = await lifeRecordApi.createGrowthRecord(genealogyId.value, payload, {
requestController: growthRecordSaveRequestController,
});
if (!pageActive) return;
if (contentPassword.value) {
try {
await lifeRecordApi.setGrowthRecordPassword(
genealogyId.value,
createdRecord.id,
contentPassword.value,
{ requestController: growthPasswordRequestController },
);
passwordSetupNotice = "成长记录已保存,并已启用内容密码。";
} catch (passwordError) {
if (!pageActive || isRequestCancelled(passwordError)) return;
const failureCopy = getRequestErrorMessage(passwordError, "内容密码设置失败");
passwordSetupNotice = `成长记录已经创建,但${failureCopy}。请打开详情重新设置;当前内容可能尚未受到密码保护。`;
}
}
}
if (!pageActive) return;
resetForm();
view.value = "list";
saveNotice.value = true;
saveNotice.value = passwordSetupNotice || "成长记录已保存。";
await loadRecords();
} catch (cause) {
if (!pageActive) return;
@@ -708,7 +816,13 @@ const deleteRecord = async () => {
}
};
const requestBack = () =>
detailTransientOpen.value
growthBatch.deleting.value
? true
: growthBatch.confirmationVisible.value
? (growthBatch.cancelDelete(), true)
: growthBatch.selectionMode.value
? (growthBatch.exitSelectionMode(), true)
: detailTransientOpen.value
? (growthRecordDetailDialog.value?.closeTransient(), true)
: view.value !== "form"
? goBack()
@@ -745,6 +859,7 @@ onUnload(() => {
growthEditorDetailRequestController.abort();
growthMediaUploadRequestController.abort();
growthRecordSaveRequestController.abort();
growthPasswordRequestController.abort();
personOptionsController.abort();
growthTypeRequestController.abort();
growthRecordDeleteController.abort();
@@ -822,6 +937,9 @@ onUnload(() => {
.field textarea {
min-height: 150rpx;
}
.field--password input + input {
margin-top: 14rpx;
}
.field--picker picker {
display: block;
}