feat(org-home): render homepage markdown with signed images on teacher/student home
Made-with: Cursor
This commit is contained in:
@@ -54,41 +54,12 @@ import {
|
||||
showSuccessToast,
|
||||
Uploader,
|
||||
} from "vant";
|
||||
import { marked } from "marked";
|
||||
import useOrg from "~/store/org";
|
||||
import { ImageHelper } from "~/utils";
|
||||
import { UpyunAccess } from "~/utils/upyun";
|
||||
import { renderOrgHomepageMarkdown, stripBrokenCompressMarkdown } from "~/utils/orgHomepageMarkdown";
|
||||
|
||||
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: {
|
||||
@@ -107,8 +78,10 @@ export default defineComponent({
|
||||
async () => {
|
||||
const raw = (markdownText.value || "").trim();
|
||||
if (!raw) return '<p class="preview-empty">暂无内容</p>';
|
||||
const html = marked.parse(raw) as string;
|
||||
return signUpyunImagesInMarkedHtml(html);
|
||||
const html = await renderOrgHomepageMarkdown(raw);
|
||||
return html.trim()
|
||||
? html
|
||||
: '<p class="preview-empty">暂无内容</p>';
|
||||
},
|
||||
'<p class="preview-empty">暂无内容</p>',
|
||||
);
|
||||
|
||||
+107
-55
@@ -1,92 +1,144 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="header">
|
||||
欢迎回来,{{name}}
|
||||
欢迎回来,{{ name }}
|
||||
</div>
|
||||
<div class="org-homepage-card">
|
||||
<div class="org-homepage-title">机构首页:{{ currentOrgName || "未选择机构" }}</div>
|
||||
<pre class="org-homepage-content">{{ homepageContent }}</pre>
|
||||
<div v-if="!orgStore.currentOrgId" class="org-homepage-plain">
|
||||
请先在「我的」页选择机构。
|
||||
</div>
|
||||
<div v-else class="org-homepage-md" v-html="homepageHtml" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref } from 'vue';
|
||||
import { computedAsync } from "@vueuse/core";
|
||||
import { computed, defineComponent, onActivated, onMounted, ref } from "vue";
|
||||
import useUser from "~/store/user";
|
||||
import useOrg from "~/store/org";
|
||||
|
||||
import { renderOrgHomepageMarkdown } from "~/utils/orgHomepageMarkdown";
|
||||
|
||||
export default defineComponent({
|
||||
name: "student-home",
|
||||
setup() {
|
||||
|
||||
const usingUser = useUser();
|
||||
const { store: orgStore, getHomepage } = useOrg();
|
||||
const homepageContent = ref("机构暂未配置首页内容。");
|
||||
const markdownSource = ref("");
|
||||
|
||||
const name = computed(() => usingUser.store.current.real_name)
|
||||
const homepageHtml = computedAsync(
|
||||
async () => {
|
||||
const raw = markdownSource.value.trim();
|
||||
if (!raw)
|
||||
return '<p class="org-homepage-empty">机构暂未配置首页内容。</p>';
|
||||
const html = await renderOrgHomepageMarkdown(raw);
|
||||
return html.trim()
|
||||
? html
|
||||
: '<p class="org-homepage-empty">机构暂未配置首页内容。</p>';
|
||||
},
|
||||
'<p class="org-homepage-empty">加载中…</p>',
|
||||
);
|
||||
|
||||
const loadHomepage = async () => {
|
||||
if (!orgStore.currentOrgId) return;
|
||||
const markdownText = await getHomepage(orgStore.currentOrgId);
|
||||
markdownSource.value = (markdownText || "").trim();
|
||||
};
|
||||
|
||||
const name = computed(() => usingUser.store.current.real_name);
|
||||
const currentOrgName = computed(() => {
|
||||
const currentOrgId = orgStore.currentOrgId;
|
||||
if (!currentOrgId) return "";
|
||||
return orgStore.orgs.find((org) => org.id === currentOrgId)?.org_name || currentOrgId;
|
||||
return (
|
||||
orgStore.orgs.find((org) => org.id === currentOrgId)?.org_name ||
|
||||
currentOrgId
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!orgStore.currentOrgId) {
|
||||
homepageContent.value = "请先在“我的”页选择机构。";
|
||||
return;
|
||||
}
|
||||
const markdownText = await getHomepage(orgStore.currentOrgId);
|
||||
homepageContent.value = (markdownText || "").trim() || "机构暂未配置首页内容。";
|
||||
});
|
||||
|
||||
|
||||
onMounted(loadHomepage);
|
||||
onActivated(loadHomepage);
|
||||
|
||||
return {
|
||||
name,
|
||||
currentOrgName,
|
||||
homepageContent,
|
||||
orgStore,
|
||||
homepageHtml,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.main {
|
||||
width: 100vw;
|
||||
overflow: auto;
|
||||
padding-bottom: 0.64rem;
|
||||
background: #f7f7f7;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
padding: 0.2rem 0.32rem;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.org-homepage-card {
|
||||
margin: 0.24rem 0.24rem 0;
|
||||
padding: 0.24rem;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.org-homepage-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.16rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.org-homepage-content {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
font-family: inherit;
|
||||
font-size: 0.28rem;
|
||||
}
|
||||
.main {
|
||||
width: 100vw;
|
||||
overflow: auto;
|
||||
padding-bottom: 0.64rem;
|
||||
background: #f7f7f7;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
padding: 0.2rem 0.32rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.org-homepage-card {
|
||||
margin: 0.24rem 0.24rem 0;
|
||||
padding: 0.24rem;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.org-homepage-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.16rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.org-homepage-plain {
|
||||
margin: 0;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
font-size: 0.28rem;
|
||||
}
|
||||
|
||||
.org-homepage-md {
|
||||
font-size: 0.28rem;
|
||||
line-height: 1.6;
|
||||
color: #444;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(.org-homepage-empty) {
|
||||
color: #969799;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(pre) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(h1),
|
||||
.org-homepage-md :deep(h2) {
|
||||
margin: 0.24rem 0 0.16rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(p) {
|
||||
margin: 0.16rem 0;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(ul) {
|
||||
padding-left: 0.4rem;
|
||||
margin: 0.16rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+107
-52
@@ -1,89 +1,144 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="header">
|
||||
欢迎回来,{{name}}
|
||||
欢迎回来,{{ name }}
|
||||
</div>
|
||||
<div class="org-homepage-card">
|
||||
<div class="org-homepage-title">机构首页:{{ currentOrgName || "未选择机构" }}</div>
|
||||
<pre class="org-homepage-content">{{ homepageContent }}</pre>
|
||||
<div v-if="!orgStore.currentOrgId" class="org-homepage-plain">
|
||||
请先在「我的」页选择机构。
|
||||
</div>
|
||||
<div v-else class="org-homepage-md" v-html="homepageHtml" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref } from 'vue';
|
||||
import { computedAsync } from "@vueuse/core";
|
||||
import { computed, defineComponent, onActivated, onMounted, ref } from "vue";
|
||||
import useUser from "~/store/user";
|
||||
import useOrg from "~/store/org";
|
||||
|
||||
import { renderOrgHomepageMarkdown } from "~/utils/orgHomepageMarkdown";
|
||||
|
||||
export default defineComponent({
|
||||
name: "teacher-home",
|
||||
setup() {
|
||||
|
||||
const usingUser = useUser();
|
||||
const { store: orgStore, getHomepage } = useOrg();
|
||||
const homepageContent = ref("机构暂未配置首页内容。");
|
||||
const markdownSource = ref("");
|
||||
|
||||
const name = computed(() => usingUser.store.current.real_name)
|
||||
const homepageHtml = computedAsync(
|
||||
async () => {
|
||||
const raw = markdownSource.value.trim();
|
||||
if (!raw)
|
||||
return '<p class="org-homepage-empty">机构暂未配置首页内容。</p>';
|
||||
const html = await renderOrgHomepageMarkdown(raw);
|
||||
return html.trim()
|
||||
? html
|
||||
: '<p class="org-homepage-empty">机构暂未配置首页内容。</p>';
|
||||
},
|
||||
'<p class="org-homepage-empty">加载中…</p>',
|
||||
);
|
||||
|
||||
const loadHomepage = async () => {
|
||||
if (!orgStore.currentOrgId) return;
|
||||
const markdownText = await getHomepage(orgStore.currentOrgId);
|
||||
markdownSource.value = (markdownText || "").trim();
|
||||
};
|
||||
|
||||
const name = computed(() => usingUser.store.current.real_name);
|
||||
const currentOrgName = computed(() => {
|
||||
const currentOrgId = orgStore.currentOrgId;
|
||||
if (!currentOrgId) return "";
|
||||
return orgStore.orgs.find((org) => org.id === currentOrgId)?.org_name || currentOrgId;
|
||||
return (
|
||||
orgStore.orgs.find((org) => org.id === currentOrgId)?.org_name ||
|
||||
currentOrgId
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!orgStore.currentOrgId) {
|
||||
homepageContent.value = "请先在“我的”页选择机构。";
|
||||
return;
|
||||
}
|
||||
const markdownText = await getHomepage(orgStore.currentOrgId);
|
||||
homepageContent.value = (markdownText || "").trim() || "机构暂未配置首页内容。";
|
||||
});
|
||||
onMounted(loadHomepage);
|
||||
onActivated(loadHomepage);
|
||||
|
||||
return {
|
||||
name,
|
||||
currentOrgName,
|
||||
homepageContent,
|
||||
orgStore,
|
||||
homepageHtml,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.main {
|
||||
width: 100vw;
|
||||
overflow: auto;
|
||||
padding-bottom: 0.64rem;
|
||||
background: #f7f7f7;
|
||||
position: relative;
|
||||
.main {
|
||||
width: 100vw;
|
||||
overflow: auto;
|
||||
padding-bottom: 0.64rem;
|
||||
background: #f7f7f7;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
padding: 0.2rem 0.32rem;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.org-homepage-card {
|
||||
margin: 0.24rem 0.24rem 0;
|
||||
padding: 0.24rem;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.org-homepage-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.16rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.org-homepage-content {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
font-family: inherit;
|
||||
font-size: 0.28rem;
|
||||
}
|
||||
.header {
|
||||
padding: 0.2rem 0.32rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.org-homepage-card {
|
||||
margin: 0.24rem 0.24rem 0;
|
||||
padding: 0.24rem;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.org-homepage-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.16rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.org-homepage-plain {
|
||||
margin: 0;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
font-size: 0.28rem;
|
||||
}
|
||||
|
||||
.org-homepage-md {
|
||||
font-size: 0.28rem;
|
||||
line-height: 1.6;
|
||||
color: #444;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(.org-homepage-empty) {
|
||||
color: #969799;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(pre) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(h1),
|
||||
.org-homepage-md :deep(h2) {
|
||||
margin: 0.24rem 0 0.16rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(p) {
|
||||
margin: 0.16rem 0;
|
||||
}
|
||||
|
||||
.org-homepage-md :deep(ul) {
|
||||
padding-left: 0.4rem;
|
||||
margin: 0.16rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { marked } from "marked";
|
||||
import { UpyunAccess } from "~/utils/upyun";
|
||||
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
/** 历史失败上传留在正文里的占位,避免继续污染渲染 */
|
||||
export function stripBrokenCompressMarkdown(md: string): string {
|
||||
return md
|
||||
.replace(/!\[[^\]]*\]\(\s*failed to open file[^)]*\)/g, "")
|
||||
.replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 窄屏编辑器可能把 `` 折成两行,GFM 无法识别,合并为单行。
|
||||
*/
|
||||
function normalizeMarkdownImageLineBreaks(md: string): string {
|
||||
return md.replace(/!\[([^\]]*)\]\s*\r?\n\s*\(/g, ";
|
||||
}
|
||||
|
||||
/** 预览 / 首页:img 又拍直链需带 _upt */
|
||||
export 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;
|
||||
}
|
||||
|
||||
/** 机构首页 Markdown → 可展示的 HTML(含又拍图签名) */
|
||||
export async function renderOrgHomepageMarkdown(markdown: string): Promise<string> {
|
||||
let cleaned = stripBrokenCompressMarkdown(markdown || "");
|
||||
cleaned = normalizeMarkdownImageLineBreaks(cleaned).trim();
|
||||
if (!cleaned) return "";
|
||||
const html = marked.parse(cleaned) as string;
|
||||
return signUpyunImagesInMarkedHtml(html);
|
||||
}
|
||||
Reference in New Issue
Block a user