Files
huike-front/src/pages/org/homepage.vue
T

270 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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="请输入机构首页内容,或插入图片生成 Markdown"
/>
</van-cell-group>
<div class="actions">
<van-button type="primary" block @click="onSave">保存</van-button>
</div>
<div class="preview">
<h5>预览</h5>
<div class="preview-md" v-html="previewHtml" />
</div>
</div>
</template>
<script lang="ts">
import { computedAsync } from "@vueuse/core";
import { 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";
import { UpyunAccess } from "~/utils/upyun";
const ORG_HOMEPAGE_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
marked.setOptions({ breaks: true, gfm: true });
/** 历史失败上传留在正文里的占位,避免继续污染预览 */
function stripBrokenCompressMarkdown(md: string): string {
return md
.replace(/!\[[^\]]*\]\(\s*failed to open file[^)]*\)/g, "")
.replace(/\n{3,}/g, "\n\n");
}
/** 预览区 img:又拍云需带 _upt,否则浏览器无法加载 */
async function signUpyunImagesInMarkedHtml(html: string): Promise<string> {
if (typeof document === "undefined" || !html.includes("<img")) {
return html;
}
const wrap = document.createElement("div");
wrap.innerHTML = html;
const imgs = wrap.querySelectorAll("img[src]");
await Promise.all(
Array.from(imgs).map(async (el) => {
const src = el.getAttribute("src");
if (!src?.includes("upyun")) return;
const signed = await UpyunAccess.getSign(src);
if (signed) el.setAttribute("src", signed);
}),
);
return wrap.innerHTML;
}
export default defineComponent({
name: "org-homepage",
components: {
[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 = computedAsync(
async () => {
const raw = (markdownText.value || "").trim();
if (!raw) return '<p class="preview-empty">暂无内容</p>';
const html = marked.parse(raw) as string;
return signUpyunImagesInMarkedHtml(html);
},
'<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 {
console.info("[org-homepage] image upload start", {
name: file.name,
size: file.size,
type: file.type,
});
const urlRaw = await ImageHelper.compress_by_form(file);
if (!urlRaw) return;
const url = urlRaw.split("?")[0];
console.info("[org-homepage] compress ok, create_resource", { url });
const rid = await ImageHelper.create_resource(url, file.name || "image.jpg", {
res_type: "org_homepage",
});
if (!rid) {
showFailToast("资源登记失败,图片可能已上传但未绑定账号");
return;
}
console.info("[org-homepage] create_resource ok", { rid });
await insertMarkdownAtCaret(`\n![](${url})\n`);
showSuccessToast("已插入图片");
} finally {
imageUploading.value = false;
}
};
onMounted(async () => {
if (!store.currentOrgId) {
showFailToast("请先选择机构");
return;
}
const loaded = (await getHomepage(store.currentOrgId)) || "";
markdownText.value = stripBrokenCompressMarkdown(loaded);
});
const onSave = async () => {
if (!store.currentOrgId) {
showFailToast("请先选择机构");
return;
}
const cleaned = stripBrokenCompressMarkdown(markdownText.value);
markdownText.value = cleaned;
const ok = await saveHomepage(store.currentOrgId, cleaned);
if (ok) {
showSuccessToast("保存成功");
}
};
return {
markdownText,
markdownFieldRef,
onSave,
previewHtml,
beforeImageRead,
onImageAfterRead,
onImageOversize,
imageUploading,
orgHomepageImageMaxBytes: ORG_HOMEPAGE_IMAGE_MAX_BYTES,
};
},
});
</script>
<style scoped lang="less">
.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 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>