feat: 将Markdown渲染改为HTML渲染
- CMS相关字段由Markdown改为WYSIWYG,前端做出对应更改 - AssetUrl重写:将CMS地址重写为本地API
This commit is contained in:
@ -2,7 +2,14 @@
|
|||||||
<div class="product-detail">
|
<div class="product-detail">
|
||||||
<el-tabs v-model="activeName" class="product-tabs" stretch>
|
<el-tabs v-model="activeName" class="product-tabs" stretch>
|
||||||
<el-tab-pane :label="$t('product-tab.details')" name="details">
|
<el-tab-pane :label="$t('product-tab.details')" name="details">
|
||||||
<markdown-renderer :content="product.description || ''" />
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div v-if="!hydrated" v-html="product.description || ''" />
|
||||||
|
<div v-else>
|
||||||
|
<html-renderer
|
||||||
|
class="html-typography"
|
||||||
|
:html="product.description || ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane :label="$t('product-tab.specs')" name="specs">
|
<el-tab-pane :label="$t('product-tab.specs')" name="specs">
|
||||||
<spec-table :data="product.specs" />
|
<spec-table :data="product.specs" />
|
||||||
@ -25,7 +32,13 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hydrated = ref(false);
|
||||||
|
|
||||||
const activeName = ref('details'); // 默认选中概览标签
|
const activeName = ref('details'); // 默认选中概览标签
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
hydrated.value = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div v-if="!hydrated" v-html="solution?.content || ''" />
|
<div v-if="!hydrated" v-html="solution?.content || ''" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<HtmlRenderer class="html-typography" :html="solution.content || ''" />
|
<html-renderer class="html-typography" :html="solution.content || ''" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div v-if="!hydrated" v-html="question.content" />
|
<div v-if="!hydrated" v-html="question.content" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<HtmlRenderer
|
<html-renderer
|
||||||
class="html-typography"
|
class="html-typography"
|
||||||
:html="question.content || ''"
|
:html="question.content || ''"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -4,7 +4,14 @@
|
|||||||
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
|
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<markdown-renderer :content="companyProfile.content || ''" />
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div v-if="!hydrated" v-html="companyProfile.content || ''" />
|
||||||
|
<div v-else>
|
||||||
|
<html-renderer
|
||||||
|
class="html-typography"
|
||||||
|
:html="companyProfile.content || ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-divider content-position="left">{{ $t('learn-more') }}</el-divider>
|
<el-divider content-position="left">{{ $t('learn-more') }}</el-divider>
|
||||||
@ -28,6 +35,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
|
const hydrated = ref(false);
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
{ label: $t('navigation.home'), to: localePath('/') },
|
{ label: $t('navigation.home'), to: localePath('/') },
|
||||||
{ label: $t('navigation.about-us') },
|
{ label: $t('navigation.about-us') },
|
||||||
@ -44,6 +52,10 @@
|
|||||||
useHead({
|
useHead({
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
hydrated.value = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -62,14 +74,6 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.markdown-body p) {
|
|
||||||
text-indent: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.markdown-body h2) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-divider__text) {
|
:deep(.el-divider__text) {
|
||||||
color: var(--el-color-info);
|
color: var(--el-color-info);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
|||||||
@ -7,7 +7,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!pending" class="page-content">
|
<div v-if="!pending" class="page-content">
|
||||||
<markdown-renderer :content="contactInfo.content || ''" />
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div v-if="!hydrated" v-html="contactInfo?.content || ''" />
|
||||||
|
<div v-else>
|
||||||
|
<html-renderer
|
||||||
|
class="html-typography"
|
||||||
|
:html="contactInfo?.content || ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="loading">
|
<div v-else class="loading">
|
||||||
<el-skeleton :rows="5" animated />
|
<el-skeleton :rows="5" animated />
|
||||||
@ -17,6 +24,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
|
const hydrated = ref(false);
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
{ label: $t('navigation.home'), to: localePath('/') },
|
{ label: $t('navigation.home'), to: localePath('/') },
|
||||||
{ label: $t('navigation.support'), to: localePath('/support') },
|
{ label: $t('navigation.support'), to: localePath('/support') },
|
||||||
@ -34,6 +42,10 @@
|
|||||||
usePageSeo({
|
usePageSeo({
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
hydrated.value = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@unocss/nuxt": "^66.4.2",
|
"@unocss/nuxt": "^66.4.2",
|
||||||
"@vueuse/nuxt": "^13.6.0",
|
"@vueuse/nuxt": "^13.6.0",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
"domhandler": "^5.0.3",
|
"domhandler": "^5.0.3",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"element-plus": "^2.10.7",
|
"element-plus": "^2.10.7",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -41,6 +41,9 @@ importers:
|
|||||||
'@vueuse/nuxt':
|
'@vueuse/nuxt':
|
||||||
specifier: ^13.6.0
|
specifier: ^13.6.0
|
||||||
version: 13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.4.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.2)(sass@1.92.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.4.0)(jiti@2.5.1)(sass@1.92.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
version: 13.9.0(magicast@0.3.5)(nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.4.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.2)(sass@1.92.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.5(@types/node@24.4.0)(jiti@2.5.1)(sass@1.92.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
||||||
|
dom-serializer:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
domhandler:
|
domhandler:
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
|
|||||||
@ -3,5 +3,13 @@ import { companyProfileService } from '~~/server/services/cms/companyProfileServ
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
||||||
|
|
||||||
return companyProfileService.getCompanyProfile(locale);
|
const companyProfile = await companyProfileService.getCompanyProfile(locale);
|
||||||
|
|
||||||
|
companyProfile.content = rewriteAssetUrls(
|
||||||
|
companyProfile.content,
|
||||||
|
useRuntimeConfig().public.directus.url,
|
||||||
|
'/api/assets'
|
||||||
|
);
|
||||||
|
|
||||||
|
return companyProfile;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,5 +3,13 @@ import { contactInfoService } from '~~/server/services/cms/contactInfoService';
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
||||||
|
|
||||||
return contactInfoService.getContactInfo(locale);
|
const contactInfo = await contactInfoService.getContactInfo(locale);
|
||||||
|
|
||||||
|
contactInfo.content = rewriteAssetUrls(
|
||||||
|
contactInfo.content,
|
||||||
|
useRuntimeConfig().public.directus.url,
|
||||||
|
'/api/assets'
|
||||||
|
);
|
||||||
|
|
||||||
|
return contactInfo;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,5 +9,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
});
|
});
|
||||||
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
||||||
|
|
||||||
return productService.getProductById(id, locale);
|
const product = await productService.getProductById(id, locale);
|
||||||
|
|
||||||
|
product.description = rewriteAssetUrls(
|
||||||
|
product.description,
|
||||||
|
useRuntimeConfig().public.directus.url,
|
||||||
|
'/api/assets'
|
||||||
|
);
|
||||||
|
|
||||||
|
return product;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,5 +2,15 @@ import { questionService } from '~~/server/services/cms/questionService';
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
||||||
|
|
||||||
return questionService.getQuestionList(locale);
|
const questions = await questionService.getQuestionList(locale);
|
||||||
|
|
||||||
|
questions.forEach((question) => {
|
||||||
|
question.content = rewriteAssetUrls(
|
||||||
|
question.content,
|
||||||
|
useRuntimeConfig().public.directus.url,
|
||||||
|
'/api/assets'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return questions;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,5 +10,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
const locale = getHeader(event, 'x-locale') || 'zh-CN';
|
||||||
|
|
||||||
return solutionService.getSolutionById(id, locale);
|
const solution = await solutionService.getSolutionById(id, locale);
|
||||||
|
|
||||||
|
solution.content = rewriteAssetUrls(
|
||||||
|
solution.content,
|
||||||
|
useRuntimeConfig().public.directus.url,
|
||||||
|
'/api/assets'
|
||||||
|
);
|
||||||
|
|
||||||
|
return solution;
|
||||||
});
|
});
|
||||||
|
|||||||
14
server/utils/rewriteAssetUrls.test.ts
Normal file
14
server/utils/rewriteAssetUrls.test.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { test, expect, describe } from 'vitest';
|
||||||
|
import { rewriteAssetUrls } from './rewriteAssetUrls';
|
||||||
|
|
||||||
|
describe('rewriteAssetUrls', () => {
|
||||||
|
const cmsBase = 'https://cms.example.com';
|
||||||
|
const proxyBase = '/api/assets';
|
||||||
|
const baseHTML =
|
||||||
|
'<img src="https://cms.example.com/assets/rand-om__-uuid-1234" /><a href="https://cms.example.com/assets/rand-om__-uuid-5678">Link</a><video src="https://otherdomain.com/video.mp4"></video>';
|
||||||
|
test('rewrites asset URLs correctly', () => {
|
||||||
|
expect(rewriteAssetUrls(baseHTML, cmsBase, proxyBase)).toBe(
|
||||||
|
'<img src="/api/assets/rand-om__-uuid-1234"><a href="/api/assets/rand-om__-uuid-5678">Link</a><video src="https://otherdomain.com/video.mp4"></video>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
36
server/utils/rewriteAssetUrls.ts
Normal file
36
server/utils/rewriteAssetUrls.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { parseDocument, DomUtils } from 'htmlparser2';
|
||||||
|
import serialize from 'dom-serializer';
|
||||||
|
|
||||||
|
export function rewriteAssetUrls(
|
||||||
|
html: string,
|
||||||
|
cmsBase: string,
|
||||||
|
proxyBase: string
|
||||||
|
) {
|
||||||
|
if (!html) return html;
|
||||||
|
|
||||||
|
const dom = parseDocument(html);
|
||||||
|
|
||||||
|
const elements = DomUtils.findAll(
|
||||||
|
(elem) => elem.type === 'tag',
|
||||||
|
dom.children
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const el of elements) {
|
||||||
|
// img / video / source -> src
|
||||||
|
// a -> href
|
||||||
|
const tag = el.name.toLowerCase();
|
||||||
|
const attr = tag === 'a' ? 'href' : 'src';
|
||||||
|
|
||||||
|
if (!el.attribs || !el.attribs[attr]) continue;
|
||||||
|
|
||||||
|
const url = el.attribs[attr];
|
||||||
|
|
||||||
|
// 替换cmsBase为proxyBase
|
||||||
|
if (url.startsWith(cmsBase)) {
|
||||||
|
const uuid = url.replace(`${cmsBase}/assets/`, '');
|
||||||
|
el.attribs[attr] = `${proxyBase}/${uuid}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialize(dom);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user