修改完成待测试
This commit is contained in:
@@ -94,6 +94,35 @@
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="editor-field editor-field--media">
|
||||
<view>
|
||||
<text class="editor-field__label">正文图片</text>
|
||||
<text class="editor-field__hint">图片按添加顺序显示,可单独移除。</text>
|
||||
</view>
|
||||
<button
|
||||
class="upload-button"
|
||||
:disabled="uploading || isSubmitting"
|
||||
@click="uploadArticleMedia"
|
||||
>
|
||||
{{ uploading ? "上传中…" : "添加正文图片" }}
|
||||
</button>
|
||||
<view v-if="mediaReceipts.length" class="body-media-list">
|
||||
<view
|
||||
v-for="(receipt, index) in mediaReceipts"
|
||||
:key="`${receipt.ossId}-${index}`"
|
||||
class="body-media-item"
|
||||
>
|
||||
<text>{{ index + 1 }}. {{ receipt.fileName || "正文图片" }}</text>
|
||||
<button
|
||||
class="remove-media-button"
|
||||
:disabled="uploading || isSubmitting"
|
||||
:aria-label="`移除第${index + 1}张正文图片`"
|
||||
@click="removeArticleMedia(index)"
|
||||
>移除</button>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="mediaUploadError" class="editor-save-error">{{ mediaUploadError }}</text>
|
||||
</view>
|
||||
<view class="editor-field">
|
||||
<text class="editor-field__label">作者名称</text>
|
||||
<view class="editor-control"
|
||||
@@ -104,6 +133,30 @@
|
||||
@input="submitError = ''"
|
||||
/></view>
|
||||
</view>
|
||||
<view v-if="!isEdit" class="editor-field">
|
||||
<text class="editor-field__label">内容密码(选填)</text>
|
||||
<text class="editor-field__hint">填写后会先创建谱文,再立即启用密码保护;密码设置失败时会明确提示。</text>
|
||||
<view class="editor-control">
|
||||
<input
|
||||
v-model="contentPassword"
|
||||
password
|
||||
maxlength="128"
|
||||
placeholder="请输入8至128位内容密码"
|
||||
placeholder-class="editor-placeholder"
|
||||
@input="submitError = ''"
|
||||
/>
|
||||
</view>
|
||||
<view class="editor-control editor-control--password-confirm">
|
||||
<input
|
||||
v-model="contentPasswordConfirm"
|
||||
password
|
||||
maxlength="128"
|
||||
placeholder="请再次输入内容密码"
|
||||
placeholder-class="editor-placeholder"
|
||||
@input="submitError = ''"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="submitError" class="editor-save-error">{{
|
||||
submitError
|
||||
}}</text>
|
||||
@@ -178,8 +231,14 @@ const uploading = ref(false);
|
||||
const discardVisible = ref(false);
|
||||
const submitError = ref("");
|
||||
const uploadError = ref("");
|
||||
const mediaUploadError = ref("");
|
||||
const coverOssId = ref(null);
|
||||
const coverFileName = ref("");
|
||||
const mediaReceipts = ref([]);
|
||||
const contentPassword = ref("");
|
||||
const contentPasswordConfirm = ref("");
|
||||
const passwordSetupError = ref("");
|
||||
const passwordProtectionEnabled = ref(false);
|
||||
const categoryOptionsState = ref("loading");
|
||||
const categoryOptions = ref([]);
|
||||
const preservedUpdateFields = ref({ sortOrder: null, status: "" });
|
||||
@@ -194,7 +253,9 @@ const form = reactive({
|
||||
const articleDetailController = createRequestController();
|
||||
const articleCategoryController = createRequestController();
|
||||
const articleCoverUploadController = createRequestController();
|
||||
const articleMediaUploadController = createRequestController();
|
||||
const articleSaveController = createRequestController();
|
||||
const articlePasswordController = createRequestController();
|
||||
const articleCreateGuard = createNonIdempotentWriteGuard();
|
||||
let pageActive = true;
|
||||
const categoryOptionLabels = computed(() => ["不设置分类", ...categoryOptions.value.map((item) => item.name)]);
|
||||
@@ -204,13 +265,20 @@ const categoryOptionIndex = computed(() => {
|
||||
});
|
||||
const categoryOptionLabel = computed(() => categoryOptionLabels.value[categoryOptionIndex.value] || "不设置分类");
|
||||
const isEdit = computed(() => mode.value === "edit");
|
||||
const mediaOssIds = computed(() => mediaReceipts.value.map((item) => item.ossId).join(","));
|
||||
const formSnapshot = computed(() =>
|
||||
JSON.stringify({ ...form, coverOssId: coverOssId.value || "" }),
|
||||
JSON.stringify({ ...form, coverOssId: coverOssId.value || "", mediaOssIds: mediaOssIds.value }),
|
||||
);
|
||||
const isDirty = computed(() =>
|
||||
isEdit.value
|
||||
? Boolean(formBaseline.value) && formSnapshot.value !== formBaseline.value
|
||||
: Boolean(Object.values(form).some((value) => value.trim()) || coverOssId.value),
|
||||
: Boolean(
|
||||
Object.values(form).some((value) => value.trim()) ||
|
||||
coverOssId.value ||
|
||||
mediaReceipts.value.length ||
|
||||
contentPassword.value ||
|
||||
contentPasswordConfirm.value
|
||||
),
|
||||
);
|
||||
const hasValidContext = computed(
|
||||
() =>
|
||||
@@ -219,10 +287,19 @@ const hasValidContext = computed(
|
||||
);
|
||||
const resultCopy = computed(() =>
|
||||
editorState.value === "success"
|
||||
? {
|
||||
? passwordSetupError.value
|
||||
? {
|
||||
eyebrow: "谱文已创建",
|
||||
title: "密码保护尚未确认",
|
||||
copy: passwordSetupError.value,
|
||||
action: "打开谱文详情",
|
||||
}
|
||||
: {
|
||||
eyebrow: "保存成功",
|
||||
title: isEdit.value ? "谱文已更新" : "谱文已提交",
|
||||
copy: "已保存,返回后会显示最新内容。",
|
||||
copy: !isEdit.value && passwordProtectionEnabled.value
|
||||
? "谱文已保存,并已启用内容密码。"
|
||||
: "已保存,返回后会显示最新内容。",
|
||||
action: isEdit.value ? "返回谱文详情" : "返回谱文列表",
|
||||
}
|
||||
: editorState.value === "error"
|
||||
@@ -313,6 +390,10 @@ const loadArticleForEdit = async () => {
|
||||
coverFileName.value = article.coverFile
|
||||
? article.coverFile.fileName || "当前封面图片"
|
||||
: "";
|
||||
mediaReceipts.value = article.mediaFiles.map((file) => ({
|
||||
ossId: file.ossId,
|
||||
fileName: file.fileName || "正文图片",
|
||||
}));
|
||||
preservedUpdateFields.value = {
|
||||
sortOrder: article.sortOrder,
|
||||
status: article.status,
|
||||
@@ -352,6 +433,35 @@ const clearCover = () => {
|
||||
uploadError.value = "";
|
||||
};
|
||||
|
||||
const uploadArticleMedia = async () => {
|
||||
if (uploading.value || isSubmitting.value) return;
|
||||
uploading.value = true;
|
||||
mediaUploadError.value = "";
|
||||
try {
|
||||
const receipt = await pickAndUploadImage({
|
||||
requestController: articleMediaUploadController,
|
||||
});
|
||||
if (!pageActive) return;
|
||||
if (mediaReceipts.value.some((item) => item.ossId === receipt.ossId)) {
|
||||
mediaUploadError.value = "这张图片已经添加。";
|
||||
return;
|
||||
}
|
||||
mediaReceipts.value = [...mediaReceipts.value, receipt];
|
||||
} catch (error) {
|
||||
if (pageActive && !isImagePickCancelled(error) && !isRequestCancelled(error)) {
|
||||
mediaUploadError.value = getRequestErrorMessage(error, "正文图片上传失败,请稍后重试。");
|
||||
}
|
||||
} finally {
|
||||
if (pageActive) uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeArticleMedia = (index) => {
|
||||
if (uploading.value || isSubmitting.value || !Number.isInteger(index)) return;
|
||||
mediaReceipts.value = mediaReceipts.value.filter((_, mediaIndex) => mediaIndex !== index);
|
||||
mediaUploadError.value = "";
|
||||
};
|
||||
|
||||
const saveArticle = async () => {
|
||||
if (isSubmitting.value || uploading.value || !hasValidContext.value) return;
|
||||
if (!form.articleTitle.trim() || !form.articleContent.trim()) {
|
||||
@@ -363,6 +473,7 @@ const saveArticle = async () => {
|
||||
const payload = {
|
||||
...form,
|
||||
coverOssId: coverOssId.value,
|
||||
mediaOssIds: mediaOssIds.value,
|
||||
...(isEdit.value ? preservedUpdateFields.value : {}),
|
||||
};
|
||||
const createAttempt = isEdit.value ? null : articleCreateGuard.begin(payload);
|
||||
@@ -371,18 +482,48 @@ const saveArticle = async () => {
|
||||
"上次提交结果暂时无法确认,请先返回谱文列表检查,避免重复创建。";
|
||||
return;
|
||||
}
|
||||
if (contentPassword.value || contentPasswordConfirm.value) {
|
||||
if (contentPassword.value.length < 8 || contentPassword.value.length > 128) {
|
||||
submitError.value = "内容密码必须为8至128位。";
|
||||
return;
|
||||
}
|
||||
if (contentPassword.value !== contentPasswordConfirm.value) {
|
||||
submitError.value = "两次输入的内容密码不一致。";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
submitError.value = "";
|
||||
passwordSetupError.value = "";
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await familyArticleApi.updateArticle(genealogyId.value, articleId.value, payload, {
|
||||
requestController: articleSaveController,
|
||||
});
|
||||
} else {
|
||||
await familyArticleApi.createArticle(genealogyId.value, payload, {
|
||||
const createdArticle = await familyArticleApi.createArticle(genealogyId.value, payload, {
|
||||
requestController: articleSaveController,
|
||||
});
|
||||
if (!pageActive) return;
|
||||
articleId.value = createdArticle.id;
|
||||
if (contentPassword.value) {
|
||||
try {
|
||||
await familyArticleApi.setArticlePassword(
|
||||
genealogyId.value,
|
||||
createdArticle.id,
|
||||
contentPassword.value,
|
||||
{ requestController: articlePasswordController },
|
||||
);
|
||||
passwordProtectionEnabled.value = true;
|
||||
} catch (passwordError) {
|
||||
if (!pageActive || isRequestCancelled(passwordError)) return;
|
||||
const failureCopy = getRequestErrorMessage(passwordError, "内容密码设置失败");
|
||||
passwordSetupError.value = `谱文已经创建,但${failureCopy}。请进入详情重新设置;当前内容可能尚未受到密码保护。`;
|
||||
}
|
||||
contentPassword.value = "";
|
||||
contentPasswordConfirm.value = "";
|
||||
}
|
||||
}
|
||||
if (!pageActive) return;
|
||||
editorState.value = "success";
|
||||
@@ -412,7 +553,7 @@ const requestBack = () =>
|
||||
});
|
||||
const handleResultAction = () =>
|
||||
editorState.value === "success"
|
||||
? isEdit.value
|
||||
? isEdit.value || passwordSetupError.value
|
||||
? returnTo("F05", { genealogyId: genealogyId.value, articleId: articleId.value })
|
||||
: returnTo("F04", { genealogyId: genealogyId.value })
|
||||
: isEdit.value
|
||||
@@ -424,7 +565,9 @@ onUnload(() => {
|
||||
articleDetailController.abort();
|
||||
articleCategoryController.abort();
|
||||
articleCoverUploadController.abort();
|
||||
articleMediaUploadController.abort();
|
||||
articleSaveController.abort();
|
||||
articlePasswordController.abort();
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
@@ -532,7 +675,8 @@ onUnload(() => {
|
||||
.editor-control--article textarea {
|
||||
min-height: 180rpx;
|
||||
}
|
||||
.editor-field--cover {
|
||||
.editor-field--cover,
|
||||
.editor-field--media {
|
||||
display: grid;
|
||||
gap: 12rpx;
|
||||
padding: 18rpx 22rpx;
|
||||
@@ -540,7 +684,8 @@ onUnload(() => {
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 252, 245, 0.7);
|
||||
}
|
||||
.editor-field--cover .editor-field__label {
|
||||
.editor-field--cover .editor-field__label,
|
||||
.editor-field--media .editor-field__label {
|
||||
margin: 0;
|
||||
}
|
||||
.editor-field__hint {
|
||||
@@ -565,6 +710,7 @@ onUnload(() => {
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.editor-control--password-confirm { margin-top: 14rpx; }
|
||||
.remove-cover-button {
|
||||
justify-self: start;
|
||||
min-height: 72rpx;
|
||||
@@ -577,6 +723,39 @@ onUnload(() => {
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
}
|
||||
.remove-cover-button::after { border: 0; }
|
||||
.body-media-list {
|
||||
display: grid;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.body-media-item {
|
||||
display: flex;
|
||||
min-height: 66rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 8rpx 12rpx 8rpx 18rpx;
|
||||
border: 1rpx solid rgba(128, 89, 49, 0.22);
|
||||
border-radius: 8rpx;
|
||||
background: rgba(255, 253, 248, 0.58);
|
||||
}
|
||||
.body-media-item > text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: $ink-muted;
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.remove-media-button {
|
||||
min-width: var(--app-touch-min);
|
||||
min-height: var(--app-touch-min);
|
||||
margin: 0;
|
||||
padding: 0 14rpx;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: $brand-red;
|
||||
font-size: clamp(13px, 20rpx, 16px);
|
||||
}
|
||||
.remove-media-button::after { border: 0; }
|
||||
.editor-placeholder {
|
||||
color: #9e8e79;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user