add StoreDiary

This commit is contained in:
Timi
2026-08-31 17:32:01 +08:00
parent 95a400cd93
commit 1e83fdf883
9 changed files with 1288 additions and 1 deletions
+322
View File
@@ -0,0 +1,322 @@
<template>
<div v-can="pageAccess" class="store-diary-edit">
<t-form class="form" label-align="left" label-width="5rem" v-entrance-transition>
<t-form-item label="所属门店">
<div class="picker-text" v-text="selectedStoreName" />
</t-form-item>
<t-form-item label="日记标题" required-mark>
<t-input
v-model.trim="article.title"
borderless
maxlength="100"
placeholder="请输入日记标题"
/>
</t-form-item>
<t-form-item label="日记摘要">
<t-textarea
v-model.trim="article.description"
borderless
maxlength="200"
autosize
placeholder="可填写本篇日记摘要"
/>
</t-form-item>
<section class="editor-wrapper" v-entrance-transition="70">
<article-editor
ref="articleEditorRef"
:model-value="article"
class="editor"
placeholder="请输入门店日记正文,可插入图片或视频"
:upload-image="uploadImage"
:upload-video="uploadVideo"
:resolve-attachment-url="resolveAttachmentUrl"
@update:model-value="updateArticle"
/>
</section>
</t-form>
</div>
</template>
<script setup lang="ts">
import {
ArticleEditor,
type Article as ArticleEditorArticle,
type ArticleEditorExpose,
type UploadResult
} from "timi-tdesign-mobile";
import { AttachmentAPI, useUserStore } from "timi-web-api";
import StoreDiaryAPI from "@/api/StoreDiaryAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { StoreDiary } from "@/types/StoreDiary";
import { getErrorMessage } from "@/utils/error";
import { compressUploadImage } from "@/utils/imageCompression";
import { getAttachmentReadURL } from "@/utils/attachment";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "StoreDiaryEdit"
});
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const routeId = computed(() => String(route.params.id || ""));
const isCreateMode = computed(() => !routeId.value);
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.storeDiary.create : permissionCodes.storeDiary.update);
const pageAccess = computed(() => ({
value: [permissionCodes.storeDiary.read, savePermissionCode.value],
mode: "and" as const
}));
// ---------- 日记表单 ----------
const isSaving = ref(false);
const articleEditorRef = ref<ArticleEditorExpose>();
const article = ref<StoreDiary>(createEmptyArticle());
function createEmptyArticle(): StoreDiary {
return {
bizType: "GAO_STORE_DIARY",
contentType: "TIP_TAP",
content: "",
title: "",
description: "",
attachmentList: [],
status: "NORMAL"
};
}
function normalizeContentType(value?: string): StoreDiary["contentType"] {
if (value === "TEXT" || value === "MARKDOWN" || value === "TIP_TAP") {
return value;
}
return "TIP_TAP";
}
function updateArticle(value: ArticleEditorArticle): void {
article.value = {
...article.value,
...value,
contentType: normalizeContentType(value.contentType)
};
}
const selectedStoreName = computed(() => {
const store = currentStoreStore.storeList.find(item => item.id === article.value.bizId)
|| (currentStoreStore.loginStore?.id === article.value.bizId ? currentStoreStore.loginStore : undefined);
return store?.name || store?.code || "请选择门店";
});
// ---------- 媒体上传 ----------
async function uploadMedia(file: File): Promise<UploadResult> {
const [tempFile] = await AttachmentAPI.upload(file);
if (!tempFile?.id) {
throw new Error("媒体上传失败");
}
return {
id: tempFile.id,
url: AttachmentAPI.getTempReadURL(tempFile.id),
name: file.name
};
}
async function uploadImage(file: File): Promise<UploadResult> {
return await uploadMedia(await compressUploadImage(file));
}
async function uploadVideo(file: File): Promise<UploadResult> {
return await uploadMedia(file);
}
function resolveAttachmentUrl(attachmentId: string): string {
return getAttachmentReadURL(attachmentId);
}
// ---------- 保存 ----------
function getContentText(content: string): string {
const parser = new DOMParser();
return parser.parseFromString(content, "text/html").body.textContent?.trim() || "";
}
function hasMediaContent(content: string): boolean {
const parser = new DOMParser();
return Boolean(parser.parseFromString(content, "text/html").querySelector("img, video"));
}
function validateArticle(value: StoreDiary): boolean {
if (!value.bizId) {
Message.warning("请选择门店");
return false;
}
if (!value.title?.trim()) {
Message.warning("请输入日记标题");
return false;
}
const content = value.content || "";
if (!getContentText(content) && !hasMediaContent(content)) {
Message.warning("请输入日记正文");
return false;
}
return true;
}
async function save(): Promise<void> {
if (isSaving.value) {
return;
}
if (!article.value.title?.trim()) {
Message.warning("请输入日记标题");
return;
}
isSaving.value = true;
try {
const editorValue = await articleEditorRef.value?.submit();
const value: StoreDiary = {
...article.value,
...(editorValue || {}),
title: article.value.title?.trim() || "",
description: article.value.description?.trim() || "",
contentType: normalizeContentType(editorValue?.contentType ?? article.value.contentType),
content: editorValue?.content ?? article.value.content ?? "",
attachmentList: editorValue?.attachmentList ?? article.value.attachmentList ?? [],
status: article.value.status || "NORMAL"
};
if (!validateArticle(value)) {
return;
}
article.value = value;
if (isCreateMode.value) {
const id = await StoreDiaryAPI.create(value);
Message.success("门店日记已创建");
await router.replace({
name: "StoreDiaryDetail",
params: { id }
});
return;
}
await StoreDiaryAPI.update(value);
Message.success("门店日记已保存");
await router.replace({
name: "StoreDiaryDetail",
params: { id: routeId.value }
});
} catch (error) {
Message.error(getErrorMessage(error, "门店日记保存失败"));
} finally {
isSaving.value = false;
}
}
// ---------- 页面数据 ----------
async function prepareStoreScope(): Promise<void> {
const store = await currentStoreStore.loadLoginStore();
if (store?.id) {
article.value.bizId = store.id;
}
}
async function loadArticle(): Promise<void> {
await prepareStoreScope();
if (isCreateMode.value) {
article.value = {
...createEmptyArticle(),
bizId: article.value.bizId || currentStoreStore.currentStoreId
};
return;
}
const result = await StoreDiaryAPI.detail(routeId.value);
article.value = {
...result,
title: result.title || "",
description: result.description || "",
contentType: result.contentType || "TIP_TAP",
content: result.content || "",
attachmentList: result.attachmentList || []
};
}
onMounted(async () => {
if (userStore.hasPermissions(savePermissionCode.value)) {
navBarStore.setRightText("保存", "var(--td-brand-color)", save);
}
try {
await loadArticle();
} catch (error) {
Message.error(getErrorMessage(error, "门店日记加载失败"));
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.store-diary-edit {
min-height: 100%;
padding-top: var(--app-nav-height);
.form {
margin: 0;
border-radius: 0;
.picker-text {
width: 100%;
text-align: right;
}
.editor-wrapper {
width: 100%;
min-width: 0;
max-width: 100%;
overflow: visible;
.editor {
width: 100%;
min-width: 0;
max-width: 100%;
border: none;
border-radius: 0;
}
:deep(.tp-editor) {
width: 100%;
min-width: 0;
max-width: 100%;
height: auto;
overflow: visible;
}
:deep(.content) {
overflow: visible;
}
:deep(.toolbar) {
width: 100%;
min-width: 0;
max-width: 100%;
top: var(--app-nav-height);
z-index: 10;
position: sticky;
}
:deep(.toolbar .scroll) {
width: 100%;
min-width: 0;
max-width: 100%;
}
}
}
}
</style>