// ==UserScript== // @name B站弹幕统计 // @namespace https://github.com/ZBpine/bili-data-statistic // @version 3.0.1 // @description 获取B站弹幕数据,并生成统计页面。 // @icon https://cdn.jsdmirror.com/gh/ZBpine/bili-data-statistic@main/docs/favicon.ico // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/list/watchlater* // @match https://www.bilibili.com/bangumi/play/* // @match https://space.bilibili.com/* // @match https://zbpine.github.io/bili-data-statistic/* // @match https://bili-data-statistic.edgeone.run/* // @require https://cdn.jsdmirror.com/gh/ZBpine/bili-data-manager@ed2aaf5f8fedf7e157a22d10e995df2f61eeb917/dist/bili-data-manager.min.js // @require https://cdn.jsdmirror.com/npm/vue@3.5.31/dist/vue.global.prod.js // @require data:application/javascript,%3Bwindow.Vue%3DVue%3BglobalThis.Vue%3DVue%3B // @require https://cdn.jsdmirror.com/npm/naive-ui@2.44.1/dist/index.prod.js // @resource staticHtml https://cdn.jsdmirror.com/gh/ZBpine/bili-data-statistic@main/docs/cn/index.html // @connect api.bilibili.com // @grant GM_getResourceText // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-end // ==/UserScript== (function (vue, naiveUi) { 'use strict'; function _interopNamespaceDefault(e) { const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); if (e) { for (const k in e) { if (k !== 'default') { const d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: () => e[k] }); } } } n.default = e; return Object.freeze(n); } const naiveUi__namespace = _interopNamespaceDefault(naiveUi); function ampCount(selector) { let cnt = 0; for (let i = 0; i < selector.length; ++i) { if (selector[i] === "&") ++cnt; } return cnt; } const separatorRegex = /\s*,(?![^(]*\))\s*/g; const extraSpaceRegex = /\s+/g; function resolveSelectorWithAmp(amp, selector) { const nextAmp = []; selector.split(separatorRegex).forEach((partialSelector) => { let round = ampCount(partialSelector); if (!round) { amp.forEach((partialAmp) => { nextAmp.push( (partialAmp && partialAmp + " ") + partialSelector ); }); return; } else if (round === 1) { amp.forEach((partialAmp) => { nextAmp.push(partialSelector.replace("&", partialAmp)); }); return; } let partialNextAmp = [ partialSelector ]; while (round--) { const nextPartialNextAmp = []; partialNextAmp.forEach((selectorItr) => { amp.forEach((partialAmp) => { nextPartialNextAmp.push(selectorItr.replace("&", partialAmp)); }); }); partialNextAmp = nextPartialNextAmp; } partialNextAmp.forEach((part) => nextAmp.push(part)); }); return nextAmp; } function resolveSelector(amp, selector) { const nextAmp = []; selector.split(separatorRegex).forEach((partialSelector) => { amp.forEach((partialAmp) => { nextAmp.push((partialAmp && partialAmp + " ") + partialSelector); }); }); return nextAmp; } function parseSelectorPath(selectorPaths) { let amp = [""]; selectorPaths.forEach((selector) => { selector = selector && selector.trim(); if ( !selector ) { return; } if (selector.includes("&")) { amp = resolveSelectorWithAmp(amp, selector); } else { amp = resolveSelector(amp, selector); } }); return amp.join(", ").replace(extraSpaceRegex, " "); } function removeElement(el) { if (!el) return; const parentElement = el.parentElement; if (parentElement) parentElement.removeChild(el); } function queryElement(id, parent) { return (parent !== null && parent !== void 0 ? parent : document.head).querySelector(`style[cssr-id="${id}"]`); } function createElement(id) { const el = document.createElement("style"); el.setAttribute("cssr-id", id); return el; } function isMediaOrSupports(selector) { if (!selector) return false; return /^\s*@(s|m)/.test(selector); } const kebabRegex = /[A-Z]/g; function kebabCase(pattern) { return pattern.replace(kebabRegex, (match) => "-" + match.toLowerCase()); } function unwrapProperty(prop, indent = " ") { if (typeof prop === "object" && prop !== null) { return " {\n" + Object.entries(prop).map((v) => { return indent + ` ${kebabCase(v[0])}: ${v[1]};`; }).join("\n") + "\n" + indent + "}"; } return `: ${prop};`; } function unwrapProperties(props2, instance, params) { if (typeof props2 === "function") { return props2({ context: instance.context, props: params }); } return props2; } function createStyle(selector, props2, instance, params) { if (!props2) return ""; const unwrappedProps = unwrapProperties(props2, instance, params); if (!unwrappedProps) return ""; if (typeof unwrappedProps === "string") { return `${selector} { ${unwrappedProps} }`; } const propertyNames = Object.keys(unwrappedProps); if (propertyNames.length === 0) { if (instance.config.keepEmptyBlock) return selector + " {\n}"; return ""; } const statements = selector ? [ selector + " {" ] : []; propertyNames.forEach((propertyName) => { const property = unwrappedProps[propertyName]; if (propertyName === "raw") { statements.push("\n" + property + "\n"); return; } propertyName = kebabCase(propertyName); if (property !== null && property !== void 0) { statements.push(` ${propertyName}${unwrapProperty(property)}`); } }); if (selector) { statements.push("}"); } return statements.join("\n"); } function loopCNodeListWithCallback(children, options, callback) { if (!children) return; children.forEach((child) => { if (Array.isArray(child)) { loopCNodeListWithCallback(child, options, callback); } else if (typeof child === "function") { const grandChildren = child(options); if (Array.isArray(grandChildren)) { loopCNodeListWithCallback(grandChildren, options, callback); } else if (grandChildren) { callback(grandChildren); } } else if (child) { callback(child); } }); } function traverseCNode(node, selectorPaths, styles, instance, params) { const $ = node.$; let blockSelector = ""; if (!$ || typeof $ === "string") { if (isMediaOrSupports($)) { blockSelector = $; } else { selectorPaths.push($); } } else if (typeof $ === "function") { const selector2 = $({ context: instance.context, props: params }); if (isMediaOrSupports(selector2)) { blockSelector = selector2; } else { selectorPaths.push(selector2); } } else { if ($.before) $.before(instance.context); if (!$.$ || typeof $.$ === "string") { if (isMediaOrSupports($.$)) { blockSelector = $.$; } else { selectorPaths.push($.$); } } else if ($.$) { const selector2 = $.$({ context: instance.context, props: params }); if (isMediaOrSupports(selector2)) { blockSelector = selector2; } else { selectorPaths.push(selector2); } } } const selector = parseSelectorPath(selectorPaths); const style2 = createStyle(selector, node.props, instance, params); if (blockSelector) { styles.push(`${blockSelector} {`); } else if (style2.length) { styles.push(style2); } if (node.children) { loopCNodeListWithCallback(node.children, { context: instance.context, props: params }, (childNode) => { if (typeof childNode === "string") { const style3 = createStyle(selector, { raw: childNode }, instance, params); styles.push(style3); } else { traverseCNode(childNode, selectorPaths, styles, instance, params); } }); } selectorPaths.pop(); if (blockSelector) { styles.push("}"); } if ($ && $.after) $.after(instance.context); } function render(node, instance, props2) { const styles = []; traverseCNode(node, [], styles, instance, props2); return styles.join("\n\n"); } function murmur2(str) { var h2 = 0; var k, i = 0, len = str.length; for (; len >= 4; ++i, len -= 4) { k = str.charCodeAt(i) & 255 | (str.charCodeAt(++i) & 255) << 8 | (str.charCodeAt(++i) & 255) << 16 | (str.charCodeAt(++i) & 255) << 24; k = (k & 65535) * 1540483477 + ((k >>> 16) * 59797 << 16); k ^= k >>> 24; h2 = (k & 65535) * 1540483477 + ((k >>> 16) * 59797 << 16) ^ (h2 & 65535) * 1540483477 + ((h2 >>> 16) * 59797 << 16); } switch (len) { case 3: h2 ^= (str.charCodeAt(i + 2) & 255) << 16; case 2: h2 ^= (str.charCodeAt(i + 1) & 255) << 8; case 1: h2 ^= str.charCodeAt(i) & 255; h2 = (h2 & 65535) * 1540483477 + ((h2 >>> 16) * 59797 << 16); } h2 ^= h2 >>> 13; h2 = (h2 & 65535) * 1540483477 + ((h2 >>> 16) * 59797 << 16); return ((h2 ^ h2 >>> 15) >>> 0).toString(36); } if (typeof window !== "undefined") { window.__cssrContext = {}; } function unmount(instance, node, id, parent) { const { els } = node; if (id === void 0) { els.forEach(removeElement); node.els = []; } else { const target = queryElement(id, parent); if (target && els.includes(target)) { removeElement(target); node.els = els.filter((el) => el !== target); } } } function addElementToList(els, target) { els.push(target); } function mount(instance, node, id, props2, head, force, anchorMetaName, parent, ssrAdapter2) { let style2; if (id === void 0) { style2 = node.render(props2); id = murmur2(style2); } if (ssrAdapter2) { ssrAdapter2.adapter(id, style2 !== null && style2 !== void 0 ? style2 : node.render(props2)); return; } if (parent === void 0) { parent = document.head; } const queriedTarget = queryElement(id, parent); if (queriedTarget !== null && !force) { return queriedTarget; } const target = queriedTarget !== null && queriedTarget !== void 0 ? queriedTarget : createElement(id); if (style2 === void 0) style2 = node.render(props2); target.textContent = style2; if (queriedTarget !== null) return queriedTarget; if (anchorMetaName) { const anchorMetaEl = parent.querySelector(`meta[name="${anchorMetaName}"]`); if (anchorMetaEl) { parent.insertBefore(target, anchorMetaEl); addElementToList(node.els, target); return target; } } if (head) { parent.insertBefore(target, parent.querySelector("style, link")); } else { parent.appendChild(target); } addElementToList(node.els, target); return target; } function wrappedRender(props2) { return render(this, this.instance, props2); } function wrappedMount(options = {}) { const { id, ssr, props: props2, head = false, force = false, anchorMetaName, parent } = options; const targetElement = mount(this.instance, this, id, props2, head, force, anchorMetaName, parent, ssr); return targetElement; } function wrappedUnmount(options = {}) { const { id, parent } = options; unmount(this.instance, this, id, parent); } const createCNode = function(instance, $, props2, children) { return { instance, $, props: props2, children, els: [], render: wrappedRender, mount: wrappedMount, unmount: wrappedUnmount }; }; const c$4 = function(instance, $, props2, children) { if (Array.isArray($)) { return createCNode(instance, { $: null }, null, $); } else if (Array.isArray(props2)) { return createCNode(instance, $, null, props2); } else if (Array.isArray(children)) { return createCNode(instance, $, props2, children); } else { return createCNode(instance, $, props2, null); } }; function CssRender(config = {}) { const cssr2 = { c: ((...args) => c$4(cssr2, ...args)), use: (plugin2, ...args) => plugin2.install(cssr2, ...args), find: queryElement, context: {}, config }; return cssr2; } function plugin$4(options) { let _bPrefix = "."; let _ePrefix = "__"; let _mPrefix = "--"; let c2; if (options) { let t = options.blockPrefix; if (t) { _bPrefix = t; } t = options.elementPrefix; if (t) { _ePrefix = t; } t = options.modifierPrefix; if (t) { _mPrefix = t; } } const _plugin = { install(instance) { c2 = instance.c; const ctx = instance.context; ctx.bem = {}; ctx.bem.b = null; ctx.bem.els = null; } }; function b(arg) { let memorizedB; let memorizedE; return { before(ctx) { memorizedB = ctx.bem.b; memorizedE = ctx.bem.els; ctx.bem.els = null; }, after(ctx) { ctx.bem.b = memorizedB; ctx.bem.els = memorizedE; }, $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); context.bem.b = arg; return `${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}`; } }; } function e(arg) { let memorizedE; return { before(ctx) { memorizedE = ctx.bem.els; }, after(ctx) { ctx.bem.els = memorizedE; }, $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); context.bem.els = arg.split(",").map((v) => v.trim()); return context.bem.els.map((el) => `${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}${_ePrefix}${el}`).join(", "); } }; } function m(arg) { return { $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); const modifiers = arg.split(",").map((v) => v.trim()); function elementToSelector(el) { return modifiers.map((modifier) => `&${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}${el !== void 0 ? `${_ePrefix}${el}` : ""}${_mPrefix}${modifier}`).join(", "); } const els = context.bem.els; if (els !== null) { return elementToSelector(els[0]); } else { return elementToSelector(); } } }; } function notM(arg) { return { $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); const els = context.bem.els; return `&:not(${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}${els !== null && els.length > 0 ? `${_ePrefix}${els[0]}` : ""}${_mPrefix}${arg})`; } }; } const cB2 = ((...args) => c2(b(args[0]), args[1], args[2])); const cE2 = ((...args) => c2(e(args[0]), args[1], args[2])); const cM2 = ((...args) => c2(m(args[0]), args[1], args[2])); const cNotM2 = ((...args) => c2(notM(args[0]), args[1], args[2])); Object.assign(_plugin, { cB: cB2, cE: cE2, cM: cM2, cNotM: cNotM2 }); return _plugin; } const cssr$2 = CssRender(); const plugin$3 = plugin$4({ blockPrefix: "." }); cssr$2.use(plugin$3); const { c: c$3, find: find$2 } = cssr$2; const { cB: cB$2, cE: cE$2, cM: cM$2, cNotM: cNotM$2 } = plugin$3; const css$1 = String.raw; const ssrContextKey$1 = "@css-render/vue3-ssr"; function createStyleString$1(id, style2) { return ``; } function ssrAdapter$1(id, style2, ssrContext) { const { styles, ids } = ssrContext; if (ids.has(id)) return; if (styles !== null) { ids.add(id); styles.push(createStyleString$1(id, style2)); } } const isBrowser$1 = typeof document !== "undefined"; function useSsrAdapter$1() { if (isBrowser$1) return void 0; const context = vue.inject(ssrContextKey$1, null); if (context === null) return void 0; return { adapter: (id, style2) => ssrAdapter$1(id, style2, context), context }; } const cssrAnchorMetaName$3 = "bds-style"; function useTheme$1(mountId, style2, mountTarget, anchorMetaName) { const ssrAdapter2 = useSsrAdapter$1(); const parent = mountTarget || document.head; const mountStyle2 = () => { style2.mount({ id: mountId, head: true, anchorMetaName: cssrAnchorMetaName$3, ssr: ssrAdapter2, parent }); }; if (ssrAdapter2) { mountStyle2(); } else { vue.onBeforeMount(mountStyle2); } } const style$c = c$3([ cB$2( "bds-shell", css$1` font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; `, [ cM$2( "static", css$1` min-height: 100vh; ` ) ] ) ]); function mountStyle$c(mountTarget) { useTheme$1("bds-app-style", style$c, mountTarget); } const ROOT_KEY = "bds-storage"; const toSegments = (path) => String(path || "").split(".").filter(Boolean); const loadRoot = () => { try { const raw = localStorage.getItem(ROOT_KEY); if (!raw) return {}; const data = JSON.parse(raw); return data && typeof data === "object" ? data : {}; } catch { return {}; } }; const saveRoot = (root) => { try { localStorage.setItem(ROOT_KEY, JSON.stringify(root)); } catch { } }; function get(path, fallback = void 0) { const segs = toSegments(path); if (!segs.length) return fallback; let current = loadRoot(); for (const seg of segs) { if (!current || typeof current !== "object" || !(seg in current)) { return fallback; } current = current[seg]; } return current === void 0 ? fallback : current; } function set(path, value) { const segs = toSegments(path); if (!segs.length) return; const root = loadRoot(); let current = root; for (let i = 0; i < segs.length - 1; i += 1) { const seg = segs[i]; const next = current[seg]; if (!next || typeof next !== "object") { current[seg] = {}; } current = current[seg]; } current[segs[segs.length - 1]] = value; saveRoot(root); } const storage = { get, set }; const colors = { aliceblue: "#F0F8FF", antiquewhite: "#FAEBD7", aqua: "#0FF", aquamarine: "#7FFFD4", azure: "#F0FFFF", beige: "#F5F5DC", bisque: "#FFE4C4", black: "#000", blanchedalmond: "#FFEBCD", blue: "#00F", blueviolet: "#8A2BE2", brown: "#A52A2A", burlywood: "#DEB887", cadetblue: "#5F9EA0", chartreuse: "#7FFF00", chocolate: "#D2691E", coral: "#FF7F50", cornflowerblue: "#6495ED", cornsilk: "#FFF8DC", crimson: "#DC143C", cyan: "#0FF", darkblue: "#00008B", darkcyan: "#008B8B", darkgoldenrod: "#B8860B", darkgray: "#A9A9A9", darkgrey: "#A9A9A9", darkgreen: "#006400", darkkhaki: "#BDB76B", darkmagenta: "#8B008B", darkolivegreen: "#556B2F", darkorange: "#FF8C00", darkorchid: "#9932CC", darkred: "#8B0000", darksalmon: "#E9967A", darkseagreen: "#8FBC8F", darkslateblue: "#483D8B", darkslategray: "#2F4F4F", darkslategrey: "#2F4F4F", darkturquoise: "#00CED1", darkviolet: "#9400D3", deeppink: "#FF1493", deepskyblue: "#00BFFF", dimgray: "#696969", dimgrey: "#696969", dodgerblue: "#1E90FF", firebrick: "#B22222", floralwhite: "#FFFAF0", forestgreen: "#228B22", fuchsia: "#F0F", gainsboro: "#DCDCDC", ghostwhite: "#F8F8FF", gold: "#FFD700", goldenrod: "#DAA520", gray: "#808080", grey: "#808080", green: "#008000", greenyellow: "#ADFF2F", honeydew: "#F0FFF0", hotpink: "#FF69B4", indianred: "#CD5C5C", indigo: "#4B0082", ivory: "#FFFFF0", khaki: "#F0E68C", lavender: "#E6E6FA", lavenderblush: "#FFF0F5", lawngreen: "#7CFC00", lemonchiffon: "#FFFACD", lightblue: "#ADD8E6", lightcoral: "#F08080", lightcyan: "#E0FFFF", lightgoldenrodyellow: "#FAFAD2", lightgray: "#D3D3D3", lightgrey: "#D3D3D3", lightgreen: "#90EE90", lightpink: "#FFB6C1", lightsalmon: "#FFA07A", lightseagreen: "#20B2AA", lightskyblue: "#87CEFA", lightslategray: "#778899", lightslategrey: "#778899", lightsteelblue: "#B0C4DE", lightyellow: "#FFFFE0", lime: "#0F0", limegreen: "#32CD32", linen: "#FAF0E6", magenta: "#F0F", maroon: "#800000", mediumaquamarine: "#66CDAA", mediumblue: "#0000CD", mediumorchid: "#BA55D3", mediumpurple: "#9370DB", mediumseagreen: "#3CB371", mediumslateblue: "#7B68EE", mediumspringgreen: "#00FA9A", mediumturquoise: "#48D1CC", mediumvioletred: "#C71585", midnightblue: "#191970", mintcream: "#F5FFFA", mistyrose: "#FFE4E1", moccasin: "#FFE4B5", navajowhite: "#FFDEAD", navy: "#000080", oldlace: "#FDF5E6", olive: "#808000", olivedrab: "#6B8E23", orange: "#FFA500", orangered: "#FF4500", orchid: "#DA70D6", palegoldenrod: "#EEE8AA", palegreen: "#98FB98", paleturquoise: "#AFEEEE", palevioletred: "#DB7093", papayawhip: "#FFEFD5", peachpuff: "#FFDAB9", peru: "#CD853F", pink: "#FFC0CB", plum: "#DDA0DD", powderblue: "#B0E0E6", purple: "#800080", rebeccapurple: "#663399", red: "#F00", rosybrown: "#BC8F8F", royalblue: "#4169E1", saddlebrown: "#8B4513", salmon: "#FA8072", sandybrown: "#F4A460", seagreen: "#2E8B57", seashell: "#FFF5EE", sienna: "#A0522D", silver: "#C0C0C0", skyblue: "#87CEEB", slateblue: "#6A5ACD", slategray: "#708090", slategrey: "#708090", snow: "#FFFAFA", springgreen: "#00FF7F", steelblue: "#4682B4", tan: "#D2B48C", teal: "#008080", thistle: "#D8BFD8", tomato: "#FF6347", turquoise: "#40E0D0", violet: "#EE82EE", wheat: "#F5DEB3", white: "#FFF", whitesmoke: "#F5F5F5", yellow: "#FF0", yellowgreen: "#9ACD32", transparent: "#0000" }; function hsv2rgb(h2, s, v) { s /= 100; v /= 100; let f = (n, k = (n + h2 / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); return [f(5) * 255, f(3) * 255, f(1) * 255]; } function hsl2rgb(h2, s, l) { s /= 100; l /= 100; let a = s * Math.min(l, 1 - l); let f = (n, k = (n + h2 / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return [f(0) * 255, f(8) * 255, f(4) * 255]; } const prefix$1 = "^\\s*"; const suffix = "\\s*$"; const percent = "\\s*((\\.\\d+)|(\\d+(\\.\\d*)?))%\\s*"; const float = "\\s*((\\.\\d+)|(\\d+(\\.\\d*)?))\\s*"; const hex = "([0-9A-Fa-f])"; const dhex = "([0-9A-Fa-f]{2})"; const hslRegex = new RegExp(`${prefix$1}hsl\\s*\\(${float},${percent},${percent}\\)${suffix}`); const hsvRegex = new RegExp(`${prefix$1}hsv\\s*\\(${float},${percent},${percent}\\)${suffix}`); const hslaRegex = new RegExp(`${prefix$1}hsla\\s*\\(${float},${percent},${percent},${float}\\)${suffix}`); const hsvaRegex = new RegExp(`${prefix$1}hsva\\s*\\(${float},${percent},${percent},${float}\\)${suffix}`); const rgbRegex = new RegExp(`${prefix$1}rgb\\s*\\(${float},${float},${float}\\)${suffix}`); const rgbaRegex = new RegExp(`${prefix$1}rgba\\s*\\(${float},${float},${float},${float}\\)${suffix}`); const sHexRegex = new RegExp(`${prefix$1}#${hex}${hex}${hex}${suffix}`); const hexRegex = new RegExp(`${prefix$1}#${dhex}${dhex}${dhex}${suffix}`); const sHexaRegex = new RegExp(`${prefix$1}#${hex}${hex}${hex}${hex}${suffix}`); const hexaRegex = new RegExp(`${prefix$1}#${dhex}${dhex}${dhex}${dhex}${suffix}`); function parseHex(value) { return parseInt(value, 16); } function hsla(color) { try { let i; if (i = hslaRegex.exec(color)) { return [ roundDeg(i[1]), roundPercent(i[5]), roundPercent(i[9]), roundAlpha(i[13]) ]; } else if (i = hslRegex.exec(color)) { return [roundDeg(i[1]), roundPercent(i[5]), roundPercent(i[9]), 1]; } throw new Error(`[seemly/hsla]: Invalid color value ${color}.`); } catch (e) { throw e; } } function hsva(color) { try { let i; if (i = hsvaRegex.exec(color)) { return [ roundDeg(i[1]), roundPercent(i[5]), roundPercent(i[9]), roundAlpha(i[13]) ]; } else if (i = hsvRegex.exec(color)) { return [roundDeg(i[1]), roundPercent(i[5]), roundPercent(i[9]), 1]; } throw new Error(`[seemly/hsva]: Invalid color value ${color}.`); } catch (e) { throw e; } } function rgba(color) { try { let i; if (i = hexRegex.exec(color)) { return [parseHex(i[1]), parseHex(i[2]), parseHex(i[3]), 1]; } else if (i = rgbRegex.exec(color)) { return [roundChannel(i[1]), roundChannel(i[5]), roundChannel(i[9]), 1]; } else if (i = rgbaRegex.exec(color)) { return [ roundChannel(i[1]), roundChannel(i[5]), roundChannel(i[9]), roundAlpha(i[13]) ]; } else if (i = sHexRegex.exec(color)) { return [ parseHex(i[1] + i[1]), parseHex(i[2] + i[2]), parseHex(i[3] + i[3]), 1 ]; } else if (i = hexaRegex.exec(color)) { return [ parseHex(i[1]), parseHex(i[2]), parseHex(i[3]), roundAlpha(parseHex(i[4]) / 255) ]; } else if (i = sHexaRegex.exec(color)) { return [ parseHex(i[1] + i[1]), parseHex(i[2] + i[2]), parseHex(i[3] + i[3]), roundAlpha(parseHex(i[4] + i[4]) / 255) ]; } else if (color in colors) { return rgba(colors[color]); } else if (hslRegex.test(color) || hslaRegex.test(color)) { const [h2, s, l, a] = hsla(color); return [...hsl2rgb(h2, s, l), a]; } else if (hsvRegex.test(color) || hsvaRegex.test(color)) { const [h2, s, v, a] = hsva(color); return [...hsv2rgb(h2, s, v), a]; } throw new Error(`[seemly/rgba]: Invalid color value ${color}.`); } catch (e) { throw e; } } function normalizeAlpha(alphaValue) { return alphaValue > 1 ? 1 : alphaValue < 0 ? 0 : alphaValue; } function stringifyRgba(r, g, b, a) { return `rgba(${roundChannel(r)}, ${roundChannel(g)}, ${roundChannel(b)}, ${normalizeAlpha(a)})`; } function compositeChannel(v1, a1, v2, a2, a) { return roundChannel((v1 * a1 * (1 - a2) + v2 * a2) / a); } function composite(background, overlay) { if (!Array.isArray(background)) background = rgba(background); if (!Array.isArray(overlay)) overlay = rgba(overlay); const a1 = background[3]; const a2 = overlay[3]; const alpha = roundAlpha(a1 + a2 - a1 * a2); return stringifyRgba(compositeChannel(background[0], a1, overlay[0], a2, alpha), compositeChannel(background[1], a1, overlay[1], a2, alpha), compositeChannel(background[2], a1, overlay[2], a2, alpha), alpha); } function changeColor(base, options) { const [r, g, b, a = 1] = Array.isArray(base) ? base : rgba(base); if (typeof options.alpha === "number") { return stringifyRgba(r, g, b, options.alpha); } return stringifyRgba(r, g, b, a); } function roundAlpha(value) { const v = Math.round(Number(value) * 100) / 100; if (v > 1) return 1; if (v < 0) return 0; return v; } function roundDeg(value) { const v = Math.round(Number(value)); if (v >= 360) return 0; if (v < 0) return 0; return v; } function roundChannel(value) { const v = Math.round(Number(value)); if (v > 255) return 255; if (v < 0) return 0; return v; } function roundPercent(value) { const v = Math.round(Number(value)); if (v > 100) return 100; if (v < 0) return 0; return v; } function toHexString(base) { if (typeof base === "string") { let i; if (i = hexRegex.exec(base)) { return i[0]; } else if (i = hexaRegex.exec(base)) { return i[0].slice(0, 7); } else if (i = sHexRegex.exec(base) || sHexaRegex.exec(base)) { return `#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`; } throw new Error(`[seemly/toHexString]: Invalid hex value ${base}.`); } return `#${base.slice(0, 3).map((unit) => roundChannel(unit).toString(16).toUpperCase().padStart(2, "0")).join("")}`; } const textColor1 = "#18191C"; const textColor2 = "#484B54"; const textColor3 = "#787D8C"; const fontTheme = { fontWeight: "400", fontWeightStrong: "700", fontSize: "15px", fontSizeSmall: "14px", fontSizeMedium: "15px", fontSizeLarge: "18px", fontSizeHuge: "20px" }; const Statistic = { labelFontSize: "13px", valueFontSize: "20px" }; const DEFAULT_THEME = { mode: "auto", lightPrimary: "#00a1d6", darkPrimary: "#ff679a" }; const toOpaqueHex = (color, fallback = DEFAULT_THEME.lightPrimary) => { try { const [r, g, b] = rgba(String(color || "").trim()); return toHexString([r, g, b]); } catch { if (fallback === color) return DEFAULT_THEME.lightPrimary; return toOpaqueHex(fallback, DEFAULT_THEME.lightPrimary); } }; const mix = (baseHex, targetHex, ratio) => { const base = toOpaqueHex(baseHex); const target = toOpaqueHex(targetHex); const alpha = Math.max(0, Math.min(1, Number(ratio) || 0)); const over = changeColor(target, { alpha }); const mixed = composite(base, over); const [r, g, b] = rgba(mixed); return toHexString([r, g, b]); }; const reverseHex = (hex2) => { const [r, g, b] = rgba(toOpaqueHex(hex2)); return toHexString([255 - r, 255 - g, 255 - b]); }; const createPrimaryPalette = (baseHex) => { const base = toOpaqueHex(baseHex); const palette = { primaryColor: base, primaryColorHover: mix(base, "#ffffff", 0.14), primaryColorPressed: mix(base, "#000000", 0.14), primaryColorSuppl: mix(base, "#ffffff", 0.06) }; return palette; }; const normalizeThemeMode = (mode) => { const value = String(mode || "").trim(); if (value === "light" || value === "dark" || value === "auto") return value; return DEFAULT_THEME.mode; }; const normalizeThemeColor = toOpaqueHex; const createLightThemeOverrides = (primaryHex = DEFAULT_THEME.lightPrimary) => ({ common: { ...createPrimaryPalette(primaryHex), textColor1, textColor2, textColor3, ...fontTheme }, Statistic }); const createDarkThemeOverrides = (primaryHex = DEFAULT_THEME.darkPrimary) => ({ common: { ...createPrimaryPalette(primaryHex), textColor1: reverseHex(textColor1), textColor2: reverseHex(textColor2), textColor3: reverseHex(textColor3), ...fontTheme }, Statistic }); const style$b = c$3([ cB$2( "bds-entry-launcher", css$1` position: fixed; left: -100px; bottom: 200px; width: 120px; height: 40px; z-index: 700; border-top-right-radius: 20px; border-bottom-right-radius: 20px; border-top-left-radius: 0; border-bottom-left-radius: 0; padding: 0 8px 0 20px; display: flex; align-items: center; justify-content: space-between; font-size: var(--n-font-size-medium); box-shadow: 0 0 5px var(--dm-launcher-shadow-color); transition: left 0.3s ease-in-out, background-color 0.2s ease-in-out; overflow: hidden; &:hover { left: -10px; } .n-button__content { width: 100%; justify-content: space-between; } `, [ cE$2( "text", css$1` white-space: nowrap; user-select: none; ` ), cE$2( "icon", css$1` display: flex; align-items: center; justify-content: center; margin-right: 2px; svg { fill: var(--dm-launcher-icon-color); } ` ) ] ) ]); function mountStyle$b(mountTarget) { useTheme$1("bds-entry-launcher-style", style$b, mountTarget); } const _hoisted_1$o = { class: "bds-entry-launcher__text" }; const _sfc_main$d = { __name: "EntryLauncher", props: { label: { type: String, default: "弹幕统计" } }, emits: ["toggle"], setup(__props2, { emit: __emit2 }) { const props2 = __props2; const emit2 = __emit2; const styleMountTarget2 = vue.inject("styleMountTarget", null); mountStyle$b(styleMountTarget2); const themeVars = naiveUi.useThemeVars(); const cssVars = vue.computed(() => { const primary = themeVars.value.primaryColor; const cardColor = themeVars.value.cardColor; const bgHover = changeColor(cardColor, { alpha: 0.9 }); const bgPressed = changeColor(cardColor, { alpha: 1 }); const borderColor = changeColor(primary, { alpha: 0.9 }); return { "--dm-launcher-icon-color": primary, "--dm-launcher-shadow-color": changeColor(primary, { alpha: 0.3 }), "--n-text-color": primary, "--n-text-color-hover": primary, "--n-text-color-pressed": primary, "--n-text-color-focus": primary, "--n-border": "1px solid transparent", "--n-border-hover": `1px solid ${borderColor}`, "--n-border-pressed": `1px solid ${borderColor}`, "--n-border-focus": `1px solid ${borderColor}`, "--n-color": changeColor(cardColor, { alpha: 0.3 }), "--n-color-hover": bgHover, "--n-color-pressed": bgPressed, "--n-color-focus": bgHover }; }); return (_ctx, _cache) => { const _component_n_button = naiveUi.NButton; return vue.openBlock(), vue.createBlock(_component_n_button, { class: "bds-entry-launcher", style: vue.normalizeStyle(vue.unref(cssVars)), onClick: _cache[0] || (_cache[0] = ($event) => emit2("toggle")) }, { default: vue.withCtx(() => [ vue.createElementVNode("span", _hoisted_1$o, vue.toDisplayString(props2.label), 1), _cache[1] || (_cache[1] = vue.createElementVNode("span", { class: "bds-entry-launcher__icon", "aria-hidden": "true" }, [ vue.createElementVNode("svg", { viewBox: "0 0 1024 1024", width: "20", height: "20" }, [ vue.createElementVNode("path", { d: "M691.2 928.2V543.1c0-32.7 26.5-59.3 59.2-59.3h118.5c32.7 0 59.3 26.5 59.3 59.2V928.2h-237z m192.6-385.1c0-8.2-6.6-14.8-14.8-14.8H750.5c-8.2 0-14.8 6.6-14.9 14.7v340.8h148.2V543.1zM395 157.8c-0.1-32.6 26.3-59.2 58.9-59.3h118.8c32.6 0 59.1 26.5 59.1 59.1v770.6H395V157.8z m44.4 725.9h148V157.9c0-8.1-6.5-14.7-14.7-14.8H454.1c-8.1 0.1-14.7 6.7-14.7 14.8v725.8zM98.6 394.9c0-32.7 26.5-59.2 59.2-59.3h118.5c32.7-0.1 59.3 26.4 59.3 59.1v533.5h-237V394.9z m44.5 488.8h148.2V394.9c0-8.2-6.7-14.8-14.8-14.8H158c-8.2 0-14.8 6.6-14.9 14.7v488.9z" }) ]) ], -1)) ]), _: 1 }, 8, ["style"]); }; } }; const style$a = cB$2( "bds-panel-shell", [ cE$2( "overlay", css$1` position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); ` ), cE$2( "panel", css$1` position: fixed; border-radius: 12px; overflow: hidden; box-shadow: 0 18px 60px rgba(0, 0, 0, 0.28); background: var(--bds-panel-shell-bg); ` ), cE$2( "panel-body", css$1` padding: 20px; box-sizing: border-box; height: 100%; overflow: hidden; ` ) ] ); function mountStyle$a(mountTarget) { useTheme$1("bds-panel-shell-style", style$a, mountTarget); } const _hoisted_1$n = { class: "bds-panel-shell" }; const _sfc_main$c = { __name: "PanelShell", props: { show: { type: Boolean, default: false }, size: { type: Number, default: 80 }, error: { type: String, default: "" }, zIndex: { type: Number, default: 800 } }, emits: ["update:show"], setup(__props2, { expose: __expose2, emit: __emit2 }) { const props2 = __props2; const emit2 = __emit2; const styleMountTarget2 = vue.inject("styleMountTarget", null); mountStyle$a(styleMountTarget2); const themeVars = naiveUi.useThemeVars(); const panelEl = vue.ref(null); const panelStyle = vue.computed(() => { const size = Math.max(20, Math.min(100, Number(props2.size) || 80)); const offset = (100 - size) / 2; return { left: `${offset}vw`, top: `${offset}vh`, width: `${size}vw`, height: `${size}vh`, zIndex: props2.zIndex + 100, "--bds-panel-shell-bg": themeVars.value.baseColor || themeVars.value.cardColor || "#fff" }; }); const overlayStyle = vue.computed(() => ({ zIndex: props2.zIndex })); const closePanel = () => { emit2("update:show", false); }; __expose2({ panelEl }); return (_ctx, _cache) => { const _component_n_alert = naiveUi.NAlert; return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$n, [ vue.withDirectives(vue.createElementVNode("div", { class: "bds-panel-shell__overlay", style: vue.normalizeStyle(vue.unref(overlayStyle)), onClick: closePanel }, null, 4), [ [vue.vShow, __props2.show] ]), vue.withDirectives(vue.createElementVNode("div", { class: "bds-panel-shell__panel", style: vue.normalizeStyle(vue.unref(panelStyle)) }, [ vue.createElementVNode("div", { class: "bds-panel-shell__panel-body", ref_key: "panelEl", ref: panelEl }, [ __props2.error ? (vue.openBlock(), vue.createBlock(_component_n_alert, { key: 0, type: "error", title: __props2.error, style: { "margin-bottom": "8px" } }, null, 8, ["title"])) : vue.createCommentVNode("", true), vue.renderSlot(_ctx.$slots, "default", { panelEl: vue.unref(panelEl) }) ], 512) ], 4), [ [vue.vShow, __props2.show] ]) ]); }; } }; const style$9 = cB$2( "bds-upload-screen", css$1` min-height: 100vh; background-color: var(--bds-upload-screen-bg-base); background-image: radial-gradient( 120% 90% at 0% 0%, var(--bds-upload-screen-bg-radial) 0%, var(--bds-upload-screen-bg-base-transparent) 30% ), radial-gradient( 90% 60% at 100% 108%, var(--bds-upload-screen-bg-radial) 0%, var(--bds-upload-screen-bg-base-transparent) 60% ); background-repeat: no-repeat; display: flex; flex-direction: column; justify-content: center; align-items: center; `, [ cE$2( "uploader", css$1` width: min(720px, 90vw); ` ), cE$2( "bar", css$1` width: min(720px, 90vw); margin-bottom: 12px; ` ), cE$2( "card", css$1` width: 100%; min-height: 240px; border: 2px dashed var(--bds-upload-border); border-radius: 14px; background: var(--bds-upload-bg); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; cursor: pointer; transition: all 0.18s ease; &:hover { border-color: var(--bds-upload-drag-border); box-shadow: var(--bds-upload-drag-shadow); transform: scale(1.01); } ` ) ] ); function mountStyle$9(mountTarget) { useTheme$1("bds-upload-screen-style", style$9, mountTarget); } const _sfc_main$b = { __name: "UploadScreen", props: { hasData: { type: Boolean, default: false }, mode: { type: String, default: "readonly" }, sourceUrl: { type: String, default: "" } }, emits: ["open-panel", "parsed-data", "update:sourceUrl"], setup(__props2, { emit: __emit2 }) { const props2 = __props2; const emit2 = __emit2; const styleMountTarget2 = vue.inject("styleMountTarget", null); mountStyle$9(styleMountTarget2); const themeVars = naiveUi.useThemeVars(); const uploadError = vue.ref(""); const fileList = vue.ref([]); const sourceUrlModel = vue.computed({ get: () => String(props2.sourceUrl || ""), set: (value) => { emit2("update:sourceUrl", String(value || "")); } }); const cssVars = vue.computed(() => { const primary = themeVars.value.primaryColor || "#00a1d6"; const base = themeVars.value.bodyColor || themeVars.value.baseColor || themeVars.value.cardColor || "#ffffff"; return { "--bds-upload-screen-bg-base": base, "--bds-upload-screen-bg-base-transparent": changeColor(base, { alpha: 0 }), "--bds-upload-screen-bg-radial": changeColor(primary, { alpha: 0.18 }), "--bds-upload-screen-bg-accent": changeColor(primary, { alpha: 0.1 }), "--bds-upload-bg": changeColor(base, { alpha: 0.88 }), "--bds-upload-border": changeColor(primary, { alpha: 0.45 }), "--bds-upload-drag-border": primary, "--bds-upload-drag-shadow": `0 8px 24px ${changeColor(primary, { alpha: 0.24 })}` }; }); const parseUploadText = (text) => { const parsed = JSON.parse(String(text || "")); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("上传数据不是有效对象"); } return parsed; }; const parseAndEmitFile = async (file) => { if (!file) throw new Error("未读取到上传文件"); const text = await file.text(); const parsed = parseUploadText(text); emit2("parsed-data", parsed); }; const handleUploadChange = async ({ file }) => { try { uploadError.value = ""; await parseAndEmitFile(file?.file || file); } catch (error) { uploadError.value = String(error?.message || error || "上传数据解析失败"); } finally { fileList.value = []; } }; const handleUpdateFileList = (list) => { fileList.value = Array.isArray(list) ? list : []; }; const handleDropCapture = async (event) => { try { uploadError.value = ""; const file = event?.dataTransfer?.files?.[0]; await parseAndEmitFile(file); } catch (error) { uploadError.value = String(error?.message || error || "上传数据解析失败"); } finally { fileList.value = []; } }; const openPanel = (nextUrl) => { if (nextUrl != null) { emit2("update:sourceUrl", String(nextUrl || "")); } emit2("open-panel"); }; return (_ctx, _cache) => { const _component_n_input = naiveUi.NInput; const _component_n_button = naiveUi.NButton; const _component_n_flex = naiveUi.NFlex; const _component_n_text = naiveUi.NText; const _component_n_upload_dragger = naiveUi.NUploadDragger; const _component_n_upload = naiveUi.NUpload; const _component_n_alert = naiveUi.NAlert; return vue.openBlock(), vue.createElementBlock("div", { class: "bds-upload-screen", style: vue.normalizeStyle(vue.unref(cssVars)) }, [ vue.createVNode(_component_n_flex, { size: 8, wrap: false, align: "center", justify: "center", class: "bds-upload-screen__bar" }, { default: vue.withCtx(() => [ props2.mode !== "readonly" ? (vue.openBlock(), vue.createBlock(_component_n_input, { key: 0, value: vue.unref(sourceUrlModel), "onUpdate:value": _cache[0] || (_cache[0] = ($event) => vue.isRef(sourceUrlModel) ? sourceUrlModel.value = $event : null), placeholder: "输入 B 站视频 / 番剧 / 用户页面 URL", style: { "flex": "1" }, onKeyup: _cache[1] || (_cache[1] = vue.withKeys(($event) => openPanel($event?.target?.value), ["enter"])) }, null, 8, ["value"])) : vue.createCommentVNode("", true), props2.hasData || props2.sourceUrl ? (vue.openBlock(), vue.createBlock(_component_n_button, { key: 1, type: "primary", onClick: _cache[2] || (_cache[2] = ($event) => openPanel()) }, { default: vue.withCtx(() => [..._cache[6] || (_cache[6] = [ vue.createTextVNode(" 打开面板 ", -1) ])]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }), vue.createVNode(_component_n_upload, { class: "bds-upload-screen__uploader", "show-file-list": false, "default-upload": false, max: 1, accept: "application/json,.json", "file-list": vue.unref(fileList), "onUpdate:fileList": handleUpdateFileList, onChange: handleUploadChange }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_upload_dragger, { class: "bds-upload-screen__card", onDropCapture: vue.withModifiers(handleDropCapture, ["stop", "prevent"]), onDragoverCapture: _cache[3] || (_cache[3] = vue.withModifiers(() => { }, ["stop", "prevent"])), onDragenterCapture: _cache[4] || (_cache[4] = vue.withModifiers(() => { }, ["stop", "prevent"])), onDragleaveCapture: _cache[5] || (_cache[5] = vue.withModifiers(() => { }, ["stop", "prevent"])) }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, { style: { "font-size": "16px" } }, { default: vue.withCtx(() => [..._cache[7] || (_cache[7] = [ vue.createTextVNode("拖拽上传弹幕数据 JSON", -1) ])]), _: 1 }), vue.createVNode(_component_n_text, { depth: "3" }, { default: vue.withCtx(() => [..._cache[8] || (_cache[8] = [ vue.createTextVNode("或点击选择文件", -1) ])]), _: 1 }) ]), _: 1 }) ]), _: 1 }, 8, ["file-list"]), vue.unref(uploadError) ? (vue.openBlock(), vue.createBlock(_component_n_alert, { key: 0, type: "error", title: vue.unref(uploadError), style: { "margin-top": "12px" } }, null, 8, ["title"])) : vue.createCommentVNode("", true) ], 4); }; } }; const _hoisted_1$m = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const Camera = vue.defineComponent({ name: "Camera", render: function render2(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$m, _cache[0] || (_cache[0] = [ vue.createElementVNode( "g", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ vue.createElementVNode("path", { d: "M5 7h1a2 2 0 0 0 2-2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1a2 2 0 0 0 2 2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2" }), vue.createElementVNode("circle", { cx: "12", cy: "13", r: "3" }) ], -1 ) ]) ); } }); const _hoisted_1$l = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const ExternalLink = vue.defineComponent({ name: "ExternalLink", render: function render3(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$l, _cache[0] || (_cache[0] = [ vue.createElementVNode( "g", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ vue.createElementVNode("path", { d: "M11 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-5" }), vue.createElementVNode("path", { d: "M10 14L20 4" }), vue.createElementVNode("path", { d: "M15 4h5v5" }) ], -1 ) ]) ); } }); const _hoisted_1$k = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const FileDownload = vue.defineComponent({ name: "FileDownload", render: function render4(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$k, _cache[0] || (_cache[0] = [vue.createStaticVNode('', 1)])); } }); const _hoisted_1$j = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const InfoCircle = vue.defineComponent({ name: "InfoCircle", render: function render5(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$j, _cache[0] || (_cache[0] = [ vue.createElementVNode( "g", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ vue.createElementVNode("circle", { cx: "12", cy: "12", r: "9" }), vue.createElementVNode("path", { d: "M12 8h.01" }), vue.createElementVNode("path", { d: "M11 12h1v4h1" }) ], -1 ) ]) ); } }); const _hoisted_1$i = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const Settings = vue.defineComponent({ name: "Settings", render: function render6(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$i, _cache[0] || (_cache[0] = [ vue.createElementVNode( "g", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ vue.createElementVNode("path", { d: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 0 0-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065z" }), vue.createElementVNode("circle", { cx: "12", cy: "12", r: "3" }) ], -1 ) ]) ); } }); const _hoisted_1$h = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const SquareCheck = vue.defineComponent({ name: "SquareCheck", render: function render7(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$h, _cache[0] || (_cache[0] = [ vue.createElementVNode( "g", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ vue.createElementVNode("rect", { x: "4", y: "4", width: "16", height: "16", rx: "2" }), vue.createElementVNode("path", { d: "M9 12l2 2l4-4" }) ], -1 ) ]) ); } }); const _hoisted_1$g = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const Trash = vue.defineComponent({ name: "Trash", render: function render8(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$g, _cache[0] || (_cache[0] = [vue.createStaticVNode('', 1)])); } }); const _hoisted_1$f = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const TrashX = vue.defineComponent({ name: "TrashX", render: function render9(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$f, _cache[0] || (_cache[0] = [vue.createStaticVNode('', 1)])); } }); const cssrAnchorMetaName$2 = "naive-ui-style"; function createInjectionKey(key) { return key; } const ssrContextKey = "@css-render/vue3-ssr"; function createStyleString(id, style2) { return ``; } function ssrAdapter(id, style2, ssrContext) { const { styles, ids } = ssrContext; if (ids.has(id)) return; if (styles !== null) { ids.add(id); styles.push(createStyleString(id, style2)); } } const isBrowser = typeof document !== "undefined"; function useSsrAdapter() { if (isBrowser) return void 0; const context = vue.inject(ssrContextKey, null); if (context === null) return void 0; return { adapter: (id, style2) => ssrAdapter(id, style2, context), context }; } const { c: c$2 } = CssRender(); const cssrAnchorMetaName$1 = "vueuc-style"; function plugin$2(options) { let _bPrefix = "."; let _ePrefix = "__"; let _mPrefix = "--"; let c2; if (options) { let t = options.blockPrefix; if (t) { _bPrefix = t; } t = options.elementPrefix; if (t) { _ePrefix = t; } t = options.modifierPrefix; if (t) { _mPrefix = t; } } const _plugin = { install(instance) { c2 = instance.c; const ctx = instance.context; ctx.bem = {}; ctx.bem.b = null; ctx.bem.els = null; } }; function b(arg) { let memorizedB; let memorizedE; return { before(ctx) { memorizedB = ctx.bem.b; memorizedE = ctx.bem.els; ctx.bem.els = null; }, after(ctx) { ctx.bem.b = memorizedB; ctx.bem.els = memorizedE; }, $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); context.bem.b = arg; return `${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}`; } }; } function e(arg) { let memorizedE; return { before(ctx) { memorizedE = ctx.bem.els; }, after(ctx) { ctx.bem.els = memorizedE; }, $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); context.bem.els = arg.split(",").map((v) => v.trim()); return context.bem.els.map((el) => `${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}${_ePrefix}${el}`).join(", "); } }; } function m(arg) { return { $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); const modifiers = arg.split(",").map((v) => v.trim()); function elementToSelector(el) { return modifiers.map((modifier) => `&${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}${el !== void 0 ? `${_ePrefix}${el}` : ""}${_mPrefix}${modifier}`).join(", "); } const els = context.bem.els; if (els !== null) { return elementToSelector(els[0]); } else { return elementToSelector(); } } }; } function notM(arg) { return { $({ context, props: props2 }) { arg = typeof arg === "string" ? arg : arg({ context, props: props2 }); const els = context.bem.els; return `&:not(${(props2 === null || props2 === void 0 ? void 0 : props2.bPrefix) || _bPrefix}${context.bem.b}${els !== null && els.length > 0 ? `${_ePrefix}${els[0]}` : ""}${_mPrefix}${arg})`; } }; } const cB2 = ((...args) => c2(b(args[0]), args[1], args[2])); const cE2 = ((...args) => c2(e(args[0]), args[1], args[2])); const cM2 = ((...args) => c2(m(args[0]), args[1], args[2])); const cNotM2 = ((...args) => c2(notM(args[0]), args[1], args[2])); Object.assign(_plugin, { cB: cB2, cE: cE2, cM: cM2, cNotM: cNotM2 }); return _plugin; } const namespace = "n"; const prefix = `.${namespace}-`; const elementPrefix = "__"; const modifierPrefix = "--"; const cssr$1 = CssRender(); const plugin$1 = plugin$2({ blockPrefix: prefix, elementPrefix, modifierPrefix }); cssr$1.use(plugin$1); const { c: c$1, find: find$1 } = cssr$1; const { cB: cB$1, cE: cE$1, cM: cM$1, cNotM: cNotM$1 } = plugin$1; const configProviderInjectionKey = createInjectionKey("n-config-provider"); const defaultClsPrefix = "n"; function useMergedClsPrefix() { const NConfigProvider2 = vue.inject(configProviderInjectionKey, null); return NConfigProvider2 ? NConfigProvider2.mergedClsPrefixRef : vue.shallowRef(defaultClsPrefix); } const commonVariables = { cubicBezierEaseInOut: "cubic-bezier(.4, 0, .2, 1)", cubicBezierEaseOut: "cubic-bezier(0, 0, .2, 1)", cubicBezierEaseIn: "cubic-bezier(.4, 0, 1, 1)" }; const { cubicBezierEaseIn, cubicBezierEaseOut } = commonVariables; function fadeInScaleUpTransition({ transformOrigin = "inherit", duration = ".2s", enterScale = ".9", originalTransform = "", originalTransition = "" } = {}) { return [c$1("&.fade-in-scale-up-transition-leave-active", { transformOrigin, transition: `opacity ${duration} ${cubicBezierEaseIn}, transform ${duration} ${cubicBezierEaseIn} ${originalTransition && `,${originalTransition}`}` }), c$1("&.fade-in-scale-up-transition-enter-active", { transformOrigin, transition: `opacity ${duration} ${cubicBezierEaseOut}, transform ${duration} ${cubicBezierEaseOut} ${originalTransition && `,${originalTransition}`}` }), c$1("&.fade-in-scale-up-transition-enter-from, &.fade-in-scale-up-transition-leave-to", { opacity: 0, transform: `${originalTransform} scale(${enterScale})` }), c$1("&.fade-in-scale-up-transition-leave-from, &.fade-in-scale-up-transition-enter-to", { opacity: 1, transform: `${originalTransform} scale(1)` })]; } const { cubicBezierEaseInOut } = commonVariables; function fadeInTransition({ name = "fade-in", enterDuration = "0.2s", leaveDuration = "0.2s", enterCubicBezier = cubicBezierEaseInOut, leaveCubicBezier = cubicBezierEaseInOut } = {}) { return [c$1(`&.${name}-transition-enter-active`, { transition: `all ${enterDuration} ${enterCubicBezier}!important` }), c$1(`&.${name}-transition-leave-active`, { transition: `all ${leaveDuration} ${leaveCubicBezier}!important` }), c$1(`&.${name}-transition-enter-from, &.${name}-transition-leave-to`, { opacity: 0 }), c$1(`&.${name}-transition-leave-from, &.${name}-transition-enter-to`, { opacity: 1 })]; } const imageStyle = c$1([c$1("body >", [cB$1("image-container", "position: fixed;")]), cB$1("image-preview-container", ` position: fixed; left: 0; right: 0; top: 0; bottom: 0; display: flex; `), cB$1("image-preview-overlay", ` z-index: -1; position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0, 0, 0, .3); `, [fadeInTransition()]), cB$1("image-preview-toolbar", ` z-index: 1; position: absolute; left: 50%; transform: translateX(-50%); border-radius: var(--n-toolbar-border-radius); height: 48px; bottom: 40px; padding: 0 12px; background: var(--n-toolbar-color); box-shadow: var(--n-toolbar-box-shadow); color: var(--n-toolbar-icon-color); transition: color .3s var(--n-bezier); display: flex; align-items: center; `, [cB$1("base-icon", ` padding: 0 8px; font-size: 28px; cursor: pointer; `), fadeInTransition()]), cB$1("image-preview-wrapper", ` position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; pointer-events: none; `, [fadeInScaleUpTransition()]), cB$1("image-preview", ` user-select: none; -webkit-user-select: none; pointer-events: all; margin: auto; max-height: calc(100vh - 32px); max-width: calc(100vw - 32px); transition: transform .3s var(--n-bezier); `), cB$1("image", ` display: inline-flex; max-height: 100%; max-width: 100%; `, [cNotM$1("preview-disabled", ` cursor: pointer; `), c$1("img", ` border-radius: inherit; `)])]); const iconStyle = cB$1("base-icon", ` height: 1em; width: 1em; line-height: 1em; text-align: center; display: inline-block; position: relative; fill: currentColor; `, [c$1("svg", ` height: 1em; width: 1em; `)]); const cssr = CssRender(); const plugin = plugin$2({ blockPrefix: "." }); cssr.use(plugin); const { c, find } = cssr; const { cB, cE, cM, cNotM } = plugin; const css = String.raw; const cssrAnchorMetaName = "nb-ui-style"; function useTheme(mountId, style2, anchorMetaName, clsPrefixRef, mount2 = "n-config") { const ssrAdapter2 = useSsrAdapter(); const NConfigProvider2 = vue.inject(configProviderInjectionKey, null); anchorMetaName = anchorMetaName || cssrAnchorMetaName; let parent; if (mount2 === "n-config") { parent = NConfigProvider2?.styleMountTarget; } else if (mount2 instanceof HTMLElement) { parent = mount2; } const mountStyle2 = () => { const clsPrefix = clsPrefixRef?.value; style2.mount({ id: clsPrefix === void 0 ? mountId : clsPrefix + mountId, head: true, props: { bPrefix: clsPrefix ? `.${clsPrefix}-` : void 0 }, anchorMetaName, ssr: ssrAdapter2, parent }); }; if (ssrAdapter2) { mountStyle2(); } else { vue.onBeforeMount(mountStyle2); } } const _sfc_main$7$1 = Object.assign({ inheritAttrs: false }, { __name: "Image", props: { size: [String, Number] }, setup(__props2) { const mergedClsPrefixRef = useMergedClsPrefix(); useTheme( "-image", imageStyle, cssrAnchorMetaName$2, mergedClsPrefixRef, null ); useTheme( "-base-icon", iconStyle, cssrAnchorMetaName$2, mergedClsPrefixRef, null ); const props2 = __props2; const attrs = vue.useAttrs(); const mergedWidth = vue.computed(() => { if (props2.size != null) return props2.size; return attrs.width; }); const mergedHeight = vue.computed(() => { if (props2.size != null) return props2.size; return attrs.height; }); const mergedObjectFit = vue.computed(() => { const v = attrs.objectFit ?? attrs["object-fit"]; return v ?? "cover"; }); return (_ctx, _cache) => { const _component_n_image = naiveUi.NImage; return vue.openBlock(), vue.createBlock(_component_n_image, vue.mergeProps(vue.unref(attrs), { width: vue.unref(mergedWidth), height: vue.unref(mergedHeight), "object-fit": vue.unref(mergedObjectFit) }), vue.createSlots({ _: 2 }, [ _ctx.$slots.placeholder ? { name: "placeholder", fn: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, "placeholder") ]), key: "0" } : _ctx.$slots.default ? { name: "placeholder", fn: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, "default") ]), key: "1" } : void 0, _ctx.$slots.error ? { name: "error", fn: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, "error") ]), key: "2" } : _ctx.$slots.default ? { name: "error", fn: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, "default") ]), key: "3" } : void 0 ]), 1040, ["width", "height", "object-fit"]); }; } }); const style$6$1 = c([ cB( "nb-archive-info-card", css` color: var(--n-text-color-1); border: 1px solid var(--n-border-color); border-radius: var(--n-border-radius); `, [ cE( "title-line", css` display: block; min-width: 0; font-size: var(--n-font-size-large); font-weight: var(--n-font-weight-strong); ` ), cE( "title", css` min-width: 0; ` ), cE( "subtitle", css` margin-left: 12px; color: var(--n-text-color-3); ` ), cE( "id-link", css` margin-left: 12px; display: inline-flex; vertical-align: baseline; ` ), cE( "main", css` align-items: flex-start; @media (max-width: 768px) { flex-wrap: wrap; } ` ), cE( "left", css` flex: 0 0 auto; ` ), cE( "cover", css` border-radius: 6px; overflow: hidden; flex-shrink: 0; ` ), cE( "owner-row", css` min-width: 0; ` ), cE( "owner-face", css` border-radius: 50%; overflow: hidden; flex: 0 0 auto; ` ), cE( "owner-link", css` color: var(--n-text-color-2); &:hover { color: var(--n-primary-color-hover); } ` ), cE( "desc", css` font-size: var(--n-font-size-mini); color: var(--n-text-color-2); white-space: pre-wrap; align-self: flex-start; ` ), cE( "time-row", css` color: var(--n-text-color-3); font-size: var(--n-font-size-mini); ` ), cE( "divider", css` margin: 2px 0; ` ) ] ) ]); function mountStyle$6$1() { useTheme("nb-archive-info-card", style$6$1); } const _hoisted_1$d = { class: "nb-archive-info-card__title" }; const _hoisted_2$5 = { key: 0, class: "nb-archive-info-card__subtitle" }; const _sfc_main$6$1 = Object.assign({ name: "ArchiveInfoCard" }, { __name: "ArchiveInfoCard", props: { archiveInfo: { type: Object, default: () => ({}) } }, setup(__props2) { const props2 = __props2; const statItems = [ { key: "like", label: "点赞" }, { key: "coin", label: "投币" }, { key: "favorite", label: "收藏" }, { key: "share", label: "分享" }, { key: "view", label: "播放" }, { key: "danmaku", label: "弹幕" }, { key: "reply", label: "评论" } ]; const ownerName = vue.computed(() => props2.archiveInfo?.owner?.name || "未知UP主"); const ownerMid = vue.computed(() => props2.archiveInfo?.owner?.mid || ""); const ownerUrl = vue.computed(() => { if (!ownerMid.value) return ""; return `https://space.bilibili.com/${ownerMid.value}`; }); const idUrl = vue.computed(() => props2.archiveInfo?.url || ""); const formatLocalTime = (ts) => { if (!ts) return "-"; return new Date(ts * 1e3).toLocaleString(); }; const formatStatValue2 = (value) => { if (value === null || value === void 0 || value === "") return "-"; const num = Number(value); if (!Number.isFinite(num)) return String(value); return new Intl.NumberFormat("en-US").format(num); }; const themeVars = naiveUi.useThemeVars(); const cssVars = vue.computed(() => { return { "--n-primary-color-hover": themeVars.value.primaryColorHover, "--n-border-color": themeVars.value.borderColor, "--n-border-radius": themeVars.value.borderRadius, "--n-divider-color": themeVars.value.dividerColor, "--n-text-color-1": themeVars.value.textColor1, "--n-text-color-2": themeVars.value.textColor2, "--n-text-color-3": themeVars.value.textColor3, "--n-font-size": themeVars.value.fontSize, "--n-font-size-mini": themeVars.value.fontSizeMini, "--n-font-size-large": themeVars.value.fontSizeLarge, "--n-font-weight-strong": themeVars.value.fontWeightStrong }; }); mountStyle$6$1(); return (_ctx, _cache) => { const _component_n_button = naiveUi.NButton; const _component_n_text = naiveUi.NText; const _component_n_flex = naiveUi.NFlex; const _component_n_ellipsis = naiveUi.NEllipsis; const _component_n_divider = naiveUi.NDivider; const _component_n_statistic = naiveUi.NStatistic; const _component_n_grid_item = naiveUi.NGridItem; const _component_n_grid = naiveUi.NGrid; const _component_n_card = naiveUi.NCard; return vue.openBlock(), vue.createBlock(_component_n_card, { bordered: true, class: "nb-archive-info-card", style: vue.normalizeStyle(vue.unref(cssVars)) }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { vertical: "", size: 10 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, { depth: 1, class: "nb-archive-info-card__title-line" }, { default: vue.withCtx(() => [ vue.createElementVNode("span", _hoisted_1$d, vue.toDisplayString(props2.archiveInfo?.title || "加载中..."), 1), props2.archiveInfo?.subtitle ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_2$5, vue.toDisplayString(props2.archiveInfo.subtitle), 1)) : vue.createCommentVNode("", true), props2.archiveInfo?.id && vue.unref(idUrl) ? (vue.openBlock(), vue.createBlock(_component_n_button, { key: 1, text: "", tag: "a", href: vue.unref(idUrl), target: "_blank", type: "primary", class: "nb-archive-info-card__id-link" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(props2.archiveInfo.id), 1) ]), _: 1 }, 8, ["href"])) : vue.createCommentVNode("", true) ]), _: 1 }), vue.createVNode(_component_n_flex, { size: 16, wrap: false, class: "nb-archive-info-card__main" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { vertical: "", size: 8, class: "nb-archive-info-card__left" }, { default: vue.withCtx(() => [ props2.archiveInfo?.cover ? (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$7$1), { key: 0, src: props2.archiveInfo.cover, width: "160", height: "90", class: "nb-archive-info-card__cover", lazy: "" }, null, 8, ["src"])) : vue.createCommentVNode("", true), vue.createVNode(_component_n_flex, { align: "center", size: 10, wrap: false, class: "nb-archive-info-card__owner-row" }, { default: vue.withCtx(() => [ props2.archiveInfo?.owner?.face ? (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$7$1), { key: 0, src: props2.archiveInfo.owner.face, size: "32", class: "nb-archive-info-card__owner-face", lazy: "" }, null, 8, ["src"])) : vue.createCommentVNode("", true), vue.unref(ownerUrl) ? (vue.openBlock(), vue.createBlock(_component_n_button, { key: 1, text: "", tag: "a", href: vue.unref(ownerUrl), target: "_blank", class: "nb-archive-info-card__owner-link" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(ownerName)), 1) ]), _: 1 }, 8, ["href"])) : (vue.openBlock(), vue.createBlock(_component_n_text, { key: 2, depth: 2 }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(ownerName)), 1) ]), _: 1 })) ]), _: 1 }) ]), _: 1 }), props2.archiveInfo?.desc ? (vue.openBlock(), vue.createBlock(_component_n_ellipsis, { key: 0, "expand-trigger": "click", "line-clamp": 7, tooltip: false, class: "nb-archive-info-card__desc" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(props2.archiveInfo.desc), 1) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }), vue.createVNode(_component_n_flex, { size: 20, wrap: true, class: "nb-archive-info-card__time-row" }, { default: vue.withCtx(() => [ vue.createElementVNode("span", null, "视频发布:" + vue.toDisplayString(formatLocalTime(props2.archiveInfo?.pubtime)), 1), vue.createElementVNode("span", null, "抓取时间:" + vue.toDisplayString(formatLocalTime(props2.archiveInfo?.fetchtime)), 1) ]), _: 1 }), vue.createVNode(_component_n_divider, { class: "nb-archive-info-card__divider" }), vue.createVNode(_component_n_grid, { cols: 4, "x-gap": 20, "y-gap": 12, responsive: "screen" }, { default: vue.withCtx(() => [ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(statItems, (item) => { return vue.createVNode(_component_n_grid_item, { key: item.key }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_statistic, { label: item.label, value: formatStatValue2(props2.archiveInfo?.stat?.[item.key]) }, null, 8, ["label", "value"]) ]), _: 2 }, 1024); }), 64)) ]), _: 1 }) ]), _: 1 }) ]), _: 1 }, 8, ["style"]); }; } }); const _hoisted_1$c = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; vue.defineComponent({ name: "ChevronDown", render: function render10(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$c, _cache[0] || (_cache[0] = [ vue.createElementVNode( "path", { d: "M6 9l6 6l6-6", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, null, -1 ) ]) ); } }); const _hoisted_1$b = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; vue.defineComponent({ name: "ChevronUp", render: function render22(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$b, _cache[0] || (_cache[0] = [ vue.createElementVNode( "path", { d: "M6 15l6-6l6 6", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, null, -1 ) ]) ); } }); const _hoisted_1$a = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; vue.defineComponent({ name: "Link", render: function render32(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$a, _cache[0] || (_cache[0] = [ vue.createElementVNode( "g", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ vue.createElementVNode("path", { d: "M10 14a3.5 3.5 0 0 0 5 0l4-4a3.5 3.5 0 0 0-5-5l-.5.5" }), vue.createElementVNode("path", { d: "M14 10a3.5 3.5 0 0 0-5 0l-4 4a3.5 3.5 0 0 0 5 5l.5-.5" }) ], -1 ) ]) ); } }); const _hoisted_1$9 = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; const MessageCircle = vue.defineComponent({ name: "MessageCircle", render: function render42(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$9, _cache[0] || (_cache[0] = [vue.createStaticVNode('', 1)])); } }); const _hoisted_1$8 = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; vue.defineComponent({ name: "Photo", render: function render52(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$8, _cache[0] || (_cache[0] = [vue.createStaticVNode('', 1)])); } }); const _hoisted_1$7 = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; vue.defineComponent({ name: "ThumbUp", render: function render62(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$7, _cache[0] || (_cache[0] = [ vue.createElementVNode( "path", { d: "M7 11v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3a4 4 0 0 0 4-4V6a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1-2 2h-7a3 3 0 0 1-3-3", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, null, -1 ) ]) ); } }); const _hoisted_1$6$1 = { xmlns: "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", viewBox: "0 0 24 24" }; vue.defineComponent({ name: "User", render: function render72(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock( "svg", _hoisted_1$6$1, _cache[0] || (_cache[0] = [ vue.createElementVNode( "g", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ vue.createElementVNode("circle", { cx: "12", cy: "7", r: "4" }), vue.createElementVNode("path", { d: "M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" }) ], -1 ) ]) ); } }); function formatProgress$1(ms) { const s = Math.floor(ms / 1e3); const hours = Math.floor(s / 3600); const min = String(Math.floor(s % 3600 / 60)).padStart(2, "0"); const sec = String(s % 60).padStart(2, "0"); return hours > 0 ? `${hours}:${min}:${sec}` : `${min}:${sec}`; } function formatTimestamp(ts) { if (!ts) return "-"; const d = new Date(ts * 1e3); const [Y, M, D, h2, m, s] = [ d.getFullYear(), String(d.getMonth() + 1).padStart(2, "0"), String(d.getDate()).padStart(2, "0"), String(d.getHours()).padStart(2, "0"), String(d.getMinutes()).padStart(2, "0"), String(d.getSeconds()).padStart(2, "0") ]; return `${Y}-${M}-${D} ${h2}:${m}:${s}`; } function toPx(val) { return typeof val === "number" ? `${val}px` : val; } function formatStatValue(value) { if (value === null || value === void 0 || value === "") return "-"; const num = Number(value); if (!Number.isFinite(num)) return String(value); return new Intl.NumberFormat("en-US").format(num); } const style$5$1 = c([ cB( "nb-command-dm-timeline", css` padding-top: 10px; color: var(--n-text-color-1); `, [ cE( "time", css` display: inline-block; color: var(--n-text-color-3); font-size: var(--n-font-size-mini); font-weight: var(--n-font-weight, normal); line-height: 1.4; ` ), cE( "item-card", css` border: 1px solid var(--n-border-color); border-radius: var(--n-border-radius); ` ), cE( "line", css` display: flex; align-items: center; gap: 8px; flex-wrap: wrap; color: var(--n-text-color-2); ` ), cE( "title", css` font-weight: var(--n-font-weight-strong); color: var(--n-text-color-1); ` ), cE( "meta", css` margin-top: 6px; margin-left: 8px; color: var(--n-text-color-3); font-size: var(--n-font-size-mini); ` ), cE( "table-wrap", css` margin-top: 8px; ` ), cE( "icon-wrap", css` width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; ` ), cE( "icon-image", css` width: 20px; height: 20px; object-fit: cover; display: block; ` ), cE( "link", css` margin-top: 8px; margin-left: 8px; display: inline-flex; max-width: 100%; &, & .n-button__content { white-space: normal; word-break: break-all; text-align: left; } ` ) ] ) ]); function mountStyle$5$1() { useTheme("nb-command-dm-timeline", style$5$1); } const _hoisted_1$5$1 = { class: "nb-command-dm-timeline__icon-wrap" }; const _hoisted_2$4 = ["src"]; const _hoisted_3$3 = { class: "nb-command-dm-timeline__time" }; const _hoisted_4$3 = { class: "nb-command-dm-timeline__line" }; const _hoisted_5$2 = { class: "nb-command-dm-timeline__title" }; const _hoisted_6$2 = { key: 0, class: "nb-command-dm-timeline__table-wrap" }; const _hoisted_7$2 = { class: "nb-command-dm-timeline__line" }; const _hoisted_8$2 = { class: "nb-command-dm-timeline__title" }; const _hoisted_9 = { key: 2, class: "nb-command-dm-timeline__line" }; const _hoisted_10 = { class: "nb-command-dm-timeline__title" }; const _hoisted_11 = { class: "nb-command-dm-timeline__line" }; const _hoisted_12 = { class: "nb-command-dm-timeline__title" }; const _hoisted_13 = { key: 0, class: "nb-command-dm-timeline__meta" }; const _hoisted_14 = { key: 1, class: "nb-command-dm-timeline__table-wrap" }; const _hoisted_15 = { key: 4, class: "nb-command-dm-timeline__line" }; const _hoisted_16 = { class: "nb-command-dm-timeline__title" }; const _hoisted_17 = { key: 5, class: "nb-command-dm-timeline__line" }; const _hoisted_18 = { class: "nb-command-dm-timeline__title" }; const _hoisted_19 = { key: 6, class: "nb-command-dm-timeline__line" }; const _hoisted_20 = { class: "nb-command-dm-timeline__title" }; const _sfc_main$5$1 = Object.assign({ name: "CommandDmTimeline" }, { __name: "CommandDmTimeline", props: { commandDms: { type: Array, default: () => [] } }, setup(__props2) { const props2 = __props2; const voteColumns = [ { title: "序号", key: "idx", width: 80, sorter: "default" }, { title: "选项", key: "desc" }, { title: "票数", key: "cnt", width: 80, sorter: "default" } ]; const gradeColumns = [ { title: "评分项", key: "content" }, { title: "平均", key: "avg_score", width: 80, sorter: "default" }, { title: "参与", key: "count", width: 80, sorter: "default" } ]; const parseExtra = (extra) => { try { if (!extra) return {}; if (typeof extra === "string") return JSON.parse(extra); return extra; } catch { return {}; } }; const toTimelineType = (type) => { const map = { primary: "info", danger: "error" }; return map[type] || type || "default"; }; const toTimeText = (cmd) => { const progressText = formatProgress$1(cmd?.progress ?? 0); const ctime = cmd?.ctime; const sendTime = typeof ctime === "number" ? formatTimestamp(ctime) : String(ctime || "-"); return `时间:${progressText} | 发送时间:${sendTime}`; }; const normalizedItems = vue.computed(() => { return props2.commandDms.map((cmd, index) => { const extra = parseExtra(cmd?.extra); const icon = extra?.icon ? String(extra.icon).replace(/^http:/, "https:") : ""; return { key: String(cmd?.idStr || cmd?.id || `${cmd?.ctime || 0}-${cmd?.progress || 0}-${index}`), extra, icon, type: toTimelineType(icon ? "" : "primary"), timestamp: toTimeText(cmd || {}), command: cmd?.command || "", content: cmd?.content || "" }; }); }); const themeVars = naiveUi.useThemeVars(); const cssVars = vue.computed(() => { return { "--n-primary-color": themeVars.value.primaryColor, "--n-border-color": themeVars.value.borderColor, "--n-border-radius": themeVars.value.borderRadius, "--n-text-color-1": themeVars.value.textColor1, "--n-text-color-2": themeVars.value.textColor2, "--n-text-color-3": themeVars.value.textColor3, "--n-font-size-mini": themeVars.value.fontSizeMini, "--n-font-weight": themeVars.value.fontWeight, "--n-font-weight-strong": themeVars.value.fontWeightStrong }; }); mountStyle$5$1(); return (_ctx, _cache) => { const _component_n_icon = naiveUi.NIcon; const _component_n_text = naiveUi.NText; const _component_n_data_table = naiveUi.NDataTable; const _component_n_button = naiveUi.NButton; const _component_n_card = naiveUi.NCard; const _component_n_timeline_item = naiveUi.NTimelineItem; const _component_n_timeline = naiveUi.NTimeline; const _component_n_empty = naiveUi.NEmpty; return vue.openBlock(), vue.createElementBlock("div", { class: "nb-command-dm-timeline", style: vue.normalizeStyle(vue.unref(cssVars)) }, [ vue.createVNode(_component_n_timeline, { "icon-size": 20 }, { default: vue.withCtx(() => [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(normalizedItems), (item) => { return vue.openBlock(), vue.createBlock(_component_n_timeline_item, { key: item.key, type: item.type }, { icon: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$5$1, [ item.icon ? (vue.openBlock(), vue.createElementBlock("img", { key: 0, src: item.icon, alt: "command-icon", class: "nb-command-dm-timeline__icon-image" }, null, 8, _hoisted_2$4)) : (vue.openBlock(), vue.createBlock(_component_n_icon, { key: 1, size: 14 }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(MessageCircle)) ]), _: 1 })) ]) ]), header: vue.withCtx(() => [ vue.createElementVNode("span", _hoisted_3$3, vue.toDisplayString(item.timestamp), 1) ]), default: vue.withCtx(() => [ vue.createVNode(_component_n_card, { hoverable: "", class: "nb-command-dm-timeline__item-card" }, { default: vue.withCtx(() => [ item.command === "#VOTE#" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [ vue.createElementVNode("div", _hoisted_4$3, [ vue.createVNode(_component_n_text, { type: "success" }, { default: vue.withCtx(() => [..._cache[0] || (_cache[0] = [ vue.createTextVNode("【投票】", -1) ])]), _: 1 }), vue.createElementVNode("b", _hoisted_5$2, vue.toDisplayString(item.extra?.question ?? item.content), 1) ]), Array.isArray(item.extra?.options) && item.extra.options.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_6$2, [ vue.createVNode(_component_n_data_table, { size: "small", bordered: true, "single-line": false, columns: voteColumns, data: item.extra.options }, null, 8, ["data"]) ])) : vue.createCommentVNode("", true) ], 64)) : item.command === "#LINK#" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [ vue.createElementVNode("div", _hoisted_7$2, [ vue.createVNode(_component_n_text, { type: "info" }, { default: vue.withCtx(() => [..._cache[1] || (_cache[1] = [ vue.createTextVNode("【关联】", -1) ])]), _: 1 }), vue.createElementVNode("b", _hoisted_8$2, vue.toDisplayString(item.content), 1) ]), item.extra?.bvid ? (vue.openBlock(), vue.createBlock(_component_n_button, { key: 0, text: "", tag: "a", target: "_blank", rel: "noopener noreferrer", href: `https://www.bilibili.com/video/${item.extra.bvid}`, class: "nb-command-dm-timeline__link" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(item.extra?.title || item.extra?.bvid), 1) ]), _: 2 }, 1032, ["href"])) : vue.createCommentVNode("", true) ], 64)) : item.command === "#GRADE#" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9, [ vue.createVNode(_component_n_text, { type: "warning" }, { default: vue.withCtx(() => [..._cache[2] || (_cache[2] = [ vue.createTextVNode("【评分】", -1) ])]), _: 1 }), vue.createElementVNode("b", _hoisted_10, vue.toDisplayString(item.extra?.msg ?? item.content), 1), vue.createVNode(_component_n_text, null, { default: vue.withCtx(() => [ vue.createTextVNode("平均: " + vue.toDisplayString(item.extra?.avg_score ?? "-"), 1) ]), _: 2 }, 1024), vue.createVNode(_component_n_text, null, { default: vue.withCtx(() => [ vue.createTextVNode("参与: " + vue.toDisplayString(item.extra?.count ?? "-"), 1) ]), _: 2 }, 1024) ])) : item.command === "#GRADESUMMARY#" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [ vue.createElementVNode("div", _hoisted_11, [ vue.createVNode(_component_n_text, { type: "warning" }, { default: vue.withCtx(() => [..._cache[3] || (_cache[3] = [ vue.createTextVNode("【评分总结】", -1) ])]), _: 1 }), vue.createElementVNode("b", _hoisted_12, vue.toDisplayString(item.extra?.msg ?? item.content), 1) ]), item.extra?.avg_score !== void 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_13, " 平均:" + vue.toDisplayString(item.extra.avg_score), 1)) : vue.createCommentVNode("", true), Array.isArray(item.extra?.grades) && item.extra.grades.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_14, [ vue.createVNode(_component_n_data_table, { size: "small", bordered: true, "single-line": false, columns: gradeColumns, data: item.extra.grades }, null, 8, ["data"]) ])) : vue.createCommentVNode("", true) ], 64)) : item.command === "#ATTENTION#" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_15, [ vue.createVNode(_component_n_text, { type: "error" }, { default: vue.withCtx(() => [..._cache[4] || (_cache[4] = [ vue.createTextVNode("【关注】", -1) ])]), _: 1 }), vue.createElementVNode("b", _hoisted_16, vue.toDisplayString(item.content), 1) ])) : item.command === "#RESERVE#" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_17, [ vue.createVNode(_component_n_text, { type: "error" }, { default: vue.withCtx(() => [..._cache[5] || (_cache[5] = [ vue.createTextVNode("【预约】", -1) ])]), _: 1 }), vue.createElementVNode("b", _hoisted_18, vue.toDisplayString(item.extra?.msg ?? item.content), 1) ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_19, [ vue.createVNode(_component_n_text, { type: "info" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(item.command || "未知互动弹幕"), 1) ]), _: 2 }, 1024), vue.createElementVNode("b", _hoisted_20, vue.toDisplayString(item.content), 1) ])) ]), _: 2 }, 1024) ]), _: 2 }, 1032, ["type"]); }), 128)) ]), _: 1 }), !vue.unref(normalizedItems).length ? (vue.openBlock(), vue.createBlock(_component_n_empty, { key: 0, description: "暂无互动弹幕" })) : vue.createCommentVNode("", true) ], 4); }; } }); c([ cB( "nb-note", css` display: flex; flex-direction: column; color: var(--n-text-color-1); font-size: 17px; min-width: 1px; `, [ cB( "meta", css` padding: 0 var(--n-padding) var(--n-padding); border-bottom: 1px solid var(--n-border-color); ` ), cB( "title", css` font-size: 28px; font-weight: 800; line-height: 1.45; white-space: pre-wrap; word-break: break-word; ` ), cB( "cover", css` width: 100%; max-width: 100%; border-radius: var(--n-border-radius); overflow: hidden; ` ), cB( "author-row", css` min-width: 0; align-items: center; ` ), cB( "author-avatar", css` border-radius: 50%; overflow: hidden; flex: 0 0 auto; ` ), cB( "author-main", css` min-width: 0; flex: 1; text-align: left; align-items: flex-start; ` ), cB( "author-name", css` font-size: 16px; font-weight: 600; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; ` ), cB( "author-name-link", css` text-decoration: none; color: var(--n-text-color-1); &:hover { color: var(--n-primary-color-hover); } ` ), cB( "author-time", css` font-size: 13px; ` ), cB( "main", css` padding: var(--n-padding); display: flex; flex-direction: column; gap: calc(var(--n-gap) * 0.75); ` ), cB( "block", css` padding: calc(var(--n-padding) * 0.4) calc(var(--n-padding) * 0.5); border-radius: calc(var(--n-border-radius) * 0.75); transition: background 0.3s var(--n-cubic-bezier-ease-out); `, [ cM( "paragraph", css` line-height: 1.8; white-space: pre-wrap; word-break: break-word; ` ), cM( "header", css` font-weight: 700; line-height: 1.5; margin-top: calc(var(--n-gap) * 0.5); ` ), cM( "list-item", css` display: flex; align-items: flex-start; gap: calc(var(--n-gap) * 0.5); line-height: 1.8; ` ), cM( "image", css` display: flex; justify-content: center; width: 100%; ` ), cM( "tag", css` display: flex; align-items: center; gap: calc(var(--n-gap) * 0.5); ` ) ] ), cE( "header-1", css` font-size: 24px; ` ), cE( "header-2", css` font-size: 22px; ` ), cE( "header-3", css` font-size: 20px; ` ), cE( "header-4", css` font-size: 18px; ` ), cB( "list-prefix", css` color: var(--n-text-color-3); min-width: 1.75em; text-align: right; user-select: none; ` ), cB( "segment", css` white-space: pre-wrap; word-break: break-word; ` ), cB( "content-image", css` width: 100%; max-width: 100%; min-width: 0; ` ), cB( "image-error", css` color: #b0b9c0; background: #e5e8ef; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; ` ), cB( "tag-link", css` cursor: pointer; `, [ c( "&:hover", css` color: var(--n-primary-color-hover); ` ) ] ), cB( "empty", css` color: var(--n-text-color-3); text-align: center; padding: calc(var(--n-padding) * 2) var(--n-padding); ` ) ] ) ]); c([ cB( "nb-comment-tree", css` display: flex; flex-direction: column; color: var(--n-text-color-1); border: 1px solid var(--n-border-color); border-radius: var(--n-border-radius); `, [ cB( "toolbar", css` padding: var(--n-padding); border-bottom: 1px solid var(--n-border-color); `, [ cB( "search-input", css` flex: 1; ` ), cB( "search-stat", css` font-size: var(--n-font-size-mini); color: var(--n-text-color-3); ` ) ] ), cB( "tree-body", css` flex: 1; min-height: 0; position: relative; ` ), cE( "item", css` min-height: 80px; padding: var(--n-padding); transition: background 0.3s var(--n-cubic-bezier-ease-out); `, [ cM( "highlight", css` background-color: var(--n-primary-color-highlight) !important; ` ), cB( "rail", css` width: var(--n-indent-size); position: relative; cursor: pointer; flex: 0 0 var(--n-indent-size); `, [ c( "&::after", css` content: ""; position: absolute; right: calc(var(--n-gap) / -2); top: calc(var(--n-padding) * -2); bottom: 0px; width: 1px; background: var(--n-divider-color); ` ), cM("active", [ c( "&::after", css` background: var(--n-primary-color-hover); width: 2px; ` ) ]) ] ), cB( "image-error", css` color: #b0b9c0; background: #e5e8ef; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; ` ), cB( "avatar", css` width: var(--n-indent-size); height: var(--n-indent-size); border-radius: 50%; ` ), cB( "main", css` flex: 1; ` ), cB("header", css``), cB( "content", css` white-space: pre-wrap; word-wrap: break-word; ` ), cB("pics", css``, [ cE( "item", css` border-radius: var(--n-border-radius); border: 1px solid var(--n-border-color); ` ) ]), cB( "footer", css` font-size: var(--n-font-size-mini); `, [ cE( "item", css` display: inline-flex; align-items: center; line-height: 1; gap: 2px; `, [ c( "&.clickable", css` cursor: pointer; `, [ c( "&:hover", css` color: var(--n-primary-color-hover); ` ) ] ) ] ) ] ) ] ), cB( "note-drawer-body", css` min-height: 1px; ` ), cB( "note-drawer-empty", css` margin-top: calc(var(--n-padding) * 2); ` ) ] ) ]); const style$2$1 = c([ cB( "nb-virtual-list", css` position: relative; min-height: 0; height: 100%; `, [ cE( "list", css` height: 100%; min-height: 0; ` ), cE( "bottom-mask", css` position: absolute; left: 0; right: 0; bottom: 0; height: var(--n-mask-height); pointer-events: none; background: linear-gradient(to bottom, var(--n-card-color-transparent), var(--n-card-color)); z-index: 1; ` ) ] ) ]); function mountStyle$2$1() { useTheme("nb-virtual-list", style$2$1); } const _hoisted_1$3$1 = { class: "nb-virtual-list__bottom-mask" }; const _sfc_main$3$1 = Object.assign({ inheritAttrs: false }, { __name: "VirtualList", props: { items: { type: Array, default: () => [] }, showBottomMask: { type: Boolean, default: true }, bottomMaskHeight: { type: Number, default: 32 } }, emits: ["scroll", "resize", "wheel"], setup(__props2, { expose: __expose2, emit: __emit2 }) { const props2 = __props2; const emit2 = __emit2; const attrs = vue.useAttrs(); const listRef = vue.ref(null); const showMask = vue.ref(false); const forwardedAttrs = vue.computed(() => { const { class: _class, style: _style, ...rest } = attrs; return rest; }); const updateBottomMask = (target) => { if (!target) { showMask.value = false; return; } const hasScrollable = target.scrollHeight - target.clientHeight > 1; const atBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - 1; showMask.value = hasScrollable && !atBottom; }; const getScrollContainer = () => { return listRef.value?.scrollbarInstRef?.containerRef || listRef.value?.getScrollContainer?.(); }; const syncBottomMask = () => { updateBottomMask(getScrollContainer()); }; const handleScroll = (e) => { updateBottomMask(e?.target); emit2("scroll", e); }; const handleResize = (e) => { vue.nextTick(syncBottomMask); emit2("resize", e); }; const handleWheel = (e) => { emit2("wheel", e); }; const scrollTo = (options, y) => { listRef.value?.scrollTo?.(options, y); }; __expose2({ scrollTo, getScrollContainer, getScrollContent: () => listRef.value?.getScrollContent?.(), getScrollbarInstRef: () => listRef.value?.scrollbarInstRef }); vue.watch( () => props2.items.length, () => { vue.nextTick(syncBottomMask); }, { immediate: true } ); const themeVars = naiveUi.useThemeVars(); const cssVars = vue.computed(() => { return { "--n-card-color": themeVars.value.cardColor, "--n-card-color-transparent": changeColor(themeVars.value.cardColor, { alpha: 0 }), "--n-mask-height": toPx(props2.bottomMaskHeight) }; }); mountStyle$2$1(); return (_ctx, _cache) => { const _component_n_virtual_list = naiveUi.NVirtualList; return vue.openBlock(), vue.createElementBlock("div", { class: vue.normalizeClass(["nb-virtual-list", vue.unref(attrs).class]), style: vue.normalizeStyle([vue.unref(attrs).style, vue.unref(cssVars)]) }, [ vue.createVNode(_component_n_virtual_list, vue.mergeProps({ ref_key: "listRef", ref: listRef, class: "nb-virtual-list__list", items: props2.items }, vue.unref(forwardedAttrs), { onScroll: handleScroll, onResize: handleResize, onWheel: handleWheel }), { default: vue.withCtx((slotProps) => [ vue.renderSlot(_ctx.$slots, "default", vue.normalizeProps(vue.guardReactiveProps(slotProps))) ]), _: 3 }, 16, ["items"]), vue.withDirectives(vue.createElementVNode("div", _hoisted_1$3$1, null, 512), [ [vue.vShow, props2.showBottomMask && vue.unref(showMask)] ]) ], 6); }; } }); const style$1$1 = c([ cB( "nb-danmaku-table", css` display: flex; flex-direction: column; min-height: 0; color: var(--n-text-color-1); font-size: var(--n-font-size); border: 1px solid var(--n-border-color); border-radius: var(--n-border-radius); overflow: hidden; `, [ cE( "header", css` display: flex; font-weight: var(--n-font-weight-strong); color: var(--n-text-color-3); background-color: var(--n-table-header-color); border-bottom: 1px solid var(--n-border-color); flex: 0 0 auto; ` ), cE( "header-cell", css` display: flex; align-items: center; gap: 6px; user-select: none; cursor: pointer; &:hover .nb-danmaku-table__sort-indicator { opacity: 0.55; } ` ), cE( "sort-indicator", css` width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 6px solid var(--n-text-color-3); opacity: 0; transition: transform 0.2s var(--n-cubic-bezier-ease-out), opacity 0.2s var(--n-cubic-bezier-ease-out), border-top-color 0.2s var(--n-cubic-bezier-ease-out); `, [ cM( "active", css` opacity: 1 !important; border-top-color: var(--n-text-color-1); ` ), cM( "desc", css` transform: rotate(180deg); ` ) ] ), cE( "cell", css` padding: 8px; box-sizing: border-box; border-right: 1px solid var(--n-border-color); min-width: 0; ` ), cE( "body", css` flex: 1; min-height: 0; ` ), cE( "row", css` display: flex; border-bottom: 1px solid var(--n-border-color); transition: background-color 0.2s var(--n-cubic-bezier-ease-out); cursor: pointer; &:hover { background-color: var(--n-hover-color); } `, [ cM( "highlight", css` background-color: var(--n-primary-color-highlight) !important; ` ) ] ), cE( "content-cell", css` white-space: normal; word-break: break-word; overflow-wrap: anywhere; ` ), cE( "empty", css` min-height: 140px; ` ), cE( "empty-wrap", css` flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; ` ) ] ) ]); function mountStyle$1$1() { useTheme("nb-danmaku-table", style$1$1); } const _hoisted_1$1$1 = { class: "nb-danmaku-table__header" }; const _hoisted_2$1$1 = ["onClick"]; const _hoisted_3$4 = ["onClick"]; const _hoisted_4$4 = { class: "nb-danmaku-table__cell", style: { width: "70px" } }; const _hoisted_5 = { class: "nb-danmaku-table__cell nb-danmaku-table__content-cell", style: { flex: 1 } }; const _hoisted_6 = { key: 1, class: "nb-danmaku-table__cell nb-danmaku-table__content-cell", style: { flex: 1 } }; const _hoisted_7 = { class: "nb-danmaku-table__cell", style: { width: "170px", borderRight: "none" } }; const _hoisted_8 = { key: 1, class: "nb-danmaku-table__empty-wrap" }; const _sfc_main$1$1 = { __name: "DanmakuTable", props: { items: { type: Array, default: () => [] }, itemHeight: { type: Number, default: 40 }, menuItems: { type: Array, default: () => [] }, to: { type: [String, Object], default: void 0 } }, emits: ["row-click", "sort-change"], setup(__props2, { expose: __expose2, emit: __emit2 }) { const props2 = __props2; const emit2 = __emit2; const vListRef = vue.ref(null); const highlightedRowKey = vue.ref(null); const pendingMenuActions = vue.ref( new Set()); const sortKey = vue.ref("progress"); const sortOrder = vue.ref("asc"); const columns = [ { key: "progress", label: "时间", style: { width: "70px" } }, { key: "content", label: "弹幕内容", style: { flex: 1 } }, { key: "ctime", label: "发送时间", style: { width: "170px", borderRight: "none" } } ]; const normalizedItems = vue.computed(() => { return props2.items.map((item, index) => ({ __raw: item, ...item, __index: index, __key: String(item?.idStr || item?.id || `${item?.ctime || 0}-${item?.progress || 0}-${index}`) })); }); const compareString = (a, b) => { return String(a || "").localeCompare(String(b || ""), "zh-Hans-CN"); }; const sortedItems = vue.computed(() => { const factor = sortOrder.value === "asc" ? 1 : -1; return [...normalizedItems.value].sort((a, b) => { let result = 0; if (sortKey.value === "content") { result = compareString(a.content, b.content); } else { const av = Number(a?.[sortKey.value]) || 0; const bv = Number(b?.[sortKey.value]) || 0; result = av - bv; } if (result === 0) { return a.__index - b.__index; } return result * factor; }); }); const sortState = vue.computed(() => ({ key: sortKey.value, order: sortOrder.value })); const isColumnActive = (key) => sortKey.value === key; const toggleSort = (key) => { if (sortKey.value === key) { sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc"; return; } sortKey.value = key; sortOrder.value = "asc"; }; const getSortState = () => ({ key: sortKey.value, order: sortOrder.value }); vue.watch( sortState, (state) => { emit2("sort-change", { ...state }); }, { immediate: true } ); const hasMenuItems = vue.computed(() => props2.menuItems.length > 0); let highlightTimer = null; const setHighlightByKey = (key, duration = 1500) => { if (highlightTimer) { clearTimeout(highlightTimer); highlightTimer = null; } if (!key && key !== 0) { highlightedRowKey.value = null; return; } highlightedRowKey.value = String(key); highlightTimer = setTimeout(() => { highlightedRowKey.value = null; highlightTimer = null; }, duration); }; const getMenuActionKey = (rowIndex, menuIndex) => `${rowIndex}::${menuIndex}`; const isMenuDisabled = (menu, rowIndex, menuIndex) => { if (menu?.disabled) return true; return pendingMenuActions.value.has(getMenuActionKey(rowIndex, menuIndex)); }; const getMenuName = (menu, item) => { if (typeof menu?.getName === "function") return menu.getName(item); return menu?.name || "操作"; }; const getDropdownOptions = (item, rowIndex) => { return props2.menuItems.map((menu, menuIndex) => ({ key: String(menuIndex), label: getMenuName(menu, item), disabled: isMenuDisabled(menu, rowIndex, menuIndex) })); }; const onMenuSelect = async (menu, item, rowIndex, menuIndex) => { if (!menu || typeof menu.onSelect !== "function") return; const actionKey = getMenuActionKey(rowIndex, menuIndex); if (pendingMenuActions.value.has(actionKey)) return; pendingMenuActions.value.add(actionKey); try { await menu.onSelect(item); } finally { pendingMenuActions.value.delete(actionKey); } }; const onDropdownSelect = (key, item, rowIndex) => { const menuIndex = Number(key); if (Number.isNaN(menuIndex)) return; const menu = props2.menuItems[menuIndex]; onMenuSelect(menu, item, rowIndex, menuIndex); }; const scrollToRow = (idx) => { vue.nextTick(() => { const targetIndex = Math.max(0, Number(idx) || 0); const visualIndex = sortedItems.value.findIndex((item) => item.__index === targetIndex); if (visualIndex < 0) return; vListRef.value?.scrollTo?.({ index: Math.max(0, visualIndex - 3), debounce: true }); setHighlightByKey(sortedItems.value[visualIndex]?.__key); }); }; const scrollToId = (id) => { vue.nextTick(() => { if (id === void 0 || id === null || id === "") return; const key = String(id); vListRef.value?.scrollTo?.({ key, debounce: true }); setHighlightByKey(key); }); }; __expose2({ scrollToRow, scrollToId, getSortState }); vue.onBeforeUnmount(() => { if (highlightTimer) { clearTimeout(highlightTimer); highlightTimer = null; } }); const themeVars = naiveUi.useThemeVars(); const cssVars = vue.computed(() => { return { "--n-primary-color-highlight": changeColor(themeVars.value.primaryColor, { alpha: Number(themeVars.value.opacity5) }), "--n-border-color": themeVars.value.borderColor, "--n-border-radius": themeVars.value.borderRadius, "--n-table-header-color": themeVars.value.tableHeaderColor, "--n-hover-color": themeVars.value.hoverColor, "--n-text-color-1": themeVars.value.textColor1, "--n-text-color-2": themeVars.value.textColor2, "--n-text-color-3": themeVars.value.textColor3, "--n-font-size": themeVars.value.fontSize, "--n-font-weight-strong": themeVars.value.fontWeightStrong, "--n-cubic-bezier-ease-out": themeVars.value.cubicBezierEaseOut, "--n-item-height": toPx(props2.itemHeight) }; }); mountStyle$1$1(); return (_ctx, _cache) => { const _component_n_dropdown = naiveUi.NDropdown; const _component_n_empty = naiveUi.NEmpty; return vue.openBlock(), vue.createElementBlock("div", { class: "nb-danmaku-table", style: vue.normalizeStyle(vue.unref(cssVars)) }, [ vue.createElementVNode("div", _hoisted_1$1$1, [ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(columns, (column) => { return vue.createElementVNode("div", { key: column.key, class: "nb-danmaku-table__cell nb-danmaku-table__header-cell", style: vue.normalizeStyle(column.style), onClick: ($event) => toggleSort(column.key) }, [ vue.createElementVNode("span", null, vue.toDisplayString(column.label), 1), vue.createElementVNode("span", { class: vue.normalizeClass(["nb-danmaku-table__sort-indicator", { "nb-danmaku-table__sort-indicator--active": isColumnActive(column.key), "nb-danmaku-table__sort-indicator--desc": isColumnActive(column.key) && vue.unref(sortOrder) === "desc" }]) }, null, 2) ], 12, _hoisted_2$1$1); }), 64)) ]), vue.unref(sortedItems).length ? (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$3$1), { key: 0, ref_key: "vListRef", ref: vListRef, class: "nb-danmaku-table__body", items: vue.unref(sortedItems), "item-size": props2.itemHeight, "key-field": "__key", "item-resizable": "" }, { default: vue.withCtx(({ item }) => [ vue.createElementVNode("div", { class: vue.normalizeClass(["nb-danmaku-table__row", { "nb-danmaku-table__row--highlight": vue.unref(highlightedRowKey) === item.__key }]), style: vue.normalizeStyle({ minHeight: vue.unref(toPx)(props2.itemHeight) }), onClick: ($event) => emit2("row-click", item.__raw) }, [ vue.createElementVNode("div", _hoisted_4$4, vue.toDisplayString(vue.unref(formatProgress$1)(item.progress ?? 0)), 1), vue.unref(hasMenuItems) ? (vue.openBlock(), vue.createBlock(_component_n_dropdown, { key: 0, trigger: "hover", placement: "bottom-end", "show-arrow": false, to: props2.to, options: getDropdownOptions(item.__raw, item.__index), onSelect: (key) => onDropdownSelect(key, item.__raw, item.__index) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_5, vue.toDisplayString(item.content || ""), 1) ]), _: 2 }, 1032, ["to", "options", "onSelect"])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_6, vue.toDisplayString(item.content || ""), 1)), vue.createElementVNode("div", _hoisted_7, vue.toDisplayString(vue.unref(formatTimestamp)(item.ctime)), 1) ], 14, _hoisted_3$4) ]), _: 1 }, 8, ["items", "item-size"])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_8, [ vue.createVNode(_component_n_empty, { class: "nb-danmaku-table__empty", description: "暂无弹幕" }) ])) ], 4); }; } }; const style$8 = c([ cB( "nb-user-card", css` color: var(--n-text-color-1); border: 1px solid var(--n-border-color); border-radius: var(--n-border-radius); `, [ cE( "header", css` align-items: flex-start; @media (max-width: 768px) { flex-wrap: wrap; } ` ), cE( "avatar-image", css` flex: 0 0 auto; border-radius: 50%; overflow: hidden; ` ), cE( "info", css` min-width: 0; flex: 1; ` ), cE( "name", css` font-size: var(--n-font-size-large); font-weight: var(--n-font-weight-strong); color: var(--n-text-color-1); ` ), cE( "sign", css` display: block; white-space: pre-wrap; ` ), cE( "desc", css` font-size: var(--n-font-size-mini); color: var(--n-text-color-3); ` ), cE( "label", css` font-weight: var(--n-font-weight-strong); ` ), cE( "mid-link", css` display: inline-flex; vertical-align: baseline; ` ), cE( "clickable-tag", css` cursor: pointer; user-select: none; ` ), cE( "divider", css` margin: 20px 0; ` ) ] ) ]); function mountStyle$8() { useTheme("nb-user-card", style$8); } const _hoisted_1$e = ["href"]; const _hoisted_2$6 = ["href"]; const _sfc_main$a = Object.assign({ name: "UserCard" }, { __name: "UserCard", props: { userCard: { type: Object, default: () => ({}) }, midHash: { type: String, default: "" } }, setup(__props2) { const props2 = __props2; const officialRoleMap = { 0: "无", 1: "个人认证 - 知名UP主", 2: "个人认证 - 大V达人", 3: "机构认证 - 企业", 4: "机构认证 - 组织", 5: "机构认证 - 媒体", 6: "机构认证 - 政府", 7: "个人认证 - 高能主播", 9: "个人认证 - 社会知名人士" }; const card = vue.computed(() => props2.userCard?.card || {}); const avatarUrl = vue.computed(() => card.value?.face || ""); const mid = vue.computed(() => card.value?.mid || ""); const midUrl = vue.computed(() => { if (!mid.value) return ""; return `https://space.bilibili.com/${mid.value}`; }); const officialInfo = vue.computed(() => { const o = card.value?.Official; if (!o || Number(o.type) === -1) return null; return { typeText: officialRoleMap[o.role] || "未知认证", title: o.title || "(无标题)", desc: o.desc || "" }; }); const statItems = vue.computed(() => { return [ { label: "关注数", value: card.value?.friend }, { label: "粉丝数", value: props2.userCard?.follower }, { label: "获赞数", value: props2.userCard?.like_num }, { label: "稿件数", value: props2.userCard?.archive_count } ]; }); const message = naiveUi.useMessage(); const copyToClipboard = async (text) => { if (!text) return; try { await navigator.clipboard.writeText(String(text)); message.success("midHash 已复制"); } catch { message.error("复制失败"); } }; const themeVars = naiveUi.useThemeVars(); const cssVars = vue.computed(() => { return { "--n-primary-color-hover": themeVars.value.primaryColorHover, "--n-border-color": themeVars.value.borderColor, "--n-border-radius": themeVars.value.borderRadius, "--n-divider-color": themeVars.value.dividerColor, "--n-tag-color": themeVars.value.tagColor, "--n-tag-text-color": themeVars.value.textColor3, "--n-text-color-1": themeVars.value.textColor1, "--n-text-color-2": themeVars.value.textColor2, "--n-text-color-3": themeVars.value.textColor3, "--n-font-size": themeVars.value.fontSize, "--n-font-size-mini": themeVars.value.fontSizeMini, "--n-font-size-large": themeVars.value.fontSizeLarge, "--n-font-weight-strong": themeVars.value.fontWeightStrong }; }); mountStyle$8(); return (_ctx, _cache) => { const _component_n_text = naiveUi.NText; const _component_n_tag = naiveUi.NTag; const _component_n_flex = naiveUi.NFlex; const _component_n_button = naiveUi.NButton; const _component_n_divider = naiveUi.NDivider; const _component_n_statistic = naiveUi.NStatistic; const _component_n_grid_item = naiveUi.NGridItem; const _component_n_grid = naiveUi.NGrid; const _component_n_card = naiveUi.NCard; return vue.openBlock(), vue.createBlock(_component_n_card, { bordered: true, class: "nb-user-card", style: vue.normalizeStyle(vue.unref(cssVars)) }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { size: 20, wrap: false, class: "nb-user-card__header" }, { default: vue.withCtx(() => [ vue.unref(avatarUrl) ? (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$7$1), { key: 0, src: vue.unref(avatarUrl), size: "100", class: "nb-user-card__avatar-image", lazy: "" }, null, 8, ["src"])) : vue.createCommentVNode("", true), vue.createVNode(_component_n_flex, { vertical: "", size: 10, class: "nb-user-card__info" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { align: "center", size: 8, wrap: true }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, { class: "nb-user-card__name" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(card)?.name || "未知用户"), 1) ]), _: 1 }), vue.unref(card)?.sex && vue.unref(card).sex !== "保密" ? (vue.openBlock(), vue.createBlock(_component_n_tag, { key: 0, size: "small", type: "info" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(card).sex), 1) ]), _: 1 })) : vue.createCommentVNode("", true), vue.unref(card)?.level_info ? (vue.openBlock(), vue.createBlock(_component_n_tag, { key: 1, size: "small", type: "success" }, { default: vue.withCtx(() => [ vue.createTextVNode(" LV" + vue.toDisplayString(vue.unref(card).level_info.current_level), 1) ]), _: 1 })) : vue.createCommentVNode("", true), vue.unref(card)?.vip?.vipStatus === 1 ? (vue.openBlock(), vue.createBlock(_component_n_tag, { key: 2, size: "small", type: "warning" }, { default: vue.withCtx(() => [..._cache[1] || (_cache[1] = [ vue.createTextVNode("大会员", -1) ])]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }), vue.createVNode(_component_n_text, { class: "nb-user-card__desc nb-user-card__sign" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(card)?.sign || "这位用户很神秘,什么都没写。"), 1) ]), _: 1 }), vue.createVNode(_component_n_flex, { align: "center", size: 8, wrap: true }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, { class: "nb-user-card__label" }, { default: vue.withCtx(() => [..._cache[2] || (_cache[2] = [ vue.createTextVNode("MID:", -1) ])]), _: 1 }), vue.unref(midUrl) ? (vue.openBlock(), vue.createBlock(_component_n_button, { key: 0, text: "", tag: "a", href: vue.unref(midUrl), target: "_blank", type: "primary", class: "nb-user-card__mid-link" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(mid)), 1) ]), _: 1 }, 8, ["href"])) : (vue.openBlock(), vue.createBlock(_component_n_text, { key: 1 }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(mid) || "-"), 1) ]), _: 1 })), props2.midHash ? (vue.openBlock(), vue.createBlock(_component_n_tag, { key: 2, size: "small", class: "nb-user-card__clickable-tag", title: "复制 midHash", onClick: _cache[0] || (_cache[0] = ($event) => copyToClipboard(props2.midHash)) }, { default: vue.withCtx(() => [ vue.createTextVNode(" Hash: " + vue.toDisplayString(props2.midHash), 1) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }), vue.unref(officialInfo) ? (vue.openBlock(), vue.createBlock(_component_n_flex, { key: 0, align: "center", size: 8, wrap: true }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, { class: "nb-user-card__label" }, { default: vue.withCtx(() => [..._cache[3] || (_cache[3] = [ vue.createTextVNode("认证:", -1) ])]), _: 1 }), vue.createVNode(_component_n_tag, { size: "small", type: "info" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(officialInfo).typeText), 1) ]), _: 1 }), vue.createVNode(_component_n_text, null, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(officialInfo).title), 1) ]), _: 1 }), vue.unref(officialInfo).desc ? (vue.openBlock(), vue.createBlock(_component_n_text, { key: 0, class: "nb-user-card__desc" }, { default: vue.withCtx(() => [ vue.createTextVNode("(" + vue.toDisplayString(vue.unref(officialInfo).desc) + ")", 1) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 })) : vue.createCommentVNode("", true), vue.unref(card)?.nameplate?.name ? (vue.openBlock(), vue.createBlock(_component_n_flex, { key: 1, align: "center", size: 8, wrap: true }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, { class: "nb-user-card__label" }, { default: vue.withCtx(() => [..._cache[4] || (_cache[4] = [ vue.createTextVNode("勋章:", -1) ])]), _: 1 }), vue.unref(card)?.nameplate?.image ? (vue.openBlock(), vue.createElementBlock("a", { key: 0, href: vue.unref(card).nameplate.image, target: "_blank", rel: "noopener noreferrer", title: "点击查看大图" }, [ vue.createVNode(_component_n_tag, { size: "small", type: "info", class: "nb-user-card__clickable-tag" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(card).nameplate.name), 1) ]), _: 1 }) ], 8, _hoisted_1$e)) : (vue.openBlock(), vue.createBlock(_component_n_tag, { key: 1, size: "small", type: "info" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(card).nameplate.name), 1) ]), _: 1 })), vue.createVNode(_component_n_text, { class: "nb-user-card__desc" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(card).nameplate.level) + " - " + vue.toDisplayString(vue.unref(card).nameplate.condition), 1) ]), _: 1 }) ]), _: 1 })) : vue.createCommentVNode("", true), vue.unref(card)?.pendant?.name && vue.unref(card)?.pendant?.image ? (vue.openBlock(), vue.createBlock(_component_n_flex, { key: 2, align: "center", size: 8, wrap: true }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, { class: "nb-user-card__label" }, { default: vue.withCtx(() => [..._cache[5] || (_cache[5] = [ vue.createTextVNode("挂件:", -1) ])]), _: 1 }), vue.createElementVNode("a", { href: vue.unref(card).pendant.image, target: "_blank", rel: "noopener noreferrer", title: "点击查看大图" }, [ vue.createVNode(_component_n_tag, { size: "small", type: "info", class: "nb-user-card__clickable-tag" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(card).pendant.name), 1) ]), _: 1 }) ], 8, _hoisted_2$6) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }) ]), _: 1 }), vue.createVNode(_component_n_divider, { class: "nb-user-card__divider" }), vue.createVNode(_component_n_grid, { cols: 4, "x-gap": 20, "y-gap": 12, responsive: "screen" }, { default: vue.withCtx(() => [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(statItems), (item) => { return vue.openBlock(), vue.createBlock(_component_n_grid_item, { key: item.label }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_statistic, { label: item.label, value: vue.unref(formatStatValue)(item.value) }, null, 8, ["label", "value"]) ]), _: 2 }, 1024); }), 128)) ]), _: 1 }) ]), _: 1 }, 8, ["style"]); }; } }); const virtualListStyle = c$2( ".v-vl", { maxHeight: "inherit", height: "100%", overflow: "auto", minWidth: "1px" }, [ c$2( "&:not(.v-vl--show-scrollbar)", { scrollbarWidth: "none" }, [ c$2("&::-webkit-scrollbar, &::-webkit-scrollbar-track-piece, &::-webkit-scrollbar-thumb", { width: 0, height: 0, display: "none" }) ] ) ] ); const binderStyle = c$2([ c$2(".v-binder-follower-container", { position: "absolute", left: "0", right: "0", top: "0", height: "0", pointerEvents: "none", zIndex: "auto" }), c$2( ".v-binder-follower-content", { position: "absolute", zIndex: "auto" }, [ c$2("> *", { pointerEvents: "all" }) ] ) ]); let mountedDefault = false; const mountedByParent = new WeakSet(); const mountOne = (style2, id, parent) => { style2.mount({ id, head: true, anchorMetaName: cssrAnchorMetaName$1, parent }); }; function mountVueucStyles(styleMountTarget2) { if (styleMountTarget2 && typeof styleMountTarget2 === "object") { if (mountedByParent.has(styleMountTarget2)) return; mountOne(virtualListStyle, "vueuc/virtual-list", styleMountTarget2); mountOne(binderStyle, "vueuc/binder", styleMountTarget2); mountedByParent.add(styleMountTarget2); return; } if (mountedDefault) return; mountOne(virtualListStyle, "vueuc/virtual-list"); mountOne(binderStyle, "vueuc/binder"); mountedDefault = true; } const style$7 = c$3([ cB$2( "bds-dm-loader-panel", css$1` display: flex; flex-direction: column; gap: 12px; `, [ cE$2( "btn", css$1` min-width: 150px; ` ), cE$2( "action-row", css$1` padding: 2px 0; ` ), cE$2( "hint-btn", css$1` width: 20px; height: 20px; font-size: 12px; ` ), cE$2( "range", css$1` width: 280px; ` ), cE$2( "progress-row", css$1` margin-top: 12px; ` ), cE$2( "progress", css$1` width: 260px; ` ) ] ) ]); function mountStyle$7(mountTarget) { useTheme$1("bds-dm-loader-panel-style", style$7, mountTarget); } const _hoisted_1$6 = { class: "bds-dm-loader-panel" }; const _sfc_main$9 = { __name: "DmDataLoaderPanel", props: { arcMgr: { type: Object, default: null }, dmMgr: { type: Object, default: null }, to: { type: [String, Object], default: void 0 } }, emits: [ "sync-data", "set-error", "initial-load-finished" ], setup(__props2, { emit: __emit2 }) { const props2 = __props2; const emit2 = __emit2; const styleMountTarget2 = vue.inject("styleMountTarget", null); mountStyle$7(styleMountTarget2); function toDateStr(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } const panelLoading = vue.ref(false); const showLoadWarning = vue.ref(true); const initialTodayStr = toDateStr( new Date()); const selectedDateRange = vue.ref([initialTodayStr, initialTodayStr]); const autoLoadXml = vue.ref(storage.get("dmLoader.autoLoadXml", false)); const autoLoadPb = vue.ref(storage.get("dmLoader.autoLoadPb", true)); let autoLoadMgr = null; const message = naiveUi.useMessage(); const downloadMenuOptions = [ { label: "JSON", key: "json", children: [ { label: "无缩进", key: "json:none" }, { label: "缩进 2", key: "json:2" }, { label: "缩进 4", key: "json:4" } ] } ]; const loadProgress = vue.reactive({ visible: false, current: 0, total: 0, text: "", detail: "", addedDm: 0, scannedDays: 0, startTs: 0 }); const normalizeTimestampMs = (value) => { const n = Number(value); if (!Number.isFinite(n) || n <= 0) return null; return n > 1e12 ? n : n * 1e3; }; const toDayStartMs = (value) => { const d = new Date(value); d.setHours(0, 0, 0, 0); return d.getTime(); }; const pubDayStartMs = vue.computed(() => { const ts = normalizeTimestampMs(props2.arcMgr?.info?.pubtime); return ts ? toDayStartMs(ts) : null; }); const todayStartMs = () => toDayStartMs(Date.now()); const retryErrorSegments = vue.ref(0); const retryErrorDates = vue.ref(0); const retryErrorTotal = vue.computed(() => retryErrorSegments.value + retryErrorDates.value); const syncRetryErrorStats = () => { retryErrorSegments.value = Number(props2.dmMgr?.errors?.segments?.length) || 0; retryErrorDates.value = Number(props2.dmMgr?.errors?.dates?.length) || 0; }; const isSameRange = (a, b) => { if (!Array.isArray(a) || !Array.isArray(b)) return false; return a[0] === b[0] && a[1] === b[1]; }; const isHistoryDateDisabled = (ts) => { const day = toDayStartMs(ts); if (day > todayStartMs()) return true; if (pubDayStartMs.value != null && day < pubDayStartMs.value) return true; return false; }; const syncDateRangeByLimits = (range = selectedDateRange.value) => { const todayStr = toDateStr( new Date()); const pubStr = pubDayStartMs.value != null ? toDateStr(new Date(pubDayStartMs.value)) : ""; let [start, end] = range || []; if (!start) start = pubStr || todayStr; if (!end) end = todayStr; if (pubStr && start < pubStr) start = pubStr; if (pubStr && end < pubStr) end = pubStr; if (start > todayStr) start = todayStr; if (end > todayStr) end = todayStr; if (start > end) start = end; const normalized = [start, end]; if (!isSameRange(selectedDateRange.value, normalized)) { selectedDateRange.value = normalized; } }; const resetDateRangeToDefault = () => { const todayStr = toDateStr( new Date()); const pubStr = pubDayStartMs.value != null ? toDateStr(new Date(pubDayStartMs.value)) : ""; selectedDateRange.value = [pubStr || todayStr, todayStr]; }; const onDateRangeUpdate = (value) => { selectedDateRange.value = Array.isArray(value) ? value : ["", ""]; syncDateRangeByLimits(selectedDateRange.value); }; const resetProgress = () => { loadProgress.visible = false; loadProgress.current = 0; loadProgress.total = 0; loadProgress.text = ""; loadProgress.detail = ""; loadProgress.addedDm = 0; loadProgress.scannedDays = 0; loadProgress.startTs = 0; }; const startProgress = () => { loadProgress.visible = true; loadProgress.current = 0; loadProgress.total = 0; loadProgress.text = "准备中..."; loadProgress.detail = ""; loadProgress.addedDm = 0; loadProgress.scannedDays = 0; loadProgress.startTs = Date.now(); }; const updateProgress = (finished, total, current, count) => { loadProgress.current = Number(finished) || 0; loadProgress.total = Number(total) || 0; const curr = String(current || "-"); const delta = Number(count) || 0; const elapsedSec = Math.max(1, Math.floor((Date.now() - loadProgress.startTs) / 1e3)); if (curr.startsWith("扫描月份:")) { loadProgress.scannedDays += Math.max(0, delta); loadProgress.text = `扫描历史日期 ${loadProgress.current}/${loadProgress.total}`; loadProgress.detail = `${curr} | 本月发现 ${delta} 天 | 累计 ${loadProgress.scannedDays} 天`; return; } if (delta > 0) loadProgress.addedDm += delta; const speed = Math.round(loadProgress.addedDm / elapsedSec); loadProgress.text = `拉取弹幕 ${loadProgress.current}/${loadProgress.total}`; loadProgress.detail = `${curr} | 当前 +${delta} | 累计 +${loadProgress.addedDm} | ${speed}/s`; }; const withLoading = async (fn) => { if (!props2.dmMgr) return; panelLoading.value = true; emit2("set-error", ""); try { await fn(); return true; } catch (error) { const msg = String(error?.message || error); emit2("set-error", msg); message.error(msg || "载入失败"); return false; } finally { panelLoading.value = false; } }; const emitSyncData = () => { const list = props2.dmMgr?.data?.danmaku_list || []; const commandDms = props2.dmMgr?.data?.danmaku_view?.commandDms || []; emit2("sync-data", { list, commandDms }); syncRetryErrorStats(); }; const loadDmXml = async () => { if (!props2.dmMgr) return; let rise = 0; const ok = await withLoading(async () => { resetProgress(); rise = Number(await props2.dmMgr.getDmXml()) || 0; if (rise < 0) throw new Error("XML 载入失败,请检查稿件信息"); emitSyncData(); }); if (!ok) return; const added = Math.max(0, rise); message.success(`XML 载入完成,新增 ${added.toLocaleString()} 条`); }; const loadDmPb = async () => { if (!props2.dmMgr) return; let rise = 0; const ok = await withLoading(async () => { startProgress(); rise = Number(await props2.dmMgr.getDmPb(updateProgress)) || 0; if (rise < 0) throw new Error("ProtoBuf 载入失败,请检查稿件信息"); emitSyncData(); resetProgress(); }); if (!ok) return; const added = Math.max(0, rise); message.success(`ProtoBuf 载入完成,新增 ${added.toLocaleString()} 条`); }; const loadDmHisRange = async () => { if (!props2.dmMgr) return; const [start, end] = selectedDateRange.value || []; if (!start || !end) { emit2("set-error", "请先选择起始与结束日期"); return; } if (!/^\d{4}-\d{2}-\d{2}$/.test(start) || !/^\d{4}-\d{2}-\d{2}$/.test(end)) { emit2("set-error", "日期格式错误,应为 YYYY-MM-DD"); return; } if (start > end) { emit2("set-error", "起始日期不能晚于结束日期"); return; } let rise = 0; const ok = await withLoading(async () => { startProgress(); rise = Number(await props2.dmMgr.getDmPbHisRange({ start, end }, updateProgress)) || 0; if (rise < 0) throw new Error("历史区间载入失败,请检查稿件信息"); emitSyncData(); resetProgress(); }); if (!ok) return; const added = Math.max(0, rise); message.success(`历史区间载入完成,新增 ${added.toLocaleString()} 条`); }; const retryDmErrors = async () => { if (!props2.dmMgr) return; if (!retryErrorTotal.value) { emit2("set-error", "当前没有可重试的错误片段"); return; } const ok = await withLoading(async () => { startProgress(); await props2.dmMgr.retryErrors(updateProgress); emitSyncData(); resetProgress(); }); if (!ok) return; message.success("重试完成"); }; const clearDanmaku = () => { if (!props2.dmMgr) return; props2.dmMgr.clearData(); emitSyncData(); emit2("set-error", ""); message.success("已清除弹幕列表"); }; const downloadDanmakuData = (indentMode = 2) => { if (!props2.dmMgr || !props2.arcMgr) return; const data = { ...props2.dmMgr.data || {}, ...props2.arcMgr.data || {} }; const title = props2.arcMgr?.info?.id?.replace(/[\\/:*?"<>|]/g, "_") || "bds-data"; const indent = indentMode === "none" ? void 0 : Number(indentMode) || 2; const text = indentMode === "none" ? JSON.stringify(data) : JSON.stringify(data, null, indent); const blob = new Blob([text], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${title}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleDownloadMenuSelect = (key) => { if (key === "json:none") { downloadDanmakuData("none"); return; } if (key === "json:4") { downloadDanmakuData(4); return; } if (key === "json:2") { downloadDanmakuData(2); } }; const progressPercent = vue.computed(() => { const total = Number(loadProgress.total || 0); const current = Number(loadProgress.current || 0); if (total <= 0) return 0; return Math.max(0, Math.min(100, Math.floor(current / total * 100))); }); vue.onMounted(async () => { resetDateRangeToDefault(); syncDateRangeByLimits(); syncRetryErrorStats(); emitSyncData(); if (props2.dmMgr && autoLoadMgr !== props2.dmMgr) { autoLoadMgr = props2.dmMgr; if (autoLoadXml.value) { await loadDmXml(); } if (autoLoadPb.value) { await loadDmPb(); } } emit2("initial-load-finished"); }); vue.watch(autoLoadXml, (value) => { storage.set("dmLoader.autoLoadXml", Boolean(value)); }); vue.watch(autoLoadPb, (value) => { storage.set("dmLoader.autoLoadPb", Boolean(value)); }); vue.onBeforeUnmount(() => { resetProgress(); }); vue.watch(() => props2.dmMgr, () => { syncRetryErrorStats(); }, { immediate: true }); return (_ctx, _cache) => { const _component_n_alert = naiveUi.NAlert; const _component_n_button = naiveUi.NButton; const _component_n_checkbox = naiveUi.NCheckbox; const _component_n_icon = naiveUi.NIcon; const _component_n_tooltip = naiveUi.NTooltip; const _component_n_flex = naiveUi.NFlex; const _component_n_date_picker = naiveUi.NDatePicker; const _component_n_dropdown = naiveUi.NDropdown; const _component_n_button_group = naiveUi.NButtonGroup; const _component_n_tag = naiveUi.NTag; const _component_n_progress = naiveUi.NProgress; const _component_n_text = naiveUi.NText; return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$6, [ vue.unref(showLoadWarning) ? (vue.openBlock(), vue.createBlock(_component_n_alert, { key: 0, type: "warning", closable: "", style: { "margin-bottom": "12px" }, onClose: _cache[0] || (_cache[0] = ($event) => showLoadWarning.value = false) }, { default: vue.withCtx(() => [..._cache[4] || (_cache[4] = [ vue.createTextVNode(" 请勿短时间频繁载入,避免触发 B 站风控 ", -1) ])]), _: 1 })) : vue.createCommentVNode("", true), vue.createVNode(_component_n_flex, { size: 12, align: "center", wrap: "", class: "bds-dm-loader-panel__action-row" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { class: "bds-dm-loader-panel__btn", type: "primary", size: "small", loading: vue.unref(panelLoading), onClick: loadDmXml }, { default: vue.withCtx(() => [..._cache[5] || (_cache[5] = [ vue.createTextVNode(" 载入 XML 实时弹幕 ", -1) ])]), _: 1 }, 8, ["loading"]), vue.createVNode(_component_n_checkbox, { checked: vue.unref(autoLoadXml), "onUpdate:checked": _cache[1] || (_cache[1] = ($event) => vue.isRef(autoLoadXml) ? autoLoadXml.value = $event : null), size: "small" }, { default: vue.withCtx(() => [..._cache[6] || (_cache[6] = [ vue.createTextVNode(" 自动载入 ", -1) ])]), _: 1 }, 8, ["checked"]), vue.createVNode(_component_n_tooltip, { trigger: "hover", placement: "top", to: __props2.to }, { trigger: vue.withCtx(() => [ vue.createVNode(_component_n_button, { size: "tiny", quaternary: "", circle: "", class: "bds-dm-loader-panel__hint-btn" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_icon, { component: vue.unref(InfoCircle) }, null, 8, ["component"]) ]), _: 1 }) ]), default: vue.withCtx(() => [ _cache[7] || (_cache[7] = vue.createTextVNode(" 实时弹幕池容量有限,通常是更近期的弹幕。 ", -1)) ]), _: 1 }, 8, ["to"]) ]), _: 1 }), vue.createVNode(_component_n_flex, { size: 12, align: "center", wrap: "", class: "bds-dm-loader-panel__action-row" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { class: "bds-dm-loader-panel__btn", type: "primary", size: "small", loading: vue.unref(panelLoading), onClick: loadDmPb }, { default: vue.withCtx(() => [..._cache[8] || (_cache[8] = [ vue.createTextVNode(" 载入 ProtoBuf 弹幕 ", -1) ])]), _: 1 }, 8, ["loading"]), vue.createVNode(_component_n_checkbox, { checked: vue.unref(autoLoadPb), "onUpdate:checked": _cache[2] || (_cache[2] = ($event) => vue.isRef(autoLoadPb) ? autoLoadPb.value = $event : null), size: "small" }, { default: vue.withCtx(() => [..._cache[9] || (_cache[9] = [ vue.createTextVNode(" 自动载入 ", -1) ])]), _: 1 }, 8, ["checked"]), vue.createVNode(_component_n_tooltip, { trigger: "hover", placement: "top", to: __props2.to }, { trigger: vue.withCtx(() => [ vue.createVNode(_component_n_button, { size: "tiny", quaternary: "", circle: "", class: "bds-dm-loader-panel__hint-btn" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_icon, { component: vue.unref(InfoCircle) }, null, 8, ["component"]) ]), _: 1 }) ]), default: vue.withCtx(() => [ _cache[10] || (_cache[10] = vue.createTextVNode(" 二进制分片数据,B站当前使用的数据,通常覆盖更全。 ", -1)) ]), _: 1 }, 8, ["to"]) ]), _: 1 }), vue.createVNode(_component_n_flex, { size: 12, align: "center", wrap: "", class: "bds-dm-loader-panel__action-row" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_date_picker, { "formatted-value": vue.unref(selectedDateRange), type: "daterange", size: "small", "value-format": "yyyy-MM-dd", "is-date-disabled": isHistoryDateDisabled, to: __props2.to, clearable: false, class: "bds-dm-loader-panel__range", "onUpdate:formattedValue": onDateRangeUpdate }, null, 8, ["formatted-value", "to"]), vue.createVNode(_component_n_button, { class: "bds-dm-loader-panel__btn", type: "primary", size: "small", loading: vue.unref(panelLoading), onClick: loadDmHisRange }, { default: vue.withCtx(() => [..._cache[11] || (_cache[11] = [ vue.createTextVNode(" 载入区间历史弹幕 ", -1) ])]), _: 1 }, 8, ["loading"]) ]), _: 1 }), vue.createVNode(_component_n_flex, { size: 12, align: "center", wrap: "" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { size: "small", type: "error", onClick: clearDanmaku }, { default: vue.withCtx(() => [..._cache[12] || (_cache[12] = [ vue.createTextVNode("清除弹幕", -1) ])]), _: 1 }), vue.createVNode(_component_n_button_group, null, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { size: "small", type: "success", onClick: _cache[3] || (_cache[3] = ($event) => downloadDanmakuData("none")) }, { default: vue.withCtx(() => [..._cache[13] || (_cache[13] = [ vue.createTextVNode("下载数据", -1) ])]), _: 1 }), vue.createVNode(_component_n_dropdown, { trigger: "click", options: downloadMenuOptions, placement: "bottom-end", to: __props2.to, onSelect: handleDownloadMenuSelect }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { size: "small", type: "success", "aria-label": "下载选项" }, { default: vue.withCtx(() => [..._cache[14] || (_cache[14] = [ vue.createTextVNode(" ▼ ", -1) ])]), _: 1 }) ]), _: 1 }, 8, ["to"]) ]), _: 1 }), vue.unref(retryErrorTotal) ? (vue.openBlock(), vue.createBlock(_component_n_button, { key: 0, size: "small", type: "warning", loading: vue.unref(panelLoading), onClick: retryDmErrors }, { default: vue.withCtx(() => [..._cache[15] || (_cache[15] = [ vue.createTextVNode(" 重试错误 ", -1) ])]), _: 1 }, 8, ["loading"])) : vue.createCommentVNode("", true), vue.unref(retryErrorTotal) ? (vue.openBlock(), vue.createBlock(_component_n_tag, { key: 1, size: "small", type: "warning" }, { default: vue.withCtx(() => [ vue.createTextVNode(" 待重试 日期 " + vue.toDisplayString(vue.unref(retryErrorDates)) + " / 片段 " + vue.toDisplayString(vue.unref(retryErrorSegments)), 1) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }), vue.createVNode(_component_n_flex, { align: "center", size: 12, wrap: "", class: "bds-dm-loader-panel__progress-row" }, { default: vue.withCtx(() => [ vue.unref(loadProgress).visible ? (vue.openBlock(), vue.createBlock(_component_n_progress, { key: 0, type: "line", percentage: vue.unref(progressPercent), "show-indicator": true, class: "bds-dm-loader-panel__progress" }, null, 8, ["percentage"])) : vue.createCommentVNode("", true), vue.unref(loadProgress).visible ? (vue.openBlock(), vue.createBlock(_component_n_text, { key: 1, depth: "2" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(loadProgress).text), 1) ]), _: 1 })) : vue.createCommentVNode("", true), vue.unref(loadProgress).visible && vue.unref(loadProgress).detail ? (vue.openBlock(), vue.createBlock(_component_n_text, { key: 2, depth: "3" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(loadProgress).detail), 1) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }) ]); }; } }; const style$6 = c$3([ cB$2( "bds-dm-chart-manager", css$1` display: flex; flex-direction: column; min-height: 0; min-width: 0; height: 100%; max-height: 100%; overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable; `, [ cE$2( "body", css$1` position: relative; flex: 1; min-height: 0; min-width: 0; ` ), cE$2( "empty", css$1` min-height: 220px; display: flex; align-items: center; justify-content: center; ` ), cE$2( "item", css$1` position: relative; height: var(--item-height, 50%); min-height: 320px; ` ), cE$2( "actions", css$1` position: absolute; top: 8px; right: 10px; display: flex; direction: rtl; align-items: center; gap: 3px; opacity: 1; z-index: 10; transition: opacity 0.2s; ` ), cE$2( "action-btn", css$1` width: 24px; height: 24px; border-radius: 999px; border: none; outline: none; display: flex; align-items: center; justify-content: center; background-color: rgba(128, 128, 128, 0.45); color: #fff; font-weight: bold; font-size: 12px; cursor: pointer; &:hover { background-color: rgba(90, 90, 90, 0.75); } &:disabled { cursor: not-allowed; opacity: 0.35; } ` ), cE$2( "chart", css$1` width: 100%; height: 100%; ` ) ] ) ]); function mountStyle$6(mountTarget) { useTheme$1("bds-dm-chart-manager-style", style$6, mountTarget); } const userChart = { key: "user", title: "用户弹幕统计", expandedH: false, actions: [ { key: "locate-user", icon: "⚲", title: "定位用户", method: "locate" } ], selection: { source: "chart:user", template: "用户 {value}", wrapTag: false, formatValue(value) { const hash = String(value || "").trim(); if (!hash) return "-"; const NButton2 = this.ctx.ui?.NButton; if (!NButton2) return hash; return this.ctx.h( NButton2, { text: true, onClick: (event) => { event?.stopPropagation?.(); this.ctx.queryMidHash?.(hash); } }, { default: () => hash } ); }, predicate: (item, value) => String(item?.midHash || "") === String(value || "") }, isValidMidHash(value) { return /^[0-9a-f]+$/i.test(String(value || "").trim()); }, getMenuItems() { return [ { getName: (item) => `发送者:${item?.midHash || "-"}`, onSelect: (item) => { const hash = String(item?.midHash || "").trim(); if (!hash) return; const el = this.ctx.element; if (el?.scrollIntoView) { el.scrollIntoView({ behavior: "smooth", block: "center" }); } this.locateInChart(hash); } } ]; }, locate() { const NInput2 = this.ctx.ui?.NInput; const feedback = this.ctx.feedback; if (!NInput2 || !feedback?.dialog) { const value = window.prompt("请输入要定位的 midHash:", ""); if (!value) return; this.locateInChart(String(value).trim()); return; } let inputValue = ""; feedback.dialog.create({ title: "定位用户", positiveText: "定位", negativeText: "取消", content: () => this.ctx.h(NInput2, { defaultValue: inputValue, placeholder: "请输入 midHash(十六进制)", autofocus: true, onUpdateValue: (value) => { inputValue = String(value || ""); }, onKeyup: (event) => { if (event?.key === "Enter") event?.stopPropagation?.(); } }), onPositiveClick: () => { const hash = String(inputValue || "").trim(); if (!this.isValidMidHash(hash)) { feedback.message?.error?.("请输入正确的 midHash(十六进制格式)"); return false; } this.locateInChart(hash); return true; } }); }, locateInChart(midHash) { if (!this.instance || !midHash) return; const feedback = this.ctx.feedback; const option = this.instance.getOption(); const labels = option?.yAxis?.[0]?.data || []; const index = labels.indexOf(midHash); if (index < 0) { if (feedback?.dialog) { feedback.dialog.warning({ title: "定位失败", content: `未在当前图表中找到用户 ${midHash}`, positiveText: "知道了" }); } else { feedback?.message?.warning?.(`未在当前图表中找到用户 ${midHash}`); } return; } const scope = this.expandedH ? 20 : 8; const start = Math.min(labels.length - scope, Math.max(0, index - 3)); const end = Math.min(labels.length - 1, start + scope - 1); this.instance.setOption({ yAxis: { axisLabel: { formatter: (value) => { if (value === midHash) return `{highlight|${value}}`; return value; }, rich: { highlight: { color: "#2080f0", fontWeight: "bold" } } } }, dataZoom: [{ startValue: start, endValue: end }] }); feedback?.message?.success?.(`已定位到用户 ${midHash}`); }, render() { const data = this.ctx.items || []; const countMap = {}; for (const item of data) { const key = String(item?.midHash || "-"); countMap[key] = (countMap[key] || 0) + 1; } const stats = Object.entries(countMap).map(([user, count]) => ({ user, count })).sort((a, b) => b.count - a.count); const users = stats.map((item) => item.user); const counts = stats.map((item) => item.count); const maxCount = Math.max(1, ...counts); const scope = this.expandedH ? 20 : 8; this.instance.setOption({ title: { text: "用户弹幕统计", subtext: `共 ${users.length} 位用户` }, tooltip: {}, grid: { left: 100 }, xAxis: { type: "value", min: 0, max: Math.ceil(maxCount * 1.1), scale: false }, yAxis: { type: "category", data: users, inverse: true }, dataZoom: [ { type: "slider", yAxisIndex: 0, startValue: 0, endValue: users.length >= scope ? scope - 1 : users.length, width: 20 } ], series: [ { type: "bar", data: counts, label: { show: true, position: "right", formatter: "{c}", fontSize: 12 } } ] }); } }; const wordcloudChart = { key: "wordcloud", title: "弹幕词云", expandedH: false, segmentMode: "simple", minLen: 2, topN: 1e3, actions: [ { key: "toggle-segment", icon: "📝", title: "切换分词模式", method: "toggleSegmentMode" } ], selection: { source: "chart:wordcloud", template: "包含词语 {value}", predicate: (item, value) => { const content = String(item?.content || "").toLowerCase(); const keyword = String(value || "").toLowerCase(); return Boolean(keyword) && content.includes(keyword); } }, getModeLabel() { return this.segmentMode === "jieba" ? "jieba" : "普通"; }, toggleSegmentMode() { this.instance?.clear?.(); this.segmentMode = this.segmentMode === "jieba" ? "simple" : "jieba"; this.ctx.feedback?.message?.success?.(`已切换到${this.getModeLabel()}分词`); this.ctx.rerender?.(); }, async render() { const data = this.ctx.items || []; const mode = this.segmentMode === "jieba" ? "jieba" : "simple"; const archiveId = String(this.ctx.arcMgr?.info?.id || "").trim(); let list = []; const segmentWords2 = this.ctx.segmentWords; if (typeof segmentWords2 !== "function") { this.ctx.feedback?.message?.error?.("分词服务不可用"); } else { list = await segmentWords2({ mode, items: data, minLen: this.minLen, topN: this.topN, archiveId }); } this.instance?.setOption({ title: { text: this.segmentMode === "jieba" ? "弹幕词云[jieba分词]" : "弹幕词云" }, tooltip: {}, series: [ { type: "wordCloud", gridSize: 8, sizeRange: [12, 40], rotationRange: [0, 0], shape: "circle", data: Array.isArray(list) ? list : [] } ] }); } }; const toDateString = (value) => { const d = new Date(value); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; }; const formatProgress = (ms) => { const sec = Math.max(0, Math.floor(Number(ms || 0) / 1e3)); const h2 = Math.floor(sec / 3600); const m = String(Math.floor(sec % 3600 / 60)).padStart(2, "0"); const s = String(sec % 60).padStart(2, "0"); return h2 > 0 ? `${h2}:${m}:${s}` : `${m}:${s}`; }; const parseProgressToSec = (value) => { const text = String(value || "").trim(); if (!text) return null; const matched = text.match(/^(\d{1,2})[::](\d{2})(?:[::](\d{2}))?$/); if (!matched) return null; const p1 = Number(matched[1]); const p2 = Number(matched[2]); const p3 = matched[3] == null ? null : Number(matched[3]); if (!Number.isInteger(p1) || !Number.isInteger(p2) || p3 != null && !Number.isInteger(p3)) return null; if (p2 > 59 || p3 != null && p3 > 59) return null; return p3 == null ? p1 * 60 + p2 : p1 * 3600 + p2 * 60 + p3; }; const parseRangeValue = (value) => { const match = /^(\d+)-(\d+)$/.exec(String(value || "")); if (!match) return null; const start = Number(match[1]); const end = Number(match[2]); if (!Number.isFinite(start) || !Number.isFinite(end)) return null; return { start, end }; }; const modeLabelMap = { 1: "普通弹幕", 2: "普通弹幕", 3: "普通弹幕", 4: "底部弹幕", 5: "顶部弹幕", 6: "逆向弹幕", 7: "高级弹幕", 8: "代码弹幕", 9: "BAS弹幕" }; const poolLabelMap = { 0: "普通池", 1: "字幕池", 2: "特殊池", 3: "互动池" }; const densityChart = { key: "density", title: "弹幕密度分布", refresh: true, labelText: "", labelPosition: "end", rangeSelectionSec: null, _labelMarks: [], actions: [ { key: "set-label", icon: "📌", title: "添加标记", method: "setLabel" }, { key: "range-filter", icon: "▭", title: "范围筛选", method: "openRangeFilterDialog" } ], applyLabelText(text) { this.labelText = String(text || ""); const timeRegex = /(\d{1,2}[::]\d{2}(?:[::]\d{2})?)/; const labels = []; for (const line of this.labelText.split("\n")) { const row = String(line || "").trim(); if (!row) continue; const matched = row.match(timeRegex); if (!matched) continue; const sec = parseProgressToSec(matched[1]); if (!Number.isFinite(sec)) continue; const label = row.replace(matched[1], "").trim() || row; labels.push({ name: label, xAxis: sec }); } this._labelMarks = labels; this.ctx.rerender?.(); }, setLabel() { const NInput2 = this.ctx.ui?.NInput; const NText2 = this.ctx.ui?.NText; const NRadioGroup2 = this.ctx.ui?.NRadioGroup; const NRadioButton2 = this.ctx.ui?.NRadioButton; const feedback = this.ctx.feedback; if (!NInput2 || !NText2 || !NRadioGroup2 || !NRadioButton2 || !feedback?.dialog) { const text = window.prompt("请输入标记(每行一个,格式 mm:ss 文本)", this.labelText || ""); if (text == null) return; this.applyLabelText(text); return; } let inputText = String(this.labelText || ""); let labelPos = String(this.labelPosition || "end"); feedback.dialog.create({ title: "添加标记", positiveText: "应用", negativeText: "取消", content: () => this.ctx.h("div", [ this.ctx.h(NText2, { type: "info" }, { default: () => "请输入标记时间和文本" }), this.ctx.h(NInput2, { type: "textarea", rows: 8, defaultValue: inputText, placeholder: "6:06 示例\n12:12 示例2", autofocus: true, style: "margin: 12px 0px;", onUpdateValue: (value) => { inputText = String(value || ""); } }), this.ctx.h("div", [ this.ctx.h(NText2, { type: "info" }, { default: () => "标记位置:" }), this.ctx.h(NRadioGroup2, { value: labelPos, size: "small", style: "margin-left: 8px; vertical-align: top;", onUpdateValue: (value) => { labelPos = String(value || "end"); } }, { default: () => [ this.ctx.h(NRadioButton2, { value: "end" }, { default: () => "顶端" }), this.ctx.h(NRadioButton2, { value: "insideEnd" }, { default: () => "内部" }) ] }) ]) ]), onPositiveClick: () => { this.labelPosition = labelPos === "insideEnd" ? "insideEnd" : "end"; this.applyLabelText(inputText); return true; } }); }, getDurationSec() { const data = this.ctx.items || []; const maxProgressMs = Math.max(0, ...data.map((item) => Number(item?.progress || 0))); const fromArc = Number(this.ctx.arcMgr?.info?.duration || 0); if (fromArc > 0) return Math.max(1, Math.ceil(fromArc)); return Math.max(1, Math.ceil(maxProgressMs / 1e3)); }, normalizeRangeSec(rangeSec, maxSec) { const fallback = [0, maxSec]; if (!Array.isArray(rangeSec) || rangeSec.length < 2) return fallback; const start = Math.max(0, Math.min(maxSec, Math.floor(Number(rangeSec[0]) || 0))); const end = Math.max(0, Math.min(maxSec, Math.floor(Number(rangeSec[1]) || 0))); return start <= end ? [start, end] : [end, start]; }, openRangeFilterDialog() { const NSlider = this.ctx.ui?.NSlider; const NInput2 = this.ctx.ui?.NInput; const feedback = this.ctx.feedback; const maxSec = this.getDurationSec(); const initial = this.normalizeRangeSec(this.rangeSelectionSec, maxSec); const secToText = (sec) => formatProgress(Number(sec || 0) * 1e3); if (!NSlider || !NInput2 || !feedback?.dialog) { const text = window.prompt("请输入范围(秒),格式:start,end", `${initial[0]},${initial[1]}`); if (!text) return; const parts = String(text).split(",").map((v) => Number(v.trim())); if (parts.length < 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) return; const [startSec, endSec] = this.normalizeRangeSec(parts, maxSec); this.rangeSelectionSec = [startSec, endSec]; this.applyRangeFilter(startSec * 1e3, endSec * 1e3); this.ctx.rerender?.(); return; } const rangeSecRef = vue.ref([...initial]); const startTextRef = vue.ref(secToText(initial[0])); const endTextRef = vue.ref(secToText(initial[1])); const startEditingRef = vue.ref(false); const endEditingRef = vue.ref(false); const syncTextByRange = (force = false) => { if (force || !startEditingRef.value) { startTextRef.value = secToText(rangeSecRef.value[0]); } if (force || !endEditingRef.value) { endTextRef.value = secToText(rangeSecRef.value[1]); } }; const syncRangeByInput = (type, text) => { const sec = parseProgressToSec(text); if (!Number.isFinite(sec)) return; const next = type === "start" ? [sec, rangeSecRef.value[1]] : [rangeSecRef.value[0], sec]; rangeSecRef.value = this.normalizeRangeSec(next, maxSec); syncTextByRange(); }; const finalizeInput = (type) => { if (type === "start") startEditingRef.value = false; else endEditingRef.value = false; const text = type === "start" ? startTextRef.value : endTextRef.value; const sec = parseProgressToSec(text); if (!Number.isFinite(sec)) return; const next = type === "start" ? [sec, rangeSecRef.value[1]] : [rangeSecRef.value[0], sec]; rangeSecRef.value = this.normalizeRangeSec(next, maxSec); syncTextByRange(true); }; const Content = vue.defineComponent({ name: "DensityRangeFilterDialogContent", setup: () => { return () => this.ctx.h("div", [ this.ctx.h("div", { style: "display: flex; align-items: center; gap: 8px;" }, [ this.ctx.h(NInput2, { value: startTextRef.value, placeholder: "mm:ss 或 hh:mm:ss", onFocus: () => { startEditingRef.value = true; }, onBlur: () => { finalizeInput("start"); }, onUpdateValue: (value) => { startTextRef.value = String(value || ""); syncRangeByInput("start", startTextRef.value); } }), this.ctx.h("span", { style: "color: var(--n-text-color-3);" }, "~"), this.ctx.h(NInput2, { value: endTextRef.value, placeholder: "mm:ss 或 hh:mm:ss", onFocus: () => { endEditingRef.value = true; }, onBlur: () => { finalizeInput("end"); }, onUpdateValue: (value) => { endTextRef.value = String(value || ""); syncRangeByInput("end", endTextRef.value); } }) ]), this.ctx.h(NSlider, { style: "margin-top: 10px;", range: true, step: 1, min: 0, max: maxSec, value: rangeSecRef.value, formatTooltip: (value) => secToText(value), onUpdateValue: (value) => { if (!Array.isArray(value) || value.length < 2) return; rangeSecRef.value = this.normalizeRangeSec(value, maxSec); syncTextByRange(); } }) ]); } }); feedback.dialog.create({ title: "范围筛选", positiveText: "筛选", negativeText: "取消", content: () => this.ctx.h(Content), onPositiveClick: () => { const startFromInput = parseProgressToSec(startTextRef.value); const endFromInput = parseProgressToSec(endTextRef.value); if (!Number.isFinite(startFromInput) || !Number.isFinite(endFromInput)) { feedback.message?.error?.("请输入正确时间格式(mm:ss 或 hh:mm:ss)"); return false; } const [startSec, endSec] = this.normalizeRangeSec([startFromInput, endFromInput], maxSec); this.rangeSelectionSec = [startSec, endSec]; this.applyRangeFilter(startSec * 1e3, endSec * 1e3); this.ctx.rerender?.(); return true; } }); }, applyRangeFilter(startMs, endMs) { const start = Math.min(startMs, endMs); const end = Math.max(startMs, endMs); const value = `${start}-${end}`; this.ctx.stageFilter({ source: "chart:density", value, template: "时间段 {value}", formatValue: (raw) => { const parsed = parseRangeValue(raw); if (!parsed) return String(raw || ""); return `${formatProgress(parsed.start)} ~ ${formatProgress(parsed.end)}`; }, predicate: (item, raw) => { const parsed = parseRangeValue(raw); if (!parsed) return false; const progress = Number(item?.progress || 0); return progress >= parsed.start && progress <= parsed.end; } }); }, onClick({ params }) { const sec = Number(params?.value?.[0]); if (!Number.isFinite(sec)) return; const table = this.ctx.tableRef?.value; if (!table || typeof table.scrollToRow !== "function") return; const sortState = table.getSortState?.(); if (!sortState || sortState.key !== "progress") { this.ctx.feedback?.message?.warning?.("请先按时间排序后再定位"); return; } const items = this.ctx.tableItems || []; if (!Array.isArray(items) || !items.length) return; const target = sec * 1e3; const idx = items.reduce((closestIdx, item, i) => { const currentDiff = Math.abs(Number(item?.progress || 0) - target); const closestDiff = Math.abs(Number(items[closestIdx]?.progress || 0) - target); return currentDiff < closestDiff ? i : closestIdx; }, 0); table.scrollToRow(idx); }, render() { const data = this.ctx.items || []; const maxProgress = Math.max(6e4, ...data.map((item) => Number(item?.progress || 0))); const durationSec = Number(this.ctx.arcMgr?.info?.duration || 0); const durationMs = durationSec > 0 ? durationSec * 1e3 : Math.max(6e4, maxProgress); const allowedIntervals = [1, 2, 3, 4, 5, 6, 10, 15, 20, 30]; let intervalMs; if (durationSec > 0) { let roughInterval = durationSec / 60; let zoom = 1e3; while (roughInterval > 45) { roughInterval /= 60; zoom *= 60; } const nearest = allowedIntervals.reduce((a, b) => Math.abs(b - roughInterval) < Math.abs(a - roughInterval) ? b : a); intervalMs = zoom * nearest; } else { const targetBins = 90; intervalMs = Math.max(1e3, Math.ceil(durationMs / targetBins / 1e3) * 1e3); } const binCount = Math.max(1, Math.ceil(durationMs / intervalMs)); this._intervalMs = intervalMs; const bins = new Array(binCount).fill(0); for (const item of data) { const progress = Number(item?.progress || 0); const idx = Math.min(binCount - 1, Math.max(0, Math.floor(progress / intervalMs))); bins[idx] += 1; } const lineData = bins.map((count, idx) => { const sec = Math.floor(idx * intervalMs / 1e3); return [sec, count]; }); const markLineData = []; for (const mark of this._labelMarks || []) { markLineData.push(mark); } const stagedFilter = this.ctx.stagedFilter || null; const isDensityStaged = stagedFilter?.source === "chart:density"; const normalizedRange = this.normalizeRangeSec(this.rangeSelectionSec, Math.ceil(durationMs / 1e3)); const hasRangeHighlight = isDensityStaged && Array.isArray(this.rangeSelectionSec) && this.rangeSelectionSec.length >= 2; const markAreaData = hasRangeHighlight ? [[{ xAxis: normalizedRange[0] }, { xAxis: normalizedRange[1] }]] : []; this.instance.setOption({ title: { text: "弹幕密度分布" }, tooltip: { trigger: "axis", formatter: (items) => { const current = items?.[0]; if (!current) return ""; const sec = Number(Array.isArray(current.value) ? current.value[0] : current.axisValue || 0); const count = Number(Array.isArray(current.value) ? current.value[1] : current.value || 0); return `时间段:${formatProgress(sec * 1e3)}
弹幕数:${count}`; } }, xAxis: { type: "value", name: "时间", min: 0, max: Math.ceil(durationMs / 1e3), axisLabel: { formatter: (val) => formatProgress(Number(val) * 1e3) } }, yAxis: { type: "value", name: "弹幕数量" }, series: [ { type: "line", smooth: true, areaStyle: {}, data: lineData, markLine: markLineData.length ? { silent: true, animation: false, symbol: "none", data: markLineData, label: { position: this.labelPosition || "end", formatter: "{b}" } } : null, markArea: markAreaData.length ? { silent: true, itemStyle: { color: "rgba(255, 100, 100, 0.2)" }, data: markAreaData } : null } ] }); } }; const dateChart = { key: "date", title: "发送日期分布", render() { const data = this.ctx.items || []; const countMap = {}; for (const item of data) { const date = toDateString(Number(item?.ctime || 0) * 1e3); countMap[date] = (countMap[date] || 0) + 1; } const sorted = Object.entries(countMap).sort((a, b) => String(a[0]).localeCompare(String(b[0]))); const x = sorted.map(([date]) => date); const y = sorted.map(([, count]) => count); const totalDays = x.length; const startIdx = Math.max(0, totalDays - 30); const hasDataZoom = totalDays > 1; this.instance.setOption({ title: { text: "发送日期分布" }, tooltip: {}, xAxis: { type: "category", data: x }, yAxis: { type: "value", name: "弹幕数量" }, dataZoom: hasDataZoom ? [ { type: "slider", startValue: startIdx, endValue: Math.max(0, totalDays - 1), xAxisIndex: 0, height: 20 } ] : [], series: [ { type: "bar", data: x.map((date, idx) => ({ value: y[idx], __selectionValue: date })), label: { show: true, position: "top" } } ] }); }, selection: { source: "chart:date", template: "日期 {value}", getValue: (params) => params?.data?.__selectionValue ?? params?.name, predicate: (item, value) => toDateString(Number(item?.ctime || 0) * 1e3) === String(value || "") } }; const hourChart = { key: "hour", title: "发送时间分布", render() { const data = this.ctx.items || []; const hours = new Array(24).fill(0); for (const item of data) { const hour = new Date(Number(item?.ctime || 0) * 1e3).getHours(); hours[hour] += 1; } this.instance.setOption({ title: { text: "发送时间分布" }, tooltip: {}, xAxis: { type: "category", data: hours.map((_, i) => `${i}时`) }, yAxis: { type: "value", name: "弹幕数量" }, series: [ { type: "bar", data: hours.map((count, hour) => ({ value: count, __selectionValue: hour })), label: { show: true, position: "top" } } ] }); }, selection: { source: "chart:hour", template: "每天 {value} 点", getValue: (params) => params?.data?.__selectionValue ?? Number.parseInt(params?.name, 10), predicate: (item, value) => new Date(Number(item?.ctime || 0) * 1e3).getHours() === Number(value) } }; const poolChart = { key: "pool", title: "弹幕池分布", getLabel(pool) { const numericPool = Number(pool); if (!Number.isFinite(numericPool)) return "_-未知池"; return `${numericPool}-${poolLabelMap[numericPool] ?? "未知池"}`; }, getMenuItems() { return [{ getName: (item) => `弹幕池:${this.getLabel(item?.pool)}` }]; }, selection: { source: "chart:pool", template: "弹幕池 {value}", formatValue: (value) => `${value}`, getValue: (params) => params?.data?.__selectionValue ?? params?.name, predicate: (item, value) => { const itemPool = Number(item?.pool); const selectedPool = Number(value); if (!Number.isFinite(selectedPool)) return !Number.isFinite(itemPool); return itemPool === selectedPool; } }, render() { const data = this.ctx.items || []; const poolMap = {}; for (const item of data) { const value = Number(item?.pool); const key = Number.isFinite(value) ? value : "_"; poolMap[key] = (poolMap[key] || 0) + 1; } const keys = Object.keys(poolMap).sort((a, b) => { if (a === "_") return 1; if (b === "_") return -1; return Number(a) - Number(b); }); const xData = keys.map((key) => this.getLabel(key)); const yData = keys.map((key) => poolMap[key]); this.instance.setOption({ title: { text: "弹幕池分布" }, tooltip: {}, xAxis: { type: "category", data: xData }, yAxis: { type: "value", name: "弹幕数量" }, series: [ { type: "bar", data: yData.map((count, idx) => ({ value: count, __selectionValue: keys[idx] })), label: { show: true, position: "top" } } ] }); } }; const weightChart = { key: "weight", title: "弹幕屏蔽等级分布", menuItems: [{ getName: (item) => `屏蔽等级:${item?.weight ?? "-"}` }], selection: { source: "chart:weight", template: "屏蔽等级 {value}", getValue: (params) => params?.data?.__selectionValue ?? params?.name, predicate: (item, value) => Number(item?.weight) === Number(value) }, render() { const data = this.ctx.items || []; const levelCount = {}; for (const item of data) { const level = Number(item?.weight); levelCount[level] = (levelCount[level] || 0) + 1; } const keys = Object.keys(levelCount).map((key) => Number(key)).sort((a, b) => a - b); const xData = keys; const yData = keys.map((key) => levelCount[key]); this.instance.setOption({ title: { text: "弹幕屏蔽等级分布" }, tooltip: {}, xAxis: { type: "category", data: xData }, yAxis: { type: "value", name: "弹幕数" }, series: [ { type: "bar", data: yData.map((count, idx) => ({ value: count, __selectionValue: keys[idx] })), label: { show: true, position: "top" } } ] }); } }; const modeChart = { key: "mode", title: "弹幕类型分布", getLabel(mode) { return `${mode}-${modeLabelMap[mode] || "未知类型"}`; }, getMenuItems() { return [{ getName: (item) => `类型:${this.getLabel(item?.mode)}` }]; }, selection: { source: "chart:mode", template: "类型 {value}", getValue: (params) => params?.data?.__selectionValue ?? params?.name, formatValue: (value) => `${value}-${modeLabelMap[value] || "未知类型"}`, predicate: (item, value) => Number(item?.mode) === Number(value) }, render() { const data = this.ctx.items || []; const countMap = {}; for (const item of data) { const key = Number(item?.mode); countMap[key] = (countMap[key] || 0) + 1; } const keys = Object.keys(countMap).map((key) => Number(key)).sort((a, b) => a - b); const xData = keys.map((key) => this.getLabel(key)); const yData = keys.map((key) => countMap[key]); this.instance.setOption({ title: { text: "弹幕类型分布" }, tooltip: {}, xAxis: { type: "category", data: xData }, yAxis: { type: "value", name: "弹幕数" }, series: [ { type: "bar", data: yData.map((count, idx) => ({ value: count, __selectionValue: keys[idx] })), label: { show: true, position: "top" } } ] }); } }; const attrChart = { key: "attr", title: "弹幕属性分布", refresh: true, chartMode: "bit", actions: [ { key: "toggle-mode", icon: "⇄", title: "切换统计方式", method: "toggleChartMode" } ], selection() { return { source: this.chartMode === "bit" ? "chart:attr:bit" : "chart:attr:value", template: this.chartMode === "bit" ? "弹幕属性 bit位 {value}" : "弹幕属性 {value}", getValue: (params) => params?.data?.__selectionValue ?? params?.name, formatValue: (value) => { const [kind, raw] = String(value || "").split(":"); if (kind === "bit") { if (raw === "-" || raw === "" || raw == null) return "bit:-"; return `bit:${raw}`; } if (kind === "attr") return `${raw} ${this.getAttrBits(raw).str}`; return String(value || ""); }, predicate: (item, value) => { const [kind, raw] = String(value || "").split(":"); const attr = Number(item?.attr ?? 0); if (kind === "bit") { const bit = Number(raw); if (!Number.isFinite(bit) || bit < 0) return attr === 0; return (attr & 1 << bit) !== 0; } if (kind === "attr") { return attr === Number(raw); } return false; } }; }, getMenuItems() { return [{ getName: (item) => `属性:${this.getAttrBits(item?.attr).str}` }]; }, getAttrBits(attr) { const value = Number(attr); if (!Number.isInteger(value) || value === 0) return { str: "bit:-", bits: [] }; const bits = []; for (let i = 0; i < 32; i += 1) { if ((value & 1 << i) !== 0) bits.push(i); } return bits.length ? { str: `bit:${bits.join("|")}`, bits } : { str: "bit:-", bits: [] }; }, toggleChartMode() { this.chartMode = this.chartMode === "attr" ? "bit" : "attr"; if (this.instance?.clear) this.instance.clear(); this.ctx.rerender(); }, render() { const data = this.ctx.items || []; if (this.chartMode === "bit") { const bitCount = Array(32).fill(0); let zeroBitCount = 0; for (const item of data) { const bits = this.getAttrBits(item?.attr).bits; if (!bits.length) { zeroBitCount += 1; } else { for (const bit of bits) bitCount[bit] += 1; } } const labels2 = []; const counts2 = []; const selectionValues = []; if (zeroBitCount > 0) { labels2.push("-"); counts2.push(zeroBitCount); selectionValues.push("bit:-"); } for (let i = 0; i < 32; i += 1) { if (bitCount[i] <= 0) continue; labels2.push(String(i)); counts2.push(bitCount[i]); selectionValues.push(`bit:${i}`); } this.instance.setOption({ title: { text: "弹幕属性 bit位分布" }, tooltip: {}, xAxis: { type: "category", data: labels2, name: "bit位" }, yAxis: { type: "value", name: "出现次数" }, series: [ { type: "bar", data: counts2.map((count, idx) => ({ value: count, __selectionValue: selectionValues[idx] })), label: { show: true, position: "top" } } ] }); return; } const attrCount = {}; for (const item of data) { const attr = Number(item?.attr ?? 0); attrCount[attr] = (attrCount[attr] || 0) + 1; } const labels = Object.keys(attrCount).map((key) => Number(key)).sort((a, b) => a - b); const counts = labels.map((label) => attrCount[label]); const total = Math.max(1, counts.reduce((sum, count) => sum + count, 0)); const percentages = counts.map((count) => (count / total * 100).toFixed(2)); this.instance.setOption({ title: { text: "弹幕属性分布" }, tooltip: { trigger: "item", formatter: (params) => { const attr = Number(params?.name); const idx = labels.indexOf(attr); return `属性值:${attr}
数量:${params.value}
占比:${percentages[idx]}%
位说明:${this.getAttrBits(attr).str}`; } }, legend: { bottom: "bottom" }, series: [ { type: "pie", radius: "50%", data: labels.map((label, idx) => ({ name: String(label), value: counts[idx], __selectionValue: `attr:${label}` })), label: { formatter: (params) => `${params.name} ${percentages[params.dataIndex]}%` } } ] }); } }; const colorCustomChartExample = `({ name: 'colorTreemapExample', title: '弹幕颜色分布', excludeWhite: true, actions: [{ key: 'toggle-exclude-white', icon: '⬜', title: '排除白色', method: 'toggleExcludeWhite' }], selection: { source: 'chart:colorExample', template: '颜色 {value}', getValue: (params) => params?.data?.__selectionValue, formatValue(value) { const hex = '#' + ((Number(value) >>> 0) & 0xffffff).toString(16).padStart(6, '0').toUpperCase(); return this.ctx.h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '2px' } }, [ this.ctx.h('span', { style: { width: '10px', height: '10px', borderRadius: '2px', border: '1px solid #00000033', background: hex, display: 'inline-block' } }), this.ctx.h('span', null, hex) ]); }, predicate: (item, value) => Number(item?.color) === Number(value) }, toggleExcludeWhite() { this.excludeWhite = !this.excludeWhite; this.ctx.rerender(); }, render() { const MAX = 50; const toHex = (v) => '#' + ((Number(v) >>> 0) & 0xffffff).toString(16).padStart(6, '0').toUpperCase(); const textColor = (hex) => { const h = String(hex).replace('#', ''); const r = parseInt(h.slice(0, 2), 16); const g = parseInt(h.slice(2, 4), 16); const b = parseInt(h.slice(4, 6), 16); return (0.299 * r + 0.587 * g + 0.114 * b) >= 165 ? '#111111' : '#ffffff'; }; const data = this.ctx.items || []; const map = new Map(); for (const item of data) { const color = Number(item?.color); if (!Number.isFinite(color)) continue; if (this.excludeWhite && (((color >>> 0) & 0xffffff) === 0xffffff)) continue; map.set(color, (map.get(color) || 0) + 1); } const sorted = [...map.entries()] .map(([color, count]) => ({ color, count, hex: toHex(color) })) .sort((a, b) => b.count - a.count); const top = sorted.slice(0, MAX); const rest = sorted.slice(MAX).reduce((sum, item) => sum + item.count, 0); const total = Math.max(1, data.length); const treeData = top.map((item) => ({ name: item.hex, value: item.count, __selectionValue: item.color, itemStyle: { color: item.hex, borderColor: '#ffffff66', borderWidth: 1 }, label: { color: textColor(item.hex) } })); if (rest > 0) { treeData.push({ name: '其他', value: rest, __selectionValue: null, itemStyle: { color: '#8c8c8c', borderColor: '#ffffff66', borderWidth: 1 }, label: { color: '#ffffff' } }); } this.instance.setOption({ title: { text: '弹幕颜色分布' }, tooltip: { formatter: (params) => { const item = params?.data; if (!item) return ''; const count = Number(item.value || 0); const ratio = ((count / total) * 100).toFixed(2); if (item.name === '其他') { return '颜色:其他
次数:' + count.toLocaleString() + '
占比:' + ratio + '%'; } return '颜色:' + item.name + '
次数:' + count.toLocaleString() + '
占比:' + ratio + '%'; } }, series: [{ type: 'treemap', roam: false, nodeClick: false, leafDepth: 1, breadcrumb: { show: false }, label: { show: true, formatter: (params) => params?.data?.name || '' }, upperLabel: { show: false }, data: treeData }] }); } })`; const defaultCharts = [ userChart, wordcloudChart, densityChart, dateChart, hourChart, poolChart, weightChart, modeChart, attrChart ]; const REF_X = 95.047; const REF_Y = 100; const REF_Z = 108.883; const srgbToLinear = (channel) => { const value = channel / 255; if (value <= 0.04045) return value / 12.92; return ((value + 0.055) / 1.055) ** 2.4; }; const rgbToXyz = (r, g, b) => { const rl = srgbToLinear(r); const gl = srgbToLinear(g); const bl = srgbToLinear(b); const x = (rl * 0.4124564 + gl * 0.3575761 + bl * 0.1804375) * 100; const y = (rl * 0.2126729 + gl * 0.7151522 + bl * 0.072175) * 100; const z = (rl * 0.0193339 + gl * 0.119192 + bl * 0.9503041) * 100; return [x, y, z]; }; const xyzPivot = (value) => { const v = value / 100; if (v > 8856e-6) return Math.cbrt(v); return 7.787 * v + 16 / 116; }; const xyzToLab = (x, y, z) => { const fx = xyzPivot(x / REF_X * 100); const fy = xyzPivot(y / REF_Y * 100); const fz = xyzPivot(z / REF_Z * 100); const l = 116 * fy - 16; const a = 500 * (fx - fy); const b = 200 * (fy - fz); return [l, a, b]; }; const colorToLab = (color) => { const [r, g, b] = rgba(String(color || "")); const [x, y, z] = rgbToXyz(r, g, b); return xyzToLab(x, y, z); }; const deltaE = (colorA, colorB) => { const [l1, a1, b1] = colorToLab(colorA); const [l2, a2, b2] = colorToLab(colorB); const dl = l1 - l2; const da = a1 - a2; const db = b1 - b2; return Math.sqrt(dl * dl + da * da + db * db); }; const _hoisted_1$5 = { class: "bds-dm-chart-manager" }; const _hoisted_2$3 = ["onMouseenter"]; const _hoisted_3$2 = { key: 0, class: "bds-dm-chart-manager__actions" }; const _hoisted_4$2 = ["title", "disabled", "onClick"]; const _sfc_main$8 = { __name: "DmChartManager", props: { items: { type: Array, default: () => [] }, chartCtx: { type: Object, default: () => ({}) }, to: { type: [String, Object], default: void 0 } }, emits: ["select-filter", "update:chartMenus"], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emit = __emit; const styleMountTarget = vue.inject("styleMountTarget", null); const runtimeWindow = vue.inject("runtimeWindow", window); const injectedActiveTheme = vue.inject("activeTheme", null); const themeSettings = vue.inject("themeSettings", null); const messageApi = naiveUi__namespace.useMessage(); mountStyle$6(styleMountTarget); const echartsLib = vue.computed(() => runtimeWindow?.echarts || globalThis.echarts); const chartDefs = vue.ref([]); const chartDefMap = vue.computed(() => new Map(chartDefs.value.map((def) => [def.key, def]))); const chartTestRef = vue.ref(null); const chartColors = vue.ref([]); const visibleChartKeys = vue.ref([]); const chartDomMap = vue.reactive({}); const expandedByKey = vue.reactive({}); const chartHover = vue.ref(null); const chartRuntimeMap = new Map(); const chartInstanceMap = new Map(); const chartRenderTaskMap = new Map(); const chartSizeMap = new Map(); const chartBodyRef = vue.ref(null); let renderQueue = Promise.resolve(); const echartsTheme = vue.computed(() => { const value = vue.unref(injectedActiveTheme); return value === "dark" ? "dark" : "default"; }); const applyToCharts = vue.computed(() => Boolean(themeSettings?.applyToCharts?.value)); const activePrimaryColor = vue.computed(() => { return String(themeSettings?.activePrimary?.value || "").trim(); }); const newChartCode = vue.ref(""); const customChartCodeMap = vue.ref({}); const customCharts = vue.computed(() => { return chartDefs.value.filter((item) => item.isCustom).map((item) => ({ key: item.key, title: item.title || item.key })); }); const hasChart = vue.computed(() => visibleChartKeys.value.length > 0); const sanitizeChartName = (name) => { return String(name || Date.now()).trim().replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_\-]/g, "").slice(0, 80); }; const logError = (...args) => { props.chartCtx?.BDM?.logger?.error?.(...args); }; const ensureChartColors = () => { if (chartColors.value.length) return chartColors.value; if (!echartsLib.value?.init || !chartTestRef.value) return []; let chartTest = null; try { chartTest = echartsLib.value.init(chartTestRef.value, echartsTheme.value); chartTest.setOption({}); const colors2 = chartTest.getOption()?.color; const palette = Array.isArray(colors2) ? colors2.filter(Boolean) : []; if (!palette.length) return []; const primary = activePrimaryColor.value; if (!primary) { chartColors.value = palette; return chartColors.value; } let nearestIdx = 0; let nearestDistance = Number.POSITIVE_INFINITY; for (let i = 0; i < palette.length; i += 1) { try { const distance = deltaE(palette[i], primary); if (distance < nearestDistance) { nearestDistance = distance; nearestIdx = i; } } catch { } } const reordered = [primary, ...palette.filter((_, idx) => idx !== nearestIdx)]; chartColors.value = reordered; return reordered; } catch (error) { logError("提取图表主题色失败", error); return []; } finally { if (chartTest?.dispose) chartTest.dispose(); } }; const buildFilterPayload = (chartKey2, filterInput = {}) => { const value = filterInput.value; if (value == null || value === "") return null; if (typeof filterInput.predicate !== "function") return null; return { source: filterInput.source || `chart:${chartKey2}`, value, template: filterInput.template || "{value}", formatValue: filterInput.formatValue || ((v) => String(v)), predicate: filterInput.predicate, wrapTag: filterInput.wrapTag !== false }; }; const stageChartFilter = async (chartKey2, filterInput) => { const payload = buildFilterPayload(chartKey2, filterInput); if (payload) emit("select-filter", payload); }; const createChartContext = (chartKey2) => { const localContext = { chartKey: chartKey2, get echarts() { return echartsLib.value; }, get instance() { return chartInstanceMap.get(chartKey2) || null; }, get element() { return chartDomMap[chartKey2] || null; }, get items() { return props.items; }, h: vue.h, ui: naiveUi__namespace, stageFilter: (filterInput) => stageChartFilter(chartKey2, filterInput), rerender: () => renderChart(chartKey2), resize: () => resizeChart(chartKey2) }; return new Proxy(localContext, { get(target, key, receiver) { if (Reflect.has(target, key)) { return Reflect.get(target, key, receiver); } const extra = props.chartCtx || {}; return Reflect.get(extra, key); }, has(target, key) { if (Reflect.has(target, key)) return true; const extra = props.chartCtx || {}; return Reflect.has(extra, key); } }); }; const ensureRuntime = (chartKey2) => { const cached = chartRuntimeMap.get(chartKey2); if (cached) return cached; const def = chartDefMap.value.get(chartKey2); if (!def) return null; const runtime = { ...def }; runtime.key = runtime.key || def.key || chartKey2; runtime.title = runtime.title || def.title || chartKey2; runtime.actions = Array.isArray(runtime.actions) ? runtime.actions : []; const hasExpandedControl = Object.prototype.hasOwnProperty.call(def, "expandedH"); if (hasExpandedControl) { if (!(chartKey2 in expandedByKey)) { expandedByKey[chartKey2] = Boolean(def.expandedH); } runtime.expandedH = Boolean(expandedByKey[chartKey2]); } runtime.ctx = createChartContext(chartKey2); chartRuntimeMap.set(chartKey2, runtime); return runtime; }; const disposeChart = (chartKey2) => { const runtime = chartRuntimeMap.get(chartKey2); if (runtime && typeof runtime.dispose === "function") { try { runtime.dispose(); } catch (error) { logError("图表销毁钩子异常", chartKey2, error); } } const instance = chartInstanceMap.get(chartKey2); if (instance && typeof instance.dispose === "function") { instance.dispose(); } chartInstanceMap.delete(chartKey2); chartSizeMap.delete(chartKey2); if (runtime) runtime.instance = null; }; const disposeAllCharts = () => { for (const key of chartInstanceMap.keys()) { disposeChart(key); } }; const resizeChart = (chartKey2) => { const runtime = chartRuntimeMap.get(chartKey2); if (runtime && typeof runtime.resize === "function") { try { const handled = runtime.resize(); if (handled !== false) { return; } } catch (error) { logError("图表尺寸钩子异常", chartKey2, error); } } const instance = chartInstanceMap.get(chartKey2); if (!instance?.resize) return; instance.resize(); }; const getChartSizeKey = (chartKey2) => { const el = chartDomMap[chartKey2]; if (!el || typeof el.getBoundingClientRect !== "function") return null; const rect = el.getBoundingClientRect(); return `${Math.round(rect.width || 0)}x${Math.round(rect.height || 0)}`; }; const removeCustomChart = (chartKey2) => { const idx = chartDefs.value.findIndex((item) => item.key === chartKey2 && item.isCustom); if (idx < 0) return; chartDefs.value.splice(idx, 1); const visibleIdx = visibleChartKeys.value.indexOf(chartKey2); if (visibleIdx >= 0) visibleChartKeys.value.splice(visibleIdx, 1); disposeChart(chartKey2); chartRuntimeMap.delete(chartKey2); delete expandedByKey[chartKey2]; const customStore = storage.get("charts.custom", {}); if (customStore && customStore[chartKey2]) { delete customStore[chartKey2]; storage.set("charts.custom", customStore); } if (customChartCodeMap.value[chartKey2]) { delete customChartCodeMap.value[chartKey2]; customChartCodeMap.value = { ...customChartCodeMap.value }; } storage.set("charts.visible", visibleChartKeys.value); emitChartMenus(); }; const normalizeChartMenuItem = (chartKey2, runtime, rawMenu) => { if (!rawMenu || typeof rawMenu !== "object") return null; return { ...rawMenu, chart: chartKey2, getName: typeof rawMenu.getName === "function" ? (item) => rawMenu.getName.call(runtime, item) : rawMenu.getName, onSelect: typeof rawMenu.onSelect === "function" ? (item) => rawMenu.onSelect.call(runtime, item) : rawMenu.onSelect }; }; const buildChartMenus = () => { const menus = []; for (const chartKey2 of visibleChartKeys.value) { const runtime = ensureRuntime(chartKey2); if (!runtime) continue; const staticMenus = Array.isArray(runtime.menuItems) ? runtime.menuItems : []; const dynamicMenus = typeof runtime.getMenuItems === "function" ? runtime.getMenuItems.call(runtime) : []; for (const menu of [...staticMenus, ...Array.isArray(dynamicMenus) ? dynamicMenus : []]) { const normalized = normalizeChartMenuItem(chartKey2, runtime, menu); if (normalized) menus.push(normalized); } } return menus; }; const emitChartMenus = () => { emit("update:chartMenus", buildChartMenus()); }; const parseCustomChartCode = (code) => { const chartDef = eval(`(${code})`); if (!chartDef || typeof chartDef !== "object") { throw new Error("图表代码不是对象"); } if (typeof chartDef.render !== "function") { throw new Error("图表缺少 render 方法"); } const baseName = sanitizeChartName(chartDef.name || chartDef.key || `chart_${Date.now()}`); if (!baseName) { throw new Error("图表名称无效"); } const chartKey = baseName.startsWith("custom_") ? baseName : `custom_${baseName}`; return { ...chartDef, key: chartKey, title: chartDef.title || chartKey, actions: Array.isArray(chartDef.actions) ? chartDef.actions : [], instance: null, ctx: null, isCustom: true }; }; const addCustomChart = () => { const code2 = String(newChartCode.value || "").trim(); if (!code2) return ""; try { const def = parseCustomChartCode(code2); const existingIndex = chartDefs.value.findIndex((item) => item.key === def.key); if (existingIndex >= 0) { const existing = chartDefs.value[existingIndex]; if (!existing?.isCustom) { throw new Error(`不能覆盖默认图表:${def.key}`); } disposeChart(def.key); chartRuntimeMap.delete(def.key); chartDefs.value.splice(existingIndex, 1, def); } else { chartDefs.value.push(def); if (!visibleChartKeys.value.includes(def.key)) { visibleChartKeys.value.push(def.key); } } const customStore = storage.get("charts.custom", {}); customStore[def.key] = code2; storage.set("charts.custom", customStore); customChartCodeMap.value = { ...customChartCodeMap.value, [def.key]: code2 }; storage.set("charts.visible", visibleChartKeys.value); emitChartMenus(); vue.nextTick(() => renderChart(def.key)); return def.key; } catch (error) { const message = String(error?.message || error || "未知错误"); messageApi.error(`添加失败:${message}`); logError(error); return ""; } }; const initCharts = () => { customChartCodeMap.value = {}; chartDefs.value = defaultCharts.map((def) => ({ ...def, isCustom: false })); const customStore = storage.get("charts.custom", {}); Object.entries(customStore || {}).forEach(([key, code2]) => { try { const parsed = parseCustomChartCode(code2); if (parsed.key !== key) parsed.key = key; parsed.title = parsed.title || key; chartDefs.value.push(parsed); customChartCodeMap.value[key] = String(code2 || ""); } catch (error) { const message = String(error?.message || error || "未知错误"); messageApi.error(`图表 ${key} 加载失败:${message}`); logError("加载自定义图表失败", key, error); } }); const storedVisible = storage.get("charts.visible", null); const allKeys = chartDefs.value.map((d) => d.key); if (Array.isArray(storedVisible) && storedVisible.length) { visibleChartKeys.value = storedVisible.filter((key) => allKeys.includes(key)); } else { const preferred = ["user", "wordcloud", "density"]; visibleChartKeys.value = preferred.filter((key) => allKeys.includes(key)); } if (!visibleChartKeys.value.length && allKeys.length) { visibleChartKeys.value = [allKeys[0]]; } emitChartMenus(); }; const resolveSelection = (def, runtime, params) => { const raw = typeof def.selection === "function" ? def.selection.call(runtime, params) : def.selection; if (!raw || typeof raw !== "object") return null; return raw; }; const runChartClick = async (chartKey2, params) => { const def = chartDefMap.value.get(chartKey2); const runtime = ensureRuntime(chartKey2); if (!def || !runtime) return; if (typeof def.onClick === "function") { runtime.instance = chartInstanceMap.get(chartKey2); await Promise.resolve(def.onClick.call(runtime, { params })); return; } const selection = resolveSelection(def, runtime, params); if (selection?.predicate) { const getValue = typeof selection.getValue === "function" ? (p) => selection.getValue.call(runtime, p) : (p) => p?.name; const value = getValue(params); if (value == null || value === "") return; const formatValue = typeof selection.formatValue === "function" ? (v) => selection.formatValue.call(runtime, v) : (v) => String(v); const predicate = (item, v) => selection.predicate.call(runtime, item, v); emit("select-filter", { source: selection.source || `chart:${chartKey2}`, template: selection.template || "{value}", value, formatValue, predicate, wrapTag: selection.wrapTag !== false }); } }; const renderChart = async (chartKey2) => { const pending = chartRenderTaskMap.get(chartKey2); await vue.nextTick(); if (pending) { await pending; } const task = (async () => { if (!echartsLib.value) return; const def = chartDefMap.value.get(chartKey2); const runtime = ensureRuntime(chartKey2); const el = chartDomMap[chartKey2]; if (!def || !el || !runtime) return; try { if (typeof runtime.init === "function") { await Promise.resolve(runtime.init.call(runtime)); } let instance = chartInstanceMap.get(chartKey2); if (!instance && !runtime.noInstance) { instance = echartsLib.value.init(el, echartsTheme.value); chartInstanceMap.set(chartKey2, instance); } if (instance) { instance.off("click"); if (typeof def.onClick === "function" || typeof def.selection === "function" || def.selection?.predicate) { instance.on("click", (params) => { runChartClick(chartKey2, params).catch((error) => logError(error)); }); } const baseOption = { title: { left: "left", top: "top" } }; if (applyToCharts.value) { const colors2 = ensureChartColors(); if (colors2.length) baseOption.color = colors2; } instance.setOption(baseOption); } runtime.instance = instance; await Promise.resolve(runtime.render.call(runtime)); const sizeKey = getChartSizeKey(chartKey2); if (sizeKey) chartSizeMap.set(chartKey2, sizeKey); } catch (error) { logError("图表渲染异常", chartKey2, error); } })(); chartRenderTaskMap.set(chartKey2, task); try { await vue.nextTick(); await task; } finally { if (chartRenderTaskMap.get(chartKey2) === task) { chartRenderTaskMap.delete(chartKey2); } } }; const enqueueRender = (task) => { renderQueue = renderQueue.then(() => task()).catch((error) => { logError("图表渲染队列异常", error); }); return renderQueue; }; const renderAllCharts = async () => { await vue.nextTick(); for (const key of visibleChartKeys.value) { try { await renderChart(key); } catch (error) { logError("图表渲染失败", key, error); } } }; const moveUp = async (chartKey2) => { const current = visibleChartKeys.value; const index = current.indexOf(chartKey2); if (index <= 0) return; const next = [...current]; [next[index - 1], next[index]] = [next[index], next[index - 1]]; visibleChartKeys.value = next; storage.set("charts.visible", visibleChartKeys.value); emitChartMenus(); await vue.nextTick(); const el = chartDomMap[chartKey2]; if (el?.scrollIntoView) { el.scrollIntoView({ behavior: "smooth", block: "nearest" }); } }; const moveDown = async (chartKey2) => { const current = visibleChartKeys.value; const index = current.indexOf(chartKey2); if (index < 0 || index >= current.length - 1) return; const next = [...current]; [next[index], next[index + 1]] = [next[index + 1], next[index]]; visibleChartKeys.value = next; storage.set("charts.visible", visibleChartKeys.value); emitChartMenus(); await vue.nextTick(); const el = chartDomMap[chartKey2]; if (el?.scrollIntoView) { el.scrollIntoView({ behavior: "smooth", block: "center" }); } }; const closeChart = (chartKey2) => { const index = visibleChartKeys.value.indexOf(chartKey2); if (index < 0) return; visibleChartKeys.value.splice(index, 1); storage.set("charts.visible", visibleChartKeys.value); emitChartMenus(); disposeChart(chartKey2); }; const toggleExpand = async (chartKey2) => { const runtime = ensureRuntime(chartKey2); if (!runtime || !("expandedH" in runtime)) return; expandedByKey[chartKey2] = !Boolean(expandedByKey[chartKey2]); runtime.expandedH = Boolean(expandedByKey[chartKey2]); await vue.nextTick(); const nextSize = getChartSizeKey(chartKey2); if (nextSize) chartSizeMap.set(chartKey2, nextSize); resizeChart(chartKey2); }; const getItemHeight = (chartKey2) => { ensureRuntime(chartKey2); return Boolean(expandedByKey[chartKey2]) ? "100%" : "50%"; }; const runCustomAction = async (chartKey2, action) => { const runtime = ensureRuntime(chartKey2); if (!runtime) return; runtime.instance = chartInstanceMap.get(chartKey2); if (typeof action.handler === "function") { await Promise.resolve(action.handler.call(runtime)); return; } if (action.method && typeof runtime[action.method] === "function") { await Promise.resolve(runtime[action.method]()); } }; const refreshChart = async (chartKey2) => { disposeChart(chartKey2); await vue.nextTick(); await renderChart(chartKey2); }; const getActionList = (chartKey2, index) => { const runtime = ensureRuntime(chartKey2); const list = []; list.push({ key: "remove", icon: "⨉", title: "移除图表", handler: () => closeChart(chartKey2) }); list.push({ key: "move-down", icon: "▼", title: "下移图表", disabled: index >= visibleChartKeys.value.length - 1, handler: () => moveDown(chartKey2) }); list.push({ key: "move-up", icon: "▲", title: "上移图表", disabled: index === 0, handler: () => moveUp(chartKey2) }); if (runtime && "refresh" in runtime) { list.push({ key: "refresh", icon: "↻", title: "刷新图表", handler: () => refreshChart(chartKey2) }); } if (runtime && "expandedH" in runtime) { list.push({ key: "expand", icon: "⇕", title: "展开/收起", handler: () => toggleExpand(chartKey2) }); } const customActions = runtime?.actions || []; for (const action of customActions) { const icon = typeof action.icon === "function" ? action.icon(runtime) : action.icon || "•"; list.push({ key: `custom-${action.key || action.method}`, icon, title: action.title || action.key || action.method, handler: () => runCustomAction(chartKey2, action) }); } return list; }; const chartResize = () => { for (const key of visibleChartKeys.value) { const nextSize = getChartSizeKey(key); if (!nextSize) continue; if (chartSizeMap.get(key) === nextSize) continue; chartSizeMap.set(key, nextSize); resizeChart(key); } }; vue.watch( () => props.items, () => { enqueueRender(renderAllCharts); } ); vue.watch( () => echartsTheme.value, () => { chartColors.value = []; for (const instance of chartInstanceMap.values()) { if (!instance || typeof instance.setTheme !== "function") continue; try { instance.setTheme(echartsTheme.value); } catch (error) { logError("图表切换主题失败", error); } } enqueueRender(renderAllCharts); } ); vue.watch( () => [applyToCharts.value, activePrimaryColor.value], ([nextApplyToCharts], [prevApplyToCharts]) => { chartColors.value = []; if (prevApplyToCharts && !nextApplyToCharts) { disposeAllCharts(); } enqueueRender(renderAllCharts); } ); vue.watch( () => visibleChartKeys.value.slice(), (current, prev = []) => { enqueueRender(async () => { const prevSet = new Set(prev); const currentSet = new Set(current); await vue.nextTick(); for (const key of prev) { if (!currentSet.has(key)) disposeChart(key); } for (const key of current) { if (!prevSet.has(key)) { try { await renderChart(key); } catch (error) { logError("新增图表渲染失败", key, error); } } } storage.set("charts.visible", current); emitChartMenus(); }); }, { deep: false } ); vue.watch( () => chartDefs.value.map((item) => item.key).join(","), () => { const validSet = new Set(chartDefs.value.map((item) => item.key)); visibleChartKeys.value = visibleChartKeys.value.filter((key) => validSet.has(key)); emitChartMenus(); } ); vue.onMounted(async () => { initCharts(); window.addEventListener("resize", chartResize); }); vue.onBeforeUnmount(() => { window.removeEventListener("resize", chartResize); disposeAllCharts(); }); __expose({ chartResize, chartBodyEl: chartBodyRef, settings: { chartDefs, customCharts, visibleChartKeys, newChartCode, addCustomChart, removeCustomChart, getCustomChartCode: (chartKey2) => customChartCodeMap.value[chartKey2] || "" } }); return (_ctx, _cache) => { const _component_n_alert = naiveUi.NAlert; const _component_n_empty = naiveUi.NEmpty; return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$5, [ !vue.unref(echartsLib) ? (vue.openBlock(), vue.createBlock(_component_n_alert, { key: 0, type: "error", title: "ECharts 未加载,无法渲染图表" })) : vue.createCommentVNode("", true), !vue.unref(hasChart) ? (vue.openBlock(), vue.createBlock(_component_n_empty, { key: 1, description: "暂无已启用图表", class: "bds-dm-chart-manager__empty" })) : vue.createCommentVNode("", true), vue.createElementVNode("div", { style: { "display": "none" }, ref_key: "chartTestRef", ref: chartTestRef }, null, 512), vue.createElementVNode("div", { ref_key: "chartBodyRef", ref: chartBodyRef, class: "bds-dm-chart-manager__body" }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(visibleChartKeys), (chartKey2, index) => { return vue.openBlock(), vue.createElementBlock("div", { key: chartKey2, class: "bds-dm-chart-manager__item", style: vue.normalizeStyle({ "--item-height": getItemHeight(chartKey2) }), onMouseenter: ($event) => chartHover.value = chartKey2, onMouseleave: _cache[0] || (_cache[0] = ($event) => chartHover.value = null) }, [ vue.unref(chartHover) === chartKey2 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_3$2, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(getActionList(chartKey2, index), (action) => { return vue.openBlock(), vue.createElementBlock("button", { key: `${chartKey2}:${action.key}`, class: "bds-dm-chart-manager__action-btn", type: "button", title: action.title, disabled: action.disabled, onClick: action.handler }, vue.toDisplayString(action.icon), 9, _hoisted_4$2); }), 128)), _cache[1] || (_cache[1] = vue.createElementVNode("span", null, null, -1)) ])) : vue.createCommentVNode("", true), vue.createElementVNode("div", { ref_for: true, ref: (el) => vue.unref(chartDomMap)[chartKey2] = el, class: "bds-dm-chart-manager__chart" }, null, 512) ], 44, _hoisted_2$3); }), 128)) ], 512) ]); }; } }; const style$5 = cB$2( "bds-chart-settings", css$1` display: flex; flex-direction: column; gap: 12px; `, [ cE$2( "block", css$1` display: flex; flex-direction: column; gap: 10px; ` ), cE$2( "custom-row", css$1` display: flex; align-items: center; justify-content: space-between; gap: 8px; ` ) ] ); function mountStyle$5(mountTarget) { useTheme$1("bds-chart-settings-style", style$5, mountTarget); } const _hoisted_1$4 = { class: "bds-chart-settings" }; const _hoisted_2$2 = { class: "bds-chart-settings__block" }; const _hoisted_3$1 = { class: "bds-chart-settings__block" }; const _hoisted_4$1 = { class: "bds-chart-settings__custom-row" }; const _sfc_main$7 = { __name: "ChartSettingsSection", props: { settings: { type: Object, default: null } }, setup(__props2) { const props2 = __props2; const styleMountTarget2 = vue.inject("styleMountTarget", null); mountStyle$5(styleMountTarget2); const settings = vue.computed(() => props2.settings || null); const chartDefs2 = vue.computed(() => settings.value?.chartDefs?.value || []); const customCharts2 = vue.computed(() => settings.value?.customCharts?.value || []); const transferOptions = vue.computed(() => { return chartDefs2.value.map((item) => ({ label: item.title || item.key, value: item.key })); }); const activeCustomKey = vue.ref(""); const visibleChartKeysModel = vue.computed({ get: () => settings.value?.visibleChartKeys?.value || [], set: (value) => { if (!settings.value?.visibleChartKeys) return; settings.value.visibleChartKeys.value = Array.isArray(value) ? value : []; } }); const newChartCodeModel = vue.computed({ get: () => String(settings.value?.newChartCode?.value || ""), set: (value) => { if (!settings.value?.newChartCode) return; settings.value.newChartCode.value = String(value || ""); } }); const removeCustomChart2 = (chartKey2) => { settings.value?.removeCustomChart?.(chartKey2); if (activeCustomKey.value === chartKey2) { activeCustomKey.value = ""; } }; const addCustomChart2 = () => { const key = settings.value?.addCustomChart?.(); if (key) activeCustomKey.value = key; }; const startCreateCustomChart = () => { activeCustomKey.value = ""; if (!settings.value?.newChartCode) return; settings.value.newChartCode.value = colorCustomChartExample; }; const selectCustomChart = (chartKey2) => { activeCustomKey.value = chartKey2; const code2 = settings.value?.getCustomChartCode?.(chartKey2); if (!settings.value?.newChartCode) return; settings.value.newChartCode.value = String(code2 || ""); }; vue.onMounted(() => { startCreateCustomChart(); }); vue.watch(customCharts2, (items) => { if (!activeCustomKey.value) return; if (items.some((item) => item.key === activeCustomKey.value)) return; activeCustomKey.value = ""; }); return (_ctx, _cache) => { const _component_n_divider = naiveUi.NDivider; const _component_n_transfer = naiveUi.NTransfer; const _component_n_input = naiveUi.NInput; const _component_n_gi = naiveUi.NGi; const _component_n_button = naiveUi.NButton; const _component_n_flex = naiveUi.NFlex; const _component_n_empty = naiveUi.NEmpty; const _component_n_icon = naiveUi.NIcon; const _component_n_list_item = naiveUi.NListItem; const _component_n_list = naiveUi.NList; const _component_n_grid = naiveUi.NGrid; return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$4, [ vue.createElementVNode("div", _hoisted_2$2, [ vue.createVNode(_component_n_divider, { style: { "margin": "4px 0" } }, { default: vue.withCtx(() => [..._cache[2] || (_cache[2] = [ vue.createTextVNode("可见图表", -1) ])]), _: 1 }), vue.createVNode(_component_n_transfer, { value: vue.unref(visibleChartKeysModel), "onUpdate:value": _cache[0] || (_cache[0] = ($event) => vue.isRef(visibleChartKeysModel) ? visibleChartKeysModel.value = $event : null), options: vue.unref(transferOptions), "source-title": "全部图表", "target-title": "已启用图表" }, null, 8, ["value", "options"]) ]), vue.createElementVNode("div", _hoisted_3$1, [ vue.createVNode(_component_n_divider, { style: { "margin": "4px 0" } }, { default: vue.withCtx(() => [..._cache[3] || (_cache[3] = [ vue.createTextVNode("自定义图表", -1) ])]), _: 1 }), vue.createVNode(_component_n_grid, { cols: 24, "x-gap": 12, "y-gap": 12 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_gi, { span: 16 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_input, { value: vue.unref(newChartCodeModel), "onUpdate:value": _cache[1] || (_cache[1] = ($event) => vue.isRef(newChartCodeModel) ? newChartCodeModel.value = $event : null), type: "textarea", rows: 13, placeholder: "输入图表对象代码,如 ({ name:'demo', title:'示例', expandedH:false, refresh:true, render(){ ... } })" }, null, 8, ["value"]) ]), _: 1 }), vue.createVNode(_component_n_gi, { span: 8 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { vertical: "", align: "center" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { style: { "margin-bottom": "12px", "width": "100%" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { type: "primary", size: "small", style: { "flex": "1" }, onClick: startCreateCustomChart }, { default: vue.withCtx(() => [..._cache[4] || (_cache[4] = [ vue.createTextVNode(" 新建 ", -1) ])]), _: 1 }), vue.createVNode(_component_n_button, { type: "primary", size: "small", style: { "flex": "1" }, onClick: addCustomChart2 }, { default: vue.withCtx(() => [..._cache[5] || (_cache[5] = [ vue.createTextVNode(" 保存 ", -1) ])]), _: 1 }) ]), _: 1 }), !vue.unref(customCharts2).length ? (vue.openBlock(), vue.createBlock(_component_n_empty, { key: 0, description: "暂无自定义图表", size: "small" })) : (vue.openBlock(), vue.createBlock(_component_n_list, { key: 1, hoverable: "", clickable: "", style: { "width": "100%" } }, { default: vue.withCtx(() => [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(customCharts2), (item) => { return vue.openBlock(), vue.createBlock(_component_n_list_item, { key: item.key, onClick: ($event) => selectCustomChart(item.key) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_4$1, [ vue.createVNode(_component_n_button, { text: "", type: item.key === vue.unref(activeCustomKey) ? "primary" : "default" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(item.title || item.key), 1) ]), _: 2 }, 1032, ["type"]), vue.createVNode(_component_n_button, { size: "tiny", tertiary: "", type: "error", title: "删除图表", "aria-label": "删除图表", onClick: vue.withModifiers(($event) => removeCustomChart2(item.key), ["stop"]) }, { icon: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(Trash)) ]), _: 1 }) ]), _: 1 }, 8, ["onClick"]) ]) ]), _: 2 }, 1032, ["onClick"]); }), 128)) ]), _: 1 })) ]), _: 1 }) ]), _: 1 }) ]), _: 1 }) ]) ]); }; } }; const EXTERNAL_PANEL_SELECTED_KEY$1 = "external.panelUrl.selected"; const EXTERNAL_PANEL_CUSTOM_LIST_KEY = "external.panelUrl.customList"; const _sfc_main$6 = { __name: "ExternalPageSettingsSection", setup(__props2) { const DEFAULT_EXTERNAL_PANEL_URL_LIST = ["https://zbpine.github.io/bili-data-statistic/", "https://bili-data-statistic.edgeone.run/cn/"]; const normalizeText = (value) => String(value || "").trim(); const uniqueUrlList = (list) => { const seen = new Set(); const output = []; for (const item of list) { const text = normalizeText(item); if (!text || seen.has(text)) continue; seen.add(text); output.push(text); } return output; }; const createOption = (url) => ({ label: url, value: url }); const defaultUrlList = uniqueUrlList(DEFAULT_EXTERNAL_PANEL_URL_LIST); const customUrlList = vue.ref(uniqueUrlList(storage.get(EXTERNAL_PANEL_CUSTOM_LIST_KEY, []))); const optionList = vue.computed(() => uniqueUrlList([...defaultUrlList, ...customUrlList.value])); const optionItems = vue.computed(() => optionList.value.map((url) => createOption(url))); const selectedUrl = vue.ref(""); const persistCustomList = () => { storage.set(EXTERNAL_PANEL_CUSTOM_LIST_KEY, [...customUrlList.value]); }; const persistSelected = (value) => { storage.set(EXTERNAL_PANEL_SELECTED_KEY$1, value); }; const ensureCustomOption = (value) => { if (defaultUrlList.includes(value)) return; if (customUrlList.value.includes(value)) return; customUrlList.value = [...customUrlList.value, value]; persistCustomList(); }; const applySelected = (value) => { const nextValue = value && typeof value === "object" && "value" in value ? value.value : value; const text = normalizeText(nextValue); if (!text) return; ensureCustomOption(text); selectedUrl.value = text; persistSelected(text); }; const handleCreate = (value) => { const text = normalizeText(value); const fallback = selectedUrl.value || optionList.value[0] || defaultUrlList[0] || ""; return createOption(text || fallback); }; const initializeSelected = () => { const storedSelected = normalizeText(storage.get(EXTERNAL_PANEL_SELECTED_KEY$1, "")); selectedUrl.value = storedSelected || optionList.value[0] || ""; if (selectedUrl.value) { persistSelected(selectedUrl.value); } }; const removeCustomUrl = (url) => { const text = normalizeText(url); if (!text) return; const nextList = customUrlList.value.filter((item) => item !== text); if (nextList.length === customUrlList.value.length) return; customUrlList.value = nextList; persistCustomList(); if (selectedUrl.value === text) { const fallback = optionList.value[0] || defaultUrlList[0] || ""; selectedUrl.value = fallback; if (fallback) persistSelected(fallback); } }; const renderOption = ({ node, option }) => { const optionValue = normalizeText(option?.value); const removable = Boolean(optionValue) && !defaultUrlList.includes(optionValue); if (!removable) return node; const handleDelete = (event) => { event.preventDefault(); event.stopPropagation(); removeCustomUrl(optionValue); }; return vue.h( "div", { style: { position: "relative", display: "flex", alignItems: "center", width: "100%" } }, [ vue.h("div", { style: { minWidth: 0, flex: 1 } }, [node]), vue.h( naiveUi.NButton, { quaternary: true, circle: true, size: "tiny", title: "删除自定义 URL", onClick: handleDelete, style: { position: "absolute", right: "28px", top: "50%", transform: "translateY(-50%)", color: "#c03a3a", zIndex: 2 } }, { icon: () => vue.h(naiveUi.NIcon, null, { default: () => vue.h(TrashX) }), default: () => null } ) ] ); }; initializeSelected(); vue.watch(optionList, () => { if (!optionList.value.length) return; if (optionList.value.includes(selectedUrl.value)) return; selectedUrl.value = optionList.value[0]; persistSelected(selectedUrl.value); }); return (_ctx, _cache) => { const _component_n_alert = naiveUi.NAlert; const _component_n_select = naiveUi.NSelect; const _component_n_form_item = naiveUi.NFormItem; const _component_n_form = naiveUi.NForm; const _component_n_space = naiveUi.NSpace; return vue.openBlock(), vue.createBlock(_component_n_space, { vertical: "", size: 12 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_alert, { type: "info", "show-icon": false, title: "输入 URL 后请在下拉列表中选择该项以添加" }), vue.createVNode(_component_n_form, { "label-placement": "top" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_form_item, { label: "外部页面 URL", style: { "margin-bottom": "8px" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_select, { value: vue.unref(selectedUrl), filterable: "", tag: "", options: vue.unref(optionItems), "render-option": renderOption, placeholder: "输入 URL 后在下拉中选择", "on-create": handleCreate, "onUpdate:value": applySelected }, null, 8, ["value", "options"]) ]), _: 1 }) ]), _: 1 }) ]), _: 1 }); }; } }; const style$4 = cB$2( "bds-theme-settings", css$1` display: flex; flex-direction: column; gap: 12px; `, [] ); function mountStyle$4(mountTarget) { useTheme$1("bds-theme-settings-style", style$4, mountTarget); } const _hoisted_1$3 = { class: "bds-theme-settings" }; const _sfc_main$5 = { __name: "ThemeSettingsSection", props: { themeSettings: { type: Object, default: null }, active: { type: Boolean, default: false } }, setup(__props2) { const props2 = __props2; const styleMountTarget2 = vue.inject("styleMountTarget", null); const activeTheme = vue.inject("activeTheme", vue.computed(() => "light")); mountStyle$4(styleMountTarget2); const themeColorDraft = vue.ref("#00a1d6"); const previewColor = vue.ref(""); const themeModeValue = vue.computed(() => { return props2.themeSettings?.mode?.value || "auto"; }); const applyToChartsModel = vue.computed({ get: () => Boolean(props2.themeSettings?.applyToCharts?.value), set: (value) => { if (!props2.themeSettings?.applyToCharts) return; props2.themeSettings.applyToCharts.value = Boolean(value); } }); const activeThemeMode = vue.computed(() => { return activeTheme?.value === "dark" ? "dark" : "light"; }); const activeThemeLabel = vue.computed(() => { return activeThemeMode.value === "dark" ? "暗色主题色" : "亮色主题色"; }); const previewOverrides = vue.computed(() => { const color = String(previewColor.value || "").trim(); if (!color) return void 0; return activeThemeMode.value === "dark" ? createDarkThemeOverrides(color) : createLightThemeOverrides(color); }); const syncThemeDraft = () => { previewColor.value = ""; themeColorDraft.value = String( activeThemeMode.value === "dark" ? props2.themeSettings?.darkPrimary?.value : props2.themeSettings?.lightPrimary?.value ) || "#00a1d6"; }; const setThemeMode = (mode) => { props2.themeSettings?.setMode?.(mode); }; const applyThemeDraft = () => { const currentLight = String(props2.themeSettings?.lightPrimary?.value || DEFAULT_THEME.lightPrimary); const currentDark = String(props2.themeSettings?.darkPrimary?.value || DEFAULT_THEME.darkPrimary); props2.themeSettings?.applyColors?.({ light: activeThemeMode.value === "light" ? themeColorDraft.value : currentLight, dark: activeThemeMode.value === "dark" ? themeColorDraft.value : currentDark }); previewColor.value = ""; syncThemeDraft(); }; const testThemeDraft = () => { previewColor.value = themeColorDraft.value; }; const resetThemeDefault = () => { const defaultColor = activeThemeMode.value === "dark" ? DEFAULT_THEME.darkPrimary : DEFAULT_THEME.lightPrimary; themeColorDraft.value = defaultColor; applyThemeDraft(); }; vue.watch( () => [props2.active, activeThemeMode.value], ([active]) => { if (active) syncThemeDraft(); }, { immediate: true } ); return (_ctx, _cache) => { const _component_n_radio_button = naiveUi.NRadioButton; const _component_n_space = naiveUi.NSpace; const _component_n_radio_group = naiveUi.NRadioGroup; const _component_n_form_item = naiveUi.NFormItem; const _component_n_color_picker = naiveUi.NColorPicker; const _component_n_button = naiveUi.NButton; const _component_n_checkbox = naiveUi.NCheckbox; const _component_n_flex = naiveUi.NFlex; const _component_n_config_provider = naiveUi.NConfigProvider; return vue.openBlock(), vue.createBlock(_component_n_config_provider, { "theme-overrides": vue.unref(previewOverrides) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$3, [ vue.createVNode(_component_n_form_item, { label: "主题模式", style: { "margin-bottom": "0" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_radio_group, { value: vue.unref(themeModeValue), "onUpdate:value": setThemeMode }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_space, null, { default: vue.withCtx(() => [ vue.createVNode(_component_n_radio_button, { value: "light" }, { default: vue.withCtx(() => [..._cache[2] || (_cache[2] = [ vue.createTextVNode("亮色", -1) ])]), _: 1 }), vue.createVNode(_component_n_radio_button, { value: "dark" }, { default: vue.withCtx(() => [..._cache[3] || (_cache[3] = [ vue.createTextVNode("暗色", -1) ])]), _: 1 }), vue.createVNode(_component_n_radio_button, { value: "auto" }, { default: vue.withCtx(() => [..._cache[4] || (_cache[4] = [ vue.createTextVNode("跟随系统", -1) ])]), _: 1 }) ]), _: 1 }) ]), _: 1 }, 8, ["value"]) ]), _: 1 }), vue.createVNode(_component_n_form_item, { label: vue.unref(activeThemeLabel), style: { "margin-bottom": "0" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_color_picker, { value: vue.unref(themeColorDraft), "onUpdate:value": _cache[0] || (_cache[0] = ($event) => vue.isRef(themeColorDraft) ? themeColorDraft.value = $event : null), modes: ["hex"], "show-alpha": false }, null, 8, ["value"]), vue.createVNode(_component_n_button, { type: "primary", style: { "margin-left": "8px" }, onClick: testThemeDraft }, { default: vue.withCtx(() => [..._cache[5] || (_cache[5] = [ vue.createTextVNode("测试主题色", -1) ])]), _: 1 }) ]), _: 1 }, 8, ["label"]), vue.createVNode(_component_n_flex, { justify: "end", align: "center" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_checkbox, { checked: vue.unref(applyToChartsModel), "onUpdate:checked": _cache[1] || (_cache[1] = ($event) => vue.isRef(applyToChartsModel) ? applyToChartsModel.value = $event : null) }, { default: vue.withCtx(() => [..._cache[6] || (_cache[6] = [ vue.createTextVNode("应用于图表", -1) ])]), _: 1 }, 8, ["checked"]), vue.createVNode(_component_n_button, { onClick: resetThemeDefault }, { default: vue.withCtx(() => [..._cache[7] || (_cache[7] = [ vue.createTextVNode("恢复默认", -1) ])]), _: 1 }), vue.createVNode(_component_n_button, { type: "primary", onClick: applyThemeDraft }, { default: vue.withCtx(() => [..._cache[8] || (_cache[8] = [ vue.createTextVNode("应用主题色", -1) ])]), _: 1 }) ]), _: 1 }) ]) ]), _: 1 }, 8, ["theme-overrides"]); }; } }; const style$3 = c$3([ cB$2( "bds-panel-settings-body", css$1` max-height: 50vh; overflow: auto; ` ) ]); function mountStyle$3(mountTarget) { useTheme$1("bds-panel-settings-style", style$3, mountTarget); } const _hoisted_1$2 = { class: "bds-panel-settings-body" }; const _sfc_main$4 = { __name: "PanelSettings", props: { chartSettings: { type: Object, default: null }, themeSettings: { type: Object, default: null }, mode: { type: String, default: "script" } }, setup(__props2) { const props2 = __props2; const styleMountTarget2 = vue.inject("styleMountTarget", null); mountStyle$3(styleMountTarget2); const tabValue = vue.ref("chart"); return (_ctx, _cache) => { const _component_n_tab_pane = naiveUi.NTabPane; const _component_n_tabs = naiveUi.NTabs; return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$2, [ vue.createVNode(_component_n_tabs, { value: vue.unref(tabValue), "onUpdate:value": _cache[0] || (_cache[0] = ($event) => vue.isRef(tabValue) ? tabValue.value = $event : null), type: "line", animated: "" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_tab_pane, { name: "chart", tab: "图表设置" }, { default: vue.withCtx(() => [ vue.createVNode(_sfc_main$7, { settings: props2.chartSettings }, null, 8, ["settings"]) ]), _: 1 }), vue.createVNode(_component_n_tab_pane, { name: "theme", tab: "主题设置" }, { default: vue.withCtx(() => [ vue.createVNode(_sfc_main$5, { "theme-settings": props2.themeSettings, active: vue.unref(tabValue) === "theme" }, null, 8, ["theme-settings", "active"]) ]), _: 1 }), props2.mode === "script" ? (vue.openBlock(), vue.createBlock(_component_n_tab_pane, { key: 0, name: "external", tab: "外部页面" }, { default: vue.withCtx(() => [ vue.createVNode(_sfc_main$6) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }, 8, ["value"]) ]); }; } }; const style$2 = c$3([ cB$2( "bds-share-qrs", css$1` display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; `, [ cE$2( "item", css$1` display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 10px; border: 1px solid var(--n-border-color); border-radius: 10px; cursor: pointer; user-select: none; transition: border-color 0.2s, transform 0.15s; &:hover, &:focus-visible { border-color: var(--n-primary-color); transform: translateY(-1px); outline: none; } ` ), cE$2( "title", css$1` font-size: 12px; ` ) ] ) ]); function mountStyle$2(mountTarget) { useTheme$1("bds-share-qrs-style", style$2, mountTarget); } const _hoisted_1$1 = { class: "bds-share-qrs" }; const _hoisted_2$1 = ["title", "onClick", "onKeydown"]; const _sfc_main$3 = { __name: "ShareQrLinks", props: { links: { type: Array, default: () => [] } }, setup(__props2) { const styleMountTarget2 = vue.inject("styleMountTarget", null); mountStyle$2(styleMountTarget2); const themeVars = naiveUi.useThemeVars(); const props2 = __props2; const qrBackgroundColor = vue.computed(() => { return themeVars.value.baseColor || themeVars.value.cardColor || "#fff"; }); const qrColor = vue.computed(() => { return themeVars.value.textColorBase || "#000"; }); const openLink = (url) => { if (!url) return; window.open(url, "_blank", "noopener,noreferrer"); }; const normalizedLinks = vue.computed(() => { return (Array.isArray(props2.links) ? props2.links : []).map((item, index) => { const url = String(item?.url || "").trim(); const title = String(item?.title || "").trim() || `Link ${index + 1}`; const icon = String(item?.icon || "").trim(); if (!url) return null; return { key: `${title}-${url}`, url, title, icon }; }).filter(Boolean); }); return (_ctx, _cache) => { const _component_n_text = naiveUi.NText; const _component_n_qr_code = naiveUi.NQrCode; return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(normalizedLinks), (item) => { return vue.openBlock(), vue.createElementBlock("div", { key: item.key, class: "bds-share-qrs__item", role: "button", tabindex: "0", title: `打开 ${item.title}`, onClick: ($event) => openLink(item.url), onKeydown: [ vue.withKeys(vue.withModifiers(($event) => openLink(item.url), ["prevent"]), ["enter"]), vue.withKeys(vue.withModifiers(($event) => openLink(item.url), ["prevent"]), ["space"]) ] }, [ vue.createVNode(_component_n_text, { depth: "2", class: "bds-share-qrs__title" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(item.title), 1) ]), _: 2 }, 1024), vue.createVNode(_component_n_qr_code, { value: item.url, type: "svg", size: 64, padding: 10, "icon-src": item.icon || void 0, "icon-size": 16, "background-color": vue.unref(qrBackgroundColor), color: vue.unref(qrColor) }, null, 8, ["value", "icon-src", "background-color", "color"]) ], 40, _hoisted_2$1); }), 128)) ]); }; } }; const style$1 = c$3([ cB$2( "bds-user-panel", css$1` ` ) ]); function mountStyle$1(mountTarget) { useTheme$1("bds-user-panel-style", style$1, mountTarget); } const CDN_BASES = { jsdelivr: "https://cdn.jsdelivr.net/", jsdmirror: "https://cdn.jsdmirror.com/" }; const DEFAULT_CDN_PROFILE = "jsdelivr"; const DEFAULT_CDN_BASE = CDN_BASES[DEFAULT_CDN_PROFILE]; const normalizeCdnBase = (value) => { const text = String(value || "").trim(); if (!text) return ""; return text.endsWith("/") ? text : `${text}/`; }; const resolveCdnBase = (profileOrBase) => { const text = String(profileOrBase || "").trim(); if (!text) return DEFAULT_CDN_BASE; if (CDN_BASES[text]) return CDN_BASES[text]; return normalizeCdnBase(text) || DEFAULT_CDN_BASE; }; const buildNpmUrl = (base, path) => `${base}npm/${path}`; const buildGhUrl = (base, path) => `${base}gh/${path}`; const createCdnUrls = (profileOrBase) => { const base = resolveCdnBase(profileOrBase); return { base, vueGlobalProd: buildNpmUrl(base, "vue@3/dist/vue.global.prod.js"), naiveUiProd: buildNpmUrl(base, "naive-ui@2/dist/index.prod.js"), biliDataManagerMain: buildGhUrl(base, "ZBpine/bili-data-manager@main/dist/bili-data-manager.min.js"), biliDataManagerPinned: buildGhUrl(base, "ZBpine/bili-data-manager@ed2aaf5f8fedf7e157a22d10e995df2f61eeb917/dist/bili-data-manager.min.js"), staticHtmlDefault: buildGhUrl(base, "ZBpine/bili-data-statistic@main/docs/index.html"), staticHtmlCn: buildGhUrl(base, "ZBpine/bili-data-statistic@main/docs/cn/index.html"), favicon: buildGhUrl(base, "ZBpine/bili-data-statistic@main/docs/favicon.ico"), echarts: buildNpmUrl(base, "echarts@6/dist/echarts.min.js"), echartsWordcloud: buildNpmUrl(base, "echarts-wordcloud@2/dist/echarts-wordcloud.min.js"), html2canvas: buildNpmUrl(base, "html2canvas@1.4.1/dist/html2canvas.min.js"), jiebaWasm: buildNpmUrl(base, "jieba-wasm@2.4.0/pkg/web/jieba_rs_wasm.js") }; }; const resolveRuntimeCdnBase = () => { if (typeof globalThis !== "undefined") { const fromWindow = normalizeCdnBase(globalThis.__BDS_CDN_BASE__); if (fromWindow) return fromWindow; } { const fromBuild = normalizeCdnBase("https://cdn.jsdmirror.com/"); if (fromBuild) return fromBuild; } return DEFAULT_CDN_BASE; }; const runtimeCdnBase = resolveRuntimeCdnBase(); const runtimeCdnUrls = createCdnUrls(runtimeCdnBase); const BILI_DATA_MANAGER_CDN = runtimeCdnUrls.biliDataManagerMain; let hashWorker = null; let hashReqId = 0; const pendingHashTasks = new Map(); const ensureHashWorker = () => { if (hashWorker) return hashWorker; const workerScript = ` let converter = null; const ensureConverter = (bdmUrl) => { if (converter) return converter; importScripts(bdmUrl); converter = self.BiliDataManager?.BiliUser || null; return converter; }; self.onmessage = (event) => { const { id, hash, maxTry, bdmUrl } = event.data || {}; try { const workerConverter = ensureConverter(bdmUrl); if (!workerConverter || typeof workerConverter.hashToMid !== 'function') { self.postMessage({ id, ok: false, error: '反查能力不可用' }); return; } const mid = workerConverter.hashToMid(hash, maxTry); self.postMessage({ id, ok: true, mid }); } catch (error) { self.postMessage({ id, ok: false, error: String(error?.message || error) }); } }; `; const blob = new Blob([workerScript], { type: "application/javascript" }); const workerUrl = URL.createObjectURL(blob); hashWorker = new Worker(workerUrl); URL.revokeObjectURL(workerUrl); hashWorker.onmessage = (event) => { const payload = event.data || {}; const task = pendingHashTasks.get(payload.id); if (!task) return; pendingHashTasks.delete(payload.id); clearTimeout(task.timer); if (!payload.ok) { task.reject(new Error(payload.error || "反查失败")); return; } task.resolve(payload.mid); }; hashWorker.onerror = () => { const tasks = Array.from(pendingHashTasks.values()); pendingHashTasks.clear(); tasks.forEach((task) => { clearTimeout(task.timer); task.reject(new Error("反查失败")); }); if (hashWorker) { hashWorker.terminate(); hashWorker = null; } }; return hashWorker; }; const hashToMidByWorker = (hash, maxTry = 1e8) => { return new Promise((resolve, reject) => { const worker = ensureHashWorker(); const id = ++hashReqId; const timer = setTimeout(() => { pendingHashTasks.delete(id); reject(new Error("反查超时")); }, 3e4); pendingHashTasks.set(id, { resolve, reject, timer }); worker.postMessage({ id, hash, maxTry, bdmUrl: BILI_DATA_MANAGER_CDN }); }); }; const _sfc_main$2 = { __name: "UserPanel", props: { url: { type: String, default: "" }, midHash: { type: String, default: "" }, mode: { type: String, default: "script" } }, setup(__props2) { const props2 = __props2; const styleMountTarget2 = vue.inject("styleMountTarget", null); const BDM = vue.inject("BDM", null); mountStyle$1(styleMountTarget2); const loading = vue.ref(false); const panelError = vue.ref(""); const loadingBar = naiveUi.useLoadingBar(); const userPanelEl = vue.ref(null); const userCard = vue.ref({}); const userMidHash = vue.ref(""); const isReadonlyMode = vue.computed(() => props2.mode === "readonly"); const normalizeHash = (value) => String(value || "").trim().toLowerCase(); const waitPaint = () => new Promise((resolve) => { if (typeof requestAnimationFrame !== "function") { setTimeout(resolve, 0); return; } requestAnimationFrame(() => requestAnimationFrame(resolve)); }); const loadUser = async () => { if (!BDM) { panelError.value = "BDM 不可用"; return; } const sourceUrl = String(props2.url || "").trim(); const sourceHash = normalizeHash(props2.midHash); if (!sourceUrl && !sourceHash) return; loading.value = true; panelError.value = ""; loadingBar.start(); await vue.nextTick(); await waitPaint(); try { let finalUrl = sourceUrl; let resolvedMid = ""; if (sourceHash) { const converter = BDM?.BiliUser; if (typeof converter?.hashToMid !== "function") { throw new Error("反查能力不可用"); } const mid = await hashToMidByWorker(sourceHash); if (!mid || mid === -1) { throw new Error("未能查到用户ID或用户不存在"); } resolvedMid = String(mid); finalUrl = `https://space.bilibili.com/${resolvedMid}`; } if (isReadonlyMode.value) { userCard.value = { card: { mid: resolvedMid, name: "查找结果", sign: "此ID通过弹幕哈希本地计算得出,非官方公开数据,请谨慎使用" } }; userMidHash.value = ""; if (typeof BDM.BiliUser.midToHash === "function") { userMidHash.value = normalizeHash(BDM.BiliUser.midToHash(resolvedMid)); } loadingBar.finish(); return; } if (!BDM?.BiliUser) throw new Error("BDM 不可用"); const userMgr = new BDM.BiliUser(finalUrl); const cardData = await userMgr.getCard(true); if (!cardData || typeof cardData !== "object" || !cardData.card) { throw new Error("无用户信息"); } const fetchedMidHash = normalizeHash(userMgr.getMidHash()); if (sourceHash && fetchedMidHash && fetchedMidHash !== sourceHash) { throw new Error("用户信息校验失败,可能匹配到错误用户"); } userCard.value = cardData; userMidHash.value = userMgr.getMidHash() || (resolvedMid ? BDM.BiliUser.midToHash(resolvedMid) : ""); loadingBar.finish(); } catch (error) { userCard.value = {}; userMidHash.value = ""; panelError.value = String(error?.message || error); loadingBar.error(); } finally { loading.value = false; } }; vue.watch(() => [props2.url, props2.midHash, props2.mode], () => { loadUser(); }, { immediate: true }); return (_ctx, _cache) => { const _component_n_alert = naiveUi.NAlert; const _component_n_spin = naiveUi.NSpin; const _component_n_flex = naiveUi.NFlex; const _component_n_loading_bar_provider = naiveUi.NLoadingBarProvider; return vue.openBlock(), vue.createBlock(_component_n_loading_bar_provider, { to: vue.unref(userPanelEl) || void 0 }, { default: vue.withCtx(() => [ vue.createElementVNode("div", { ref_key: "userPanelEl", ref: userPanelEl, class: "bds-user-panel" }, [ vue.createVNode(_component_n_flex, { vertical: "", size: 8 }, { default: vue.withCtx(() => [ vue.unref(panelError) ? (vue.openBlock(), vue.createBlock(_component_n_alert, { key: 0, type: "error", title: vue.unref(panelError) }, null, 8, ["title"])) : vue.createCommentVNode("", true), vue.createVNode(_component_n_spin, { show: vue.unref(loading) }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(_sfc_main$a), { "user-card": vue.unref(userCard), "mid-hash": vue.unref(userMidHash) }, null, 8, ["user-card", "mid-hash"]) ]), _: 1 }, 8, ["show"]) ]), _: 1 }) ], 512) ]), _: 1 }, 8, ["to"]); }; } }; const style = c$3([ cB$2( "bds-dm-panel", css$1` height: 100%; min-height: 0; min-width: 0; `, [ cE$2( "main", css$1` display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; align-items: stretch; height: 100%; min-height: 0; min-width: 0; overflow: hidden; ` ), cE$2( "left", css$1` min-width: 0; min-height: 0; overflow: auto; padding: 2px; ` ), cE$2( "right", css$1` display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 0; overflow: hidden; ` ), cE$2( "right-header", css$1` padding: 8px; border-bottom: 1px solid var(--dm-border-color); box-shadow: rgba(0, 0, 0, 0.05) 2px 2px 2px; ` ), cE$2( "result-block", css$1` padding: 0 8px; border-left: 2px solid var(--dm-border-color); ` ), cE$2( "table-block", css$1` display: flex; flex-direction: column; height: calc(100% - 4px); min-height: 360px; flex-shrink: 0; ` ), cE$2( "table", css$1` flex: 1; min-height: 0; ` ), c$3("@media (max-width: 1100px)", [ cE$2( "main", css$1` grid-template-columns: 1fr; ` ) ]) ] ) ]); function mountStyle(mountTarget) { useTheme$1("bds-dm-panel-style", style, mountTarget); } const JIEBA_WASM_MODULE_URL = runtimeCdnUrls.jiebaWasm; const simpleTokenCache = new Map(); const jiebaTokenCache = new Map(); let currentArchiveId = ""; let segmentWorker = null; let segmentReqId = 0; const pendingSegmentTasks = new Map(); const setCache = (cache, key, value) => { cache.set(key, value); }; const normalizeWords = (words) => { if (!Array.isArray(words)) return []; return words.map((word) => String(word || "").trim()).filter((word) => word.length > 0); }; const simpleSegment = (content) => { const words = String(content || "").replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, " ").split(/\s+/); return normalizeWords(words); }; const ensureSegmentWorker = () => { if (segmentWorker) return segmentWorker; const workerScript = ` let jiebaModulePromise = null; const ensureJieba = async (moduleUrl) => { if (!jiebaModulePromise) { jiebaModulePromise = (async () => { const mod = await import(moduleUrl); if (typeof mod.default === 'function') { await mod.default(); } return mod; })(); } return jiebaModulePromise; }; const compressRepeats = (text, maxRepeat = 3) => { let next = String(text || ''); for (let len = 1; len <= 8; len += 1) { const regex = new RegExp('((.{1,' + len + '}))\\\\1{' + maxRepeat + ',}', 'g'); next = next.replace(regex, (_, __, word) => word.repeat(maxRepeat)); } return next; }; const normalizeWords = (words) => { if (!Array.isArray(words)) return []; return words .map((word) => String(word || '').trim()) .filter((word) => word.length > 0); }; self.onmessage = async (event) => { const { id, contents, jiebaUrl } = event.data || {}; try { const source = Array.isArray(contents) ? contents : []; const jieba = await ensureJieba(jiebaUrl); const list = []; for (const row of source) { const content = String(row || ''); if (!content) continue; const safeContent = compressRepeats(content); const words = typeof jieba.cut === 'function' ? jieba.cut(safeContent, true) : []; list.push({ content, tokens: normalizeWords(words) }); } self.postMessage({ id, ok: true, list }); } catch (error) { self.postMessage({ id, ok: false, error: String(error?.message || error) }); } }; `; const blob = new Blob([workerScript], { type: "application/javascript" }); const workerUrl = URL.createObjectURL(blob); segmentWorker = new Worker(workerUrl, { name: "segment-words-worker" }); URL.revokeObjectURL(workerUrl); segmentWorker.onmessage = (event) => { const payload = event.data || {}; const task = pendingSegmentTasks.get(payload.id); if (!task) return; pendingSegmentTasks.delete(payload.id); clearTimeout(task.timer); if (!payload.ok) { task.reject(new Error(payload.error || "分词失败")); return; } task.resolve(Array.isArray(payload.list) ? payload.list : []); }; segmentWorker.onerror = () => { const tasks = Array.from(pendingSegmentTasks.values()); pendingSegmentTasks.clear(); tasks.forEach((task) => { clearTimeout(task.timer); task.reject(new Error("分词失败")); }); if (segmentWorker) { segmentWorker.terminate(); segmentWorker = null; } }; return segmentWorker; }; const segmentJiebaMissing = (contents, timeout = 3e4) => { return new Promise((resolve, reject) => { const worker = ensureSegmentWorker(); const id = ++segmentReqId; const timer = setTimeout(() => { pendingSegmentTasks.delete(id); reject(new Error("分词超时")); }, Math.max(1e3, Number(timeout) || 3e4)); pendingSegmentTasks.set(id, { resolve, reject, timer }); worker.postMessage({ id, contents, jiebaUrl: JIEBA_WASM_MODULE_URL }); }); }; const addItemTokensToFreq = (freq, tokens, minLen) => { const uniq = new Set((Array.isArray(tokens) ? tokens : []).filter((token) => token.length >= minLen)); for (const token of uniq) { freq[token] = (freq[token] || 0) + 1; } }; const aggregateByItems = (items, cache, minLen) => { const freq = Object.create(null); for (const item of Array.isArray(items) ? items : []) { const content = String(item?.content || ""); if (!content) continue; let tokens = cache.get(content); if (!Array.isArray(tokens)) { tokens = simpleSegment(content); setCache(cache, content, tokens); } addItemTokensToFreq(freq, tokens, minLen); } return freq; }; const segmentWords = async ({ mode = "simple", items = [], minLen = 2, topN = 1e3, timeout = 3e4, archiveId = "" } = {}) => { const sourceMode = mode === "jieba" ? "jieba" : "simple"; const safeMinLen = Math.max(1, Number(minLen) || 2); const safeTopN = Math.max(1, Number(topN) || 1e3); const nextArchiveId = String(archiveId || "").trim(); if (nextArchiveId !== currentArchiveId) { simpleTokenCache.clear(); jiebaTokenCache.clear(); currentArchiveId = nextArchiveId; } const cache = sourceMode === "jieba" ? jiebaTokenCache : simpleTokenCache; const sourceItems = Array.isArray(items) ? items : []; const missing = sourceMode === "jieba" ? Array.from(new Set(sourceItems.map((item) => String(item?.content || "")).filter((content) => content && !cache.has(content)))) : []; if (sourceMode === "simple") { const freq2 = aggregateByItems(sourceItems, cache, safeMinLen); return Object.entries(freq2).map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value).slice(0, safeTopN); } else if (missing.length > 0) { const result = await segmentJiebaMissing(missing, timeout); for (const row of result) { const content = String(row?.content || ""); if (!content) continue; const tokens = normalizeWords(row?.tokens || []); setCache(cache, content, tokens); } } const freq = Object.create(null); for (const item of sourceItems) { const content = String(item?.content || ""); if (!content) continue; addItemTokensToFreq(freq, cache.get(content) || [], safeMinLen); } return Object.entries(freq).map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value).slice(0, safeTopN); }; const INJECT_SCRIPT_ID = "bds-injected-data"; const safeSerialize = (data) => { return JSON.stringify(data).replace(/<\//g, "<\\/"); }; const injectPanelData = (htmlText, data) => { const html = String(htmlText || ""); if (!html.trim()) throw new Error("静态模板为空"); const doc = new DOMParser().parseFromString(html, "text/html"); const payload = safeSerialize(data || {}); const staleScripts = doc.querySelectorAll( 'script[data-bds-echarts], script[src*="echarts.min.js"], script[src*="echarts-wordcloud.min.js"]' ); staleScripts.forEach((node) => node.remove()); const oldNode = doc.getElementById(INJECT_SCRIPT_ID); if (oldNode) oldNode.remove(); const scriptNode = doc.createElement("script"); scriptNode.id = INJECT_SCRIPT_ID; scriptNode.type = "application/json"; scriptNode.textContent = payload; if (!doc.body) { const body = doc.createElement("body"); doc.documentElement.appendChild(body); } doc.body.appendChild(scriptNode); return ` ${doc.documentElement.outerHTML}`; }; const downloadHtmlText = (htmlText, fileName) => { const blob = new Blob([htmlText], { type: "text/html;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); }; const _hoisted_1 = { class: "bds-dm-panel__main" }; const _hoisted_2 = { class: "bds-dm-panel__left" }; const _hoisted_3 = { class: "bds-dm-panel__table-block" }; const _hoisted_4 = { class: "bds-dm-panel__right" }; const DEFAULT_EXTERNAL_PANEL_URL = "https://zbpine.github.io/bili-data-statistic/"; const EXTERNAL_PANEL_SELECTED_KEY = "external.panelUrl.selected"; const EXTERNAL_PANEL_LEGACY_KEY = "external.panelUrl"; const EXTERNAL_PANEL_WINDOW_NAME = "bds-readonly-panel"; const PANEL_TRANSFER_TYPE = "BDS_PANEL_TRANSFER"; const PANEL_TRANSFER_ACK_TYPE = "BDS_PANEL_TRANSFER_ACK"; const _sfc_main$1 = { __name: "DmPanel", props: { url: { type: String, default: "" }, to: { type: [String, Object], default: void 0 }, active: { type: Boolean, default: false }, mode: { type: String, default: "script" } }, emits: ["update:url"], setup(__props2, { emit: __emit2 }) { const ECHARTS_URL = runtimeCdnUrls.echarts; const WORDCLOUD_URL = runtimeCdnUrls.echartsWordcloud; const HTML2CANVAS_URL = runtimeCdnUrls.html2canvas; let echartsLoadTask = null; let html2canvasLoadTask = null; const pageScriptTasks = new Map(); const findScriptBySrcPart = (srcPart) => { return Array.from(document.scripts || []).find((script) => String(script?.src || "").includes(srcPart)); }; const appendPageScript = (url) => { if (pageScriptTasks.has(url)) { return pageScriptTasks.get(url); } const existing = findScriptBySrcPart(url); if (existing) { return Promise.resolve(); } const task = new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = url; script.async = true; script.dataset.bdsEcharts = "true"; script.onload = () => { script.dataset.bdsLoaded = "true"; resolve(); }; script.onerror = () => { pageScriptTasks.delete(url); reject(new Error(`load script failed: ${url}`)); }; (document.head || document.documentElement).appendChild(script); }); pageScriptTasks.set(url, task); return task; }; const ensurePageEcharts = async () => { if (!echartsLoadTask) { echartsLoadTask = (async () => { if (!runtimeWindow2?.echarts?.init) { await appendPageScript(ECHARTS_URL); } await appendPageScript(WORDCLOUD_URL); if (!runtimeWindow2?.echarts?.init) { throw new Error("echarts 未加载到 window"); } })(); } await echartsLoadTask; }; const ensurePageHtml2canvas = async () => { if (!html2canvasLoadTask) { html2canvasLoadTask = (async () => { if (!runtimeWindow2?.html2canvas) { await appendPageScript(HTML2CANVAS_URL); } if (!runtimeWindow2?.html2canvas) { throw new Error("html2canvas 未加载到 window"); } return runtimeWindow2.html2canvas; })(); } return await html2canvasLoadTask; }; const normalizeExternalPanelUrl = (value) => { const text = String(value || "").trim(); if (!text) return null; try { const parsed = new URL(text); if (!["http:", "https:"].includes(parsed.protocol)) return null; return parsed.href; } catch { return null; } }; const getSelectedExternalPanelUrl = () => { const selected = normalizeExternalPanelUrl(storage.get(EXTERNAL_PANEL_SELECTED_KEY, "")); if (selected) return selected; const legacy = normalizeExternalPanelUrl(storage.get(EXTERNAL_PANEL_LEGACY_KEY, "")); if (legacy) return legacy; return DEFAULT_EXTERNAL_PANEL_URL; }; const props2 = __props2; const emit2 = __emit2; const styleMountTarget2 = vue.inject("styleMountTarget", null); const BDM = vue.inject("BDM", null); const data = vue.inject("data", vue.shallowRef(null)); const runtimeWindow2 = vue.inject("runtimeWindow", window); const getStaticHtmlText = vue.inject("getStaticHtmlText", null); const themeSettings2 = vue.inject("themeSettings", null); mountStyle(styleMountTarget2); const arcMgr = vue.shallowRef(null); const dmMgr = vue.shallowRef(null); const panelError = vue.ref(""); const archiveInfo = vue.shallowRef({}); const commandDms = vue.shallowRef([]); const dmBase = vue.shallowRef([]); const committedDmView = vue.shallowRef([]); const stagedDmView = vue.shallowRef([]); const danmakuTableRef = vue.ref(null); const chartManagerRef = vue.ref(null); const dividerRef = vue.ref(null); const committedFilters = vue.ref([]); const stagedFilter = vue.ref(null); const regexText = vue.ref("^(哈|呵|h|ha|H|HA|233+)+$"); const regexExclude = vue.ref(false); const chartMenus = vue.ref([]); const panelSettingsVisible = vue.ref(false); const midHashDialogVisible = vue.ref(false); const userPanelModalVisible = vue.ref(false); const pendingMidHash = vue.ref(""); const expandedNames = vue.ref(props2.mode === "readonly" ? ["list"] : ["load", "list"]); const userTouchedCollapse = vue.ref(false); const leftContentRef = vue.ref(null); const sharingImage = vue.ref(false); const exportingPanel = vue.ref(false); const sharePreviewVisible = vue.ref(false); const sharePreviewUrl = vue.ref(""); const shareImageBlob = vue.shallowRef(null); const openingExternalPanel = vue.ref(false); const qrLinkItems = [ { url: "https://greasyfork.org/zh-CN/scripts/534432", title: "GreasyFork", icon: "https://greasyfork.org/vite/assets/blacklogo96-CxYTSM_T.png" }, { url: "https://scriptcat.org/zh-CN/script-show-page/3750", title: "ScriptCat", icon: "https://scriptcat.org/favicon.ico" } ]; let filterIdSeed = 1; const hasInitialized = vue.ref(false); const currentArchiveId2 = vue.ref(""); const isReadonlyMode = vue.computed(() => props2.mode === "readonly"); const echartsReady = vue.ref(Boolean(runtimeWindow2?.echarts?.init)); const message = naiveUi.useMessage(); const dialog = naiveUi.useDialog(); const notification = naiveUi.useNotification(); const modal = naiveUi.useModal(); const themeVars = naiveUi.useThemeVars(); const panelCssVars = vue.computed(() => { return { "--dm-border-color": themeVars.value.borderColor }; }); const baseColor = vue.computed(() => themeVars.value.baseColor || themeVars.value.cardColor || "#fff"); const chartCtx = vue.computed(() => ({ BDM, arcMgr: arcMgr.value, dmMgr: dmMgr.value, tableRef: danmakuTableRef, tableItems: stagedDmView.value, stagedFilter: stagedFilter.value, committedFilters: committedFilters.value, segmentWords, feedback: { message, dialog, notification, modal }, queryMidHash: openMidHashQuery })); const isListExpanded = vue.computed(() => expandedNames.value.includes("list")); const chartSettings = vue.computed(() => chartManagerRef.value?.settings || null); const setPanelError = (error) => { panelError.value = error ? String(error?.message || error) : ""; }; const clearStage = () => { stagedFilter.value = null; }; const formatFilterValueNode = (filter, value) => { const shouldWrapTag = filter?.wrapTag !== false; const rendered = typeof filter.formatValue === "function" ? filter.formatValue(value) : String(value); const fallback = rendered == null || rendered === "" ? String(value) : rendered; if (!shouldWrapTag) return fallback; if (vue.isVNode(fallback)) return fallback; return vue.h(naiveUi.NTag, { size: "small" }, { default: () => String(fallback) }); }; const FilterLabel = vue.defineComponent({ name: "FilterLabel", props: { filter: { type: Object, required: true } }, setup(componentProps) { return () => { const filter = componentProps.filter; const values = Array.isArray(filter.values) ? filter.values : []; const renderedValues = values.flatMap((value, index) => { const node = formatFilterValueNode(filter, value); if (index === 0) return [node]; return [", ", node]; }); const template = String(filter.template || "{value}"); if (template.includes("{value}")) { const [before, after = ""] = template.split("{value}"); return vue.h("span", [before, ...renderedValues, after]); } return vue.h("span", [template, " ", ...renderedValues]); }; } }); const applySingleFilter = (items, filter) => { const values = Array.isArray(filter.values) ? filter.values : []; if (!values.length || typeof filter.predicate !== "function") return items; return items.filter((item) => { const matched = values.some((value) => filter.predicate(item, value)); return filter.exclude ? !matched : matched; }); }; const buildStagedView = (baseItems) => { let next = [...baseItems]; if (stagedFilter.value) { next = applySingleFilter(next, { ...stagedFilter.value, exclude: false }); } return next; }; const rebuildFilterViews = () => { let next = [...dmBase.value]; const activeCommitted = committedFilters.value.filter((filter) => filter.enabled); for (const filter of activeCommitted) { next = applySingleFilter(next, filter); } committedDmView.value = next; stagedDmView.value = buildStagedView(next); }; const clearAllFilters = () => { committedFilters.value = []; clearStage(); regexExclude.value = false; committedDmView.value = [...dmBase.value]; stagedDmView.value = [...dmBase.value]; }; const applyRegexFilter = () => { try { const regex = new RegExp(regexText.value, "i"); const regexFilter = { id: `f-${filterIdSeed++}`, source: "regex", template: "正则筛选 {value}", values: [regexText.value], formatValue: (value) => `/${value}/i`, predicate: (item) => regex.test(String(item?.content || "")), wrapTag: true, enabled: true, exclude: regexExclude.value }; const oldIdx = committedFilters.value.findIndex((item) => item.source === "regex"); if (oldIdx >= 0) committedFilters.value.splice(oldIdx, 1, regexFilter); else committedFilters.value.push(regexFilter); clearStage(); setPanelError(""); rebuildFilterViews(); } catch (error) { setPanelError("无效正则表达式"); } }; const stageFilter = (payload) => { if (!payload || typeof payload.predicate !== "function") return; const source = String(payload.source || "chart:unknown"); const value = payload.value; if (value == null || value === "") return; if (!stagedFilter.value || stagedFilter.value.source !== source) { stagedFilter.value = { source, template: payload.template || "{value}", values: [value], formatValue: payload.formatValue || ((v) => String(v)), predicate: payload.predicate, wrapTag: payload.wrapTag !== false }; stagedDmView.value = buildStagedView(committedDmView.value); return; } const draft = stagedFilter.value; draft.template = payload.template || draft.template; draft.formatValue = payload.formatValue || draft.formatValue; draft.predicate = payload.predicate; draft.wrapTag = payload.wrapTag !== false; const index = draft.values.findIndex((item) => item === value); if (index >= 0) draft.values.splice(index, 1); else draft.values.push(value); if (!draft.values.length) stagedFilter.value = null; stagedDmView.value = buildStagedView(committedDmView.value); }; const commitStagedFilter = () => { const draft = stagedFilter.value; if (!draft || !draft.values.length) return; committedFilters.value.push({ id: `f-${filterIdSeed++}`, source: draft.source, template: draft.template, values: [...draft.values], formatValue: draft.formatValue, predicate: draft.predicate, wrapTag: draft.wrapTag !== false, enabled: true, exclude: false }); stagedFilter.value = null; rebuildFilterViews(); }; const unstageFilter = () => { stagedFilter.value = null; stagedDmView.value = buildStagedView(committedDmView.value); }; const toggleCommittedEnabled = (id) => { const target = committedFilters.value.find((item) => item.id === id); if (!target) return; target.enabled = !target.enabled; rebuildFilterViews(); }; const toggleCommittedExclude = (id) => { const target = committedFilters.value.find((item) => item.id === id); if (!target) return; target.exclude = !target.exclude; if (target.source === "regex") regexExclude.value = target.exclude; rebuildFilterViews(); }; const removeCommittedFilter = (id) => { const idx = committedFilters.value.findIndex((item) => item.id === id); if (idx < 0) return; const removed = committedFilters.value[idx]; committedFilters.value.splice(idx, 1); if (removed.source === "regex") regexExclude.value = false; rebuildFilterViews(); }; const hasStagedFilter = vue.computed(() => { return Boolean(stagedFilter.value && Array.isArray(stagedFilter.value.values) && stagedFilter.value.values.length > 0); }); const hasAnyFilter = vue.computed(() => { return committedFilters.value.length > 0 || hasStagedFilter.value; }); const syncDanmakuState = ({ list = [], commandDms: cmd = [] } = {}) => { dmBase.value = Array.isArray(list) ? [...list] : []; commandDms.value = Array.isArray(cmd) ? cmd : []; clearAllFilters(); }; let ensureRunning = false; let ensurePending = false; let ensurePromise = null; const queueEnsureVideoBase = async () => { if (ensureRunning) { ensurePending = true; return ensurePromise; } ensureRunning = true; ensurePromise = (async () => { let lastError = null; do { ensurePending = false; try { await ensureVideoBase(); lastError = null; } catch (error) { lastError = error; } } while (ensurePending); if (lastError) { throw lastError; } })().finally(() => { ensureRunning = false; ensurePromise = null; }); return ensurePromise; }; const ensureVideoBase = async () => { if (!BDM?.BiliArchive || !BDM?.BiliDanmaku) throw new Error("BDM 不可用"); let sourceUrl = String(props2.url || "").trim(); const payload = data?.value; data.value = null; if (payload && typeof payload === "object") { const nextArcMgr2 = new BDM.BiliArchive(); const info2 = nextArcMgr2.setData(payload); const nextInfo2 = info2 || nextArcMgr2.info || {}; const nextDmMgr = new BDM.BiliDanmaku(nextInfo2); nextDmMgr.setData(payload); const nextId2 = String(nextInfo2?.id || `upload-${Date.now()}`).trim(); arcMgr.value = nextArcMgr2; archiveInfo.value = nextInfo2; dmMgr.value = nextDmMgr; sourceUrl = String(nextInfo2?.url || "").trim(); emit2("update:url", sourceUrl); if (currentArchiveId2.value !== nextId2) { expandedNames.value = isReadonlyMode.value ? ["list"] : ["load", "list"]; userTouchedCollapse.value = false; } currentArchiveId2.value = nextId2; } syncDanmakuState({ list: dmMgr.value?.data?.danmaku_list || [], commandDms: dmMgr.value?.data?.danmaku_view?.commandDms || [] }); if (isReadonlyMode.value) return; if (!sourceUrl) { if (payload && typeof payload === "object") return; throw new Error("未提供稿件 URL"); } const parsed = typeof BDM.BiliArchive.parseUrl === "function" ? BDM.BiliArchive.parseUrl(sourceUrl) : {}; const parsedId = String(parsed?.id || "").trim(); if (!parsedId) throw new Error("无法从URL中解析稿件 ID"); if (arcMgr.value && dmMgr.value && currentArchiveId2.value === parsedId) return; const nextArcMgr = new BDM.BiliArchive(); const info = await nextArcMgr.getData(sourceUrl); const nextInfo = info || nextArcMgr.info || {}; const nextId = String(nextInfo?.id || parsedId).trim(); if (!nextId) throw new Error("稿件信息获取失败"); arcMgr.value = nextArcMgr; archiveInfo.value = nextInfo; dmMgr.value = new BDM.BiliDanmaku(nextInfo); sourceUrl = String(nextInfo?.url || "").trim(); emit2("update:url", sourceUrl); if (currentArchiveId2.value !== nextId) { expandedNames.value = isReadonlyMode.value ? ["list"] : ["load", "list"]; userTouchedCollapse.value = false; } currentArchiveId2.value = nextId; }; const danmakuTableMenus = vue.computed(() => { const defaultMenus = [ { getName: (item) => `点赞数:${Number.isInteger(item?.likes) ? item.likes : "点击获取"}`, onSelect: async (item) => { if (!dmMgr.value?.api || !archiveInfo.value?.cid || !item?.idStr) return; try { const likes = await dmMgr.value.api.getLikes(archiveInfo.value.cid, [String(item.idStr)]); item.likes = likes?.[String(item.idStr)]?.likes ?? 0; } catch (error) { BDM?.logger?.warn("[dm likes] 查询失败", error); } }, disabled: isReadonlyMode.value } ]; return [...defaultMenus, ...chartMenus.value]; }); const updateChartMenus = (menus) => { chartMenus.value = Array.isArray(menus) ? menus : []; }; const copyMidHash = async (midHash) => { const hash = String(midHash || "").trim(); if (!hash) return; try { await navigator.clipboard.writeText(hash); message.success("midHash已复制到剪贴板"); } catch (error) { BDM?.logger?.error?.("midHash复制失败", error); message.error("复制失败"); } }; const openMidHashQuery = (midHash) => { const hash = String(midHash || "").trim(); if (!hash) return; pendingMidHash.value = hash; midHashDialogVisible.value = true; }; const revokeSharePreview = () => { if (sharePreviewUrl.value) { URL.revokeObjectURL(sharePreviewUrl.value); sharePreviewUrl.value = ""; } shareImageBlob.value = null; }; const canvasToBlob = (canvas, type = "image/png") => { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) resolve(blob); else reject(new Error("图片导出失败")); }, type); }); }; const shareImage = async () => { if (sharingImage.value) return; const html2canvasTask = ensurePageHtml2canvas(); const leftEl = leftContentRef.value?.$el; const tableEl = danmakuTableRef.value?.$el?.parentElement; const dividerEl = dividerRef.value?.$el; const chartBodyEl = chartManagerRef.value?.chartBodyEl || null; const chartItemEls = Array.from(chartBodyEl?.children || []); try { sharingImage.value = true; await vue.nextTick(); const elList = [leftEl, tableEl, dividerEl, ...chartItemEls].filter((el) => { if (!el) return false; return window.getComputedStyle(el).display !== "none"; }); if (!elList.length) { throw new Error("找不到截图区域"); } const pixelRatio = Math.max(1, Number(window.devicePixelRatio) || 1); const baseCaptureOptions = { scale: pixelRatio, backgroundColor: baseColor.value, useCORS: true }; const html2canvas = await html2canvasTask; const canvasList = (await Promise.all( elList.map((el) => html2canvas(el, baseCaptureOptions)) )).filter(Boolean); if (!canvasList.length) { throw new Error("截图生成失败"); } const padding = Math.round(24 * pixelRatio); const contentWidth = Math.max(...canvasList.map((c2) => c2.width)); const width = contentWidth + padding * 2; const height = padding * 2 + canvasList.reduce((sum, c2) => sum + c2.height, 0); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("无法创建绘图上下文"); ctx.fillStyle = baseColor.value; ctx.fillRect(0, 0, width, height); let y = padding; for (const itemCanvas of canvasList) { const x = padding + Math.floor((contentWidth - itemCanvas.width) / 2); ctx.drawImage(itemCanvas, x, y); y += itemCanvas.height; } const finalBlob = await canvasToBlob(canvas, "image/png"); revokeSharePreview(); shareImageBlob.value = finalBlob; sharePreviewUrl.value = URL.createObjectURL(finalBlob); sharePreviewVisible.value = true; } catch (error) { BDM?.logger?.error?.("分享图片生成失败", error); message.error("截图生成失败"); } finally { sharingImage.value = false; } }; const downloadShareImage = () => { const blob = shareImageBlob.value; if (!blob) return; const url = URL.createObjectURL(blob); const link = document.createElement("a"); const id = String(archiveInfo.value?.id || "bili-data-statistic").trim() || "bili-data-statistic"; link.href = url; link.download = `${id}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const buildExportData = () => { const merged = { ...dmMgr.value?.data || {}, ...arcMgr.value?.data || {} }; if (Object.keys(merged).length > 0) { return merged; } if (data?.value && typeof data.value === "object") { return data.value; } return null; }; const downloadInjectedPanel = async () => { if (exportingPanel.value) return; if (typeof getStaticHtmlText !== "function") { message.error("静态模板读取能力不可用"); return; } const exportData = buildExportData(); if (!exportData) { message.warning("当前没有可导出的弹幕数据"); return; } try { exportingPanel.value = true; const templateHtml = await getStaticHtmlText(); const injectedHtml = injectPanelData(templateHtml, exportData); const id = String(archiveInfo.value?.id || "bili-data-statistics").trim() || "bili-data-statistics"; downloadHtmlText(injectedHtml, `${id}.html`); message.success("静态面板已下载"); } catch (error) { BDM?.logger?.error?.("静态面板下载失败", error); message.error("静态面板下载失败"); } finally { exportingPanel.value = false; } }; const openExternalPanel = async () => { if (openingExternalPanel.value) return; const targetUrl = getSelectedExternalPanelUrl(); const targetOrigin = new URL(targetUrl).origin; const exportData = buildExportData(); if (!exportData) { message.warning("当前没有可发送的弹幕数据"); return; } const targetWindow = window.open(targetUrl, EXTERNAL_PANEL_WINDOW_NAME); if (!targetWindow) { message.error("打开外部页面失败,请检查浏览器拦截设置"); return; } openingExternalPanel.value = true; const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; const transferPayload = { type: PANEL_TRANSFER_TYPE, sessionId, version: 1, payload: exportData }; let retryTimer = null; let timeoutTimer = null; const cleanup = () => { if (retryTimer) { clearInterval(retryTimer); retryTimer = null; } if (timeoutTimer) { clearTimeout(timeoutTimer); timeoutTimer = null; } window.removeEventListener("message", onMessage); openingExternalPanel.value = false; }; const sendPayload = () => { try { targetWindow.postMessage(transferPayload, targetOrigin); } catch { } }; const onMessage = (event) => { if (event.origin !== targetOrigin) return; if (event.source !== targetWindow) return; const data2 = event.data; if (!data2 || typeof data2 !== "object") return; if (data2.type !== PANEL_TRANSFER_ACK_TYPE) return; if (data2.sessionId !== sessionId) return; cleanup(); message.success("已新标签页打开并发送数据"); }; window.addEventListener("message", onMessage); sendPayload(); retryTimer = setInterval(sendPayload, 400); timeoutTimer = setTimeout(() => { cleanup(); message.warning("外部页面未响应,可在外部页面加载完成后重试"); }, 1e4); }; const confirmMidHashQuery = () => { midHashDialogVisible.value = false; userPanelModalVisible.value = true; }; const onCollapseUpdate = (names) => { userTouchedCollapse.value = true; expandedNames.value = Array.isArray(names) ? names : []; }; const handleInitialLoadFinished = () => { if (userTouchedCollapse.value) return; if (isReadonlyMode.value) return; expandedNames.value = expandedNames.value.filter((name) => name !== "load"); }; vue.watch( () => props2.active, async (active) => { if (!active) return; if (!hasInitialized.value) { mountVueucStyles(styleMountTarget2 || void 0); hasInitialized.value = true; } try { setPanelError(""); await queueEnsureVideoBase(); if (!echartsReady.value) { ensurePageEcharts().then(() => { echartsReady.value = true; vue.nextTick(() => { chartManagerRef.value?.chartResize?.(); }); }).catch((error) => { setPanelError(error); }); } else { await vue.nextTick(); chartManagerRef.value?.chartResize?.(); } } catch (error) { setPanelError(error); } }, { immediate: true } ); vue.watch( () => data?.value, async (payload, previous) => { if (!props2.active) return; if (!payload || typeof payload !== "object") return; if (payload === previous) return; try { setPanelError(""); await queueEnsureVideoBase(); } catch (error) { setPanelError(error); } } ); vue.watch(hasAnyFilter, (next, prev) => { if (!next || prev) return; if (expandedNames.value.includes("result")) return; expandedNames.value = [...expandedNames.value, "result"]; }); vue.onBeforeUnmount(() => { revokeSharePreview(); }); return (_ctx, _cache) => { const _component_n_alert = naiveUi.NAlert; const _component_n_divider = naiveUi.NDivider; const _component_n_collapse_item = naiveUi.NCollapseItem; const _component_n_checkbox = naiveUi.NCheckbox; const _component_n_icon = naiveUi.NIcon; const _component_n_button = naiveUi.NButton; const _component_n_flex = naiveUi.NFlex; const _component_n_text = naiveUi.NText; const _component_n_collapse = naiveUi.NCollapse; const _component_n_input = naiveUi.NInput; const _component_n_popover = naiveUi.NPopover; const _component_n_modal = naiveUi.NModal; const _component_n_tag = naiveUi.NTag; return vue.openBlock(), vue.createElementBlock("div", { class: "bds-dm-panel", style: vue.normalizeStyle(vue.unref(panelCssVars)) }, [ vue.createElementVNode("div", _hoisted_1, [ vue.createElementVNode("div", _hoisted_2, [ vue.createVNode(_component_n_flex, { size: 12, vertical: "", ref_key: "leftContentRef", ref: leftContentRef }, { default: vue.withCtx(() => [ vue.unref(panelError) ? (vue.openBlock(), vue.createBlock(_component_n_alert, { key: 0, type: "error", title: vue.unref(panelError) }, null, 8, ["title"])) : vue.createCommentVNode("", true), vue.createVNode(vue.unref(_sfc_main$6$1), { "archive-info": vue.unref(archiveInfo) }, null, 8, ["archive-info"]), vue.createVNode(_component_n_divider, { style: { "margin": "4px 0" } }), vue.unref(arcMgr) && vue.unref(dmMgr) ? (vue.openBlock(), vue.createBlock(_component_n_collapse, { key: 1, "display-directive": "show", "expanded-names": vue.unref(expandedNames), "onUpdate:expandedNames": onCollapseUpdate }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_collapse_item, { name: "load", title: `载入弹幕 ${vue.unref(dmBase).length.toLocaleString()} 条` }, { default: vue.withCtx(() => [ !vue.unref(isReadonlyMode) ? (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$9), { key: vue.unref(archiveInfo).id || vue.unref(currentArchiveId2), "arc-mgr": vue.unref(arcMgr), "dm-mgr": vue.unref(dmMgr), to: props2.to, onSyncData: syncDanmakuState, onSetError: setPanelError, onInitialLoadFinished: handleInitialLoadFinished }, null, 8, ["arc-mgr", "dm-mgr", "to"])) : vue.createCommentVNode("", true) ]), _: 1 }, 8, ["title"]), vue.unref(commandDms).length ? (vue.openBlock(), vue.createBlock(_component_n_collapse_item, { key: 0, name: "command", title: `互动弹幕 ${vue.unref(commandDms).length.toLocaleString()} 条` }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(_sfc_main$5$1), { "command-dms": vue.unref(commandDms) }, null, 8, ["command-dms"]) ]), _: 1 }, 8, ["title"])) : vue.createCommentVNode("", true), vue.createVNode(_component_n_collapse_item, { name: "result", title: `筛选弹幕 ${vue.unref(committedDmView).length.toLocaleString()} 条` }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { vertical: "", size: 8, class: "bds-dm-panel__result-block" }, { default: vue.withCtx(() => [ vue.unref(committedFilters).length ? (vue.openBlock(), vue.createBlock(_component_n_flex, { key: 0, vertical: "", size: 8 }, { default: vue.withCtx(() => [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(committedFilters), (item) => { return vue.openBlock(), vue.createBlock(_component_n_flex, { key: item.id, align: "center", size: [12, 4], wrap: "" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_checkbox, { checked: item.enabled, "onUpdate:checked": () => toggleCommittedEnabled(item.id) }, null, 8, ["checked", "onUpdate:checked"]), vue.createVNode(vue.unref(FilterLabel), { filter: item }, null, 8, ["filter"]), vue.createVNode(_component_n_checkbox, { checked: item.exclude, style: { "margin-left": "auto" }, "onUpdate:checked": () => toggleCommittedExclude(item.id) }, { default: vue.withCtx(() => [..._cache[10] || (_cache[10] = [ vue.createTextVNode(" 排除 ", -1) ])]), _: 1 }, 8, ["checked", "onUpdate:checked"]), vue.createVNode(_component_n_button, { size: "tiny", tertiary: "", type: "error", title: "清除筛选", onClick: ($event) => removeCommittedFilter(item.id) }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(TrashX)) ]), _: 1 }) ]), _: 1 }, 8, ["onClick"]) ]), _: 2 }, 1024); }), 128)) ]), _: 1 })) : vue.createCommentVNode("", true), vue.unref(hasStagedFilter) ? (vue.openBlock(), vue.createBlock(_component_n_flex, { key: 1, vertical: "", size: 6 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { align: "center", size: [12, 4], wrap: "" }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(FilterLabel), { filter: vue.unref(stagedFilter) }, null, 8, ["filter"]), vue.createVNode(_component_n_text, { depth: "3" }, { default: vue.withCtx(() => [ vue.createTextVNode("弹幕共 " + vue.toDisplayString(vue.unref(stagedDmView).length.toLocaleString()) + " 条", 1) ]), _: 1 }), vue.createVNode(_component_n_button, { size: "tiny", tertiary: "", type: "error", title: "清除子筛选", onClick: unstageFilter, style: { "margin-left": "auto" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(TrashX)) ]), _: 1 }) ]), _: 1 }), vue.createVNode(_component_n_button, { size: "tiny", tertiary: "", type: "success", title: "提交子筛选结果作为新的数据源", onClick: commitStagedFilter }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(SquareCheck)) ]), _: 1 }) ]), _: 1 }) ]), _: 1 }) ]), _: 1 })) : vue.createCommentVNode("", true) ]), _: 1 }) ]), _: 1 }, 8, ["title"]), vue.createVNode(_component_n_collapse_item, { name: "list", title: `列表弹幕 ${vue.unref(stagedDmView).length.toLocaleString()} 条` }, null, 8, ["title"]) ]), _: 1 }, 8, ["expanded-names"])) : vue.createCommentVNode("", true) ]), _: 1 }, 512), vue.withDirectives(vue.createElementVNode("div", _hoisted_3, [ vue.createVNode(vue.unref(_sfc_main$1$1), { class: "bds-dm-panel__table", items: vue.unref(stagedDmView), "item-height": 38, "menu-items": vue.unref(danmakuTableMenus), to: props2.to, ref_key: "danmakuTableRef", ref: danmakuTableRef }, null, 8, ["items", "menu-items", "to"]) ], 512), [ [vue.vShow, vue.unref(isListExpanded)] ]), vue.withDirectives(vue.createVNode(_component_n_divider, { style: { "padding": "16px 0" }, ref_key: "dividerRef", ref: dividerRef }, null, 512), [ [vue.vShow, vue.unref(sharingImage)] ]) ]), vue.createElementVNode("div", _hoisted_4, [ vue.createVNode(_component_n_flex, { align: "center", size: 8, wrap: "", class: "bds-dm-panel__right-header" }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_input, { value: vue.unref(regexText), "onUpdate:value": _cache[0] || (_cache[0] = ($event) => vue.isRef(regexText) ? regexText.value = $event : null), placeholder: "请输入正则表达式", size: "small", style: { "flex": "1", "min-width": "200px" } }, null, 8, ["value"]), vue.createVNode(_component_n_button, { size: "small", type: vue.unref(committedFilters).length ? void 0 : "warning", onClick: applyRegexFilter }, { default: vue.withCtx(() => [..._cache[11] || (_cache[11] = [ vue.createTextVNode("筛选", -1) ])]), _: 1 }, 8, ["type"]), vue.createVNode(_component_n_button, { size: "small", type: vue.unref(committedFilters).length ? "warning" : void 0, onClick: clearAllFilters }, { default: vue.withCtx(() => [..._cache[12] || (_cache[12] = [ vue.createTextVNode("取消筛选", -1) ])]), _: 1 }, 8, ["type"]), props2.mode !== "script" ? (vue.openBlock(), vue.createBlock(_component_n_button, { key: 0, size: "small", circle: "", title: "转为图片", loading: vue.unref(sharingImage), onClick: shareImage }, { icon: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(Camera)) ]), _: 1 }) ]), _: 1 }, 8, ["loading"])) : (vue.openBlock(), vue.createBlock(_component_n_button, { key: 1, size: "small", circle: "", title: "新标签页打开", loading: vue.unref(openingExternalPanel), onClick: openExternalPanel }, { icon: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(ExternalLink)) ]), _: 1 }) ]), _: 1 }, 8, ["loading"])), vue.createVNode(_component_n_button, { size: "small", circle: "", title: "下载面板", loading: vue.unref(exportingPanel), onClick: downloadInjectedPanel }, { icon: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(FileDownload)) ]), _: 1 }) ]), _: 1 }, 8, ["loading"]), vue.createVNode(_component_n_popover, { trigger: "hover", placement: "bottom-end", to: props2.to }, { trigger: vue.withCtx(() => [ vue.createVNode(_component_n_button, { size: "small", circle: "", title: "设置", onClick: _cache[1] || (_cache[1] = ($event) => panelSettingsVisible.value = true) }, { icon: vue.withCtx(() => [ vue.createVNode(_component_n_icon, null, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(Settings)) ]), _: 1 }) ]), _: 1 }) ]), default: vue.withCtx(() => [ vue.createVNode(vue.unref(_sfc_main$3), { links: qrLinkItems }) ]), _: 1 }, 8, ["to"]) ]), _: 1 }), !vue.unref(echartsReady) ? (vue.openBlock(), vue.createBlock(_component_n_alert, { key: 0, type: "info", title: "图表库加载中..." })) : (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$8), { key: 1, ref_key: "chartManagerRef", ref: chartManagerRef, items: vue.unref(committedDmView), "chart-ctx": vue.unref(chartCtx), to: props2.to, onSelectFilter: stageFilter, "onUpdate:chartMenus": updateChartMenus }, null, 8, ["items", "chart-ctx", "to"])) ]) ]), vue.createVNode(_component_n_modal, { show: vue.unref(panelSettingsVisible), "onUpdate:show": _cache[2] || (_cache[2] = ($event) => vue.isRef(panelSettingsVisible) ? panelSettingsVisible.value = $event : null), preset: "card", title: "设置", style: { "width": "50vw", "max-height": "60vh" }, to: props2.to }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(_sfc_main$4), { "chart-settings": vue.unref(chartSettings), "theme-settings": vue.unref(themeSettings2), mode: props2.mode }, null, 8, ["chart-settings", "theme-settings", "mode"]) ]), _: 1 }, 8, ["show", "to"]), vue.createVNode(_component_n_modal, { show: vue.unref(midHashDialogVisible), "onUpdate:show": _cache[5] || (_cache[5] = ($event) => vue.isRef(midHashDialogVisible) ? midHashDialogVisible.value = $event : null), preset: "card", title: "提示", style: { "width": "450px" }, to: props2.to }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { vertical: "", size: 10 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_text, null, { default: vue.withCtx(() => [..._cache[13] || (_cache[13] = [ vue.createTextVNode("是否尝试反查用户ID?", -1) ])]), _: 1 }), vue.createVNode(_component_n_tag, { size: "small", type: "info", style: { "cursor": "pointer", "align-self": "flex-start" }, title: "点击复制 midHash", onClick: _cache[3] || (_cache[3] = ($event) => copyMidHash(vue.unref(pendingMidHash))) }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(pendingMidHash)), 1) ]), _: 1 }), vue.createVNode(_component_n_text, { depth: "3" }, { default: vue.withCtx(() => [..._cache[14] || (_cache[14] = [ vue.createTextVNode("可能需要一段时间,且10位数以上ID容易查错", -1) ])]), _: 1 }), vue.createVNode(_component_n_flex, { justify: "end", size: 8 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { size: "small", onClick: _cache[4] || (_cache[4] = ($event) => midHashDialogVisible.value = false) }, { default: vue.withCtx(() => [..._cache[15] || (_cache[15] = [ vue.createTextVNode("否", -1) ])]), _: 1 }), vue.createVNode(_component_n_button, { size: "small", type: "warning", onClick: confirmMidHashQuery }, { default: vue.withCtx(() => [..._cache[16] || (_cache[16] = [ vue.createTextVNode("是", -1) ])]), _: 1 }) ]), _: 1 }) ]), _: 1 }) ]), _: 1 }, 8, ["show", "to"]), vue.unref(userPanelModalVisible) ? (vue.openBlock(), vue.createBlock(_component_n_modal, { key: 0, show: true, preset: "card", title: "用户信息", style: { "width": "60vw" }, to: props2.to, "onUpdate:show": _cache[6] || (_cache[6] = (show) => { if (!show) userPanelModalVisible.value = false; }) }, { default: vue.withCtx(() => [ vue.createVNode(_sfc_main$2, { url: props2.url, "mid-hash": vue.unref(pendingMidHash), mode: props2.mode }, null, 8, ["url", "mid-hash", "mode"]) ]), _: 1 }, 8, ["to"])) : vue.createCommentVNode("", true), vue.createVNode(_component_n_modal, { show: vue.unref(sharePreviewVisible), "onUpdate:show": [ _cache[8] || (_cache[8] = ($event) => vue.isRef(sharePreviewVisible) ? sharePreviewVisible.value = $event : null), _cache[9] || (_cache[9] = (show) => { if (!show) revokeSharePreview(); }) ], preset: "card", title: "截图预览", style: { "width": "50vw" }, to: props2.to }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_flex, { vertical: "", size: 12 }, { default: vue.withCtx(() => [ vue.unref(sharePreviewUrl) ? (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$7$1), { key: 0, src: vue.unref(sharePreviewUrl), alt: "截图预览", width: "100%", "object-fit": "contain", style: { "max-height": "60vh" } }, null, 8, ["src"])) : vue.createCommentVNode("", true), vue.createVNode(_component_n_flex, { justify: "end", size: 8 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_button, { onClick: _cache[7] || (_cache[7] = ($event) => sharePreviewVisible.value = false) }, { default: vue.withCtx(() => [..._cache[17] || (_cache[17] = [ vue.createTextVNode("关闭", -1) ])]), _: 1 }), vue.createVNode(_component_n_button, { type: "warning", onClick: downloadShareImage }, { default: vue.withCtx(() => [..._cache[18] || (_cache[18] = [ vue.createTextVNode("保存图片", -1) ])]), _: 1 }) ]), _: 1 }) ]), _: 1 }) ]), _: 1 }, 8, ["show", "to"]) ], 4); }; } }; const appError = ""; const _sfc_main = { __name: "App", setup(__props2) { const styleMountTarget2 = vue.inject("styleMountTarget", null); const BDM = vue.inject("BDM", null); const APP_MODE = vue.inject("APP_MODE", vue.ref("script")); const data = vue.inject("data", vue.shallowRef(null)); const sourceUrl = vue.inject("sourceUrl", vue.shallowRef("")); mountStyle$c(styleMountTarget2); const showPanel = vue.ref(false); const isScriptApp = vue.computed(() => APP_MODE.value === "script"); const hasProvidedData = vue.computed(() => Boolean(data?.value && typeof data.value === "object")); if (isScriptApp.value) { sourceUrl.value = location.href; } const isUserPage = vue.computed(() => { const urlText = String(sourceUrl?.value || "").trim(); if (!urlText) return false; try { return new URL(urlText).hostname === "space.bilibili.com"; } catch { return false; } }); const panelSize = vue.computed(() => { if (!isScriptApp.value) return 90; return isUserPage.value ? 60 : 80; }); const entryLabel = vue.computed(() => isUserPage.value ? "用户信息" : "弹幕统计"); const themeMode = vue.ref(normalizeThemeMode(storage.get("theme.mode", DEFAULT_THEME.mode))); const lightPrimary = vue.ref(normalizeThemeColor(storage.get("theme.lightPrimary", DEFAULT_THEME.lightPrimary), DEFAULT_THEME.lightPrimary)); const darkPrimary = vue.ref(normalizeThemeColor(storage.get("theme.darkPrimary", DEFAULT_THEME.darkPrimary), DEFAULT_THEME.darkPrimary)); const applyToCharts2 = vue.ref(Boolean(storage.get("theme.applyToCharts", false))); const osTheme = naiveUi.useOsTheme(); const isDarkThemeActive = vue.computed(() => { if (themeMode.value === "dark") return true; if (themeMode.value === "light") return false; return osTheme.value === "dark"; }); const theme = vue.computed(() => { return isDarkThemeActive.value ? naiveUi.darkTheme : null; }); const shellStyle = vue.computed(() => ({ colorScheme: isDarkThemeActive.value ? "dark" : "light" })); const themeOverrides = vue.computed(() => { return isDarkThemeActive.value ? createDarkThemeOverrides(darkPrimary.value) : createLightThemeOverrides(lightPrimary.value); }); const activeTheme = vue.computed(() => isDarkThemeActive.value ? "dark" : "light"); const activePrimary = vue.computed(() => isDarkThemeActive.value ? darkPrimary.value : lightPrimary.value); const applyThemeColors = ({ light, dark }) => { const nextLight = normalizeThemeColor(light, lightPrimary.value); const nextDark = normalizeThemeColor(dark, darkPrimary.value); lightPrimary.value = nextLight; darkPrimary.value = nextDark; storage.set("theme.lightPrimary", nextLight); storage.set("theme.darkPrimary", nextDark); }; const setThemeMode = (mode) => { const next = normalizeThemeMode(mode); if (themeMode.value === next) return; themeMode.value = next; storage.set("theme.mode", next); }; vue.provide("themeSettings", { mode: themeMode, lightPrimary, darkPrimary, activePrimary, applyToCharts: applyToCharts2, setMode: setThemeMode, applyColors: applyThemeColors }); vue.provide("activeTheme", activeTheme); vue.watch(activePrimary, (color) => { BDM.changeColor(color); }, { immediate: true }); vue.watch(applyToCharts2, (value) => { storage.set("theme.applyToCharts", Boolean(value)); }); const panelShellRef = vue.ref(null); const panelEl = vue.computed(() => panelShellRef.value?.panelEl || null); let keyboardBlockAttached = false; const blockPanelKeyboardEvent = (event) => { if (!showPanel.value) return; const root = panelEl.value; if (!root) return; const path = typeof event.composedPath === "function" ? event.composedPath() : []; if (Array.isArray(path) && path.length && !path.includes(root)) return; event.stopPropagation(); event.stopImmediatePropagation?.(); }; const attachKeyboardBlock = () => { if (keyboardBlockAttached) return; window.addEventListener("keydown", blockPanelKeyboardEvent, true); window.addEventListener("keypress", blockPanelKeyboardEvent, true); window.addEventListener("keyup", blockPanelKeyboardEvent, true); keyboardBlockAttached = true; }; const detachKeyboardBlock = () => { if (!keyboardBlockAttached) return; window.removeEventListener("keydown", blockPanelKeyboardEvent, true); window.removeEventListener("keypress", blockPanelKeyboardEvent, true); window.removeEventListener("keyup", blockPanelKeyboardEvent, true); keyboardBlockAttached = false; }; const activateWithData = async (nextData) => { showPanel.value = false; await vue.nextTick(); data.value = nextData; await vue.nextTick(); showPanel.value = true; }; const handleParsedUploadData = async (parsed) => { await activateWithData(parsed); }; const handleOpenPanel = () => { showPanel.value = true; }; const handleTogglePanel = () => { const nextShow = !showPanel.value; if (nextShow && isScriptApp.value) { sourceUrl.value = location.href; } showPanel.value = nextShow; }; vue.watch(showPanel, (open) => { if (open) attachKeyboardBlock(); else detachKeyboardBlock(); }); vue.watch( hasProvidedData, (hasData) => { if (isScriptApp.value) return; if (hasData) showPanel.value = true; }, { immediate: true } ); vue.onBeforeUnmount(() => { detachKeyboardBlock(); }); return (_ctx, _cache) => { const _component_n_loading_bar_provider = naiveUi.NLoadingBarProvider; const _component_n_modal_provider = naiveUi.NModalProvider; const _component_n_message_provider = naiveUi.NMessageProvider; const _component_n_notification_provider = naiveUi.NNotificationProvider; const _component_n_dialog_provider = naiveUi.NDialogProvider; const _component_n_config_provider = naiveUi.NConfigProvider; return vue.openBlock(), vue.createBlock(_component_n_config_provider, { theme: vue.unref(theme), "theme-overrides": vue.unref(themeOverrides), locale: vue.unref(naiveUi.zhCN), "date-locale": vue.unref(naiveUi.dateZhCN), "style-mount-target": vue.unref(styleMountTarget2) }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_dialog_provider, { to: vue.unref(panelEl) || void 0 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_notification_provider, { to: vue.unref(panelEl) || void 0 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_message_provider, { to: vue.unref(panelEl) || void 0 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_modal_provider, { to: vue.unref(panelEl) || void 0 }, { default: vue.withCtx(() => [ vue.createVNode(_component_n_loading_bar_provider, { to: vue.unref(panelEl) || void 0 }, { default: vue.withCtx(() => [ vue.createElementVNode("div", { class: vue.normalizeClass(["bds-shell", { "bds-shell--static": !vue.unref(isScriptApp) }]), style: vue.normalizeStyle(vue.unref(shellStyle)) }, [ vue.unref(isScriptApp) ? (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$d), { key: 0, label: vue.unref(entryLabel), onToggle: handleTogglePanel }, null, 8, ["label"])) : (vue.openBlock(), vue.createBlock(vue.unref(_sfc_main$b), { key: 1, "source-url": vue.unref(sourceUrl), "onUpdate:sourceUrl": _cache[0] || (_cache[0] = ($event) => vue.isRef(sourceUrl) ? sourceUrl.value = $event : null), "has-data": vue.unref(hasProvidedData), mode: vue.unref(APP_MODE), onOpenPanel: handleOpenPanel, onParsedData: handleParsedUploadData }, null, 8, ["source-url", "has-data", "mode"])), vue.createVNode(vue.unref(_sfc_main$c), { ref_key: "panelShellRef", ref: panelShellRef, show: vue.unref(showPanel), "onUpdate:show": _cache[2] || (_cache[2] = ($event) => vue.isRef(showPanel) ? showPanel.value = $event : null), size: vue.unref(panelSize), error: appError }, { default: vue.withCtx(() => [ !vue.unref(isUserPage) && vue.unref(BDM) ? (vue.openBlock(), vue.createBlock(_sfc_main$1, { key: 0, url: vue.unref(sourceUrl), "onUpdate:url": _cache[1] || (_cache[1] = ($event) => vue.isRef(sourceUrl) ? sourceUrl.value = $event : null), to: vue.unref(panelEl) || void 0, active: vue.unref(showPanel), mode: vue.unref(APP_MODE) }, null, 8, ["url", "to", "active", "mode"])) : vue.createCommentVNode("", true), vue.unref(isUserPage) && vue.unref(BDM) ? (vue.openBlock(), vue.createBlock(_sfc_main$2, { key: 1, mode: vue.unref(APP_MODE), url: vue.unref(sourceUrl) }, null, 8, ["mode", "url"])) : vue.createCommentVNode("", true) ]), _: 1 }, 8, ["show", "size"]) ], 6) ]), _: 1 }, 8, ["to"]) ]), _: 1 }, 8, ["to"]) ]), _: 1 }, 8, ["to"]) ]), _: 1 }, 8, ["to"]) ]), _: 1 }, 8, ["to"]) ]), _: 1 }, 8, ["theme", "theme-overrides", "locale", "date-locale", "style-mount-target"]); }; } }; var _GM_getResourceText = (() => typeof GM_getResourceText != "undefined" ? GM_getResourceText : void 0)(); var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); var _monkeyWindow = (() => window)(); const HOST_ID = "bds-root-host"; const MOUNT_ID = "bds-root"; const BRIDGE_EVENT = "BDS_HTTP_BRIDGE_READY"; const isStaticSite = !location.hostname.endsWith(".bilibili.com"); if (isStaticSite) { if (typeof _GM_xmlhttpRequest === "function") { const bridgeTarget = _unsafeWindow || _monkeyWindow || window; bridgeTarget.__BDS_HTTP_REQUEST__ = (details) => _GM_xmlhttpRequest(details); window.dispatchEvent(new CustomEvent(BRIDGE_EVENT)); } } if (isStaticSite) ; else { let ensureShadowMount = function() { let host = document.getElementById(HOST_ID); if (!host) { host = document.createElement("div"); host.id = HOST_ID; document.documentElement.appendChild(host); } const shadowRoot = host.shadowRoot || host.attachShadow({ mode: "open" }); let mount3 = shadowRoot.getElementById(MOUNT_ID); if (!mount3) { mount3 = document.createElement("div"); mount3.id = MOUNT_ID; shadowRoot.appendChild(mount3); } return { styleMountTarget: shadowRoot, mount: mount3 }; }; const { styleMountTarget: styleMountTarget2, mount: mount2 } = ensureShadowMount(); const data = vue.shallowRef(null); const runtimeWindow2 = _unsafeWindow || _monkeyWindow; const getStaticHtmlText = async () => { if (typeof _GM_getResourceText !== "function") { throw new Error("GM_getResourceText 不可用"); } const html = _GM_getResourceText("staticHtml"); if (!html) throw new Error("静态模板资源 staticHtml 为空"); return String(html); }; const sourceUrl = vue.shallowRef(location.href); const BiliDataManager = _monkeyWindow.BiliDataManager || globalThis.BiliDataManager; if (!BiliDataManager) throw new Error("BiliDataManager 未加载"); if (typeof _GM_xmlhttpRequest !== "function") throw new Error("GM_xmlhttpRequest 不可用"); const BDM = BiliDataManager.create({ httpRequest: _GM_xmlhttpRequest, name: "BDS", isLog: true }); vue.createApp(_sfc_main).provide("styleMountTarget", styleMountTarget2).provide("BDM", BDM).provide("APP_MODE", vue.ref("script")).provide("data", data).provide("sourceUrl", sourceUrl).provide("runtimeWindow", runtimeWindow2).provide("getStaticHtmlText", getStaticHtmlText).mount(mount2); } })(Vue, naive);