feat(org): homepage markdown image insert via compress pipeline
Made-with: Cursor
This commit is contained in:
+147
-7
@@ -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\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>
|
||||
|
||||
Reference in New Issue
Block a user