Files
console-dev/app/components/ProductDescription.vue

599 lines
18 KiB
Vue
Raw Permalink 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.
<script>
// 安全展示商品介绍:简单的“标签:内容”转为信息块,复杂排版保留清洗后的富文本。
import "~/assets/styles/ProductDescription.Ctb7jDuO.css";
import { toDisplayString } from "vue";
import { renderList } from "vue";
import { useId } from "vue";
import { normalizeClass } from "vue";
import { watch } from "vue";
import { ref } from "vue";
import { createCommentVNode } from "vue";
import { computed } from "vue";
import { createElementBlock } from "vue";
import { Fragment } from "vue";
import { createElementVNode } from "vue";
import { createTextVNode } from "vue";
import { unref } from "vue";
import { defineComponent } from "vue";
import { openBlock } from "vue";
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
import { n as product_name_n } from "~/lib/product-name.js";
import { n as sanitize_n } from "~/lib/sanitize.js";
var v = {
配置: `pi-sliders-h`,
规格: `pi-sliders-h`,
套餐: `pi-sliders-h`,
线路: `pi-sitemap`,
网络线路: `pi-sitemap`,
网络: `pi-sitemap`,
回程: `pi-sitemap`,
去程: `pi-sitemap`,
电信: `pi-sitemap`,
联通: `pi-sitemap`,
移动: `pi-sitemap`,
网络类型: `pi-sitemap`,
线路类型: `pi-sitemap`,
稳定性: `pi-shield`,
可用性: `pi-shield`,
SLA: `pi-shield`,
防御能力: `pi-shield`,
硬防: `pi-shield`,
处理器: `pi-microchip`,
CPU: `pi-microchip`,
内存: `pi-server`,
存储: `pi-server`,
硬盘: `pi-server`,
系统盘: `pi-server`,
数据盘: `pi-server`,
带宽: `pi-sitemap`,
流量: `pi-sitemap`,
IP: `pi-sitemap`,
IP地址: `pi-sitemap`,
防御: `pi-shield`,
数据中心: `pi-server`,
机房: `pi-server`,
地域: `pi-server`,
系统: `pi-server`,
操作系统: `pi-server`,
镜像: `pi-server`,
区域: `pi-server`,
适用场景: `pi-info-circle`,
适用业务: `pi-info-circle`,
说明: `pi-info-circle`,
备注: `pi-info-circle`,
交付: `pi-server`,
开通: `pi-server`,
备份: `pi-server`,
使用须知: `pi-info-circle`,
计费方式: `pi-clock`,
购买周期: `pi-clock`,
付款周期: `pi-clock`,
},
y = RegExp(`^(${Object.keys(v).join(`|`)})\\s*[::]\\s*(.*)$`),
b = new Set([`p`, `div`, `br`, `span`, `b`, `strong`]),
x = new Set([
`p`,
`div`,
`h1`,
`h2`,
`h3`,
`h4`,
`h5`,
`h6`,
`hr`,
`blockquote`,
`pre`,
`ul`,
`ol`,
`li`,
`dl`,
`dt`,
`dd`,
`table`,
`thead`,
`tbody`,
`tfoot`,
`tr`,
`caption`,
`figure`,
`figcaption`,
]);
function S(e) {
return /<\/?[a-z][^>]*>|<!doctype\b[^>]*>|<!--/i.test(e);
}
// 处理历史数据中的实体编码,最多解码两轮,遇到实际 HTML 后停止继续解码。
function C(e) {
let t = e;
for (
let e = 0;
e < 2 && !S(t) && /&(?:[a-z]+|#\d+|#x[\da-f]+);/i.test(t);
e++
) {
let e = t.replace(
/&(?:[a-z]+|#\d+|#x[\da-f]+);/gi,
(e) =>
new DOMParser().parseFromString(e, `text/html`).body.textContent ?? e,
);
if (e === t) break;
t = e;
}
return t;
}
// 按块级元素、换行和表格单元格边界提取文本,避免直接 textContent 丢失段落结构。
function w(e, t, n = ``) {
let r = ``;
function i() {
r &&
!r.endsWith(`
`) &&
(r += `
`);
}
function a(e) {
if ((e === t && (r += n), e.nodeType === 3)) {
r += e.textContent ?? ``;
return;
}
if (e.nodeType !== 1) return;
let o = e,
s = o.localName;
if (s === `br`) {
r += `
`;
return;
}
(x.has(s) && i(),
s === `img` &&
o.hasAttribute(`src`) &&
(r += o.getAttribute(`alt`) ?? ``));
for (let t of Array.from(e.childNodes)) a(t);
((s === `td` || s === `th`) && (r += ` `), x.has(s) && i());
}
for (let t of Array.from(e.childNodes)) a(t);
return r
.replace(
/\r\n?/g,
`
`,
)
.replace(/\u00a0/g, ` `)
.replace(/[\u200b\ufeff]/g, ``)
.split(
`
`,
)
.map((e) => e.trim())
.join(
`
`,
)
.trim();
}
// 仅允许无属性的简单标签转成字段块;带格式或不符合标签语义的内容保留富文本。
function T(e, t) {
for (let n of Array.from(e.querySelectorAll(`*`))) {
let r = n.localName;
if (!b.has(r) || n.attributes.length) return !1;
if (r !== `b` && r !== `strong`) continue;
let i = (n.textContent ?? ``).trim().replace(/\s*[::]$/, ``);
if (!Object.hasOwn(v, i)) return !1;
let a = ``;
for (; t.includes(a);) a += ``;
let o = w(e, n, a),
s = o.indexOf(a),
c =
o
.slice(0, s)
.split(
`
`,
)
.pop() ?? ``,
l =
o.slice(s + a.length).split(`
`)[0] ?? ``;
if (c.trim() || !y.test(l)) return !1;
}
return !0;
}
// 先做 HTML 清洗,再判断空内容与呈现模式;图片即使没有正文也不能被判为空。
function E(e) {
if (typeof e != `string` || !e.trim())
return {
html: ``,
text: ``,
blocks: [],
rich: !1,
empty: !0,
};
let t = C(e),
n = S(t)
? t
: t.replace(/&/g, `&amp;`).replace(/</g, `&lt;`).replace(/>/g, `&gt;`),
r = sanitize_n(n),
i = new DOMParser().parseFromString(r, `text/html`).body,
a = w(i);
if (!a && !i.querySelector(`img[src]`))
return {
html: r,
text: a,
blocks: [],
rich: !1,
empty: !0,
};
if (!T(i, a))
return {
html: r,
text: a,
blocks: [],
rich: !0,
empty: !1,
};
let o = [],
s = [];
function c() {
let e = s
.join(
`
`,
)
.trim();
(e &&
o.push({
kind: `text`,
text: e,
}),
(s = []));
}
for (let e of a.split(`
`)) {
let t = e.match(y);
if (!t) {
s.push(e);
continue;
}
c();
let n = t[1];
o.push({
kind: `field`,
label: n,
value: t[2] ?? ``,
icon: v[n],
});
}
return (
c(),
{
html: r,
text: a,
blocks: o,
rich: !1,
empty: !1,
}
);
}
var D = [`aria-label`],
O = {
key: 0,
class: `description-header`,
},
k = [`id`],
A = {
key: 1,
class: `description-paragraph`,
},
j = {
key: 1,
class: `description-paragraph description-preview`,
},
M = [`innerHTML`],
N = [`aria-expanded`, `aria-controls`, `aria-label`],
P = {
key: 1,
class: `product-description description-empty`,
},
F = Object.assign(
_plugin_vue_export_helper_t(
defineComponent({
__name: `ProductDescription`,
props: {
description: {},
compact: {
type: Boolean,
default: !1,
},
heading: {},
productName: {},
},
setup(p) {
let h = p,
_ = computed(() => E(h.description)),
v = ref(!1),
y = useId();
watch(
() => h.description,
() => {
v.value = !1;
},
);
// 以 Unicode 码点截断紧凑摘要,避免把代理对字符从中间切开。
function b(e, t) {
let n = Array.from(e);
return n.length > t ? `${n.slice(0, t).join(``)}…` : e;
}
let x = computed(
() =>
h.compact &&
!_.value.empty &&
(_.value.rich ||
_.value.blocks.length > 3 ||
_.value.blocks.some(
(e) =>
Array.from(e.kind === `field` ? e.value : e.text).length >
100,
)),
),
S = computed(() =>
!h.compact || v.value
? _.value.blocks
: _.value.blocks.slice(0, 3).map((e) =>
e.kind === `field`
? {
...e,
value: b(e.value, 100),
}
: {
...e,
text: b(e.text, 100),
},
),
),
C = computed(() => b(_.value.text.replace(/\s+/g, ` `), 160)),
w = computed(
() =>
`${v.value ? `收起` : `展开`}${h.productName ? ` ${product_name_n(h.productName)} 的` : ``}产品介绍`,
),
T = {
featured: [`配置`, `规格`, `套餐`],
network: [
`线路`,
`网络线路`,
`网络`,
`回程`,
`去程`,
`电信`,
`联通`,
`移动`,
`网络类型`,
`线路类型`,
`带宽`,
`流量`,
`IP`,
`IP地址`,
],
notes: [`说明`, `备注`, `适用场景`, `适用业务`, `使用须知`],
security: [`稳定性`, `可用性`, `SLA`, `防御能力`, `硬防`, `防御`],
hardware: [
`处理器`,
`CPU`,
`内存`,
`存储`,
`硬盘`,
`系统盘`,
`数据盘`,
],
resources: [
`数据中心`,
`机房`,
`地域`,
`系统`,
`操作系统`,
`镜像`,
`区域`,
`交付`,
`开通`,
`备份`,
],
billing: [`计费方式`, `购买周期`, `付款周期`],
};
function F(e) {
return (
Object.entries(T).find(([, t]) => t.includes(e))?.[0] || `default`
);
}
return (n, i) =>
unref(_).empty
? p.compact
? (openBlock(),
createElementBlock(
`p`,
P,
`进入配置页查看可选规格与购买时长。`,
))
: createCommentVNode(``, !0)
: (openBlock(),
createElementBlock(
`section`,
{
key: 0,
class: normalizeClass([
`product-description`,
{
"description-compact": p.compact,
"description-panel": p.heading,
},
]),
"aria-label":
p.heading ||
`${unref(product_name_n)(p.productName)}产品介绍`,
},
[
p.heading
? (openBlock(),
createElementBlock(`header`, O, [
createElementVNode(
`h2`,
null,
toDisplayString(p.heading),
1,
),
]))
: createCommentVNode(``, !0),
createElementVNode(
`div`,
{
id: unref(y),
class: `description-content`,
},
[
unref(_).rich
? p.compact && !unref(v)
? (openBlock(),
createElementBlock(
`p`,
j,
toDisplayString(unref(C) || `查看图文介绍`),
1,
))
: (openBlock(),
createElementBlock(
`div`,
{
key: 2,
class: `description-rich`,
innerHTML: unref(_).html,
},
null,
8,
M,
))
: (openBlock(!0),
createElementBlock(
Fragment,
{
key: 0,
},
renderList(
unref(S),
(t, n) => (
openBlock(),
createElementBlock(
Fragment,
{
key: n,
},
[
t.kind === `field`
? (openBlock(),
createElementBlock(
`dl`,
{
key: 0,
class: normalizeClass([
`description-field`,
[
`description-field--${F(t.label)}`,
],
]),
},
[
createElementVNode(`dt`, null, [
createElementVNode(
`i`,
{
class: normalizeClass([
`pi`,
t.icon,
]),
"aria-hidden": `true`,
},
null,
2,
),
createElementVNode(
`span`,
null,
toDisplayString(t.label),
1,
),
]),
createElementVNode(
`dd`,
null,
toDisplayString(t.value),
1,
),
],
2,
))
: (openBlock(),
createElementBlock(
`p`,
A,
toDisplayString(t.text),
1,
)),
],
64,
)
),
),
128,
)),
],
8,
k,
),
unref(x)
? (openBlock(),
createElementBlock(
`button`,
{
key: 1,
type: `button`,
class: `description-toggle`,
"aria-expanded": unref(v),
"aria-controls": unref(y),
"aria-label": unref(w),
onClick: (i[0] ||= (e) => (v.value = !unref(v))),
},
[
createTextVNode(
toDisplayString(
unref(v) ? `收起介绍` : `展开完整介绍`,
),
1,
),
createElementVNode(
`i`,
{
class: normalizeClass([
`pi`,
unref(v) ? `pi-angle-up` : `pi-angle-down`,
]),
"aria-hidden": `true`,
},
null,
2,
),
],
8,
N,
))
: createCommentVNode(``, !0),
],
10,
D,
));
},
}),
[[`__scopeId`, `data-v-8a6cfb44`]],
),
{
__name: `ProductDescription`,
},
);
export {};
export default F;
</script>