feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
label="新建证件档案"
|
||||
@click="openDocumentCreate"
|
||||
/>
|
||||
<text v-if="!documents.length" class="document-dialog-empty">这位成员还没有可查看的重要证件</text>
|
||||
<text v-if="!documents.length" class="document-dialog-empty">{{ personId ? "这位成员还没有可查看的重要证件" : "当前家谱还没有可查看的重要证件" }}</text>
|
||||
<view
|
||||
v-for="documentSummary in documents"
|
||||
:key="documentSummary.documentId"
|
||||
@@ -34,7 +34,7 @@
|
||||
<view>
|
||||
<text>{{ documentSummary.documentTitle }}</text>
|
||||
<text
|
||||
>{{ documentTypeLabel(documentSummary.documentType) }}{{
|
||||
>{{ documentSummary.lineagePersonName ? `${documentSummary.lineagePersonName} · ` : "" }}{{ documentTypeLabel(documentSummary.documentType) }}{{
|
||||
documentSummary.maskedIdentifier
|
||||
? ` · ${documentSummary.maskedIdentifier}`
|
||||
: ""
|
||||
@@ -56,6 +56,7 @@
|
||||
<view class="document-unlock-action" role="button" aria-label="解锁重要证件" @click="unlockDocument">
|
||||
<text>{{ documentState === 'submitting' ? '正在解锁…' : '解锁并查看' }}</text>
|
||||
</view>
|
||||
<button class="document-recovery-link" @click="passwordRecoveryVisible = true">忘记内容密码?</button>
|
||||
</template>
|
||||
<template v-else-if="documentDialogMode === 'detail' && selectedDocument">
|
||||
<view class="document-detail-row"><text>证件类型</text><text>{{ documentTypeLabel(selectedDocument.documentType) }}</text></view>
|
||||
@@ -71,12 +72,12 @@
|
||||
<text>{{ resourceUsageLabel(resource.usageType) }}</text>
|
||||
<view class="document-resource-actions">
|
||||
<button @click="openDocumentResource(resource)">查看</button>
|
||||
<button v-if="selectedDocument.canEdit && member?.canManageDocuments" @click="replaceDocumentResource(resource)">替换</button>
|
||||
<button v-if="selectedDocument.canEdit && member?.canManageDocuments" @click="requestDeleteDocumentResource(resource)">删除</button>
|
||||
<button v-if="selectedDocument.canEdit" @click="replaceDocumentResource(resource)">替换</button>
|
||||
<button v-if="selectedDocument.canEdit" @click="requestDeleteDocumentResource(resource)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="documentError" class="document-dialog-error">{{ documentError }}</text>
|
||||
<view v-if="member?.canManageDocuments" class="document-manage-actions">
|
||||
<view v-if="selectedDocument.canEdit || selectedDocument.canDelete" class="document-manage-actions">
|
||||
<AppButton v-if="selectedDocument.canEdit" compact label="编辑资料" @click="openDocumentEdit" />
|
||||
<AppButton v-if="selectedDocument.canEdit" compact type="secondary" label="添加文件" @click="addDocumentResource" />
|
||||
<AppButton
|
||||
@@ -164,6 +165,15 @@
|
||||
@confirm="disableDocumentPassword"
|
||||
@cancel="documentPasswordDisableVisible = false"
|
||||
/>
|
||||
<ContentPasswordRecoveryDialog
|
||||
:visible="passwordRecoveryVisible"
|
||||
:genealogy-id="genealogyId"
|
||||
resource-type="PERSON_DOCUMENT"
|
||||
:resource-id="String(selectedDocument?.documentId || '')"
|
||||
@close="passwordRecoveryVisible = false"
|
||||
@complete="completePasswordRecovery"
|
||||
@busy-change="passwordRecoveryBusy = $event"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -172,11 +182,12 @@ import { computed, onUnmounted, reactive, ref, watch } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ContentPasswordRecoveryDialog from "@/components/ContentPasswordRecoveryDialog.vue";
|
||||
import {
|
||||
PERSON_DOCUMENT_RESOURCE_USAGE,
|
||||
PERSON_DOCUMENT_RESOURCE_USAGE_LABELS,
|
||||
PERSON_DOCUMENT_TYPE_OPTIONS as documentTypeOptions
|
||||
PERSON_DOCUMENT_RESOURCE_USAGE_LABELS
|
||||
} from "@/services/api/person-document-contract.js";
|
||||
import { businessDictionaryApi } from "@/services/api/business-dictionary-service.js";
|
||||
import {
|
||||
createRequestController,
|
||||
isRequestCancelled
|
||||
@@ -188,7 +199,7 @@ import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guar
|
||||
|
||||
const props = defineProps({
|
||||
genealogyId: { type: String, required: true },
|
||||
personId: { type: String, required: true },
|
||||
personId: { type: String, default: "" },
|
||||
member: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["busy-change", "transient-change"]);
|
||||
@@ -197,6 +208,7 @@ const documentDialogVisible = ref(false);
|
||||
const documentDialogMode = ref("list");
|
||||
const documentState = ref("idle");
|
||||
const documents = ref([]);
|
||||
const documentTypeOptions = ref([]);
|
||||
const selectedDocument = ref(null);
|
||||
const documentPassword = ref("");
|
||||
const documentPasswordConfirm = ref("");
|
||||
@@ -205,6 +217,8 @@ const documentError = ref("");
|
||||
const documentDeleteVisible = ref(false);
|
||||
const documentResourceDeleteTarget = ref(null);
|
||||
const documentPasswordDisableVisible = ref(false);
|
||||
const passwordRecoveryVisible = ref(false);
|
||||
const passwordRecoveryBusy = ref(false);
|
||||
const documentUploadReceipt = ref(null);
|
||||
const documentForm = reactive({
|
||||
documentType: "id_card",
|
||||
@@ -213,6 +227,7 @@ const documentForm = reactive({
|
||||
description: "",
|
||||
});
|
||||
const documentListRequestController = createRequestController();
|
||||
const documentTypeRequestController = createRequestController();
|
||||
const documentDetailRequestController = createRequestController();
|
||||
const documentDraftUploadRequestController = createRequestController();
|
||||
const documentSaveRequestController = createRequestController();
|
||||
@@ -239,7 +254,9 @@ const documentDialogTitle = computed(() => {
|
||||
case "detail":
|
||||
return selectedDocument.value?.documentTitle || "证件详情";
|
||||
default:
|
||||
return `${member.value?.name || "成员"}的重要证件`;
|
||||
return props.personId
|
||||
? `${member.value?.name || "成员"}的重要证件`
|
||||
: "家谱重要证件";
|
||||
}
|
||||
});
|
||||
const documentDialogMessage = computed(() => {
|
||||
@@ -255,20 +272,16 @@ const documentDialogMessage = computed(() => {
|
||||
}
|
||||
});
|
||||
const documentTypeLabel = (documentType) =>
|
||||
({
|
||||
id_card: "身份证",
|
||||
household_register: "户口簿",
|
||||
birth_certificate: "出生证明",
|
||||
marriage_certificate: "结婚证",
|
||||
other: "其他证件",
|
||||
})[documentType] || "重要证件";
|
||||
documentTypeOptions.value.find((option) => option.value === documentType)?.label ||
|
||||
documents.value.find((document) => document.documentType === documentType)?.documentTypeLabel ||
|
||||
"重要证件";
|
||||
const documentTypeLabels = computed(() =>
|
||||
documentTypeOptions.map((documentType) => documentType.label),
|
||||
documentTypeOptions.value.map((documentType) => documentType.label),
|
||||
);
|
||||
const documentTypeIndex = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
documentTypeOptions.findIndex(
|
||||
documentTypeOptions.value.findIndex(
|
||||
(documentType) => documentType.value === documentForm.documentType,
|
||||
),
|
||||
),
|
||||
@@ -280,11 +293,12 @@ const hasTransient = computed(() =>
|
||||
documentDialogVisible.value ||
|
||||
documentDeleteVisible.value ||
|
||||
documentResourceDeleteTarget.value ||
|
||||
documentPasswordDisableVisible.value,
|
||||
documentPasswordDisableVisible.value ||
|
||||
passwordRecoveryVisible.value,
|
||||
),
|
||||
);
|
||||
const isBusy = computed(() =>
|
||||
documentState.value === "loading" || documentState.value === "submitting",
|
||||
documentState.value === "loading" || documentState.value === "submitting" || passwordRecoveryBusy.value,
|
||||
);
|
||||
const shouldIgnoreDocumentFailure = (error) => !isActive || isRequestCancelled(error);
|
||||
const isCurrentPersonContext = (personId, activeWorkflow) =>
|
||||
@@ -301,6 +315,7 @@ watch(isBusy, (value) => emit("busy-change", value), { immediate: true });
|
||||
const resetDocumentWorkflow = () => {
|
||||
workflowVersion += 1;
|
||||
documentListRequestController.abort();
|
||||
documentTypeRequestController.abort();
|
||||
documentDetailRequestController.abort();
|
||||
documentDraftUploadRequestController.abort();
|
||||
documentSaveRequestController.abort();
|
||||
@@ -315,6 +330,7 @@ const resetDocumentWorkflow = () => {
|
||||
documentDeleteVisible.value = false;
|
||||
documentResourceDeleteTarget.value = null;
|
||||
documentPasswordDisableVisible.value = false;
|
||||
passwordRecoveryVisible.value = false;
|
||||
documentDialogMode.value = "list";
|
||||
documentState.value = "idle";
|
||||
documents.value = [];
|
||||
@@ -336,13 +352,22 @@ const open = async () => {
|
||||
selectedDocument.value = null;
|
||||
documentAccessToken.value = "";
|
||||
try {
|
||||
const documentRows = await personDocumentApi.getPersonDocuments(
|
||||
props.genealogyId,
|
||||
{ lineagePersonId: props.personId },
|
||||
{ requestController: documentListRequestController },
|
||||
);
|
||||
const documentQuery = props.personId
|
||||
? { lineagePersonId: props.personId }
|
||||
: {};
|
||||
const [documentRows, typeOptions] = await Promise.all([
|
||||
personDocumentApi.getPersonDocuments(
|
||||
props.genealogyId,
|
||||
documentQuery,
|
||||
{ requestController: documentListRequestController },
|
||||
),
|
||||
businessDictionaryApi.getBusinessDictionaryOptions("gen_person_document_type", {
|
||||
requestController: documentTypeRequestController,
|
||||
}),
|
||||
]);
|
||||
if (!isCurrentPersonContext(documentPersonId, activeWorkflow)) return;
|
||||
documents.value = documentRows;
|
||||
documentTypeOptions.value = typeOptions;
|
||||
documentState.value = "ready";
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -363,9 +388,9 @@ const showDocumentList = () => {
|
||||
documentError.value = "";
|
||||
};
|
||||
const openDocumentCreate = () => {
|
||||
if (!member.value?.canCreateDocument) return;
|
||||
if (!member.value?.canCreateDocument || !documentTypeOptions.value.length) return;
|
||||
Object.assign(documentForm, {
|
||||
documentType: "id_card",
|
||||
documentType: documentTypeOptions.value.find((option) => option.default)?.value || documentTypeOptions.value[0].value,
|
||||
documentTitle: "",
|
||||
maskedIdentifier: "",
|
||||
description: "",
|
||||
@@ -376,19 +401,23 @@ const openDocumentCreate = () => {
|
||||
documentError.value = "";
|
||||
};
|
||||
const openDocumentEdit = () => {
|
||||
if (!selectedDocument.value?.canEdit || !member.value?.canManageDocuments) return;
|
||||
if (!selectedDocument.value?.canEdit) return;
|
||||
const currentTypeAvailable = documentTypeOptions.value.some(
|
||||
(option) => option.value === selectedDocument.value.documentType,
|
||||
);
|
||||
Object.assign(documentForm, {
|
||||
documentType: selectedDocument.value.documentType,
|
||||
documentType: currentTypeAvailable ? selectedDocument.value.documentType : "",
|
||||
documentTitle: selectedDocument.value.documentTitle,
|
||||
maskedIdentifier: selectedDocument.value.maskedIdentifier,
|
||||
description: selectedDocument.value.description,
|
||||
});
|
||||
documentDialogMode.value = "edit";
|
||||
documentState.value = "ready";
|
||||
documentError.value = "";
|
||||
documentError.value = currentTypeAvailable ? "" : "原证件类型已停用,请重新选择。";
|
||||
};
|
||||
const selectDocumentType = (event) => {
|
||||
documentForm.documentType = documentTypeOptions[Number(event.detail.value)]?.value || "id_card";
|
||||
documentForm.documentType = documentTypeOptions.value[Number(event.detail.value)]?.value || "";
|
||||
documentError.value = "";
|
||||
};
|
||||
const uploadDocumentImage = async () => {
|
||||
if (documentState.value === "submitting") return;
|
||||
@@ -527,7 +556,7 @@ const loadDocumentDetail = async (documentId, accessToken) => {
|
||||
}
|
||||
};
|
||||
const updateDocument = async () => {
|
||||
if (!selectedDocument.value?.canEdit || !member.value?.canManageDocuments || documentState.value === "submitting") return;
|
||||
if (!selectedDocument.value?.canEdit || documentState.value === "submitting") return;
|
||||
if (!documentForm.documentTitle.trim()) {
|
||||
documentError.value = "请填写证件名称。";
|
||||
return;
|
||||
@@ -535,13 +564,14 @@ const updateDocument = async () => {
|
||||
documentState.value = "submitting";
|
||||
documentError.value = "";
|
||||
const documentPersonId = props.personId;
|
||||
const targetPersonId = props.personId || selectedDocument.value.lineagePersonId;
|
||||
const documentId = selectedDocument.value.documentId;
|
||||
const activeWorkflow = workflowVersion;
|
||||
try {
|
||||
const updatedDocument = await personDocumentApi.updatePersonDocument(
|
||||
props.genealogyId,
|
||||
documentId,
|
||||
{ lineagePersonId: documentPersonId, ...documentForm },
|
||||
{ lineagePersonId: targetPersonId, ...documentForm },
|
||||
{ requestController: documentSaveRequestController },
|
||||
);
|
||||
if (!isCurrentDocumentContext(documentPersonId, documentId, activeWorkflow)) return;
|
||||
@@ -600,6 +630,10 @@ const unlockDocument = async () => {
|
||||
documentError.value = getRequestErrorMessage(error, "密码不正确,请重新输入。");
|
||||
}
|
||||
};
|
||||
const completePasswordRecovery = (newPassword) => {
|
||||
documentPassword.value = newPassword;
|
||||
documentError.value = "内容密码已重置,请点击“解锁并查看”确认。";
|
||||
};
|
||||
const openDocumentResource = async (resource) => {
|
||||
if (!selectedDocument.value) return;
|
||||
const documentPersonId = props.personId;
|
||||
@@ -651,7 +685,7 @@ const openDocumentResource = async (resource) => {
|
||||
}
|
||||
};
|
||||
const addDocumentResource = async () => {
|
||||
if (!selectedDocument.value?.canEdit || !member.value?.canManageDocuments || documentState.value === "submitting") return;
|
||||
if (!selectedDocument.value?.canEdit || documentState.value === "submitting") return;
|
||||
const documentPersonId = props.personId;
|
||||
const documentId = selectedDocument.value.documentId;
|
||||
const activeWorkflow = workflowVersion;
|
||||
@@ -711,7 +745,7 @@ const addDocumentResource = async () => {
|
||||
}
|
||||
};
|
||||
const replaceDocumentResource = async (resource) => {
|
||||
if (!selectedDocument.value?.canEdit || !member.value?.canManageDocuments || documentState.value === "submitting") return;
|
||||
if (!selectedDocument.value?.canEdit || documentState.value === "submitting") return;
|
||||
documentState.value = "submitting";
|
||||
documentError.value = "";
|
||||
const documentPersonId = props.personId;
|
||||
@@ -753,7 +787,7 @@ const replaceDocumentResource = async (resource) => {
|
||||
}
|
||||
};
|
||||
const requestDeleteDocumentResource = (resource) => {
|
||||
if (!selectedDocument.value?.canEdit || !member.value?.canManageDocuments) return;
|
||||
if (!selectedDocument.value?.canEdit) return;
|
||||
documentResourceDeleteTarget.value = resource;
|
||||
};
|
||||
const confirmDeleteDocumentResource = async () => {
|
||||
@@ -785,7 +819,7 @@ const confirmDeleteDocumentResource = async () => {
|
||||
}
|
||||
};
|
||||
const openDocumentPassword = () => {
|
||||
if (!selectedDocument.value?.canEdit || !member.value?.canManageDocuments) return;
|
||||
if (!selectedDocument.value?.canEdit) return;
|
||||
documentPassword.value = "";
|
||||
documentPasswordConfirm.value = "";
|
||||
documentError.value = "";
|
||||
@@ -861,7 +895,7 @@ const handleDocumentConfirm = () => {
|
||||
resetDocumentWorkflow();
|
||||
};
|
||||
const requestDeleteDocument = () => {
|
||||
if (!selectedDocument.value?.canDelete || !member.value?.canManageDocuments) return;
|
||||
if (!selectedDocument.value?.canDelete) return;
|
||||
documentDeleteVisible.value = true;
|
||||
};
|
||||
const confirmDeleteDocument = async () => {
|
||||
@@ -891,6 +925,11 @@ const confirmDeleteDocument = async () => {
|
||||
}
|
||||
};
|
||||
const closeTransient = () => {
|
||||
if (passwordRecoveryVisible.value) {
|
||||
if (passwordRecoveryBusy.value) return false;
|
||||
passwordRecoveryVisible.value = false;
|
||||
return true;
|
||||
}
|
||||
if (documentResourceDeleteTarget.value) {
|
||||
documentResourceDeleteTarget.value = null;
|
||||
return true;
|
||||
@@ -973,6 +1012,8 @@ onUnmounted(() => {
|
||||
font-size: clamp(14px, 22rpx, 17px);
|
||||
}
|
||||
.document-upload-button::after { border: 0; }
|
||||
.document-recovery-link { display: block; min-height: var(--app-touch-min); margin: 6rpx auto 0; padding: 0 16rpx; border: 0; background: transparent; color: $brand-red; font-size: clamp(13px, 21rpx, 16px); }
|
||||
.document-recovery-link::after { border: 0; }
|
||||
.document-manage-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user