feat(org): homepage markdown image insert via compress pipeline

Made-with: Cursor
This commit is contained in:
2026-04-28 08:07:34 +08:00
parent 1c59acc72b
commit 6d74a34aa2
+147 -7
View File
@@ -1,13 +1,34 @@
<template>
<div class="main">
<van-cell-group inset>
<div class="editor-toolbar">
<span class="toolbar-hint">图片与头像相同经压缩任务处理</span>
<van-uploader
:max-count="1"
:max-size="orgHomepageImageMaxBytes"
accept="image/*"
:after-read="onImageAfterRead"
:before-read="beforeImageRead"
@oversize="onImageOversize"
>
<van-button
type="primary"
size="small"
icon="photo-o"
:loading="imageUploading"
>
插入图片
</van-button>
</van-uploader>
</div>
<van-field
ref="markdownFieldRef"
v-model="markdownText"
rows="10"
autosize
type="textarea"
label="机构首页 Markdown"
placeholder="请输入机构首页内容"
placeholder="请输入机构首页内容,或插入图片生成 Markdown"
/>
</van-cell-group>
@@ -17,15 +38,28 @@
<div class="preview">
<h5>预览</h5>
<pre>{{ markdownText }}</pre>
<div class="preview-md" v-html="previewHtml" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import { Button, CellGroup, Field, showFailToast, showSuccessToast } from "vant";
import { computed, defineComponent, nextTick, onMounted, ref } from "vue";
import {
Button,
CellGroup,
Field,
showFailToast,
showSuccessToast,
Uploader,
} from "vant";
import { marked } from "marked";
import useOrg from "~/store/org";
import { ImageHelper } from "~/utils";
const ORG_HOMEPAGE_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
marked.setOptions({ breaks: true, gfm: true });
export default defineComponent({
name: "org-homepage",
@@ -33,10 +67,73 @@ export default defineComponent({
[Button.name]: Button,
[CellGroup.name]: CellGroup,
[Field.name]: Field,
[Uploader.name]: Uploader,
},
setup() {
const { store, getHomepage, saveHomepage } = useOrg();
const markdownText = ref("");
const markdownFieldRef = ref<InstanceType<typeof Field> | null>(null);
const imageUploading = ref(false);
const previewHtml = computed(() => {
const raw = (markdownText.value || "").trim();
return raw ? marked(raw) : '<p class="preview-empty">暂无内容</p>';
});
const getMarkdownTextarea = (): HTMLTextAreaElement | null => {
const root = markdownFieldRef.value as unknown as { $el?: HTMLElement };
return root?.$el?.querySelector?.("textarea") ?? null;
};
const insertMarkdownAtCaret = async (snippet: string) => {
const textarea = getMarkdownTextarea();
const text = markdownText.value;
if (!textarea) {
markdownText.value = text + snippet;
return;
}
const start = textarea.selectionStart ?? text.length;
const end = textarea.selectionEnd ?? text.length;
markdownText.value = text.slice(0, start) + snippet + text.slice(end);
await nextTick();
const ta = getMarkdownTextarea();
if (ta) {
const pos = start + snippet.length;
ta.focus();
ta.setSelectionRange(pos, pos);
}
};
const beforeImageRead = (file: File) => {
if (!file.type.startsWith("image/")) {
showFailToast("请选择图片文件");
return false;
}
return true;
};
const onImageOversize = () => {
showFailToast("文件大小不能超过 10MB");
};
const onImageAfterRead = async (item: { file: File } | { file: File }[]) => {
const first = Array.isArray(item) ? item[0] : item;
const file = first?.file;
if (!file) {
showFailToast("读取文件失败");
return;
}
imageUploading.value = true;
try {
const urlRaw = await ImageHelper.compress_by_form(file);
if (!urlRaw) return;
const url = urlRaw.split("?")[0];
await insertMarkdownAtCaret(`\n![](${url})\n`);
showSuccessToast("已插入图片");
} finally {
imageUploading.value = false;
}
};
onMounted(async () => {
if (!store.currentOrgId) {
@@ -57,7 +154,17 @@ export default defineComponent({
}
};
return { markdownText, onSave };
return {
markdownText,
markdownFieldRef,
onSave,
previewHtml,
beforeImageRead,
onImageAfterRead,
onImageOversize,
imageUploading,
orgHomepageImageMaxBytes: ORG_HOMEPAGE_IMAGE_MAX_BYTES,
};
},
});
</script>
@@ -66,14 +173,47 @@ export default defineComponent({
.main {
padding: 0.5rem;
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.24rem;
padding: 0.32rem 0.32rem 0;
}
.toolbar-hint {
font-size: 0.24rem;
color: #969799;
flex: 1;
min-width: 4rem;
}
.actions {
margin: 0.8rem 0;
}
.preview pre {
white-space: pre-wrap;
.preview h5 {
margin: 0 0 0.32rem;
font-size: 0.28rem;
color: #323233;
}
.preview-md {
word-break: break-word;
background: #f7f8fa;
border-radius: 8px;
padding: 0.5rem;
font-size: 0.28rem;
line-height: 1.6;
}
.preview-md :deep(img) {
max-width: 100%;
height: auto;
vertical-align: middle;
}
.preview-md :deep(.preview-empty) {
color: #969799;
margin: 0;
}
.preview-md :deep(pre) {
white-space: pre-wrap;
word-break: break-word;
}
</style>